Skip to content

feat(search): add optional temporal relevance boost#451

Open
mvanhorn wants to merge 2 commits intotobi:mainfrom
mvanhorn:osc/367-temporal-relevance-boost
Open

feat(search): add optional temporal relevance boost#451
mvanhorn wants to merge 2 commits intotobi:mainfrom
mvanhorn:osc/367-temporal-relevance-boost

Conversation

@mvanhorn
Copy link
Copy Markdown
Contributor

@mvanhorn mvanhorn commented Mar 21, 2026

Summary

Adds an opt-in recency parameter to search scoring that applies exponential time-decay to final scores. Documents from today score unchanged; at the configured half-life, scores drop gently; very old documents asymptote to score * (1 - weight) and are never zeroed out.

Why this matters

For temporal corpora like meeting notes, journals, and chat transcripts, document age is a meaningful relevance signal. "What did we discuss about the API?" should favor last week's meeting over January's.

  • #367 - detailed spec with formula, API design, and scope estimate
  • #331 - @funfunmyshan confirmed temporal decay is the one remaining scoring gap after investigating --explain output (BM25 normalization and dedup already handled)

Changes

  • applyRecencyDecay() in store.ts - exponential decay: score * (1 - weight + weight * 2^(-ageDays / halfLife))
  • recency option added to HybridQueryOptions, StructuredSearchOptions, and SDK SearchOptions
  • --recency-days <n> CLI flag on query command (default weight 0.15)
  • recencyDays parameter on MCP query tool
  • recencyDecay field in --explain output when recency is active
  • Batch-lookups modified_at from documents table (existing column, no schema changes)
  • Applied after RRF + reranker blend (step 7) - does not interfere with retrieval or reranking

Demo

Recency boost demo

With --recency-days 7, the March 12 document drops from 0.2000 to 0.1800 (10% reduction at ~9 days age with 7-day half-life), while today's document stays at 1.0000. The recencyDecay multiplier is visible in --explain output.

Testing

6 unit tests covering:

  • No decay for today's documents
  • Expected decay at half-life (30 days -> 7.5% reduction with default weight)
  • Asymptotic behavior (year-old docs -> 0.85, never zeroed out)
  • Steeper decay with shorter half-life
  • Empty modifiedAt returns score unchanged
  • Score proportions preserved across documents

All 35 tests pass (npx vitest run test/store.helpers.unit.test.ts).

This contribution was developed with AI assistance (Claude Code).

Closes #367

Add opt-in recency parameter that applies exponential time-decay to
search scores. Documents from today score unchanged; at the configured
half-life, scores drop by weight/2 (~7.5% with default weight 0.15);
very old documents asymptote to score * (1 - weight), never zeroed out.

Formula: score * (1 - weight + weight * 2^(-ageDays / halfLife))

Available via:
  - CLI: qmd query "test" --recency-days 30
  - SDK: store.search({ query: "test", recency: { halfLife: 30, weight: 0.15 } })
  - MCP: query tool recencyDays parameter

Applied after RRF + reranker blend (step 7 in the pipeline), so it
does not interfere with retrieval or reranking. Uses existing
modified_at timestamps from the documents table - no schema changes.

Closes tobi#367

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: optional temporal relevance boost in search scoring

1 participant