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:
co-authored by
Claude Opus 4.8
parent
b91af923d2
commit
de6c4b360d
@@ -32,7 +32,10 @@ from nextcloud_mcp_server.models.semantic import (
|
||||
from nextcloud_mcp_server.observability.metrics import (
|
||||
instrument_tool,
|
||||
)
|
||||
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.bm25_hybrid import BM25HybridSearchAlgorithm
|
||||
from nextcloud_mcp_server.search.context import get_chunk_with_context
|
||||
from nextcloud_mcp_server.search.verification import verify_search_results
|
||||
@@ -88,6 +91,7 @@ def configure_semantic_tools(mcp: FastMCP):
|
||||
str | None,
|
||||
Field(
|
||||
description=(
|
||||
"Deprecated single-folder filter; prefer path_prefixes. "
|
||||
"Restrict to files under this folder/path "
|
||||
"(e.g. '/Projects/Reports'). Matches the file_path of "
|
||||
"indexed files only, so setting it implicitly limits "
|
||||
@@ -95,6 +99,18 @@ def configure_semantic_tools(mcp: FastMCP):
|
||||
),
|
||||
),
|
||||
] = None,
|
||||
path_prefixes: Annotated[
|
||||
list[str] | None,
|
||||
Field(
|
||||
description=(
|
||||
"Restrict to files under any of these folders/paths "
|
||||
"(e.g. ['/Projects/Reports', '/Shared/Specs']). Folders are "
|
||||
"OR-ed together. Matches the file_path of indexed files "
|
||||
"only, so setting it implicitly limits results to files. "
|
||||
"None or empty = no path filter."
|
||||
),
|
||||
),
|
||||
] = None,
|
||||
) -> SemanticSearchResponse:
|
||||
"""
|
||||
Search Nextcloud content using BM25 hybrid search with cross-app support.
|
||||
@@ -127,9 +143,12 @@ 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).
|
||||
path_prefix: Deprecated single-folder filter; prefer path_prefixes. Restrict to files
|
||||
under this folder/path (e.g. "/Projects/Reports"). Folded into path_prefixes.
|
||||
path_prefixes: Restrict to files under any of these folders/paths (OR-ed), e.g.
|
||||
["/Projects/Reports", "/Shared/Specs"]. Matches the file_path of indexed files
|
||||
only — setting it implicitly limits results to files. None/empty = no path filter
|
||||
(default).
|
||||
|
||||
Returns:
|
||||
SemanticSearchResponse with matching documents ranked by fusion scores.
|
||||
@@ -200,11 +219,10 @@ 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
|
||||
# Merge the legacy single path_prefix and the path_prefixes list into one
|
||||
# cleaned list, dropping blank/whitespace entries so an empty UI field
|
||||
# doesn't filter out every result (ADR-027 Phase 2).
|
||||
folder_prefixes = normalize_path_prefixes(path_prefix, path_prefixes)
|
||||
|
||||
# Expand the caller's identity to every owner whose content they
|
||||
# have read access to via Nextcloud shares. Lets a user find files
|
||||
@@ -252,7 +270,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,
|
||||
path_prefixes=folder_prefixes,
|
||||
)
|
||||
all_results.extend(unverified_results)
|
||||
else:
|
||||
@@ -280,7 +298,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,
|
||||
path_prefixes=folder_prefixes,
|
||||
)
|
||||
all_results.extend(unverified_results)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user