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
+22
View File
@@ -84,6 +84,17 @@ def configure_semantic_tools(mcp: FastMCP):
),
),
] = None,
path_prefix: Annotated[
str | None,
Field(
description=(
"Restrict to files under this folder/path "
"(e.g. '/Projects/Reports'). Matches the file_path of "
"indexed files only, so setting it implicitly limits "
"results to files. None = no path filter."
),
),
] = None,
) -> SemanticSearchResponse:
"""
Search Nextcloud content using BM25 hybrid search with cross-app support.
@@ -116,6 +127,9 @@ def configure_semantic_tools(mcp: FastMCP):
modified_before: Only return documents whose last-modified time is at or before this
instant. Same formats as modified_after. None = no upper bound (default). Must be
>= modified_after when both are supplied.
path_prefix: Restrict to files under this folder/path (e.g. "/Projects/Reports").
Matches the file_path of indexed files only — setting it implicitly limits results
to files. None = no path filter (default).
Returns:
SemanticSearchResponse with matching documents ranked by fusion scores.
@@ -186,6 +200,12 @@ def configure_semantic_tools(mcp: FastMCP):
)
)
# Treat a blank/whitespace path_prefix as "no filter" so an empty UI
# field doesn't filter out every result (ADR-027 Phase 2).
path_prefix = path_prefix.strip() if path_prefix else None
if not path_prefix:
path_prefix = None
# Expand the caller's identity to every owner whose content they
# have read access to via Nextcloud shares. Lets a user find files
# owners have shared with them without having to re-index those
@@ -232,6 +252,7 @@ def configure_semantic_tools(mcp: FastMCP):
accessible_owners=accessible_owners,
modified_after=modified_after_ts,
modified_before=modified_before_ts,
path_prefix=path_prefix,
)
all_results.extend(unverified_results)
else:
@@ -259,6 +280,7 @@ def configure_semantic_tools(mcp: FastMCP):
accessible_owners=accessible_owners,
modified_after=modified_after_ts,
modified_before=modified_before_ts,
path_prefix=path_prefix,
)
all_results.extend(unverified_results)