From 6b781b61d27fdf0734661109e0e69e0efe7d7fae Mon Sep 17 00:00:00 2001 From: saidsurucu Date: Sun, 3 May 2026 01:55:42 +0300 Subject: [PATCH] docs(semantic_search): recommend multilingual-e5-large for Turkish (#22) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Different embedding model families need different prompt prefixes — Gemini wants "task: ... | query: ..." and "title: ... | text: ...", e5 wants "query: ..." / "passage: ...", and using the wrong one silently degrades retrieval quality. Add EMBEDDING_PROMPT_STYLE (gemini/e5/raw) so the prefix matches the chosen model. Defaults: gemini for OpenRouter (matches the existing default google/gemini-embedding-001), e5 for the local provider (matches the recommended multilingual-e5-large setup). Both override via env var or constructor. Update README and .env.example to recommend intfloat/multilingual- e5-large served by HuggingFace Text Embeddings Inference (one docker run) as the Turkish-optimized local setup, with a clear env var reference table. Ollama and OpenRouter remain documented as alternatives. Co-Authored-By: Claude Opus 4.7 (1M context) --- .env.example | 29 +++++++++--- README.md | 93 +++++++++++++++++++++++++++---------- semantic_search/embedder.py | 74 +++++++++++++++++++++++++---- 3 files changed, 155 insertions(+), 41 deletions(-) diff --git a/.env.example b/.env.example index 851a3f6..97f4bf7 100644 --- a/.env.example +++ b/.env.example @@ -89,15 +89,30 @@ OPENROUTER_API_KEY=sk-or-v1-your_openrouter_api_key_here # OPENROUTER_EMBEDDING_MODEL=google/gemini-embedding-001 # OPENROUTER_EMBEDDING_DIMENSION=3072 -# --- Option B: Local OpenAI-compatible server (Ollama / llama.cpp / vLLM) ----- -# Uncomment to use your own server instead of OpenRouter (no API key required). -# Defaults target Ollama with nomic-embed-text. For Turkish, bge-m3 (1024 dims) -# tends to work better — pull it with: `ollama pull bge-m3` +# --- Option B: Local OpenAI-compatible server (no API key required) ---------- +# Recommended for Turkish: intfloat/multilingual-e5-large served by HuggingFace +# Text Embeddings Inference (TEI). One-line setup: +# +# docker run -p 8080:80 ghcr.io/huggingface/text-embeddings-inference:latest \ +# --model-id intfloat/multilingual-e5-large +# +# Then uncomment the block below. Other model families work too — set +# EMBEDDING_PROMPT_STYLE to match: e5 / gemini / raw. +# # EMBEDDING_PROVIDER=local -# LOCAL_EMBEDDING_BASE_URL=http://localhost:11434/v1 -# LOCAL_EMBEDDING_MODEL=nomic-embed-text -# LOCAL_EMBEDDING_DIMENSION=768 +# LOCAL_EMBEDDING_BASE_URL=http://localhost:8080/v1 +# LOCAL_EMBEDDING_MODEL=intfloat/multilingual-e5-large +# LOCAL_EMBEDDING_DIMENSION=1024 +# EMBEDDING_PROMPT_STYLE=e5 # LOCAL_EMBEDDING_API_KEY= # most local servers ignore this +# +# Ollama fallback (if you prefer Ollama and don't need top Turkish quality): +# ollama serve && ollama pull nomic-embed-text +# EMBEDDING_PROVIDER=local +# LOCAL_EMBEDDING_BASE_URL=http://localhost:11434/v1 +# LOCAL_EMBEDDING_MODEL=nomic-embed-text +# LOCAL_EMBEDDING_DIMENSION=768 +# EMBEDDING_PROMPT_STYLE=raw # nomic uses its own search_query/search_document # ============================================================================= # USAGE INSTRUCTIONS diff --git a/README.md b/README.md index f8a20e8..5ab3a34 100644 --- a/README.md +++ b/README.md @@ -181,20 +181,40 @@ Yargı MCP'yi Gemini CLI ile kullanmak için: ---
-🧠 Semantik Arama (Opsiyonel - OpenRouter API) +🧠 Semantik Arama (Opsiyonel) -Yargı MCP, **semantik arama** özelliği ile kararları anlamsal olarak sıralayabilir. Bu özellik opsiyoneldir ve `OPENROUTER_API_KEY` ayarlandığında otomatik olarak etkinleşir. +Yargı MCP, **semantik arama** özelliği ile kararları anlamsal olarak sıralayabilir. Opsiyoneldir; iki yoldan biri yapılandırıldığında otomatik etkinleşir: + +- **Yerel** (önerilen, ücretsiz): kendi makinenizdeki OpenAI-uyumlu embedding sunucusu (HuggingFace TEI, llama.cpp, Ollama, vLLM, LM Studio…) +- **Hosted**: OpenRouter API anahtarı ### Semantik Arama Nasıl Çalışır? 1. `initial_keyword` ile Bedesten API'den 100 karar çekilir 2. `query` ile bu kararlar embedding modeli kullanılarak anlamsal olarak sıralanır 3. En alakalı kararlar döndürülür -### OpenRouter API Anahtarı Alma -1. [OpenRouter](https://openrouter.ai/) sitesine gidin -2. Hesap oluşturun ve API anahtarı alın (ücretsiz kredi ile başlayabilirsiniz) +### Önerilen Türkçe Kurulumu (Yerel — `multilingual-e5-large`) -### Claude Desktop için Yapılandırma +`intfloat/multilingual-e5-large` Türkçe için kıyas ettiğimiz açık kaynak modeller arasında en iyilerinden. HuggingFace'in **Text Embeddings Inference (TEI)** sunucusuyla tek komutta ayağa kalkar ve OpenAI-uyumlu API sunar: + +```bash +docker run -p 8080:80 ghcr.io/huggingface/text-embeddings-inference:latest \ + --model-id intfloat/multilingual-e5-large +``` + +Sonra Yargı MCP'ye şu env vars'ları geçirin: + +```bash +EMBEDDING_PROVIDER=local +LOCAL_EMBEDDING_BASE_URL=http://localhost:8080/v1 +LOCAL_EMBEDDING_MODEL=intfloat/multilingual-e5-large +LOCAL_EMBEDDING_DIMENSION=1024 +EMBEDDING_PROMPT_STYLE=e5 +``` + +> ⚠️ **Önemli:** `EMBEDDING_PROMPT_STYLE=e5` şart — e5 modelleri `query:` / `passage:` öneki bekleyecek şekilde eğitilmiştir; yanlış önek sessizce kaliteyi düşürür. + +#### Claude Desktop örneği (yerel TEI) ```json { "mcpServers": { @@ -202,35 +222,60 @@ Yargı MCP, **semantik arama** özelliği ile kararları anlamsal olarak sırala "command": "uvx", "args": ["yargi-mcp"], "env": { - "OPENROUTER_API_KEY": "sk-or-v1-xxx..." + "EMBEDDING_PROVIDER": "local", + "LOCAL_EMBEDDING_BASE_URL": "http://localhost:8080/v1", + "LOCAL_EMBEDDING_MODEL": "intfloat/multilingual-e5-large", + "LOCAL_EMBEDDING_DIMENSION": "1024", + "EMBEDDING_PROMPT_STYLE": "e5" } } } } ``` -### 5ire için Yapılandırma -Tool ayarlarında **Environment Variables** alanına ekleyin: +### Alternatif 1: Ollama (yerel, daha hafif kurulum) + +```bash +ollama serve +ollama pull nomic-embed-text # 768 dim, İngilizce ağırlıklı ``` + +```bash +EMBEDDING_PROVIDER=local +LOCAL_EMBEDDING_BASE_URL=http://localhost:11434/v1 +LOCAL_EMBEDDING_MODEL=nomic-embed-text +LOCAL_EMBEDDING_DIMENSION=768 +EMBEDDING_PROMPT_STYLE=raw +``` + +> Ollama kütüphanesinde `multilingual-e5-large` doğrudan yok; Türkçe için TEI yolu daha doğru sonuç verir. + +### Alternatif 2: OpenRouter (hosted) + +```bash OPENROUTER_API_KEY=sk-or-v1-xxx... +# İsteğe bağlı — varsayılan google/gemini-embedding-001 (3072 dim, ÜCRETLİ) +# OPENROUTER_EMBEDDING_MODEL=... +# OPENROUTER_EMBEDDING_DIMENSION=... +# EMBEDDING_PROMPT_STYLE=gemini # varsayılan ``` -### Gemini CLI için Yapılandırma -```json -{ - "mcpServers": { - "yargi_mcp": { - "command": "uvx", - "args": ["yargi-mcp"], - "env": { - "OPENROUTER_API_KEY": "sk-or-v1-xxx..." - } - } - } -} -``` +API anahtarınızı [openrouter.ai/keys](https://openrouter.ai/keys) adresinden alın. Varsayılan model `google/gemini-embedding-001` artık ücretli — ücretsiz bir model seçerseniz `OPENROUTER_EMBEDDING_MODEL`, `OPENROUTER_EMBEDDING_DIMENSION` ve uygun `EMBEDDING_PROMPT_STYLE` değerlerini birlikte ayarlayın. -> 💡 **Not:** `OPENROUTER_API_KEY` ayarlanmazsa semantik arama aracı görünmez, diğer 24 araç normal şekilde çalışmaya devam eder. +### Yapılandırma Referansı + +| Env Var | Açıklama | Örnek | +|---|---|---| +| `EMBEDDING_PROVIDER` | `local` ise yerel sunucu, boş ise OpenRouter | `local` | +| `EMBEDDING_PROMPT_STYLE` | `gemini` / `e5` / `raw` — modelin beklediği önek | `e5` | +| `LOCAL_EMBEDDING_BASE_URL` | Yerel sunucunun OpenAI-uyumlu URL'i | `http://localhost:8080/v1` | +| `LOCAL_EMBEDDING_MODEL` | Model adı | `intfloat/multilingual-e5-large` | +| `LOCAL_EMBEDDING_DIMENSION` | Modelin çıktı boyutu (mutlaka eşleşmeli) | `1024` | +| `OPENROUTER_API_KEY` | OpenRouter anahtarı (sadece hosted için) | `sk-or-v1-…` | +| `OPENROUTER_EMBEDDING_MODEL` | OpenRouter model id'si | `google/gemini-embedding-001` | +| `OPENROUTER_EMBEDDING_DIMENSION` | OpenRouter modelinin çıktı boyutu | `3072` | + +> 💡 **Not:** Hiçbir embedding sağlayıcı yapılandırılmazsa semantik arama aracı görünmez, diğer 24 araç normal şekilde çalışır.
diff --git a/semantic_search/embedder.py b/semantic_search/embedder.py index d49c6c1..67e0c24 100644 --- a/semantic_search/embedder.py +++ b/semantic_search/embedder.py @@ -14,13 +14,50 @@ DEFAULT_DIMENSION = 3072 # Local provider defaults — Ollama with nomic-embed-text out of the box. # Override via LOCAL_EMBEDDING_BASE_URL / LOCAL_EMBEDDING_MODEL / -# LOCAL_EMBEDDING_DIMENSION when using a different server or model -# (e.g. llama.cpp's server, vLLM, LM Studio, or a different Ollama model -# such as bge-m3 — better for Turkish — at 1024 dimensions). +# LOCAL_EMBEDDING_DIMENSION when using a different server or model. +# For Turkish, intfloat/multilingual-e5-large (1024 dims, prompt_style=e5) +# served via HuggingFace TEI is the recommended setup — see README. LOCAL_DEFAULT_BASE_URL = "http://localhost:11434/v1" LOCAL_DEFAULT_MODEL = "nomic-embed-text" LOCAL_DEFAULT_DIMENSION = 768 +# Prompt-template styles. Embedding models are trained with specific +# prefixes — using the wrong style silently degrades retrieval quality. +# - "gemini": "task: {task} | query: {text}" / "title: {title} | text: {text}" +# (matches google/gemini-embedding-001, the OpenRouter default) +# - "e5": "query: {text}" / "passage: {text}" +# (matches intfloat/multilingual-e5-* models — best for Turkish) +# - "raw": no prefix; pass text through as-is +PROMPT_STYLES = ("gemini", "e5", "raw") +DEFAULT_PROMPT_STYLE = "gemini" + + +def _format_query(prompt_style: str, query: str, task: str) -> str: + if prompt_style == "e5": + return f"query: {query}" + if prompt_style == "raw": + return query + # gemini (default) + return f"task: {task} | query: {query}" + + +def _format_document(prompt_style: str, doc: str, title: str) -> str: + if prompt_style == "e5": + return f"passage: {doc}" + if prompt_style == "raw": + return doc + # gemini (default) + return f"title: {title} | text: {doc}" + + +def _resolve_prompt_style(explicit: Optional[str], default: str) -> str: + style = (explicit or os.getenv("EMBEDDING_PROMPT_STYLE") or default).strip().lower() + if style not in PROMPT_STYLES: + raise ValueError( + f"Unknown EMBEDDING_PROMPT_STYLE {style!r}; expected one of {PROMPT_STYLES}" + ) + return style + def is_openrouter_available() -> bool: """Check if OpenRouter API key is available.""" @@ -66,19 +103,21 @@ class _BaseOpenAICompatibleEmbedder: client = None model: str = "" dimension: int = 0 + prompt_style: str = DEFAULT_PROMPT_STYLE def encode_query(self, query: str, task: str = "search result") -> np.ndarray: """ - Encode a search query. + Encode a search query. Prefix is selected by ``self.prompt_style``. Args: query: The search query text - task: Task type for prompt template + task: Task hint used by the gemini-style prefix; ignored for + e5/raw styles. Returns: Numpy array of embeddings (``self.dimension`` elements). """ - text = f"task: {task} | query: {query}" + text = _format_query(self.prompt_style, query, task) try: response = self.client.embeddings.create( @@ -119,7 +158,7 @@ class _BaseOpenAICompatibleEmbedder: texts = [] for i, doc in enumerate(documents): title = titles[i] if titles and i < len(titles) else "none" - texts.append(f"title: {title} | text: {doc}") + texts.append(_format_document(self.prompt_style, doc, title)) try: response = self.client.embeddings.create( @@ -186,7 +225,12 @@ class OpenRouterEmbedder(_BaseOpenAICompatibleEmbedder): "X-Title": "Yargi MCP Server", } - def __init__(self, model: Optional[str] = None, dimension: Optional[int] = None): + def __init__( + self, + model: Optional[str] = None, + dimension: Optional[int] = None, + prompt_style: Optional[str] = None, + ): api_key = os.getenv("OPENROUTER_API_KEY") if not api_key: raise ValueError("OPENROUTER_API_KEY environment variable is not set") @@ -206,10 +250,14 @@ class OpenRouterEmbedder(_BaseOpenAICompatibleEmbedder): "OPENROUTER_EMBEDDING_DIMENSION", DEFAULT_DIMENSION, ) + # Default to gemini-style prefix for OpenRouter — matches the default + # google/gemini-embedding-001 model. Override via constructor or + # EMBEDDING_PROMPT_STYLE env var when picking a different model. + self.prompt_style = _resolve_prompt_style(prompt_style, "gemini") logger.info( f"OpenRouter Embedder initialized with model: {self.model} " - f"(dimension={self.dimension})" + f"(dimension={self.dimension}, prompt_style={self.prompt_style})" ) @@ -240,6 +288,7 @@ class LocalEmbedder(_BaseOpenAICompatibleEmbedder): model: Optional[str] = None, dimension: Optional[int] = None, api_key: Optional[str] = None, + prompt_style: Optional[str] = None, ): try: from openai import OpenAI @@ -266,10 +315,15 @@ class LocalEmbedder(_BaseOpenAICompatibleEmbedder): "LOCAL_EMBEDDING_DIMENSION", LOCAL_DEFAULT_DIMENSION, ) + # Default to e5 prefix for local — the recommended Turkish setup + # (multilingual-e5-large). Override via EMBEDDING_PROMPT_STYLE when + # using a different model family (e.g. nomic, bge). + self.prompt_style = _resolve_prompt_style(prompt_style, "e5") logger.info( f"Local Embedder initialized: model={self.model} " - f"base_url={self.base_url} dimension={self.dimension}" + f"base_url={self.base_url} dimension={self.dimension} " + f"prompt_style={self.prompt_style}" )