feat(search): ADR-027 Phase 2 — file-path filter

Add a path_prefix filter to semantic search, honoured on both the MCP tool and
the dense-only visualization/API paths through the shared filter contract.

- build_base_filter_conditions: append FieldCondition(file_path,
  MatchText(path_prefix)) when set. file_path is only on doc_type == "file"
  points, so a non-empty path_prefix implicitly restricts to files.
- Promote path_prefix to an explicit keyword param on the SearchAlgorithm ABC
  and both algorithms; thread it through nc_semantic_search (blank ⇒ no filter),
  the /api/v1 search endpoints, and the viz route.
- Add a file_path TEXT payload index to _PAYLOAD_INDEX_FIELDS (no content
  re-index; idempotent startup migration). MatchText tokenizes on server Qdrant
  and matches by substring on local/embedded qdrant-client — both serve folder
  scoping.
- Update ADR-027 (Phase 2 implemented; readiness table; semantics note). Tests.

Refs ADR-027 Phase 2. Deck #177.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-06-03 00:51:12 +02:00
co-authored by Claude Opus 4.8
parent c2c8dc1a08
commit ab128bef5b
11 changed files with 121 additions and 10 deletions
+5
View File
@@ -144,6 +144,9 @@ async def vector_visualization_search(request: Request) -> JSONResponse:
doc_types_param = request.query_params.get("doc_types", "")
doc_types = doc_types_param.split(",") if doc_types_param else None
# ADR-027 Phase 2 path filter (files only); blank ⇒ no filter.
path_prefix = (request.query_params.get("path_prefix") or "").strip() or None
# Parse ADR-027 modified-date range filter. Accepts RFC 3339 / ISO 8601
# datetimes or Unix seconds; normalized to int Unix seconds. Absent ⇒
# open-ended. Unparseable input or an inverted range returns 400.
@@ -232,6 +235,7 @@ async def vector_visualization_search(request: Request) -> JSONResponse:
accessible_owners=accessible_owners,
modified_after=modified_after,
modified_before=modified_before,
path_prefix=path_prefix,
)
all_results.extend(unverified_results)
else:
@@ -254,6 +258,7 @@ async def vector_visualization_search(request: Request) -> JSONResponse:
accessible_owners=accessible_owners,
modified_after=modified_after,
modified_before=modified_before,
path_prefix=path_prefix,
)
all_results.extend(unverified_results)
# Sort by score, then cap to the same limit*2 over-fetch budget