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:
co-authored by
Claude Opus 4.8
parent
c2c8dc1a08
commit
ab128bef5b
@@ -34,6 +34,7 @@ from qdrant_client.models import (
|
||||
FieldCondition,
|
||||
Filter,
|
||||
MatchAny,
|
||||
MatchText,
|
||||
MatchValue,
|
||||
Range,
|
||||
)
|
||||
@@ -194,6 +195,7 @@ def build_base_filter_conditions(
|
||||
doc_type: str | None = None,
|
||||
modified_after: int | None = None,
|
||||
modified_before: int | None = None,
|
||||
path_prefix: str | None = None,
|
||||
) -> list[Condition]:
|
||||
"""Build the common ``must`` conditions shared by every search algorithm.
|
||||
|
||||
@@ -209,6 +211,7 @@ def build_base_filter_conditions(
|
||||
2. ``build_ownership_filter(...)`` — ACL-aware ``owner_id``/``user_id`` scope.
|
||||
3. ``doc_type`` exact match — only when ``doc_type`` is truthy.
|
||||
4. ``modified_at`` range — only when at least one bound is given.
|
||||
5. ``file_path`` text match — only when ``path_prefix`` is given.
|
||||
|
||||
Args:
|
||||
user_id: Querying user.
|
||||
@@ -217,6 +220,16 @@ def build_base_filter_conditions(
|
||||
doc_type: Optional single document-type filter.
|
||||
modified_after: Inclusive lower bound on ``modified_at`` (Unix seconds).
|
||||
modified_before: Inclusive upper bound on ``modified_at`` (Unix seconds).
|
||||
path_prefix: Optional folder/path filter on the ``file_path`` payload
|
||||
field (ADR-027 Phase 2). Implemented with ``MatchText`` against the
|
||||
text-indexed ``file_path``. ``file_path`` is only written for
|
||||
``doc_type == "file"`` points, so a non-empty ``path_prefix``
|
||||
implicitly restricts results to files. NOTE the match semantics
|
||||
differ by backend: server Qdrant tokenizes (AND-of-tokens, so
|
||||
``"/Projects/Reports"`` matches files whose path contains both the
|
||||
``Projects`` and ``Reports`` tokens), while the local/embedded
|
||||
qdrant-client matches by substring containment. Both serve folder
|
||||
scoping; neither is a strict left-anchored prefix.
|
||||
|
||||
Returns:
|
||||
A list of Qdrant ``Condition`` objects for a parent ``must`` clause.
|
||||
@@ -242,4 +255,9 @@ def build_base_filter_conditions(
|
||||
)
|
||||
)
|
||||
|
||||
if path_prefix:
|
||||
conditions.append(
|
||||
FieldCondition(key="file_path", match=MatchText(text=path_prefix))
|
||||
)
|
||||
|
||||
return conditions
|
||||
|
||||
@@ -291,6 +291,7 @@ class SearchAlgorithm(ABC):
|
||||
accessible_owners: list[str] | None = None,
|
||||
modified_after: int | None = None,
|
||||
modified_before: int | None = None,
|
||||
path_prefix: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> list[SearchResult]:
|
||||
"""Execute search with the given parameters.
|
||||
@@ -312,6 +313,10 @@ class SearchAlgorithm(ABC):
|
||||
``accessible_owners`` (ADR-027). ``None`` ⇒ open-ended.
|
||||
modified_before: Optional inclusive upper bound on ``modified_at``
|
||||
(Unix seconds, UTC). ``None`` ⇒ open-ended.
|
||||
path_prefix: Optional folder/path filter on the ``file_path`` payload
|
||||
field (ADR-027 Phase 2). Only ``doc_type == "file"`` points carry
|
||||
``file_path``, so a non-empty value implicitly restricts results
|
||||
to files. ``None`` ⇒ no path filter.
|
||||
**kwargs: Algorithm-specific parameters
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -74,6 +74,7 @@ class BM25HybridSearchAlgorithm(SearchAlgorithm):
|
||||
accessible_owners: list[str] | None = None,
|
||||
modified_after: int | None = None,
|
||||
modified_before: int | None = None,
|
||||
path_prefix: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> list[SearchResult]:
|
||||
"""
|
||||
@@ -99,6 +100,8 @@ class BM25HybridSearchAlgorithm(SearchAlgorithm):
|
||||
seconds, UTC); ``None`` ⇒ open-ended (ADR-027).
|
||||
modified_before: Inclusive upper bound on ``modified_at`` (Unix
|
||||
seconds, UTC); ``None`` ⇒ open-ended (ADR-027).
|
||||
path_prefix: Folder/path filter on ``file_path`` (files only);
|
||||
``None`` ⇒ no path filter (ADR-027 Phase 2).
|
||||
**kwargs: Additional parameters (score_threshold override)
|
||||
|
||||
Returns:
|
||||
@@ -148,6 +151,7 @@ class BM25HybridSearchAlgorithm(SearchAlgorithm):
|
||||
doc_type=doc_type,
|
||||
modified_after=modified_after,
|
||||
modified_before=modified_before,
|
||||
path_prefix=path_prefix,
|
||||
)
|
||||
|
||||
query_filter = Filter(must=filter_conditions)
|
||||
|
||||
@@ -54,6 +54,7 @@ class SemanticSearchAlgorithm(SearchAlgorithm):
|
||||
accessible_owners: list[str] | None = None,
|
||||
modified_after: int | None = None,
|
||||
modified_before: int | None = None,
|
||||
path_prefix: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> list[SearchResult]:
|
||||
"""Execute semantic search using vector similarity.
|
||||
@@ -78,6 +79,8 @@ class SemanticSearchAlgorithm(SearchAlgorithm):
|
||||
seconds, UTC); ``None`` ⇒ open-ended (ADR-027).
|
||||
modified_before: Inclusive upper bound on ``modified_at`` (Unix
|
||||
seconds, UTC); ``None`` ⇒ open-ended (ADR-027).
|
||||
path_prefix: Folder/path filter on ``file_path`` (files only);
|
||||
``None`` ⇒ no path filter (ADR-027 Phase 2).
|
||||
**kwargs:
|
||||
- score_threshold (float): override the instance default
|
||||
|
||||
@@ -118,6 +121,7 @@ class SemanticSearchAlgorithm(SearchAlgorithm):
|
||||
doc_type=doc_type,
|
||||
modified_after=modified_after,
|
||||
modified_before=modified_before,
|
||||
path_prefix=path_prefix,
|
||||
)
|
||||
|
||||
# ACL pre-filter (design §11), opt-in via ACL_PREFILTER_ENABLED and OFF
|
||||
|
||||
Reference in New Issue
Block a user