feat(evidence): use server-side Qdrant BM25 retrieval

This commit is contained in:
2026-08-24 18:15:33 +02:00
parent 0e9add09a9
commit 29d41ac258
15 changed files with 253 additions and 34 deletions
+95 -2
View File
@@ -86,7 +86,8 @@ class FakeQdrantHttp:
if method == "POST" and path == "/collections/workspace-semantic/points/query":
if self.malformed_query:
return FakeResponse(200, {"result": {"points": "nope"}})
wanted = _match_points(self.points.values(), json["filter"])
filter_value = json["filter"] if "filter" in json else json["prefetch"][0]["filter"]
wanted = _match_points(self.points.values(), filter_value)
scored = sorted(
(
{
@@ -150,7 +151,7 @@ def _match_clause(payload, clause):
raise AssertionError(clause)
def _write_record(record_id: str, kind: str, *, metadata=None):
def _write_record(record_id: str, kind: str, *, metadata=None, sparse_text=None, sparse_language=None):
return VectorWriteRecord(
record=VectorRecord(
id=record_id,
@@ -162,6 +163,8 @@ def _write_record(record_id: str, kind: str, *, metadata=None):
),
embedding=[0.1] * 1024,
content_hash="sha256:" + "a" * 64,
sparse_text=sparse_text,
sparse_language=sparse_language,
)
@@ -358,6 +361,89 @@ def test_upsert_serializes_qdrant_point_payloads(record, semantic_kind):
assert point["payload"]["content_hash"] == record.content_hash
def test_evidence_upsert_sends_dense_and_server_side_italian_bm25():
fake = FakeQdrantHttp()
store = _store(fake)
record = _write_record(
"demo:gen:11111111111111111111111111111111:chunk:1",
"evidence",
metadata={
"workspace_id": "demo",
"vector_generation": "gen:11111111111111111111111111111111",
"document_id": "doc:abc",
},
sparse_text="ricovero per cardiomiopatia dilatativa",
sparse_language="italian",
)
store.upsert("evidence", [record])
point = next(iter(fake.points.values()))
assert point["vector"] == {
"": record.embedding,
"bm25": {
"text": "ricovero per cardiomiopatia dilatativa",
"model": "qdrant/bm25",
"options": {"language": "italian"},
},
}
def test_evidence_search_uses_filtered_dense_and_bm25_prefetches_with_default_rrf():
fake = FakeQdrantHttp()
store = _store(fake)
generation = "gen:" + "1" * 32
store.upsert("evidence", [
_write_record(
f"demo:{generation}:chunk:1",
"evidence",
metadata={"workspace_id": "demo", "vector_generation": generation, "document_id": "doc:abc"},
sparse_text="ricovero per cardiomiopatia dilatativa",
sparse_language="italian",
)
])
store.search(
["evidence"], [0.2] * 1024, limit=10, kinds=["evidence"],
query_text="cardiomiopatia", query_language="italian",
metadata_filter={"workspace_id": "demo", "vector_generation": generation, "document_ids": ["doc:abc"]},
)
query = next(call[2] for call in reversed(fake.calls) if call[1].endswith("/points/query"))
assert query["query"] == {"rrf": {}}
assert query["limit"] == 10
assert query["prefetch"] == [
{
"query": [0.2] * 1024,
"limit": 20,
"filter": {"must": [
{"key": "workspace_id", "match": {"value": "demo"}},
{"key": "workspace_revision", "match": {"value": "a" * 40}},
{"key": "kind", "match": {"any": ["evidence"]}},
{"key": "record_kind", "match": {"any": ["evidence"]}},
{"key": "vector_generation", "match": {"value": generation}},
{"key": "document_id", "match": {"any": ["doc:abc"]}},
]},
},
{
"query": {
"text": "cardiomiopatia", "model": "qdrant/bm25",
"options": {"language": "italian"},
},
"using": "bm25",
"limit": 20,
"filter": {"must": [
{"key": "workspace_id", "match": {"value": "demo"}},
{"key": "workspace_revision", "match": {"value": "a" * 40}},
{"key": "kind", "match": {"any": ["evidence"]}},
{"key": "record_kind", "match": {"any": ["evidence"]}},
{"key": "vector_generation", "match": {"value": generation}},
{"key": "document_id", "match": {"any": ["doc:abc"]}},
]},
},
]
def test_search_filters_by_workspace_and_allowed_record_kinds():
fake = FakeQdrantHttp()
store = _store(fake)
@@ -384,6 +470,13 @@ def test_search_filters_by_workspace_and_allowed_record_kinds():
}
def test_evidence_search_refuses_dense_only_fallback():
store = _store(FakeQdrantHttp())
with pytest.raises(VectorStoreError, match="hybrid query text"):
store.search(["evidence"], [0.2] * 1024, limit=5, kinds=["evidence"])
def test_search_excludes_inconsistent_semantic_kind_in_bound_workspace():
fake = FakeQdrantHttp()
store = _store(fake)