Hızlı Başlangıç: Azure Cosmos DB'de Node.js ile vektör araması

Node.js istemci kitaplığıyla Azure Cosmos DB'de vektör aramasını kullanın. Vektör verilerini uygulamalarınızda verimli bir şekilde depolayın ve sorgulayın.

Bu hızlı başlangıçta, text-embedding-3-small modelindeki vektörlerle birlikte bir JSON dosyasında örnek bir otel veri kümesi kullanılmaktadır. Veri kümesi otel adlarını, konumlarını, açıklamalarını ve vektör eklemelerini içerir.

GitHub üzerinde kaynak sağlama ile örnek kodu bulun.

Prerequisites

Vektörlerle veri dosyası oluşturma

  1. Oteller veri dosyası için yeni bir veri dizini oluşturun:

    mkdir data
    
  2. vektörler içeren ham veri dosyasınıdata dizininize indirin.

    curl -o data/HotelsData_toCosmosDB_Vector.json https://raw.githubusercontent.com/Azure-Samples/cosmos-db-vector-samples/refs/heads/main/data/HotelsData_toCosmosDB_Vector.json
    

Node.js projesi oluşturma

  1. Projeniz için veri diziniyle aynı düzeyde yeni bir eşdüzey dizin oluşturun ve bunu Visual Studio Code'da açın:

    mkdir vector-search-quickstart
    code vector-search-quickstart
    
  2. Terminalde bir Node.js projesi başlatın:

    npm init -y
    npm pkg set type="module"
    
  3. Gerekli paketleri yükleyin:

    npm install @azure/identity @azure/cosmos openai
    npm install @types/node cross-env --save-dev
    
    • @azure/identity - parolasız (yönetilen kimlik) bağlantılar için Azure kimlik doğrulama kitaplığı
    • @azure/cosmos - Veritabanı işlemleri için Azure Cosmos DB istemci kitaplığı
    • openai - Azure OpenAI ile eklemeler oluşturmak için OpenAI SDK'sı
    • @types/node (geliştirme) - Node.js API'leri için TypeScript tür tanımları
    • cross-env (dev) - npm betikleri için platformlar arası ortam değişkeni ayarı
  4. Ortam değişkenleri için proje kökünde bir .env dosya oluşturun:

    # Identity for local developer authentication with Azure CLI
    AZURE_TOKEN_CREDENTIALS=AzureCliCredential
    
    # Azure OpenAI Embedding Settings
    AZURE_OPENAI_EMBEDDING_MODEL=text-embedding-3-small
    AZURE_OPENAI_EMBEDDING_API_VERSION=2023-05-15
    AZURE_OPENAI_EMBEDDING_ENDPOINT=
    
    # Cosmos DB configuration
    AZURE_COSMOSDB_ENDPOINT=
    
    # Data file
    DATA_FILE_WITH_VECTORS=../data/HotelsData_toCosmosDB_Vector.json
    FIELD_TO_EMBED=Description
    EMBEDDED_FIELD=DescriptionVector
    EMBEDDING_DIMENSIONS=1536
    

    Dosyadaki .env yer tutucu değerlerini kendi bilgilerinizle değiştirin:

    • AZURE_OPENAI_EMBEDDING_ENDPOINT: Azure OpenAI kaynak uç noktası URL'niz
    • AZURE_COSMOSDB_ENDPOINT: Azure Cosmos DB uç nokta URL'niz
  5. TypeScript'i yapılandırmak için bir tsconfig.json dosya ekleyin:

    {
        "compilerOptions": {
            "target": "ES2020",
            "module": "NodeNext",
            "moduleResolution": "nodenext",
            "declaration": true,
            "outDir": "./dist",
            "strict": true,
            "esModuleInterop": true,
            "skipLibCheck": true,
            "noImplicitAny": false,
            "forceConsistentCasingInFileNames": true,
            "sourceMap": true,
            "resolveJsonModule": true,
        },
        "include": [
            "src/**/*"
        ],
        "exclude": [
            "node_modules",
            "dist"
        ]
    }
    

Belge şemasını anlama

Uygulamayı oluşturmadan önce vektörlerin Azure Cosmos DB belgelerinde nasıl depolandığını anlayın. Her otel belgesinde aşağıdakiler bulunur:

  • Standart alanlar: HotelId, HotelName, Description, Category, vb.
  • Vektör alanı: DescriptionVector - Otel açıklamasının anlamsal anlamını temsil eden 1536 kayan noktalı sayı dizisi

Otel belgesi yapısının basitleştirilmiş bir örneği aşağıda verilmiştir:

{
  "HotelId": "1",
  "HotelName": "Stay-Kay City Hotel",
  "Description": "This classic hotel is fully-refurbished...",
  "Rating": 3.6,
  "DescriptionVector": [
    -0.04886505,
    -0.02030743,
    0.01763356,
    ...
    // 1536 dimensions total
  ]
}

Eklemeleri depolamayla ilgili önemli noktalar:

  • Vektör dizileri belgelerinizde standart JSON dizileri olarak depolanır
  • Vektör ilkesi yolu (/DescriptionVector), veri türünü (float32), boyutları (1536) ve uzaklık işlevini (kosinüs) tanımlar
  • Dizin oluşturma ilkesi , verimli benzerlik araması için vektör alanında bir vektör dizini oluşturur
  • Ekleme performansını iyileştirmek için vektör alanı standart dizin oluşturmanın dışında tutulmalıdır

Bu ilkeler, bu örnek projenin uzaklık ölçümleri için Bicep şablonlarında tanımlanır. Vektör ilkeleri ve dizin oluşturma hakkında daha fazla bilgi için bkz. Azure Cosmos DB'de vektör araması.

Npm betikleri oluşturma

package.json Dosyayı düzenleyin ve şu betikleri ekleyin:

TypeScript dosyalarını derlemek ve DiskANN dizin uygulamasını çalıştırmak için bu betikleri kullanın.

"scripts": { 
    "build": "tsc",
    "start:diskann": "cross-env VECTOR_ALGORITHM=diskann node --env-file .env dist/vector-search.js"
}

TypeScript dosyalarınız için bir src dizin oluşturun. İki dosya ekleyin: vector-search.ts ve utils.ts vektör arama uygulamanız için:

mkdir src
touch src/vector-search.ts
touch src/utils.ts

Aşağıdaki kodu dosyaya yapıştırın vector-search.ts .

 import path from 'path';
import { readFileReturnJson, getClientsPasswordless, validateFieldName, insertData, printSearchResults, getQueryActivityId } from './utils.js';

// ESM specific features - create __dirname equivalent
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

type VectorAlgorithm = 'diskann' | 'quantizedflat';

interface AlgorithmConfig {
    containerName: string;
    algorithmName: string;
}

const algorithmConfigs: Record<VectorAlgorithm, AlgorithmConfig> = {
    diskann: {
        containerName: 'hotels_diskann',
        algorithmName: 'DiskANN'
    },
    quantizedflat: {
        containerName: 'hotels_quantizedflat',
        algorithmName: 'QuantizedFlat'
    }
};

const config = {
    query: "quintessential lodging near running trails, eateries, retail",
    dbName: "Hotels",
    algorithm: (process.env.VECTOR_ALGORITHM || 'diskann').trim().toLowerCase() as VectorAlgorithm,
    dataFile: process.env.DATA_FILE_WITH_VECTORS!,
    embeddedField: process.env.EMBEDDED_FIELD!,
    embeddingDimensions: parseInt(process.env.EMBEDDING_DIMENSIONS! || process.env.VECTOR_EMBEDDING_DIMENSIONS || '1536', 10),
    deployment: process.env.AZURE_OPENAI_EMBEDDING_MODEL!,
    distanceFunction: process.env.VECTOR_DISTANCE_FUNCTION || 'cosine',
};

async function main() {
    const { aiClient, dbClient } = getClientsPasswordless();

    try {
        // Validate algorithm selection
        if (!Object.keys(algorithmConfigs).includes(config.algorithm)) {
            throw new Error(`Invalid algorithm '${config.algorithm}'. Must be one of: ${Object.keys(algorithmConfigs).join(', ')}`);
        }

        if (!aiClient) {
            throw new Error('Azure OpenAI client is not configured. Please check your environment variables.');
        }
        if (!dbClient) {
            throw new Error('Database client is not configured. Please check your environment variables.');
        }

        const algorithmConfig = algorithmConfigs[config.algorithm];
        const collectionName = algorithmConfig.containerName;

        try {
            const database = dbClient.database(config.dbName);
            console.log(`Connected to database: ${config.dbName}`);

            const container = database.container(collectionName);
            console.log(`Connected to container: ${collectionName}`);
            console.log(`\n📊 Vector Search Algorithm: ${algorithmConfig.algorithmName}`);
            console.log(`📏 Distance Function: ${config.distanceFunction}`);

            // Verify container exists by attempting a read
            await container.read();

            const data = await readFileReturnJson(path.join(__dirname, "..", config.dataFile));
            await insertData(container, data);

            const createEmbeddedForQueryResponse = await aiClient.embeddings.create({
                model: config.deployment,
                input: [config.query]
            });

            const safeEmbeddedField = validateFieldName(config.embeddedField);
            const queryText = `SELECT TOP 5 c.HotelName, c.Description, c.Rating, VectorDistance(c.${safeEmbeddedField}, @embedding) AS SimilarityScore FROM c ORDER BY VectorDistance(c.${safeEmbeddedField}, @embedding)`;

            console.log('\n--- Executing Vector Search Query ---');
            console.log('Query:', queryText);
            console.log('Parameters: @embedding (vector with', createEmbeddedForQueryResponse.data[0].embedding.length, 'dimensions)');
            console.log('--------------------------------------\n');

            const queryResponse = await container.items
                .query({
                    query: queryText,
                    parameters: [
                        { name: "@embedding", value: createEmbeddedForQueryResponse.data[0].embedding }
                    ]
                })
                .fetchAll();

            const activityId = getQueryActivityId(queryResponse);
            if (activityId) {
                console.log('Query activity ID:', activityId);
            }

            const { resources, requestCharge } = queryResponse;

            printSearchResults(resources, requestCharge);
        } catch (error) {
            if ((error as any).code === 404) {
                throw new Error(`Container or database not found. Ensure database '${config.dbName}' and container '${collectionName}' exist before running this script.`);
            }
            throw error;
        }
    } catch (error) {
        console.error('App failed:', error);
        process.exitCode = 1;
    }
}

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

Bu kod:

  • Ortam değişkenlerinden bir DiskANN veya quantizedFlat vektör algoritması yapılandırılır.
  • Azure OpenAI ve Azure Cosmos DB'ye parolasız kimlik doğrulaması kullanarak bağlanır.
  • JSON dosyasından önceden vektörleştirilmiş otel verilerini yükler.
  • Verileri uygun kapsayıcıya ekler.
  • Doğal dil sorgusu (quintessential lodging near running trails, eateries, retail) için ekleme oluşturur.
  • Benzerlik puanına göre sıralanan en benzer 5 oteli almak için bir VectorDistance SQL sorgusu yürütür.
  • Eksik istemciler, geçersiz algoritma seçimi ve mevcut olmayan kapsayıcılar/veritabanları için hataları işler.

Kodu anlama: Azure OpenAI ile eklemeler oluşturma

Kod, sorgu metni için eklemeler oluşturur:

const createEmbeddedForQueryResponse = await aiClient.embeddings.create({
    model, // OpenAI embedding model, e.g. "text-embedding-3-small"
    input  // Array of description strings to embed, e.g. ["quintessential lodging near running trails"]
});

client.embeddings.create için bu OpenAI API çağrısı, "koşu patikalarının yakınında çok uygun bir konaklama" gibi metni anlamsal anlamını yakalayan 1536 boyutlu bir vektöre dönüştürür. Ekleme oluşturma hakkında daha fazla ayrıntı için bkz. Azure OpenAI ekleme belgeleri.

Kodu anlama: Azure Cosmos DB'de vektörleri depolama

Vektör dizilerine sahip tüm belgeler, executeBulkOperations işlevi kullanılarak ölçekli olarak eklenir.

const response = await container.items.executeBulkOperations(operations);

Bu, önceden oluşturulmuş DescriptionVector dizilerini içeren otel belgelerini kapsayıcıya ekler. Tüm belge verilerini güvenli bir şekilde aktarabilirsiniz, ve bağlamda istemci kütüphanesi toplu işleme ve yeniden denemeleri sizin için işleyebilir.

Kod, VectorDistance işlevi kullanılarak bir vektör araması gerçekleştirir.

const queryText = `SELECT TOP 5 c.HotelName, c.Description, c.Rating, VectorDistance(c.${safeEmbeddedField}, @embedding) AS SimilarityScore FROM c ORDER BY VectorDistance(c.${safeEmbeddedField}, @embedding)`;

const queryResponse = await container.items
    .query({
        query: queryText,
        parameters: [
            { name: "@embedding", value: createEmbeddedForQueryResponse.data[0].embedding }
        ]
    })
    .fetchAll();

Bu kod, sorgunun ekleme vektörünü (@embedding) her belgenin depolanan vektör alanıylaDescriptionVector ( ) karşılaştırmak için VectorDistance işlevini kullanan, adı ve benzerlik puanı en az benzer olan en iyi 5 oteli sıralanmış olarak döndüren parametreli bir SQL sorgusu oluşturur. Sorgu yerleştirmesi, enjeksiyondan kaçınmak için bir parametre olarak geçirilir ve önceden Azure OpenAI'nin embeddings.create çağrısının bir sonucu olarak elde edilir.

Bu sorgu ne döndürür:

  • Vektör uzaklığı temelinde en benzer 5 otel
  • Otel özellikleri: HotelName, Description, Rating
  • SimilarityScore: Her otelin sorgunuza ne kadar benzer olduğunu gösteren sayısal değer
  • En benzerden en az benzere sıralanmış sonuçlar

İşlev hakkında VectorDistance daha fazla bilgi için VectorDistance belgelerine bakın.

Yardımcı program işlevleri oluşturma

Aşağıdaki kodu içine utils.tsyapıştırın:

import { CosmosClient, BulkOperationType } from '@azure/cosmos';
import { AzureOpenAI } from "openai";
import { promises as fs } from "fs";
import { DefaultAzureCredential, getBearerTokenProvider } from "@azure/identity";
// Define a type for JSON data
export type JsonData = Record<string, any>;

export function getClients(): { aiClient: AzureOpenAI | null; dbClient: CosmosClient | null } {

    let aiClient: AzureOpenAI | null = null;
    let dbClient: CosmosClient | null = null;

    const apiKey = process.env.AZURE_OPENAI_EMBEDDING_KEY!;
    const apiVersion = process.env.AZURE_OPENAI_EMBEDDING_API_VERSION!;
    const endpoint = process.env.AZURE_OPENAI_EMBEDDING_ENDPOINT!;
    const deployment = process.env.AZURE_OPENAI_EMBEDDING_MODEL!;

    if (apiKey && apiVersion && endpoint && deployment) {

        aiClient = new AzureOpenAI({
            apiKey,
            apiVersion,
            endpoint,
            deployment
        });
    }

    // Cosmos DB connection string or endpoint/key
    // You may need to use endpoint and key separately for CosmosClient
    const cosmosEndpoint = process.env.AZURE_COSMOSDB_ENDPOINT!;
    const cosmosKey = process.env.AZURE_COSMOSDB_KEY!;

    if (cosmosEndpoint && cosmosKey) {
        dbClient = new CosmosClient({ endpoint: cosmosEndpoint, key: cosmosKey });
    }

    return { aiClient, dbClient };
}

/**
 * Get Azure OpenAI and Cosmos DB clients using passwordless authentication (managed identity)
 * This function uses DefaultAzureCredential for authentication instead of API keys
 * 
 * @returns Object containing AzureOpenAI and CosmosClient instances or null if configuration is missing
 */
export function getClientsPasswordless(): { aiClient: AzureOpenAI | null; dbClient: CosmosClient | null } {
    let aiClient: AzureOpenAI | null = null;
    let dbClient: CosmosClient | null = null;

    // For Azure OpenAI with DefaultAzureCredential
    const apiVersion = process.env.AZURE_OPENAI_EMBEDDING_API_VERSION!;
    const endpoint = process.env.AZURE_OPENAI_EMBEDDING_ENDPOINT!;
    const deployment = process.env.AZURE_OPENAI_EMBEDDING_MODEL!;

    if (apiVersion && endpoint && deployment) {
        const credential = new DefaultAzureCredential();
        const scope = "https://cognitiveservices.azure.com/.default";
        const azureADTokenProvider = getBearerTokenProvider(credential, scope);
        aiClient = new AzureOpenAI({
            apiVersion,
            endpoint,
            deployment,
            azureADTokenProvider
        });
    }

    // For Cosmos DB with DefaultAzureCredential
    const cosmosEndpoint = process.env.AZURE_COSMOSDB_ENDPOINT!;

    if (cosmosEndpoint) {
        const credential = new DefaultAzureCredential();

        dbClient = new CosmosClient({
            endpoint: cosmosEndpoint,
            aadCredentials: credential // Use DefaultAzureCredential instead of key
        });
    }

    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 writeFileJson(filePath: string, jsonData: JsonData): Promise<void> {
    const jsonString = JSON.stringify(jsonData, null, 2);
    await fs.writeFile(filePath, jsonString, "utf-8");

    console.log(`Wrote JSON file to ${filePath}`);
}

/**
 * Check if a container has any documents
 * @param container - Cosmos DB container reference
 * @returns Number of documents in the container
 */
async function getDocumentCount(container: any): Promise<number> {
    const countResult = await container.items
        .query('SELECT VALUE COUNT(1) FROM c')
        .fetchAll();

    return countResult.resources[0];
}

export async function insertData(container, data): Promise<{ total: number; inserted: number; failed: number; skipped: number; requestCharge: number }> {
    // Check if container already has documents
    const existingCount = await getDocumentCount(container);

    if (existingCount > 0) {
        console.log(`Container already has ${existingCount} documents. Skipping insert.`);
        return { total: 0, inserted: 0, failed: 0, skipped: existingCount, requestCharge: 0 };
    }

    // Cosmos DB uses containers instead of collections
    // Use SDK bulk operations; let SDK handle batching, dispatch, and throttling
    console.log(`Inserting ${data.length} items using executeBulkOperations...`);

    // Prepare bulk operations for all items
    const operations = data.map((item: any) => ({
        operationType: BulkOperationType.Create,
        resourceBody: {
            id: item.HotelId,  // Map HotelId to id (required by Cosmos DB)
            ...item,
        },
        // Partition key must be passed as array: [value] for /HotelId partition
        partitionKey: [item.HotelId],
    }));

    let inserted = 0;
    let failed = 0;
    let skipped = 0;
    let totalRequestCharge = 0;

    try {
        const startTime = Date.now();
        console.log(`Starting bulk insert (${operations.length} items)...`);

        const response = await container.items.executeBulkOperations(operations);

        const endTime = Date.now();
        const duration = ((endTime - startTime) / 1000).toFixed(2);
        console.log(`Bulk insert completed in ${duration}s`);

        totalRequestCharge += getBulkOperationRUs(response);

        // Count inserted, skipped, and failed
        if (response) {
            response.forEach((result: any) => {
                if (result.statusCode >= 200 && result.statusCode < 300) {
                    inserted++;
                } else if (result.statusCode === 409) {
                    skipped++;
                } else {
                    failed++;
                }
            });
        }
    } catch (error) {
        console.error(`Bulk insert failed:`, error);
        failed = operations.length;
    }

    console.log(`\nInsert Request Charge: ${totalRequestCharge.toFixed(2)} RUs\n`);
    return { total: data.length, inserted, failed, skipped, requestCharge: totalRequestCharge };
}

/**
 * Validates a field name to ensure it's a safe identifier for use in queries.
 * This prevents NoSQL injection when using string interpolation in query construction.
 * 
 * @param fieldName - The field name to validate
 * @returns The validated field name
 * @throws Error if the field name contains invalid characters
 * 
 * @example
 * ```typescript
 * const safeField = validateFieldName(config.embeddedField);
 * const query = `SELECT * FROM c WHERE c.${safeField} = @value`;
 * ```
 */
export function validateFieldName(fieldName: string): string {
    // Allow only alphanumeric characters and underscores, must start with letter or underscore
    const validIdentifierPattern = /^[A-Za-z_][A-Za-z0-9_]*$/;

    if (!validIdentifierPattern.test(fieldName)) {
        throw new Error(
            `Invalid field name: "${fieldName}". ` +
            `Field names must start with a letter or underscore and contain only letters, numbers, and underscores.`
        );
    }

    return fieldName;
}

/**
 * Print search results in a consistent format
 */
export function printSearchResults(searchResults: any[], requestCharge?: number): void {
    console.log('\n--- Search Results ---');
    if (!searchResults || searchResults.length === 0) {
        console.log('No results found.');
        return;
    }

    searchResults.forEach((result, index) => {
        console.log(`${index + 1}. ${result.HotelName}, Score: ${result.SimilarityScore.toFixed(4)}`);
    });

    if (requestCharge !== undefined) {
        console.log(`\nVector Search Request Charge: ${requestCharge.toFixed(2)} RUs`);
    }
    console.log('');
}

export function getQueryActivityId(queryResponse: any): string | undefined {
    if (!queryResponse) {
        return undefined;
    }

    const diagnostics = queryResponse.diagnostics as any;
    const gatewayStats = Array.isArray(diagnostics?.clientSideRequestStatistics?.gatewayStatistics)
        ? diagnostics.clientSideRequestStatistics.gatewayStatistics
        : [];
    const gatewayActivityId = gatewayStats.find((entry: any) => entry?.activityId)?.activityId;

    return queryResponse.activityId ?? gatewayActivityId;
}

export function getBulkOperationRUs(response: any): number {
    // Response shape can vary depending on SDK/version:
    // - An array of operation results
    // - An object with `resources` or `results` array
    // - A single operation result object
    if (!response) {
        console.warn('Empty response. Cannot calculate RUs from bulk operation.');
        return 0;
    }

    // Normalize into an array of result items
    let items: any[] = [];
    if (Array.isArray(response)) {
        items = response;
    } else if (Array.isArray(response.resources)) {
        items = response.resources;
    } else if (Array.isArray(response.results)) {
        items = response.results;
    } else if (Array.isArray(response.result)) {
        items = response.result;
    } else if (typeof response === 'object') {
        // If it's a single operation result, wrap it so downstream logic is uniform
        items = [response];
    } else {
        console.warn('Response does not contain bulk operation results.');
        return 0;
    }

    let totalRequestCharge = 0;

    items.forEach((result: any) => {
        let requestCharge = 0;

        // 1) Direct numeric property
        if (typeof result.requestCharge === 'number') {
            requestCharge = result.requestCharge;
        }

        // 1b) Some SDKs nest the operation response under `response` and expose requestCharge there
        if (!requestCharge && result.response && typeof result.response.requestCharge === 'number') {
            requestCharge = result.response.requestCharge;
        }

        if (!requestCharge && result.response && typeof result.response.requestCharge === 'string') {
            const parsed = parseFloat(result.response.requestCharge);
            requestCharge = isNaN(parsed) ? 0 : parsed;
        }

        // 2) String numeric value
        if (!requestCharge && typeof result.requestCharge === 'string') {
            const parsed = parseFloat(result.requestCharge);
            requestCharge = isNaN(parsed) ? 0 : parsed;
        }

        // 3) operationResponse may contain headers in different shapes
        if (!requestCharge && result.operationResponse) {
            const op = result.operationResponse;
            const headerVal = op.headers?.['x-ms-request-charge']
                ?? (typeof op.headers?.get === 'function' ? op.headers.get('x-ms-request-charge') : undefined)
                ?? op._response?.headers?.['x-ms-request-charge'];

            if (headerVal !== undefined) {
                const parsed = parseFloat(headerVal as any);
                requestCharge = isNaN(parsed) ? 0 : parsed;
            }
        }

        // 4) Some responses include headers at top-level or in `headers`
        if (!requestCharge && result.headers) {
            const hv = result.headers['x-ms-request-charge'] ?? (typeof result.headers.get === 'function' ? result.headers.get('x-ms-request-charge') : undefined);
            if (hv !== undefined) {
                const parsed = parseFloat(hv as any);
                requestCharge = isNaN(parsed) ? 0 : parsed;
            }
        }

        // 5) Fallback: some SDKs expose RU on resourceOperation or nested fields
        if (!requestCharge) {
            // Try several nested locations where headers may be present
            const candidateHeaders =
                result.operationResponse?._response?.headers
                ?? result.operationResponse?.headers
                ?? result._response?.headers
                ?? result.headers;

            const fallback = candidateHeaders ? (candidateHeaders['x-ms-request-charge'] ?? (typeof candidateHeaders.get === 'function' ? candidateHeaders.get('x-ms-request-charge') : undefined)) : undefined;

            if (fallback !== undefined) {
                const parsed = parseFloat(fallback as any);
                requestCharge = isNaN(parsed) ? 0 : parsed;
            }
        }

        totalRequestCharge += requestCharge;
    });

    // If we didn't find any RUs, print a small sample to help debugging
    if (totalRequestCharge === 0) {
        try {
            const sample = items[0];
            const sampleKeys = sample ? Object.keys(sample) : [];
            console.warn('getBulkOperationRUs: no RUs found. Sample result keys:', sampleKeys);
            if (sample && sample.response) {
                try {
                    const respKeys = Object.keys(sample.response);
                    console.warn('  sample.response keys:', respKeys);
                    const hdrs = sample.response.headers ?? sample.response._response?.headers ?? sample.response?.operationResponse?.headers;
                    console.warn('  sample.response headers sample:', hdrs ? Object.keys(hdrs) : hdrs);
                } catch (e) {
                    console.warn('  Could not inspect sample.response for headers:', e);
                }
            }
        } catch (e) {
            console.warn('Could not inspect sample result for debugging:', e);
        }
    }

    return totalRequestCharge;
}

Bu yardımcı program modülü şu temel işlevleri sağlar:

  • getClientsPasswordless: Parolasız kimlik doğrulaması kullanarak Azure OpenAI ve Azure Cosmos DB için istemciler oluşturur ve döndürür. Her iki kaynakta da RBAC'yi etkinleştirin ve Azure CLI'da oturum açın
  • insertData: Azure Cosmos DB kapsayıcısına toplu olarak veri ekler ve belirtilen alanlarda standart dizinler oluşturur
  • printSearchResults: Puan ve otel adı dahil olmak üzere bir vektör aramasının sonuçlarını yazdırır
  • validateFieldName: Verilerde bir alan adının mevcut olduğunu doğrular
  • getBulkOperationRUs: Belge sayısına ve vektör boyutlarına göre toplu işlemler için İstek Birimlerini (RU) tahmin eder

Azure CLI ile kimlik doğrulaması

Uygulamanın Azure kaynaklarına güvenli bir şekilde erişebilmesi için uygulamayı çalıştırmadan önce Azure CLI'da oturum açın.

az login

Kod, Azure Cosmos DB'ye ve Azure OpenAI'ye getClientsPasswordless'den utils.ts işlevini kullanarak erişmek için yerel geliştirici kimlik doğrulamanızı kullanır. AZURE_TOKEN_CREDENTIALS=AzureCliCredential ayarladığınızda, kimlik bilgisi zincirinden DefaultAzureCredential'in hangi kimlik bilgisini kullanacağını deterministik olarak belirlersiniz. İşlev, @azure/identity içindeki DefaultAzureCredential'a dayanır; bu, sıralı bir kimlik bilgisi sağlayıcısı zincirinden geçer, ancak önce Azure CLI kimlik bilgilerini çözümlemek için ortam değişkenine öncelik verir. Azure Kimlik kitaplığını kullanarak Azure hizmetlerinde JavaScript uygulamalarının kimliğini doğrulama hakkında daha fazla bilgi edinin.

Uygulamayı derleme ve çalıştırma

TypeScript dosyalarını derleyin ve uygulamayı çalıştırın:

npm run build
npm run start:diskann

Uygulamanın günlük ve çıktıları şunları gösterir:

  • Veri ekleme durumu
  • Vektör dizini oluşturma
  • Otel adları ve benzerlik puanları ile arama sonuçları
Connected to database: Hotels
Connected to container: hotels_diskann

📊 Vector Search Algorithm: DiskANN
📏 Distance Function: cosine
Reading JSON file from C:\azure-samples\cosmos-db-vector-samples\data\HotelsData_toCosmosDB_Vector.json
Inserting 50 items using executeBulkOperations...
Starting bulk insert (50 items)...
Bulk insert completed in 3.19s

Insert Request Charge: 6805.25 RUs


--- Executing Vector Search Query ---
Query: SELECT TOP 5 c.HotelName, c.Description, c.Rating, VectorDistance(c.DescriptionVector, @embedding) AS SimilarityScore FROM c ORDER BY VectorDistance(c.DescriptionVector, @embedding)
Parameters: @embedding (vector with 1536 dimensions)
--------------------------------------

Query activity ID: <ACTIVITY_ID>

--- Search Results ---
1. Royal Cottage Resort, Score: 0.4991
2. Country Comfort Inn, Score: 0.4786
3. Nordick's Valley Motel, Score: 0.4635
4. Economy Universe Motel, Score: 0.4461
5. Roach Motel, Score: 0.4388


Vector Search Request Charge: 5.32 RUs

Mesafe ölçümleri

Azure Cosmos DB, vektör benzerliği için üç uzaklık işlevini destekler:

Distance Fonksiyonu Puan Aralığı Yorumlama En Uygun
Kosünüs (varsayılan) 0,0 - 1,0 Daha yüksek puanlar (1,0'a yakın) daha fazla benzerlik gösterir Genel metin benzerliği, Azure OpenAI eklemeleri (bu hızlı başlangıçta kullanılır)
Öklid (L2) 0,0 ile ∞ Düşük = daha benzer Uzamsal veriler, büyüklük önemli olduğunda
NoktaLı Ürün -∞'den +∞'ye Daha yüksek = daha benzer Vektör büyüklükleri normalleştirildiğinde

Uzaklık işlevi, kapsayıcı oluşturulurken vektör ekleme ilkesinde ayarlanır. Bu, örnek depodaki altyapıda sağlanır. Kapsayıcı tanımının bir parçası olarak tanımlanır.

{
    name: 'hotels_diskann'
    partitionKeyPaths: [
        '/HotelId'
    ]
    indexingPolicy: {
        indexingMode: 'consistent'
        automatic: true
        includedPaths: [
        {
            path: '/*'
        }
        ]
        excludedPaths: [
        {
            path: '/_etag/?'
        }
        {
            path: '/DescriptionVector/*'
        }
        ]
        vectorIndexes: [
        {
            path: '/DescriptionVector'
            type: 'diskANN'
        }
        ]
    }
    vectorEmbeddingPolicy: {
        vectorEmbeddings: [
        {
            path: '/DescriptionVector'
            dataType: 'float32'
            dimensions: 1536
            distanceFunction: 'cosine'
        }
        ]
    }
}

Bu Bicep kodu, otel belgelerini vektör arama özellikleriyle depolamak için Azure Cosmos DB kapsayıcı yapılandırmasını tanımlar.

Mülkiyet Description
partitionKeyPaths Belgeleri HotelId kullanarak dağıtılmış depolama için böler.
indexingPolicy Yazma performansını iyileştirmek için sistem alanı /* ve _etag dizisi dışında tüm belge özelliklerinde DescriptionVector otomatik dizin oluşturma yapılandırır. Bunun yerine özel vectorIndexes bir yapılandırma kullandıklarından vektör alanlarının standart dizin oluşturması gerekmez.
vectorIndexes Verimli benzerlik aramaları için '/DescriptionVector' yolunda bir DiskANN veya quantizedFlat dizini oluşturur.
vectorEmbeddingPolicy Vektör alanının özelliklerini tanımlar: 1536 boyutlu float32 veri türü (model çıkışıyla text-embedding-3-small eşleşen) ve sorgular sırasında vektörler arasındaki benzerliği ölçmek için mesafe işlevi olarak kosinüs kullanılır.

Benzerlik puanlarını yorumlama

Kosinüs benzerliği kullanan örnek çıktıda:

  • 0.4991 (Royal Cottage Resort) - En yüksek benzerlik, "koşu parkurlarına yakın konaklama, yemek mekanları, perakende" için en iyi eşleşme
  • 0.4388 (Roach Motel) - Daha düşük benzerlik, hala ilgili ama daha az eşleştirme
  • 1,0'a yakın puanlar daha güçlü semantik benzerliği gösterir
  • 0'a yakın puanlar çok az benzerlik gösteriyor

Önemli notlar:

  • Mutlak puan değerleri ekleme modelinize ve verilerinize bağlıdır
  • Mutlak eşikler yerine göreli derecelendirmeye odaklanma
  • Azure OpenAI eklemeleri kosinüs benzerliğiyle en iyi sonucu sağlar

Uzaklık işlevleri hakkında ayrıntılı bilgi için bkz. Uzaklık işlevleri nelerdir?

Visual Studio Code'da verileri görüntüleme ve yönetme

  1. Azure Cosmos DB hesabınıza bağlanmak için Visual Studio Code'da Cosmos DB uzantısını seçin.

  2. Hotels veritabanındaki verileri ve dizinleri görüntüleyin.

    Oteller veritabanı öğeleri ve bir JSON belge düzenleyicisi ile Azure Cosmos DB uzantısını gösteren Visual Studio Code'un ekran görüntüsü.

Kaynakları temizle

NoSQL hesabı için API'ye artık ihtiyacınız kalmadığında ilgili kaynak grubunu silebilirsiniz.

  1. Azure portalında daha önce oluşturduğunuz kaynak grubuna gidin.

    Tip

    Bu hızlı başlangıç kılavuzunda ad olarak msdocs-cosmos-quickstart-rg önermiştik.

  2. Kaynak grubunu sil seçeneğini seçin.

    Bir kaynak grubunun gezinti çubuğundaki Kaynak grubunu sil seçeneğinin ekran görüntüsü.

  3. Silmek istediğinizden emin misiniz iletişim kutusunda kaynak grubunun adını girin ve Sil'i seçin.

    Kaynak grubunun silme onay sayfasının ekran görüntüsü.