---
title: Search by meaning
description: Rows found by what they mean rather than the words they use, with vectors kept in SQLite and nothing to set up.
section: Data
order: 13
---

# Search by meaning

<p class="lead">Ask a collection for the rows nearest to some words, and they come back closest first. With an embeddings model, "excursion" finds the museum trip. Without one, it still works on the words. The vectors are kept in the same SQLite file as the rows, and there is no index to build.</p>

```js title="client"
const { items } = await sluurp.collection("notes").list({ near: "museum trips" });
```

```http title="HTTP"
GET /api/collections/notes/records?near=museum%20trips
```

Each row comes with a `_score`: 1 is the same, and lower is further away. Everything else about a list still applies. The caller sees only the rows the [rules](/docs/rules) let them list, and `filter` narrows them before they are ranked:

```js title="client"
await sluurp.collection("notes").list({
  near: "a pupil who is struggling",
  filter: `class = "4B"`,
  perPage: 10,
});
```

## Which fields are compared

By default, the collection's searchable fields are compared: those with `"searchable": true` in the schema, the same fields `search=` looks in. A collection with none compares its text, email and URL fields. `on` names others:

```js title="client"
await sluurp.collection("posts").list({ near: "school trip", on: "title,body" });
```

## Meaning, or words

What the vectors capture depends on the AI settings in the admin (**Platform → AI**).

- **With an embeddings model**, near means near in meaning. It can be any server that speaks the OpenAI embeddings API: Ollama on your own machine, OpenAI, or others. On Ollama, run `ollama pull nomic-embed-text`, set the base URL to `http://localhost:11434/v1` and the embeddings model to `nomic-embed-text`. Rows then leave the server only for that machine.
- **Without one**, the vectors are made from the words and their three-letter pieces. "museum trips" still finds "Museum trip — 4B", and a typo is forgiven, but "excursion" does not find it. This needs nothing and sends nothing anywhere.

Changing the model is safe. Vectors are kept per model, so the new one makes its own as they are needed.

## `near` and `search`

| | `search=` | `near=` |
|---|---|---|
| Finds | rows containing these words | rows about the same thing |
| Order | as `sort` says | closest first |
| Behind it | SQLite's full-text index | vectors, compared one by one |
| Good for | names, codes, exact phrases | questions, topics, "anything like this" |

Use `search` when the reader knows the words, and `near` when they know what they mean.

## How it works

- **A vector is made the first time a row is searched**, and kept with a hash of the words it was made from. When those words change, it is made again. Nothing is done when rows are written, so writes cost what they always did.
- **Vectors are rows in an ordinary table** in the project's database (`_vectors_<dimensions>`), and are backed up with everything else. They are compared by [sqlite-vector](https://github.com/sqliteai/sqlite-vector) (Apache-2.0), which is compiled into the binary.
- **The comparison is exact.** Every row the caller may list, up to 5,000 after `filter`, is compared, so there is no approximate index to tune and no row missed by one. For a larger collection, use `filter` to narrow it first: a class, a term, the last year.
- **A page has at most 500 rows**, as any list.

## The social feed

Posts are found the same way, from the feeds the reader may read:

```js title="client"
const { items } = await sluurp.social.feed({ near: "lost property" });
```

```http title="HTTP"
GET /api/social/feed?near=lost%20property
```

MikroSchool uses this in its search palette (Ctrl-K), where posts are found by what they are about as well as by their words.
