feat(search): ADR-027 Phase 1 — modified-date range filter
Add a modified_after/modified_before date-range filter to semantic search, honoured on both the MCP tool path (BM25HybridSearchAlgorithm) and the dense-only visualization/API path (SemanticSearchAlgorithm) through one shared contract. - Promote modified_after/modified_before to explicit keyword params on the SearchAlgorithm ABC and both concrete algorithms; factor the shared placeholder+ownership+doc_type+date filter into access_filter.build_base_filter_conditions so new filters land in one place. - nc_semantic_search: accept RFC 3339 / ISO 8601 (or Unix seconds) bounds via utils.validation.parse_modified_timestamp; Annotated/Field constraints on the numeric args; explicit McpError guard for after > before. Thread the parsed bounds through the cross-app and per-doc_type dispatch. - /api/v1 search endpoints + viz route parse the same formats and 400 on bad or inverted ranges. - Add a modified_at INTEGER payload index to _PAYLOAD_INDEX_FIELDS; the idempotent _ensure_payload_indexes() startup path migrates existing collections with no content re-index. - Update ADR-027 to resolve the review feedback (validation placement, shared algorithm contract, deferral of nc_semantic_search_answer, payload index, RFC-3339-at-the-boundary rationale). Add unit tests. Refs ADR-027. 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
f6ab04b2d9
commit
c2c8dc1a08
@@ -37,7 +37,10 @@ from nextcloud_mcp_server.search.context import (
|
||||
get_chunk_with_context,
|
||||
)
|
||||
from nextcloud_mcp_server.search.verification import verify_search_results
|
||||
from nextcloud_mcp_server.utils.validation import is_valid_nextcloud_doc_id
|
||||
from nextcloud_mcp_server.utils.validation import (
|
||||
is_valid_nextcloud_doc_id,
|
||||
parse_modified_timestamp,
|
||||
)
|
||||
from nextcloud_mcp_server.vector.oauth_sync import (
|
||||
NotProvisionedError,
|
||||
get_user_client_basic_auth,
|
||||
@@ -198,6 +201,22 @@ async def unified_search(request: Request) -> JSONResponse:
|
||||
1.0,
|
||||
"score_threshold",
|
||||
)
|
||||
|
||||
# ADR-027 modified-date range filter. Accepts RFC 3339 / ISO 8601
|
||||
# datetimes or Unix seconds; normalized to int Unix seconds for the
|
||||
# numeric Range filter. Absent bound ⇒ open-ended.
|
||||
modified_after = parse_modified_timestamp(
|
||||
body.get("modified_after"), param_name="modified_after"
|
||||
)
|
||||
modified_before = parse_modified_timestamp(
|
||||
body.get("modified_before"), param_name="modified_before"
|
||||
)
|
||||
if (
|
||||
modified_after is not None
|
||||
and modified_before is not None
|
||||
and modified_after > modified_before
|
||||
):
|
||||
raise ValueError("modified_after must be <= modified_before")
|
||||
except ValueError as e:
|
||||
return JSONResponse({"error": str(e)}, status_code=400)
|
||||
|
||||
@@ -245,6 +264,8 @@ async def unified_search(request: Request) -> JSONResponse:
|
||||
limit=search_limit,
|
||||
doc_type=doc_type,
|
||||
accessible_owners=owners,
|
||||
modified_after=modified_after,
|
||||
modified_before=modified_before,
|
||||
)
|
||||
)
|
||||
# Sort, then cap to a fixed over-fetch budget before the result
|
||||
@@ -262,6 +283,8 @@ async def unified_search(request: Request) -> JSONResponse:
|
||||
user_id=user_id,
|
||||
limit=search_limit,
|
||||
accessible_owners=owners,
|
||||
modified_after=modified_after,
|
||||
modified_before=modified_before,
|
||||
)
|
||||
return results
|
||||
|
||||
@@ -413,6 +436,17 @@ async def vector_search(request: Request) -> JSONResponse:
|
||||
limit = min(body.get("limit", 10), 50) # Enforce max limit
|
||||
include_pca = body.get("include_pca", True)
|
||||
doc_types = body.get("doc_types") # Optional list of document types
|
||||
# ADR-027 modified-date range filter. Accepts RFC 3339 / ISO 8601
|
||||
# datetimes or Unix seconds; normalized to int Unix seconds. None ⇒ open.
|
||||
try:
|
||||
modified_after = parse_modified_timestamp(
|
||||
body.get("modified_after"), param_name="modified_after"
|
||||
)
|
||||
modified_before = parse_modified_timestamp(
|
||||
body.get("modified_before"), param_name="modified_before"
|
||||
)
|
||||
except ValueError as e:
|
||||
return JSONResponse({"error": str(e)}, status_code=400)
|
||||
|
||||
if not query:
|
||||
return JSONResponse(
|
||||
@@ -420,6 +454,16 @@ async def vector_search(request: Request) -> JSONResponse:
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
if (
|
||||
modified_after is not None
|
||||
and modified_before is not None
|
||||
and modified_after > modified_before
|
||||
):
|
||||
return JSONResponse(
|
||||
{"error": "modified_after must be <= modified_before"},
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
# Validate algorithm
|
||||
valid_algorithms = {"semantic", "bm25", "hybrid"}
|
||||
if algorithm not in valid_algorithms:
|
||||
@@ -455,6 +499,8 @@ async def vector_search(request: Request) -> JSONResponse:
|
||||
limit=limit,
|
||||
doc_type=doc_type,
|
||||
accessible_owners=owners,
|
||||
modified_after=modified_after,
|
||||
modified_before=modified_before,
|
||||
)
|
||||
)
|
||||
# Sort merged results by score and limit
|
||||
@@ -467,6 +513,8 @@ async def vector_search(request: Request) -> JSONResponse:
|
||||
user_id=user_id,
|
||||
limit=limit,
|
||||
accessible_owners=owners,
|
||||
modified_after=modified_after,
|
||||
modified_before=modified_before,
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
Reference in New Issue
Block a user