feat(search): support multiple folders in the semantic-search path filter

Extend the ADR-027 Phase 2 path filter from a single path_prefix to a
list of folders. The new normalize_path_prefixes() helper is the single
source of truth for trimming, dropping blanks, and de-duplicating, and
folds the legacy single path_prefix into the list for backward
compatibility.

build_base_filter_conditions() adds one MatchText to the must clause for
a single folder (unchanged shape) and OR-s multiple folders via a nested
Filter(should=[...]) so a file under any selected folder matches while
still AND-ing against the ACL/doc_type/date conditions.

path_prefixes is threaded through every search surface: the
nc_semantic_search MCP tool, the visualization API (JSON body), and the
viz route (CSV query param). The Astrolabe frontend folder picker that
produces these lists ships in a companion astrolabe PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-06-03 12:51:37 +02:00
co-authored by Claude Opus 4.8
parent b91af923d2
commit de6c4b360d
9 changed files with 269 additions and 50 deletions
+14 -5
View File
@@ -33,7 +33,10 @@ from nextcloud_mcp_server.search import (
BM25HybridSearchAlgorithm,
SemanticSearchAlgorithm,
)
from nextcloud_mcp_server.search.access_filter import list_accessible_owners
from nextcloud_mcp_server.search.access_filter import (
list_accessible_owners,
normalize_path_prefixes,
)
from nextcloud_mcp_server.search.context import (
get_chunk_bbox_and_page_from_qdrant,
get_chunk_with_context,
@@ -144,8 +147,14 @@ 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
# ADR-027 Phase 2 path filter (files only); blank ⇒ no filter. Accept a
# comma-separated path_prefixes list (multi-folder) plus the legacy single
# path_prefix; normalize_path_prefixes drops blanks and de-dupes.
path_prefix = request.query_params.get("path_prefix")
path_prefixes = normalize_path_prefixes(
path_prefix,
(request.query_params.get("path_prefixes") or "").split(","),
)
# Parse ADR-027 modified-date range filter. Accepts RFC 3339 / ISO 8601
# datetimes or Unix seconds; normalized to int Unix seconds. Absent ⇒
@@ -235,7 +244,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,
path_prefixes=path_prefixes,
)
all_results.extend(unverified_results)
else:
@@ -258,7 +267,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,
path_prefixes=path_prefixes,
)
all_results.extend(unverified_results)
# Sort by score, then cap to the same limit*2 over-fetch budget