Edit

Quickstart: Vector index with TypeScript in Azure DocumentDB

In this quickstart, you compare three vector index algorithms (DiskANN, HNSW, and IVF) and three similarity functions (cosine, L2, and inner product) to find the optimal configuration for your search workload. This quickstart uses a sample hotel 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.

Create the Node.js project

You should end up with the following project structure:

select-algorithm-typescript/
├── data/
│   └── Hotels_Vector.json
├── src/
│   ├── compare-all.ts
│   └── utils.ts
├── package.json
└── tsconfig.json
  1. Create a new directory for your project and open it in Visual Studio Code:

    mkdir select-algorithm-typescript
    cd select-algorithm-typescript
    code .
    

  1. Initialize a TypeScript Node.js project:

    npm init -y
    npm pkg set type="module"
    

    Verify the project was initialized:

    ls package.json
    

  1. Install the required packages:

    npm install mongodb openai @azure/identity
    npm install --save-dev typescript @types/node
    
    • mongodb: MongoDB driver for Node.js.
    • openai: OpenAI client library to create vectors.
    • @azure/identity: Azure Identity library for passwordless authentication.
    • typescript: TypeScript compiler.

    Verify that npm list shows all installed packages without errors.

  2. Create a tsconfig.json file in the project root:

    {
      "compilerOptions": {
        "target": "ES2022",
        "module": "Node16",
        "moduleResolution": "Node16",
        "esModuleInterop": true,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true,
        "resolveJsonModule": true,
        "strict": true,
        "rootDir": "./src",
        "outDir": "./dist"
      },
      "include": ["src/**/*"],
      "exclude": ["node_modules"]
    }
    
  3. Update your package.json to include:

    {
      "type": "module",
      "scripts": {
        "build": "tsc",
        "start": "node dist/compare-all.js"
      }
    }
    
  4. Create the src directory:

    mkdir src
    

Create data file with vectors

  1. Create a new data directory for the hotels data file:

    mkdir data
    

  1. Download the Hotels_Vector.json data file with vectors to your data directory:

    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

Replace the placeholder values with your own information:

  • AZURE_OPENAI_EMBEDDING_ENDPOINT: Your Azure OpenAI resource endpoint URL
  • DOCUMENTDB_CLUSTER_NAME: Your Azure DocumentDB cluster name

Prefer passwordless authentication, but it requires additional setup. For more information on setting up managed identity and the full range of your authentication options, see Authenticate JavaScript apps to Azure services using the Azure SDK for JavaScript.

Create the algorithm comparison code

Create the src/compare-all.ts file with the following code:

import path from 'path';
import { readFileReturnJson, getClientsPasswordless, getConfig, insertData } from './utils.js';
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

interface AlgorithmConfig {
    name: string;
    kind: string;
    options: Record<string, number>;
}

interface SearchResult {
    algorithm: string;
    similarity: string;
    top1Name: string;
    top1Score: number;
    top2Name: string;
    top2Score: number;
}

const ALGORITHMS: AlgorithmConfig[] = [
    { name: 'IVF', kind: 'vector-ivf', options: { numLists: 1 } },
    { name: 'HNSW', kind: 'vector-hnsw', options: { m: 16, efConstruction: 64 } },
    { name: 'DiskANN', kind: 'vector-diskann', options: { maxDegree: 32, lBuild: 50 } },
];

const SIMILARITIES = ['COS', 'L2', 'IP'];

async function main() {
    const baseConfig = getConfig();
    const queryText = process.env.QUERY_TEXT || 'luxury hotel near the beach';
    const topK = parseInt(process.env.TOP_K || '5', 10);
    const collectionName = 'hotels';

    const { aiClient, dbClient } = getClientsPasswordless();

    try {
        if (!aiClient) throw new Error('AI client is not configured.');
        if (!dbClient) throw new Error('Database client is not configured.');

        await dbClient.connect();
        const db = dbClient.db(baseConfig.dbName);

        // Drop collection if it exists for a clean start
        const collections = await db.listCollections({ name: collectionName }).toArray();
        if (collections.length > 0) {
            try {
                const col = db.collection(collectionName);
                const existingIndexes = await col.listIndexes().toArray();
                for (const idx of existingIndexes) {
                    if (idx.name !== '_id_') {
                        try {
                            await col.dropIndex(idx.name);
                        } catch {}
                    }
                }
                await db.dropCollection(collectionName);
                console.log(`Dropped existing collection: ${collectionName}`);
            } catch (e: any) {
                console.log(`Cleanup note: ${e.message.split('\n')[0]}`);
            }
        }

        // Load data once for reuse
        const data = await readFileReturnJson(path.join(__dirname, '..', baseConfig.dataFile));
        console.log(`Loaded ${data.length} documents`);

        // Insert data into collection
        const collection = db.collection(collectionName);
        await insertData(baseConfig, collection, data, false);

        // Generate one embedding for the query
        console.log(`\nQuery: "${queryText}"`);
        const embeddingResponse = await aiClient.embeddings.create({
            model: baseConfig.deployment,
            input: [queryText]
        });
        const queryVector = embeddingResponse.data[0].embedding;
        console.log(`Embedding generated (${queryVector.length} dimensions)`);

        // Sequential create→search→drop for each algorithm+similarity combo
        // DocumentDB does not allow multiple vector indexes of the same kind on the same field
        console.log(`\nRunning searches (top ${topK} results)...\n`);
        const results: SearchResult[] = [];

        for (const algo of ALGORITHMS) {
            for (const sim of SIMILARITIES) {
                const indexName = `vector_${algo.kind.replace('vector-', '')}_${sim.toLowerCase()}`;

                // 1. Drop all existing vector indexes
                const indexes = await collection.listIndexes().toArray();
                let droppedAny = false;
                for (const idx of indexes) {
                    if (idx.key && idx.key[baseConfig.embeddedField] === 'cosmosSearch') {
                        try { await collection.dropIndex(idx.name); droppedAny = true; } catch {}
                    }
                }
                // 2. Create this specific index
                const indexOptions = {
                    createIndexes: collectionName,
                    indexes: [{
                        name: indexName,
                        key: { [baseConfig.embeddedField]: 'cosmosSearch' },
                        cosmosSearchOptions: {
                            kind: algo.kind,
                            ...algo.options,
                            similarity: sim,
                            dimensions: baseConfig.embeddingDimensions
                        }
                    }]
                };
                await db.command(indexOptions);
                console.log(`  ✓ ${indexName} created`);

                // 3. Search with bounded retry while the new index becomes ready
                const searchPipeline = [
                    {
                        $search: {
                            cosmosSearch: {
                                vector: queryVector,
                                path: baseConfig.embeddedField,
                                k: topK
                            }
                        }
                    },
                    {
                        $project: {
                            score: { $meta: 'searchScore' },
                            document: '$$ROOT'
                        }
                    }
                ];

                let searchResults: any[] = [];
                let lastSearchError: unknown;
                for (let attempt = 1; attempt <= 6; attempt++) {
                    try {
                        searchResults = await collection.aggregate(searchPipeline).toArray();
                        if (searchResults.length > 0 || attempt === 6) {
                            break;
                        }
                        console.log(`  ...search returned no results yet, retrying (${attempt}/6)`);
                    } catch (e) {
                        lastSearchError = e;
                        if (attempt === 6) {
                            throw e;
                        }
                        console.log(`  ...search not ready yet, retrying (${attempt}/6)`);
                    }

                    await new Promise(r => setTimeout(r, 2000));
                }

                if (searchResults.length === 0 && lastSearchError) {
                    throw lastSearchError;
                }

                // Record top 2 results
                const top1 = searchResults[0] as any;
                const top2 = searchResults[1] as any;
                results.push({
                    algorithm: algo.name,
                    similarity: sim,
                    top1Name: top1?.document?.HotelName ?? '(none)',
                    top1Score: top1?.score ?? 0,
                    top2Name: top2?.document?.HotelName ?? '(none)',
                    top2Score: top2?.score ?? 0,
                });
            }
        }

        // Print comparison table
        printComparisonTable(results);

    } catch (error) {
        console.error('Compare-all failed:', error);
        process.exitCode = 1;
    } finally {
        // Cleanup: drop the comparison collection
        if (dbClient) {
            try {
                const db = dbClient.db(baseConfig.dbName);
                await db.dropCollection(collectionName);
                console.log(`\nCleanup: dropped collection "${collectionName}"`);
            } catch (cleanupErr) {
                console.error('Cleanup warning:', cleanupErr);
            }
            await dbClient.close();
            console.log('Database connection closed');
        }
    }
}

function printComparisonTable(results: SearchResult[]) {
    const algoW = 10;
    const simW = 8;
    const name1W = 28;
    const score1W = 8;
    const name2W = 28;
    const score2W = 8;
    const diffW = 7;

    const pad = (s: string, w: number) => s.length >= w ? s.slice(0, w) : s + ' '.repeat(w - s.length);

    const cols = [algoW, simW, name1W, score1W, name2W, score2W, diffW];
    const topLine    = `┌${cols.map(w => '─'.repeat(w)).join('┬')}┐`;
    const headerSep  = `├${cols.map(w => '─'.repeat(w)).join('┼')}┤`;
    const rowSep     = `├${cols.map(w => '─'.repeat(w)).join('┼')}┤`;
    const bottomLine = `└${cols.map(w => '─'.repeat(w)).join('┴')}┘`;

    console.log(topLine);
    console.log(
        `│${pad(' Algorithm', algoW)}│${pad(' Metric', simW)}│${pad(' Top 1 Result', name1W)}│${pad(' Score', score1W)}│${pad(' Top 2 Result', name2W)}│${pad(' Score', score2W)}│${pad(' Diff', diffW)}│`
    );
    console.log(headerSep);

    results.forEach((r, i) => {
        const diff = Math.abs(r.top1Score - r.top2Score).toFixed(4);
        console.log(
            `│${pad(` ${r.algorithm}`, algoW)}│${pad(` ${r.similarity}`, simW)}│${pad(` ${r.top1Name}`, name1W)}│${pad(` ${r.top1Score.toFixed(4)}`, score1W)}│${pad(` ${r.top2Name}`, name2W)}│${pad(` ${r.top2Score.toFixed(4)}`, score2W)}│${pad(` ${diff}`, diffW)}│`
        );
        if (i < results.length - 1) {
            console.log(rowSep);
        }
    });

    console.log(bottomLine);
}

main().catch(error => {
    console.error('Unhandled error:', error);
    process.exitCode = 1;
});

This script orchestrates the algorithm comparison by:

  • Loading configuration from environment variables.
  • Initializing MongoDB and Azure OpenAI clients with passwordless authentication.
  • Loading hotel data with precalculated embeddings.
  • Testing each algorithm/similarity combination by creating a collection, inserting data, creating an index, and executing a search.
  • Measuring and comparing search performance across all configurations.
  • Displaying results in a comparison table.

Create utility functions

Create the src/utils.ts file with the following code:

import { Collection, Document, MongoClient, OIDCResponse, OIDCCallbackParams } from 'mongodb';
import { AzureOpenAI } from 'openai';
import { promises as fs } from "fs";
import { AccessToken, DefaultAzureCredential, TokenCredential, getBearerTokenProvider } from '@azure/identity';

// Define a type for JSON data
export type JsonData = Record<string, any>;

export function getConfig() {
    return {
        dbName: process.env.AZURE_DOCUMENTDB_DATABASENAME || 'Hotels',
        dataFile: process.env.DATA_FILE_WITH_VECTORS || 'data/Hotels_Vector.json',
        embeddedField: process.env.EMBEDDED_FIELD || 'DescriptionVector',
        similarity: process.env.SIMILARITY || '',
        embeddingDimensions: parseInt(process.env.EMBEDDING_DIMENSIONS || '1536', 10),
        deployment: process.env.AZURE_OPENAI_EMBEDDING_MODEL || 'text-embedding-3-small',
        batchSize: parseInt(process.env.LOAD_SIZE_BATCH || '100', 10)
    };
}

export const AzureIdentityTokenCallback = async (params: OIDCCallbackParams, credential: TokenCredential): Promise<OIDCResponse> => {
    const tokenResponse: AccessToken | null = await credential.getToken(['https://ossrdbms-aad.database.windows.net/.default']);

    if (!tokenResponse || !tokenResponse.token) {
        throw new Error('Failed to acquire token');
    }

    return {
        accessToken: tokenResponse.token,
        expiresInSeconds: Math.floor((tokenResponse.expiresOnTimestamp - Date.now()) / 1000)
    };
};

export function getClientsPasswordless(): { aiClient: AzureOpenAI | null; dbClient: MongoClient | null } {
    let aiClient: AzureOpenAI | null = null;
    let dbClient: MongoClient | null = null;

    // Validate all required environment variables upfront
    const endpoint = process.env.AZURE_OPENAI_EMBEDDING_ENDPOINT!;
    const deployment = process.env.AZURE_OPENAI_EMBEDDING_MODEL!;
    const clusterName = process.env.DOCUMENTDB_CLUSTER_NAME!;

    if (!endpoint || !deployment || !clusterName) {
        throw new Error('Missing required environment variables: AZURE_OPENAI_EMBEDDING_ENDPOINT, AZURE_OPENAI_EMBEDDING_MODEL, DOCUMENTDB_CLUSTER_NAME');
    }

    console.log(`Using Azure OpenAI Embedding Deployment/Model: ${deployment}`);

    const credential = new DefaultAzureCredential();

    // For Azure OpenAI with DefaultAzureCredential
    {
        const scope = "https://cognitiveservices.azure.com/.default";
        const azureADTokenProvider = getBearerTokenProvider(credential, scope);
        aiClient = new AzureOpenAI({
            apiVersion: process.env.AZURE_OPENAI_EMBEDDING_API_VERSION || "2023-05-15",
            endpoint,
            deployment,
            azureADTokenProvider,
            timeout: 30000,
            maxRetries: 3,
        });
    }

    // For DocumentDB with DefaultAzureCredential (uses signed-in user)
    {
        dbClient = new MongoClient(
            `mongodb+srv://${clusterName}.global.mongocluster.cosmos.azure.com/`, {
            connectTimeoutMS: 120000,
            tls: true,
            retryWrites: false,
            maxIdleTimeMS: 120000,
            authMechanism: 'MONGODB-OIDC',
            authMechanismProperties: {
                OIDC_CALLBACK: (params: OIDCCallbackParams) => AzureIdentityTokenCallback(params, credential),
                ALLOWED_HOSTS: ['*.azure.com']
            }
        }
        );
    }

    return { aiClient, dbClient };
}

export async function readFileReturnJson(filePath: string): Promise<JsonData[]> {

    console.log(`Reading JSON file from ${filePath}`);

    const fileAsString = await fs.readFile(filePath, "utf-8");
    return JSON.parse(fileAsString);
}

export async function insertData(
    config: { batchSize: number },
    collection: Collection,
    data: Document[],
    createScalarIndexes: boolean = true
) {
    console.log(`Processing in batches of ${config.batchSize}...`);
    const totalBatches = Math.ceil(data.length / config.batchSize);

    let inserted = 0;
    let failed = 0;

    for (let i = 0; i < totalBatches; i++) {
        const start = i * config.batchSize;
        const end = Math.min(start + config.batchSize, data.length);
        const batch = data.slice(start, end);

        try {
            const result = await collection.insertMany(batch, { ordered: false });
            inserted += result.insertedCount || 0;
            console.log(`Batch ${i + 1} complete: ${result.insertedCount} inserted`);
        } catch (error: any) {
            if (error?.writeErrors) {
                console.error(`Error in batch ${i + 1}: ${error?.writeErrors.length} failures`);
                failed += error?.writeErrors.length;
                inserted += batch.length - error?.writeErrors.length;
            } else {
                console.error(`Error in batch ${i + 1}:`, error);
                failed += batch.length;
            }
        }

        // Small pause between batches to reduce resource contention
        if (i < totalBatches - 1) {
            await new Promise(resolve => setTimeout(resolve, 100));
        }
    }

    if (createScalarIndexes) {
        const indexColumns = ["HotelId", "Category", "Description", "Description_fr"];
        for (const col of indexColumns) {
            const indexSpec: Record<string, number> = {};
            indexSpec[col] = 1;
            await collection.createIndex(indexSpec);
        }
    }

    return { total: data.length, inserted, failed };
}

export function printSearchResults(insertSummary: any, vectorIndexSummary: any, searchResults: Document[]) {
    console.log(`\nInsert summary: ${JSON.stringify(insertSummary)}`);
    console.log(`Vector index: ${JSON.stringify(vectorIndexSummary)}`);

    if (!searchResults || searchResults.length === 0) {
        console.log('No search results found.');
        return;
    }

    searchResults.map((result: Document, index: number) => {
        const { document, score } = result;
        console.log(`${index + 1}. HotelName: ${document.HotelName}, Score: ${score.toFixed(4)}`);
    });
}

/**
 * Print a side-by-side comparison table of vector search results across collections
 */
export function printComparisonTable(
    results: Array<{
        collectionName: string;
        algorithm: string;
        similarity: string;
        searchResults: any[];
        latencyMs: number;
    }>
): void {
    console.log('\n╔══════════════════════════════════════════════════════════════════════════════════╗');
    console.log('║                     Vector Algorithm Comparison Results                         ║');
    console.log('╠══════════════════════════════════════════════════════════════════════════════════╣');

    // Header
    console.log(
        '║ ' +
        'Algorithm'.padEnd(12) +
        'Similarity'.padEnd(14) +
        'Top Result'.padEnd(24) +
        'Score'.padEnd(12) +
        'Latency(ms)'.padEnd(14) +
        '║'
    );
    console.log('╠══════════════════════════════════════════════════════════════════════════════════╣');

    for (const r of results) {
        const topResult = r.searchResults[0];
        const topName = topResult ? (topResult.document.HotelName as string).substring(0, 22) : 'N/A';
        const topScore = topResult ? topResult.score.toFixed(4) : 'N/A';

        console.log(
            '║ ' +
            r.algorithm.padEnd(12) +
            r.similarity.padEnd(14) +
            topName.padEnd(24) +
            topScore.padEnd(12) +
            r.latencyMs.toFixed(0).padEnd(14) +
            '║'
        );
    }

    console.log('╚══════════════════════════════════════════════════════════════════════════════════╝');

    // Detailed results per collection
    for (const r of results) {
        console.log(`\n--- ${r.algorithm} / ${r.similarity} (${r.collectionName}) ---`);
        if (r.searchResults.length === 0) {
            console.log('  No results.');
            continue;
        }
        r.searchResults.forEach((item: Document, i: number) => {
            console.log(`  ${i + 1}. ${item.document.HotelName}, Score: ${item.score.toFixed(4)}`);
        });
        console.log(`  Latency: ${r.latencyMs.toFixed(0)}ms`);
    }
}

The utilities provide essential functions for:

  • Passwordless authentication to DocumentDB and Azure OpenAI using DefaultAzureCredential.
  • Reading JSON data files.
  • Batch insertion of documents with DocumentDB's 16 MB payload limit in mind.
  • Formatted display of comparison results showing algorithm performance.

Note

The Node.js sample configures the DocumentDB connection with retryWrites=false, which is required for DocumentDB vector search operations.

Run the code

  1. Sign in with Azure CLI for passwordless authentication:

    az login
    
  2. Execute the comparison script to test all 9 algorithm × similarity combinations:

    npm run build
    npm start
    

Expected output:

Using Azure OpenAI Embedding Deployment/Model: text-embedding-3-small
Reading JSON file from data/Hotels_Vector.json
Loaded 50 documents
Processing in batches of 50...
Batch 1 complete: 50 inserted

Query: "luxury hotel near the beach"
Embedding generated (1536 dimensions)

Running searches (top 5 results)...  ✓ 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│
└──────────┴────────┴────────────────────────────┴────────┴────────────────────────────┴────────┴───────┘

Cleanup: dropped collection "hotels"
Database connection closed

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 demonstrates key behaviors of vector search in DocumentDB:

  • All algorithms return identical results on small datasets. With 50 documents, every algorithm finds the same matches because the dataset fits entirely in memory regardless of index structure. Algorithm selection becomes important at scale (millions of documents) where tradeoffs in latency, memory, and recall diverge.

  • COS and IP produce identical scores (0.6184 / 0.5056) because the text-embedding-3-small model outputs normalized (unit-length) vectors. For normalized vectors, cosine similarity equals inner product mathematically.

  • L2 (Euclidean distance) scores represent distance. In this output, the top result has the lower L2 score (0.8736) and the second result is farther away (0.9943).

  • Score separation (Diff column) shows the gap between the top two results. A smaller diff indicates the algorithm found results with more similar relevance scores.

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 numLists for larger datasets, increase nProbes for 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 maxDegree and lBuild for better accuracy, increase lSearch for 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 m and efConstruction for better index quality, increase efSearch for 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
MongoServerSelectionError Verify your DOCUMENTDB_CLUSTER_NAME environment variable and ensure your IP is in the DocumentDB firewall rules.
MongoServerError: Authentication failed Verify your Microsoft Entra token is valid. Run az login to refresh your credentials.
TypeScript compilation errors Run npx tsc --version to verify TypeScript is installed. Check tsconfig.json settings match the values shown in this article.
Cannot find module errors Run npm install to ensure all dependencies are installed.
Embedding dimension mismatch Verify the AZURE_OPENAI_EMBEDDING_MODEL environment variable matches the model deployed in your Azure OpenAI resource.
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:

  1. Install the DocumentDB for VS Code extension.
  2. Connect to your Azure DocumentDB cluster.
  3. Expand the cluster, right-click the Hotels database, and select Drop Database.