Skip to content

Add recency bias to semantic search (timestamps in prompts + time-decay reranking) - #930

Open
crspeller wants to merge 5 commits into
masterfrom
cursor/recency-bias-search-5df5
Open

Add recency bias to semantic search (timestamps in prompts + time-decay reranking)#930
crspeller wants to merge 5 commits into
masterfrom
cursor/recency-bias-search-5df5

Conversation

@crspeller

@crspeller crspeller commented Jul 27, 2026

Copy link
Copy Markdown
Member

Summary

Semantic search currently ranks results purely by vector similarity, so stale posts can outrank fresh ones and the answering LLM never sees when a message was written. This PR adds a recency bias in two layers:

1. Expose post timestamps to the LLM (always on)

  • search.RAGResult now carries CreateAt; prompts/search_results.tmpl renders it as a time="RFC3339" attribute (omitted when the timestamp is unknown) and search_system.tmpl instructs the model to prefer recent messages when results conflict, without inferring recency for undated messages.
  • The MCP search_posts tool output and the /api/v1/search/raw endpoint (plus the external MCP server's HTTP client) now include the post timestamp too.
  • New format.TimeFromMillis helper shared by format.WritePost and the prompt template func formatTime.

2. Opt-in time-decay reranking (over-fetch + rerank)

  • When enabled, CompositeSearch.Search over-fetches candidates by pure ANN similarity (4× limit, bounded to [20, 200], offset folded in), reranks them in memory with a floored exponential half-life decay, then applies the caller's offset/limit window to the reranked order:

    adjusted = rawScore × (floor + (1 − floor) × 0.5^(ageDays / halfLifeDays))

  • Defaults: 7-day half-life, 0.7 floor — modeled on Elasticsearch function_score exp-decay and Azure AI Search freshness boosting. The floor bounds worst-case demotion so an old canonical answer (raw 0.9 → 0.63) still outranks a weak fresh match (< 0.63).

  • The returned Score stays the raw similarity: MinScore filtering and the relevance surfaced to callers remain similarity-based; recency influences ordering only. Ties break by newer CreateAt.

  • The pure ORDER BY embedding <-> $1 inner query is unchanged, so the HNSW index stays fully usable.

  • Deep pagination keeps working: the store's per-search cap is now a shared contract (embeddings.MaxSearchResults, used by pgvector), and when a requested window extends past the rerankable candidate pool the search falls back to store-side pagination in raw similarity order instead of silently truncating deep pages.

  • New config fields on EmbeddingSearchConfig (recencyBiasEnabled, recencyHalfLifeDays, recencyFloor) with defaulting/clamping via GetRecencyBiasSettings(), mirrored in the system console Embedding Search panel. These are query-time-only settings; changing them never requires a reindex.

Tests

  • embeddings/recency_test.go: decay multiplier math, candidate pool sizing, config resolution, and rerank ordering (fresh overtakes decayed, floor guarantee, unknown-timestamp handling, recency tie-break, raw score preservation).
  • embeddings/composite_test.go: end-to-end rerank through CompositeSearch.Search — over-fetch options, reranked order, offset/limit applied post-rerank, disabled passthrough, and store-pagination fallback for windows beyond the search cap.
  • embeddings/integration_test.go (TestRecencyBiasEndToEnd): full path against real pgvector — two posts with identical content (identical similarity via the deterministic mock provider) where the newer post must rank first, proving the ordering comes from the recency rerank.
  • search/search_test.go: CreateAt enrichment and time="…" rendering (including omission for missing/negative timestamps) through the real prompt templates.
  • format, mcpserver/tools tests for the new timestamp plumbing.

Verified locally: go vet, golangci-lint (0 issues), mattermost-govet license check, webapp tsc/eslint/jest, i18n and e2e-shard drift checks, and the Go test suite including the postgres pgvector suite against a real container.

Ticket Link

NONE

Screenshots

N/A (three new fields in the existing Embedding Search system console panel)

Release Note

Semantic search results now include post timestamps in the LLM prompt so answers can prefer recent information. Added optional recency bias for semantic search: when enabled, results are reranked with an exponential time decay (configurable half-life and floor) so recent posts rank higher. Added config settings embeddingSearchConfig.recencyBiasEnabled, embeddingSearchConfig.recencyHalfLifeDays, and embeddingSearchConfig.recencyFloor.
Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features

    • Added optional recency bias for semantic/embedding search, with configurable enablement, half-life (days), and recency floor.
    • Search responses now expose creation timestamps (including raw search), and prompts can include per-message time attributes to emphasize newer information.
  • Improvements

    • Timestamp rendering is now consistent (RFC3339 UTC), improving freshness/context across results and prompts.
  • Bug Fixes

    • Semantic search results now correctly include creation timestamps for downstream display and logic.

cursoragent and others added 3 commits July 23, 2026 13:42
Co-authored-by: Christopher Speller <crspeller@users.noreply.github.com>
Co-authored-by: Christopher Speller <crspeller@users.noreply.github.com>
Co-authored-by: Christopher Speller <crspeller@users.noreply.github.com>
@cursor

cursor Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Cursor Agent can help with this pull request. Just @cursor in comments and I'll start working on changes in this branch.
Learn more about Cursor Agents

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 944452c9e9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread embeddings/composite.go
@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown

🤖 LLM Evaluation Results

OpenAI

⚠️ Overall: 21/28 tests passed (75.0%)

Provider Total Passed Failed Pass Rate
⚠️ OPENAI 28 21 7 75.0%

❌ Failed Evaluations

Show 7 failures

OPENAI

1. TestReactEval/[openai]_react_cat_message

  • Score: 0.00
  • Rubric: The word/emoji is a cat emoji or a heart/love emoji
  • Reason: The output is the text "smiley_cat", which is not itself a cat emoji (e.g., 🐱/😺) or a heart/love emoji (e.g., ❤️/😍).

2. TestConversationMentionHandling/[openai]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: is a list of bugs
  • Reason: The output asks the user to paste bug reports and describes how it will format them, but it does not actually provide a list of bugs.

3. TestConversationMentionHandling/[openai]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: includes a description of each bug
  • Reason: The output asks the user to paste bug reports and lists intended table columns, but it does not actually include any bug entries or descriptions of each bug.

4. TestConversationMentionHandling/[openai]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: attributes each bug to a user
  • Reason: The output asks the user to paste bug reports and proposes a table including a 'Reported by' column, but it does not actually attribute any specific bug to a user.

5. TestConversationMentionHandling/[openai]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: attributes the bug about trying to save without a color and the save button not doing anything to @maria.nunez
  • Reason: The output requests the user to paste bug reports and does not mention the specific bug ('trying to save without a color' and 'save button not doing anything') nor attribute it to @maria.nunez.

6. TestConversationMentionHandling/[openai]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: the bug about the end user being able to change channel banner is attributed to @maria.nunez
  • Reason: The output asks the user to paste bug reports and does not mention any specific bug, does not attribute the channel banner change bug to anyone, and does not contain the handle @maria.nunez.

7. TestDirectMessageConversations/[openai]_bot_dm_tool_introspection

  • Score: 0.00
  • Rubric: mentions Github and refers to the documentation
  • Reason: The output refers to documentation (docs.mattermost.com) but does not mention GitHub. Since the rubric requires both mentioning GitHub and referring to the documentation, it fails.

Anthropic

⚠️ Overall: 21/28 tests passed (75.0%)

Provider Total Passed Failed Pass Rate
⚠️ ANTHROPIC 28 21 7 75.0%

❌ Failed Evaluations

Show 7 failures

ANTHROPIC

1. TestReactEval/[anthropic]_react_cat_message

  • Score: 0.00
  • Rubric: The word/emoji is a cat emoji or a heart/love emoji
  • Reason: The output is the text string "heart_eyes_cat", not an actual cat emoji (e.g., 😺/🐱) or a heart/love emoji (e.g., ❤️/😍).

2. TestConversationMentionHandling/[anthropic]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: is a list of bugs
  • Reason: The output does not provide a list of bugs; it states inability to access bug reports and suggests steps to find them, but includes no bug entries.

3. TestConversationMentionHandling/[anthropic]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: includes a description of each bug
  • Reason: The output explains it cannot access bug reports and suggests searching or sharing details, but it does not provide any bug descriptions.

4. TestConversationMentionHandling/[anthropic]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: attributes each bug to a user
  • Reason: The output contains no bug list at all and does not attribute any bugs to any users; it only states lack of access and suggests how to find bugs.

5. TestConversationMentionHandling/[anthropic]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: attributes the bug about trying to save without a color and the save button not doing anything to @maria.nunez
  • Reason: The output does not mention the specific bug about saving without a color or the save button doing nothing, and it does not attribute any bug to @maria.nunez. It only states lack of access and suggests ways to search or share details.

6. TestConversationMentionHandling/[anthropic]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: the bug about the end user being able to change channel banner is attributed to @maria.nunez
  • Reason: The output does not mention the specific bug about an end user being able to change the channel banner, nor does it attribute that bug to @maria.nunez.

7. TestDirectMessageConversations/[anthropic]_bot_dm_tool_introspection

  • Score: 0.50
  • Rubric: mentions Github and refers to the documentation
  • Reason: The output refers to documentation (docs.mattermost.com) but does not mention GitHub anywhere, so it does not satisfy the rubric requiring both GitHub and a documentation reference.

This comment was automatically generated by the eval CI pipeline.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Semantic search now supports configurable recency-biased reranking. Creation timestamps flow through search APIs and MCP results into formatted prompts, while the embedding search administration panel exposes recency settings.

Changes

Semantic search recency and timestamps

Layer / File(s) Summary
Recency reranking engine
embeddings/recency.go, embeddings/composite.go, embeddings/composite_test.go
Adds time-decay weighting, candidate over-fetching, stable reranking, pagination handling, and tests for enabled and disabled recency behaviour.
Recency configuration and integration
embeddings/embeddings.go, search/embeddings.go, embeddings/integration_test.go, mcpserver/eval_helpers_test.go, search/search_eval_test.go, postgres/pgvector.go
Adds recency configuration defaults, wires settings into composite search construction, validates end-to-end ranking, and centralizes the vector-store result cap.
Search timestamp propagation
api/api_search.go, search/search.go, mcpserver/tools/search.go, mcpserver/tools/search_http.go, mcpserver/tools/search_http_test.go, search/search_test.go
Carries CreateAt timestamps through raw search responses, enriched results, semantic results, and HTTP search conversions.
Timestamp formatting in prompts
format/format.go, format/format_test.go, llm/prompts.go, prompts/*.tmpl, search/search_test.go
Adds Unix-millisecond to RFC3339 formatting and conditionally includes message time attributes in prompts.
Recency settings UI
webapp/src/components/system_console/embedding_search/*, webapp/src/i18n/en.json
Adds recency toggle and numeric controls with client-side normalization, defaults, types, and translations.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Admin
  participant SearchConfig
  participant CompositeSearch
  participant VectorStore
  participant PromptBuilder
  Admin->>SearchConfig: Configure recency settings
  SearchConfig->>CompositeSearch: Pass resolved settings
  CompositeSearch->>VectorStore: Fetch expanded candidates
  VectorStore-->>CompositeSearch: Return timestamped results
  CompositeSearch-->>PromptBuilder: Return reranked results
  PromptBuilder-->>Admin: Render optional message times
Loading

Suggested labels: Setup Cloud Test Server

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: recency bias for semantic search via timestamp exposure and time-decay reranking.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/recency-bias-search-5df5

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@embeddings/recency.go`:
- Around line 90-101: Update recencyFetchLimit to cap the computed
offset-plus-pool value at the search limit enforced by postgres.PGVector.Search,
using a shared maximum-search constant or contract if available. Preserve the
existing zero-limit behavior and candidate-pool calculations while ensuring the
returned fetch limit never exceeds the store’s hard cap.

In `@prompts/search_results.tmpl`:
- Line 1: Update the time attribute condition in the Results range to evaluate
the formatted timestamp returned by formatTime rather than the raw .CreateAt
value, so negative timestamps omit the attribute when formatting yields an empty
string. Preserve the existing formatTime output and attribute rendering for
valid timestamps.

In `@prompts/search_system.tmpl`:
- Line 11: Update the message timestamp guidance near the recency rule to state
that timestamps are optional and should only be used when present. Preserve the
instruction to prefer newer messages when timestamps are available, and avoid
implying recency for messages without timestamps.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: e30a6016-df75-4a16-9b62-69169f4616b0

📥 Commits

Reviewing files that changed from the base of the PR and between aaa3e4f and 944452c.

📒 Files selected for processing (23)
  • api/api_search.go
  • embeddings/composite.go
  • embeddings/composite_test.go
  • embeddings/embeddings.go
  • embeddings/integration_test.go
  • embeddings/recency.go
  • embeddings/recency_test.go
  • format/format.go
  • format/format_test.go
  • llm/prompts.go
  • mcpserver/eval_helpers_test.go
  • mcpserver/tools/search.go
  • mcpserver/tools/search_http.go
  • mcpserver/tools/search_http_test.go
  • prompts/search_results.tmpl
  • prompts/search_system.tmpl
  • search/embeddings.go
  • search/search.go
  • search/search_eval_test.go
  • search/search_test.go
  • webapp/src/components/system_console/embedding_search/embedding_search_panel.tsx
  • webapp/src/components/system_console/embedding_search/types.tsx
  • webapp/src/i18n/en.json

Comment thread embeddings/recency.go
Comment thread prompts/search_results.tmpl Outdated
Comment thread prompts/search_system.tmpl Outdated
cursoragent and others added 2 commits July 28, 2026 13:00
Co-authored-by: Christopher Speller <crspeller@users.noreply.github.com>
…tional

Co-authored-by: Christopher Speller <crspeller@users.noreply.github.com>
@crspeller
crspeller requested a review from nickmisasi July 28, 2026 13:36
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.

3 participants