Skip to main content

Overview

The HNSWIndex struct implements the Hierarchical Navigable Small World graph algorithm for approximate nearest neighbor (ANN) search. This is the performance core of SolVec, providing fast and accurate vector similarity search.

Parameters

M

Max connections per node per layer (default: 16)

ef_construction

Beam width during index build (default: 200)

ef_search

Beam width during query (default: 50)

Type Signature

pub struct HNSWIndex {
    m: usize,
    m_max_0: usize,
    ef_construction: usize,
    ef_search: usize,
    ml: f64,
    vectors: AHashMap<String, Vector>,
    layers: Vec<AHashMap<String, Vec<String>>>,
    entry_point: Option<String>,
    entry_point_level: usize,
    total_inserts: usize,
    total_deletes: usize,
    metric: DistanceMetric,
    dimension: Option<usize>,
}

Constructor Methods

new

Create a new HNSW index with custom parameters.
pub fn new(m: usize, ef_construction: usize, metric: DistanceMetric) -> Self
m
usize
required
Max connections per node. 16 is standard. Higher = better recall, more memory.
ef_construction
usize
required
Build-time beam width. 200 is standard. Higher = better quality, slower build.
metric
DistanceMetric
required
Distance metric for similarity computation. Options: Cosine, Euclidean, DotProduct.
Example:
use solvec_core::hnsw::HNSWIndex;
use solvec_core::types::DistanceMetric;

let mut index = HNSWIndex::new(16, 200, DistanceMetric::Cosine);

default_cosine

Create an index with sensible defaults — what most users should use.
pub fn default_cosine() -> Self
HNSWIndex
HNSWIndex
Returns an index with M=16, ef_construction=200, and Cosine metric.
Example:
let mut index = HNSWIndex::default_cosine();

Core Methods

insert

Insert a vector into the index. If a vector with the same ID already exists, it is updated.
pub fn insert(&mut self, vector: Vector) -> Result<(), SolVecError>
vector
Vector
required
The vector to insert. Must have a unique ID and non-empty values.
Result
Result<(), SolVecError>
Returns Ok(()) on success, or SolVecError::DimensionMismatch if the vector dimension doesn’t match the index dimension.
Example:
use solvec_core::types::Vector;

let vector = Vector::new("vec_1", vec![1.0, 0.0, 0.0]);
index.insert(vector)?;

query

Query the index for top-K nearest neighbors.
pub fn query(
    &self,
    query_vector: &[f32],
    top_k: usize,
) -> Result<Vec<QueryResult>, SolVecError>
query_vector
&[f32]
required
The query vector to search for.
top_k
usize
required
Number of nearest neighbors to return. Must be >= 1.
Result
Result<Vec<QueryResult>, SolVecError>
Returns results sorted by score descending (most similar first). Each QueryResult contains:
  • id: Vector ID
  • score: Similarity score (higher = more similar)
  • metadata: Associated metadata
Example:
// Insert some vectors
index.insert(Vector::new("a", vec![1.0, 0.0, 0.0]))?;
index.insert(Vector::new("b", vec![0.9, 0.1, 0.0]))?;
index.insert(Vector::new("c", vec![0.0, 1.0, 0.0]))?;

// Query for top 2 nearest neighbors
let results = index.query(&[1.0, 0.0, 0.0], 2)?;
assert_eq!(results.len(), 2);
assert_eq!(results[0].id, "a"); // Most similar

delete

Delete a vector by ID.
pub fn delete(&mut self, id: &str) -> Result<(), SolVecError>
id
&str
required
The ID of the vector to delete.
Result
Result<(), SolVecError>
Returns Ok(()) on success, or SolVecError::VectorNotFound if the vector doesn’t exist.
Example:
index.delete("vec_1")?;
assert_eq!(index.len(), 0);

update

Update a vector (convenience wrapper for delete + insert).
pub fn update(&mut self, vector: Vector) -> Result<(), SolVecError>
vector
Vector
required
The updated vector with the same ID.
Example:
let updated = Vector::new("a", vec![0.0, 1.0, 0.0]);
index.update(updated)?;

Configuration Methods

Set ef_search parameter — increase for better recall at cost of speed.
pub fn set_ef_search(&mut self, ef: usize)
ef
usize
required
The new ef_search value (minimum: 1).
Example:
index.set_ef_search(100); // Increase for better recall

Query Methods

len

Number of vectors in the index.
pub fn len(&self) -> usize
usize
usize
The total number of vectors currently indexed.

is_empty

Whether the index contains no vectors.
pub fn is_empty(&self) -> bool
bool
bool
Returns true if the index is empty, false otherwise.

metric

The distance metric used by this index.
pub fn metric(&self) -> DistanceMetric
DistanceMetric
DistanceMetric
Returns the distance metric: Cosine, Euclidean, or DotProduct.

stats

Get detailed statistics about the index.
pub fn stats(&self) -> IndexStats
IndexStats
IndexStats
Returns an IndexStats struct containing:
  • vector_count: Number of vectors
  • layer_count: Number of HNSW layers
  • entry_point_level: Top layer level
  • dimension: Vector dimension
  • total_inserts: Total insertions (including updates)
  • total_deletes: Total deletions
  • metric: Distance metric
Example:
let stats = index.stats();
println!("Vectors: {}, Layers: {}", stats.vector_count, stats.layer_count);

Serialization Methods

to_json

Serialize the entire index to JSON for persistence.
pub fn to_json(&self) -> Result<String, SolVecError>
Result
Result<String, SolVecError>
Returns JSON string representation of the index, or SolVecError::SerializationError on failure.
Example:
let json = index.to_json()?;
std::fs::write("index.json", json)?;

from_json

Deserialize an index from JSON.
pub fn from_json(json: &str) -> Result<Self, SolVecError>
json
&str
required
JSON string representation of an index.
Result
Result<HNSWIndex, SolVecError>
Returns the deserialized index, or SolVecError::SerializationError on failure.
Example:
let json = std::fs::read_to_string("index.json")?;
let restored_index = HNSWIndex::from_json(&json)?;

let results = restored_index.query(&query, 10)?;

Complete Example

use solvec_core::hnsw::HNSWIndex;
use solvec_core::types::{DistanceMetric, Vector};
use std::collections::HashMap;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create index
    let mut index = HNSWIndex::new(16, 200, DistanceMetric::Cosine);

    // Insert vectors with metadata
    let mut meta = HashMap::new();
    meta.insert(
        "source".to_string(),
        serde_json::Value::String("agent_memory".to_string()),
    );

    index.insert(Vector::with_metadata(
        "user_alex_intro",
        vec![0.9, 0.1, 0.0, 0.0],
        meta.clone(),
    ))?;

    index.insert(Vector::with_metadata(
        "user_alex_startup",
        vec![0.8, 0.2, 0.1, 0.0],
        meta.clone(),
    ))?;

    index.insert(Vector::with_metadata(
        "user_bob_intro",
        vec![0.0, 0.0, 0.9, 0.1],
        meta,
    ))?;

    // Query for similar vectors
    let query = vec![0.85, 0.15, 0.0, 0.0];
    let results = index.query(&query, 2)?;

    println!("Top {} results:", results.len());
    for (i, result) in results.iter().enumerate() {
        println!("{}. {} (score: {:.4})", i + 1, result.id, result.score);
    }

    // Persist index
    let json = index.to_json()?;
    std::fs::write("index.json", json)?;

    // Restore index
    let json = std::fs::read_to_string("index.json")?;
    let restored = HNSWIndex::from_json(&json)?;
    assert_eq!(restored.len(), 3);

    Ok(())
}

IndexStats Struct

pub struct IndexStats {
    pub vector_count: usize,
    pub layer_count: usize,
    pub entry_point_level: usize,
    pub dimension: usize,
    pub total_inserts: usize,
    pub total_deletes: usize,
    pub metric: DistanceMetric,
}

Error Handling

All methods that can fail return Result<T, SolVecError>. Common errors:
  • DimensionMismatch: Vector dimension doesn’t match index dimension
  • VectorNotFound: Attempted to delete a non-existent vector
  • InvalidVector: Vector has empty ID or contains NaN/infinite values
  • InvalidTopK: top_k parameter is 0
  • SerializationError: Failed to serialize/deserialize index

Performance Characteristics

  • Insert: O(log N) on average
  • Query: O(log N) on average
  • Delete: O(M * log N) where M is max connections
  • Space: O(N * M) where N is number of vectors

See Also

Build docs developers (and LLMs) love