feat(retrieval): rerank semantic search results

This commit is contained in:
Julian lechner committed 2026-09-17 13:07:47 +02:00
1 parent 2975619da8
commit 727c499d2c
3 files changed
+56 -4

No files matched your search

+36
View File
@@ -0,0 +1,36 @@
import importlib.util
from pathlib import Path
import unittest
ROOT = Path(__file__).resolve().parent.parent
SPEC = importlib.util.spec_from_file_location('retrieval_server', ROOT / 'shared/retrieval/server.py')
MODULE = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(MODULE)
class RetrievalServerTests(unittest.TestCase):
def test_rerank_uses_cross_encoder_scores(self):
index = MODULE.Index.__new__(MODULE.Index)
class Reranker:
def predict(self, pairs):
self.pairs = pairs
return [0.1, 0.8, 0.5]
reranker = Reranker()
index.reranker = lambda: reranker
rows = [
{'path': 'a.md', 'score': 0.9, 'text': 'first'},
{'path': 'b.md', 'score': 0.8, 'text': 'second'},
{'path': 'c.md', 'score': 0.7, 'text': 'third'},
]
result = index.rerank('query', rows, 2)
self.assertEqual([item['path'] for item in result], ['b.md', 'c.md'])
self.assertEqual(reranker.pairs, [('query', 'first'), ('query', 'second'), ('query', 'third')])
if __name__ == '__main__':
unittest.main()
+1 -1
View File
@@ -1,2 +1,2 @@
mcp>=1.9,<2
sentence-transformers>=4.1,<5
sentence-transformers>=5.4,<6
+19 -3
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Local MCP semantic search backed by Qwen3-Embedding-0.6B."""
"""Local MCP semantic search backed by Qwen3 embedding and reranking."""
from __future__ import annotations
@@ -11,6 +11,7 @@ import sqlite3
MODEL = "Qwen/Qwen3-Embedding-0.6B"
RERANKER_MODEL = "Qwen/Qwen3-Reranker-0.6B"
TEXT_EXTENSIONS = {".c", ".cpp", ".cs", ".go", ".java", ".js", ".json", ".md", ".py", ".ps1", ".rs", ".sh", ".toml", ".ts", ".tsx", ".txt", ".yaml", ".yml"}
@@ -31,6 +32,7 @@ class Index:
self.db.execute("create table if not exists chunks (path text, text text, vector blob)")
self.db.commit()
self.model = None
self.reranker_model = None
def files(self):
ignored = {".git", ".venv", "generated", "node_modules", ".my-ai-configuration"}
@@ -49,6 +51,19 @@ class Index:
self.model = SentenceTransformer(MODEL)
return self.model
def reranker(self):
if self.reranker_model is None:
from sentence_transformers import CrossEncoder
self.reranker_model = CrossEncoder(RERANKER_MODEL)
return self.reranker_model
def rerank(self, query: str, rows: list[dict], limit: int):
candidates = rows[:min(len(rows), max(limit, 20))]
scores = self.reranker().predict([(query, item["text"]) for item in candidates])
for item, score in zip(candidates, scores):
item["score"] = round(float(score), 6)
return sorted(candidates, key=lambda item: item["score"], reverse=True)[:limit]
def rebuild(self):
encoder = self.encoder()
values = []
@@ -73,7 +88,8 @@ class Index:
import numpy as np
score = float(np.dot(vector, np.frombuffer(blob, dtype=np.float32)))
rows.append({"path": path, "score": round(score, 6), "text": text})
return sorted(rows, key=lambda item: item["score"], reverse=True)[:max(1, min(top_k, 20))]
limit = max(1, min(top_k, 20))
return self.rerank(query, sorted(rows, key=lambda item: item["score"], reverse=True), limit)
def main():
@@ -91,7 +107,7 @@ def main():
@mcp.tool()
def semantic_search(query: str, top_k: int = 5) -> list[dict]:
"""Find repository passages by meaning with Qwen3-Embedding-0.6B."""
"""Find repository passages with Qwen3 embedding and reranking."""
return index.search(query, top_k)
@mcp.tool()