fix(evidence): fail closed when BM25 is unavailable

This commit is contained in:
2026-08-24 18:21:21 +02:00
parent 29d41ac258
commit 6176410f42
5 changed files with 70 additions and 5 deletions
+45 -1
View File
@@ -32,6 +32,7 @@ class FakeQdrantHttp:
self.dimension = dimension
self.distance = distance
self.collection = None
self.sparse_vectors: dict[str, dict] | None = None
self.payload_indexes: set[str] = set()
self.points: dict[str, dict] = {}
self.calls: list[tuple[str, str, dict | None]] = []
@@ -56,7 +57,11 @@ class FakeQdrantHttp:
"result": {
"config": {
"params": {
"vectors": {"size": self.dimension, "distance": self.distance}
"vectors": {"size": self.dimension, "distance": self.distance},
**(
{"sparse_vectors": self.sparse_vectors}
if self.sparse_vectors is not None else {}
),
}
},
"payload_schema": {
@@ -69,6 +74,7 @@ class FakeQdrantHttp:
self.collection = json
self.dimension = json["vectors"]["size"]
self.distance = json["vectors"]["distance"]
self.sparse_vectors = json.get("sparse_vectors")
return FakeResponse(200, {"status": "ok"})
if method == "PUT" and path == "/collections/workspace-semantic/index":
@@ -182,6 +188,15 @@ def _store(
)
def _ready_collection_with_bm25(fake: FakeQdrantHttp) -> None:
fake.collection = {"vectors": {"size": 1024, "distance": "Cosine"}}
fake.payload_indexes = {
"content_hash", "document_id", "kind", "record_key", "record_kind",
"vector_generation", "workspace_id", "workspace_revision",
}
fake.sparse_vectors = {"bm25": {"modifier": "idf"}}
def test_point_id_is_deterministic_uuidv5():
assert point_id("demo", "memory", "memory:1") == str(
uuid5(NAMESPACE_URL, "thothii:demo:memory:memory:1")
@@ -324,6 +339,20 @@ def test_health_fails_when_required_payload_indexes_are_missing_without_creating
assert fake.payload_indexes == set()
def test_health_reports_when_evidence_bm25_is_unavailable_without_disabling_dense_callers():
fake = FakeQdrantHttp()
fake.collection = {"vectors": {"size": 1024, "distance": "Cosine"}}
fake.payload_indexes = {
"content_hash", "document_id", "kind", "record_key", "record_kind",
"vector_generation", "workspace_id", "workspace_revision",
}
health = _store(fake).health()
assert health.ok is True
assert health.bm25_compatible is False
@pytest.mark.parametrize(
("record", "semantic_kind"),
[
@@ -363,6 +392,7 @@ def test_upsert_serializes_qdrant_point_payloads(record, semantic_kind):
def test_evidence_upsert_sends_dense_and_server_side_italian_bm25():
fake = FakeQdrantHttp()
_ready_collection_with_bm25(fake)
store = _store(fake)
record = _write_record(
"demo:gen:11111111111111111111111111111111:chunk:1",
@@ -391,6 +421,7 @@ def test_evidence_upsert_sends_dense_and_server_side_italian_bm25():
def test_evidence_search_uses_filtered_dense_and_bm25_prefetches_with_default_rrf():
fake = FakeQdrantHttp()
_ready_collection_with_bm25(fake)
store = _store(fake)
generation = "gen:" + "1" * 32
store.upsert("evidence", [
@@ -477,6 +508,19 @@ def test_evidence_search_refuses_dense_only_fallback():
store.search(["evidence"], [0.2] * 1024, limit=5, kinds=["evidence"])
def test_hybrid_evidence_search_refuses_a_collection_without_bm25():
fake = FakeQdrantHttp()
_ready_collection_with_bm25(fake)
fake.sparse_vectors = None
store = _store(fake)
with pytest.raises(VectorStoreError, match="BM25 collection configuration"):
store.search(
["evidence"], [0.2] * 1024, limit=5, kinds=["evidence"],
query_text="cardiomiopatia", query_language="italian",
)
def test_search_excludes_inconsistent_semantic_kind_in_bound_workspace():
fake = FakeQdrantHttp()
store = _store(fake)