fix(chunk-context): address PR #767 round-2 review — gate, parity, doc

Latest reviewer comment flagged five items on top of the original PR. This
commit addresses every one:

🟡 1. Skip the offset-based Qdrant fallback for `doc_type=file` when
   `chunk_index` is supplied. Qdrant Cloud's strict mode rejects unindexed
   filter fields with HTTP 400, which `_get_chunk_from_qdrant` catches and
   logs at `logger.error` — masking real Qdrant problems in monitoring.
   Notes/cards keep the offset fallback (cheap, useful for legacy data).

🟡 2. Add a `logger.warning` and clarifying inline comment in the doc-text
   fallback path when `chunk_index` is None — surfaces the pre-existing
   "0/N misreport" so callers can detect it. Type-nullability propagation
   is deferred to a follow-up (out of scope for this hotfix).

🟢 3. Simplify `if chunk_text and doc_id_int is not None:` →
   `if chunk_text:` with an inner `assert doc_id_int is not None` for
   `ty` narrowing. The outer second clause was dead.

🟢 4. Add `doc_type` `FieldCondition` to the offset-based image lookup in
   both `visualization.py` and `viz_routes.py` for parity with the
   `chunk_index` branches.

🟢 5. Inline the `chunk_filter` local in `visualization.py` directly into
   the `must=[]` list (matches `viz_routes.py` style).

Adds `tests/unit/test_chunk_context_offset_gate.py` with three regression
tests covering the gate matrix: (file, with-index → skip offset),
(note, with-index → still tries offset), (file, no-index → still tries
offset). Lives at top-level rather than `tests/unit/search/` to side-step
a pre-existing circular-init issue in `nextcloud_mcp_server.search`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-05-08 21:28:50 +02:00
co-authored by Claude Opus 4.7
parent 8457c427a5
commit 51c1d42ea3
4 changed files with 174 additions and 6 deletions
+25 -2
View File
@@ -274,6 +274,11 @@ async def get_chunk_with_context(
# Effective chunk_index for adjacent lookups, marker insertion, and the
# response payload — keep `chunk_index is not None` distinct from this so
# the gate at line ~280 still controls *whether* to take the indexed path.
# NOTE: when the caller doesn't supply chunk_index, this defaults to 0, so
# the doc-text fallback path will report the chunk as "0/N" in markers and
# the response payload regardless of its actual position. Callers that
# need accurate position metadata in the fallback path must pass
# chunk_index. See PR #767 review.
effective_chunk_index = chunk_index if chunk_index is not None else 0
# Try to get chunk from Qdrant (fast path).
@@ -285,12 +290,20 @@ async def get_chunk_with_context(
chunk_text = await _get_chunk_by_index_from_qdrant(
user_id, doc_id_int, doc_type, chunk_index
)
if chunk_text is None:
# Skip the offset fallback for files when the indexed lookup was
# already attempted: Qdrant Cloud's strict mode requires an index on
# filtered fields, and chunk_start/end_offset aren't indexed there, so
# the call returns 400 and surfaces a misleading logger.error. The
# file fast-fail below correctly handles the miss without it.
if chunk_text is None and not (chunk_index is not None and doc_type == "file"):
chunk_text = await _get_chunk_from_qdrant(
user_id, doc_id_int, doc_type, chunk_start, chunk_end
)
if chunk_text and doc_id_int is not None:
if chunk_text:
# chunk_text can only be non-None inside the `if doc_id_int is not None:`
# block above, so doc_id_int is guaranteed non-None here. Narrow for ty.
assert doc_id_int is not None
logger.info(
f"Retrieved chunk from Qdrant cache for {doc_type} {doc_id} "
f"(avoids document re-fetch/re-parse)"
@@ -390,6 +403,16 @@ async def get_chunk_with_context(
f"(Qdrant cache miss, possibly legacy data)"
)
# When chunk_index isn't supplied, the response and markers will report
# this chunk as 0/N regardless of its actual position (effective_chunk_index
# defaulted to 0 above). Surface this so callers can detect the inaccuracy.
if chunk_index is None:
logger.warning(
f"chunk_index not supplied for {doc_type} {doc_id} doc-text "
f"fallback; position metadata in response will default to "
f"0/{total_chunks}"
)
# Fetch full document text (notes, deck cards, news items, etc.)
full_text = await _fetch_document_text(nc_client, doc_id, doc_type, user_id)
if full_text is None: