Examples¶
This page provides practical examples of using HNSW for various use cases.
Text Embedding Search¶
This example demonstrates how to use HNSW to search for similar text embeddings:
package main
import (
"fmt"
"log"
"github.com/TFMV/hnsw"
)
func main() {
// Create a new HNSW graph
graph := hnsw.NewGraph[string]()
// Sample text embeddings (in a real application, these would be generated by a model like OpenAI's text-embedding-ada-002)
embeddings := map[string][]float32{
"dog": {1.0, 0.2, 0.1},
"puppy": {0.9, 0.3, 0.2},
"canine": {0.8, 0.3, 0.3},
"cat": {0.1, 1.0, 0.2},
"kitten": {0.2, 0.9, 0.3},
"feline": {0.3, 0.8, 0.3},
"bird": {0.1, 0.2, 1.0},
"sparrow": {0.2, 0.3, 0.9},
"avian": {0.3, 0.3, 0.8},
}
// Add embeddings to the graph
for text, embedding := range embeddings {
err := graph.Add(hnsw.Node[string]{
Key: text,
Value: embedding,
})
if err != nil {
log.Fatalf("Failed to add node: %v", err)
}
}
// Search for similar terms to "dog"
results, err := graph.Search(embeddings["dog"], 3)
if err != nil {
log.Fatalf("Failed to search: %v", err)
}
fmt.Println("Terms similar to 'dog':")
for i, result := range results {
fmt.Printf("%d. %s (distance: %.2f)\n", i+1, result.Key, result.Dist)
}
// Search for terms similar to "cat" but dissimilar to "dog"
results, err = graph.SearchWithNegative(embeddings["cat"], embeddings["dog"], 3, 0.5)
if err != nil {
log.Fatalf("Failed to search: %v", err)
}
fmt.Println("\nTerms similar to 'cat' but dissimilar to 'dog':")
for i, result := range results {
fmt.Printf("%d. %s (distance: %.2f)\n", i+1, result.Key, result.Dist)
}
}
Image Similarity Search¶
This example shows how to use HNSW for image similarity search:
package main
import (
"fmt"
"log"
"math/rand"
"time"
"github.com/TFMV/hnsw"
"github.com/TFMV/hnsw/hnsw-extensions/meta"
)
// ImageMetadata represents metadata for an image
type ImageMetadata struct {
Filename string `json:"filename"`
Width int `json:"width"`
Height int `json:"height"`
Format string `json:"format"`
Tags []string `json:"tags"`
CreatedAt string `json:"created_at"`
}
func main() {
// Create a new metadata graph
graph := meta.NewMetaGraph[int, ImageMetadata]()
// In a real application, these would be feature vectors extracted from images
// using a model like ResNet or EfficientNet
rand.Seed(time.Now().UnixNano())
// Add 1000 random image vectors with metadata
for i := 0; i < 1000; i++ {
// Create a random 128-dimensional vector
vector := make([]float32, 128)
for j := range vector {
vector[j] = rand.Float32()
}
// Normalize the vector (important for cosine distance)
var sum float32
for _, v := range vector {
sum += v * v
}
norm := float32(1.0 / float32(math.Sqrt(float64(sum))))
for j := range vector {
vector[j] *= norm
}
// Create metadata
metadata := ImageMetadata{
Filename: fmt.Sprintf("image_%d.jpg", i),
Width: rand.Intn(800) + 400,
Height: rand.Intn(600) + 300,
Format: "JPEG",
Tags: []string{"sample", fmt.Sprintf("category_%d", i%10)},
CreatedAt: time.Now().Format(time.RFC3339),
}
// Add to graph
err := graph.Add(hnsw.Node[int]{
Key: i,
Value: vector,
}, metadata)
if err != nil {
log.Fatalf("Failed to add node: %v", err)
}
}
// Create a query vector (in a real application, this would be from a query image)
queryVector := make([]float32, 128)
for i := range queryVector {
queryVector[i] = rand.Float32()
}
// Normalize the query vector
var sum float32
for _, v := range queryVector {
sum += v * v
}
norm := float32(1.0 / float32(math.Sqrt(float64(sum))))
for i := range queryVector {
queryVector[i] *= norm
}
// Search for similar images
results, err := graph.SearchWithMetadata(queryVector, 5)
if err != nil {
log.Fatalf("Failed to search: %v", err)
}
fmt.Println("Similar images:")
for i, result := range results {
fmt.Printf("%d. %s (distance: %.4f)\n",
i+1,
result.Metadata.Filename,
result.Dist)
fmt.Printf(" Tags: %v\n", result.Metadata.Tags)
fmt.Printf(" Dimensions: %dx%d\n", result.Metadata.Width, result.Metadata.Height)
}
}
Product Recommendation¶
This example demonstrates how to use HNSW with the Faceted Search extension for product recommendations:
package main
import (
"fmt"
"log"
"math/rand"
"time"
"github.com/TFMV/hnsw"
"github.com/TFMV/hnsw/hnsw-extensions/facets"
)
func main() {
// Create a new faceted graph
graph := facets.NewFacetedGraph[int]()
// Sample product categories
categories := []string{"Electronics", "Clothing", "Home", "Books", "Sports"}
// Sample price ranges
priceRanges := []struct{ min, max float64 }{
{10, 50}, {50, 100}, {100, 200}, {200, 500}, {500, 1000},
}
rand.Seed(time.Now().UnixNano())
// Add 1000 random product vectors with facets
for i := 0; i < 1000; i++ {
// Create a random 64-dimensional vector
vector := make([]float32, 64)
for j := range vector {
vector[j] = rand.Float32()
}
// Create facets
category := categories[rand.Intn(len(categories))]
priceRange := priceRanges[rand.Intn(len(priceRanges))]
price := priceRange.min + rand.Float64()*(priceRange.max-priceRange.min)
inStock := rand.Intn(10) < 8 // 80% chance of being in stock
facetValues := map[string]interface{}{
"category": category,
"price": price,
"inStock": inStock,
"rating": float64(3 + rand.Intn(3)), // Rating between 3-5
}
// Add to graph
err := graph.Add(hnsw.Node[int]{
Key: i,
Value: vector,
}, facetValues)
if err != nil {
log.Fatalf("Failed to add node: %v", err)
}
}
// Create a query vector (in a real application, this would be from user preferences)
queryVector := make([]float32, 64)
for i := range queryVector {
queryVector[i] = rand.Float32()
}
// Create a filter for electronics products in stock with price < 200
filter := facets.And(
facets.Eq("category", "Electronics"),
facets.Range("price", 0, 200),
facets.Eq("inStock", true),
)
// Search for similar products with filter
results, err := graph.SearchWithFilter(queryVector, 5, filter)
if err != nil {
log.Fatalf("Failed to search: %v", err)
}
fmt.Println("Recommended Electronics products under $200:")
for i, result := range results {
// Get facets for the result
facets, err := graph.GetFacets(result.Key)
if err != nil {
log.Fatalf("Failed to get facets: %v", err)
}
fmt.Printf("%d. Product ID: %d (similarity: %.4f)\n",
i+1,
result.Key,
1-result.Dist) // Convert distance to similarity
fmt.Printf(" Category: %s\n", facets["category"])
fmt.Printf(" Price: $%.2f\n", facets["price"])
fmt.Printf(" Rating: %.1f\n", facets["rating"])
}
}
Concurrent Operations¶
This example demonstrates how to use HNSW in a concurrent environment:
package main
import (
"fmt"
"log"
"math/rand"
"sync"
"time"
"github.com/TFMV/hnsw"
)
func main() {
// Create a new HNSW graph
graph := hnsw.NewGraph[int]()
// Add some initial nodes
for i := 0; i < 1000; i++ {
vector := make([]float32, 32)
for j := range vector {
vector[j] = rand.Float32()
}
err := graph.Add(hnsw.Node[int]{
Key: i,
Value: vector,
})
if err != nil {
log.Fatalf("Failed to add node: %v", err)
}
}
// Number of concurrent operations
numOperations := 100
// Wait group to wait for all goroutines to finish
var wg sync.WaitGroup
wg.Add(numOperations)
// Perform concurrent searches
for i := 0; i < numOperations; i++ {
go func(id int) {
defer wg.Done()
// Create a random query vector
query := make([]float32, 32)
for j := range query {
query[j] = rand.Float32()
}
// Search for similar vectors
results, err := graph.Search(query, 5)
if err != nil {
log.Printf("Search error in goroutine %d: %v", id, err)
return
}
fmt.Printf("Goroutine %d found %d results\n", id, len(results))
}(i)
}
// Wait for all searches to complete
wg.Wait()
// Perform concurrent adds and deletes
wg.Add(numOperations * 2)
// Concurrent adds
for i := 0; i < numOperations; i++ {
go func(id int) {
defer wg.Done()
// Create a random vector
vector := make([]float32, 32)
for j := range vector {
vector[j] = rand.Float32()
}
// Add to graph
nodeID := 1000 + id
err := graph.Add(hnsw.Node[int]{
Key: nodeID,
Value: vector,
})
if err != nil {
log.Printf("Add error in goroutine %d: %v", id, err)
}
}(i)
}
// Concurrent deletes
for i := 0; i < numOperations; i++ {
go func(id int) {
defer wg.Done()
// Delete a random node
nodeID := rand.Intn(1000)
deleted := graph.Delete(nodeID)
if deleted {
fmt.Printf("Goroutine %d deleted node %d\n", id, nodeID)
}
}(i)
}
// Wait for all operations to complete
wg.Wait()
fmt.Printf("Final graph size: %d nodes\n", graph.Len())
}
Next Steps¶
Now that you've seen some practical examples of HNSW in action, you can explore:
- Advanced Techniques: Learn about more sophisticated usage patterns
- Extensions: Discover how to extend HNSW functionality
- Performance Tuning: Optimize HNSW for your specific use case