Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
This quickstart walks you through building a Go application that compares all three vector index algorithms (DiskANN, HNSW, and IVF) side by side with different similarity functions to help you choose the best configuration for your workload. The sample uses a hotels dataset with precalculated embeddings from the text-embedding-3-small model.
Find the sample code on GitHub.
Prerequisites
- An Azure subscription. If you don't have an Azure subscription, create a free account.
An existing Azure DocumentDB cluster. If you don't have a cluster, create a new cluster.
-
Custom domain is configured.
text-embedding-3-smallmodel is deployed.
Visual Studio Code. Ensure that you have the Azure DocumentDB extension.
Use the Bash environment in Azure Cloud Shell. For more information, see Get started with Azure Cloud Shell.
If you prefer to run CLI reference commands locally, install the Azure CLI. If you're running on Windows or macOS, consider running Azure CLI in a Docker container. For more information, see How to run the Azure CLI in a Docker container.
If you're using a local installation, sign in to the Azure CLI by using the az login command. To finish the authentication process, follow the steps displayed in your terminal. For other sign-in options, see Authenticate to Azure using Azure CLI.
When you're prompted, install the Azure CLI extension on first use. For more information about extensions, see Use and manage extensions with the Azure CLI.
Run az version to find the version and dependent libraries that are installed. To upgrade to the latest version, run az upgrade.
Azure Developer CLI (optional). Use
azd upto deploy all required Azure resources in one command.Go 1.24 or greater
Create a Go project
Create a new directory for your project and open it in Visual Studio Code:
mkdir select-algorithm-go cd select-algorithm-go code .
Initialize a new Go module:
go mod init documentdb-vector-samplesVerify the module was initialized:
cat go.mod
Install the required packages:
go get github.com/Azure/azure-sdk-for-go/sdk/azcore@v1.20.0 go get github.com/Azure/azure-sdk-for-go/sdk/azidentity@v1.13.1 go get github.com/openai/openai-go/v3@v3.12.0 go get go.mongodb.org/mongo-driver@v1.17.6 go mod tidyazcore: Core Azure SDK functionality for Go.azidentity: Azure Identity library for passwordless authentication with DefaultAzureCredential.openai-go/v3: OpenAI client library with Azure support to generate embeddings.mongo-driver: Official MongoDB driver for Go to work with DocumentDB.
Verify the packages are installed:
go list -m all | grep mongo
Create a
srcdirectory:mkdir src
Create data file with vectors
Create a new data directory for the hotels data file:
mkdir data
Download the
Hotels_Vector.jsonraw data file with vectors to yourdatadirectory:curl -o data/Hotels_Vector.json https://raw.githubusercontent.com/Azure-Samples/documentdb-samples/refs/heads/main/ai/data/Hotels_Vector.json
Verify the file was downloaded:
ls data/Hotels_Vector.json
You should see Hotels_Vector.json in the data directory.
Configure environment variables
Set the required environment variables in your current shell session before you run the sample:
export DOCUMENTDB_CLUSTER_NAME=<your-cluster-name>
export AZURE_OPENAI_EMBEDDING_ENDPOINT=https://<your-resource>.openai.azure.com
export AZURE_OPENAI_EMBEDDING_MODEL=text-embedding-3-small
export AZURE_DOCUMENTDB_DATABASENAME=Hotels
export DATA_FILE_WITH_VECTORS=data/Hotels_Vector.json
export EMBEDDED_FIELD=DescriptionVector
export EMBEDDING_DIMENSIONS=1536
For the passwordless authentication in this article, replace the placeholder values in your current shell session with your own information:
AZURE_OPENAI_EMBEDDING_ENDPOINT: Your Azure OpenAI resource endpoint URLDOCUMENTDB_CLUSTER_NAME: Your Azure DocumentDB cluster name
Prefer passwordless authentication. For more information on setting up managed identity and the full range of your authentication options, see Authenticate Go apps to Azure services by using the Azure SDK for Go.
Create code files
Create the main application file:
touch src/main.go
When you're done, the project structure should look like this:
select-algorithm-go/
├── data/
│ └── Hotels_Vector.json
├── src/
│ ├── compare_all.go
│ ├── main.go
│ └── utils.go
└── go.mod
Create the algorithm comparison code
Create the following source files in the src directory.
src/main.go
package main
import (
"context"
"fmt"
"log"
)
func main() {
fmt.Println("Starting vector algorithm comparison...")
ctx := context.Background()
config, err := LoadConfig()
if err != nil {
log.Fatalf("Invalid configuration: %v", err)
}
fmt.Println("\nInitializing clients with passwordless authentication...")
mongoClient, azureOpenAIClient, err := GetClientsPasswordless(ctx, config)
if err != nil {
log.Fatalf("Failed to initialize clients: %v", err)
}
defer mongoClient.Disconnect(ctx)
err = RunCompareAll(ctx, config, mongoClient, azureOpenAIClient)
if err != nil {
log.Fatalf("Compare all failed: %v", err)
}
fmt.Println("\nComparison completed successfully!")
}
src/compare_all.go
package main
import (
"context"
"fmt"
"math"
"strconv"
"strings"
"time"
"github.com/openai/openai-go/v3"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)
// CompareResult holds the result of a single algorithm+metric search
type CompareResult struct {
Algorithm string
Metric string
Results []SearchResult
Top1Name string
Top1Score float64
Top2Name string
Top2Score float64
Error error
}
// indexSpec defines one of the 9 combinations
type indexSpec struct {
Algorithm string
Kind string
Metric string
IndexName string
Options bson.D
}
// RunCompareAll executes all 9 algorithm×metric combinations on a single collection
func RunCompareAll(ctx context.Context, config *Config, dbClient *mongo.Client, aiClient openai.Client) error {
queryText := getEnvOrDefault("QUERY_TEXT", "luxury hotel near the beach")
topK, _ := strconv.Atoi(getEnvOrDefault("TOP_K", "5"))
fmt.Println("\n" + strings.Repeat("=", 70))
fmt.Println(" COMPARE ALL: 3 Algorithms × 3 Similarity Metrics (9 combinations)")
fmt.Println(strings.Repeat("=", 70))
fmt.Printf("Query: %q\n", queryText)
fmt.Printf("Top-K: %d\n", topK)
// 1. Drop collection for clean comparison, then load data
database := dbClient.Database(config.DatabaseName)
collection := database.Collection("hotels")
// Drop existing collection for a clean comparison
if err := collection.Drop(ctx); err != nil {
fmt.Printf("Note: could not drop collection (may not exist): %v\n", err)
} else {
fmt.Println("Dropped existing 'hotels' collection")
}
// Ensure cleanup on exit
defer func() {
fmt.Println("\nCleanup: dropping comparison collection...")
if dropErr := collection.Drop(ctx); dropErr != nil {
fmt.Printf("Cleanup warning: %v\n", dropErr)
} else {
fmt.Println("Cleanup: dropped collection 'hotels'")
}
}()
fmt.Printf("\nLoading data from %s...\n", config.DataFile)
data, err := ReadFileReturnJSON(config.DataFile)
if err != nil {
return fmt.Errorf("failed to load data: %v", err)
}
documentsWithEmbeddings := FilterDocumentsWithEmbeddings(data, config.VectorField)
if len(documentsWithEmbeddings) == 0 {
return fmt.Errorf("no documents found with embeddings in field '%s'", config.VectorField)
}
fmt.Printf("Loaded %d documents with embeddings\n", len(documentsWithEmbeddings))
stats, err := PrepareCollection(ctx, collection, documentsWithEmbeddings, config.BatchSize)
if err != nil {
return err
}
fmt.Printf("Insertion completed: %d inserted, %d failed\n", stats.Inserted, stats.Failed)
// 2. Generate ONE embedding for the query (reused for all 9 searches)
fmt.Printf("\nGenerating embedding for query: %q\n", queryText)
queryEmbedding, err := GenerateEmbedding(ctx, aiClient, queryText, config.ModelName)
if err != nil {
return fmt.Errorf("failed to generate query embedding: %v", err)
}
fmt.Printf("Embedding generated (%d dimensions)\n", len(queryEmbedding))
// 3. Define all 9 index specs
metrics := []string{"COS", "L2", "IP"}
specs := buildIndexSpecs(config.VectorField, config.Dimensions, metrics)
// 4. Create→search→drop each index sequentially (DocumentDB only allows one vector index per field)
fmt.Printf("\nRunning %d vector index comparisons (create→search→drop)...\n", len(specs))
var results []CompareResult
successfulComparisons := 0
failedComparisons := 0
for _, spec := range specs {
// Drop all existing vector indexes on this field
DropVectorIndexes(ctx, collection, config.VectorField)
// Create this specific index with retry (drop may still be in progress)
var createErr error
for attempt := 0; attempt < 3; attempt++ {
if attempt > 0 {
time.Sleep(3 * time.Second)
}
createErr = createNamedVectorIndex(ctx, collection, config.VectorField, spec)
if createErr == nil {
break
}
}
if createErr != nil {
results = append(results, CompareResult{
Algorithm: spec.Algorithm,
Metric: spec.Metric,
Error: createErr,
})
failedComparisons++
fmt.Printf(" ⚠ %s: %v\n", spec.IndexName, createErr)
continue
}
fmt.Printf(" ✓ %s created\n", spec.IndexName)
// Search using simple cosmosSearch with bounded retry for index readiness.
searchResults, searchErr := runVectorSearchWithRetry(ctx, collection, queryEmbedding, config.VectorField, topK)
top1Name, top1Score := extractResult(searchResults, 0)
top2Name, top2Score := extractResult(searchResults, 1)
cr := CompareResult{
Algorithm: spec.Algorithm,
Metric: spec.Metric,
Results: searchResults,
Top1Name: top1Name,
Top1Score: top1Score,
Top2Name: top2Name,
Top2Score: top2Score,
Error: searchErr,
}
results = append(results, cr)
if searchErr != nil {
failedComparisons++
} else {
successfulComparisons++
}
}
// 6. Print comparison table
fmt.Println()
printComparisonTable(results)
fmt.Printf("\nSummary: %d succeeded, %d failed\n", successfulComparisons, failedComparisons)
if successfulComparisons == 0 {
return fmt.Errorf("all %d comparisons failed", failedComparisons)
}
return nil
}
func runVectorSearchWithRetry(ctx context.Context, collection *mongo.Collection, queryEmbedding []float64, vectorField string, topK int) ([]SearchResult, error) {
const maxAttempts = 6
const retryDelay = 2 * time.Second
var searchResults []SearchResult
var searchErr error
for attempt := 1; attempt <= maxAttempts; attempt++ {
searchResults, searchErr = vectorSearchSimple(ctx, collection, queryEmbedding, vectorField, topK)
if searchErr == nil {
if len(searchResults) > 0 {
return searchResults, nil
}
searchErr = fmt.Errorf("search returned no results")
}
if attempt < maxAttempts {
time.Sleep(retryDelay)
}
}
return searchResults, searchErr
}
// buildIndexSpecs creates the 9 index specifications
func buildIndexSpecs(vectorField string, dimensions int, metrics []string) []indexSpec {
var specs []indexSpec
type algoConfig struct {
name string
kind string
options bson.D
}
algos := []algoConfig{
{"IVF", "vector-ivf", bson.D{{"numLists", 1}}},
{"HNSW", "vector-hnsw", bson.D{{"m", 16}, {"efConstruction", 64}}},
{"DiskANN", "vector-diskann", bson.D{{"maxDegree", 32}, {"lBuild", 50}}},
}
for _, algo := range algos {
for _, metric := range metrics {
metricLower := strings.ToLower(metric)
opts := bson.D{
{"kind", algo.kind},
{"dimensions", dimensions},
{"similarity", metric},
}
for _, o := range algo.options {
opts = append(opts, o)
}
specs = append(specs, indexSpec{
Algorithm: algo.name,
Kind: algo.kind,
Metric: metric,
IndexName: fmt.Sprintf("vector_%s_%s", strings.ToLower(algo.name), metricLower),
Options: opts,
})
}
}
return specs
}
// createNamedVectorIndex creates a single named vector index
func createNamedVectorIndex(ctx context.Context, collection *mongo.Collection, vectorField string, spec indexSpec) error {
indexCommand := bson.D{
{"createIndexes", collection.Name()},
{"indexes", []bson.D{
{
{"name", spec.IndexName},
{"key", bson.D{
{vectorField, "cosmosSearch"},
}},
{"cosmosSearchOptions", spec.Options},
},
}},
}
var result bson.M
err := collection.Database().RunCommand(ctx, indexCommand).Decode(&result)
if err != nil {
if strings.Contains(err.Error(), "already exists") || strings.Contains(err.Error(), "IndexAlreadyExists") {
return nil
}
return err
}
return nil
}
// vectorSearchSimple performs a vector search using the active vector index
func vectorSearchSimple(ctx context.Context, collection *mongo.Collection, embedding []float64, vectorField string, topK int) ([]SearchResult, error) {
pipeline := []bson.M{
{
"$search": bson.M{
"cosmosSearch": bson.M{
"vector": embedding,
"path": vectorField,
"k": topK,
},
},
},
{
"$project": bson.M{
"document": "$$ROOT",
"score": bson.M{"$meta": "searchScore"},
},
},
}
cursor, err := collection.Aggregate(ctx, pipeline)
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
var results []SearchResult
for cursor.Next(ctx) {
var result SearchResult
if err := cursor.Decode(&result); err != nil {
continue
}
results = append(results, result)
}
if err := cursor.Err(); err != nil {
return nil, err
}
return results, nil
}
// extractResult returns the name and score of the result at the given index
func extractResult(results []SearchResult, idx int) (string, float64) {
if idx >= len(results) {
return "(no results)", 0
}
doc := results[idx].Document.(bson.D)
var name string
for _, elem := range doc {
if elem.Key == "HotelName" {
name = fmt.Sprintf("%v", elem.Value)
break
}
}
if name == "" {
name = "Unknown"
}
return name, results[idx].Score
}
// printComparisonTable outputs a formatted table of results
func printComparisonTable(results []CompareResult) {
fmt.Println("┌──────────┬────────┬────────────────────────────┬────────┬────────────────────────────┬────────┬───────┐")
fmt.Printf("│ %-8s │ %-6s │ %-26s │ %-6s │ %-26s │ %-6s │ %-5s │\n",
"Algorithm", "Metric", "Top 1 Result", "Score", "Top 2 Result", "Score", "Diff")
fmt.Println("├──────────┼────────┼────────────────────────────┼────────┼────────────────────────────┼────────┼───────┤")
for _, r := range results {
if r.Error != nil {
fmt.Printf("│ %-8s │ %-6s │ %-26s │ %-6s │ %-26s │ %-6s │ %-5s │\n",
r.Algorithm, r.Metric, "ERROR", "-", "-", "-", "-")
continue
}
top1 := r.Top1Name
if len(top1) > 26 {
top1 = top1[:26]
}
top2 := r.Top2Name
if len(top2) > 26 {
top2 = top2[:26]
}
diff := math.Abs(r.Top1Score - r.Top2Score)
fmt.Printf("│ %-8s │ %-6s │ %-26s │ %6.4f │ %-26s │ %6.4f │%6.4f │\n",
r.Algorithm, r.Metric, top1, r.Top1Score, top2, r.Top2Score, diff)
}
fmt.Println("└──────────┴────────┴────────────────────────────┴────────┴────────────────────────────┴────────┴───────┘")
}
src/utils.go
package main
import (
"context"
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/azure"
"github.com/openai/openai-go/v3/option"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
// Config holds the application configuration
type Config struct {
ClusterName string
DatabaseName string
DataFile string
VectorField string
ModelName string
Dimensions int
BatchSize int
Similarity string
Algorithm string
}
// SearchResult represents a search result document
type SearchResult struct {
Document interface{} `bson:"document"`
Score float64 `bson:"score"`
}
// InsertStats holds statistics about data insertion
type InsertStats struct {
Total int `json:"total"`
Inserted int `json:"inserted"`
Failed int `json:"failed"`
}
// LoadConfig loads configuration from environment variables
func LoadConfig() (*Config, error) {
dimensions, err := parsePositiveIntEnv("EMBEDDING_DIMENSIONS", "1536")
if err != nil {
return nil, err
}
batchSize, err := parsePositiveIntEnv("LOAD_SIZE_BATCH", "100")
if err != nil {
return nil, err
}
return &Config{
ClusterName: getEnvOrDefault("DOCUMENTDB_CLUSTER_NAME", ""),
DatabaseName: getEnvOrDefault("AZURE_DOCUMENTDB_DATABASENAME", "Hotels"),
DataFile: getEnvOrDefault("DATA_FILE_WITH_VECTORS", "data/Hotels_Vector.json"),
VectorField: getEnvOrDefault("EMBEDDED_FIELD", "DescriptionVector"),
ModelName: getEnvOrDefault("AZURE_OPENAI_EMBEDDING_MODEL", "text-embedding-3-small"),
Dimensions: dimensions,
BatchSize: batchSize,
Similarity: getEnvOrDefault("SIMILARITY", ""),
Algorithm: strings.ToLower(getEnvOrDefault("ALGORITHM", "")),
}, nil
}
func parsePositiveIntEnv(key, defaultValue string) (int, error) {
value := getEnvOrDefault(key, defaultValue)
parsedValue, err := strconv.Atoi(value)
if err != nil {
return 0, fmt.Errorf("%s must be a positive integer, got %q", key, value)
}
if parsedValue <= 0 {
return 0, fmt.Errorf("%s must be greater than 0, got %q", key, value)
}
return parsedValue, nil
}
// getEnvOrDefault returns environment variable value or default if not set
func getEnvOrDefault(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
// GetClientsPasswordless creates MongoDB and Azure OpenAI clients with passwordless authentication
func GetClientsPasswordless(ctx context.Context, config *Config) (*mongo.Client, openai.Client, error) {
if config.ClusterName == "" {
return nil, openai.Client{}, fmt.Errorf("DOCUMENTDB_CLUSTER_NAME environment variable is required")
}
// Create Azure credential
credential, err := azidentity.NewDefaultAzureCredential(nil)
if err != nil {
return nil, openai.Client{}, fmt.Errorf("failed to create Azure credential: %v", err)
}
// Connect to DocumentDB with OIDC authentication
mongoURI := fmt.Sprintf("mongodb+srv://%s.global.mongocluster.cosmos.azure.com/", config.ClusterName)
fmt.Println("Attempting OIDC authentication...")
mongoClient, err := connectWithOIDC(ctx, mongoURI, credential)
if err != nil {
return nil, openai.Client{}, fmt.Errorf("OIDC authentication failed: %v", err)
}
fmt.Println("OIDC authentication successful!")
// Get Azure OpenAI endpoint
azureOpenAIEndpoint := os.Getenv("AZURE_OPENAI_EMBEDDING_ENDPOINT")
if azureOpenAIEndpoint == "" {
return nil, openai.Client{}, fmt.Errorf("AZURE_OPENAI_EMBEDDING_ENDPOINT environment variable is required")
}
// Create Azure OpenAI client with credential-based authentication
openAIClient := openai.NewClient(
option.WithBaseURL(fmt.Sprintf("%s/openai/v1", azureOpenAIEndpoint)),
azure.WithTokenCredential(credential))
return mongoClient, openAIClient, nil
}
// connectWithOIDC attempts to connect using OIDC authentication
func connectWithOIDC(ctx context.Context, mongoURI string, credential *azidentity.DefaultAzureCredential) (*mongo.Client, error) {
oidcCallback := func(ctx context.Context, args *options.OIDCArgs) (*options.OIDCCredential, error) {
scope := "https://ossrdbms-aad.database.windows.net/.default"
fmt.Printf("Getting token with scope: %s\n", scope)
token, err := credential.GetToken(ctx, policy.TokenRequestOptions{
Scopes: []string{scope},
})
if err != nil {
return nil, fmt.Errorf("failed to get token with scope %s: %v", scope, err)
}
fmt.Printf("Successfully obtained token\n")
return &options.OIDCCredential{
AccessToken: token.Token,
}, nil
}
clientOptions := options.Client().
ApplyURI(mongoURI).
SetConnectTimeout(30 * time.Second).
SetServerSelectionTimeout(30 * time.Second).
SetRetryWrites(false).
SetAuth(options.Credential{
AuthMechanism: "MONGODB-OIDC",
AuthMechanismProperties: map[string]string{
"TOKEN_RESOURCE": "https://ossrdbms-aad.database.windows.net",
},
OIDCMachineCallback: oidcCallback,
})
mongoClient, err := mongo.Connect(ctx, clientOptions)
if err != nil {
return nil, err
}
return mongoClient, nil
}
// ReadFileReturnJSON reads a JSON file and returns the data as a slice of maps
func ReadFileReturnJSON(filePath string) ([]map[string]interface{}, error) {
file, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("error reading file '%s': %v", filePath, err)
}
var data []map[string]interface{}
err = json.Unmarshal(file, &data)
if err != nil {
return nil, fmt.Errorf("error parsing JSON in file '%s': %v", filePath, err)
}
return data, nil
}
// InsertData inserts data into a MongoDB collection in batches
func InsertData(ctx context.Context, collection *mongo.Collection, data []map[string]interface{}, batchSize int) (*InsertStats, error) {
totalDocuments := len(data)
insertedCount := 0
failedCount := 0
fmt.Printf("Starting batch insertion of %d documents...\n", totalDocuments)
for i := 0; i < totalDocuments; i += batchSize {
end := i + batchSize
if end > totalDocuments {
end = totalDocuments
}
batch := data[i:end]
batchNum := (i / batchSize) + 1
documents := make([]interface{}, len(batch))
for j, doc := range batch {
documents[j] = doc
}
result, err := collection.InsertMany(ctx, documents, options.InsertMany().SetOrdered(false))
if err != nil {
if bulkErr, ok := err.(mongo.BulkWriteException); ok {
errorCount := len(bulkErr.WriteErrors)
insertedCount += len(batch) - errorCount
failedCount += errorCount
fmt.Printf("Batch %d had errors: %d inserted, %d failed\n", batchNum, len(batch)-errorCount, errorCount)
for _, writeErr := range bulkErr.WriteErrors {
fmt.Printf(" Error: %s\n", writeErr.Message)
}
} else {
failedCount += len(batch)
fmt.Printf("Batch %d failed completely: %v\n", batchNum, err)
}
} else {
insertedCount += len(result.InsertedIDs)
fmt.Printf("Batch %d completed: %d documents inserted\n", batchNum, len(result.InsertedIDs))
}
time.Sleep(100 * time.Millisecond)
}
return &InsertStats{
Total: totalDocuments,
Inserted: insertedCount,
Failed: failedCount,
}, nil
}
// DropVectorIndexes drops existing vector indexes on the specified field
func DropVectorIndexes(ctx context.Context, collection *mongo.Collection, vectorField string) error {
cursor, err := collection.Indexes().List(ctx)
if err != nil {
return fmt.Errorf("could not list indexes: %v", err)
}
defer cursor.Close(ctx)
var vectorIndexes []string
for cursor.Next(ctx) {
var index bson.M
if err := cursor.Decode(&index); err != nil {
continue
}
if key, ok := index["key"].(bson.M); ok {
if indexType, exists := key[vectorField]; exists && indexType == "cosmosSearch" {
if name, ok := index["name"].(string); ok {
vectorIndexes = append(vectorIndexes, name)
}
}
}
}
for _, indexName := range vectorIndexes {
fmt.Printf("Dropping existing vector index: %s\n", indexName)
_, err := collection.Indexes().DropOne(ctx, indexName)
if err != nil {
fmt.Printf("Warning: Could not drop index %s: %v\n", indexName, err)
}
}
if len(vectorIndexes) > 0 {
fmt.Printf("Dropped %d existing vector index(es)\n", len(vectorIndexes))
} else {
fmt.Println("No existing vector indexes found to drop")
}
return nil
}
// PerformVectorSearch performs a vector search using the cosmosSearch aggregation pipeline
func PerformVectorSearch(ctx context.Context, collection *mongo.Collection, client openai.Client, query, vectorField, model string, topK int) ([]SearchResult, error) {
fmt.Printf("Performing vector search for: '%s'\n", query)
queryEmbedding, err := GenerateEmbedding(ctx, client, query, model)
if err != nil {
return nil, fmt.Errorf("error generating embedding: %v", err)
}
pipeline := []bson.M{
{
"$search": bson.M{
"cosmosSearch": bson.M{
"vector": queryEmbedding,
"path": vectorField,
"k": topK,
},
},
},
{
"$project": bson.M{
"document": "$$ROOT",
"score": bson.M{"$meta": "searchScore"},
},
},
}
cursor, err := collection.Aggregate(ctx, pipeline)
if err != nil {
return nil, fmt.Errorf("error performing vector search: %v", err)
}
defer cursor.Close(ctx)
var results []SearchResult
for cursor.Next(ctx) {
var result SearchResult
if err := cursor.Decode(&result); err != nil {
fmt.Printf("Warning: Could not decode result: %v\n", err)
continue
}
results = append(results, result)
}
if err := cursor.Err(); err != nil {
return nil, fmt.Errorf("cursor error: %v", err)
}
return results, nil
}
// GenerateEmbedding generates an embedding for the given text using Azure OpenAI
func GenerateEmbedding(ctx context.Context, client openai.Client, text, modelName string) ([]float64, error) {
resp, err := client.Embeddings.New(ctx, openai.EmbeddingNewParams{
Input: openai.EmbeddingNewParamsInputUnion{
OfString: openai.String(text),
},
Model: modelName,
})
if err != nil {
return nil, fmt.Errorf("failed to generate embedding: %v", err)
}
if len(resp.Data) == 0 {
return nil, fmt.Errorf("no embedding data received")
}
embedding := make([]float64, len(resp.Data[0].Embedding))
for i, v := range resp.Data[0].Embedding {
embedding[i] = float64(v)
}
return embedding, nil
}
// PrintSearchResults prints search results in a formatted way
func PrintSearchResults(results []SearchResult, algorithm string) {
if len(results) == 0 {
fmt.Println("No search results found.")
return
}
fmt.Printf("\n%s Search Results (top %d):\n", strings.ToUpper(algorithm), len(results))
fmt.Println(strings.Repeat("=", 80))
for i, result := range results {
doc := result.Document.(bson.D)
var hotelName string
for _, elem := range doc {
if elem.Key == "HotelName" {
hotelName = fmt.Sprintf("%v", elem.Value)
break
}
}
fmt.Printf("%d. HotelName: %s, Score: %.4f\n", i+1, hotelName, result.Score)
}
}
// FilterDocumentsWithEmbeddings returns only documents that contain the vector field
func FilterDocumentsWithEmbeddings(data []map[string]interface{}, vectorField string) []map[string]interface{} {
var filtered []map[string]interface{}
for _, doc := range data {
if _, exists := doc[vectorField]; exists {
filtered = append(filtered, doc)
}
}
return filtered
}
// PrepareCollection clears existing data and inserts new documents
func PrepareCollection(ctx context.Context, collection *mongo.Collection, data []map[string]interface{}, batchSize int) (*InsertStats, error) {
fmt.Printf("Preparing collection '%s'...\n", collection.Name())
deleteResult, err := collection.DeleteMany(ctx, bson.M{})
if err != nil {
return nil, fmt.Errorf("failed to clear existing data: %v", err)
}
if deleteResult.DeletedCount > 0 {
fmt.Printf("Cleared %d existing documents from collection\n", deleteResult.DeletedCount)
}
stats, err := InsertData(ctx, collection, data, batchSize)
if err != nil {
return nil, fmt.Errorf("failed to insert data: %v", err)
}
return stats, nil
}
This code provides a vector algorithm comparison application with these key features:
- Passwordless authentication: Uses
DefaultAzureCredentialfor both Azure OpenAI and DocumentDB via OIDC. - Three vector algorithms: Implements DiskANN, HNSW, and IVF with algorithm-specific tuning parameters.
- Three similarity functions: Supports COS (cosine), L2 (Euclidean), and IP (inner product).
- Single compare-all entry point: Always runs all 9 algorithm × similarity combinations in one pass.
- Index lifecycle automation: Creates, queries, and drops each vector index in sequence.
- Comparison output: Generates a formatted table showing the top two results and score gap for each combination.
- Production-ready patterns: Includes batched insertion, error handling, and connection pooling.
Note
The Go sample configures the DocumentDB connection with retryWrites=false, which is required for DocumentDB vector search operations.
Run the code
After setting the environment variables in your shell session, run the application:
go run ./src/
The application does the following:
- Connect to Azure DocumentDB and Azure OpenAI using passwordless authentication
- Load the hotel data and insert it into the
hotelscollection - Generate an embedding for the search query
- Run all 9 vector index comparisons by creating, querying, and dropping each index in sequence
- Display a comparison table with the top two results and score gap for each combination
- Drop the
hotelscollection during cleanup
Expected output:
======================================================================
COMPARE ALL: 3 Algorithms × 3 Similarity Metrics (9 combinations)
======================================================================
Query: "luxury hotel near the beach"
Top-K: 5
Loading data from data/Hotels_Vector.json...
Loaded 50 documents with embeddings
Insertion completed: 50 inserted, 0 failed
Generating embedding for query: "luxury hotel near the beach"
Embedding generated (1536 dimensions)
Running 9 vector index comparisons (create→search→drop)...
✓ vector_ivf_cos created
✓ vector_ivf_l2 created
✓ vector_ivf_ip created
✓ vector_hnsw_cos created
✓ vector_hnsw_l2 created
✓ vector_hnsw_ip created
✓ vector_diskann_cos created
✓ vector_diskann_l2 created
✓ vector_diskann_ip created
┌──────────┬────────┬────────────────────────────┬────────┬────────────────────────────┬────────┬───────┐
│ Algorithm│ Metric │ Top 1 Result │ Score │ Top 2 Result │ Score │ Diff │
├──────────┼────────┼────────────────────────────┼────────┼────────────────────────────┼────────┼───────┤
│ IVF │ COS │ Ocean Water Resort & Spa │ 0.6184 │ Windy Ocean Motel │ 0.5056 │ 0.1128│
│ IVF │ L2 │ Ocean Water Resort & Spa │ 0.8736 │ Windy Ocean Motel │ 0.9943 │ 0.1208│
│ IVF │ IP │ Ocean Water Resort & Spa │ 0.6184 │ Windy Ocean Motel │ 0.5056 │ 0.1128│
│ HNSW │ COS │ Ocean Water Resort & Spa │ 0.6184 │ Windy Ocean Motel │ 0.5056 │ 0.1128│
│ HNSW │ L2 │ Ocean Water Resort & Spa │ 0.8736 │ Windy Ocean Motel │ 0.9943 │ 0.1208│
│ HNSW │ IP │ Ocean Water Resort & Spa │ 0.6184 │ Windy Ocean Motel │ 0.5056 │ 0.1128│
│ DiskANN │ COS │ Ocean Water Resort & Spa │ 0.6184 │ Windy Ocean Motel │ 0.5056 │ 0.1128│
│ DiskANN │ L2 │ Ocean Water Resort & Spa │ 0.8736 │ Windy Ocean Motel │ 0.9943 │ 0.1208│
│ DiskANN │ IP │ Ocean Water Resort & Spa │ 0.6184 │ Windy Ocean Motel │ 0.5056 │ 0.1128│
└──────────┴────────┴────────────────────────────┴────────┴────────────────────────────┴────────┴───────┘
Summary: 9 succeeded, 0 failed
Cleanup: dropped collection 'hotels'
The Diff column shows the score gap between the top-1 and top-2 results. A smaller diff indicates the algorithm found results with more similar relevance scores.
Understanding the results
The comparison table shows how different algorithms perform on the same dataset with the same query:
- Algorithm: DiskANN, HNSW, or IVF
- Metric: The similarity metric (COS, L2, or IP)
- Top 1 Result: The highest-ranked hotel for that algorithm and metric
- Score: The relevance score for the corresponding result
- Top 2 Result: The second-highest-ranked hotel for that algorithm and metric
- Diff: The score gap between the top two results
Choosing the right algorithm
Important
For production workloads, start with DiskANN on an M30+ cluster. DiskANN supports higher embedding dimensions, uses less cluster memory, and is less likely to require an index redesign as your models evolve.
Use this quick-reference table to select the right algorithm for your workload:
| Scenario | Algorithm | Cluster tier | Max dimensions |
|---|---|---|---|
| Dev/test, demos, small datasets | IVF | M10+ | 2,000 |
| Production (default) | DiskANN | M30+ | 16,000 |
| Production (max recall priority) | HNSW | M30+ | 8,000 |
IVF (inverted file index):
- Best for: Test environments, demos, and small clusters
- Pros: Fast to build, low resource requirements
- Cons: Lower recall compared to graph-based algorithms at scale
- Tune: Increase
numListsfor larger datasets, increasenProbesfor better recall
DiskANN (disk-based approximate nearest neighbor) — recommended for production:
- Best for: Production workloads on M30+ clusters
- Pros: Supports embeddings up to 16,000 dimensions, keeps most index data on disk freeing cluster memory for reads and writes, lighter index updates, easier backups, faster recovery
- Cons: Requires M30+ cluster tier
- Tune: Increase
maxDegreeandlBuildfor better accuracy, increaselSearchfor better recall - Why default: As embedding models evolve (some already exceed 8,000 dimensions), DiskANN avoids costly index redesigns. Its disk-based architecture also means your cluster memory stays available for operational workloads rather than index storage.
HNSW (hierarchical navigable small world):
- Best for: Production workloads on M30+ clusters where maximum recall is the top priority
- Pros: Excellent recall, fast queries
- Cons: Requires M30+ cluster tier, supports embeddings up to 8,000 dimensions (vs 16,000 for DiskANN), higher memory usage since the full graph lives in RAM
- Tune: Increase
mandefConstructionfor better index quality, increaseefSearchfor better recall
Choosing the right similarity function
| Function | Score meaning | Best for |
|---|---|---|
| COS (Cosine) | Higher = more similar (0–1) | Text embeddings (normalized vectors) |
| L2 (Euclidean) | Lower = more similar (distance) | When magnitude matters |
| IP (Inner Product) | Higher = more similar | Equivalent to COS for normalized vectors |
For the text-embedding-3-small model used in this quickstart, COS (cosine similarity) is recommended because OpenAI embeddings are normalized and optimized for cosine similarity.
Troubleshooting
| Issue | Solution |
|---|---|
server selection error |
Verify your environment variables are set correctly. Ensure your IP is in the DocumentDB firewall rules. |
authentication failed |
Verify your Microsoft Entra token is valid. Run az login to refresh your credentials. |
go: module not found |
Run go mod tidy to resolve dependencies. |
| Build errors | Ensure Go 1.24+ is installed. Run go version to check. |
| Empty search results | The vector index may take a few minutes to build. Wait 2-3 minutes after index creation, then rerun the script. |
Clean up resources
Remove the database using the DocumentDB for VS Code extension:
- Install the DocumentDB for VS Code extension.
- Connect to your Azure DocumentDB cluster.
- Expand the cluster, right-click the Hotels database, and select Drop Database.