docs(semantic_search): recommend multilingual-e5-large for Turkish (#22)

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) <noreply@anthropic.com>
This commit is contained in:
saidsurucu
2026-05-03 01:55:42 +03:00
co-authored by Claude Opus 4.7
parent fb29146755
commit 6b781b61d2
3 changed files with 155 additions and 41 deletions
+22 -7
View File
@@ -89,15 +89,30 @@ OPENROUTER_API_KEY=sk-or-v1-your_openrouter_api_key_here
# OPENROUTER_EMBEDDING_MODEL=google/gemini-embedding-001 # OPENROUTER_EMBEDDING_MODEL=google/gemini-embedding-001
# OPENROUTER_EMBEDDING_DIMENSION=3072 # OPENROUTER_EMBEDDING_DIMENSION=3072
# --- Option B: Local OpenAI-compatible server (Ollama / llama.cpp / vLLM) ----- # --- Option B: Local OpenAI-compatible server (no API key required) ----------
# Uncomment to use your own server instead of OpenRouter (no API key required). # Recommended for Turkish: intfloat/multilingual-e5-large served by HuggingFace
# Defaults target Ollama with nomic-embed-text. For Turkish, bge-m3 (1024 dims) # Text Embeddings Inference (TEI). One-line setup:
# tends to work better — pull it with: `ollama pull bge-m3` #
# 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 # EMBEDDING_PROVIDER=local
# LOCAL_EMBEDDING_BASE_URL=http://localhost:11434/v1 # LOCAL_EMBEDDING_BASE_URL=http://localhost:8080/v1
# LOCAL_EMBEDDING_MODEL=nomic-embed-text # LOCAL_EMBEDDING_MODEL=intfloat/multilingual-e5-large
# LOCAL_EMBEDDING_DIMENSION=768 # LOCAL_EMBEDDING_DIMENSION=1024
# EMBEDDING_PROMPT_STYLE=e5
# LOCAL_EMBEDDING_API_KEY= # most local servers ignore this # 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 # USAGE INSTRUCTIONS
+69 -24
View File
@@ -181,20 +181,40 @@ Yargı MCP'yi Gemini CLI ile kullanmak için:
--- ---
<details> <details>
<summary>🧠 <strong>Semantik Arama (Opsiyonel - OpenRouter API)</strong></summary> <summary>🧠 <strong>Semantik Arama (Opsiyonel)</strong></summary>
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? ### Semantik Arama Nasıl Çalışır?
1. `initial_keyword` ile Bedesten API'den 100 karar çekilir 1. `initial_keyword` ile Bedesten API'den 100 karar çekilir
2. `query` ile bu kararlar embedding modeli kullanılarak anlamsal olarak sıralanır 2. `query` ile bu kararlar embedding modeli kullanılarak anlamsal olarak sıralanır
3. En alakalı kararlar döndürülür 3. En alakalı kararlar döndürülür
### OpenRouter API Anahtarı Alma ### Önerilen Türkçe Kurulumu (Yerel — `multilingual-e5-large`)
1. [OpenRouter](https://openrouter.ai/) sitesine gidin
2. Hesap oluşturun ve API anahtarı alın (ücretsiz kredi ile başlayabilirsiniz)
### 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 ```json
{ {
"mcpServers": { "mcpServers": {
@@ -202,35 +222,60 @@ Yargı MCP, **semantik arama** özelliği ile kararları anlamsal olarak sırala
"command": "uvx", "command": "uvx",
"args": ["yargi-mcp"], "args": ["yargi-mcp"],
"env": { "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 ### Alternatif 1: Ollama (yerel, daha hafif kurulum)
Tool ayarlarında **Environment Variables** alanına ekleyin:
```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... 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 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.
```json
{
"mcpServers": {
"yargi_mcp": {
"command": "uvx",
"args": ["yargi-mcp"],
"env": {
"OPENROUTER_API_KEY": "sk-or-v1-xxx..."
}
}
}
}
```
> 💡 **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.
</details> </details>
+64 -10
View File
@@ -14,13 +14,50 @@ DEFAULT_DIMENSION = 3072
# Local provider defaults — Ollama with nomic-embed-text out of the box. # Local provider defaults — Ollama with nomic-embed-text out of the box.
# Override via LOCAL_EMBEDDING_BASE_URL / LOCAL_EMBEDDING_MODEL / # Override via LOCAL_EMBEDDING_BASE_URL / LOCAL_EMBEDDING_MODEL /
# LOCAL_EMBEDDING_DIMENSION when using a different server or 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 # For Turkish, intfloat/multilingual-e5-large (1024 dims, prompt_style=e5)
# such as bge-m3 — better for Turkish — at 1024 dimensions). # served via HuggingFace TEI is the recommended setup — see README.
LOCAL_DEFAULT_BASE_URL = "http://localhost:11434/v1" LOCAL_DEFAULT_BASE_URL = "http://localhost:11434/v1"
LOCAL_DEFAULT_MODEL = "nomic-embed-text" LOCAL_DEFAULT_MODEL = "nomic-embed-text"
LOCAL_DEFAULT_DIMENSION = 768 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: def is_openrouter_available() -> bool:
"""Check if OpenRouter API key is available.""" """Check if OpenRouter API key is available."""
@@ -66,19 +103,21 @@ class _BaseOpenAICompatibleEmbedder:
client = None client = None
model: str = "" model: str = ""
dimension: int = 0 dimension: int = 0
prompt_style: str = DEFAULT_PROMPT_STYLE
def encode_query(self, query: str, task: str = "search result") -> np.ndarray: 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: Args:
query: The search query text 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: Returns:
Numpy array of embeddings (``self.dimension`` elements). Numpy array of embeddings (``self.dimension`` elements).
""" """
text = f"task: {task} | query: {query}" text = _format_query(self.prompt_style, query, task)
try: try:
response = self.client.embeddings.create( response = self.client.embeddings.create(
@@ -119,7 +158,7 @@ class _BaseOpenAICompatibleEmbedder:
texts = [] texts = []
for i, doc in enumerate(documents): for i, doc in enumerate(documents):
title = titles[i] if titles and i < len(titles) else "none" 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: try:
response = self.client.embeddings.create( response = self.client.embeddings.create(
@@ -186,7 +225,12 @@ class OpenRouterEmbedder(_BaseOpenAICompatibleEmbedder):
"X-Title": "Yargi MCP Server", "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") api_key = os.getenv("OPENROUTER_API_KEY")
if not api_key: if not api_key:
raise ValueError("OPENROUTER_API_KEY environment variable is not set") raise ValueError("OPENROUTER_API_KEY environment variable is not set")
@@ -206,10 +250,14 @@ class OpenRouterEmbedder(_BaseOpenAICompatibleEmbedder):
"OPENROUTER_EMBEDDING_DIMENSION", "OPENROUTER_EMBEDDING_DIMENSION",
DEFAULT_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( logger.info(
f"OpenRouter Embedder initialized with model: {self.model} " 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, model: Optional[str] = None,
dimension: Optional[int] = None, dimension: Optional[int] = None,
api_key: Optional[str] = None, api_key: Optional[str] = None,
prompt_style: Optional[str] = None,
): ):
try: try:
from openai import OpenAI from openai import OpenAI
@@ -266,10 +315,15 @@ class LocalEmbedder(_BaseOpenAICompatibleEmbedder):
"LOCAL_EMBEDDING_DIMENSION", "LOCAL_EMBEDDING_DIMENSION",
LOCAL_DEFAULT_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( logger.info(
f"Local Embedder initialized: model={self.model} " 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}"
) )