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
pipavailable - Service principal or managed identity with
Search Index Data ContributorandSearch Service Contributorroles 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.
Execute hybrid search
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.