Implement semantic and hybrid search with Azure Cosmos DB for NoSQL
Back to the AI-200 path
AI-200Chapter 10

Microsoft AI-200 Certification Study

Implement semantic and hybrid search with Azure Cosmos DB for NoSQL

Store embeddings beside operational data, choose vector policies and DiskANN indexes, rank semantic and hybrid results, and refresh vectors reactively with the change feed.

Suggested study time: 105 minutes • Intermediate • Complete original rewrite with a concise version of every topic, assessment review, and guided semantic-search lab

Neon Microsoft Certified AI-200 shield with vector embeddings, semantic search, Azure Cosmos DB, and event-driven refresh symbols

1. Turn operational documents into semantic-searchable knowledge

Keyword search misses relevant material whenever users and authors choose different words. A support question about a dropped wireless connection can still match a WiFi troubleshooting article when both texts are represented as nearby vectors. for NoSQL stores those embeddings with the source document and its metadata, so an AI application can query one operational store instead of synchronizing a separate vector database.

  • Persist embeddings beside the content and filterable metadata.
  • Configure vector policies and indexes that match the embedding model.
  • Run similarity, filtered, hybrid, and multi-vector queries.
  • Refresh embeddings automatically when source content changes.

Topic summary

Semantic search compares meaning in vector space; colocating vectors, documents, and metadata simplifies the AI data path.

2. Design documents that keep content, metadata, and embeddings together

An embedding is an array of numbers produced by a machine-learning model from text, images, or other content. Similar inputs occupy nearby positions in a high-dimensional space. The model fixes the vector dimensions: text-embedding-ada-002 emits 1,536 values, while text-embedding-3-large can emit 3,072. More dimensions may preserve nuance but increase storage and index work.

A useful document carries an id, searchable source text, category, product, creation date, access attributes, and the embedding. Metadata supports exact filters while the vector supports semantic ranking. Title and body can share one embedding, or separate paths can support distinct ranking strategies.

Source content and metadata are converted into an embedding and stored together in an Azure Cosmos DB document.
The embedding model produces the vector; keeps it next to the data it represents.

Topic summary

Store the original content, filterable metadata, and model-compatible embedding in the same item.

3. Configure the vector policy and distance function

A vector policy maps every embedding path to its element type, dimensions, and distance function. The values must agree with the model output. Cosine compares direction and is the usual choice for normalized text embeddings; dot product considers direction and magnitude and is equivalent to cosine for normalized vectors; Euclidean measures straight-line distance, where smaller values mean closer vectors.

vector_policy = {
    "vectorEmbeddings": [{
        "path": "/embedding",
        "dataType": "float32",
        "distanceFunction": "cosine",
        "dimensions": 1536
    }]
}

indexing_policy = {
    "indexingMode": "consistent",
    "automatic": True,
    "includedPaths": [{"path": "/*"}],
    "excludedPaths": [{"path": "/_etag/?"}, {"path": "/embedding/*"}],
    "vectorIndexes": [{"path": "/embedding", "type": "diskANN"}]
}

Enable NoSQL vector search on the account before using it. Treat the vector layout as an early container-design decision: current service guidance allows paths to be added or removed, but an existing policy or index configuration is not edited directly—drop and recreate that configuration when its settings must change.

Topic summary

The embedding path, type, dimensions, and distance function must describe the vectors exactly.

4. Choose vector data types and indexes deliberately

float32 favors precision and straightforward integration. float16 cuts vector storage roughly in half with a usually small quality effect. int8 and uint8 require quantized embeddings and can reduce storage further, but accuracy must be measured with representative queries.

Vector index choices.
IndexLimits and behaviorTypical fit
flatExact brute-force search; up to 505 dimensions.Small collections or validation that needs full recall.
quantizedFlatCompressed exact scan; up to 4,096 dimensions.Roughly up to 50,000 vectors in the search scope.
diskANNFast approximate DiskANN index; up to 4,096 dimensions.Large collections where latency and RU efficiency matter.

quantizedFlat and diskANN need at least 1,000 vectors before their index becomes effective; smaller collections fall back to a full scan. Exclude embedding arrays from normal range indexing because it adds write and storage cost without helping vector queries.

Decision path from a query embedding through metadata filters, a vector index, similarity ranking, and hybrid full-text ranking.
Index and query choices balance recall, latency, result count, and RU consumption.

Topic summary

Select precision and index type from vector dimensions, collection size, latency, recall, and RU targets—not from a universal default.

5. Create the container and write synchronized embeddings

Create the container with both vector and indexing policies, and choose a partition key that matches common filters. A support library partitioned by /category can route category-scoped searches to one partition. Generate the embedding from the exact content that will be searched, then upsert the document and vector together.

from azure.cosmos import CosmosClient, PartitionKey

container = database.create_container(
    id="support-knowledge",
    partition_key=PartitionKey(path="/category"),
    indexing_policy=indexing_policy,
    vector_embedding_policy=vector_policy
)
from openai import AzureOpenAI

text = f"{title}
{content}"
response = openai_client.embeddings.create(
    input=text,
    model="text-embedding-ada-002"
)

container.upsert_item({
    "id": document_id,
    "title": title,
    "content": content,
    "category": category,
    "productId": product_id,
    "createdDate": created_date,
    "embedding": response.data[0].embedding
})

Use the same embedding deployment for documents and queries. When searchable content changes, regenerate its vector before upserting so stored semantics never drift from the visible text.

Topic summary

Create the vector-aware container first, then persist source text, metadata, and its freshly generated embedding atomically.

6. Execute parameterized VectorDistance queries

VectorDistance compares the stored path with a query vector and uses the distance function defined by the policy. Generate the query vector with the same model as the documents; vectors from different models inhabit incompatible spaces. Return the score in SELECT and use the same expression in ORDER BY to rank results.

query = """
SELECT TOP 10
    c.id,
    c.title,
    VectorDistance(c.embedding, @queryVector) AS similarity
FROM c
ORDER BY VectorDistance(c.embedding, @queryVector)
"""

results = container.query_items(
    query=query,
    parameters=[{"name": "@queryVector", "value": query_embedding}],
    enable_cross_partition_query=True
)

Pass the vector as @queryVector instead of embedding thousands of numbers in the query string. Parameters keep the statement readable, support plan reuse, and avoid fragile string construction. Always use TOP N; an unlimited semantic query returns unnecessary documents and increases latency and RU consumption.

Topic summary

Embed the query with the document model, parameterize the vector, rank with VectorDistance, and cap the result set.

7. Interpret scores and balance indexed versus exact search

With cosine similarity, larger values indicate closer meaning. +1 is identical direction, 0.7–0.9 is often strongly related, 0.5–0.7 moderately related, and low or negative values weakly related. These bands are starting points, not service guarantees; tune thresholds against labeled examples to balance precision and recall.

  • RAG commonly supplies 5–10 passages to control token cost and distraction.
  • Interactive search often returns 10–20 items and paginates.
  • Recommendations commonly show 3–5 related items.
  • Indexed search is approximate and fast; setting the third VectorDistance argument to true forces an exact full scan.

Reserve brute force for evaluation, small datasets, or cases that truly require complete recall. For routine production queries, choose DiskANN or quantizedFlat, project only needed properties, target a partition, inspect latency, and track RU charges.

Topic summary

Quality comes from measured thresholds and recall; efficiency comes from indexed search, small TOP values, narrow projections, and partition targeting.

8. Combine vector ranking with metadata and partition filters

Real applications constrain meaning by category, date, product, version, status, or access group. Put those predicates in WHERE so the optimizer can reduce the candidate set before or during vector comparison. Selective pre-filtering lowers work; post-filtering preserves a global semantic ranking but can return fewer rows than requested.

query = """
SELECT TOP 10 c.id, c.title,
    VectorDistance(c.embedding, @queryVector) AS similarity
FROM c
WHERE c.category = @category
  AND c.createdDate >= @startDate
ORDER BY VectorDistance(c.embedding, @queryVector)
"""

results = container.query_items(
    query=query,
    parameters=[
        {"name": "@queryVector", "value": query_embedding},
        {"name": "@category", "value": "networking"},
        {"name": "@startDate", "value": "2026-01-01T00:00:00Z"}
    ],
    partition_key="networking"
)
  • Use documentType for FAQs, guides, or release notes.
  • Use half-open date ranges for time windows.
  • Use productId or version for product scope.
  • Use ARRAY_CONTAINS for authorized access groups.
  • Exclude draft, archived, or deprecated records by status.

When category is the partition key, provide both the WHERE predicate and partition_key argument. That explicitly routes the request to one partition and avoids cross-partition fan-out.

Topic summary

Filter inside the vector query, index the filter fields, and pass the partition key when the filter identifies one partition.

Semantic ranking handles paraphrases, while full-text scoring is stronger for exact error codes, product names, and technical tokens. Hybrid search configures both vector and full-text policies and indexes, then uses Reciprocal Rank Fusion to merge their rankings.

SELECT TOP 10 *
FROM c
ORDER BY RANK RRF(
    VectorDistance(c.embedding, @queryVector),
    FullTextScore(c.content, @term1, @term2),
    [2, 1]
)

The [2, 1] weights make vector ranking twice as influential as full text. Increase the semantic weight for natural-language descriptions, increase the text weight for exact identifiers, or start equally and tune with representative queries. Hybrid evaluation costs more RUs than a single scorer, so use it where combined relevance creates measurable value.

Topic summary

ORDER BY RANK RRF combines VectorDistance and FullTextScore; weights express which signal matters more.

A document can store separate title and content embeddings, or vectors from different modalities. Multiple VectorDistance calls can be fused with RRF so items that rank well in either space remain eligible and items strong in both rise to the top. Each vector path needs its own policy entry with the correct dimensions and metric.

Complex filters, full-text scoring, and multiple vector comparisons add RU cost. Test with production-like vector counts and realistic filter selectivity; a development collection of thousands of items does not predict the behavior of millions. Inspect x-ms-request-charge and query metrics, and verify every WHERE property has a suitable index.

Topic summary

Multi-vector ranking broadens relevance, but every added scorer must justify its latency and RU cost under representative load.

11. Use the change feed as an embedding-refresh signal

The change feed is enabled by default and records item changes in order within each partition-key range. A consumer can resume after downtime, but failure recovery can redeliver work, so processing must be idempotent. This event stream is a better source of truth for embedding refresh than periodically scanning every document.

Change feed consumption models.
ModelStrengthsChoose it for
PushAutomatic delivery, partition balancing, checkpoints, and simpler continuous operation. or a continuously running change feed processor.
PullExplicit scheduling, batching, and control with fewer supporting resources.Periodic refresh, migration, or custom batch orchestration.
Document updates flow through the Azure Cosmos DB change feed to an Azure Function that regenerates and stores the embedding.
A lease container checkpoints progress while content hashes prevent unnecessary regeneration.

Topic summary

Treat the change feed as a durable event source and make refresh handlers safe to run more than once.

12. Process changes with , leases, and selective refresh

An trigger hides partition ownership and checkpoint management. Its lease container stores which processor owns each range and how far processing advanced; this enables coordination, failover, and horizontal scale. A small 400-RU/s lease container is often enough unless the change volume is unusually high.

import azure.functions as func

app = func.FunctionApp()

@app.cosmos_db_trigger(
    arg_name="documents",
    container_name="support-knowledge",
    database_name="support-db",
    connection="CosmosDBConnection",
    lease_container_name="leases",
    create_lease_container_if_not_exists=True
)
def refresh_embeddings(documents: func.DocumentList):
    for document in documents:
        if needs_refresh(document):
            document["embedding"] = create_embedding(document)
            document["contentHash"] = content_hash(document)
            container.upsert_item(document)

Do not regenerate embeddings for metadata-only changes. Compare title, content, description, and summary, or store a SHA-256 hash of the exact input text. Update the hash with the embedding. Category, status, or permission changes can then bypass a costly embedding call when they do not alter semantic content.

Topic summary

The trigger and leases provide reliable orchestration; content-aware checks keep refresh cost proportional to meaningful changes.

13. Scale refresh safely and retain control with the pull model

For bursts, embed multiple texts in one request when supported, retry throttled calls with exponential backoff, and decouple change detection through Azure when embedding throughput must scale independently. Priority queues can refresh active troubleshooting content before archived material.

iterator = container.query_items_change_feed(start_time="Beginning")

for page in iterator.by_page():
    for change in page:
        process_idempotently(change)
    save_checkpoint(iterator.continuation_token)
  • Persist continuation tokens between batch runs.
  • Handle an item deleted before processing as a benign not-found case.
  • Use ETags when concurrent updates must not overwrite newer content.
  • Send repeatedly failing work to a dead-letter queue.
  • Monitor ChangeFeedProcessorHostLag and add processor instances when backlog grows.

Topic summary

, retry, checkpoint, and monitor the pipeline; isolate poison work and make every update idempotent.

14. Guided lab, assessment review, and Microsoft references

The approximately 30-minute lab deploys an for NoSQL account with vector search, creates vector policies and indexes, loads support tickets with precomputed embeddings, implements Python similarity functions, and tests the result in a Flask application. It requires an Azure subscription with deployment permissions, , the latest Azure CLI, and Python 3.12 or newer.

  1. Configure the starter project and deployment script.
  2. Provision the account and vector-enabled container.
  3. Build parameterized VectorDistance functions.
  4. Load sample tickets and verify semantic matches.
  5. Exercise the search flow through the Flask interface.

Assessment review

  • text-embedding-ada-002: float32, 1,536 dimensions, cosine.
  • Reduce TOP from 100 to the 10–20 results users actually need.
  • Filter category in WHERE and pass partition_key when category partitions the container.
  • Combine VectorDistance and FullTextScore with ORDER BY RANK RRF.
  • Use an trigger to refresh embeddings without polling.
  1. Vector search in for NoSQL
  2. Hybrid search in for NoSQL
  3. VectorDistance system function
  4. Work with the change feed
  5. Use the change feed with
  6. Azure OpenAI embeddings tutorial

Topic summary

The lab joins container policy, indexing, embedding generation, semantic queries, and application validation; the assessment reinforces the highest-value design decisions.