How to Use Vector Indexes for Semantic Search in Neomodel
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, 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:
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.
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. This container holds the query vector, top-k limit, optional similarity threshold, and target attribute name.
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.
# 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:
# 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 (and its async counterpart in 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:
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_namedoes not exist on the model, neomodel raises anAttributeError(lines 48-55 inneomodel/sync_/match.py). - Missing Vector Index: If the attribute exists but lacks a
vector_indexconfiguration, anAttributeErrorspecifies 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
ArrayPropertywith aVectorIndexinstance to store embeddings and configure similarity algorithms inneomodel/properties.py. - Create the underlying Neo4j index by calling
db.install_labels(Model)before executing searches. - Perform semantic searches by passing a
VectorFilterto.filter(), specifyingtopk,candidate_vector, and optionally athresholddefined inneomodel/semantic_filters.py. - The
QueryBuilderinneomodel/sync_/match.pytranslates these filters into optimizedCALL 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 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →