Recall Tuning
AutoMem ships with a ranking pipeline that works well out of the box — apart from the metadata sidecar weight SEARCH_WEIGHT_METADATA (new in v0.16.0 and active by default), the v0.16.0 ranking knobs on this page default to no behavior change from prior releases, while the vector candidate pool and artifact-type exclusion added in v0.16.1 are on by default. This guide is for operators who want to tune recall for a specific corpus: bias toward freshness, tighten a tag-scoped pool against off-topic memories, or adjust how much structured metadata and relationships contribute.
For how the score is computed (the 10 weighted components and the merge/rank flow), read Hybrid Search first — this page assumes that model. For the request-time parameters (recency_bias, state_mode, min_score), see Recall Operations. For the full variable list with defaults, see Configuration Reference.
Introduced across AutoMem v0.16.0 and v0.16.1. All knobs are environment variables read at startup; several also expose a per-request override on /recall.
The ranking pipeline at a glance
Section titled “The ranking pipeline at a glance”flowchart TD
Q["Query + tags"] --> Pool["Candidate pool<br/>(vector + keyword + relation)"]
Pool --> Score["Weighted score<br/>10 components → sum 1.95"]
Score --> Gate{"RECALL_RELEVANCE_GATE<br/>> 0?"}
Gate -->|"yes: off-topic result"| Damp["Scale query-independent<br/>components by evidence/gate"]
Gate -->|"no / on-topic"| Rerank{"recency_bias<br/>active?"}
Damp --> Rerank
Rerank -->|"yes"| Temporal["+ SEARCH_WEIGHT_TEMPORAL<br/>× relative recency"]
Rerank -->|"no"| Norm["final_score<br/>(not normalized)"]
Temporal --> Norm
Norm --> Out["Ranked results"]
Three stages are tunable independently: the component weights (what evidence counts and how much), the relevance gate (a within-pool guard against off-topic-but-important memories), and the recency re-rank (an optional freshness bias on top of the base score).
Component weights
Section titled “Component weights”The base final_score is a weighted sum of ten components plus an unweighted context bonus (below). It is not normalized to [0.0, 1.0], so a strong match can score above 1.0. Default weights (they sum to 1.95):
| Weight variable | Default | Contribution |
|---|---|---|
SEARCH_WEIGHT_VECTOR | 0.35 | Semantic similarity (Qdrant) |
SEARCH_WEIGHT_KEYWORD | 0.35 | Lexical match |
SEARCH_WEIGHT_METADATA | 0.35 | Structured metadata field match |
SEARCH_WEIGHT_RELATION | 0.25 | Graph relationship strength |
SEARCH_WEIGHT_TAG | 0.20 | Query-token hits against tag and metadata terms |
SEARCH_WEIGHT_EXACT | 0.20 | Whole-query exact match against metadata terms |
SEARCH_WEIGHT_RECENCY | 0.10 | Age decay (see below) |
SEARCH_WEIGHT_IMPORTANCE | 0.10 | User-assigned importance |
SEARCH_WEIGHT_CONFIDENCE | 0.05 | Classification certainty |
SEARCH_WEIGHT_RELEVANCE | 0.00 | Consolidation decay relevance_score (age, access, relationships, importance, confidence); opt-in |
The context bonus is not one of these ten weights. The context_tags soft boost, together with the context_types and priority_ids hints and the automatic language, style, and context-label hints, feeds a separate additive context_bonus term that is added to the sum outside the weight table and is never gated. Its per-signal amounts are hardcoded rather than configurable: 0.45 for a tag hit, 0.25 for a type hit, 0.2 for a keyword hit, and 0.9 for a priority_ids match.
Two more additive terms also sit outside the ten weights: the recency re-rank below adds SEARCH_WEIGHT_TEMPORAL × relative recency when it runs, and memories added by entity expansion (expand_entities) get a flat +0.15.
Raise a weight to make that signal dominate; lower it to mute noise. Changing one weight shifts the balance against the others. Because the weighted sum is not normalized, it also changes the absolute scores, so re-check any min_score or RECALL_MIN_SCORE threshold after tuning.
Recency: age decay vs. relative re-rank
Section titled “Recency: age decay vs. relative re-rank”AutoMem has two independent freshness mechanisms. Don’t confuse them:
1. Age decay (always on)
Section titled “1. Age decay (always on)”SEARCH_WEIGHT_RECENCY (0.10) contributes an absolute age-decay score to every result. Its shape is set by:
| Variable | Default | Notes |
|---|---|---|
SEARCH_RECENCY_WINDOW_DAYS | 180 | Decay window. Non-positive values fall back to 180. |
SEARCH_RECENCY_CURVE | linear | linear → score reaches 0 at the window edge; exp → the window acts as a half-life. Invalid values fall back to linear. |
Use exp when recent memories should stay strongly ranked well inside the window but old ones shouldn’t hit exactly zero; use a shorter window for fast-moving corpora.
2. Relative recency re-rank (opt-in)
Section titled “2. Relative recency re-rank (opt-in)”A separate, post-scoring re-rank (issues #158/#159). When active, candidate timestamps are min-max normalized across the current candidate set and SEARCH_WEIGHT_TEMPORAL × that relative recency is added to each final score.
| Variable | Default | Notes |
|---|---|---|
RECALL_RECENCY_BIAS | off | off never re-ranks; on always; auto only when the query expresses temporal intent (“latest”, “current”, …). Invalid values fall back to off. |
SEARCH_WEIGHT_TEMPORAL | 0.10 | Re-rank bonus weight. Inert unless the re-rank runs. |
The re-rank is inert by default — SEARCH_WEIGHT_TEMPORAL changes nothing until RECALL_RECENCY_BIAS is on/auto or a request passes the recency_bias param. Set RECALL_RECENCY_BIAS=auto to make “what’s the latest…” style queries prefer fresh memories without penalizing topical queries.
The relevance gate
Section titled “The relevance gate”RECALL_RELEVANCE_GATE (default 0.0 = disabled) addresses issue #130: a high-importance but off-topic memory riding query-independent score (importance, confidence, recency, tag overlap, consolidation relevance) to the top of a tag-scoped pool.
When the gate is set and the query yields at least one keyword token, a result whose best query-topical evidence — the max of its vector, keyword, metadata, and exact-match components — falls below the threshold has its query-independent components scaled by evidence / gate. Words under three characters and common stopwords don’t count as keyword tokens, so a tags-only or embedding-only request, or a query made only of such words, is never gated. This is a linear ramp, not a cliff: a barely-relevant result is dampened a little, a wholly off-topic one a lot. The context bonus is never gated, so context_tags remains the explicit soft-boost channel.
0.0— disabled; legacy scoring exactly.~0.15–0.25— a gentle guard that keeps obviously off-topic memories out of the top of a scoped pool.- Values are clamped to
[0.0, 1.0].
Reach for the gate when tag-scoped recall surfaces importance-inflated memories that don’t match the query. Leave it at 0.0 for open, query-only recall where every candidate is already topical.
Vector candidate pool
Section titled “Vector candidate pool”Vector search fetches more candidates than the response limit so hybrid re-ranking can promote memories with strong importance, tag, or exact-match signals that rank slightly lower on raw cosine similarity.
| Variable | Default | Notes |
|---|---|---|
RECALL_VECTOR_OVERFETCH | 4 | Multiplier on limit for the vector fetch pool. 1 restores legacy 1× behavior. |
RECALL_VECTOR_FETCH_CAP | 200 | Cap on the over-fetched candidate count, kept separate from RECALL_MAX_LIMIT. The fetch never drops below the per-query result limit (limit, or per_query_limit when set). |
If recall misses high-importance exact-topic memories that vector search ranks just outside the top-K, try raising RECALL_VECTOR_OVERFETCH before changing score weights.
Internal artifact types
Section titled “Internal artifact types”Consolidation creates internal MetaPattern cluster summaries that should not appear in user-facing recall. These are filtered by RECALL_EXCLUDED_TYPES (default MetaPattern, comma-separated). The same set is excluded from /health memory/vector counts and /admin/sync drift accounting.
Add types here only for other internal artifacts you create programmatically and never want ranked in /recall.
Tag-score normalization
Section titled “Tag-score normalization”SEARCH_TAG_SCORE_TOKEN_CAP (default 0 = disabled) caps the denominator used to normalize tag-overlap scores, so long queries aren’t penalized relative to short ones. 0 selects the legacy denominator (full query length).
Leave this at 0 unless you have measured otherwise. A production-corpus A/B (2026-06-11, 200 queries against a 10k-memory clone) showed cap values of 2/3/4 regress Recall@5 by 14/7/4 percentage points on ungated queries — the capped denominator inflates tag scores and amplifies tag noise over vector/keyword evidence. The knob exists for corpora with unusually terse queries and disciplined tagging; it is opt-in for a reason.
Consolidation clustering
Section titled “Consolidation clustering”The background consolidation pass groups near-duplicate memories into clusters. Two knobs control cluster formation:
| Variable | Default | Notes |
|---|---|---|
CONSOLIDATION_CLUSTER_SIMILARITY_THRESHOLD | 0.75 | Minimum cosine similarity for a memory to join a cluster. Higher = tighter, fewer clusters. |
CONSOLIDATION_MIN_CLUSTER_SIZE | 3 | Minimum members before a cluster is formed. |
Raise the similarity threshold if unrelated memories are being clustered. CONSOLIDATION_MIN_CLUSTER_SIZE only decides which groups are reported as clusters: a MetaPattern summary is written only when cluster["size"] >= 5, a fixed threshold, so any value of 5 or less writes the same summaries, and lowering it from the default 3 only reports more clusters. See Consolidation & Decay for the full lifecycle.
A tuning recipe
Section titled “A tuning recipe”- Start at defaults. They are the benchmarked configuration; change one thing at a time.
- Freshness matters? Set
RECALL_RECENCY_BIAS=autofirst (targets temporal queries only). Escalate toon+ a higherSEARCH_WEIGHT_TEMPORALonly if that’s not enough. For absolute age decay, shortenSEARCH_RECENCY_WINDOW_DAYSor switch toexp. - Off-topic memories in scoped pools? Introduce
RECALL_RELEVANCE_GATEat ~0.2and adjust. - Metadata/relationships under- or over-weighted? Nudge
SEARCH_WEIGHT_METADATA/SEARCH_WEIGHT_RELATION. - Measure. Use the recall lab and
scripts/audit_relevance.pybefore and after — tuning by feel regresses quietly.
All values are read at startup; restart the service after changing them.