n4nAI

Semantic Kernel vector store tutorial: Azure AI Search setup

Build a production-ready Semantic Kernel vector store with Azure AI Search — prerequisites, index creation, ingestion, and hybrid search with runnable code.

n4n Team3 min read658 words

Audio narration

Coming soon — every post will get a voice note here.

Semantic Kernel’s vector store abstraction lets you swap backends without rewriting application logic. Azure AI Search is a strong choice when you need managed infrastructure, hybrid search (vector + keyword), and deep Azure integration. This tutorial walks through a complete, runnable setup: creating the index, configuring the vector store connector, ingesting documents, and executing hybrid queries.

Prerequisites

  • Azure subscription with permission to create Azure AI Search resources
  • Azure AI Search service (Basic tier or higher for vector support)
  • Azure OpenAI deployment for embeddings (text-embedding-3-small or text-embedding-3-large)
  • Python 3.10+ with pip available
  • Service principal or managed identity with Search Index Data Contributor and Search Service Contributor roles on the search service

Install the required packages:

pip install semantic-kernel[azure] azure-identity azure-search-documents==11.4.0

The azure extra pulls in the Azure AI Search vector store connector. Pin azure-search-documents to 11.4.0 — newer versions have breaking changes in the vector query API that the connector hasn’t caught up to yet.

Create the Azure AI Search index

Semantic Kernel expects a specific index schema. You can let the connector create it automatically, but defining it explicitly gives you control over analyzers, scoring profiles, and vector algorithm parameters.

# create_index.py
import os
from azure.search.documents.indexes import SearchIndexClient
from azure.search.documents.indexes.models import (
    SearchIndex,
    SearchField,
    SearchFieldDataType,
    SimpleField,
    SearchableField,
    VectorSearch,
    VectorSearchProfile,
    HnswAlgorithmConfiguration,
    HnswParameters,
    SemanticSearch,
    SemanticConfiguration,
    SemanticPrioritizedFields,
    SemanticField,
)
from azure.identity import DefaultAzureCredential

SEARCH_ENDPOINT = os.environ["AZURE_SEARCH_ENDPOINT"]  # https://<name>.search.windows.net
INDEX_NAME = "sk-docs"

credential = DefaultAzureCredential()
index_client = SearchIndexClient(endpoint=SEARCH_ENDPOINT, credential=credential)

fields = [
    SimpleField(name="id", type=SearchFieldDataType.String, key=True, filterable=True),
    SearchableField(name="content", type=SearchFieldDataType.String, analyzer_name="en.microsoft"),
    SimpleField(name="source", type=SearchFieldDataType.String, filterable=True, facetable=True),
    SimpleField(name="chunk_id", type=SearchFieldDataType.Int32, filterable=True),
    SearchField(
        name="content_vector",
        type=SearchFieldDataType.Collection(SearchFieldDataType.Single),
        vector_search_dimensions=1536,  # text-embedding-3-small
        vector_search_profile_name="hnsw-profile",
    ),
]

vector_search = VectorSearch(
    profiles=[
        VectorSearchProfile(name="hnsw-profile", algorithm_configuration_name="hnsw-config")
    ],
    algorithms=[
        HnswAlgorithmConfiguration(
            name="hnsw-config",
            parameters=HnswParameters(m=4, ef_construction=400, ef_search=500, metric="cosine"),
        )
    ],
)

semantic_search = SemanticSearch(
    configurations=[
        SemanticConfiguration(
            name="semantic-config",
            prioritized_fields=SemanticPrioritizedFields(
                content_fields=[SemanticField(field_name="content")],
                keywords_fields=[SemanticField(field_name="source")],
            ),
        )
    ]
)

index = SearchIndex(name=INDEX_NAME, fields=fields, vector_search=vector_search, semantic_search=semantic_search)
result = index_client.create_or_update_index(index)
print(f"Index '{result.name}' created or updated.")

Run it:

export AZURE_SEARCH_ENDPOINT="https://your-service.search.windows.net"
python create_index.py

Expected output:

Index 'sk-docs' created or updated.

Verify in the Azure portal: the index should show 1536-dimension vector field content_vector, HNSW profile hnsw-profile, and semantic configuration semantic-config.

Configure the Semantic Kernel vector store connector

The connector wraps the Azure SDK and implements SK’s VectorStoreRecordCollection protocol. You’ll need an embedding service — here we use Azure OpenAI.

# vector_store_setup.py
import os
from semantic_kernel.connectors.ai.open_ai import AzureTextEmbedding
from semantic_kernel.connectors.memory.azure_ai_search import AzureAISearchVectorStore
from semantic_kernel.data import VectorStoreRecordDefinition, VectorStoreRecordField
from azure.identity import DefaultAzureCredential

AOAI_ENDPOINT = os.environ["AZURE_OPENAI_ENDPOINT"]
AOAI_DEPLOYMENT = os.environ["AZURE_OPENAI_EMBEDDING_DEPLOYMENT"]  # text-embedding-3-small
AOAI_API_VERSION = os.environ.get("AZURE_OPENAI_API_VERSION", "2024-02-01")
SEARCH_ENDPOINT = os.environ["AZURE_SEARCH_ENDPOINT"]
INDEX_NAME = "sk-docs"

embedding_service = AzureTextEmbedding(
    deployment_name=AOAI_DEPLOYMENT,
    endpoint=AOAI_ENDPOINT,
    api_version=AOAI_API_VERSION,
    credential=DefaultAzureCredential(),
)

vector_store = AzureAISearchVectorStore(
    search_endpoint=SEARCH_ENDPOINT,
    credential=DefaultAzureCredential(),
)

collection = vector_store.get_collection(
    name=INDEX_NAME,
    data_model_type=dict,
    embedding_service=embedding_service,
    record_definition=VectorStoreRecordDefinition(
        fields={
            "id": VectorStoreRecordField(name="id", is_key=True, type="str"),
            "content": VectorStoreRecordField(name="content", type="str", is_full_text_searchable=True),
            "source": VectorStoreRecordField(name="source", type="str", is_filterable=True),
            "chunk_id": VectorStoreRecordField(name="chunk_id", type="int", is_filterable=True),
            "content_vector": VectorStoreRecordField(name="content_vector", type="list[float]", is_vector=True, dimensions=1536),
        }
    ),
)

The record_definition maps your Python dict keys to the index fields. is_full_text_searchable=True enables keyword search on content; is_vector=True enables vector search on content_vector. The connector handles embedding generation automatically when you upsert records.

Ingest documents with chunking

Real workloads need chunking. Semantic Kernel doesn’t include a chunker, so we’ll use a simple sliding-window approach. Replace with langchain.text_splitter or llama-index if you need recursive or semantic chunking.

# ingest.py
import uuid
from vector_store_setup import collection

DOCUMENTS = [
    {
        "id": "doc-1",
        "text": "Semantic Kernel is an open-source SDK that lets you combine conventional programming languages with large language models. It provides planners, memories, and connectors for AI orchestration.",
        "source": "sk-overview.md",
    },
    {
        "id": "doc-2",
        "text": "Azure AI Search supports hybrid search: vector similarity (HNSW), full-text BM25, and semantic reranking in a single query. This reduces latency compared to running separate queries and merging results client-side.",
        "source": "azure-search-hybrid.md",
    },
    {
        "id": "doc-3",
        "text": "When using Semantic Kernel with Azure AI Search, the vector store connector handles embedding generation, upsert, and query translation. You work with Python dicts or dataclasses; the connector maps fields to the index schema.",
        "source": "sk-azure-integration.md",
    },
]

CHUNK_SIZE = 500
CHUNK_OVERLAP = 50

def chunk_text(text: str, size: int, overlap: int) -> list[str]:
    chunks = []
    start = 0
    while start < len(text):
        end = min(start + size, len(text))
        chunks.append(text[start:end])
        start += size - overlap
    return chunks

async def ingest():
    records = []
    for doc in DOCUMENTS:
        chunks = chunk_text(doc["text"], CHUNK_SIZE, CHUNK_OVERLAP)
        for i, chunk in enumerate(chunks):
            records.append({
                "id": f"{doc['id']}-chunk-{i}",
                "content": chunk,
                "source": doc["source"],
                "chunk_id": i,
            })
    
    await collection.upsert(records)
    print(f"Upserted {len(records)} chunks.")

if __name__ == "__main__":
    import asyncio
    asyncio.run(ingest())

Run it:

export AZURE_OPENAI_ENDPOINT="https://your-openai.openai.azure.com"
export AZURE_OPENAI_EMBEDDING_DEPLOYMENT="text-embedding-3-small"
python ingest.py

Expected output:

Upserted 7 chunks.

Wait 5–10 seconds for indexing to complete, then verify in the Azure portal: the index document count should match your chunk count.

Hybrid search combines vector similarity, BM25 keyword matching, and semantic reranking. The connector exposes this through VectorSearchOptions.

# search.py
from vector_store_setup import collection
from semantic_kernel.data import VectorSearchOptions, VectorSearchFilter

async def hybrid_search(query: str, top: int = 5):
    options = VectorSearchOptions(
        top=top,
        vector_search_mode="hybrid",  # vector + keyword + semantic rerank
        semantic_configuration="semantic-config",
        include_total_count=True,
    )
    
    results = await collection.search(query, options=options)
    
    print(f"Query: '{query}'")
    print(f"Total hits: {results.total_count}")
    async for record in results.results:
        print(f"  Score: {record.score:.4f} | Source: {record.record['source']} | Chunk: {record.record['chunk_id']}")
        print(f"  Content: {record.record['content'][:120]}...")
        print()

async def filtered_search(query: str, source_filter: str, top: int = 5):
    from azure.search.documents.models import FilterableField
    # The connector passes filter expressions directly to Azure AI Search OData syntax
    options = VectorSearchOptions(
        top=top,
        vector_search_mode="hybrid",
        semantic_configuration="semantic-config",
        filter=f"source eq '{source_filter}'",
    )
    
    results = await collection.search(query, options=options)
    print(f"Filtered query: '{query}' (source={source_filter})")
    async for record in results.results:
        print(f"  Score: {record.score:.4f} | {record.record['content'][:100]}...")

if __name__ == "__main__":
    import asyncio
    asyncio.run(hybrid_search("How does Semantic Kernel work with Azure AI Search?"))
    asyncio.run(filtered_search("hybrid search", "azure-search-hybrid.md"))

Run it:

python search.py

Expected output (scores will vary):

Query: 'How does Semantic Kernel work with Azure AI Search?'
Total hits: 3
  Score: 0.9231 | Source: sk-azure-integration.md | Chunk: 0
  Content: When using Semantic Kernel with Azure AI Search, the vector store connector handles embedding generation, upsert, and query translation...
  Score: 0.8472 | Source: sk-overview.md | Chunk: 0
  Content: Semantic Kernel is an open-source SDK that lets you combine conventional programming languages with large language models...
  Score: 0.7815 | Source: azure-search-hybrid.md | Chunk: 0
  Content: Azure AI Search supports hybrid search: vector similarity (HNSW), full-text BM25, and semantic reranking in a single query...

Filtered query: 'hybrid search' (source=azure-search-hybrid.md)
  Score: 0.9512 | Source: azure-search-hybrid.md | Chunk: 0
  Content: Azure AI Search supports hybrid search: vector similarity (HNSW), full-text BM25, and semantic reranking in a single query...

Tune vector search parameters

The HNSW parameters in the index definition affect recall and latency. For production, adjust based on your dataset size and latency budget:

Parameter Effect Typical range
m Graph connectivity; higher = better recall, more memory 4–16
ef_construction Build-time search width; higher = better index quality, slower indexing 200–800
ef_search Query-time search width; higher = better recall, higher latency 100–1000

For a 1M-document index with sub-100ms p99 latency, start with m=8, ef_construction=500, ef_search=400. Run recall@k evaluation against a labeled test set before committing.

Handle authentication in production

DefaultAzureCredential works locally (Azure CLI, VS Code, environment) and in Azure (managed identity). In containerized deployments, assign a user-assigned managed identity to the container and set AZURE_CLIENT_ID:

export AZURE_CLIENT_ID="<user-assigned-mi-client-id>"

The credential chain will pick it up automatically. Avoid connection strings or API keys in code.

Common pitfalls

Dimension mismatch: The index vector_search_dimensions must match your embedding model output. text-embedding-3-small = 1536, text-embedding-3-large = 3072, ada-002 = 1536. A mismatch causes upsert to fail with a cryptic 400 error.

Semantic reranking requires Standard tier: The Free tier supports vector and keyword search but not semantic reranking. If semantic_configuration is set on a Free tier service, queries return 400.

Connector version drift: The SK Azure AI Search connector lags the Azure SDK. Pin azure-search-documents==11.4.0 and check the connector’s requirements.txt before upgrading either.

Filter syntax: The filter parameter in VectorSearchOptions passes directly to Azure AI Search OData. Use single quotes for string literals: source eq 'my-doc.pdf'. Date filters: last_updated ge 2024-01-01T00:00:00Z.

Next steps

  • Add a delete-by-filter path for document updates: await collection.delete(filter="source eq 'old-version.pdf'")
  • Implement incremental ingestion using a change feed or last-modified timestamps
  • Add observability: log query latency, result counts, and embedding token usage
  • Evaluate semantic ranker impact by comparing vector_search_mode="hybrid" vs "vector" on your test set

The vector store abstraction means you can swap to Qdrant, Pinecone, or Weaviate by changing the connector and record definition — the ingestion and search code stays the same.

Tagssemantic-kernelazure-ai-searchvector-storetutorial

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All semantic kernel memory & vector stores posts →