embedpick 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
embedpick/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """embedpick — kendi verinizde embedding modeli kıyaslama aracı."""
2
+
3
+ from .benchmark import run_benchmark
4
+ from .data import load_corpus, load_queries, validate
5
+
6
+ __version__ = "0.1.0"
7
+ __all__ = ["run_benchmark", "load_corpus", "load_queries", "validate"]
embedpick/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
embedpick/benchmark.py ADDED
@@ -0,0 +1,97 @@
1
+ """Benchmark akışını yürütür ve sonuç tablosunu üretir."""
2
+
3
+ import pandas as pd
4
+
5
+ from .metrics import consensus_failures, recall_at_k, score_all
6
+ from .retrievers import BM25Retriever, EmbeddingRetriever
7
+
8
+
9
+ def run_benchmark(
10
+ corpus,
11
+ queries,
12
+ model_names,
13
+ k=5,
14
+ repeats=5,
15
+ include_baseline=True,
16
+ use_presets=True,
17
+ verbose=False,
18
+ ):
19
+ retrievers = []
20
+ if include_baseline:
21
+ retrievers.append(BM25Retriever())
22
+
23
+ for name in model_names:
24
+ if use_presets:
25
+ retrievers.append(EmbeddingRetriever(name))
26
+ else:
27
+ # Önekler kapalı: modelin belgelenmiş kullanım kuralı yok sayılıyor.
28
+ # Yanlış yapılandırmanın maliyetini ölçmek için.
29
+ retrievers.append(EmbeddingRetriever(name, query_prefix="", doc_prefix=""))
30
+
31
+ satirlar = []
32
+ sorgu_bazli = {}
33
+
34
+ for retriever in retrievers:
35
+ print(f"\n-> {retriever.label}")
36
+ maliyet = retriever.index(corpus, repeats)
37
+ bulunanlar = retriever.search(queries["query"].tolist(), k)
38
+
39
+ recall, mrr = score_all(bulunanlar, queries, k, verbose=verbose)
40
+
41
+ sorgu_bazli[retriever.label] = {
42
+ q: recall_at_k(bulunan, dogru, k)
43
+ for q, bulunan, dogru in zip(
44
+ queries["query"], bulunanlar, queries["relevant_ids"]
45
+ )
46
+ }
47
+
48
+ satirlar.append(
49
+ {
50
+ "model": retriever.label,
51
+ f"recall@{k}": round(recall, 3),
52
+ f"mrr@{k}": round(mrr, 3),
53
+ "docs_per_sec": round(maliyet["docs_per_sec"], 1),
54
+ "dim": maliyet["dim"],
55
+ "index_mb": round(maliyet["index_mb"], 2),
56
+ }
57
+ )
58
+
59
+ _add_relative_speed(satirlar)
60
+ return pd.DataFrame(satirlar), sorgu_bazli
61
+
62
+
63
+ def _add_relative_speed(satirlar):
64
+ """Mutlak hız makineye ve o anki işlemci durumuna göre kat kat oynar.
65
+ Aynı çalıştırma içindeki oranlar ise sabit kalır."""
66
+ sinirsel = [r for r in satirlar if r["dim"] > 0]
67
+ if not sinirsel:
68
+ return
69
+
70
+ en_hizli = max(r["docs_per_sec"] for r in sinirsel)
71
+ for r in satirlar:
72
+ r["rel_speed"] = f"{r['docs_per_sec'] / en_hizli:.2f}x" if r["dim"] > 0 else "-"
73
+
74
+
75
+ def report(df, sorgu_bazli, k):
76
+ print()
77
+ print(df.to_string(index=False))
78
+
79
+ print(
80
+ "\nNot: docs_per_sec makineye ve o anki işlemci durumuna göre değişir."
81
+ "\n Makineler arası kıyas için rel_speed sütununu kullanın"
82
+ "\n (en hızlı sinirsel model = 1.00x)."
83
+ )
84
+
85
+ # Tek yöntem varken "hiçbiri bulamadı" demek anlamsız; uyarının değeri
86
+ # birden fazla yöntemin aynı sorguda batmasından geliyor.
87
+ ortak = consensus_failures(sorgu_bazli) if len(sorgu_bazli) > 1 else []
88
+ if ortak:
89
+ print(
90
+ f"\nHiçbir yöntemin ilk {k} sonuçta bulamadığı sorgular:"
91
+ )
92
+ for q in ortak:
93
+ print(f" - {q}")
94
+ print(
95
+ " Bütün yöntemler aynı sorguda batıyorsa sorun genelde modelde değil,"
96
+ "\n etiketlerdedir. Bu sorguların relevant_ids değerlerini gözden geçirin."
97
+ )
embedpick/cli.py ADDED
@@ -0,0 +1,111 @@
1
+ """embedpick komut satırı arayüzü."""
2
+
3
+ import argparse
4
+ import sys
5
+ from importlib import resources
6
+
7
+ from .benchmark import report, run_benchmark
8
+ from .data import load_corpus, load_queries, validate
9
+ from .retrievers import DEFAULT_MODELS
10
+
11
+
12
+ def sample_path(name):
13
+ """Pakete gömülü örnek veri dosyasının yolu.
14
+
15
+ Paket pip ile kurulduğunda çalışma dizininde data/ klasörü olmaz,
16
+ o yüzden varsayılan veri kurulum dizininden okunuyor.
17
+ """
18
+ return str(resources.files("embedpick") / "sample_data" / name)
19
+
20
+
21
+ def build_parser():
22
+ p = argparse.ArgumentParser(
23
+ prog="embedpick",
24
+ description=(
25
+ "Kendi verinizde embedding modellerini kıyaslayın. "
26
+ "Kalite, hız ve bellek maliyetini tek tabloda gösterir."
27
+ ),
28
+ )
29
+ p.add_argument(
30
+ "--corpus",
31
+ help="Aranacak dokümanlar (sütunlar: id,text). "
32
+ "Verilmezse pakete gömülü örnek veri kullanılır.",
33
+ )
34
+ p.add_argument(
35
+ "--queries",
36
+ help="Sorgular ve doğru cevaplar (sütunlar: query,relevant_ids). "
37
+ "Verilmezse pakete gömülü örnek veri kullanılır.",
38
+ )
39
+ p.add_argument(
40
+ "--models",
41
+ nargs="+",
42
+ default=DEFAULT_MODELS,
43
+ help="Hugging Face model adları, boşlukla ayrılmış",
44
+ )
45
+ p.add_argument("-k", type=int, default=5, help="İlk kaç sonuç değerlendirilsin")
46
+ p.add_argument(
47
+ "--repeats", type=int, default=5, help="Hız ölçümü kaç kez tekrarlansın"
48
+ )
49
+ p.add_argument(
50
+ "--no-baseline",
51
+ action="store_true",
52
+ help="BM25 kelime bazlı baseline'ı atla (önerilmez)",
53
+ )
54
+ p.add_argument(
55
+ "--no-presets",
56
+ action="store_true",
57
+ help=(
58
+ "Bilinen model öneklerini uygulama. e5 gibi önek bekleyen modeller "
59
+ "düşük skor alır; yanlış yapılandırmanın maliyetini ölçmek için."
60
+ ),
61
+ )
62
+ p.add_argument(
63
+ "--verbose",
64
+ action="store_true",
65
+ help="Her sorgunun sonucunu tek tek yazdır",
66
+ )
67
+ p.add_argument("--out", help="Sonuç tablosunu CSV olarak kaydet")
68
+ return p
69
+
70
+
71
+ def main(argv=None):
72
+ args = build_parser().parse_args(argv)
73
+
74
+ corpus_path = args.corpus or sample_path("corpus.csv")
75
+ queries_path = args.queries or sample_path("queries.csv")
76
+
77
+ if args.corpus is None and args.queries is None:
78
+ print("Kendi veriniz verilmedi, pakete gömülü örnek veri kullanılıyor.")
79
+
80
+ try:
81
+ corpus = load_corpus(corpus_path)
82
+ queries = load_queries(queries_path)
83
+ validate(corpus, queries)
84
+ except (FileNotFoundError, ValueError) as exc:
85
+ print(f"Hata: {exc}", file=sys.stderr)
86
+ return 1
87
+
88
+ print(f"{len(corpus)} doküman, {len(queries)} sorgu, k={args.k}")
89
+
90
+ df, sorgu_bazli = run_benchmark(
91
+ corpus,
92
+ queries,
93
+ model_names=args.models,
94
+ k=args.k,
95
+ repeats=args.repeats,
96
+ include_baseline=not args.no_baseline,
97
+ use_presets=not args.no_presets,
98
+ verbose=args.verbose,
99
+ )
100
+
101
+ report(df, sorgu_bazli, args.k)
102
+
103
+ if args.out:
104
+ df.to_csv(args.out, index=False, encoding="utf-8")
105
+ print(f"\nTablo kaydedildi: {args.out}")
106
+
107
+ return 0
108
+
109
+
110
+ if __name__ == "__main__":
111
+ raise SystemExit(main())
embedpick/data.py ADDED
@@ -0,0 +1,69 @@
1
+ """Korpus ve sorgu dosyalarını okur, formatlarını doğrular."""
2
+
3
+ import pandas as pd
4
+
5
+
6
+ def load_corpus(path):
7
+ df = pd.read_csv(path, encoding="utf-8")
8
+
9
+ missing = {"id", "text"} - set(df.columns)
10
+ if missing:
11
+ raise ValueError(
12
+ f"{path} dosyasında eksik sütun: {sorted(missing)}. "
13
+ "Beklenen başlık satırı: id,text"
14
+ )
15
+
16
+ if df["id"].duplicated().any():
17
+ tekrar = df.loc[df["id"].duplicated(), "id"].tolist()
18
+ raise ValueError(f"{path} içinde tekrar eden id değerleri var: {tekrar}")
19
+
20
+ df["text"] = df["text"].astype(str).str.strip()
21
+ return df
22
+
23
+
24
+ def load_queries(path):
25
+ df = pd.read_csv(path, encoding="utf-8")
26
+
27
+ missing = {"query", "relevant_ids"} - set(df.columns)
28
+ if missing:
29
+ raise ValueError(
30
+ f"{path} dosyasında eksik sütun: {sorted(missing)}. "
31
+ "Beklenen başlık satırı: query,relevant_ids"
32
+ )
33
+
34
+ df["query"] = df["query"].astype(str).str.strip()
35
+ df["relevant_ids"] = df["relevant_ids"].apply(_parse_ids)
36
+ return df
37
+
38
+
39
+ def _parse_ids(value):
40
+ parts = [p.strip() for p in str(value).split(";") if p.strip()]
41
+ if not parts:
42
+ raise ValueError(f"Boş relevant_ids değeri bulundu: {value!r}")
43
+ try:
44
+ return [int(p) for p in parts]
45
+ except ValueError as exc:
46
+ raise ValueError(
47
+ f"relevant_ids sayı olmalı, alınan: {value!r}. "
48
+ "Birden fazla id noktalı virgülle ayrılır, örnek: 3;11"
49
+ ) from exc
50
+
51
+
52
+ def validate(corpus, queries):
53
+ """Sorgu etiketlerinin korpusta gerçekten var olduğunu kontrol eder.
54
+
55
+ Etiket hataları benchmark sonuçlarını sessizce bozar: olmayan bir id'yi
56
+ hiçbir model bulamaz, sen de modeli suçlarsın. Baştan yakalamak daha iyi.
57
+ """
58
+ bilinen = set(corpus["id"])
59
+ sorunlar = []
60
+
61
+ for q, ids in zip(queries["query"], queries["relevant_ids"]):
62
+ eksik = [i for i in ids if i not in bilinen]
63
+ if eksik:
64
+ sorunlar.append(f' "{q}" -> korpusta olmayan id: {eksik}')
65
+
66
+ if sorunlar:
67
+ raise ValueError(
68
+ "Sorgu etiketleri korpusla uyuşmuyor:\n" + "\n".join(sorunlar)
69
+ )
embedpick/metrics.py ADDED
@@ -0,0 +1,54 @@
1
+ """Erişim metrikleri: recall@k ve MRR@k."""
2
+
3
+ import numpy as np
4
+
5
+
6
+ def recall_at_k(retrieved_ids, relevant_ids, k):
7
+ """İlk k sonuçta doğru dokümanların ne kadarı yakalandı."""
8
+ hits = len(set(retrieved_ids[:k]) & set(relevant_ids))
9
+ return hits / len(relevant_ids)
10
+
11
+
12
+ def reciprocal_rank(retrieved_ids, relevant_ids, k):
13
+ """İlk doğru sonucun sırasının tersi. 1. sıra -> 1.0, 2. sıra -> 0.5."""
14
+ for rank, doc_id in enumerate(retrieved_ids[:k], start=1):
15
+ if doc_id in relevant_ids:
16
+ return 1.0 / rank
17
+ return 0.0
18
+
19
+
20
+ def score_all(all_retrieved, queries, k, verbose=False):
21
+ """Tüm sorguları puanlar, ortalama recall ve MRR döndürür."""
22
+ recalls = []
23
+ rrs = []
24
+
25
+ for q, retrieved_ids, relevant in zip(
26
+ queries["query"], all_retrieved, queries["relevant_ids"]
27
+ ):
28
+ r = recall_at_k(retrieved_ids, relevant, k)
29
+ rr = reciprocal_rank(retrieved_ids, relevant, k)
30
+ recalls.append(r)
31
+ rrs.append(rr)
32
+ if verbose:
33
+ print(
34
+ f" [r={r:.2f} rr={rr:.2f}] {q}"
35
+ f" -> {retrieved_ids} | doğru: {relevant}"
36
+ )
37
+
38
+ return float(np.mean(recalls)), float(np.mean(rrs))
39
+
40
+
41
+ def consensus_failures(per_model_scores, threshold=0.0):
42
+ """Hiçbir modelin bulamadığı sorguları döndürür.
43
+
44
+ Bütün modeller aynı sorguda batıyorsa suçlu genelde model değil, etikettir.
45
+ """
46
+ if not per_model_scores:
47
+ return []
48
+
49
+ sorgular = list(per_model_scores.values())[0].keys()
50
+ return [
51
+ q
52
+ for q in sorgular
53
+ if all(skorlar[q] <= threshold for skorlar in per_model_scores.values())
54
+ ]
@@ -0,0 +1,150 @@
1
+ """Arama yöntemleri: kelime bazlı BM25 baseline ve embedding tabanlı arama."""
2
+
3
+ import re
4
+ import time
5
+
6
+ import numpy as np
7
+
8
+ # Bazı modeller metinlerin başına önek bekler. Önek konmazsa model kötü
9
+ # çalışır ve kullanıcı bunu "model kötüymüş" diye yorumlar. Bilinenleri
10
+ # burada tutuyoruz ki varsayılan davranış doğru olsun.
11
+ MODEL_PRESETS = {
12
+ "intfloat/multilingual-e5-small": {"query": "query: ", "doc": "passage: "},
13
+ "intfloat/multilingual-e5-base": {"query": "query: ", "doc": "passage: "},
14
+ "intfloat/multilingual-e5-large": {"query": "query: ", "doc": "passage: "},
15
+ "BAAI/bge-m3": {"query": "", "doc": ""},
16
+ }
17
+
18
+ DEFAULT_MODELS = [
19
+ "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
20
+ "intfloat/multilingual-e5-small",
21
+ "trmteb/turkish-embedding-model",
22
+ ]
23
+
24
+
25
+ def get_prefixes(model_name):
26
+ if model_name in MODEL_PRESETS:
27
+ return MODEL_PRESETS[model_name]
28
+
29
+ # Tanımadığımız bir e5 varyantı gelirse yine de uyaralım.
30
+ if "e5" in model_name.lower():
31
+ print(
32
+ f" uyarı: {model_name} bir e5 varyantı gibi görünüyor. "
33
+ "e5 modelleri 'query: ' / 'passage: ' öneki bekler, "
34
+ "önek olmadan skorlar düşük çıkar."
35
+ )
36
+ return {"query": "", "doc": ""}
37
+
38
+
39
+ def tr_tokenize(text):
40
+ """Türkçe farkındalıklı basit kelime ayırıcı.
41
+
42
+ Python'un lower() metodu Türkçe bilmez: "I" -> "i" yapar, oysa "ı" olmalı.
43
+ "İ" ise "i" artı ayrı bir birleşen nokta karakterine bölünür.
44
+ """
45
+ text = text.replace("I", "ı").replace("İ", "i")
46
+ return re.findall(r"\w+", text.lower(), flags=re.UNICODE)
47
+
48
+
49
+ class BM25Retriever:
50
+ """Kelime bazlı baseline. Embedding modelleri bunu geçemiyorsa
51
+ harcanan hesap gücü boşa gidiyor demektir."""
52
+
53
+ label = "BM25 (baseline)"
54
+ dim = 0
55
+
56
+ def __init__(self):
57
+ self._bm25 = None
58
+ self._ids = None
59
+
60
+ def index(self, corpus, repeats):
61
+ from rank_bm25 import BM25Okapi
62
+
63
+ texts = corpus["text"].tolist()
64
+ timings = []
65
+ for _ in range(repeats):
66
+ start = time.perf_counter()
67
+ tokenized = [tr_tokenize(t) for t in texts]
68
+ bm25 = BM25Okapi(tokenized)
69
+ timings.append(time.perf_counter() - start)
70
+
71
+ self._bm25 = bm25
72
+ self._ids = corpus["id"].tolist()
73
+
74
+ index_sec = float(np.median(timings))
75
+ return {
76
+ "docs_per_sec": len(texts) / index_sec,
77
+ "index_mb": 0.0,
78
+ "dim": 0,
79
+ }
80
+
81
+ def search(self, queries, k):
82
+ sonuclar = []
83
+ for q in queries:
84
+ scores = self._bm25.get_scores(tr_tokenize(q))
85
+ top = np.argsort(-scores)[:k]
86
+ sonuclar.append([self._ids[i] for i in top])
87
+ return sonuclar
88
+
89
+
90
+ class EmbeddingRetriever:
91
+ """sentence-transformers modeli + FAISS iç çarpım indeksi.
92
+
93
+ Vektörler normalize edildiği için iç çarpım = kosinüs benzerliği.
94
+ """
95
+
96
+ def __init__(self, model_name, query_prefix=None, doc_prefix=None):
97
+ self.model_name = model_name
98
+ self.label = model_name.split("/")[-1]
99
+
100
+ onekler = get_prefixes(model_name)
101
+ self.query_prefix = onekler["query"] if query_prefix is None else query_prefix
102
+ self.doc_prefix = onekler["doc"] if doc_prefix is None else doc_prefix
103
+
104
+ self._model = None
105
+ self._index = None
106
+ self._ids = None
107
+ self.dim = 0
108
+
109
+ def index(self, corpus, repeats):
110
+ from sentence_transformers import SentenceTransformer
111
+
112
+ self._model = SentenceTransformer(self.model_name)
113
+ texts = [self.doc_prefix + t for t in corpus["text"].tolist()]
114
+
115
+ # Isınma: tüm korpusu bir kez geçiyoruz. Kısa bir ısınma turu
116
+ # işlemciyi düşük frekanstan çıkarmaya yetmiyor ve ilk ölçümler
117
+ # yanıltıcı derecede yavaş çıkıyor.
118
+ self._encode(texts)
119
+
120
+ # Medyan alıyoruz; ortalama tek bir tepe değerden bozulur.
121
+ timings = []
122
+ for _ in range(repeats):
123
+ start = time.perf_counter()
124
+ emb = self._encode(texts)
125
+ timings.append(time.perf_counter() - start)
126
+
127
+ import faiss
128
+
129
+ self.dim = int(emb.shape[1])
130
+ self._index = faiss.IndexFlatIP(self.dim)
131
+ self._index.add(np.asarray(emb, dtype="float32"))
132
+ self._ids = corpus["id"].tolist()
133
+
134
+ encode_sec = float(np.median(timings))
135
+ return {
136
+ "docs_per_sec": len(texts) / encode_sec,
137
+ "index_mb": emb.nbytes / (1024 * 1024),
138
+ "dim": self.dim,
139
+ }
140
+
141
+ def search(self, queries, k):
142
+ texts = [self.query_prefix + q for q in queries]
143
+ emb = self._encode(texts)
144
+ _, positions = self._index.search(np.asarray(emb, dtype="float32"), k)
145
+ return [[self._ids[p] for p in row] for row in positions]
146
+
147
+ def _encode(self, texts):
148
+ return self._model.encode(
149
+ texts, normalize_embeddings=True, show_progress_bar=False
150
+ )
@@ -0,0 +1,73 @@
1
+ id,text
2
+ 1,Kargom hala elime ulaşmadı
3
+ 2,Sipariş takip numaram sistemde görünmüyor
4
+ 3,Paketim yanlış adrese teslim edilmiş
5
+ 4,Kurye kapıyı çalmadan teslim edilemedi olarak işaretlemiş
6
+ 5,Teslimat tarihi sürekli ileriye atılıyor
7
+ 6,Kargo firmasını değiştirebilir miyim
8
+ 7,Siparişim iki ayrı paket halinde mi gelecek
9
+ 8,Şubeden teslim alma seçeneği var mı
10
+ 9,Gönderim ücreti neden bu kadar yüksek
11
+ 10,Yurt dışına gönderim yapıyor musunuz
12
+ 11,İade sürecini nasıl başlatabilirim
13
+ 12,Ürünü kaç gün içinde geri gönderebilirim
14
+ 13,Beden değişimi yapmak mümkün mü
15
+ 14,İade kargo ücretini kim karşılıyor
16
+ 15,Geri gönderdiğim ürün elinize ulaştı mı
17
+ 16,Paramı ne zaman geri alacağım
18
+ 17,Kullanılmış ürün iade edilebilir mi
19
+ 18,Hediye olarak aldığım ürünü değiştirebilir miyim
20
+ 19,İade formunu nereden indirebilirim
21
+ 20,Farklı renk ile değişim yapmak istiyorum
22
+ 21,Kredi kartımdan iki kez çekim yapılmış
23
+ 22,Fatura adresimi değiştirmek istiyorum
24
+ 23,Ödemem onaylandı ama sipariş görünmüyor
25
+ 24,Taksit seçenekleri neden çıkmıyor
26
+ 25,Kapıda ödeme yapabilir miyim
27
+ 26,Kurumsal fatura kesilmesini istiyorum
28
+ 27,Havale ile ödeme yaptım ama onaylanmadı
29
+ 28,Kartımdan çekilen tutar sipariş tutarından fazla
30
+ 29,E-arşiv faturama nereden ulaşırım
31
+ 30,Ödeme sayfasında hata alıyorum
32
+ 31,Şifremi unuttum giriş yapamıyorum
33
+ 32,Hesabım askıya alınmış neden
34
+ 33,Üyeliğimi tamamen silmek istiyorum
35
+ 34,E-posta adresimi güncellemek istiyorum
36
+ 35,Doğrulama kodu telefonuma gelmiyor
37
+ 36,Aynı e-posta ile ikinci hesap açabilir miyim
38
+ 37,Hesabıma başkası girmiş olabilir
39
+ 38,Telefon numaramı değiştiremiyorum
40
+ 39,Bildirim tercihlerimi nereden ayarlarım
41
+ 40,Üyelik avantajları nelerdir
42
+ 41,Ürün hasarlı geldi kutusu ezilmişti
43
+ 42,Ürün açıklamadaki renkten farklı geldi
44
+ 43,Gelen üründe eksik parça var
45
+ 44,Ürünün orijinal olmadığını düşünüyorum
46
+ 45,Ambalaj açılmış görünüyor
47
+ 46,Ürün ilk kullanımda bozuldu
48
+ 47,Beden tablosu gerçeği yansıtmıyor
49
+ 48,Ürün fotoğraftakinden çok daha küçük
50
+ 49,Kumaş kalitesi beklediğim gibi değil
51
+ 50,Son kullanma tarihi geçmiş ürün gönderilmiş
52
+ 51,Kampanya kodu sepette geçerli olmadı
53
+ 52,İndirimli aldığım ürün sipariş sonrası zamlandı
54
+ 53,Hediye çekimi nasıl kullanırım
55
+ 54,Bedava gönderim kampanyası devam ediyor mu
56
+ 55,Sepetimdeki ürünün fiyatı değişti
57
+ 56,Bu ürün iki al bir öde kampanyasına dahil mi
58
+ 57,Puanlarım hesabıma yansımadı
59
+ 58,Öğrenci indirimi var mı
60
+ 59,Garanti kapsamında tamir talebi oluşturmak istiyorum
61
+ 60,Garanti belgesi paketten çıkmadı
62
+ 61,En yakın yetkili servis nerede
63
+ 62,Garanti süresi ne kadar
64
+ 63,Tamire gönderdiğim ürün ne zaman döner
65
+ 64,Garanti kapsamı dışında olduğu söylenip ücret istendi
66
+ 65,Ürünün yedek parçasını nereden alabilirim
67
+ 66,Kullanım kılavuzuna nereden ulaşabilirim
68
+ 67,Siparişimi iptal etmek istiyorum
69
+ 68,Tükenen ürün tekrar gelecek mi
70
+ 69,Teslimat adresimi güncelleyebilir miyim
71
+ 70,Siparişime ürün ekleyebilir miyim
72
+ 71,Ön sipariş verdiğim ürün ne zaman gönderilir
73
+ 72,Müşteri hizmetlerine nasıl ulaşabilirim
@@ -0,0 +1,27 @@
1
+ query,relevant_ids
2
+ kargo gecikmesi,1;5
3
+ paketim nerede,1;2
4
+ yanlış adrese teslimat,3
5
+ paramı geri istiyorum,16;11
6
+ ürünü geri göndermek istiyorum,11;12
7
+ kıyafet küçük geldi,47;48
8
+ çift çekim yapıldı,21;28
9
+ fatura bilgilerimi güncelleme,22;29
10
+ hesabıma giremiyorum,31;35
11
+ güvenlik ihlali şüphesi,37
12
+ bozuk ürün geldi,46;41
13
+ kutu ezilmiş halde geldi,41;45
14
+ renk farklı,42;20
15
+ indirim kodu çalışmıyor,51;53
16
+ ücretsiz kargo,54;9
17
+ tamir ve servis,59;61;63
18
+ siparişi durdurmak istiyorum,67
19
+ adres değiştirme,69;22
20
+ ürün ne zaman stoklara girecek,68
21
+ size nasıl ulaşabilirim,72
22
+ taksitli alışveriş,24
23
+ ödeme yaparken sorun,30;27
24
+ üyelikten çıkmak istiyorum,33
25
+ sahte ürün şüphesi,44
26
+ yurt dışı gönderim,10
27
+ garanti süresi,62;60
@@ -0,0 +1,239 @@
1
+ Metadata-Version: 2.4
2
+ Name: embedpick
3
+ Version: 0.1.0
4
+ Summary: Benchmark embedding models on your own data, with a keyword-search baseline
5
+ Author: Mehmet Alper Tuğtekin
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Mehmetalpertugtekin/embedpick
8
+ Project-URL: Repository, https://github.com/Mehmetalpertugtekin/embedpick
9
+ Project-URL: Issues, https://github.com/Mehmetalpertugtekin/embedpick/issues
10
+ Keywords: embeddings,benchmark,semantic-search,information-retrieval,sentence-transformers,turkish-nlp
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Classifier: Topic :: Text Processing :: Indexing
22
+ Classifier: Natural Language :: Turkish
23
+ Classifier: Natural Language :: English
24
+ Requires-Python: >=3.9
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: pandas>=1.3
28
+ Requires-Dist: numpy>=1.21
29
+ Requires-Dist: rank-bm25>=0.2
30
+ Requires-Dist: sentence-transformers>=2.2
31
+ Requires-Dist: faiss-cpu>=1.7
32
+ Provides-Extra: dev
33
+ Requires-Dist: pytest>=7.0; extra == "dev"
34
+ Provides-Extra: ui
35
+ Requires-Dist: gradio>=4.0; extra == "ui"
36
+ Dynamic: license-file
37
+
38
+ # embedpick
39
+ ![tests](https://github.com/Mehmetalpertugtekin/embedpick/actions/workflows/tests.yml/badge.svg)
40
+
41
+ Benchmark embedding models on **your own data**, not on someone else's leaderboard.
42
+
43
+ [Türkçe README](README.tr.md)
44
+
45
+ ## Why
46
+
47
+ Picking an embedding model usually goes like this: open Hugging Face, sort by
48
+ downloads, take the top result. The scores on the model card were measured on
49
+ general-purpose English benchmarks. Your data is not that.
50
+
51
+ embedpick answers a narrower and more useful question: *given my documents and
52
+ my queries, which model actually finds the right thing, and what does it cost
53
+ me in time and memory?*
54
+
55
+ It reports quality, speed and memory side by side, and — unlike most benchmark
56
+ tooling — it puts a plain keyword-search baseline in the same table.
57
+
58
+ ## The baseline is the point
59
+
60
+ Every run includes BM25, a classic keyword-matching algorithm with no neural
61
+ network involved. If an embedding model cannot beat it, running that model is
62
+ wasted compute.
63
+
64
+ Results on the included Turkish customer-support dataset (72 documents,
65
+ 26 queries, k=5):
66
+
67
+ | Model | recall@5 | MRR@5 | docs/sec | dim | rel. speed |
68
+ |---|---|---|---|---|---|
69
+ | BM25 (baseline) | 0.564 | 0.564 | ~226,000 | — | — |
70
+ | paraphrase-multilingual-MiniLM-L12-v2 | 0.615 | 0.596 | 340 | 384 | 1.00x |
71
+ | multilingual-e5-small | 0.692 | 0.708 | 304 | 384 | 0.89x |
72
+ | trmteb/turkish-embedding-model | **0.846** | **0.865** | 103 | 768 | 0.30x |
73
+
74
+ Read that first two rows carefully. MiniLM buys a 5-point recall gain over
75
+ keyword matching, and pays roughly 600x the indexing time for it. On this
76
+ dataset that trade is hard to justify.
77
+
78
+ The Turkish-specific model is a different story: +28 points over the baseline
79
+ is a real jump, and worth the 3x slowdown against the other neural models.
80
+
81
+ So the lesson is not "embeddings are overrated." It is: **a badly chosen
82
+ embedding model is not better than keyword search, and nobody checks.**
83
+
84
+ ## Findings from the sample dataset
85
+
86
+ **Documented configuration is not always the right configuration.** The e5
87
+ model card specifies `query: ` and `passage: ` prefixes on input text. Applying
88
+ them on this dataset *lowered* recall@5 from 0.750 to 0.692 and MRR from 0.792
89
+ to 0.708 — same model, same data, same hardware.
90
+
91
+ A plausible reason: these documents are short, five to eight words each, so an
92
+ English prefix takes up a large share of every one of them. Whatever the cause,
93
+ the recommended setting cost about six points here, and nothing short of
94
+ measuring on your own data would have shown it.
95
+
96
+ embedpick applies known prefixes by default, following the model authors'
97
+ instructions, and `--no-presets` turns them off so you can check. Across 26
98
+ queries the gap is worth roughly one and a half queries, so read the direction
99
+ as suggestive rather than settled.
100
+
101
+ **BM25 and embeddings fail on different queries.** On `ödeme yaparken sorun`
102
+ ("problem while paying") BM25 scored 1.00 and MiniLM scored 0.00. On
103
+ `sahte ürün şüphesi` ("suspected counterfeit") it was the reverse. They are
104
+ complementary, which is the empirical case for hybrid retrieval.
105
+
106
+ **Small corpora hide everything.** On a 16-document pilot all three models
107
+ scored 1.0 and looked identical. At 72 documents a 23-point spread appeared.
108
+ A benchmark that cannot separate models tells you nothing about them.
109
+
110
+ **Absolute timings are unreliable; ratios are not.** Across repeated runs on
111
+ the same machine, `docs/sec` varied by up to 4x while the ratio between models
112
+ stayed within about 20%. Report `rel_speed` when comparing across machines.
113
+
114
+ ## Install
115
+
116
+ ```bash
117
+ git clone https://github.com/Mehmetalpertugtekin/embedpick.git
118
+ cd embedpick
119
+ python -m venv .venv
120
+ .venv\Scripts\activate # Windows
121
+ source .venv/bin/activate # macOS / Linux
122
+ pip install -r requirements.txt
123
+ ```
124
+
125
+ Python 3.9+.
126
+
127
+ ## Usage
128
+
129
+ ```bash
130
+ # run with the bundled sample dataset
131
+ python -m embedpick
132
+
133
+ # your own data
134
+ python -m embedpick --corpus my_docs.csv --queries my_queries.csv
135
+
136
+ # pick models, evaluate top 10, show per-query detail
137
+ python -m embedpick --models intfloat/multilingual-e5-base -k 10 --verbose
138
+
139
+ # save the table
140
+ python -m embedpick --out results.csv
141
+ ```
142
+
143
+ `python -m embedpick --help` lists every flag.
144
+
145
+ ## Development
146
+
147
+ ```bash
148
+ pip install -r requirements-dev.txt
149
+ python -m pytest
150
+ ```
151
+
152
+ Tests do not download any models, so they run in seconds. Make sure your
153
+ virtual environment is active first — a missing `rank_bm25` in the test output
154
+ usually means you installed into the system Python by mistake.
155
+
156
+ ## Data format
157
+
158
+ **corpus.csv** — the documents to search over:
159
+
160
+ ```csv
161
+ id,text
162
+ 1,Kargom hala elime ulaşmadı
163
+ 2,Sipariş takip numaram sistemde görünmüyor
164
+ ```
165
+
166
+ **queries.csv** — queries and their correct answers:
167
+
168
+ ```csv
169
+ query,relevant_ids
170
+ kargo gecikmesi,1;5
171
+ paketim nerede,1;2
172
+ ```
173
+
174
+ Separate multiple correct answers with `;`. A query can have any number of
175
+ them. Files must be UTF-8.
176
+
177
+ Twenty to thirty queries is usually enough to see real differences. Include
178
+ queries that share no words with their correct answer — those are the ones
179
+ that test semantic matching rather than lucky keyword overlap.
180
+
181
+ ## Metrics
182
+
183
+ **recall@k** — of the correct documents, what fraction appeared in the top k.
184
+ Use this when you show the user a list of results.
185
+
186
+ **MRR@k** — 1.0 if the first correct answer is at rank 1, 0.5 at rank 2, 0.33
187
+ at rank 3, averaged over queries. Use this when you show one answer, or feed
188
+ the top hit to an LLM.
189
+
190
+ These can disagree, and the disagreement is informative. In an earlier run
191
+ e5 had higher recall than MiniLM but lower MRR: it found more, but ranked
192
+ worse. Which model is "better" depends on what you are building.
193
+
194
+ **docs/sec** — indexing throughput. Machine-dependent, see the caveat above.
195
+
196
+ **dim** and **index_mb** — vector width and index size. A 768-dimensional
197
+ model needs twice the memory of a 384-dimensional one at the same corpus size.
198
+
199
+ ## Label checking
200
+
201
+ Two things happen automatically:
202
+
203
+ - If `relevant_ids` points at a document id that is not in the corpus, the run
204
+ stops and names the query. Otherwise that query silently scores zero forever
205
+ and you blame the model.
206
+ - If *no* method — including BM25 — finds a query's answer, embedpick flags it.
207
+ When everything fails on the same query, the label is usually wrong. This
208
+ caught a mislabelled query during development.
209
+
210
+ ## Sample dataset
211
+
212
+ `embedpick/sample_data/` contains 72 synthetic Turkish customer-support messages across nine
213
+ themes (shipping, returns, payment, account, product quality, promotions,
214
+ warranty, order management, support) and 26 labelled queries. It is written,
215
+ not scraped, so it carries no licensing or privacy constraints.
216
+
217
+ Several queries deliberately share no vocabulary with their answers — for
218
+ example `güvenlik ihlali şüphesi` ("suspected security breach") maps to
219
+ *"Hesabıma başkası girmiş olabilir"* ("someone else may have accessed my
220
+ account"). Keyword search cannot solve these; that is the point.
221
+
222
+ ## Caveats
223
+
224
+ - Timing on a 72-document corpus is close to measurement noise, particularly
225
+ for BM25. Treat the throughput column as indicative.
226
+ - BM25 and neural encoders scale differently. The ratio at 72 documents is not
227
+ the ratio at 100,000.
228
+ - Results are from one machine, CPU only, no GPU.
229
+
230
+ ## Roadmap
231
+
232
+ - Hybrid retrieval (BM25 + embedding score fusion) as a fourth row
233
+ - Hub detection: flag documents that surface for nearly every query
234
+ - Markdown report export
235
+ - Optional Gradio interface
236
+
237
+ ## License
238
+
239
+ MIT
@@ -0,0 +1,15 @@
1
+ embedpick/__init__.py,sha256=Ybzv5pEthoIrzGby5H69tBPsRknzd1BEzeGiFoBaOWE,258
2
+ embedpick/__main__.py,sha256=k1ocEWawweo1qCJWNFAAvyxz3tcY13dzvCenHszij30,48
3
+ embedpick/benchmark.py,sha256=uqh4T39jcjis86TeMkXCMkumjXx9GFRS2P3B_WGTAw4,3157
4
+ embedpick/cli.py,sha256=RihVIlj2WWrwbqfdiQ_Bqw2ZTKHhWnnxVv6YBXt8RZo,3312
5
+ embedpick/data.py,sha256=1pNyTzmngcADcRf8BGSvNW9wx0GHdQpiRx5MJMcPXCg,2183
6
+ embedpick/metrics.py,sha256=T0S-8fs-lrl3Bef6txDpLQxyT6J2tUS8Rglz7rjfw1I,1676
7
+ embedpick/retrievers.py,sha256=Xe30WabS75qPcCb16Re86D3d3T7o6fxLGyVf8gS5IzY,5004
8
+ embedpick/sample_data/corpus.csv,sha256=Uqzp1qWWFvKPH4xLXFBCqIgrKe0fKKk08MKxy1AqSSI,3236
9
+ embedpick/sample_data/queries.csv,sha256=XdyfiATgpvsMwmGuX2FijQhhqKnkfi_AloqcSyHNJAg,775
10
+ embedpick-0.1.0.dist-info/licenses/LICENSE,sha256=TmjrxRWdTFitR2wsHBSyM7Jp4XH-PGzc7JMTmZj9-aM,1100
11
+ embedpick-0.1.0.dist-info/METADATA,sha256=shJNskBoGeHmIxgcd2KSfVCDfIhQSxJ9e3yyujS-tMg,9261
12
+ embedpick-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
13
+ embedpick-0.1.0.dist-info/entry_points.txt,sha256=OuhrM3J7vIydF3ZEHW-MmgCna4RTBQoVsiw0Z5WnZWw,49
14
+ embedpick-0.1.0.dist-info/top_level.txt,sha256=QErZwZoHszm4YRgjfBVg0b-v8hKDQ55A_kIwjYXlhbs,10
15
+ embedpick-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ embedpick = embedpick.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mehmet Alper Tuğtekin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ embedpick