Sentence Similarity
sentence-transformers
Safetensors
xlm-roberta
feature-extraction
dense
Generated from Trainer
dataset_size:41454
loss:MultipleNegativesRankingLoss
Eval Results (legacy)
text-embeddings-inference
Instructions to use soof/miswag-category-mapper with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- sentence-transformers
How to use soof/miswag-category-mapper with sentence-transformers:
from sentence_transformers import SentenceTransformer model = SentenceTransformer("soof/miswag-category-mapper") sentences = [ "Bleu Eau De Parfum Men's Perfume عطر بلو للرجال", "Niche Perfumes | عطور النيش | عطور النيش | Beauty > Fragrance > Niche Perfumes | الجمال والعناية > عطور > عطور النيش", "Men Blouses | بلوز رجالي | بلوز رجالي | Clothes, Shoes & Bags > Men Clothes > Men Blouses > Men Blouses | ملابس، أحذية وحقائب > ملابس رجالية > بلوز وتيشرت رجالي > بلوز رجالي", "Men Shoes | حذاء رجالي | حذاء رجالي | Men Fashion > Men Shoes | ملابس رجالية > حذاء رجالي", "Men Niche Perfumes | عطور رجالية | عطور رجالية | Beauty > Fragrance > Niche Perfumes > Men Niche Perfumes | الجمال والعناية > عطور > عطور النيش > عطور رجالية", "Arabian, Oud & Makhmaria | عطور عربية ومخمرية | عطور عربية ومخمرية | Beauty > Fragrance > Arabian, Oud & Makhmaria | الجمال والعناية > عطور > عطور عربية ومخمرية", "Men Fragrance | عطور رجالية | بۆنی پیاوانی | Beauty > Fragrance > Fragrance > Men Fragrance | الجمال والعناية > عطور > العطور > عطور رجالية" ] embeddings = model.encode(sentences) similarities = model.similarity(embeddings, embeddings) print(similarities.shape) # [7, 7] - Notebooks
- Google Colab
- Kaggle
File size: 6,427 Bytes
0e34fc6 4f37be7 0e34fc6 515ebe9 0e34fc6 4f37be7 0e34fc6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | import json
import re
import sys
import unicodedata
from pathlib import Path
from typing import Set
import numpy as np
from sentence_transformers import SentenceTransformer
# ── Inline text_utils (avoids sys.path / import issues on HF endpoints) ──
_ARABIC_DIACRITICS_RE = re.compile(
"[\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4"
"\u06E7-\u06E8\u06EA-\u06ED]+"
)
_ALEF_VARIANTS = {
"\u0622": "\u0627", "\u0623": "\u0627",
"\u0625": "\u0627", "\u0671": "\u0627",
}
_ALEF_RE = re.compile("[" + "".join(_ALEF_VARIANTS.keys()) + "]")
_TURKISH_UPPER_MAP = {"\u0130": "i", "I": "\u0131"}
_ARABIC_PREFIXES = ("وال", "بال", "كال", "فال", "لل", "ال")
ALL_STOPWORDS: Set[str] = {
# Arabic
"في", "من", "على", "الى", "إلى", "عن", "مع", "هذا", "هذه",
"ذلك", "تلك", "هو", "هي", "هم", "هن", "نحن", "انا", "أنا",
"كان", "كانت", "يكون", "ان", "أن", "لا", "ما", "لم", "لن",
"قد", "او", "أو", "و", "ثم", "بعد", "قبل", "حتى", "عند",
"كل", "بعض", "غير", "بين", "الذي", "التي", "الذين", "اللاتي",
"اللذان", "اللتان", "اذا", "إذا", "لو", "كي", "حيث",
# English
"the", "a", "an", "and", "or", "but", "in", "on", "at", "to",
"for", "of", "with", "by", "from", "is", "it", "as", "was",
"are", "were", "be", "been", "being", "have", "has", "had",
"do", "does", "did", "will", "would", "could", "should", "may",
"might", "shall", "can", "not", "no", "this", "that", "these",
"those", "i", "you", "he", "she", "we", "they", "me", "him",
"her", "us", "them", "my", "your", "his", "its", "our", "their",
"so", "if", "then", "than", "too", "very", "just",
# Turkish
"ve", "bir", "bu", "de", "da", "ile", "icin", "için", "ama",
"fakat", "veya", "ya", "ki", "ne", "o", "su", "şu", "ben",
"sen", "biz", "siz", "onlar", "gibi", "daha", "en", "cok",
"çok", "var", "yok", "mi", "mu", "mı", "mü", "ise", "hem",
"hep", "her", "kadar", "sonra", "once", "önce", "bunu", "sunu",
"şunu", "onu",
}
def _normalize_text(text: str) -> str:
text = unicodedata.normalize("NFC", text)
text = _ARABIC_DIACRITICS_RE.sub("", text)
text = _ALEF_RE.sub(lambda m: _ALEF_VARIANTS[m.group()], text)
text = text.replace("\u0629", "\u0647") # taa marbuta -> haa
text = text.replace("\u0649", "\u064A") # alef maksura -> yaa
for src, dst in _TURKISH_UPPER_MAP.items():
text = text.replace(src, dst)
text = text.lower()
text = text.replace("-", " ").replace("_", " ")
text = re.sub(r"\s+", " ", text).strip()
return text
def tokenize(text: str) -> Set[str]:
normalized = _normalize_text(text)
tokens = set(normalized.split()) - ALL_STOPWORDS
extra: Set[str] = set()
for token in tokens:
for prefix in _ARABIC_PREFIXES:
if token.startswith(prefix) and len(token) > len(prefix) + 1:
extra.add(token[len(prefix):])
tokens |= extra
return {t for t in tokens if len(t) > 1}
# ── Handler ──────────────────────────────────────────────────────────────
class EndpointHandler:
def __init__(self, path: str = ""):
model_dir = Path(path)
# Load from model/ subdir to avoid ST auto-detection at repo root
model_path = model_dir / "model"
if model_path.exists():
self.bi_encoder = SentenceTransformer(str(model_path))
else:
self.bi_encoder = SentenceTransformer(str(model_dir))
# Load precomputed category embeddings (numpy, no faiss needed)
self.category_embeddings = np.load(
str(model_dir / "category_embeddings.npy")
)
with open(model_dir / "category_metadata.json", "r", encoding="utf-8") as f:
self.categories = json.load(f)
def __call__(self, data):
inputs = data.get("inputs", "")
params = data.get("parameters", {})
top_k = params.get("top_k", 5)
prefer_level = params.get("prefer_level", None)
if isinstance(inputs, str):
inputs = [inputs]
results = []
for text in inputs:
preds = self._predict(text, top_k=top_k, prefer_level=prefer_level)
results.append({"input": text, "predictions": preds})
return results if len(results) > 1 else results[0]
def _predict(self, product_text, top_k=5, prefer_level=None):
# Bi-encoder retrieval via numpy dot product (embeddings are normalized)
query_emb = self.bi_encoder.encode(
[product_text], normalize_embeddings=True, convert_to_numpy=True
)
scores = (self.category_embeddings @ query_emb.T).flatten()
# Top 20 candidates
top_indices = np.argsort(scores)[::-1][:20]
candidates = []
for idx in top_indices:
cat = self.categories[idx]
candidates.append({**cat, "bi_score": float(scores[idx])})
if not candidates:
return []
# Keyword boosting
query_tokens = tokenize(product_text)
for c in candidates:
cat_text = f"{c['alias']} {c['name_en']} {c['name_ar']} {c['name_ku']}"
cat_tokens = tokenize(cat_text)
path_tokens = tokenize(" ".join(c["path_en"]))
overlap = len(query_tokens & (cat_tokens | path_tokens))
c["keyword_boost"] = overlap * 0.02
c["bi_score_boosted"] = c["bi_score"] + c["keyword_boost"]
candidates.sort(key=lambda x: x["bi_score_boosted"], reverse=True)
if prefer_level is not None:
for c in candidates:
if c["level"] == prefer_level:
c["bi_score_boosted"] *= 1.1
candidates.sort(key=lambda x: x["bi_score_boosted"], reverse=True)
return [
{
"alias": c["alias"],
"level": c["level"],
"name_en": c["name_en"],
"name_ar": c["name_ar"],
"path_en": " > ".join(c["path_en"]),
"path_ar": " > ".join(c["path_ar"]),
"score": round(c["bi_score_boosted"], 4),
}
for c in candidates[:top_k]
]
|