Quickstart: Create and query vector indexes in Azure Cosmos DB for NoSQL using Go

In this quickstart, you run the Go create-index sample for Azure Cosmos DB for NoSQL. The sample creates two containers with different vector index types—DiskANN and QuantizedFlat—loads a pre-vectorized hotel dataset, and compares the scores and rankings produced by Cosine, DotProduct, and Euclidean distance functions.

DiskANN and QuantizedFlat are two of the vector index types available in Azure Cosmos DB for NoSQL. For a comparison of when to use each, see Vector indexing policies.

The sample uses a JSON dataset with hotel names, regions, descriptions, and 1536-dimension vectors generated by the text-embedding-3-small model. The sample partitions the hotel documents by geographic region.

Prerequisites

Azure resources are created in this quickstart with the Azure Developer CLI.

Tip

Agent Kit helps coding agents work with Azure Cosmos DB quickly and efficiently using recommended best practices. To get started, run:

npx skills add AzureCosmosDB/cosmosdb-agent-kit

To learn more, see Azure Cosmos DB Agent Kit.

App dependencies

The sample uses the following Go module dependencies:

The Go sample calls the Azure OpenAI embeddings REST API directly instead of using an Azure OpenAI client library.

Authenticate to Azure

The sample uses passwordless authentication through DefaultAzureCredential and Microsoft Entra ID. Sign in to Azure before you run the sample so it can access your Azure resources securely.

azd auth login

Provision the resources

  1. Clone the sample repository.

    git clone https://github.com/Azure-Samples/cosmos-db-vector-samples.git
    cd cosmos-db-vector-samples
    
  2. Create an Azure Developer CLI environment.

    azd env new cosmos-nosql
    
  3. Set the database name for the create-index samples.

    azd env set AZURE_COSMOSDB_CREATE_INDEX_DATABASENAME "HotelsCreateIndex"
    
  4. Provision the Azure resources and role assignments.

    azd up
    
  5. Change to the Go sample directory.

    cd nosql-create-index-go
    

Set up environment variables

This sample uses a local .env file to store the Azure resource and project settings. Generate the file from the active Azure Developer CLI environment.

azd env get-values > .env

Load the values into the current shell because the Go app reads process environment variables and doesn't load .env automatically.

Get-Content .env | Where-Object { $_ -match '^[^#].*=' } | ForEach-Object { $k,$v = $_ -split '=',2; [Environment]::SetEnvironmentVariable($k.Trim(), $v.Trim().Trim('"').Trim("'")) }

The checked-in example shows the default configuration shape:

AZURE_COSMOSDB_ENDPOINT=https://YOUR-COSMOS-ACCOUNT.documents.azure.com:443/
AZURE_COSMOSDB_CREATE_INDEX_DATABASENAME=HotelsCreateIndex
AZURE_OPENAI_EMBEDDING_ENDPOINT=https://YOUR-OPENAI-RESOURCE.openai.azure.com/
AZURE_OPENAI_EMBEDDING_DEPLOYMENT=text-embedding-3-small
AZURE_SUBSCRIPTION_ID=00000000-0000-0000-0000-000000000000
AZURE_RESOURCE_GROUP=YOUR-RESOURCE-GROUP
AZURE_COSMOSDB_ACCOUNT_NAME=YOUR-COSMOS-ACCOUNT
AZURE_LOCATION=eastus2
DATA_FILE_WITH_VECTORS_AND_REGIONS=./data/HotelsData_toCosmosDB_Vector_byRegion.json

# Optional. These documented defaults can be deleted and recreated without an opt-in flag.
AZURE_COSMOSDB_CREATE_INDEX_DISKANN_CONTAINER_NAME=hotels_diskann
AZURE_COSMOSDB_CREATE_INDEX_QUANTIZEDFLAT_CONTAINER_NAME=hotels_quantizedflat

# Required only when either container name above is customized.
AZURE_COSMOSDB_CREATE_INDEX_ALLOW_DESTRUCTIVE_OPERATIONS=false

The source defaults to the hotels_diskann and hotels_quantizedflat containers. The sample deletes and recreates these containers, and then deletes them again during cleanup.

Build and run the project

  1. Download the project dependencies.

    go mod download
    
  2. If you opened a new terminal after setup, load the .env values into that shell again.

  3. Run the sample.

    go run .
    
  4. Review an excerpt from the recorded output.

    Query: "hotel near the ocean"
    Embedding generated (1536 dimensions)
    
    Running search (top 5 results for each distance function)...
    

The sample demonstrates two goals:

  • Control plane: Authenticate, use the existing database, and delete and recreate the two configured vector-indexed containers.
  • Data plane: Load the same 50-document dataset, validate 1536-dimension embeddings, ingest documents by using Region as the partition key, generate a query embedding, run Cosine, DotProduct, and Euclidean queries, display ranked results and a comparison summary, and clean up the two containers.

Understand the vector distance functions

The sample's core purpose is comparing all three distance functions against the same containers and query embedding. Each function measures "closeness" differently, and that difference affects both the numeric scores and, for near ties, the ranking.

What each function measures

Function What it measures Score range
Cosine The angle between two vectors — magnitude-independent -1 to 1; higher values indicate greater similarity
DotProduct The inner product — accounts for both direction and magnitude Any real number; higher values indicate greater similarity
Euclidean Straight-line (L2) distance between vector endpoints — magnitude-sensitive 0 to 2 for unit-normalized vectors; lower values indicate greater similarity

How the example output reflects these differences

Across the sample's recorded output for the 50-hotel dataset, Cosine and DotProduct produce nearly identical scores and return the hotels in the same order. Euclidean produces scores in a different magnitude range for the same hotels.

Why Cosine and DotProduct can align closely here: This sample uses embeddings from the same model and compares the same documents across each distance function. As a result, Cosine and DotProduct often produce similar rankings. Depending on your data distribution and whether embeddings are normalized, the exact scores and ordering can still differ.

Cosine and DotProduct are similarity measures, not mathematical distance metrics. Cosine measures how closely the vectors point in the same direction. DotProduct measures that alignment while also accounting for vector magnitude. For both measures, a larger score means greater similarity.

Why Euclidean differs: Euclidean is a distance metric that measures the straight-line distance between vector endpoints. A smaller score means the vectors are closer together. The exact score ranges depend on the embeddings in your dataset.

How the distance function override works

Each query in this sample passes the function through the distanceFunction option in VectorDistance():

VectorDistance(c.embedding, @embedding, false, {'distanceFunction': 'Cosine'})

In this query, false tells Azure Cosmos DB to use the vector index instead of performing a brute-force search. The distanceFunction option tells it to calculate similarity by using cosine distance. This setting applies only to the current query—it doesn't change the distance function configured for the container. For the full signature, see the VectorDistance reference.

Tip

Match the query distance function to how your embedding model was trained. cosine is the standard default for OpenAI text embedding models, including text-embedding-3-small. Querying with a different function than the one stored in the container's vector policy still computes correctly, but might not use the vector index as efficiently.

Explore the app code

The following sections describe the main code paths in the Go sample.

nosql-create-index-go/
├── .env.example
├── config.go
├── controlplane.go
├── dataplane.go
├── go.mod
├── main.go
└── output/
    └── sample-output.txt
File Purpose
main.go Loads configuration and orchestrates control-plane and data-plane operations.
config.go Loads and validates environment variables.
controlplane.go Creates, recreates, verifies, and deletes the vector-indexed containers.
dataplane.go Loads documents, calls the embeddings REST API, ingests data, and runs vector queries.

Explore the credential and client setup

The orchestration code loads the configuration, creates one DefaultAzureCredential, and uses that credential to create the Azure Cosmos DB client.

cfg, err := LoadConfig()
if err != nil {
    log.Fatalf("configuration error: %v", err)
}

credential, err := azidentity.NewDefaultAzureCredential(nil)
if err != nil {
    log.Fatalf("failed to create DefaultAzureCredential: %v", err)
}

cosmosClient, err := azcosmos.NewClient(cfg.CosmosEndpoint, credential, nil)
if err != nil {
    log.Fatalf("failed to create Azure Cosmos DB client: %v", err)
}

databaseClient, err := cosmosClient.NewDatabase(cfg.DatabaseName)
if err != nil {
    log.Fatalf("failed to access database %q: %v", cfg.DatabaseName, err)
}

The same credential is passed to the control-plane method and later obtains the bearer token for the Azure OpenAI embeddings REST request.

Explore the control-plane container creation

The CreateContainersWithVectorIndexes function uses the Azure Resource Manager SDK to recreate one DiskANN container and one QuantizedFlat container in the existing database.

// CreateContainersWithVectorIndexes creates SQL containers with vector indexes using the ARM SDK
func CreateContainersWithVectorIndexes(
    ctx context.Context,
    credential *azidentity.DefaultAzureCredential,
    config *Config,
) error {
    if err := validateContainerDeletionSafety(config); err != nil {
        return fmt.Errorf("unsafe container deletion configuration: %w", err)
    }

    // Create ARM client
    client, err := armcosmos.NewSQLResourcesClient(config.SubscriptionID, credential, nil)
    if err != nil {
        return fmt.Errorf("failed to create ARM client: %w", err)
    }

    // Create containers with vector indexes
    indexConfigs := []struct {
        indexType     armcosmos.VectorIndexType
        containerName string
    }{
        {armcosmos.VectorIndexTypeDiskANN, config.DiskANNContainerName},
        {armcosmos.VectorIndexTypeQuantizedFlat, config.QuantizedFlatContainerName},
    }

    embeddingPath := "/" + strings.TrimPrefix(config.EmbeddingFieldName, "/")
    partitionKeyPath := "/" + config.PartitionKeyFieldName

    for _, indexConfig := range indexConfigs {
        fmt.Printf("\n=== Creating Container: %s ===\n", indexConfig.containerName)
        fmt.Printf("  Index type:     %s\n", string(indexConfig.indexType))
        fmt.Printf("  Embedding path: %s\n", embeddingPath)
        fmt.Printf("  Dimensions:     %d\n", config.EmbeddingDimensions)
        fmt.Printf("  Distance func:  cosine (queried with all 3 metrics)\n")

        // Build container resource with vector embedding policy
        containerResource := &armcosmos.SQLContainerResource{
            ID: ptr(indexConfig.containerName),
            PartitionKey: &armcosmos.ContainerPartitionKey{
                Paths: []*string{ptr(partitionKeyPath)},
                Kind:  ptr(armcosmos.PartitionKindHash),
            },
            VectorEmbeddingPolicy: &armcosmos.VectorEmbeddingPolicy{
                VectorEmbeddings: []*armcosmos.VectorEmbedding{
                    {
                        Path:             ptr(embeddingPath),
                        DataType:         ptr(armcosmos.VectorDataTypeFloat32),
                        Dimensions:       ptr(int32(config.EmbeddingDimensions)),
                        DistanceFunction: ptr(armcosmos.DistanceFunctionCosine),
                    },
                },
            },
            IndexingPolicy: &armcosmos.IndexingPolicy{
                IndexingMode: ptr(armcosmos.IndexingModeConsistent),
                Automatic:    ptr(true),
                IncludedPaths: []*armcosmos.IncludedPath{
                    {Path: ptr("/*")},
                },
                ExcludedPaths: []*armcosmos.ExcludedPath{
                    {Path: ptr(`/"_etag"/?`)},
                    {Path: ptr(embeddingPath + "/*")},
                },
                VectorIndexes: []*armcosmos.VectorIndex{
                    {
                        Path: ptr(embeddingPath),
                        Type: ptr(indexConfig.indexType),
                    },
                },
            },
        }

        // Delete existing container for idempotent re-runs
        fmt.Printf("  Pre-creation delete target: %s/%s/%s/%s\n", config.ResourceGroup, config.AccountName, config.DatabaseName, indexConfig.containerName)
        deleteStart := time.Now()
        deleteCtx, deleteCancel := context.WithTimeout(ctx, armDeleteTimeout)
        deletePoller, err := client.BeginDeleteSQLContainer(
            deleteCtx,
            config.ResourceGroup,
            config.AccountName,
            config.DatabaseName,
            indexConfig.containerName,
            nil,
        )
        if err != nil {
            deleteCancel()
            if isNotFound(err) {
                fmt.Printf("  ✓ Pre-creation deletion completed in %.1fs; container did not exist\n", time.Since(deleteStart).Seconds())
            } else {
                return fmt.Errorf("failed to begin pre-creation deletion for container %q: %w", indexConfig.containerName, err)
            }
        } else {
            fmt.Printf("  Waiting for pre-creation deletion to complete (polling every %s)...\n", armPollFrequency)
            _, err = deletePoller.PollUntilDone(
                deleteCtx,
                &runtime.PollUntilDoneOptions{Frequency: armPollFrequency},
            )
            deleteCancel()
            if err != nil {
                if isNotFound(err) {
                    fmt.Printf("  ✓ Pre-creation deletion completed in %.1fs; container no longer exists\n", time.Since(deleteStart).Seconds())
                } else {
                    return fmt.Errorf("failed to wait for pre-creation deletion of container %q: %w", indexConfig.containerName, err)
                }
            } else {
                fmt.Printf("  ✓ Pre-creation deletion completed in %.1fs\n", time.Since(deleteStart).Seconds())
            }
        }

        // Create the container
        fmt.Printf("  Creating container with vector index...\n")
        start := time.Now()
        createCtx, createCancel := context.WithTimeout(ctx, armCreateTimeout)

        // Note: Options is nil to support serverless accounts (which don't allow explicit throughput).
        // For provisioned accounts, you could set Options.Throughput or Options.AutoscaleSettings.
        containerPoller, err := client.BeginCreateUpdateSQLContainer(
            createCtx,
            config.ResourceGroup,
            config.AccountName,
            config.DatabaseName,
            indexConfig.containerName,
            armcosmos.SQLContainerCreateUpdateParameters{
                Location: ptr(config.Location),
                Properties: &armcosmos.SQLContainerCreateUpdateProperties{
                    Resource: containerResource,
                    Options:  nil, // nil = serverless compatible (no throughput setting)
                },
            },
            nil,
        )
        if err != nil {
            createCancel()
            return fmt.Errorf("failed to begin creating container %s: %w", indexConfig.containerName, err)
        }

        fmt.Printf("  Waiting for container creation to complete (polling every %s)...\n", armPollFrequency)
        if _, err := containerPoller.PollUntilDone(createCtx, &runtime.PollUntilDoneOptions{Frequency: armPollFrequency}); err != nil {
            createCancel()
            return fmt.Errorf("failed to create container %s: %w", indexConfig.containerName, err)
        }
        createCancel()

        elapsed := time.Since(start)
        fmt.Printf("  ✓ Container created in %.2fs\n", elapsed.Seconds())

        // Verify the container exists and has the correct configuration
        readCtx, readCancel := context.WithTimeout(ctx, armReadTimeout)
        got, err := client.GetSQLContainer(
            readCtx,
            config.ResourceGroup,
            config.AccountName,
            config.DatabaseName,
            indexConfig.containerName,
            nil,
        )
        readCancel()
        if err != nil {
            return fmt.Errorf("failed to verify container %s: %w", indexConfig.containerName, err)
        }

        res := got.Properties.Resource
        if res == nil {
            return fmt.Errorf("read-back resource for %s is nil", indexConfig.containerName)
        }

        // Log verification
        if res.VectorEmbeddingPolicy != nil && len(res.VectorEmbeddingPolicy.VectorEmbeddings) > 0 {
            emb := res.VectorEmbeddingPolicy.VectorEmbeddings[0]
            fmt.Printf("  ✓ Verified: Vector embedding policy configured\n")
            fmt.Printf("    - Path: %s\n", *emb.Path)
            fmt.Printf("    - DataType: %s\n", *emb.DataType)
            fmt.Printf("    - Dimensions: %d\n", *emb.Dimensions)
            fmt.Printf("    - DistanceFunction: %s\n", *emb.DistanceFunction)
        }

        if res.IndexingPolicy != nil && len(res.IndexingPolicy.VectorIndexes) > 0 {
            idx := res.IndexingPolicy.VectorIndexes[0]
            fmt.Printf("  ✓ Verified: Vector index configured\n")
            fmt.Printf("    - Path: %s\n", *idx.Path)
            fmt.Printf("    - Type: %s\n", *idx.Type)
        }
    }

    fmt.Printf("\n✓ All containers created successfully\n")
    return nil
}

The function completes the control-plane goal by applying /Region as the partition key path and /embedding as the 1536-dimension float32 vector path to both containers. The vector policy and index are immutable, so the function deletes and recreates both containers on every run.

Explore the document ingestion

The InsertDocuments function associates each of the 50 hotel documents with its Region partition key and writes the same data to both containers.

func InsertDocuments(ctx context.Context, container *azcosmos.ContainerClient, documents []map[string]any) (InsertStats, error) {
    stats := InsertStats{Total: len(documents)}
    semaphore := make(chan struct{}, maxInsertConcurrency)
    var mu sync.Mutex
    var wg sync.WaitGroup

    for _, document := range documents {
        document := document
        wg.Add(1)
        go func() {
            defer wg.Done()
            semaphore <- struct{}{}
            defer func() { <-semaphore }()

            // Extract Region from document for partition key
            region, ok := document["Region"].(string)
            if !ok {
                mu.Lock()
                stats.Failed++
                mu.Unlock()
                return
            }
            partitionKey := azcosmos.NewPartitionKey().AppendString(region)

            body, err := json.Marshal(document)
            if err != nil {
                mu.Lock()
                stats.Failed++
                mu.Unlock()
                return
            }

            // Retry with backoff on 429 (rate limiting)
            maxRetries := 5
            for attempt := 0; attempt <= maxRetries; attempt++ {
                response, err := container.CreateItem(ctx, partitionKey, body, nil)
                if err != nil {
                    var responseError *azcore.ResponseError
                    if errors.As(err, &responseError) {
                        if responseError.StatusCode == http.StatusConflict {
                            mu.Lock()
                            stats.Skipped++
                            mu.Unlock()
                            return
                        }
                        if responseError.StatusCode == http.StatusTooManyRequests && attempt < maxRetries {
                            wait := time.Duration(attempt+1) * time.Second
                            time.Sleep(wait)
                            continue
                        }
                    }
                    mu.Lock()
                    stats.Failed++
                    mu.Unlock()
                    return
                }

                mu.Lock()
                stats.Inserted++
                stats.RequestCharge += float64(response.RequestCharge)
                mu.Unlock()
                return
            }
        }()
    }

    wg.Wait()
    if stats.Failed > 0 {
        return stats, fmt.Errorf("failed to insert %d documents", stats.Failed)
    }

    return stats, nil
}

The function limits insertion concurrency to one operation at a time, treats 409 responses as existing documents, retries 429 responses, and returns an error if any document fails.

Explore the vector similarity queries

The GenerateEmbedding function gets an Azure OpenAI bearer token from the shared credential, submits the sample query text to the embeddings REST API, and validates the returned vector dimensions.

func GenerateEmbedding(ctx context.Context, httpClient *http.Client, credential azcore.TokenCredential, cfg *Config, text string) ([]float32, error) {
    token, err := credential.GetToken(ctx, policy.TokenRequestOptions{Scopes: []string{"https://cognitiveservices.azure.com/.default"}})
    if err != nil {
        return nil, fmt.Errorf("get Azure OpenAI bearer token: %w", err)
    }

    requestBody, err := json.Marshal(embeddingsRequest{Input: text})
    if err != nil {
        return nil, fmt.Errorf("marshal embeddings request: %w", err)
    }

    endpoint, err := url.Parse(strings.TrimRight(cfg.OpenAIEmbeddingEndpoint, "/"))
    if err != nil {
        return nil, fmt.Errorf("parse AZURE_OPENAI_EMBEDDING_ENDPOINT: %w", err)
    }
    endpoint.Path = fmt.Sprintf("/openai/deployments/%s/embeddings", cfg.OpenAIEmbeddingDeployment)
    query := endpoint.Query()
    query.Set("api-version", cfg.OpenAIAPIVersion)
    endpoint.RawQuery = query.Encode()

    request, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), bytes.NewReader(requestBody))
    if err != nil {
        return nil, fmt.Errorf("create embeddings request: %w", err)
    }
    request.Header.Set("Authorization", "Bearer "+token.Token)
    request.Header.Set("Content-Type", "application/json")

    response, err := httpClient.Do(request)
    if err != nil {
        return nil, fmt.Errorf("call Azure OpenAI embeddings API: %w", err)
    }
    defer response.Body.Close()

    responseBody, err := io.ReadAll(response.Body)
    if err != nil {
        return nil, fmt.Errorf("read embeddings response: %w", err)
    }

    if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices {
        return nil, fmt.Errorf("Azure OpenAI embeddings request failed: status=%d body=%s", response.StatusCode, strings.TrimSpace(string(responseBody)))
    }

    var payload embeddingsResponse
    if err := json.Unmarshal(responseBody, &payload); err != nil {
        return nil, fmt.Errorf("parse embeddings response: %w", err)
    }
    if payload.Error != nil {
        return nil, fmt.Errorf("Azure OpenAI embeddings error: %s", payload.Error.Message)
    }
    if len(payload.Data) == 0 {
        return nil, errors.New("Azure OpenAI embeddings response did not include any vectors")
    }

    embedding := make([]float32, len(payload.Data[0].Embedding))
    for index, value := range payload.Data[0].Embedding {
        embedding[index] = float32(value)
    }
    if len(embedding) != cfg.EmbeddingDimensions {
        return nil, fmt.Errorf("embedding dimensions mismatch: got %d, expected %d", len(embedding), cfg.EmbeddingDimensions)
    }

    return embedding, nil
}

The orchestration code passes that embedding to QueryTopHotels for every combination of the two containers and three comparison functions.

// --- Query ---
embedding, err := GenerateEmbedding(ctx, httpClient, credential, cfg, cfg.QueryText)
if err != nil {
    log.Fatalf("failed to generate Azure OpenAI embedding: %v", err)
}

fmt.Printf("\nQuery: %q\n", cfg.QueryText)
fmt.Printf("Embedding generated (%d dimensions)\n", len(embedding))
fmt.Println("\nRunning search (top 5 results for each distance function)...")

type metricResult struct {
    containerName string
    metric        string
    results       []VectorSearchResult
    ru            float64
}
var allResults []metricResult
distanceFunctions := []string{"Cosine", "DotProduct", "Euclidean"}

for _, containerName := range cfg.ContainerNames {
    containerClient, err := databaseClient.NewContainer(containerName)
    if err != nil {
        log.Fatalf("failed to access container %q: %v", containerName, err)
    }

    for _, distanceFunction := range distanceFunctions {
        results, ru, err := QueryTopHotels(ctx, containerClient, embedding, cfg.EmbeddingFieldName, distanceFunction, cfg.PartitionKeyFieldValue)
        if err != nil {
            log.Fatalf("container %q query failed: %v", containerName, err)
        }
        fmt.Printf("  ✓ %s queried (%.2f RUs)\n", containerName, ru)
        allResults = append(allResults, metricResult{
            containerName: containerName,
            metric:        distanceFunction,
            results:       results,
            ru:            ru,
        })
    }
}

The QueryTopHotels function validates the embedding field name, binds the embedding as a SQL parameter, scopes the request to one partition, and reads the ranked results.

func QueryTopHotels(ctx context.Context, container *azcosmos.ContainerClient, embedding []float32, embeddingField, distanceFunction, partitionKeyValue string) ([]VectorSearchResult, float64, error) {
    if !validIdentifier.MatchString(embeddingField) {
        return nil, 0, fmt.Errorf("invalid embedding field name: %q", embeddingField)
    }

    // Convert float32 to float64 for parameter binding
    // The Cosmos DB SDK expects []float64 for numeric array parameters
    embedding64 := make([]float64, len(embedding))
    for i, v := range embedding {
        embedding64[i] = float64(v)
    }

    // ORDER BY VectorDistance(...) is required to rank results nearest-first.
    // Without it, SELECT TOP 5 returns 5 arbitrary documents from the partition,
    // not the nearest neighbors. SDK partition-key routing scopes the search to a
    // single partition, so the SQL stays focused on vector ranking.
    queryText := fmt.Sprintf(`SELECT TOP 5
        c.HotelId,
        c.HotelName,
        c.Description,
        VectorDistance(c.%s, @embedding, false, {'distanceFunction': '%s'}) AS SimilarityScore
    FROM c
    ORDER BY VectorDistance(c.%s, @embedding, false, {'distanceFunction': '%s'})`, embeddingField, distanceFunction, embeddingField, distanceFunction)

    options := azcosmos.QueryOptions{
        QueryParameters: []azcosmos.QueryParameter{
            {
                Name:  "@embedding",
                Value: embedding64,
            },
        },
    }

    // SDK-level partition key routing scopes the vector query to one partition.
    partitionKey := azcosmos.NewPartitionKey().AppendString(partitionKeyValue)
    pager := container.NewQueryItemsPager(queryText, partitionKey, &options)

    var results []VectorSearchResult
    var totalRequestCharge float64

    for pager.More() {
        page, err := pager.NextPage(ctx)
        if err != nil {
            return nil, totalRequestCharge, fmt.Errorf("query page: %w", err)
        }
        totalRequestCharge += float64(page.RequestCharge)
        for _, item := range page.Items {
            var result VectorSearchResult
            if err := json.Unmarshal(item, &result); err != nil {
                return nil, totalRequestCharge, fmt.Errorf("parse vector search result: %w", err)
            }
            results = append(results, result)
        }
    }

    return results, totalRequestCharge, nil
}

These steps complete the data-plane comparison goal by running the same query embedding with Cosine, DotProduct, and Euclidean against both vector index types. For guidance on choosing a distance function for your own data, see the vector distance function overview earlier in this article.

Explore the single-partition query pattern

The sample scopes each query to the configured Region value, which defaults to Northeast, through the SDK partition key option.

Mechanism How it works Sample code
SDK partition key Passes a partition key to NewQueryItemsPager so the request targets the configured partition key value. container.NewQueryItemsPager(queryText, partitionKey, &options)
Comparison Single-partition query Cross-partition query
Query scope One configured region. All regions.
Documents in the supplied dataset 10 documents in the default Northeast region. 50 documents across all regions.
Routing Uses the SDK partition key value. Requires cross-partition fan-out.

You can alternatively add a WHERE c.Region = @region predicate to the SQL statement. This sample uses the SDK partition key option instead.

An ORDER BY VectorDistance(...) clause is required for nearest-neighbor ranking. The expression repeats the same VectorDistance(...) call used in the SELECT clause.

View and manage data in Visual Studio Code

Use the Azure Databases extension for Visual Studio Code to connect to your Azure Cosmos DB account and browse the hotels_diskann and hotels_quantizedflat containers.

  1. In Visual Studio Code, select the Azure icon in the Activity Bar.
  2. Under Resources, expand Azure Cosmos DB, and locate your account.
  3. Expand your account > HotelsCreateIndex > hotels_diskann or hotels_quantizedflat.
  4. Select a document to view its hotel fields and the embedding vector array.

Note

The sample deletes both containers at the end of each run. To retain the containers for inspection, comment out the DeleteContainers call in main.go before you run the sample.

Troubleshooting

Symptom Cause Fix
configuration error: missing required environment variables The generated configuration is missing or isn't loaded into the current shell. Verify the .env values, load them into the current shell, and rerun the sample.
DefaultAzureCredential can't authenticate The current shell doesn't have an authenticated Azure identity. Run azd auth login, and then rerun the sample.
403 response during container creation The identity lacks control-plane access. Verify that the identity has permission to create and delete Azure Cosmos DB containers through Azure Resource Manager.
403 response during document ingestion or query The identity lacks data-plane access. Verify that the identity has Cosmos DB Built-in Data Contributor. Role assignments can take several minutes to propagate.
Azure OpenAI authorization error The identity lacks access to the embedding deployment. Verify that the identity has Cognitive Services OpenAI User on the Azure OpenAI resource.
Embedding dimension mismatch The embedding deployment returns a vector size other than 1536. Verify that AZURE_OPENAI_EMBEDDING_DEPLOYMENT identifies the expected text-embedding-3-small deployment.
Database or container configuration error The configured database doesn't exist or the two container names are identical. Verify the database and container settings, and then rerun the sample.

Clean up resources

The sample automatically deletes its vector-indexed containers at the end of each run. To remove the remaining Azure resources that the sample provisions, run azd down.

azd down