feat: add Weibull decay and rerank pipeline for search results - #1032
feat: add Weibull decay and rerank pipeline for search results#1032zackchiutw wants to merge 1 commit into
Conversation
Introduce a config-driven post-retrieval rerank pipeline that adjusts search ranking beyond raw cosine distance. Four stages are supported: Weibull time-decay, keyword overlap boost, emotional_weight boost, and optional LLM reranking via Anthropic API. All stages are independently toggleable via ~/.mempalace/config.json and backward-compatible — no config means identical behavior to before. The pipeline runs after upstream's closet boost + BM25 hybrid rank so those signals are preserved. When any stage is enabled, search_memories over-fetches 3x candidates so the reorder has headroom, applies the max_distance filter on the post-rerank fused_distance, and returns a reranked=True flag for transparency. Layer1 now supports optional read-time decay (k=1.2, λ=365, floor=0.6) and Layer3 is refactored to use the unified search_memories() output. Knowledge graph query_entity() accepts apply_decay to apply the same Weibull curve to stored confidence values at read time. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4520e22 to
b3b9881
Compare
Scanned all 233 open upstream PRs today against our open PRs and fork-ahead / planned-work items. Findings merged into README: - P2 (decay) and P3 Tier-0 (LLM rerank): both covered by MemPalace#1032 (@zackchiutw, MERGEABLE, 2026-04-19 — Weibull decay + 4-stage rerank pipeline). Older simpler version at #337. Dropped as fork work; watching MemPalace#1032. - P7 (alternative storage): formally out of scope. RFC 001 MemPalace#743 (@igorls) defines the plugin contract; four backend PRs already in flight (MemPalace#700, #381 Qdrant; MemPalace#574, MemPalace#575 LanceDB). Fork consumes, does not rebuild. - P0 (multi-label tags): still fork/upstream candidate. MemPalace#1033 (@zackchiutw) ships adjacent privacy-tag + progressive disclosure but not the full multi-label scheme. - Merged MemPalace#1023 section acknowledges complementary MemPalace#976 (felipetruman) which adds broader mine_global_lock() + HNSW num_threads pin. Gives future-us a map so we don't re-file MemPalace#1036-style duplicates.
Changes (all 7 approved items): 1. Tighten line-15 status line to keep only non-redundant bits (test count, Discussion MemPalace#1017, issues link) 2. Remove "Fork Changes / Headlines" subsection — duplicated the three differentiators already in the lead paragraph 3. Remove "Planned work / Done" — 5 bullets of PR status already in the "Open upstream PRs" table 4. Trim P2 "original design notes" — MemPalace#1032 is MERGEABLE; kept the prune CLI fork-opportunity note 5. Tighten MemPalace#1023 row in "Merged upstream" — moved the MemPalace#976 mapping detail to a single compact sentence 6. Collapse "Pulled in from upstream v3.3.1" from 6 bullets to one paragraph pointing at the release notes 7. Prune three oldest "Superseded by upstream" bullets (epsilon mtime, .jsonl, max_distance — all upstream-authored or shipped by upstream before the fork claimed them as contributions) 8. Drop P7 "Original fork-mode design notes" (flashcard/AAAK/ diary modes) now that the section explicitly marks P7 as dropped fork work Net: 345 → 316 lines. No unique info removed; anything cut is either duplicated elsewhere in the same README or retrievable from the cited PR/release link.
…CLI (#158) (#254) * feat(search): recency decay weighting + mempalace prune --stale-days CLI (#158) Recency is an opt-in, recall-preserving ranking signal: a bounded exponential-decay distance shift on drawer age (mempalace.recency), gated off by default via PALACE_RECENCY_BOOST, half-life tunable via PALACE_RECENCY_HALFLIFE_DAYS. The shift can reorder neighbours but is capped so it never displaces a relevant drawer out of the result set. `mempalace prune --stale-days N` removes drawers older than N days from an optional wing/room scope. Dry-run by default; deletion requires --confirm. Drawers with no parseable filed_at are treated as ageless and are never pruned. Fully local — no network, no external API. Upstream tracks Weibull decay in MemPalace#1032 (informational). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: record recency-decay + prune CLI in fork manifest (#158) Add fork-changes.yaml entry; regenerate FORK_CHANGELOG, README, llms-full, and python-api docs. Bump README test count 3404 -> 3424. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
|
Went through this in detail because we run a very similar patch in production (listwise LLM rerank on top of a vector+BM25 candidate pool) and wanted to compare notes. A few observations, offered as independent data since our implementation differs from stage 4 here. On the idea itself: we have measurement that LLM rerank at the retrieval layer is worth doing. On a private 200-question eval it took us from 129/200 to 161/200 correct (sign test p=0.00004), and answer-in-top-3 from 40% to 63%. On the public LongMemEval held-out 450, our hybrid retrieval + local-model rerank hits R@5 = 1.000 against the published 0.984 no-rerank baseline. That's a different implementation than Three things from our production experience that might be worth folding in before this lands:
One smaller note: the diff wires rerank into Happy to share more detail on our listwise implementation/eval setup if useful. |
Summary
Introduces a config-driven post-retrieval rerank pipeline that adjusts search ranking beyond raw cosine distance. Four stages, independently toggleable via
~/.mempalace/config.json:emotional_weightmetadata get a small boostAll stages are off by default. No config = identical behavior to before. When any stage is enabled,
search_memoriesover-fetches 3× candidates, runs the pipeline after the existing closet-boost + BM25 hybrid rank (both preserved), re-appliesmax_distanceagainst the post-rerankfused_distance, and flags the response withreranked: Truefor transparency.Why
Raw cosine distance treats every memory as if it were written yesterday. For a memory system where identity and recency matter, that's a regression trap — old notes crowd out fresh ones. The pipeline is modular so users can tune the trade-off per palace (e.g., λ=90 days for a work palace, λ=365 for a personal history wing).
What changes
mempalace/reranker.py(new)fused_distancefieldmempalace/searcher.pyrerank=Trueparam onsearch_memories; pipeline runs after_hybrid_rankso closet/BM25 signals are preserved;_load_rerank_config+_apply_rerankhelpers keep complexity under the C901 thresholdmempalace/config.pyrerank_configproperty readingconfig.json["rerank"]mempalace/mcp_server.pyrerankparam + schema entry onmempalace_searchmempalace/layers.pysearch_memories()dictmempalace/knowledge_graph.pyquery_entity(apply_decay=...)applies the same Weibull curve to stored confidence at read timetests/test_reranker.py(new)tests/test_layers.pyExample config
{ "rerank": { "weibull_decay": {"enabled": true, "k": 1.5, "lambda": 90, "floor": 0.3}, "keyword_boost": {"enabled": true, "weight": 0.30}, "importance_boost":{"enabled": false, "weight": 0.15}, "llm_rerank": {"enabled": false, "model": "claude-haiku-4-5-20251001", "top_k": 10} } }Test plan
pytest tests/test_reranker.py tests/test_searcher.py tests/test_layers.py tests/test_config.py tests/test_mcp_server.py— 186 passedruff check .— all checks passedruff format --checkon modified files — cleanrerankkey in config,search_memoriesreturns identical output (norerankedflag, no new fields in response)mempalace_searchMCP tool acceptsrerank: falseto bypass the pipeline when desiredNotes for reviewers
rerankis enabled but no stages are configured, the pipeline is an identity function and thererankedflag stays falsellm_rerankstage is gated behind a local import ofanthropicand skipped if unavailable🤖 Generated with Claude Code