refactor(search): address PR #750 review feedback

- Cap all_results to limit*2 after sort in the per-doc_types branch of
  nc_semantic_search to bound over-verification (was unbounded N-types).
- Switch BatchVerifier from (client, doc_ids, user_id) to (client, results,
  semaphore). Verifiers now read file paths and deck board/stack ids from
  SearchResult.metadata instead of doing fresh Qdrant scrolls — eliminates
  one duplicate round-trip per file/deck-card verification.
- Bound per-id verification concurrency with a shared anyio.Semaphore
  (default 20, matching server/semantic.py context-expansion convention).
  Prevents httpx pool exhaustion / rate limiting on large search pages.
- Propagate stack_id from Qdrant payload to SearchResult.metadata in both
  bm25_hybrid.py and semantic.py (board_id was already propagated).
- Drop now-unused _resolve_file_path / _resolve_deck_metadata helpers.
- Drop redundant int(d) in requested predicate from _verify_news_items.
- Rewrite eviction comment to be honest about inline (not background)
  execution and the resulting latency coupling.
- ADR-019 status: Proposed -> Accepted.
- Add news property to NextcloudClientProtocol.
- Widen SearchResult.id and SemanticSearchResult.id to int | str to match
  BatchVerifier signature and document support for future string-id types.
- Flip openWorldHint to True on nc_semantic_search_answer (it calls into
  Nextcloud via nc_semantic_search).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-05-01 08:22:28 +02:00
co-authored by Claude Opus 4.7
parent d90e793d19
commit 7784ec02d7
8 changed files with 380 additions and 270 deletions
+8 -3
View File
@@ -142,8 +142,13 @@ def configure_semantic_tools(mcp: FastMCP):
)
all_results.extend(unverified_results)
# Sort combined results by score
# Sort combined results by score, then cap to `limit * 2` to
# match the cross-app branch's over-fetch budget. Without this
# cap, N requested doc_types × `limit * 2` results would all
# flow into verification, multiplying the Nextcloud round-trip
# cost by N.
all_results.sort(key=lambda r: r.score, reverse=True)
all_results = all_results[: limit * 2]
# ADR-019: Verify-on-read. The vector index is a recall layer;
# Nextcloud is the source of truth for access. Filter out ghost
@@ -300,7 +305,7 @@ def configure_semantic_tools(mcp: FastMCP):
title="Search with AI-Generated Answer",
annotations=ToolAnnotations(
readOnlyHint=True, # Search doesn't modify data
openWorldHint=False, # Searches only indexed Nextcloud data
openWorldHint=True, # Calls into Nextcloud via nc_semantic_search
),
)
@require_scopes("semantic.read")
@@ -432,7 +437,7 @@ def configure_semantic_tools(mcp: FastMCP):
async with semaphore:
if result.doc_type == "note":
try:
note = await client.notes.get_note(result.id)
note = await client.notes.get_note(int(result.id))
content = note.get("content", "")
accessible_results[index] = result
full_contents[index] = content