# User interests

A user's interests are the set of interest tags Stream uses to personalize feeds for that user. They drive two features:

- The [`interest` activity selector](https://getstream.io/activity-feeds/docs/flutter/activity-selectors/#interest-activity-selector), which pulls in activities whose `interest_tags` match the user's interests.
- [`interest` ranking](https://getstream.io/activity-feeds/docs/flutter/custom-ranking/#interest-weights), which exposes `interest_score` in the ranking expression.

Stream computes interests automatically from the user's reactions. This page explains how that works, and how you can seed or manage a user's interests yourself, for example to personalize the feed of a brand-new user from an onboarding questionnaire.

## Interest tags on activities

Interests are matched against the `interest_tags` field of an activity. Tags can be set when [creating an activity](https://getstream.io/activity-feeds/docs/flutter/activities/#overview-of-all-activity-fields) or computed automatically by the `text_interest_tags` [activity processor](https://getstream.io/activity-feeds/docs/flutter/activity-processors/). Tags are normalized: trimmed and lower-cased.

## Computed and manual interests

Each interest tag on a user has a `source`:

| Source     | Written by                           | `weight`                            | `count`                                                                           |
| ---------- | ------------------------------------ | ----------------------------------- | --------------------------------------------------------------------------------- |
| `computed` | Stream, from the user's reactions    | Always `1.0`                        | Number of distinct, non-deleted activities the user reacted to that carry the tag |
| `manual`   | You, through the API or the importer | The weight you set, `-1.0` to `1.0` | Always `0`                                                                        |

**Computed interests** are derived from the activities the user reacted to, either directly or through a comment. Every tag in `interest_tags` on those activities is tallied, and the tally becomes the tag's `count`.

**Manual interests** are seeded by you. They carry the weight you choose, where `1.0` means a strong interest and `-1.0` a strong dislike. The recompute never overwrites a manual tag: once a tag is manual it keeps its weight until you change or remove it.

### When computed interests are updated

When a user reacts, or you change their interests through the API, Stream queues them for a recompute. A background worker drains that queue about once a minute, so a new reaction is reflected in the user's interests within roughly a minute, not immediately.

The recompute only runs for apps that use interests: apps with at least one feed group or feed view that has an `interest` activity selector or `interest` ranking. If your app has no such configuration, reactions are not tracked. Once you add one, a user with no interests yet gets them derived from their full reaction history the first time their interests are read. Nothing is lost by enabling interests later.

The manual interests API works for every app, regardless of feed configuration.

## Ordering and how interests are used

A user's interests are ordered by:

1. `weight`, highest first
2. `source`, manual before computed at equal weight
3. `count`, highest first
4. `tag`, alphabetically

Because computed tags always have weight `1.0`, a manual tag seeded with the default weight ranks above every computed tag. This is what makes seeding work for a user who is already active: the seeded tag does not get pushed out by tags derived from reactions. If you want computed tags to win over a seed, give the seed a weight below `1.0`, for example `0.8`.

The selector and ranking read only part of the list:

- The `interest` activity selector uses the top 5 tags with a positive weight.
- `interest` ranking uses the top 5 tags with a positive weight, plus every tag with a weight of `0` or below. A dislike therefore always penalizes matching activities, no matter how many interests the user has.

## Limits

- A user holds at most 50 interest tags in total.
- A write is rejected with a `400` error if it would leave the user with more than 50 manual tags. Otherwise computed tags are trimmed, lowest count first, to keep the total at 50.
- A single request sets or removes between 1 and 50 tags.
- A tag is at most 255 characters long.

## Managing interests

Three endpoints read and write a user's interests. Server-side clients can address any user. Client-side clients can only address the authenticated user. The target user must exist and must not be deleted or deactivated.

>
> **Info:** The examples below use cURL against the REST API and the Node.js server SDK. Other server-side SDKs expose the same three operations (get, upsert and delete user interests) under their own naming conventions, for example `upsert_user_interests` in Python.
>

### Reading interests

Returns the user's interests in the [order described above](#ordering-and-how-interests-are-used). `limit` defaults to 10 and accepts values up to 100.

```js label="Node.js"
const response = await client.feeds.getUserInterests({
  user_id: "alice",
  limit: 20,
});

console.log(response.interests);
```

```bash label="cURL"
curl "https://feeds.stream-io-api.com/api/v2/feeds/users/alice/interests?limit=20&api_key=${API_KEY}" \
  --header "Stream-Auth-Type: jwt" \
  --header "Authorization: ${TOKEN}"
```

Each interest has a `tag`, its `weight`, its `source` and a `count` (the tally for computed tags, always `0` for manual tags):

```json
{
  "interests": [
    { "tag": "cycling", "count": 0, "weight": 1.0, "source": "manual" },
    { "tag": "running", "count": 12, "weight": 1.0, "source": "computed" },
    { "tag": "jazz", "count": 0, "weight": 0.8, "source": "manual" },
    { "tag": "golf", "count": 0, "weight": -1.0, "source": "manual" }
  ]
}
```

### Setting interests

Adds or updates manual interests. `weight` is optional and defaults to `1.0`. Tags already present on the user are updated in place: a computed tag becomes manual and keeps the weight you supply from then on. Tags you do not mention are left untouched.

Tags are trimmed and lower-cased before they are stored. If the same tag appears more than once in a request, the last weight wins.

The response contains the user's full interest list after the write.

```js label="Node.js"
const response = await client.feeds.upsertUserInterests({
  user_id: "alice",
  interests: [
    { tag: "cycling", weight: 0.8 },
    { tag: "jazz" }, // weight defaults to 1.0
    { tag: "golf", weight: -1.0 }, // a dislike
  ],
});
```

```bash label="cURL"
curl -X PUT "https://feeds.stream-io-api.com/api/v2/feeds/users/alice/interests?api_key=${API_KEY}" \
  --header "Stream-Auth-Type: jwt" \
  --header "Authorization: ${TOKEN}" \
  --header "Content-Type: application/json" \
  --data '{
    "interests": [
      { "tag": "cycling", "weight": 0.8 },
      { "tag": "jazz" },
      { "tag": "golf", "weight": -1.0 }
    ]
  }'
```

### Removing interests

Removes the given tags from the user, whatever their source, and returns the remaining interests.

A removed computed tag comes back within about a minute if the user's reactions still support it, because the delete itself schedules a recompute. Deleting is therefore not a way to suppress a computed tag. To keep a tag out of the selector and ranking for good, set it as a manual interest with a weight of `0` or below instead: the selector ignores non-positive weights and ranking applies them as a penalty.

```js label="Node.js"
const response = await client.feeds.deleteUserInterests({
  user_id: "alice",
  tags: ["cycling", "jazz"],
});
```

```bash label="cURL"
curl -X DELETE "https://feeds.stream-io-api.com/api/v2/feeds/users/alice/interests?tags=cycling&tags=jazz&api_key=${API_KEY}" \
  --header "Stream-Auth-Type: jwt" \
  --header "Authorization: ${TOKEN}"
```

### Per-request interest weights

Stored interests are the default input for `interest_score`. You can also pass `interest_weights` on a feed read to adjust the weights for that request only, and `overwrite_interest_weights` to replace the stored interests entirely. Neither changes the user's stored interests. See [Interest weights](https://getstream.io/activity-feeds/docs/flutter/custom-ranking/#interest-weights) in the ranking guide.

## Importing interests

You can seed interests in bulk with the [import feature](https://getstream.io/activity-feeds/docs/flutter/importing-data-feeds/#user-interests). A `user_interest` line carries a user's whole set of manual interests and follows the same per-record validation rules as the API:

```json
{
  "type": "user_interest",
  "data": {
    "user_id": "alice",
    "interests": [{ "tag": "cycling", "weight": 0.8 }, { "tag": "jazz" }]
  }
}
```

Imported interests are stored as manual. Use one line per user; a second `user_interest` line for the same user in one file is reported as a duplicate.

>
> **Warning:** The importer does not enforce the 50-tag limit against interests the user already has. Keep the total per user within the limit yourself when importing into an app with existing data.
>

## Seeding interests at onboarding

A common use case is asking new users what they are interested in during onboarding, then writing those answers as manual interests before they see their first feed. The [For You feed guide](https://getstream.io/activity-feeds/docs/flutter/for-you-feed/#seeding-interests-at-onboarding) walks through this.

---

For the most recent version of this documentation, visit [https://getstream.io/activity-feeds/docs/flutter/user-interests/](https://getstream.io/activity-feeds/docs/flutter/user-interests/).