How Cognee personalizes memory
Per-user preference personalization lets one user’s ratings shape that user’s own retrieval ranking, without changing what anyone else sees. Cognee distills the ratings a user gives in sessions into a compact per-user preference record, and applies it as a ranking nudge on that user’s later recalls. Which memories a user can reach at all is set separately, by Data Isolation and Multi-Tenancy. Personalization is off by default. Turn it on with:PERSONALIZATION_INFLUENCE, PREFERENCE_ALPHA, PREFERENCE_BETA — are
documented in Setup Configuration. For a runnable walkthrough of
the rate → improve → recall loop, see the
Feedback System guide. This page explains
the mechanism.
Where the ratings come from
A rating can reach personalization two ways:- Explicitly, via
cognee.session.add_feedback(..., feedback_score=1..5). - Inferred, when
AUTO_FEEDBACKis on. The per-turn analysis can read a 1–5 rating of the previous answer out of what you say next (“that was exactly right”, “no, that’s wrong again”). The rating question is only added to the analysis whenPERSONALIZATION_ENABLEDis on, and a value outside 1–5 degrades to “no signal” instead of raising. With the flag off, a turn that carries only a rating writes nothing.
feedback_score wins over an inferred rating for the same Q&A entry. A rating of
3 is neutral and is treated as a no-op.
The same ratings also feed the global feedback weights, which move
ranking for every user. That mechanism is independent of personalization — you can run either,
both, or neither.
How preferences are stored
Personalization keeps one internal preference node per (user, dataset) pair, grouped under theuser_preferences node set. From it, weighted prefers edges point at the graph nodes that
were actually used to build the answers that user rated (only nodes — edges are never prefers
targets).
- A weight of
0.5is neutral, meaning “no signal”. - Each rating moves a weight toward its target by
PREFERENCE_ALPHA(default0.3). - Untouched weights decay back toward neutral by
PREFERENCE_BETA(default0.02) per conversation turn. Decay is computed on read from a turn counter, so nothing is rewritten in the background and there are no timestamps involved. A weight that has decayed to within0.01of neutral is pruned. - Stated preferences (“always answer in bullet points”) are also folded into the preference node’s text, newest first, capped at 2000 characters — the oldest lines fall off first, never mid-line. That text is injected as guidance into graph, hybrid, and RAG completion prompts.
How preferences are updated
The preference update runs as a stage ofimprove()
when you pass session_ids — the same call that applies global feedback weights. It runs once
for all the sessions you pass, because preferences aggregate across a user’s sessions. It is
safe to re-run: each turn is counted once and each rating spent once. Like its neighbouring
stages it is best-effort — a failure is logged and never blocks the rest of improve() — and it
is a no-op that writes nothing at all when PERSONALIZATION_ENABLED is off.
What changes at retrieval time
When weights exist for the current user and dataset, they act as a multiplicative nudge on ranking, capped byPERSONALIZATION_INFLUENCE (default 0.3, i.e. at most 30%):
- Graph completion — the personal weight multiplies into triplet scoring.
- Hybrid — it multiplies alongside the existing importance and truth factors.
- RAG completion (
RAG_COMPLETION) — when the loaded weights actually match rows in the chunk collection, the candidate fetch widens to the retriever’s existingwide_search_top_k(default100), the results are re-sorted by personalized distance, and the list is trimmed back totop_k. So personalization can change which chunks make the cut, not only their order. Weights that match nothing leave the fetch attop_k.
PERSONALIZATION_INFLUENCE=0, the ranking factor is exactly 1.0,
so an empty or non-matching weight map leaves every path arithmetically identical to an
un-personalized run.
Two conditions are needed for personalization to apply at all: a user must be in context, and
exactly one dataset must resolve. A search that spans several datasets never personalizes.
Reads are fail-open everywhere — a missing, empty, or broken preference node costs you the
personalization, never the search.
Preference nodes are internal and are never surfaced in retrieval output. They are filtered at
the graph read chokepoints: graph projection for search, conflict
review, natural-language search, and the provenance and schema-inventory views (which also
drop every edge touching an internal node). The one exception is
SearchType.CYPHER, which runs
your Cypher verbatim and applies no filter.