fix(vector): address PR review round 2 — status branching, doc_id guard, doc restore
- _ensure_keyword_payload_indexes: distinguish 400 (schema conflict, warning)
from other status codes (5xx/network, error) so a transient outage doesn't
silently leave the collection unindexed.
- build_search_result_from_point: use .get("doc_id") + return None on missing
instead of KeyError-crashing the search; reverse metadata merge order so
payload-derived chunk_index/total_chunks win over caller-supplied extras.
- docs/configuration.md: restore the OpenAI/Mistral/Bedrock/Simple provider
sections + reference-table rows that were dropped in the rebase. Reword
the "Startup migrations" bullet to describe what the code actually does
(no sampling — full scroll, zero writes when clean). Add operator note
about the SemanticSearchResult.id TypeError path.
- tests: pytest.approx for float equality (Sonar python:S1244); coverage
for non-400 → ERROR, payload={doc_id: None}, and missing doc_id key.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
6aba589a6e
commit
b5b4025bb4
@@ -162,6 +162,14 @@ def test_build_search_result_from_point_returns_none_when_payload_missing():
|
||||
assert build_search_result_from_point(point) is None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_build_search_result_from_point_returns_none_when_doc_id_missing():
|
||||
"""A payload without a doc_id key is skipped instead of raising KeyError."""
|
||||
point = _make_point(point_id="p-bad", payload={"doc_type": "note"})
|
||||
|
||||
assert build_search_result_from_point(point) is None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_build_search_result_from_point_note_payload():
|
||||
"""Note-type payload populates the SearchResult fields without metadata extras."""
|
||||
@@ -187,7 +195,7 @@ def test_build_search_result_from_point_note_payload():
|
||||
assert sr.doc_type == "note"
|
||||
assert sr.title == "Hello"
|
||||
assert sr.excerpt == "world"
|
||||
assert sr.score == 0.91
|
||||
assert sr.score == pytest.approx(0.91)
|
||||
assert sr.chunk_start_offset == 0
|
||||
assert sr.chunk_end_offset == 100
|
||||
assert sr.chunk_index == 0
|
||||
@@ -257,21 +265,34 @@ def test_build_search_result_from_point_deck_card_metadata():
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_build_search_result_from_point_merges_metadata_extras():
|
||||
"""metadata_extras override/augment the helper's computed metadata dict."""
|
||||
"""metadata_extras augment the helper's computed metadata dict.
|
||||
|
||||
Common fields (chunk_index, total_chunks) win over caller-supplied
|
||||
extras to keep them tied to the actual point.
|
||||
"""
|
||||
point = _make_point(
|
||||
point_id="p-4",
|
||||
payload={"doc_id": "1", "doc_type": "note"},
|
||||
payload={
|
||||
"doc_id": "1",
|
||||
"doc_type": "note",
|
||||
"chunk_index": 3,
|
||||
"total_chunks": 9,
|
||||
},
|
||||
)
|
||||
|
||||
sr = build_search_result_from_point(
|
||||
point, metadata_extras={"search_method": "bm25_hybrid_rrf"}
|
||||
point,
|
||||
metadata_extras={
|
||||
"search_method": "bm25_hybrid_rrf",
|
||||
# Caller tries to override a common field — should be ignored.
|
||||
"chunk_index": "should-be-overwritten",
|
||||
},
|
||||
)
|
||||
|
||||
assert sr is not None
|
||||
assert sr.metadata["search_method"] == "bm25_hybrid_rrf"
|
||||
# Common fields still present
|
||||
assert "chunk_index" in sr.metadata
|
||||
assert "total_chunks" in sr.metadata
|
||||
assert sr.metadata["chunk_index"] == 3
|
||||
assert sr.metadata["total_chunks"] == 9
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
|
||||
@@ -103,6 +103,32 @@ async def test_ensure_keyword_payload_indexes_logs_400_as_warning(mocker, caplog
|
||||
assert "different schema" in warnings[0].getMessage()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
async def test_ensure_keyword_payload_indexes_logs_non_400_as_error(mocker, caplog):
|
||||
"""A non-400 status from create_payload_index escalates to ERROR.
|
||||
|
||||
A 5xx response (e.g., Qdrant temporarily unavailable) should not be
|
||||
silently downgraded to a warning the way a 400 schema-conflict is.
|
||||
The loop still continues so the remaining fields get attempted.
|
||||
"""
|
||||
client = mocker.AsyncMock()
|
||||
client.create_payload_index.side_effect = [
|
||||
_make_unexpected(500, b'{"status":{"error":"internal server error"}}'),
|
||||
None,
|
||||
None,
|
||||
]
|
||||
|
||||
with caplog.at_level("ERROR", logger="nextcloud_mcp_server.vector.qdrant_client"):
|
||||
await _ensure_keyword_payload_indexes(client, "test-collection")
|
||||
|
||||
assert client.create_payload_index.await_count == len(_KEYWORD_PAYLOAD_FIELDS)
|
||||
errors = [r for r in caplog.records if r.levelname == "ERROR"]
|
||||
assert len(errors) == 1
|
||||
msg = errors[0].getMessage()
|
||||
assert "500" in msg
|
||||
assert "internal server error" in msg
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _backfill_doc_id_to_string
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -231,3 +257,27 @@ async def test_backfill_handles_none_payload(mocker):
|
||||
points=[2],
|
||||
wait=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
async def test_backfill_handles_payload_with_explicit_none_doc_id(mocker):
|
||||
"""A payload of {doc_id: None, ...} is skipped just like payload=None."""
|
||||
client = mocker.AsyncMock()
|
||||
# Build the record manually to distinguish payload=None from payload={"doc_id": None}.
|
||||
point_with_explicit_none = SimpleNamespace(
|
||||
id=1, payload={"doc_id": None, "doc_type": "file"}
|
||||
)
|
||||
client.scroll.side_effect = [
|
||||
([point_with_explicit_none, _record(2, 99)], None),
|
||||
]
|
||||
|
||||
await _backfill_doc_id_to_string(client, "test-collection")
|
||||
|
||||
# Only the int doc_id at point 2 was rewritten; the explicit-None payload was skipped.
|
||||
assert client.set_payload.await_count == 1
|
||||
client.set_payload.assert_awaited_with(
|
||||
collection_name="test-collection",
|
||||
payload={"doc_id": "99"},
|
||||
points=[2],
|
||||
wait=True,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user