Skip to main content
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. Personalization is off by default. Turn it on with:
The tunable knobs — 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_FEEDBACK is 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 when PERSONALIZATION_ENABLED is 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.
An explicit 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 the user_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.5 is neutral, meaning “no signal”.
  • Each rating moves a weight toward its target by PREFERENCE_ALPHA (default 0.3).
  • Untouched weights decay back toward neutral by PREFERENCE_BETA (default 0.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 within 0.01 of 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 of improve() 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 by PERSONALIZATION_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 existing wide_search_top_k (default 100), the results are re-sorted by personalized distance, and the list is trimmed back to top_k. So personalization can change which chunks make the cut, not only their order. Weights that match nothing leave the fetch at top_k.
At a neutral weight, or with 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, triplet embedding, contradiction detection, 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.

Feedback System

Rate answers and fold ratings into ranking

Improve

Enrich the graph and bridge session memory

Setup Configuration

Personalization environment variables