---
title: Search by vector similarity
description: "relate, the embedded Cypher graph database (relatedb.ai): Store embeddings as typed vectors, index them, and find the nearest nodes with the SEARCH clause, with the score range of each similarity function."
type: how-to
surfaces:
  - cypher
status: stable
verified: 0.1.2
verified_by:
  - cypher
canonical_url: https://relatedb.ai/docs/guides/cypher/vector-search/
prerequisites:
  - get-started/first-graph
related:
  - reference/cypher/search
  - cookbook/hybrid-recall
  - cookbook/query-the-relate-docs
  - reference/cypher/differences-from-neo4j
  - guides/integrations/llamaindex
  - guides/migrate/from-a-vector-store
last_updated: 2026-11-09
---

# Search by vector similarity

Store embeddings as typed vectors, index them, and find the nearest nodes with the SEARCH clause, with the score range of each similarity function.

> Verified against relate 0.1.2. If your `relate --version` is newer, read https://relatedb.ai/docs/reference/changelog/#since-0.1.2

This guide shows you how to store embeddings on [nodes](https://relatedb.ai/docs/reference/glossary/#node), index them, and find the nearest nodes with the [SEARCH clause](https://relatedb.ai/docs/reference/glossary/#search). The cells run in page order on one [in-memory store](https://relatedb.ai/docs/reference/glossary/#in-memory-store), in your browser and in the docs build, so each step sees what the steps before it wrote.

## Prerequisites

- The example graph from [your first graph](https://relatedb.ai/docs/get-started/first-graph/): three people and the two projects they work on. The first step creates it, so this page runs on its own.
- Embeddings from the model of your choice. This guide uses three-dimensional vectors so the numbers stay readable; a model's vectors have hundreds of dimensions, and every step works the same way.

## Load the example graph

This cell writes the shared graph: Ada, Grace and Noor, the `relate` and `opencypher-tck` projects, and who works on what.

```cypher run name=seed
CREATE (ada:Person {name: 'Ada', role: 'engineer'}),
       (grace:Person {name: 'Grace', role: 'engineer'}),
       (noor:Person {name: 'Noor', role: 'maintainer'}),
       (relate:Project {name: 'relate', language: 'Rust'}),
       (tck:Project {name: 'opencypher-tck', language: 'Gherkin'}),
       (ada)-[:WORKS_ON {since: 2024}]->(relate),
       (grace)-[:WORKS_ON {since: 2023}]->(relate),
       (grace)-[:WORKS_ON {since: 2022}]->(tck),
       (noor)-[:MAINTAINS]->(relate),
       (ada)-[:KNOWS]->(grace)
```

```table
(no rows) · nodes created: 5, relationships created: 5, properties set: 13, labels added: 5
```

## Store the vectors

Give each person a `focus` vector: how much of their work goes to the engine, the tests, and the docs. Pass the vectors as a [parameter](https://relatedb.ai/docs/reference/glossary/#parameter) and store each one with `vector(list, dimensions, FLOAT32)`. The typed vector is also a guard: a list of the wrong length fails the statement with `type error: vector() dimension does not match list` instead of being stored.

```json data=focus
{"people": [
  {"name": "Ada", "focus": [0.9, 0.1, 0.2]},
  {"name": "Grace", "focus": [0.3, 0.9, 0.1]},
  {"name": "Noor", "focus": [0.2, 0.3, 0.9]}
]}
```

```cypher run name=store-vectors params=focus
UNWIND $people AS row
MATCH (p:Person {name: row.name})
SET p.focus = vector(row.focus, 3, FLOAT32)
RETURN p.name AS name, valueType(p.focus) AS type
ORDER BY name
```

```table
| name    | type                                   |
|---------|----------------------------------------|
| "Ada"   | "VECTOR<FLOAT32 NOT NULL>(3) NOT NULL" |
| "Grace" | "VECTOR<FLOAT32 NOT NULL>(3) NOT NULL" |
| "Noor"  | "VECTOR<FLOAT32 NOT NULL>(3) NOT NULL" |
3 rows · properties set: 3
```

The coordinate type is a bare keyword, such as `FLOAT32`, `FLOAT64`, `INT8` or `INT64`; the [search reference](https://relatedb.ai/docs/reference/cypher/search/#functions) lists every accepted spelling.

## Create a vector index

Create the index on the property, with the vector length and the similarity function in `OPTIONS`. `WITH [p.role]` declares a [property](https://relatedb.ai/docs/reference/glossary/#property) that a search can filter on before it ranks.

```cypher run name=create-index
CREATE VECTOR INDEX person_focus IF NOT EXISTS
FOR (p:Person) ON (p.focus) WITH [p.role]
OPTIONS {indexConfig: {`vector.dimensions`: 3, `vector.similarity_function`: 'cosine'}}
```

```table
(no rows) · indexes added: 1
```

The `indexConfig` keys contain dots, so quote them in backticks or single quotes; an unquoted `vector.dimensions` is a parse error. The similarity function is `'cosine'` (the default) or `'euclidean'`. The [search reference](https://relatedb.ai/docs/reference/cypher/search/#vector-index-options) lists every option and its default.

## Find the nearest nodes

The question vector leans hard on tests. `SEARCH` binds `p` to the nearest people and `SCORE AS` names the similarity:

```json data=question
{"question": [0.1, 0.9, 0.1]}
```

```cypher run name=nearest params=question
MATCH (p:Person)
SEARCH p IN (VECTOR INDEX person_focus FOR $question LIMIT 2) SCORE AS score
RETURN p.name AS name, round(score, 4) AS score
ORDER BY score DESC
```

```table
| name    | score  |
|---------|--------|
| "Grace" | 0.989  |
| "Noor"  | 0.7151 |
2 rows
```

Three rules shape the clause:

- `LIMIT` is required: it is the number of neighbors the index returns.
- The score exists only under the name `SCORE AS` gives it; there is no implicit `score` variable.
- The variable after `SEARCH` is the one the `MATCH` pattern declares, and the query vector cannot refer to it.

A higher score means more similar. The search uses the index's similarity function, so these scores are the ones `vector.similarity.cosine` returns.

## Filter inside the search

Put the filter inside the parentheses. It runs before the ranking and the `LIMIT`, so the search still returns two people when one of the nearest neighbors does not match:

```cypher run name=filtered params=question
MATCH (p:Person)
SEARCH p IN (VECTOR INDEX person_focus FOR $question WHERE p.role = 'engineer' LIMIT 2) SCORE AS score
RETURN p.name AS name, round(score, 4) AS score
ORDER BY score DESC
```

```table
| name    | score  |
|---------|--------|
| "Grace" | 0.989  |
| "Ada"   | 0.6184 |
2 rows
```

Noor, the second nearest, is a maintainer, so Ada takes the second place. The filter may name only the properties in the index's `WITH [...]` list, compared with simple comparisons or `IN` a list, joined by `AND`. A `WHERE` after the `SEARCH` clause filters only the rows the search already returned, so it can leave fewer than `LIMIT`.

The result is an ordinary match, so the next clause can walk the graph from it:

```cypher run name=then-traverse
MATCH (p:Person)
SEARCH p IN (VECTOR INDEX person_focus FOR [0.2, 0.2, 0.9] LIMIT 1) SCORE AS score
MATCH (p)-[:WORKS_ON|MAINTAINS]->(proj:Project)
RETURN p.name AS name, collect(proj.name) AS projects
```

```table
| name   | projects   |
|--------|------------|
| "Noor" | ["relate"] |
1 row
```

## See the search on 40 real vectors

Three readable vectors hide what a search does among hundreds of dimensions. The 40 notes in this cell come from the [hybrid recall](https://relatedb.ai/cookbook/hybrid-recall/) recipe, with the same text and vectors: notes from three working sessions of one project, each with the 384-dimension vector that recipe's `hash-384` embedder computed. That embedder [hashes lowercased words and character trigrams](https://relatedb.ai/cookbook/hybrid-recall/#embeddings-computed-ahead-of-time), so it measures shared words rather than meaning, which is enough to see the mechanics. The cell stores each note with its session and its first topic, and creates a cosine index that can filter on the session:

```json data=notes src=https://relatedb.ai/docs/guides/cypher/vector-search/data/notes.json
{
  "data_block": "notes",
  "inline": false,
  "url": "https://relatedb.ai/docs/guides/cypher/vector-search/data/notes.json",
  "media_type": "application/json",
  "bytes": 76107,
  "sha256": "f70448d164c694ffaf673a17e903f036fc01f5af037eba68d390c5c6ee0790e8",
  "shape": {"notes":"array of 40 objects"},
  "first": {"notes":{"id":"n01","session":"session-1","topics":["planning"],"text":"Relay kickoff: ticket intake, the agent console and billing all live in one monorepo.","e":"array of 384 numbers"}}
}
```

```cypher run name=load-notes params=notes
UNWIND $notes AS r
CREATE (:Note {id: r.id, text: r.text, session: r.session, topic: r.topics[0],
               embedding: vector(r.e, 384, FLOAT32)});
CREATE VECTOR INDEX note_embedding IF NOT EXISTS
FOR (n:Note) ON (n.embedding) WITH [n.session]
OPTIONS {indexConfig: {`vector.dimensions`: 384, `vector.similarity_function`: 'cosine'}};
MATCH (n:Note)
RETURN count(n) AS notes, count(DISTINCT n.session) AS sessions
```

```table
| notes | sessions |
|-------|----------|
| 40    | 3        |
1 row · nodes created: 40, properties set: 200, labels added: 40, indexes added: 1
```

The question is the recipe's first, embedded the same way. `$k` is the number of notes the search returns:

```json data=recall
{"q": "what did we decide about oauth tokens", "k": 5, "session": "session-3",
 "v": [0.0,0.185695,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.185695,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,-0.185695,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,-0.185695,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.185695,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,-0.185695,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.185695,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.185695,0.0,0.0,0.0,0.0,0.0,0.0,0.371391,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.371391,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.185695,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.185695,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.185695,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.185695,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,-0.185695,0.0,0.0,0.0,0.0,0.0,0.0,0.0,-0.185695,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,-0.185695,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,-0.185695,0.0,0.0,0.0,0.0,0.0,-0.185695,0.0,0.0,0.0,0.0,0.0,0.0,0.0,-0.185695,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,-0.185695,0.0,0.0,0.0,0.0,0.0,0.0,-0.185695,0.0,0.0,0.0,0.0,0.185695,0.0,0.0]}
```

```cypher run name=recall params=recall
MATCH (n:Note)
SEARCH n IN (VECTOR INDEX note_embedding FOR $v LIMIT $k) SCORE AS score
RETURN n.id AS id, n.session AS session, round(score, 4) AS score, n.text AS text
ORDER BY score DESC, id
```

```table
| id    | session     | score  | text                                                                            |
|-------|-------------|--------|---------------------------------------------------------------------------------|
| "n02" | "session-1" | 0.6252 | "Customer login goes through Auth0; refresh tokens live in HttpOnly cookies."   |
| "n25" | "session-2" | 0.5692 | "Refresh tokens rotate on every use once Okta is live."                         |
| "n03" | "session-1" | 0.5585 | "OAuth callbacks are published to Kafka so the audit service sees every login." |
| "n15" | "session-2" | 0.5536 | "Migrate customer login from Auth0 to Okta before the April renewal."           |
| "n04" | "session-1" | 0.5435 | "Each tenant gets its own Kafka topic for ticket events."                       |
5 rows
```

The same search with a filter inside it keeps the notes of `$session` before it ranks:

```cypher run name=recall-session params=recall
MATCH (n:Note)
SEARCH n IN (VECTOR INDEX note_embedding FOR $v WHERE n.session = $session LIMIT $k) SCORE AS score
RETURN n.id AS id, n.session AS session, round(score, 4) AS score, n.text AS text
ORDER BY score DESC, id
```

```table
| id    | session     | score  | text                                                                                                                                                                           |
|-------|-------------|--------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| "n31" | "session-3" | 0.5386 | "Magic-link login is requested by three enterprise customers."                                                                                                                 |
| "n35" | "session-3" | 0.5383 | "Tempo retention is fourteen days; older traces are sampled out."                                                                                                              |
| "n29" | "session-3" | 0.5287 | "Okta cutover is done; Auth0 stays read-only for thirty days."                                                                                                                 |
| "n26" | "session-3" | 0.5279 | "Incident KAFKA-142: a broker disk filled overnight because retention on the audit stream was unbounded; retention is now capped and a disk alert pages the on-call engineer." |
| "n34" | "session-3" | 0.5271 | "Deploy freeze for the billing launch starts on the 23rd."                                                                                                                     |
5 rows
```

An index ranks with the similarity function it was created with. To rank with the other one without a second index, order by the function; this statement reads every note instead of using the index:

```cypher run name=recall-euclidean params=recall
MATCH (n:Note)
WITH n, vector.similarity.euclidean(n.embedding, $v) AS score
ORDER BY score DESC, n.id LIMIT $k
RETURN n.id AS id, n.session AS session, round(score, 4) AS score, n.text AS text
```

```table
| id    | session     | score  | text                                                                            |
|-------|-------------|--------|---------------------------------------------------------------------------------|
| "n02" | "session-1" | 0.4001 | "Customer login goes through Auth0; refresh tokens live in HttpOnly cookies."   |
| "n25" | "session-2" | 0.3672 | "Refresh tokens rotate on every use once Okta is live."                         |
| "n03" | "session-1" | 0.3615 | "OAuth callbacks are published to Kafka so the audit service sees every login." |
| "n15" | "session-2" | 0.359  | "Migrate customer login from Auth0 to Okta before the April renewal."           |
| "n04" | "session-1" | 0.3539 | "Each tenant gets its own Kafka topic for ticket events."                       |
5 rows
```

The explorer runs these three statements on this page's database. Each point is a note, placed by the first two principal components of the 40 vectors and coloured by topic; the cross is the question.

The top 5 by `recall` for "what did we decide about oauth tokens", as recorded with k = 5:

| Rank | id  | session   | score  | text                                                                          |
| ---- | --- | --------- | ------ | ----------------------------------------------------------------------------- |
| 1    | n02 | session-1 | 0.6252 | Customer login goes through Auth0; refresh tokens live in HttpOnly cookies.   |
| 2    | n25 | session-2 | 0.5692 | Refresh tokens rotate on every use once Okta is live.                         |
| 3    | n03 | session-1 | 0.5585 | OAuth callbacks are published to Kafka so the audit service sees every login. |
| 4    | n15 | session-2 | 0.5536 | Migrate customer login from Auth0 to Okta before the April renewal.           |
| 5    | n04 | session-1 | 0.5435 | Each tenant gets its own Kafka topic for ticket events.                       |

The picture places the 40 vectors of the `notes` block (384 dimensions) on their first two principal components, which hold 7.2% and 6.4% of the variance, computed when the page was recorded. The cross is the question vector `v`, and the rings mark the top 5, numbered by rank.

Live: a k slider runs the page's search statements on its database in your browser with that k, and the view marks the ids the engine returns.

![The 40 vectors of notes on two principal components, with the question and its top 5](https://relatedb.ai/docs/guides/cypher/vector-search/element-vector-explorer-1-light.svg)

Three things to notice:

- The picture flattens 384 dimensions into two, so a ringed note can sit farther from the cross than one the search skipped. The engine ranks in all 384 dimensions; the picture only shows which notes it returned.
- With the pre-filter on, the notes outside session 3 grey out and the search still returns `k` notes, all from session 3: the filter runs before the ranking and the `LIMIT`.
- The `hash-384` vectors have length 1, so cosine and euclidean rank the notes in the same order and only the scores differ. For unit vectors the squared distance is `2 - 2 * cosine`, and both scores fall as it grows.

A question can only be searched against vectors from the model that embeds it, so a sentence of your own cannot join these notes. The [Query the relate docs](https://relatedb.ai/cookbook/query-the-relate-docs/#ask-your-own-question) recipe searches docs sections embedded with the bge-small-en-v1.5 model, and in a browser it can download that model, embed a question you type, and search with it.

## Read the score

The two similarity functions return a rebounded score, not a raw cosine or distance. Thresholds belong on these scales:

| Function                            | Range                                                 | Closer means | Examples                                                          |
| ----------------------------------- | ----------------------------------------------------- | ------------ | ----------------------------------------------------------------- |
| `vector.similarity.cosine(a, b)`    | 0 to 1                                                | higher       | `1.0` same direction, `0.5` orthogonal, `0.0` opposite            |
| `vector.similarity.euclidean(a, b)` | greater than 0, up to 1: `1 / (1 + squared distance)` | higher       | `1.0` identical; `[0, 0]` and `[3, 4]` give `0.03846153989434242` |
| `vector_distance(a, b, <metric>)`   | the raw metric                                        | lower        | `EUCLIDEAN` for `[0, 0]` and `[3, 4]` gives `5.0`                 |

```cypher run name=scores
RETURN vector.similarity.cosine([1, 0], [1, 0]) AS same,
       vector.similarity.cosine([1, 0], [0, 1]) AS orthogonal,
       vector.similarity.cosine([1, 0], [-1, 0]) AS opposite,
       vector.similarity.euclidean([0, 0], [3, 4]) AS euclidean,
       vector_distance(vector([0, 0], 2, FLOAT32), vector([3, 4], 2, FLOAT32), EUCLIDEAN) AS distance
```

```table
| same | orthogonal | opposite | euclidean           | distance |
|------|------------|----------|---------------------|----------|
| 1.0  | 0.5        | 0.0      | 0.03846153989434242 | 5.0      |
1 row
```

The similarity functions take a list or a typed vector; `vector_distance` takes typed vectors only. The [search reference](https://relatedb.ai/docs/reference/cypher/search/#functions) lists the metrics.

## Combine with full-text search

A vector search finds what is near in meaning; a full-text search finds the exact words. To use both, chain one `MATCH … SEARCH` per index through `WITH`, so the second search keeps only what the first found, or fuse the two ranked lists by reciprocal rank. [Hybrid recall](https://relatedb.ai/cookbook/hybrid-recall/) builds the fused version over 40 notes in one statement, and the [search reference](https://relatedb.ai/docs/reference/cypher/search/#hybrid-search) has both forms.

## Verify

`SHOW INDEXES` lists the index, the properties it covers (the vector property, then the `WITH` properties) and its state:

```cypher run name=verify
SHOW INDEXES YIELD name, type, labelsOrTypes, properties, state
WHERE type = 'VECTOR'
RETURN name, type, labelsOrTypes, properties, state
```

```table
| name             | type     | labelsOrTypes | properties               | state    |
|------------------|----------|---------------|--------------------------|----------|
| "note_embedding" | "VECTOR" | ["Note"]      | ["embedding", "session"] | "ONLINE" |
| "person_focus"   | "VECTOR" | ["Person"]    | ["focus", "role"]        | "ONLINE" |
2 rows
```

Two `ONLINE` indexes, `person_focus` and `note_embedding`, and two rows from the nearest-node search mean the index serves the query.

## If it goes wrong

A query vector whose length differs from `vector.dimensions` fails:

```cypher run name=wrong-length expect=error
MATCH (p:Person)
SEARCH p IN (VECTOR INDEX person_focus FOR [0.1, 0.9] LIMIT 2) SCORE AS score
RETURN p.name AS name, score
```

```table
error: type error: VectorIndexDimensionalityMismatch
```

| Message                                                           | Cause                                                                                       | Remedy                                                                                                                                 |
| ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `type error: VectorIndexDimensionalityMismatch`                   | The query vector's length is not the index's `vector.dimensions`.                           | Embed the question with the same model as the stored vectors.                                                                          |
| `syntax error: UndefinedVariable: variable 'score' not defined`   | The query reads `score` without `SCORE AS`.                                                 | Add `SCORE AS score` after the closing parenthesis.                                                                                    |
| `syntax error: parse error: expected LIMIT, got ")" (RightParen)` | The search has no `LIMIT`.                                                                  | Add `LIMIT <k>` inside the parentheses.                                                                                                |
| `WrongPropertyForVectorSearchFilter`                              | The `WHERE` inside the search names a property the index does not declare.                  | Add the property to the index's `WITH [...]` list, or filter after the search.                                                         |
| A node never comes back, and nothing fails                        | Its property holds a list or vector of another length: the index skips it without an error. | Store vectors with `vector(list, <dimensions>, FLOAT32)` at the index's dimensions; it refuses a list of another length at write time. |

## Related

- [Search](https://relatedb.ai/docs/reference/cypher/search/): index DDL, every `SEARCH` form, options and defaults, and the functions.
- [Hybrid recall](https://relatedb.ai/cookbook/hybrid-recall/): vector and full-text search fused in one statement.
- [Differences from Neo4j](https://relatedb.ai/docs/reference/cypher/differences-from-neo4j/): the procedure forms and what to write instead.

## Terms used on this page

- **node**: An entity in the graph: it carries zero or more labels and a map of properties, and relationships connect it to other nodes.
- **search clause**: The Cypher 25 `SEARCH` clause, which restricts a `MATCH` to the top results of a vector or full-text index and binds their score.
- **in-memory store**: A store that lives only in the process that opened it, named `:memory:`; the default `--database`, gone when the process exits.
- **parameter**: A named value a statement refers to as `$name` and the caller binds separately, such as with `--param name=value` on the CLI.
- **property**: A named value stored on a node or relationship, such as `name: 'Ada'`; values are scalars, temporal and spatial values, vectors, or lists of them.
