Skip to content

Examples

This page provides practical examples of using HNSW for various use cases.

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)
 }
}

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: