Documentation tagged with Hierarchical Navigable Small World (HNSW) in the Geode graph database. HNSW is an algorithm for approximate nearest neighbor (ANN) search in high-dimensional vector spaces, enabling efficient vector similarity search for machine learning applications.
Introduction to HNSW
Hierarchical Navigable Small World (HNSW) is a graph-based algorithm for approximate nearest neighbor search that has become the industry standard for vector similarity search. Developed by Yury Malkov and Dmitry Yashunin in 2016, HNSW builds a multi-layer proximity graph that enables logarithmic search complexity while maintaining high recall.
The algorithm solves a critical problem in modern AI applications: how to efficiently search through millions or billions of high-dimensional vectors (embeddings) to find the most similar items. Traditional exact search has O(N) complexity—you must compare against every vector. HNSW achieves sub-linear search time through a clever graph structure.
HNSW is used in:
- Semantic search: Find documents similar to a query embedding
- Recommendation systems: Discover similar products, content, or users
- Image search: Find visually similar images
- Anomaly detection: Identify outliers in embedding space
- Retrieval-Augmented Generation (RAG): Find relevant context for LLMs
Geode’s HNSW implementation integrates vector search seamlessly with graph queries, enabling powerful combined operations like “find similar products purchased by friends of this user.”
Core HNSW Concepts
Navigable Small World Graphs
HNSW builds on the concept of “small world” networks—graphs where most nodes can be reached from any other node in a small number of hops, despite the network’s large size. Examples include social networks (six degrees of separation) and the World Wide Web.
A navigable small world graph adds long-range connections that enable efficient greedy search:
Layer 2: A -------- B
| |
Layer 1: A -- C -- B -- D
| | | |
Layer 0: A-C-E-B-D-F-G-H (all nodes)
Search starts at the top layer (sparse, long-range connections) and descends to lower layers (dense, short-range connections), refining the result at each level.
Hierarchical Construction
HNSW uses a hierarchical structure with multiple layers:
- Layer 0: Contains all vectors with dense connections to nearby neighbors
- Layer 1+: Contain progressively fewer vectors, selected probabilistically
- Top layer: Has very few nodes, enabling fast initial navigation
Each node’s maximum layer is chosen randomly using an exponentially decaying probability:
P(layer = l) = (1/M)^l
Where M is typically 4-6, giving:
- 100% of nodes at layer 0
- ~20% of nodes at layer 1
- ~4% of nodes at layer 2
- ~0.8% of nodes at layer 3
This creates a logarithmic search structure similar to skip lists.
Greedy Search Algorithm
HNSW search is beautifully simple:
- Start at entry point: Begin at a random node in the top layer
- Greedy local search: Move to the neighbor closest to the query
- Repeat until local minimum: Stop when no neighbor is closer
- Descend layer: Drop to the next layer, continue search
- Return results: At layer 0, return k nearest neighbors
This achieves O(log N) complexity in practice.
Construction Algorithm
Building an HNSW index:
- For each vector to insert:
- Choose maximum layer l randomly
- Find nearest neighbors using greedy search
- Connect to M nearest neighbors at each layer
- Use Mmax connections at layer 0 for higher accuracy
- Prune connections to maintain navigability
The construction is online—you can add vectors incrementally without rebuilding the entire index.
How HNSW Works in Geode
Vector Properties
Store embeddings as node properties:
-- Create node with embedding
INSERT (:Document {
id: 'doc-123',
title: 'Introduction to Graph Databases',
content: '...',
embedding: [0.23, -0.45, 0.67, ..., 0.12] -- 768-dimensional vector
});
Creating HNSW Indexes
Build an HNSW index on vector properties:
-- Create HNSW index for semantic search
CREATE INDEX document_embeddings
FOR (d:Document)
ON (d.embedding) USING vector
WITH (metric = 'cosine');
Parameters:
- metric: Distance metric the index is built for. One of
l2(default),euclidean,cosine,dot,dot_product,inner_product.
metric is the only supported parameter. Vector dimensionality is inferred
from the indexed data, and the HNSW graph parameters (m, ef_construction,
ef_search) are not currently tunable — they use engine-managed defaults and
are reserved for a future release.
Vector Similarity Search
Query similar vectors:
-- Find 10 most similar documents to a query embedding
MATCH (d:Document)
WHERE similarity(d.embedding, $query_embedding) > 0.7
RETURN d.title, d.id, similarity(d.embedding, $query_embedding) AS similarity
ORDER BY similarity DESC
LIMIT 10;
-- Or use the k-nearest-neighbour function, which returns the ids of the k
-- closest elements for a vector property
WITH vector_knn('embedding', $query_embedding, 10, 'cosine') AS ids
MATCH (d:Document)
WHERE id(d) IN ids
RETURN d.title, similarity(d.embedding, $query_embedding) AS similarity
ORDER BY similarity DESC;
There is no CALL vector.search(...) procedure: vector search is expressed with
the vector functions (similarity(), distance(), vector_knn()) inside a
normal MATCH.
Combining Vector and Graph Queries
The real power: integrate vector search with graph traversal:
-- Find similar products purchased by friends
MATCH (me:User {id: $userId})-[:FRIEND]->(friend:User)
-[:PURCHASED]->(product:Product)
WHERE similarity(product.embedding, $query_embedding) > 0.8
RETURN DISTINCT product.name,
similarity(product.embedding, $query_embedding) AS similarity,
COUNT(DISTINCT friend) AS friend_count
ORDER BY similarity DESC, friend_count DESC
LIMIT 10;
-- Semantic search with metadata filtering
MATCH (doc:Document)
WHERE doc.category = 'technical'
AND doc.publish_date > date('2024-01-01')
AND similarity(doc.embedding, $query_embedding) > 0.75
RETURN doc.title, doc.author, similarity(doc.embedding, $query_embedding) AS score
ORDER BY score DESC
LIMIT 20;
Use Cases
Semantic Search
Find documents by meaning, not just keywords:
-- Traditional keyword search: misses synonyms, context
MATCH (d:Document)
WHERE d.content CONTAINS 'database'
RETURN d.title;
-- Semantic search: understands meaning
-- $query_embedding is the embedding of "systems for storing data"
WITH vector_knn('embedding', $query_embedding, 10, 'cosine') AS ids
MATCH (d:Document)
WHERE id(d) IN ids
RETURN d.title;
-- Returns documents about databases, even without keyword "database"
Recommendation Systems
Discover similar items:
-- Content-based recommendations
MATCH (item:Product {id: $productId})
MATCH (similar_product:Product)
WHERE similar_product.id <> $productId
AND similar_product.in_stock = true
WITH similar_product,
similarity(similar_product.embedding, item.embedding) AS similarity
WHERE similarity > 0.7
RETURN similar_product.name, similarity
ORDER BY similarity DESC
LIMIT 10;
Retrieval-Augmented Generation (RAG)
Find relevant context for LLM prompts:
-- Retrieve relevant context for RAG
WITH vector_knn('embedding', $question_embedding, 5, 'cosine') AS ids
MATCH (doc:Document)
WHERE id(doc) IN ids
RETURN doc.content AS context,
similarity(doc.embedding, $question_embedding) AS similarity
ORDER BY similarity DESC;
Pass the returned context to your LLM to ground responses in your knowledge base.
Anomaly Detection
Identify outliers in embedding space:
-- Find anomalous user behavior: users whose behaviour embedding is far from
-- every other user's is an outlier
MATCH (u:User)
MATCH (other:User)
WHERE id(other) <> id(u)
WITH u, avg(similarity(u.behavior_embedding, other.behavior_embedding)) AS avg_similarity
WHERE avg_similarity < 0.5 -- Low similarity to the population = anomaly
RETURN u.id, u.name, avg_similarity
ORDER BY avg_similarity ASC;
Image Search
Find visually similar images:
-- Image similarity using CLIP embeddings
MATCH (img:Image)
WHERE similarity(img.clip_embedding, $query_image_embedding) > 0.85
RETURN img.url, img.caption, similarity(img.clip_embedding, $query_image_embedding) AS similarity
ORDER BY similarity DESC
LIMIT 20;
Best Practices
Choosing HNSW Parameters
Balance accuracy, speed, and memory:
The HNSW graph parameters (m, ef_construction, ef_search) are managed by
the engine and are not currently exposed as index options. Tune the accuracy,
speed and memory trade-off with the levers you do control:
metric: Match the index metric to the function your queries use, otherwise the index cannot serve the query.
Query shape: Pre-filter on labels and properties to shrink the candidate
set, and keep LIMIT as small as the application allows.
Recall: Loosen the distance threshold and raise LIMIT, then re-rank in
the query when you need more relevant results.
Memory: Use smaller embeddings (384 dims instead of 768) and index only the properties you actually search.
Embedding Generation
Use high-quality embeddings:
Choose appropriate model:
- Text: BERT, RoBERTa, sentence-transformers, OpenAI text-embedding-3
- Images: CLIP, ResNet, EfficientNet
- Multimodal: CLIP, ALIGN
Normalize embeddings: For cosine similarity, normalize to unit length
embedding = model.encode(text) embedding = embedding / np.linalg.norm(embedding)Use consistent dimensions: All vectors in an index must have same dimensionality
Index Maintenance
Incremental updates: Add vectors as needed
-- Add new document to existing index
INSERT (:Document {
id: 'new-doc',
title: 'New Article',
embedding: [...] -- Automatically added to index
});
Rebuild for better quality: Periodically rebuild for optimal graph structure
-- Rebuild index
DROP INDEX document_embeddings;
CREATE INDEX document_embeddings FOR (d:Document) ON (d.embedding) USING vector
WITH (metric = 'cosine');
Query Optimization
Pre-filter when possible:
-- Efficient: Filter before vector search
MATCH (d:Document)
WHERE d.category = 'science'
AND d.year > 2020
AND similarity(d.embedding, $query) > 0.8
RETURN d;
-- Less efficient: score every document, then post-filter
MATCH (d:Document)
WITH d, similarity(d.embedding, $query) AS score
WHERE d.category = 'science' -- Post-filter
RETURN d, score;
Size k for the accuracy you need:
-- High-recall search: retrieve more candidates, then re-rank
WITH vector_knn('embedding', $q, 100, 'cosine') AS ids
MATCH (d:Document)
WHERE id(d) IN ids
RETURN d, similarity(d.embedding, $q) AS score
ORDER BY score DESC
LIMIT 10;
-- Fast search (may miss some results)
WITH vector_knn('embedding', $q, 10, 'cosine') AS ids
MATCH (d:Document)
WHERE id(d) IN ids
RETURN d;
Performance Characteristics
Time Complexity
- Construction: O(N log N) expected
- Search: O(log N) expected
- Insertion: O(log N) expected
- Deletion: O(log N) expected
Memory Usage
Memory = N * (dimensions * 4 bytes + M * 8 bytes per layer)
Example (1M vectors, 768 dims, engine default M=16):
= 1M * (768 * 4 + 16 * 8 * 2.5 layers)
= 1M * (3,072 + 320)
= 3.4 GB
M here is the internal HNSW graph degree. It is fixed by the engine and is
not a user-settable index option.
Performance Benchmarks
Typical performance (10k vectors, 10-NN):
- Latency: 1-5ms at ~90% recall
- Recall@10: ~90% (parameter dependent)
- Notes: Performance varies with result set size, vector dimensions, and hardware
Monitoring and Troubleshooting
Index Statistics
There is no vector.index.* procedure namespace. Index metadata is exposed
through the standard introspection procedure:
-- List indexes (name, type, label, properties)
CALL db.indexes();
Query Performance
-- Profile a vector query
PROFILE
WITH vector_knn('embedding', $q, 10, 'cosine') AS ids
MATCH (d:Document)
WHERE id(d) IN ids
RETURN d, similarity(d.embedding, $q) AS similarity;
Common Issues
Low recall: Loosen the distance threshold, raise LIMIT/k and re-rank
High latency: Lower LIMIT/k or optimize pre-filtering
Out of memory: Use smaller embeddings or partition data
Slow indexing: Batch inserts and rebuild the index once after a bulk load
Related Topics
- Vector Search - General vector search capabilities
- Machine Learning - ML integration
- Embeddings - Working with embeddings
- Semantic Search - Semantic search applications
- Recommendation Systems - Building recommenders
- BM25 Ranking - Traditional text search ranking
Further Reading
- Vector Search - Complete vector search documentation
- Embeddings - Working with embeddings
- Performance - Performance optimization
- AI Integration - AI and machine learning integration
- Original HNSW Paper - Academic paper
Geode’s HNSW implementation brings vector similarity search to graph databases, enabling AI applications that combine semantic understanding with graph relationships.
Advanced HNSW Implementation Details
Layer Assignment Probability
HNSW layers are assigned using exponential decay:
P(layer = l) = (1/M)^l
For M = 4:
- Layer 0: 100% of nodes
- Layer 1: 25% of nodes
- Layer 2: 6.25% of nodes
- Layer 3: 1.56% of nodes
Connection Strategy
Each node maintains:
- M connections at layers > 0
- 2M connections at layer 0 (base layer for higher recall)
Heuristic selection: Choose neighbors that maximize navigability:
score(candidate) = distance(query, candidate) - distance(query, current_best)
Query Optimization Strategies
Adaptive Candidate Sizing
Dynamically adjust search effort with k:
-- High-stakes query: retrieve a deep candidate set and re-rank
WITH vector_knn('embedding', $query, 100, 'cosine') AS ids
MATCH (d:Document)
WHERE id(d) IN ids
RETURN d, similarity(d.embedding, $query) AS similarity
ORDER BY similarity DESC
LIMIT 10;
-- Batch processing: optimize throughput
WITH vector_knn('embedding', $query, 10, 'cosine') AS ids
MATCH (p:Product)
WHERE id(p) IN ids
RETURN p;
Early Termination
The HNSW search-effort parameter (ef_search) is not exposed. Widen the
candidate set with k instead, and stop as soon as the top score is good
enough:
async def adaptive_vector_search(client, query_emb, min_confidence=0.95):
for k in [10, 32, 64, 128, 256]:
results, _ = await client.query("""
WITH vector_knn('embedding', $query, $k, 'cosine') AS ids
MATCH (d:Document)
WHERE id(d) IN ids
RETURN d, similarity(d.embedding, $query) AS similarity
ORDER BY similarity DESC
""", {"query": query_emb, "k": k})
top_score = results.rows[0]['similarity']
if top_score >= min_confidence:
return results.rows # Early termination
return results.rows # Max effort reached
Index Construction Strategies
Bulk Loading Optimization
Build index from sorted data:
-- Sort nodes by degree (high-degree nodes first)
MATCH (n:Node)
WITH n, SIZE((n)-[:RELATED]-()) AS degree
ORDER BY degree DESC
-- Insert in batches
CALL {
WITH n
SET n.embedding = $computed_embedding
} IN TRANSACTIONS OF 10000 ROWS;
-- Rebuild index after bulk load
DROP INDEX embeddings_idx;
CREATE INDEX embeddings_idx FOR (n:Node) ON (n.embedding) USING vector
WITH (metric = 'cosine');
Incremental Index Maintenance
-- Track index quality metric
MATCH (stats:IndexStats {index_name: 'embeddings_idx'})
WITH stats.inserts_since_rebuild AS inserts,
stats.total_vectors AS total
WITH inserts * 1.0 / total AS insert_ratio
WHERE insert_ratio > 0.1 // 10% growth
-- There is no index-rebuild procedure: drop and recreate the index instead
DROP INDEX embeddings_idx;
CREATE INDEX embeddings_idx FOR (n:Node) ON (n.embedding) USING vector
WITH (metric = 'cosine');
Distance Metrics Deep Dive
Cosine Similarity
Best for normalized vectors:
cosine(u, v) = (u · v) / (||u|| × ||v||)
= Σ(ui × vi) / sqrt(Σui²) × sqrt(Σvi²)
Range: [-1, 1]
- 1: Identical direction
- 0: Orthogonal
- -1: Opposite direction
Optimization: Pre-normalize vectors to unit length, then use dot product:
Geode has no vector-normalization function, so normalize in the client before writing the property and before binding the query vector:
-- Vectors are stored already normalized (unit length)
MATCH (n:Node {id: $id})
SET n.embedding = $normalized_embedding;
-- Use dot product (equivalent to cosine for normalized vectors)
MATCH (n:Node)
RETURN n.id, vector_dot(n.embedding, $normalized_query) AS score
ORDER BY score DESC
LIMIT 10;
Euclidean Distance (L2)
Measures absolute distance:
euclidean(u, v) = sqrt(Σ(ui - vi)²)
Properties:
- Sensitive to magnitude
- Triangle inequality holds
- Metric space properties
Manhattan Distance (L1)
Sum of absolute differences:
manhattan(u, v) = Σ|ui - vi|
Use cases:
- Sparse vectors
- Grid-based distances
- Outlier-robust similarity
Production Deployment Patterns
Multi-Index Strategy
Separate indexes for different embedding types:
-- Text embeddings (768d, BERT)
CREATE INDEX text_embeddings FOR (d:Document) ON (d.text_embedding) USING vector
WITH (metric = 'cosine');
-- Image embeddings (512d, CLIP)
CREATE INDEX image_embeddings FOR (p:Product) ON (p.image_embedding) USING vector
WITH (metric = 'cosine');
-- User embeddings (128d, custom)
CREATE INDEX user_embeddings FOR (u:User) ON (u.behavior_embedding) USING vector
WITH (metric = 'euclidean');
-- Query the property backed by the appropriate index
WITH vector_knn('text_embedding', $text_query, 10, 'cosine') AS ids
MATCH (d:Document)
WHERE id(d) IN ids
RETURN d;
Backup and Recovery
Vector indexes have no dedicated export/import/verify procedures — they are covered by the regular database backup and are rebuilt from the stored vectors:
-- Inspect the indexes that exist
CALL db.indexes();
-- Recreate an index after restoring data
DROP INDEX embeddings_idx;
CREATE INDEX embeddings_idx FOR (n:Node) ON (n.embedding) USING vector
WITH (metric = 'cosine');
Benchmarking and Performance
Recall Measurement
async def measure_recall(client, test_queries, ground_truth, k=10):
recalls = []
for query_emb, true_neighbors in zip(test_queries, ground_truth):
# HNSW approximate search
results, _ = await client.query("""
WITH vector_knn('embedding', $query, $k, 'cosine') AS ids
MATCH (n:TestVector)
WHERE id(n) IN ids
RETURN n.id AS id
""", {"query": query_emb, "k": k})
retrieved = {row['id'] for row in results.rows}
relevant = set(true_neighbors[:k])
recall = len(retrieved & relevant) / k
recalls.append(recall)
return np.mean(recalls)
# Typical results with engine-managed graph parameters:
# k=10: recall@10 ≈ 0.93, latency ≈ 2ms
# k=50: recall@10 ≈ 0.97, latency ≈ 5ms
# k=200: recall@10 ≈ 0.99, latency ≈ 15ms
Throughput vs Latency Trade-offs
# Latency-optimized (single query)
config_latency = {
"k": 10,
"batch_size": 1
}
# Latency and recall depend on dataset, dimensions, and result set size
# Throughput-optimized (batch queries)
config_throughput = {
"k": 50,
"batch_size": 100
}
# Throughput and recall depend on dataset, dimensions, and hardware
Further Reading
- HNSW Original Paper: Malkov & Yashunin (2016) - Efficient and Robust ANN Search
- Graph-Based ANN: NSW, HNSW, NSG, and DiskANN Algorithms
- Distance Metrics: Cosine, Euclidean, Inner Product, and Custom Metrics
- Index Tuning: Choosing the Right Metric and Query Shape
- Production Systems: Scaling to Billions of Vectors
Browse tagged content for comprehensive HNSW and vector search documentation.