Scoring
iLAB Memory rankea observations con dos esquemas de scoring distintos: SearchScore para mem_search, ContextScore para los memories[] que devuelve mem_session_start. Comparten helpers pero combinan signals con pesos distintos — y sus valores no son comparables.
Nunca compares un valor de SearchScore con uno de ContextScore. Diferentes fórmulas, diferentes escalas, diferente significado semántico. El único invariante que comparten es el rango [0.0, 1.0]. Esto es Braess #5 — ver Architecture.
Los dos esquemas
SearchScore
Usado por mem_search. Combina la relevancia BM25 a tu query, recency y revision count. Más alto = más relevante a la query que hiciste.
ContextScore
Usado por mem_session_start.memories. Combina recency, revision count y una prioridad por type. Más alto = más saliente como contexto default a cargar.
SearchScore — relevancia a una query
SearchScore = 0.60 * fts5_rank
+ 0.25 * recency_score(updated_at)
+ 0.15 * revision_score(revision_count)
| Signal | Peso | Qué mide |
|---|---|---|
fts5_rank | 0.60 | BM25 de SQLite FTS5, pre-normalizado a [0, 1] |
recency_score | 0.25 | decay exponencial, half-life de 30 días |
revision_score | 0.15 | lineal en revision_count, capeado en 10 |
El signal dominante es el match de la query. Recency es desempate secundario — una observation de 6 meses que clava la query igual le gana a una flamante que no matchea.
ContextScore — saliencia como contexto default
ContextScore = 0.50 * recency_score(updated_at)
+ 0.30 * revision_score(revision_count)
+ 0.20 * type_priority(type)
| Signal | Peso | Qué mide |
|---|---|---|
recency_score | 0.50 | mismo decay exponencial, half-life de 30 días |
revision_score | 0.30 | lineal en revision_count, capeado en 10 |
type_priority | 0.20 | peso por type (ver abajo) |
Acá no hay query — el objetivo es "si tengo que cargar N memorias al arrancar la sesión, ¿cuáles N importan más para este usuario ahora?" Recency manda; type priority es un dedo en la balanza para los hechos estables del usuario.
Prioridades por type
| Type | Prioridad |
|---|---|
profile | 1.00 |
preference | 0.90 |
decision | 0.70 |
pattern | 0.60 |
discovery | 0.50 |
summary | 0.30 |
| type desconocido | 0.50 |
Las observations de profile y preference flotan al tope de memories[] aun cuando son un poquito más viejas. discovery y pattern rankean más bajo — aparecen sobre todo vía search.
Helpers compartidos
Ambas fórmulas llaman las mismas primitivas. Hay una sola definición de recency_score y una sola de revision_score en el codebase (scoring.py), y cada composite las arma con sus propios pesos.
from ilab_memory.scoring import recency_score, revision_score
print(recency_score("2025-12-01T00:00:00+00:00")) # ~0.39 si "now" es 2026-04-20
print(revision_score(5)) # 0.5
print(revision_score(50)) # 1.0 (capeado en REVISION_CAP=10)
Esto es Braess #3. Agregar un signal nuevo a un composite requiere actualizar el otro en el mismo PR. Los helpers compartidos son la única fuente de verdad.
¿Por qué dos esquemas?
Porque responden preguntas distintas.
mem_search: "Entre todo lo que tengo de este usuario, ¿qué está más cerca de este string de query?" — la relevancia a la query domina.mem_session_start: "Entre todo lo que tengo de este usuario, ¿qué debería ver el LLM por default antes de que el usuario hable?" — recency y saliencia dominan.
Un esquema único no podría pesar el FTS rank tanto en 0.60 (cuando hay query) como en 0.0 (cuando no la hay). Separarlos mantiene cada fórmula honesta y tuneable.
No mezcles los scores
hits = mem.mem_search(user_id="alice", query="auth") # SearchScore
resp = mem.mem_session_start(user_id="alice") # ContextScore en resp.memories
# NO hagas esto:
all_obs = sorted(hits + resp.memories, key=lambda o: o.score, reverse=True)
# El sort no significa nada — los scores están en escalas incompatibles.
Si necesitás un ranking unificado entre ambos, calculalo vos desde las observations subyacentes (ej. cargá los registros completos vía mem_get_observation y aplicá tu propia fórmula). La librería se rehúsa deliberadamente a fusionar los dos.
Siguiente
- Architecture / Braess #5 — por qué esta regla está enforzada a nivel de tipos (
SearchScoreyContextScoreson clases Pydantic frozen distintas). - Observations — cómo se ven los objetos scoreados.