feat(evidence): use server-side Qdrant BM25 retrieval
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user