Embeddable, extensible implementation of a Hierarchical Navigable Small World (HNSW) index
2.4K
A pure C# implementation of Hierarchical Navigable Small World (HNSW) graphs for approximate nearest neighbor search. This library provides a thread-safe, embeddable solution for vector similarity search in .NET applications.
Note: This library is in its early stages of development. We welcome your patience, constructive feedback, and contributions! Please be kind and considerate when reporting issues or suggesting improvements. I am not an expert on this topic and relied heavily on available AI tools to build this library. Pull requests are greatly appreciated!
HnswLite implements the Hierarchical Navigable Small World algorithm, which provides fast approximate nearest neighbor search with excellent recall rates. The library is designed to be embeddable, extensible, and easy to use in any .NET application.
HnswIndex.Server) with Docker image and Postman collectionFor version history, see CHANGELOG.md.
HnswLite is ideal for:
(vector_count * dimension * 4 bytes) + (vector_count * M * 32 bytes)Note: the above are estimations. This library has not been tested (yet) at any level of scale.
M: Number of connections per vector (default: 16). Think of this as how many "friends" each vector has in the network. More connections mean better search quality but use more memory. For most cases, 16-32 works well.EfConstruction: Size of the candidate list when building the index (default: 200). This controls how thoroughly the algorithm searches for connections when adding new vectors. Higher values create better quality indices but take longer to build. For faster batch insertion, consider reducing to 50-100.Ef (search parameter): Size of the candidate list during search (default: 50-200). This controls how many paths the algorithm explores when searching. Higher values find better results but take more time. Set this based on your speed/quality needs.Seed: Set a consistent seed value for reproducible index builds (useful for testing).AddNodesAsync for batch operations instead of individual AddAsync callsEfConstruction for faster insertion (trade-off with search quality)We value your input! If you encounter any issues or have suggestions:
using Hnsw;
using Hnsw.RamStorage;
using Hnsw.SqliteStorage;
// Create an index for 128-dimensional vectors in RAM
HnswIndex index = new HnswIndex(128, new RamHnswStorage(), new RamHnswLayerStorage());
// Or using SQLite (with proper disposal)
using SqliteHnswStorage sqliteStorage = new SqliteHnswStorage("my-index.db");
using SqliteHnswLayerStorage sqliteLayerStorage = new SqliteHnswLayerStorage(sqliteStorage.Connection);
HnswIndex sqliteIndex = new HnswIndex(128, sqliteStorage, sqliteLayerStorage);
// Configure parameters (optional)
index.M = 16;
index.EfConstruction = 200;
index.DistanceFunction = new CosineDistance();
// Add vectors to the index
Guid vectorId = Guid.NewGuid();
List<float> vector = new List<float>(128); // Your 128-dimensional embedding
// ... populate vector with data ...
await index.AddAsync(vectorId, vector);
// Add multiple vectors
Dictionary<Guid, List<float>> batch = new Dictionary<Guid, List<float>>();
for (int i = 0; i < 1000; i++)
{
Guid id = Guid.NewGuid();
List<float> v = GenerateRandomVector(128); // Your vector generation logic
batch[id] = v;
}
await index.AddNodesAsync(batch);
// Search for nearest neighbors
List<float> queryVector = new List<float>(128); // Your query embedding
// ... populate query vector ...
List<SearchResult> neighbors = await index.GetTopKAsync(queryVector, k: 10);
foreach (SearchResult result in neighbors)
{
Console.WriteLine($"ID: {result.GUID}, Distance: {result.Distance:F4}");
}
// Save the index
HnswState state = await index.ExportStateAsync();
// ... serialize state to disk ...
// Load the index
HnswIndex newIndex = new HnswIndex(128, new RamHnswStorage(), new RamHnswLayerStorage());
await newIndex.ImportStateAsync(state);
Resource Management:
using statements with SQLite storage to ensure proper cleanupBatch Operations:
// GOOD: Use batch operations for multiple vectors
Dictionary<Guid, List<float>> batch = new Dictionary<Guid, List<float>>();
// ... populate batch ...
await index.AddNodesAsync(batch);
// AVOID: Individual adds in a loop
foreach (Item item in items)
{
await index.AddAsync(item.Id, item.Vector); // Slower
}
Search Performance:
// Adjust ef parameter based on your needs
List<SearchResult> quickResults = await index.GetTopKAsync(query, k: 10, ef: 50); // Faster, lower quality
List<SearchResult> bestResults = await index.GetTopKAsync(query, k: 10, ef: 400); // Slower, higher quality
Refer to Hnsw.RamStorage and Hnsw.SqliteStorage for actual implementations. To implement your own backend, you need to implement:
IHnswLayerStorage - Manages layer assignments for nodesIHnswNode - Represents a single node with its vector and neighborsIHnswStorage - Handles node persistence and retrievalRefer to the src/Docker directory for assets related to running in Docker. The Docker image can be found on Docker Hub and a Postman collection is contained within this repository's root directory.
This library is available under the MIT license.
This implementation is based on the paper: Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs by Yu. A. Malkov and D. A. Yashunin.
Content type
Image
Digest
sha256:f0d2a9f74…
Size
27.6 MB
Last updated
8 days ago
docker pull jchristn77/hnswlite-dashboard