Skip to main content

Scoring

iLAB Memory ranks observations with two distinct scoring schemes: SearchScore for mem_search, ContextScore for the memories[] returned by mem_session_start. They share helper math but combine signals with different weights — and their values are not comparable.

warning

Never compare a SearchScore value to a ContextScore value. Different formulas, different scales, different semantic meaning. The only invariant they share is the [0.0, 1.0] range. This is Braess #5 — see Architecture.

The two schemes

SearchScore

Used by mem_search. Combines BM25 relevance to your query, recency, and revision count. Higher = more relevant to the query you asked.

ContextScore

Used by mem_session_start.memories. Combines recency, revision count, and a per-type priority. Higher = more salient as default context to load.

SearchScore — relevance to a query

SearchScore = 0.60 * fts5_rank
+ 0.25 * recency_score(updated_at)
+ 0.15 * revision_score(revision_count)
SignalWeightWhat it measures
fts5_rank0.60BM25 from SQLite FTS5, pre-normalized to [0, 1]
recency_score0.25exponential decay, 30-day half-life
revision_score0.15linear in revision_count, capped at 10

The dominant signal is the query match. Recency is a secondary tiebreaker — a 6-month-old observation that nails the query still beats a brand-new observation that doesn't.

ContextScore — salience as default context

ContextScore = 0.50 * recency_score(updated_at)
+ 0.30 * revision_score(revision_count)
+ 0.20 * type_priority(type)
SignalWeightWhat it measures
recency_score0.50same exponential decay, 30-day half-life
revision_score0.30linear in revision_count, capped at 10
type_priority0.20per-type weight (see below)

There is no query here — the goal is "if I have to load N memories at session start, which N matter most for this user right now?" Recency dominates; type priority is a thumb on the scale for stable user facts.

Type priorities

TypePriority
profile1.00
preference0.90
decision0.70
pattern0.60
discovery0.50
summary0.30
any unknown type0.50
tip

Profile and preference observations bubble to the top of memories[] even when slightly older. Discoveries and patterns rank lower — they show up most often through search.

Shared helpers

Both formulas call the same primitives. There is one definition of recency_score and one of revision_score in the codebase (scoring.py), and each composite assembles them with its own weights.

from ilab_memory.scoring import recency_score, revision_score

print(recency_score("2025-12-01T00:00:00+00:00")) # ~0.39 if "now" is 2026-04-20
print(revision_score(5)) # 0.5
print(revision_score(50)) # 1.0 (capped at REVISION_CAP=10)
note

This is Braess #3. Adding a new signal to one composite requires updating the other in the same PR. The shared helpers are the single source of truth.

Why two schemes?

Because they answer different questions.

  • mem_search: "Among everything I have for this user, what is closest to this query string?" — query relevance dominates.
  • mem_session_start: "Among everything I have for this user, what should the LLM see by default before the user even speaks?" — recency and salience dominate.

A single scheme could not weight FTS rank both at 0.60 (when there is a query) and at 0.0 (when there is not). Splitting them keeps each formula honest and tunable.

Don't mix the scores

hits = mem.mem_search(user_id="alice", query="auth") # SearchScore
resp = mem.mem_session_start(user_id="alice") # ContextScore in resp.memories

# DO NOT do this:
all_obs = sorted(hits + resp.memories, key=lambda o: o.score, reverse=True)
# The sort is meaningless — the scores are on incompatible scales.

If you need a unified ranking across both, compute it yourself from the underlying observations (e.g., load full records via mem_get_observation and apply your own formula). The library deliberately refuses to fuse the two.

Next

  • Architecture / Braess #5 — why this rule is enforced at the type level (SearchScore vs ContextScore are distinct frozen Pydantic classes).
  • Observations — what the scored objects look like.