> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cognee.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# User Preferences

> How per-user preference personalization stores ratings and nudges retrieval ranking.

**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:

```dotenv theme={null}
PERSONALIZATION_ENABLED="true"
```

The tunable knobs — `PERSONALIZATION_INFLUENCE`, `PREFERENCE_ALPHA`, `PREFERENCE_BETA` — are
documented in [Setup Configuration](/setup-configuration/overview). For a runnable walkthrough of
the rate → improve → recall loop, see the
[Feedback System guide](/guides/feedback-system#personalize-ranking-per-user). 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](/guides/feedback-system), 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()`](/core-concepts/main-operations/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.

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

<Columns cols={3}>
  <Card title="Feedback System" icon="brain-circuit" href="/guides/feedback-system">
    Rate answers and fold ratings into ranking
  </Card>

  <Card title="Improve" icon="sparkles" href="/core-concepts/main-operations/improve">
    Enrich the graph and bridge session memory
  </Card>

  <Card title="Setup Configuration" icon="settings" href="/setup-configuration/overview">
    Personalization environment variables
  </Card>
</Columns>
