Skip to main content

Optimize Azure AI Search for enterprise RAG with hybrid search, chunking, document-level security, agentic retrieval, observability, and Terraform-based deployment.

Architectural Runbook: Optimizing Azure AI Search for Enterprise RAG Workflows

A basic RAG PoC takes one afternoon to build. A production RAG system that retrieves accurately at enterprise scale, enforces document-level security, handles complex multi-step queries, and stays observable under load takes considerably more. This runbook covers every layer — from hybrid search tuning and chunking strategy to agentic retrieval, security filters, and Terraform IaC — in the depth that production demands.

Hybrid
Search mode — the only production-appropriate retrieval mode. Pure vector search misses exact keyword matches. Pure BM25 misses semantic meaning. Hybrid captures both.
RRF
Reciprocal Rank Fusion — the algorithm Azure AI Search uses to merge BM25 and vector result sets into a single ranked list without requiring scores on the same scale
k=60
The RRF constant that controls the influence of rank position. The default value in Azure AI Search. Increasing it flattens the ranking curve; decreasing it amplifies top-rank differences.
3 layers
The Azure AI Search retrieval stack: keyword (BM25) + vector (HNSW) + semantic re-ranking (cross-encoder transformer). Each layer adds relevance precision that the previous cannot provide alone.

The Enterprise RAG Architecture: Components and Data Flow

Retrieval-Augmented Generation grounds a large language model in your organisation's proprietary data by retrieving relevant document chunks at query time and injecting them into the LLM's context window. The quality of the generated answer is entirely determined by the quality of the retrieved chunks. A better generation model cannot compensate for poor retrieval. This is the central insight that separates PoC RAG implementations from production ones: the effort belongs in the retrieval layer, not the generation layer.

Azure AI Search is Microsoft's managed retrieval engine for enterprise RAG. It provides three retrieval mechanisms in a single service: full-text keyword search using the BM25 algorithm, dense vector search using the Hierarchical Navigable Small World (HNSW) graph algorithm, and transformer-based semantic re-ranking that re-scores the top results using a cross-encoder model. The combination of all three — applied in sequence to every query — is what makes enterprise-grade retrieval achievable.

Figure 1 — Enterprise RAG architecture: ingestion pipeline and query pipeline with all Azure AI Search layers
INGESTION PIPELINE (offline / scheduled)Data SourcesBlob StorageSQL / SharePointIndexer + SkillsetChunk · EnrichGenerate embeddingsAzure OpenAItext-embedding-3-large3072-dim vectorsAzure AI SearchIndex stores:• Chunk text (BM25 inverted)• Vector (HNSW graph)• Metadata + security fieldsQUERY PIPELINE (real-time)User Query"Natural language"① Keyword (BM25)Full-text ranked listExact terms, acronyms② Vector (HNSW)Semantic ranked listMeaning + synonyms③ RRF MergeUnified ranked list④ Semantic RankerCross-encoder re-scoreTop-3 captions + answersSecurityFilter ODataTop-K ChunksCaptions + source docsAzure OpenAI (GPT-4.1)Generates answer from chunksGrounded responseAnswer to UserCited + groundedAzure AI Search componentData source / compute---Security filter applied to all queries
Every enterprise RAG query passes through four sequential layers: BM25 keyword search and HNSW vector search run in parallel, RRF merges their result sets into one ranked list, and the Semantic Ranker re-scores the top results using a cross-encoder model. Security filters apply to the entire pipeline before any result is returned.
The 8 Runbook Sections
Runbook 1Configure Hybrid Search (BM25 + HNSW + RRF)
FoundationStart Here

Why hybrid over pure vector: Vector search captures semantic meaning — it understands that "cardiac arrest" and "heart attack" refer to the same condition. But it fails at exact matching: a product serial number like "AX-2047-C" or an acronym like "PAYE" returns poor results because these tokens have no meaningful vector neighbourhood. BM25 keyword search excels at exact term matching but misses semantic relationships. Hybrid search runs both simultaneously and merges the results using Reciprocal Rank Fusion (RRF) — a rank combination algorithm that does not require scores from the two systems to be on the same scale.

The RRF formula: For each document d in the merged result, the RRF score is the sum of 1/(k + rank_i) across all result lists i where the document appears. The constant k (default 60) controls how much influence rank position has — higher k flattens differences between rank positions, lower k amplifies them. In practice, the default of 60 performs well across most enterprise document retrieval scenarios.

1

Define the index schema with both text and vector fields

The index must have a field marked searchable: true for BM25 keyword search and a type: Collection(Edm.Single) field configured for vector search. Both must point to the same source content (the chunk text) — one stores the raw text, the other stores its embedding.

2

Configure the HNSW vector algorithm profile

Set m (max connections per node, default 4) and efConstruction (build-time search width, default 400). Higher m and efConstruction improve recall at the cost of index build time and memory. For production: m=16, efConstruction=800 is a well-balanced starting point. The metric should be cosine for text embeddings from OpenAI models.

3

Issue hybrid queries with both search text and vector

Set queryType: "semantic", include both the search (text) parameter and vectorQueries parameter in the same request. Azure AI Search automatically applies RRF fusion when both are present.

4

Set vector_semantic_hybrid as your query mode

This is Microsoft's own recommended default from their internal benchmarking. It activates hybrid retrieval (keyword + vector) followed by semantic re-ranking in a single query call — the optimal configuration for enterprise RAG without any further tuning.

Python — Create hybrid search index and execute hybrid query# pip install azure-search-documents azure-identity
from azure.search.documents.indexes import SearchIndexClient
from azure.search.documents.indexes.models import (
    SearchIndex, SearchField, SearchFieldDataType,
    VectorSearch, HnswAlgorithmConfiguration, HnswParameters,
    VectorSearchProfile, SemanticConfiguration, SemanticSearch,
    SemanticPrioritizedFields, SemanticField
)
from azure.search.documents import SearchClient
from azure.search.documents.models import VectorizedQuery
from azure.identity import DefaultAzureCredential

credential = DefaultAzureCredential() # Managed Identity — no keys
endpoint = "https://YOUR-SEARCH.search.windows.net"
INDEX_NAME = "enterprise-rag-index"

# ── INDEX SCHEMA ─────────────────────────────────────────
fields = [
    SearchField(name="id", type=SearchFieldDataType.String, key=True),
    SearchField(name="chunk_text", type=SearchFieldDataType.String,
        searchable=True, analyzer_name="en.microsoft"), # BM25
    SearchField(name="embedding",
        type=SearchFieldDataType.Collection(SearchFieldDataType.Single),
        searchable=True, vector_search_dimensions=3072, # text-embedding-3-large
        vector_search_profile_name="hnsw-profile"),
    SearchField(name="source_url", type=SearchFieldDataType.String, retrievable=True),
    SearchField(name="department", type=SearchFieldDataType.String, filterable=True),
    SearchField(name="allowed_groups", # Security field — see Runbook 5
        type=SearchFieldDataType.Collection(SearchFieldDataType.String),
        filterable=True),
]

vector_search = VectorSearch(
    algorithms=[HnswAlgorithmConfiguration(name="hnsw-algo",
        parameters=HnswParameters(
            m=16, # Max connections per HNSW node
            ef_construction=800, # Build-time search width (recall vs speed)
            metric="cosine" # Use cosine for OpenAI embeddings
        ))],
    profiles=[VectorSearchProfile(name="hnsw-profile", algorithm_configuration_name="hnsw-algo")]
)

# ── HYBRID QUERY ─────────────────────────────────────────
search_client = SearchClient(endpoint, INDEX_NAME, credential)

def hybrid_search(query_text: str, query_embedding: list, top_k: int = 5,
                  security_filter: str = None):
    vector_query = VectorizedQuery(
        vector=query_embedding,
        k_nearest_neighbors=top_k * 3, # Over-retrieve for re-ranking
        fields="embedding"
    )
    results = search_client.search(
        search_text=query_text, # BM25 keyword search
        vector_queries=[vector_query], # HNSW vector search
        query_type="semantic", # Activates RRF + semantic re-ranking
        semantic_configuration_name="my-semantic-config",
        query_caption="extractive", # Extract captions from top results
        filter=security_filter, # OData security filter (Runbook 5)
        top=top_k,
        select="chunk_text,source_url"
    )
    return [r for r in results]
Figure 2 — Chunking strategy comparison: how chunk size and overlap affect retrieval precision and context quality
Three chunking approaches and their retrieval trade-offsFixed-Size Chunking512–1024 tokens per chunkChunk 1 (512 tokens)Chunk 2 (512 tokens)Chunk 3 (512 tokens)✓ Simple, fast to implement✗ Splits sentences mid-ideaOverlapping Chunks (Best Default)512 tokens + 128 overlapChunk 1: tokens 0–512Chunk 2: tokens 384–896 (128 overlap)Chunk 3: tokens 768–1280✓ Prevents boundary truncation✓ Context preserved across chunk edgesSemantic Boundary ChunkingSplit on headings / paragraphsH2: Quarterly Results [boundary]Chunk: full paragraph under headingNatural semantic unit preservedH2: Risk Factors [next boundary]✓ Best for structured documents✗ Variable chunk sizes↑ RECOMMENDED DEFAULT for most enterprise RAG use casesRecommended for legal / financial / medical docsChunk size guidelines: General docs: 512 tokens · Code: 1024 tokens · Legal/medical: semantic boundaries · FAQ: Q+A pair as one chunk
Overlapping chunks (512 tokens + 128 overlap) are the recommended default. The overlap prevents information from being truncated at chunk boundaries — a major source of retrieval failures where the answer spans two chunks and neither chunk alone scores high enough to be returned.
Runbook 2Chunking Strategy for High Retrieval Precision
CriticalIngestion Layer

Chunking is the single decision with the largest impact on retrieval quality. Chunks that are too small lack context for the semantic ranker to score accurately. Chunks that are too large dilute the relevance score with irrelevant content and consume excessive context window space. The correct chunk size is not a universal constant — it depends on your content type, your embedding model's token limit, and the specificity of queries your users ask.

1

Start with 512 tokens + 128 overlap as the default

This configuration works well for general enterprise documents: policy documents, HR guides, product documentation, internal wikis. The 128-token overlap ensures that information at chunk boundaries is captured in both adjacent chunks, preventing the "boundary problem" where an answer is split across two chunks and neither scores high enough independently.

2

Adjust for content type

Code repositories: increase to 1,024 tokens (functions and classes should not be split mid-definition). Legal contracts and medical records: use semantic boundary chunking based on sections and clauses — preserve complete regulatory provisions. FAQ databases: keep each Q+A pair as a single chunk regardless of length. Tabular data: one row per chunk with the header row repeated in each chunk.

3

Preserve metadata in every chunk

Every chunk document should include: the source document title, the source URL, the section heading, the page number, a document timestamp, and any access control metadata (department, user groups). This metadata is critical for generating cited answers and for security filtering. A retrieved chunk with no provenance metadata is unusable in a production system.

4

Measure retrieval quality before deploying chunk size changes

Build an evaluation set of 50–100 representative questions with known correct source documents. Run retrieval with different chunk sizes and measure Recall@5 (is the correct document in the top 5 results?) and MRR (Mean Reciprocal Rank). Do not change chunk size in production without evaluation data showing the new size outperforms the current one.

Python — Chunking with overlap using LangChain's RecursiveCharacterTextSplitter# pip install langchain-text-splitters tiktoken
from langchain_text_splitters import RecursiveCharacterTextSplitter
import tiktoken

def chunk_document(text: str, source_metadata: dict,
                    chunk_size: int = 512, overlap: int = 128) -> list[dict]:
    enc = tiktoken.get_encoding("cl100k_base") # OpenAI tokenizer

    splitter = RecursiveCharacterTextSplitter(
        chunk_size=chunk_size,
        chunk_overlap=overlap,
        length_function=lambda t: len(enc.encode(t)), # Token-based, not char-based
        separators=["\n\n", "\n", ". ", " ", ""] # Prefer natural boundaries
    )

    chunks = splitter.split_text(text)
    return [
        {
            "id": f"{source_metadata['doc_id']}-chunk-{i}",
            "chunk_text": chunk,
            "token_count": len(enc.encode(chunk)),
            "chunk_index": i,
            "source_url": source_metadata["url"],
            "source_title": source_metadata["title"],
            "department": source_metadata.get("department", "general"),
            "allowed_groups": source_metadata.get("allowed_groups", ["all"]),
            "embedding": None # Populated by vectorization step
        }
        for i, chunk in enumerate(chunks)
    ]
Runbook 3Enable and Tune Semantic Ranking
Precision LayerStandard+ Tiers

Semantic Ranking is Azure AI Search's cross-encoder re-ranking layer. After BM25 and HNSW return their merged RRF result set, the Semantic Ranker takes the top 50 results and passes each one through a transformer cross-encoder model that evaluates the full (query, document) pair — far more computationally expensive than the initial retrieval, but dramatically more accurate at distinguishing between plausibly relevant documents. The ranker also generates extractive captions — highlighted sentences that best answer the query — which are the ideal grounding material for the LLM's response generation step.

Semantic Ranking requires Standard tier or above. It is available as an add-on on Basic tier with limitations. The additional latency is typically 100–300ms per query — well worth the precision improvement for enterprise document retrieval where answer quality is the primary metric.

1

Define a Semantic Configuration in the index

The semantic configuration specifies which fields the ranker prioritises: the title field (highest weight), content fields (main body text), and keyword fields (metadata). The ranker uses this priority ordering to score the relevance of each document to the query.

2

Enable query captions and answers

Set queryCaption: "extractive" to get highlighted text snippets from the most relevant passage in each result. Set queryAnswer: "extractive" to get a direct answer extracted from the top result if the ranker identifies one. Pass these captions to the LLM instead of the full chunk text to reduce context window consumption by 60–80%.

3

Tune the rerankerScore threshold

Every result from Semantic Ranking includes a @search.reranker_score between 0 and 4. A score below 1.5 typically indicates a result that is marginally relevant at best. Filter out results below your threshold before passing context to the LLM — this reduces hallucination risk from weakly relevant context and reduces token consumption.

Python — Semantic configuration and caption-based RAG context assembly# Add semantic configuration to index definition
from azure.search.documents.indexes.models import (
    SemanticConfiguration, SemanticSearch,
    SemanticPrioritizedFields, SemanticField
)

semantic_config = SemanticConfiguration(
    name="my-semantic-config",
    prioritized_fields=SemanticPrioritizedFields(
        title_field=SemanticField(field_name="source_title"),
        content_fields=[SemanticField(field_name="chunk_text")],
    )
)
semantic_search = SemanticSearch(configurations=[semantic_config])

# ── QUERY WITH CAPTIONS + RERANKER SCORE FILTERING ───────
RERANKER_THRESHOLD = 1.5 # Exclude weakly relevant results

results = search_client.search(
    search_text=query_text,
    vector_queries=[vector_query],
    query_type="semantic",
    semantic_configuration_name="my-semantic-config",
    query_caption="extractive|highlight-false", # Clean text, no HTML tags
    query_answer="extractive|count-1", # Extract best direct answer if present
    top=10, select="chunk_text,source_url,source_title"
)

# Build LLM context from captions, filtered by reranker score
context_chunks = []
for result in results:
    if result.get("@search.reranker_score", 0) < RERANKER_THRESHOLD:
        continue # Skip low-confidence results
    captions = result.get("@search.captions", [])
    caption_text = captions[0].text if captions else result["chunk_text"][:500]
    context_chunks.append({
        "text": caption_text,
        "source": result["source_url"],
        "title": result["source_title"],
        "score": result["@search.reranker_score"]
    })
return context_chunks
Runbook 4Integrated Vectorization with Azure OpenAI Embeddings
Ingestion AutomationZero Custom Code

Integrated Vectorization allows Azure AI Search to generate embeddings automatically during ingestion — without any custom code to call the embedding API, handle batching, manage rate limits, or store vectors. You configure an Azure OpenAI embedding skill in the indexer's skillset, and the indexer handles chunking, embedding generation, and index population in a single managed pipeline. At query time, you can also configure the index to vectorize query text automatically using the same embedding model, eliminating the need to call the embedding API in your application code.

1

Deploy text-embedding-3-large in Azure AI Foundry

In the Azure AI Foundry portal, deploy the text-embedding-3-large model (3,072 dimensions — Microsoft's recommended embedding model for production RAG). Note the endpoint URL and deployment name. Configure Managed Identity on the Azure AI Search service and assign "Cognitive Services OpenAI User" role on the Azure OpenAI resource.

2

Create an Azure OpenAI skill in the indexer skillset

Add an AzureOpenAIEmbeddingSkill to the indexer's skillset. Configure it to receive the chunk text and output the embedding vector. The skill handles batching, retry on rate limits, and dimension validation automatically.

3

Configure vectorizer on the index for query-time embedding

Add a vectorizer to the HNSW profile pointing at the same Azure OpenAI deployment. When a search query arrives with no pre-computed vector, Azure AI Search calls the embedding API automatically and runs the vector search without the application needing to generate the query embedding.

4

Use change detection for incremental index updates

Configure the indexer to run on a schedule (e.g., every 15 minutes) with high-watermark change detection enabled on Blob Storage. The indexer only processes documents added or modified since the last run — avoiding the cost of re-embedding unchanged documents. For SharePoint and SQL sources, native change tracking achieves the same result.

Python — Indexer skillset with Azure OpenAI embedding skillfrom azure.search.documents.indexes.models import (
    SearchIndexerSkillset, AzureOpenAIEmbeddingSkill,
    InputFieldMappingEntry, OutputFieldMappingEntry,
    SplitSkill, SearchIndexer, SearchIndexerDataSourceConnection
)

# ── SKILLSET WITH CHUNK + EMBED SKILLS ───────────────────
split_skill = SplitSkill(
    name="chunk-skill",
    text_split_mode="pages",
    maximum_page_length=512,
    page_overlap_length=128,
    inputs=[InputFieldMappingEntry(name="text", source="/document/content")],
    outputs=[OutputFieldMappingEntry(name="textItems",
        target_name="pages")]
)

embedding_skill = AzureOpenAIEmbeddingSkill(
    name="embed-skill",
    resource_uri="https://YOUR-OPENAI.openai.azure.com",
    deployment_name="text-embedding-3-large",
    model_name="text-embedding-3-large",
    dimensions=3072,
    inputs=[InputFieldMappingEntry(name="text", source="/document/pages/*")],
    outputs=[OutputFieldMappingEntry(name="embedding",
        target_name="embedding")]
)

skillset = SearchIndexerSkillset(
    name="rag-skillset",
    skills=[split_skill, embedding_skill],
    description="Chunk and embed documents for RAG"
)
Runbook 5Document-Level Security Filters
Enterprise RequiredOData Filter

Enterprise RAG deployments typically have strict information security requirements: the HR chatbot should only retrieve documents the querying user is authorised to read. Without document-level security filters, every user can retrieve every document in the index — including documents from other departments, restricted projects, or regulated data categories. Azure AI Search implements this through OData filter expressions applied at query time.

The pattern: store each document's access control list (the group or user identifiers who can read it) in a filterable field in the index. At query time, your application resolves the current user's group memberships from Microsoft Entra ID, constructs an OData filter expression that limits results to documents in those groups, and passes it to every search call. The filter is applied before ranking — no restricted documents appear in any result set.

1

Add an allowed_groups field to the index (filterable, not searchable)

Define a Collection(Edm.String) field named allowed_groups with filterable: true but searchable: false. This field stores the list of Entra group object IDs that are permitted to read this document. Populate it during ingestion from the source document's ACL metadata.

2

Resolve user's group memberships at query time

Use the Microsoft Graph API to retrieve the current user's transitive group memberships: GET /me/transitiveMemberOf/microsoft.graph.group?$select=id. Cache the result per user session (group memberships rarely change mid-session) with a 15-minute TTL. Never hard-code group IDs in application code — always resolve dynamically from the authenticated identity.

3

Construct and apply the OData security filter on every query

Build the filter expression from the user's group list and pass it to the filter parameter of every search call. The filter must be applied at the Azure AI Search level — never filter results in application code after retrieval, because the vector search top-k calculation will have already excluded potentially-accessible documents.

Python — Security filter construction from Entra group membershipsimport aiohttp, asyncio
from azure.identity.aio import OnBehalfOfCredential

async def get_user_groups(user_access_token: str) -> list[str]:
    # Use On-Behalf-Of flow to call Graph API as the user
    credential = OnBehalfOfCredential(
        tenant_id=TENANT_ID, client_id=APP_CLIENT_ID,
        client_secret=APP_CLIENT_SECRET,
        user_assertion=user_access_token
    )
    token = await credential.get_token("https://graph.microsoft.com/.default")
    async with aiohttp.ClientSession() as session:
        resp = await session.get(
            "https://graph.microsoft.com/v1.0/me/transitiveMemberOf"
            "/microsoft.graph.group?$select=id",
            headers={"Authorization": f"Bearer {token.token}"}
        )
        data = await resp.json()
        return [g["id"] for g in data.get("value", [])]

def build_security_filter(group_ids: list[str]) -> str:
    # OData: allowed_groups contains any of the user's group IDs
    if not group_ids:
        return "allowed_groups/any(g: g eq 'all')" # Public docs only
    conditions = [f"allowed_groups/any(g: g eq '{gid}')" for gid in group_ids]
    conditions.append("allowed_groups/any(g: g eq 'all')") # Public docs
    return " or ".join(conditions)

# Usage in search call
user_groups = await get_user_groups(request.headers["Authorization"].split()[1])
security_filter = build_security_filter(user_groups)
results = hybrid_search(query_text, query_embedding, security_filter=security_filter)
Runbook 6Agentic Retrieval for Multi-Step Complex Queries
AdvancedAI Foundry

Standard RAG retrieval uses the user's query directly as the search input. This works well for simple factual questions but breaks down on complex, multi-part queries: "Summarise the Q3 revenue trend and explain how the APAC supply chain disruption contributed to the shortfall compared to the same period last year." This query requires multiple retrieval passes — one for Q3 revenue data, one for APAC supply chain events, one for the prior year comparison — before the LLM can synthesise a coherent answer.

Agentic Retrieval (GA in Azure AI Foundry 2026) solves this by having an LLM decompose complex queries into sub-queries, execute each independently against Azure AI Search, and then synthesise the retrieved chunks into a complete context before the final generation step. The pattern is sometimes called "query planning" or "multi-hop RAG."

1

Detect whether a query requires multi-hop retrieval

Use a lightweight classifier (GPT-5-nano or a rule-based check for conjunctions and temporal comparisons) to route queries. Simple queries go directly to standard hybrid search. Complex queries (containing "and", "compared to", "explain both", multiple named entities) go to the agentic retrieval path.

2

Use an LLM to decompose the complex query into sub-queries

Prompt GPT-4.1-mini with the original query and ask it to return a JSON list of 2–4 independent search sub-queries that together would retrieve all information needed to answer the original question. Each sub-query should be independently executable against Azure AI Search.

3

Execute sub-queries in parallel against Azure AI Search

Run all sub-queries simultaneously using asyncio.gather() — not sequentially. Parallelism keeps total latency close to a single-hop query while retrieving multiple times the context. Deduplicate results by chunk ID before passing to the final generation step.

4

Synthesise retrieved context and generate the final response

Pass all deduplicated chunks from all sub-queries as context to the final generation LLM (GPT-4.1). Structure the prompt to include which sub-query each chunk came from — this helps the LLM organise multi-part answers correctly and attribute each claim to the relevant source.

Python — Agentic multi-hop retrieval orchestrationimport asyncio, json
from openai import AzureOpenAI

DECOMPOSE_PROMPT = """You are a search query planner for a RAG system.
Given the user's question, decompose it into 2-4 independent search sub-queries.
Return ONLY a JSON array of strings. Example: ["query 1", "query 2"]
User question: {question}"""

async def agentic_retrieval(question: str, user_groups: list,
                           openai_client, embed_fn, search_fn):
    # Step 1: Decompose the query
    decompose_resp = openai_client.chat.completions.create(
        model="gpt41-mini", # Cheap model for planning
        messages=[{"role": "user",
            "content": DECOMPOSE_PROMPT.format(question=question)}],
        max_tokens=200, temperature=0
    )
    sub_queries = json.loads(decompose_resp.choices[0].message.content)

    # Step 2: Embed and search all sub-queries in parallel
    security_filter = build_security_filter(user_groups)

    async def search_subquery(sq: str):
        embedding = embed_fn(sq) # text-embedding-3-large
        return sq, search_fn(sq, embedding, top_k=3,
            security_filter=security_filter)

    results = await asyncio.gather(*[search_subquery(sq) for sq in sub_queries])

    # Step 3: Deduplicate by chunk ID
    seen, context_blocks = set(), []
    for sq, chunks in results:
        for chunk in chunks:
            if chunk["id"] not in seen:
                seen.add(chunk["id"])
                context_blocks.append({"sub_query": sq, "chunk": chunk})

    return sub_queries, context_blocks
Runbook 7Observability, Latency Tuning, and Tier Selection
ProductionAzure Monitor

A RAG system that works in testing and fails silently in production is worse than one that fails loudly — silent failures produce confident wrong answers, which erode user trust faster than obvious errors. Observability in enterprise RAG requires two tracks: infrastructure metrics (latency, error rates, index size) and retrieval quality metrics (reranker scores, cache hit rates, query latency distribution).

TierMax IndexesMax Index SizeSemantic RankingRecommended For
Free350MBNoPoC / evaluation only
Basic152GB/partitionAdd-onSmall pilot deployments under 10k documents
Standard S15025GB/partitionIncludedProduction: 10k–500k documents
Standard S2200100GB/partitionIncludedProduction: 500k–5M documents
Storage Optimised L1/L2102TB/partitionIncludedLarge-corpus RAG: 5M+ documents, archive search
1

Enable diagnostic logging to Log Analytics

Portal: Azure AI Search → Diagnostic settings → Add setting → send to Log Analytics workspace. Enable: OperationLogs, IndexingOperationsLogs, and QueryLogs. QueryLogs capture every search query with its latency, result count, and index name — the foundation for both performance monitoring and retrieval quality analysis.

2

Alert on P95 query latency above 2 seconds

Create an Azure Monitor alert on the SearchLatency metric (P95 over 5-minute windows). A P95 above 2,000ms indicates retrieval is adding perceptible delay to the user experience. Common causes: index too large for the tier (add replicas or partitions), excessive k_nearest_neighbors value, or semantic ranking applied to too many results.

3

Log reranker scores and monitor score distribution

In your application, log the @search.reranker_score for the top result of every query to Application Insights. Track the P25 reranker score over time. A declining P25 score (many queries returning low-confidence top results) indicates the index has become stale and needs re-indexing, or chunk size is misaligned with query patterns.

4

Tune efSearch for latency vs recall trade-off on self-managed vector profiles

If query latency exceeds targets, reduce efSearch (the runtime search width of the HNSW graph) in your vector search profile. The default efSearch is 500. Reducing it to 200–300 typically reduces query latency by 30–50% with only 2–5% recall reduction. This is the primary latency knob for HNSW-based vector search.

Runbook 8Terraform IaC for the Full Search Stack
Infrastructure as CodeRepeatable Deployment

Every component of the enterprise RAG stack — Azure AI Search, the Azure OpenAI embedding deployment, storage accounts, private endpoints, and RBAC assignments — should be in Terraform. This enables repeatable deployment across environments (dev/staging/production), code-reviewed infrastructure changes, and disaster recovery from a known-good state.

Terraform — Azure AI Search (Standard S1) with Managed Identity and Key Vault integration# ── AZURE AI SEARCH SERVICE ───────────────────────────────
resource "azurerm_search_service" "main" {
  name              = var.search_service_name
  resource_group_name = var.resource_group_name
  location          = var.location
  sku               = "standard" # S1: semantic ranking included
  replica_count     = var.environment == "production" ? 2 : 1
  partition_count   = 1
  identity {
    type = "SystemAssigned" # For Managed Identity access to OpenAI
  }
  local_authentication_enabled = false # Force Entra auth only
  public_network_access_enabled = var.environment == "production" ? false : true
}

# ── RBAC: Search MI can call Azure OpenAI for vectorization ─
resource "azurerm_role_assignment" "search_to_openai" {
  scope               = azurerm_cognitive_account.openai.id
  role_definition_name = "Cognitive Services OpenAI User"
  principal_id        = azurerm_search_service.main.identity[0].principal_id
}

# ── AZURE OPENAI EMBEDDING DEPLOYMENT ────────────────────
resource "azurerm_cognitive_deployment" "embedding" {
  name              = "text-embedding-3-large"
  cognitive_account_id = azurerm_cognitive_account.openai.id
  model {
    format  = "OpenAI"
    name    = "text-embedding-3-large"
    version = "1"
  }
  scale {
    type     = "Standard"
    capacity = 120 # 120K TPM — adjust based on indexing volume
  }
}

# ── PRIVATE ENDPOINT (Production) ────────────────────────
resource "azurerm_private_endpoint" "search" {
  count              = var.environment == "production" ? 1 : 0
  name              = "pe-${var.search_service_name}"
  resource_group_name = var.resource_group_name
  location          = var.location
  subnet_id         = var.private_endpoint_subnet_id
  private_service_connection {
    name                      = "psc-search"
    private_connection_resource_id = azurerm_search_service.main.id
    is_manual_connection        = false
    subresource_names           = ["searchService"]
  }
}

Architecture Decision Summary

Always use hybrid search. Pure vector misses exact keyword matches (product codes, acronyms, proper names). Pure BM25 misses semantic relationships. Hybrid search with RRF fusion is Microsoft's own recommended default and consistently outperforms either approach alone in enterprise document retrieval benchmarks.
The Semantic Ranker is not optional in production. RRF fusion returns a ranked list based on retrieval scores. The Semantic Ranker re-scores using a cross-encoder model that evaluates the full query-document pair — a fundamentally more accurate relevance signal. The 100–300ms additional latency is always worth the precision improvement.
Start with 512 tokens + 128 overlap for chunking. Measure Recall@5 before changing it. Smaller chunks improve specificity but may lack context. Larger chunks reduce specificity and dilute embedding quality. The overlap prevents the boundary truncation problem that causes retrievable answers to fall between chunks.
Use Integrated Vectorization. Writing custom code to batch documents, call the embedding API, handle rate limits, and upload vectors to the index is re-implementing infrastructure that Azure AI Search's skillset pipeline provides natively. Use it for both ingestion (embedding skill) and query time (vectorizer in the HNSW profile).
Security filters must apply at the index level — never in application code. Filtering after retrieval means the vector search top-k has already excluded potentially accessible documents. Apply OData filters on every search call. Resolve user group memberships at query time from Entra ID.
Agentic retrieval for complex queries. Multi-part questions ("compare X to Y across Z time period") require multiple retrieval passes before a coherent answer can be generated. Query decomposition into parallel sub-queries is the architectural pattern that makes complex enterprise use cases viable.
Log and monitor reranker scores, not just latency. A declining P25 reranker score across production queries is the leading indicator that retrieval quality is degrading — often weeks before users start complaining. Set up alerts on reranker score distribution alongside the standard infrastructure metrics.

Frequently Asked Questions

When should I use Azure AI Search vs a dedicated vector database like Pinecone or Weaviate?
Azure AI Search is the right choice when: your documents require both keyword and semantic retrieval (hybrid search), you need enterprise security (Entra integration, private endpoints, OData security filters), your stack is on Azure and you want managed integration with Azure OpenAI and Azure Blob Storage, or you need semantic ranking with extractive captions. Dedicated vector databases are better when: you have extremely high query volumes requiring horizontal scaling beyond Azure AI Search's tier limits, your use case is purely vector similarity with no keyword retrieval, or you need sub-5ms p99 latency at very large scale. For most enterprise RAG deployments on Azure, Azure AI Search is architecturally superior because of the hybrid retrieval and semantic ranking capabilities that pure vector databases cannot match.
What embedding model should I use for enterprise RAG in 2026?
Use text-embedding-3-large (3,072 dimensions) for production enterprise RAG. It outperforms text-embedding-3-small on retrieval benchmarks for long-form enterprise documents, and the cost difference is marginal at typical enterprise indexing volumes (embedding generation is a one-time cost at indexing time, not at query time when you use integrated vectorization). If latency of the embedding call during ingestion is a concern, text-embedding-3-small (1,536 dimensions) is acceptable for general-purpose content but underperforms on technical, legal, and medical domain content.
How many replicas and partitions should I provision for a production Azure AI Search service?
Replicas provide query throughput and availability — add replicas when P95 query latency is high under load or when you need high availability (2+ replicas required for SLA). Partitions provide index storage capacity — add partitions when your index exceeds 80% of current partition capacity. For a production deployment handling 100+ queries per minute with a 500k-document index on Standard S1: start with 2 replicas and 1 partition. Monitor SearchLatency (P95) and index storage usage; scale up when P95 exceeds 1.5 seconds or storage exceeds 80% of capacity. Scaling replicas up and down takes approximately 15–30 minutes with no downtime.
How do I handle documents in multiple languages in the same index?
For multi-language enterprise RAG, use two strategies together: language-specific analyzers (set analyzer_name to the appropriate language analyzer for the document's language on the chunk_text field, or use the languageAnalyzer field mapping), and a language-agnostic vector field (text-embedding-3-large produces cross-lingual embeddings that understand semantic similarity across languages). The vector component handles semantic retrieval across languages correctly. The BM25 keyword component needs language-specific analyzers to handle morphology correctly. Add a language metadata field and use it as a filter when queries come in a known language to restrict keyword search to same-language documents while letting vector search remain cross-lingual.

Popular posts from this blog

Learn how to use Azure Chaos Studio to simulate data center outages, test Azure OpenAI failover, and validate AI app resiliency using KQL and CLI workflows

Resiliency Testing Chaos Studio Zone Down Azure OpenAI Failover Testing AI Resiliency: Using Azure Chaos Studio to Simulate Data Center Outages on Your LLM Every multi-region Azure OpenAI architecture diagram has a failover arrow drawn on it. Almost none of them have ever actually been triggered. The arrow is a hypothesis, confirmed only by a real outage — unless you deliberately cause a controlled one first, on your own schedule, with a rollback plan, instead of finding out during an incident that the failover you designed never quite worked the way the diagram promised. The failure signature this guide resolves # The gap this article closes — a real architecture review finding: Design doc, page 4: "In the event of a regional outage, Azure Front Door automatically routes traffic to the secondary Azure OpenAI deployment in West Europe, with an expected failover time under 60 seconds." Verification performed to support this claim: NONE. Last time this path was ac...

Improve AI application performance by reducing latency, optimizing embeddings, and lowering cloud inference costs

Performance Fix Foundry Local 1.2 Linux ARM64 Embeddings Offline ASR The Edge Latency Drop: Fixing Latency Spikes by Offloading Embeddings to Foundry Local 1.2 You are paying a full cloud round trip — network, TLS, queue, throttle risk — to turn a twelve-word search query into a vector. That is the most expensive way possible to do one of the cheapest computations in your stack. Foundry Local 1.2 now runs on Linux ARM64, which means embeddings and speech recognition can happen on a Raspberry Pi, a Jetson, or a Graviton instance — offline, unmetered, and in single-digit milliseconds. The failure signature this guide resolves # Application Insights — the embedding call, not the LLM, is your tail latency: name p50 p95 p99 calls/day POST /embeddings (cloud) 89 ms 412 ms 3,847 ms 1,240,000 POST /chat/completions (cloud) 940 ms 1,720 ms 2,910 ms 38,000 ^^^^^^^^ ...

Learn how to select Azure Files and Blob storage tiers, avoid early deletion fees, model costs, and automate lifecycle management for large file migrations.

Choosing the Right Azure Storage Tier for Large File Migrations The complete decision framework for storage tier selection during large file migrations — Azure Files tiers, Blob tiers, cost modelling, early deletion traps, lifecycle automation, and the 2026 changes that affect every migration running today. By Francis Avorgbedor | Azure Engineer  ·  July 14, 2026  ·  18 min read  ·  Storage Tiers · Cost Optimisation · Migration FA Francis Avorgbedor Azure Engineer  ·  SEVENAI  ·  Azure Field Notes 9 Distinct Azure storage tiers across Files and Blob — most engineers know only 3 15hrs Archive tier rehydration time at standard priority — the delay teams forget to plan for 128KB Minimum billable object size for Cool/Cold/Archive from July 2026 — a 32× trap for small files 70% How much retrieval and transaction fees add to a theoretical Archive storage bill The most expensive mistake I see on large Azure file migrations is not choosing the w...

Find the hidden Windows 10 system files consuming up to 500GB, including hibernation, shadow copies, backups, and WinSxS, with safe cleanup steps.

  The 500GB System File That Eats Your Hard Drive Something on your Windows 10 drive is consuming hundreds of gigabytes and the normal tools cannot find it. This guide identifies every known culprit — from hibernation files and shadow copies to runaway backups and the Windows component store — and tells you exactly what is safe to delete, what to leave alone, and what the commands actually do.

Learn safe methods to reset Azure virtual machines using managed disks while preserving critical workloads

How to Reset an Azure Virtual Machine to Factory Settings Using a Managed Disk Azure does not have a single "factory reset" button. What it does have is something better: the OS Disk Swap — a method that swaps out the corrupted or misconfigured OS disk for a clean Windows Server managed disk without deleting the VM, its NICs, its IP addresses, or any attached data disks. Here is how it works, when to use it, and the exact steps to execute it safely. FA Francis Avorgbedor Azure Engineer July 16, 2026 15 min read Azure VMs · Windows Server · Real-World Fix 3 Methods to achieve a clean Windows Server installation on an existing Azure VM ~15min Typical OS Disk Swap duration — VM retains its NICs, IPs, and data disks throughout 0 Data disks affected by an OS Disk Swap — data disks remain attached and untouched 1 Snapshot of the original OS disk you must take before starting — no exceptions Introduction Why Azure Does Not Have a Simple Factory Reset — and What to Do Instead On a ph...

Determine Windows 11 compatibility, upgrade requirements, costs, and performance expectations on older hardware

Can I Update My Old Computer to Windows 11 — and How Much Will It Cost? Your i7, 16GB RAM, 512GB SSD machine is powerful enough to run Windows 11 comfortably. The TPM 2.0 and Secure Boot wall is a security checkbox, not a performance ceiling. Here are two proven ways to get past it, what each one costs, and what you are trading away by doing so. $0 Cost of the Windows 11 licence if your existing Windows 10 is genuine — the upgrade remains free in 2026 2 Proven methods to bypass TPM 2.0 and Secure Boot — Rufus (easy) and Registry edit (manual) 25H2 Current Windows 11 version — all known bypass methods tested and confirmed working as of July 2026 Oct 2025 Windows 10 end of life — no more security updates. Staying on Windows 10 now carries real risk. First — Check Your BIOS Before Anything Else You Might Not Actually Need a Bypass Before running any bypass, open your BIOS and look at two settings. Many computers that fail the Windows 11 compatibility check have TPM 2.0 present in the hard...

Solve common AKS issues with practical troubleshooting techniques for networking, scaling, upgrades, and workloads

Troubleshooting Guide AKS Kubernetes Real Solutions kubectl Azure Kubernetes Service (AKS) Troubleshooting Guide: Real Solutions to Common Problems CrashLoopBackOff at 2am. Pods stuck Pending with no obvious cause. Nodes going NotReady mid-deployment. DNS resolution silently failing in production. Every AKS engineer encounters these — the difference between engineers who panic and engineers who stay calm is knowing the exact sequence of diagnostic commands to run. This guide gives you that sequence, the root cause analysis for each failure mode, and the fix. 3 commands 90% of AKS problems are diagnosed with the same three kubectl commands: describe pod, logs --previous, and get events — in that order, every time Exit 137 The exit code that tells you everything: container killed by SIGKILL — either the Linux OOM killer (memory limit exceeded) or kubelet after grace period expired 5 min The CrashLoopBackOff ceiling: Kubernetes applies exponential backoff (10s → 20s → 40s → 80s → 160s → 3...

Step-by-step guide to deploying scalable AI chatbots on Azure with OpenAI and App Service

Step-by-Step Guide Azure OpenAI App Service Production Python How to Deploy an AI Chatbot on Azure Using Azure OpenAI and App Service From zero to a production-grade AI chatbot: provision Azure OpenAI, write a streaming Flask API backend, deploy it on Azure App Service with Managed Identity, wire in conversation history and content safety, and instrument it with Application Insights — all with complete code and Terraform IaC. No API keys in environment variables. No hardcoded secrets. No half-finished PoC patterns. 7 phases This guide covers the full deployment lifecycle: architecture design → resource provisioning → backend code → App Service deployment → streaming → security → monitoring Zero keys The chatbot authenticates to Azure OpenAI using Managed Identity and DefaultAzureCredential — no API keys stored in environment variables, Key Vault, or code SSE Server-Sent Events stream GPT tokens to the browser as they generate — the same token-by-token typing effect users expect from pr...