---
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: 12
---

# Search by meaning

<p class="lead">Semantic search: query a collection with some text and get rows ranked by similarity. With an embeddings model, "excursion" finds the museum trip; without one, it falls back to fuzzy word matching. Vectors live in the same SQLite file as the rows, with 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 gets a `_score` (1 = identical, lower = less similar). Everything else about `list` still applies: [rules](/docs/rules) limit which rows are considered, and `filter` narrows them before ranking:

```js title="client"
await sluurp.collection("notes").list({
  near: "a pupil who is struggling",
  filter: `class = "4B"`,
  perPage: 10,
});
```

## Fields

By default, fields marked `"searchable": true` are used (the same ones `search=` uses). If there are none, text, email and URL fields are used. Override with `on`:

```js title="client"
await sluurp.collection("posts").list({ near: "school trip", on: "title,body" });
```

## Embeddings

How vectors are computed depends on the AI settings in the admin UI (**Platform → AI**).

- **Local model file:** Sluurp computes embeddings in-process, nothing else to run. Point it at a `.gguf` embeddings model such as [nomic-embed-text-v1.5](https://huggingface.co/nomic-ai/nomic-embed-text-v1.5-GGUF) (84 MB); **Find models on this machine** lists ones already downloaded by LM Studio or Hugging Face. Supports BERT-style models (Nomic, MiniLM, BGE, E5) in F32, F16, Q8_0, Q4_K, Q5_K or Q6_K. Tens of milliseconds per text on a laptop.
- **Embeddings API:** any OpenAI-compatible embeddings endpoint (local Ollama, OpenAI, …). With Ollama: `ollama pull nomic-embed-text`, base URL `http://localhost:11434/v1`, model `nomic-embed-text`. Text is sent only to that server.
- **Neither:** vectors are built from words and character trigrams. "museum trips" still finds "Museum trip — 4B" and typos are tolerated, but "excursion" won't match. Nothing is sent anywhere.

Switching models is safe: vectors are stored per model and recomputed lazily.

## `near` and `search`

| | `search=` | `near=` |
|---|---|---|
| Matches | rows containing the words | rows about the same thing |
| Order | by `sort` | most similar first |
| Engine | SQLite full-text index | exact vector comparison |
| Good for | names, codes, exact phrases | questions, topics, "more like this" |

Use `search` when users know the exact words, `near` when they know the meaning.

## How it works

- **Vectors are computed lazily** the first time a row is searched, and stored with a hash of the source text; they're recomputed when the text changes. Writes cost nothing extra.
- **Vectors are ordinary rows** in the project database (`_vectors_<dimensions>`), backed up with everything else, and compared with [sqlite-vector](https://github.com/sqliteai/sqlite-vector) (Apache-2.0), compiled into the binary.
- **Search is exact, not approximate.** Every visible row (up to 5,000 after `filter`) is compared, so there's no index to tune and no missed rows. For bigger collections, narrow first with `filter` (a class, a term, the last year).
- **Max 500 rows per page**, as with any list.

## The social feed

Posts can be searched the same way, across feeds the user can read:

```js title="client"
const { items } = await sluurp.social.feed({ near: "lost property" });
```

```http title="HTTP"
GET /api/social/feed?near=lost%20property
```

A search palette can show both: semantic matches next to keyword matches.
