Skip to content

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.


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).


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 variableDefaultContribution
SEARCH_WEIGHT_VECTOR0.35Semantic similarity (Qdrant)
SEARCH_WEIGHT_KEYWORD0.35Lexical match
SEARCH_WEIGHT_METADATA0.35Structured metadata field match
SEARCH_WEIGHT_RELATION0.25Graph relationship strength
SEARCH_WEIGHT_TAG0.20Query-token hits against tag and metadata terms
SEARCH_WEIGHT_EXACT0.20Whole-query exact match against metadata terms
SEARCH_WEIGHT_RECENCY0.10Age decay (see below)
SEARCH_WEIGHT_IMPORTANCE0.10User-assigned importance
SEARCH_WEIGHT_CONFIDENCE0.05Classification certainty
SEARCH_WEIGHT_RELEVANCE0.00Consolidation 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.


AutoMem has two independent freshness mechanisms. Don’t confuse them:

SEARCH_WEIGHT_RECENCY (0.10) contributes an absolute age-decay score to every result. Its shape is set by:

VariableDefaultNotes
SEARCH_RECENCY_WINDOW_DAYS180Decay window. Non-positive values fall back to 180.
SEARCH_RECENCY_CURVElinearlinear → 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.

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.

VariableDefaultNotes
RECALL_RECENCY_BIASoffoff never re-ranks; on always; auto only when the query expresses temporal intent (“latest”, “current”, …). Invalid values fall back to off.
SEARCH_WEIGHT_TEMPORAL0.10Re-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.


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 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.

VariableDefaultNotes
RECALL_VECTOR_OVERFETCH4Multiplier on limit for the vector fetch pool. 1 restores legacy 1× behavior.
RECALL_VECTOR_FETCH_CAP200Cap 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.


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.


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.


The background consolidation pass groups near-duplicate memories into clusters. Two knobs control cluster formation:

VariableDefaultNotes
CONSOLIDATION_CLUSTER_SIMILARITY_THRESHOLD0.75Minimum cosine similarity for a memory to join a cluster. Higher = tighter, fewer clusters.
CONSOLIDATION_MIN_CLUSTER_SIZE3Minimum 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.


  1. Start at defaults. They are the benchmarked configuration; change one thing at a time.
  2. Freshness matters? Set RECALL_RECENCY_BIAS=auto first (targets temporal queries only). Escalate to on + a higher SEARCH_WEIGHT_TEMPORAL only if that’s not enough. For absolute age decay, shorten SEARCH_RECENCY_WINDOW_DAYS or switch to exp.
  3. Off-topic memories in scoped pools? Introduce RECALL_RELEVANCE_GATE at ~0.2 and adjust.
  4. Metadata/relationships under- or over-weighted? Nudge SEARCH_WEIGHT_METADATA / SEARCH_WEIGHT_RELATION.
  5. Measure. Use the recall lab and scripts/audit_relevance.py before and after — tuning by feel regresses quietly.

All values are read at startup; restart the service after changing them.