# How to Use Vector Indexes for Semantic Search in Neomodel

> Learn how Neomodel uses vector indexes for semantic search. Combine VectorIndex metadata and VectorFilter for powerful Cypher clauses via the query builder.

- Repository: [Neo4j Contrib/neomodel](https://github.com/neo4j-contrib/neomodel)
- Tags: how-to-guide
- Published: 2026-03-08

---

**Neomodel enables semantic search by combining `VectorIndex` metadata on `ArrayProperty` fields with `VectorFilter` instances passed to `.filter()`, which the query builder converts into native Neo4j `CALL db.index.vector.query*` Cypher clauses.**

Neo4j 5+ introduced native vector indexes for high-performance similarity search, and the neo4j-contrib/neomodel library exposes this capability directly through its object-graph mapping layer. By declaring a `VectorIndex` on an `ArrayProperty` and querying with a `VectorFilter`, you can perform nearest-neighbor retrieval without writing raw Cypher.

## Define a Vector-Indexed Property

Semantic search requires embeddings stored as vector arrays. In [`neomodel/properties.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/properties.py), the `VectorIndex` class configures the index dimensions and similarity algorithm, while `ArrayProperty` stores the actual embedding vectors.

To enable vector search on a model, attach a `VectorIndex` instance to an `ArrayProperty`:

```python
from neomodel import (
    StructuredNode,
    StringProperty,
    FloatProperty,
    ArrayProperty,
    VectorIndex,
)

class Document(StructuredNode):
    title = StringProperty()
    embedding = ArrayProperty(
        base_property=FloatProperty(),
        vector_index=VectorIndex(dimensions=1536, similarity_function="cosine")
    )

```

The `VectorIndex` parameters determine how Neo4j constructs the underlying index. The `dimensions` argument must match your embedding model output size, and `similarity_function` accepts `"cosine"`, `"euclidean"`, or `"dotproduct"`. Neomodel derives the index name automatically using the pattern `vector_index_<Label>_<property>`.

## Install the Vector Schema

Unlike standard indexes, vector indexes must be explicitly created in the database before querying. The `db.install_labels()` method in neomodel inspects your model definitions and executes the appropriate `CALL db.index.vector.createNodeIndex` Cypher command.

```python
from neomodel import db

db.install_labels(Document)

```

This call creates the index `vector_index_Document_embedding` in Neo4j. You only need to run this once per environment, typically during application initialization or migrations.

## Execute Semantic Queries with VectorFilter

Neomodel abstracts vector search behind the standard `.filter()` API using the `VectorFilter` class defined in [`neomodel/semantic_filters.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/semantic_filters.py). This container holds the query vector, top-k limit, optional similarity threshold, and target attribute name.

```python
from neomodel import VectorFilter

query_vector = [0.15, 0.22, 0.08, 0.04]  # Your embedding here

results = Document.nodes.filter(
    vector_filter=VectorFilter(
        topk=5,
        vector_attribute_name="embedding",
        candidate_vector=query_vector,
        threshold=0.7
    )
).all()

```

The query returns a list of tuples where each tuple contains the model instance and its similarity score as a float: `(Document, 0.823)`. The `threshold` parameter filters results by minimum similarity, accepting values between 0 and 1 for cosine similarity.

## Combine Vector Search with Standard Filters

Vector queries integrate seamlessly with existing neomodel filter syntax. The `QueryBuilder` class processes both the `VectorFilter` and standard property filters simultaneously, generating a single optimized Cypher query.

```python

# Find documents similar to the query vector but only if title contains "Python"

results = Document.nodes.filter(
    vector_filter=VectorFilter(
        topk=10,
        vector_attribute_name="embedding",
        candidate_vector=query_vector
    ),
    title__contains="Python"
).all()

```

You can also traverse relationships during vector searches. When combining with relationship filters, the result tuple expands to include the related nodes:

```python

# Assuming Article has a RelationshipFrom to Author

results = Article.nodes.filter(
    vector_filter=VectorFilter(
        topk=3,
        vector_attribute_name="embedding",
        candidate_vector=query_vector
    ),
    authors__name="Alice"
).all()

# Returns: [(Article, Author, AuthoredBy, score), ...]

```

## Query Architecture and Cypher Generation

According to the neo4j-contrib/neomodel source code, when you pass a `VectorFilter` to `.filter()`, neomodel stores it in the `NodeSet.vector_query` attribute. The `QueryBuilder.build_vector_query()` method in [`neomodel/sync_/match.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/sync_/match.py) (and its async counterpart in [`neomodel/async_/match.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/async_/match.py)) validates that the specified attribute exists and has a `vector_index` configured.

The builder then constructs a Cypher query using the `CALL db.index.vector.queryTopK` procedure:

```cypher
CALL db.index.vector.queryTopK('vector_index_Document_embedding', $topk, $candidateVector) 
YIELD node, score 
RETURN node, score

```

The method injects this clause into the query AST, which the Cypher renderer processes alongside any additional `WHERE` clauses from standard filters. This architecture isolates Neo4j-specific vector syntax inside the query builder while maintaining a clean Python API.

## Error Handling and Validation

The `build_vector_query()` method implements strict validation to prevent runtime Cypher errors:

- **Missing Attribute**: If `vector_attribute_name` does not exist on the model, neomodel raises an `AttributeError` (lines 48-55 in [`neomodel/sync_/match.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/sync_/match.py)).
- **Missing Vector Index**: If the attribute exists but lacks a `vector_index` configuration, an `AttributeError` specifies that the property is not vector-indexed (lines 56-59).
- **Invalid Threshold**: If the threshold parameter is not a numeric type, neomodel raises a `ValueError` (lines 61-63).

These safeguards ensure that schema mismatches are caught during query construction rather than at the database level.

## Summary

- Declare vector-capable properties using `ArrayProperty` with a `VectorIndex` instance to store embeddings and configure similarity algorithms in [`neomodel/properties.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/properties.py).
- Create the underlying Neo4j index by calling `db.install_labels(Model)` before executing searches.
- Perform semantic searches by passing a `VectorFilter` to `.filter()`, specifying `topk`, `candidate_vector`, and optionally a `threshold` defined in [`neomodel/semantic_filters.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/semantic_filters.py).
- The `QueryBuilder` in [`neomodel/sync_/match.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/sync_/match.py) translates these filters into optimized `CALL db.index.vector.query*` Cypher clauses.
- Results return as tuples containing the model instance and similarity score, compatible with standard neomodel filter chaining and relationship traversals.

## Frequently Asked Questions

### What Neo4j version is required for vector indexes in neomodel?

Neomodel's vector index support targets Neo4j 5.11 or later, which introduced the `db.index.vector.queryTopK` and `db.index.vector.queryRange` procedures. Earlier versions lack native vector index support and will return procedure not found errors when executing `VectorFilter` queries.

### Can I use vector search with asynchronous neomodel queries?

Yes, the async API in [`neomodel/async_/match.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/async_/match.py) mirrors the synchronous implementation. Import `VectorFilter` from the same module and pass it to `.filter()` on async node sets. The `build_vector_query()` method in the async module handles the Cypher generation identically, allowing non-blocking semantic searches with `await Document.nodes.filter(vector_filter=...).all()`.

### Why am I getting an AttributeError when using VectorFilter?

An `AttributeError` typically indicates one of two issues: either the `vector_attribute_name` string does not match any property defined on your model, or the specified property exists but was not configured with a `VectorIndex` during class definition. Verify that your model uses `ArrayProperty(base_property=FloatProperty(), vector_index=VectorIndex(...))` and that the attribute name in your filter matches the property name exactly.

### How do I choose between cosine, euclidean, and dotproduct similarity?

Set the `similarity_function` parameter in `VectorIndex` based on your embedding model's characteristics. Cosine similarity works best for normalized text embeddings from models like OpenAI's text-embedding-ada-002. Euclidean distance suits dense vectors where magnitude matters, while dotproduct is optimized for models specifically trained to maximize inner product similarity. The choice must match the algorithm used during vector generation to ensure accurate nearest-neighbor results.