# Search sources

> **Info:** This is release candidate documentation for **Stream Chat React SDK v15**. For the latest stable version, see the [latest version (v14)](https://getstream.io/chat/docs/sdk/react/).

Search in the state layer is a `SearchController` with a set of sources. Each source searches one kind of item: channels, users, messages, or anything your app adds. The controller holds the search text and runs it against the sources that are active.

>
> **Info:** **Example**
>
> The user types "des" in the sidebar search. The controller passes "des" to its active source, the channel source, which queries the channels whose name matches "des". The results land in that source's state, page by page, like a paginator's. Unlike a paginator's, they don't change as events arrive.
>

Showing search in the UI, and replacing the result components, is on the [Search](https://getstream.io/chat/docs/sdk/react/v15/features/channels/search/) feature page.

## The search controller

```ts
import {
  ChannelSearchSource,
  MessageSearchSource,
  SearchController,
  UserSearchSource,
} from "stream-chat";

const searchController = new SearchController({
  client,
  sources: [
    new ChannelSearchSource(client),
    new UserSearchSource(client),
    new MessageSearchSource(client),
  ],
});

searchController.activate(); // activates the first source, channels
void searchController.search("des");

// later: search messages instead
searchController.activateSource("messages");
void searchController.search("des");
```

The controller's `state` store holds:

- **`isActive`**: whether search is open;
- **`searchQuery`**: the current search text;
- **`sources`**: every source, active or not.

Its methods:

- **`SearchController.activate()`** opens search. If no source is active, it activates the first one, or every source when `keepSingleActiveSource` is `false`.
- **`SearchController.search(text)`** sets the search text and passes it to every active source. Each source waits for the user to stop typing before it queries.
- **`SearchController.activateSource(type)`** and **`SearchController.deactivateSource(type)`** turn a source on or off. The last active source can't be turned off.
- **`SearchController.clear()`** empties the search text and every source's results, and keeps search open. **`SearchController.exit()`** does the same and closes search.
- **`SearchController.addSource(source)`**, **`SearchController.removeSource(type)`** and **`SearchController.getSource(type)`** manage the sources.
- **`SearchController.cancelSearchQueries()`** cancels the queries that are waiting or in flight.

By default only one source is active at a time: activating one turns the others off. To search several at once, set `keepSingleActiveSource` to `false`:

```ts
client.config.set({ searchController: { keepSingleActiveSource: false } });
```

`client.config` reaches a controller only when it was created with `client`, as above.

When you no longer need a controller you created, call `SearchController.dispose()`. Its channel results then no longer keep their channels in memory, and it stops following `client.config`. `SearchController.registerSubscriptions()` undoes it. The React SDK creates a controller with the channel, user and message sources when the app doesn't pass one.

## Built-in sources

| Source                      | `type`       | Searches                                                              | In the UI's default controller |
| --------------------------- | ------------ | --------------------------------------------------------------------- | ------------------------------ |
| `ChannelSearchSource`       | `"channels"` | the user's channels whose `name` matches the text                     | yes                            |
| `UserSearchSource`          | `"users"`    | users whose `id` or `name` matches, without the current user          | yes                            |
| `MessageSearchSource`       | `"messages"` | messages that contain the text, in the user's channels, newest first  | yes                            |
| `ChannelMemberSearchSource` | `"members"`  | members of one channel whose `name` matches or whose `id` is the text | no                             |

The UI layer creates a `SearchController` with the channel, user and message sources. A `SearchController` you create yourself has only the sources you pass.

The built-in sources come in two kinds: three that run one query, and `MessageSearchSource`, which runs two.

### Channel, user and member sources

Each runs one query: `queryChannels`, `queryUsers` or `queryMembers`. Create one with the client and an options object, as in `new ChannelSearchSource(client, options)`. `ChannelMemberSearchSource` takes the channel instead. These options narrow what the query finds:

| Option          | What it does                                                                  |
| --------------- | ----------------------------------------------------------------------------- |
| `filters`       | narrows the results                                                           |
| `sort`          | orders them                                                                   |
| `searchOptions` | the query's other options, except `limit` and `offset`, which the source sets |

- "Matches" in the table means `$autocomplete`: a word in the field starts with the text. A member's `id` must equal the text.
- Channels found aren't watched. Opening one loads and watches it.
- Its filter builder is `source.filterBuilder`.

### Message source

`MessageSearchSource` runs two queries: it searches messages, then loads the results' channels that the client doesn't have, without watching them. Create it with the client and an options object, `new MessageSearchSource(client, options)`. These options narrow what its queries find:

| Option                                       | Applies to                                                                                                                                                          |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `messageSearchChannelFilters`                | the search: which channels to search                                                                                                                                |
| `messageSearchFilters`                       | the search: which messages                                                                                                                                          |
| `messageSearchSort`                          | the search: the order. Messages always sort by `created_at` first, newest first by default; a `created_at` entry sets the direction, and other fields sort after it |
| `channelQueryFilters`, `channelQueryOptions` | loading the results' channels                                                                                                                                       |

- Each of the two queries has its own filter builders: `messageSearchChannelFilterBuilder` and `messageSearchFilterBuilder` for the search, and `channelQueryFilterBuilder` for loading the channels.

### Changing the options later

The options in the tables above, such as `filters` and `sort`, are also properties of the source, so they can change after it's created. The new value applies from the next query:

```ts
import { ChannelSearchSource, UserSearchSource } from "stream-chat";

const channels = new ChannelSearchSource(client, {
  filters: { type: "messaging" },
  sort: [{ direction: -1, field: "last_message_at" }],
});
const users = new UserSearchSource(client, { filters: { role: "user" } });

// later
users.filters = { ...users.filters, banned: false };
```

### Building the filters from the search text at runtime

Setting a source's `filters` gives it a fixed filter. To build the filter each time the source queries instead, give the source **filter generators**: functions that receive the search text, as `searchQuery`, and any values you add as context, and return part of the filter, or `null` to leave it out. The source sends those parts together with `filters`.

The generators go in the source's constructor, in its third argument, as `initialFilterConfig`. It's an object with one entry per generator, and each entry has two fields:

- `generate`: the generator function;
- `enabled`: whether its part is used.

The key names the generator. Pick any name for a generator of your own. The key doesn't have to match the field the generator filters on. A built-in generator's name replaces it:

| Source                      | Built-in generator                                                     | Its condition                                                |
| --------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------ |
| `ChannelSearchSource`       | `name`                                                                 | `name` matches the text                                      |
| `UserSearchSource`          | `$or`                                                                  | `id` or `name` matches the text                              |
| `ChannelMemberSearchSource` | `default`                                                              | `name` matches, or `id` equals the text                      |
| `MessageSearchSource`       | `text` in the message search, `cid` when loading the results' channels | the messages contain the text; the channels are the results' |

The values for the context go in the same argument, as `initialContext`.

#### Adding a generator

This generator leaves out archived channels while `hideArchived` is `true`:

```ts
import { ChannelSearchSource } from "stream-chat";

const channels = new ChannelSearchSource<{ hideArchived: boolean }>(
  client,
  undefined,
  {
    initialContext: { hideArchived: true },
    initialFilterConfig: {
      archived: {
        enabled: true,
        generate: ({ hideArchived }) =>
          hideArchived ? { archived: false } : null,
      },
    },
  },
);
```

The built-in `name` generator stays, so the text "des" now finds the user's channels named "des…" that aren't archived. To change `hideArchived` while the user searches, see [Switching generators at runtime](#switching-generators-at-runtime).

#### Replacing a built-in generator

A generator with a built-in generator's name replaces it. This `name` generator matches the text against the channel's name or its members' names. Returning an `$or` makes either match enough:

```ts
import { ChannelSearchSource } from "stream-chat";

const channels = new ChannelSearchSource(client, undefined, {
  initialFilterConfig: {
    name: {
      enabled: true,
      generate: ({ searchQuery }) =>
        searchQuery
          ? {
              $or: [
                { name: { $autocomplete: searchQuery } },
                { "member.user.name": { $autocomplete: searchQuery } },
              ],
            }
          : null,
    },
  },
});
```

Whether the built-in generators stay depends on the source:

- `ChannelSearchSource` and `MessageSearchSource` keep their built-in generators and add yours, so only a generator with the same name replaces one.
- `UserSearchSource` and `ChannelMemberSearchSource` use only your generators, so include one for the search text.
- `MessageSearchSource` takes the third argument per query, under `messageSearchChannel`, `messageSearch` and `channelQuery`. The `channelQuery` generators receive `cids`, the results' channel ids, instead of the search text.

#### Switching generators at runtime

The source's filter builder, `source.filterBuilder`, changes the generators while the user searches. `MessageSearchSource` has one per query: `messageSearchChannelFilterBuilder`, `messageSearchFilterBuilder` and `channelQueryFilterBuilder`. The change applies from the next query.

- `FilterBuilder.updateContext(values)` changes the values `generate` receives.
- `FilterBuilder.enableFilter(name)` and `FilterBuilder.disableFilter(name)` turn a generator on or off.

With the `archived` generator above, a "Show archived" switch changes the context:

```ts
import type { ChannelSearchSource } from "stream-chat";

const setShowArchived = (
  channels: ChannelSearchSource<{ hideArchived: boolean }>,
  showArchived: boolean,
) => channels.filterBuilder.updateContext({ hideArchived: !showArchived });
```

## Source state

Each source keeps its results in its `state` store:

| Field            | What it holds                                                   |
| ---------------- | --------------------------------------------------------------- |
| `items`          | The results loaded so far. `undefined` before the first search. |
| `isLoading`      | Whether a query is running.                                     |
| `hasNext`        | Whether there are more results to load.                         |
| `lastQueryError` | The error of the last failed query.                             |
| `searchQuery`    | The text the results are for.                                   |
| `isActive`       | Whether the source is active.                                   |

`source.search()` with no text loads the next page. It does nothing while a query runs or when `hasNext` is `false`. A new search text replaces the results and restarts the search from the beginning.

```ts
import { MessageSearchSource } from "stream-chat";

const messages = new MessageSearchSource(client);
messages.activate();

const unsubscribe = messages.state.subscribeWithSelector(
  ({ hasNext, isLoading, items }) => ({ hasNext, isLoading, items }),
  ({ hasNext, isLoading, items }) => {
    console.log(`${items?.length ?? 0} messages found`, { hasNext, isLoading });
  },
);

void messages.search("release plan");
```

### When a source queries

Every source's constructor options also take settings that tune when it queries: the second argument for the built-in sources, as in `new ChannelSearchSource(client, options)`, and the only argument for a source of your own.

| Option                   | Default | What it does                                                                                                                           |
| ------------------------ | ------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `pageSize`               | `10`    | How many results a page holds.                                                                                                         |
| `shortQueryDebounceMs`   | `500`   | How long to wait after the last keystroke, for a text up to `shortQueryMaxLength`.                                                     |
| `longQueryDebounceMs`    | `300`   | How long to wait for a longer text.                                                                                                    |
| `shortQueryMaxLength`    | `2`     | The longest text that counts as short.                                                                                                 |
| `debounceMs`             | none    | One wait for every text, in place of the two above.                                                                                    |
| `allowEmptySearchString` | `false` | Whether a source queries with an empty text. Always `true` for `ChannelMemberSearchSource`, so it lists members before the user types. |
| `resetOnNewSearchQuery`  | `true`  | Whether a new text clears the results before its own arrive.                                                                           |

A short text matches much of the data, which makes it the slowest query to run, so it waits longer. Typing `a`, `ab`, then `abc` waits 500 ms, 500 ms, then 300 ms, and only the last query runs. `source.setDebounceOptions()` changes the waits later.

When a new query starts while the previous one is in flight, the previous one is cancelled, and its late response is ignored. A cancelled query never sets `lastQueryError`. `source.cancelScheduledQuery()` and `source.resetState()` cancel too.

## Adding your own source

A source for your own data extends `BaseSearchSource`. It needs a `type`, a `BaseSearchSource.query()` that returns `{ items }` for one page, and a `BaseSearchSource.filterQueryResults()` that returns the items to show.

```ts
import { BaseSearchSource, SearchController } from "stream-chat";
import type { SearchQueryOptions, SearchSourceOptions } from "stream-chat";

type Project = { id: string; name: string };

class ProjectSearchSource extends BaseSearchSource<Project> {
  readonly type = "projects";

  constructor(options?: SearchSourceOptions) {
    super(options);
  }

  protected async query(
    searchQuery: string,
    { signal }: SearchQueryOptions = {},
  ) {
    const params = new URLSearchParams({
      limit: String(this.pageSize),
      offset: String(this.offset ?? 0),
      q: searchQuery,
    });
    // Passing the signal lets a newer query cancel this request.
    const response = await fetch(`/api/projects?${params}`, { signal });
    return { items: (await response.json()) as Project[] };
  }

  protected filterQueryResults(items: Project[]) {
    return items;
  }
}

const searchController = new SearchController({
  client,
  sources: [new ProjectSearchSource({ pageSize: 20 })],
});
```

How `BaseSearchSource.query()` pages:

- **By offset.** Return `{ items }`. The source moves `offset` forward by the number of items, and `hasNext` stays `true` while a page comes back full.
- **By cursor.** Return `{ items, next }`. The source keeps `next` for the following query, and `hasNext` is `true` while `next` is set.

A source whose data is local can extend `BaseSearchSourceSync` instead, with a `BaseSearchSourceSync.query()` that returns the page directly.

## Search vs paginators

Search results are a snapshot. A new message that contains the text doesn't join the search results, and a renamed channel doesn't leave them. To refresh the results, search again.

Channel results are the same `Channel` instances the rest of the state layer uses, and opening one loads and watches it. For data that has to stay current, use a paginator: [Channel list paginators](https://getstream.io/chat/docs/sdk/react/v15/state/loading-data/channel-list-paginators/) or [Message paginators](https://getstream.io/chat/docs/sdk/react/v15/state/loading-data/message-paginators/).

## Used in

- [Search](https://getstream.io/chat/docs/sdk/react/v15/features/channels/search/)
- [Search customization](https://getstream.io/chat/docs/sdk/react/v15/recipes/channel-list/search-customization/)
- [Search menu](https://getstream.io/chat/docs/sdk/react/v15/recipes/app/search-menu/)

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/v15/state/loading-data/search-sources/](https://getstream.io/chat/docs/sdk/react/v15/state/loading-data/search-sources/).