Skip to content

feat(search): add temporal-validity filter stage to pipeline (Goal 4)#534

Merged
memtomem merged 2 commits intomainfrom
feat/temporal-validity-filter
Apr 29, 2026
Merged

feat(search): add temporal-validity filter stage to pipeline (Goal 4)#534
memtomem merged 2 commits intomainfrom
feat/temporal-validity-filter

Conversation

@memtomem
Copy link
Copy Markdown
Owner

Summary

Implements RFC §Pipeline integration of the temporal-validity feature.
Goal 1+2+3 (#533) wired the metadata through the indexer; this PR adds
the actual filter stage so chunks with expired / not-yet-active
windows are dropped from search results.

  • New _apply_validity_filter helper in search/pipeline.py
    inclusive both ends, None = unbounded, (None, None) = always-valid.
  • Wired into SearchPipeline.search() at the β position: AND-combined with
    source/tag filter, before time-decay. Pipeline order:
    ... → rerank → source/tag filter → validity_filter → time-decay → MMR → ...
  • New as_of_unix: int | None = None parameter on the pipeline entry
    point. None falls back to int(time.time()); explicit values bypass
    cache read AND write.
  • CLAUDE.md "Search pipeline order" invariant updated.

Cache semantics (Option A — explicit bypass, default cached)

Path Cache read Cache write Rationale
as_of_unix=None (default) hit allowed populate slot up-to-cache_ttl staleness is invisible at RFC's date-only granularity
as_of_unix=<explicit> bypassed bypassed historical queries must not poison default-path slot

Granularity note in the helper docstring documents the cache-TTL ↔
date-boundary trade-off so future readers don't rediscover it.

Scope (atomic PR)

  • IN: filter helper + wiring + cache gating + invariant comment + 15 tests.
  • OUT: mem_search(as_of=...) MCP API, mm search --as-of CLI, mm list
    validity column (Goal 5+6), Web UI badge (Goal 7) — each separate PR.

Tests added (15)

TestApplyValidityFilter (10) — pure helper unit:

  • inside / before / after window
  • boundary-lower / boundary-upper inclusive
  • half-bounded (valid_from only, valid_to only)
  • always-valid (None, None)
  • order preserved
  • mixed survivor/reject input

TestValidityFilterPipelineWiring (5) — SearchPipeline.search integration:

  • default as_of_unix=None uses time.time() (monkeypatch fixed instant)
  • explicit historical as_of_unix filters window correctly
  • AND with source_filter — chunk must pass both
  • default path caches filtered result (bm25_search invoked once across two calls)
  • explicit as_of_unix bypasses cache read AND write

Local sweep: 3063 passed, 46 deselected (ollama). ruff clean. mypy
advisory clean for pipeline.py.

Test plan

  • ruff check + format
  • pytest -m "not ollama"
  • mypy advisory (pipeline.py)
  • CI green
  • Confirm filter does not regress search latency on a real DB
    (manual smoke after merge — Goal 5+6 will add the surface to test it
    end-to-end via mm search --as-of)

🤖 Generated with Claude Code

pandas-studio and others added 2 commits April 29, 2026 12:21
…Goal 4)

Implements RFC §Pipeline integration: chunks tagged with frontmatter
``valid_from`` / ``valid_to`` are now filtered out of search results
when the request's ``as_of`` timestamp falls outside their validity
window. ``(None, None)`` chunks remain always-valid (opt-in default).

Why this PR:
- Goal 1+2+3 (#533) wired the metadata through the indexer and made
  ``bm25_search`` / ``dense_search`` round-trip the new columns. The
  filter stage was the next atomic step — chunks already carry the
  window, but nothing was acting on it.
- Decoupled from Goal 5+6 (``mem_search(as_of=...)`` + ``mm search
  --as-of`` + ``mm list`` validity column) so review can focus on the
  filter semantics alone.

Pipeline placement (β position, AND with source/tag filter):
    ... → cross-encoder rerank → source/tag filter
        → validity_filter → time-decay → MMR → ...

Cache semantics (RFC §Comparison semantics + cache_ttl interaction):
- ``as_of_unix=None`` (default) → ``int(time.time())`` fallback;
  result lands in the existing TTL cache. Up-to-cache_ttl staleness
  near a date boundary is acceptable because RFC bounds are
  date-only (24h granularity).
- Explicit ``as_of_unix`` → bypasses BOTH cache read and cache write,
  so historical queries (e.g. "as of last week") never poison the
  default-path cache slot.

Tests added (15 new):
- ``TestApplyValidityFilter`` — pure helper unit (inside / before /
  after / boundary-lower / boundary-upper / half-bounded {lower, upper}
  / always-valid / order-preserved / mixed-input).
- ``TestValidityFilterPipelineWiring`` — pipeline integration via
  ``SearchPipeline.search`` with AsyncMock storage: default uses
  ``time.time()``; explicit ``as_of_unix`` filters historical window;
  AND with source filter; default-path caches; explicit-path bypasses
  cache read AND write.

CLAUDE.md "Search pipeline order" invariant updated to include the
new stage.

Goal 5+6 (``mem_search(as_of=...)`` API + CLI surfaces) and Goal 7
(Web UI badge) remain in separate PRs per RFC §Implementation sketch.

Co-Authored-By: Claude <[email protected]>
Review feedback on PR #534:

- Remove ``assert pipeline_mod._apply_validity_filter is not None`` from
  ``test_default_uses_current_time``. The symbol is imported a few lines
  above so it cannot be ``None`` — the assertion only verifies its own
  existence and adds no signal beyond what the chunk-content equality
  already proves.
- Add a docstring note to ``test_default_path_caches_filtered_result``
  explaining that pinning ``time.time`` to a constant intentionally
  disables the TTL boundary; the test asserts the cache reuse path
  (one storage call across two searches), not TTL expiry. Prevents a
  reader from misreading the monkeypatch as a TTL exercise.

Co-Authored-By: Claude <[email protected]>
@memtomem memtomem merged commit 330ad11 into main Apr 29, 2026
7 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 29, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants