Table of Contents

Introduction

You can find all the C# code samples here: Embeddings GitHub Repository

Before we jump into integrated vectorization in Azure AI Search, I suggest checking out these earlier posts. They’ll give you the context you need to fully appreciate today’s topic:

In the previous post, we replaced the C# logic responsible for converting a text query into an embedding with a vectorizer. This simplified our code and offloaded that responsibility to Azure AI Search service.

In this blog post, we take that automation to the next level, meaning the logic responsible for indexing our phrases won’t be needed at all:

var batch = IndexDocumentsBatch.Upload(documentsToBeIndexed);
var indexDocumentsResult = await _searchClient.IndexDocumentsAsync(batch);

What Integrated Vectorization Actually Is

Let’s start with a simple definition:

Integrated Vectorization in Azure AI Search offloads embedding generation for both the indexing phase and the query phase, eliminating the need to handle it in your own code.

Instead of writing custom C# logic to generate embeddings from your data source and then adding more custom logic to convert a user query into an embedding you can simply offload all of it to Azure AI Search.

How Integrated Vectorization Differs from Vectorizers

After reading the previous post, you already know the difference, but let me repeat it here just in case:

  • Vectorizer is a component that converts a user query into an embedding.
  • Integration Vectorization is an approach/pattern that lets you offload embedding creation in both phases: indexing and querying so the Azure AI Search service handles it automatically.

Components configuration

Integrated Vectorization pattern in Azure AI Search service.

This screenshot shows the integrated vectorization pattern in Azure AI Search. As you can see, we’ll be pulling data from Cosmos, generating embeddings during document indexing (based on the Phrase field), and then querying them using a vectorizer.

{
    "id": "1",
    "Tags": [
        "Mars"
    ],
    "Phrase": "Red Planet",
    "_rid": "RMduANfFaWsBAAAAAAAAAA==",
    "_self": "dbs/RMduAA==/colls/RMduANfFaWs=/docs/RMduANfFaWsBAAAAAAAAAA==/",
    "_etag": "\"2300efd9-0000-0200-0000-6958d4c80000\"",
    "_attachments": "attachments/",
    "_ts": 1767429320
}

Let’s start with the Data Source component, which enables our Azure AI Search instance to read data from Azure Cosmos DB.

Creating a Data Source

Let’s define our data source using a JSON definition:

{
  "@odata.context": "https://deployed-in-azure-aisearch-eastus2.search.windows.net/$metadata#datasources/$entity",
  "@odata.etag": "\"0x8DE4AB52D38232E\"",
  "name": "integrated-vectorization-test-data-cosmosdb",
  "description": null,
  "type": "cosmosdb",
  "subtype": null,
  "indexerPermissionOptions": [],
  "credentials": {
    "connectionString": "ResourceId=/subscriptions/cf9e4b91-d6e2-4ae8-a891-541e24bf8e07/resourceGroups/deployed-in-azure-ai-search/providers/Microsoft.DocumentDB/databaseAccounts/deployed-in-azure-cosmosdb-eastus;Database=MyDatabase;IdentityAuthType=AccessToken"
  },
  "container": {
    "name": "IntegratedVectorizationTestData",
    "query": "SELECT * FROM c"
  },
  "dataChangeDetectionPolicy": null,
  "dataDeletionDetectionPolicy": null,
  "encryptionKey": null,
  "identity": null
}

As you can see, we define some basic properties such as the name, type and container, which includes both the container name and the query. This is a simple example, so I kept the configuration minimal. In a real-world setup, you would typically use a query like

SELECT * FROM c WHERE c._ts >= @HighWaterMark ORDER BY c._ts

and also specify a dataChangeDetectionPolicy (you can find more info about it here).

I’d like to draw a bit more attention to the credentials.connectionString section. When you’re just getting started with Azure AI Search, it’s tempting to rely on API key authentication because it feels simpler. However, I strongly encourage you to use RBAC whenever possible. It provides a more secure and maintainable approach in the long run (you can find more info here).

The first thing we need to do is enable a managed identity for our Azure AI Search service. Let’s use system-managed identity:

Enabling system-assigned managed identity in Azure AI Search service.

Once the managed identity is created, we can focus on assigning the appropriate RBAC roles. We need to assign two roles: one for control-plane access and one for data-plane access.

  • Control-plane: Cosmos DB Account Reader
  • Data-plane: Cosmos DB Built‑in Data Reader (you can find more details on how to assign this using the AZ CLI here)
Cosmos DB Account Reader Role RBAC role assigned.

We can’t assign the Cosmos DB Built‑in Data Reader role through the Azure Portal, and it won’t appear there either. Fortunately, the AZ CLI provides full support for assigning it. I have assigned it already and here is the result of az cosmosdb sql role assignment list -g 'deployed-in-azure-ai-search' -a 'deployed-in-azure-cosmosdb-eastus' invocation.

[
    {
        "id": "/subscriptions/cf9e4b91-d6e2-4ae8-a891-541e24bf8e07/resourceGroups/deployed-in-azure-ai-search/providers/Microsoft.DocumentDB/databaseAccounts/deployed-in-azure-cosmosdb-eastus/sqlRoleAssignments/9203f828-ee35-4d84-a631-b5ea5ccbcb72",
        "name": "9203f828-ee35-4d84-a631-b5ea5ccbcb72",
        "principalId": "c8ad61f7-38af-4895-a2ba-87e12a6b9bfc",
        "resourceGroup": "deployed-in-azure-ai-search",
        "roleDefinitionId": "/subscriptions/cf9e4b91-d6e2-4ae8-a891-541e24bf8e07/resourceGroups/deployed-in-azure-ai-search/providers/Microsoft.DocumentDB/databaseAccounts/deployed-in-azure-cosmosdb-eastus/sqlRoleDefinitions/00000000-0000-0000-0000-000000000001",
        "scope": "/subscriptions/cf9e4b91-d6e2-4ae8-a891-541e24bf8e07/resourceGroups/deployed-in-azure-ai-search/providers/Microsoft.DocumentDB/databaseAccounts/deployed-in-azure-cosmosdb-eastus",
        "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments"
    }
]

Let’s discuss 3 fields which I consider especially relevant

  • principalIdc8ad... is the identity ID of the system-assigned managed identity we enabled earlier (you can see it in one of the previous screenshots).
  • roleDefinitionId – as you can see, I selected the Cosmos DB Built‑in Data Reader role, which ends with 00000000-0000-0000-0000-000000000001.
  • scope – I assigned this RBAC role at the account level, not at the container level (.../colls/IntegratedVectorizationTestData), even though I’m pulling data from the IntegratedVectorizationTestData container.

Our Azure AI Search instance can now connect securely and pull data from Azure Cosmos DB, so we can move on to the next step.

Creating a Skillset

Now it’s the right time to instruct Azure AI Search to generate embeddings automatically for us. To do that, we create a skillset – a configurable pipeline of operations that enriches your data before it’s indexed. If you haven’t worked with skillsets before, think of them as the pre-processing stage of the indexing process.

If you’re doing this for the first time, I encourage you to use the Azure Portal. You can open Azure Cosmos DB and select Integrations > Add Azure AI Search, or open the Azure AI Search service Overview screen and choose Import Data > RAG. Both workflows lead to the same result – a JSON definition.

Import new data wizard in Azure AI Search service using RAG option. Vectorize your text step.
Azure AI Search overview > Import Data > RAG

If you use Import Data > RAG, it will add a skillset with two skills: the first is Microsoft.Skills.Text.SplitSkill, which is responsible for chunking the source data, and the second is AzureOpenAIEmbeddingSkill (you can find all the details here). We don’t need the first skill for this example (but in production workloads its very likely that you need SplitSkill too), and I want to keep the skillset definition as simple as possible, so here’s what it looks like:

{
  "@odata.etag": "\"0x8DE4AAB2C525563\"",
  "name": "skillset-generate-embeddings",
  "description": "Skillset to generate embeddings only",
  "skills": [
    {
      "@odata.type": "#Microsoft.Skills.Text.AzureOpenAIEmbeddingSkill",
      "name": "#1",
      "context": "/document",
      "resourceUri": "https://azure-foundry-001-resource.openai.azure.com",
      "deploymentId": "text-embedding-3-small",
      "dimensions": 1536,
      "modelName": "text-embedding-3-small",
      "inputs": [
        {
          "name": "text",
          "source": "/document/Phrase",
          "inputs": []
        }
      ],
      "outputs": [
        {
          "name": "embedding",
          "targetName": "Vector"
        }
      ]
    }
  ]
}

Let’s analyze the most important fields:

  • modelName – the embedding model we’re going to use during the indexing process. ❗❗❗ We MUST use the same model for our vectorizer to ensure that all vectors, both from indexing and querying, remain in the same embedding space.
  • inputs[0].source – we specify here which field should be vectorized. In our case it is the Phrase field. Please note it has to be prefixed with /document/
  • outputs[0].targetName – specifies the name of the variable that will store the generated embedding (❗❗❗this doesn’t automatically write the value to the index, an additional output field mapping is required for that step)

As you can see, there is no apiKey property in the JSON definition. That’s because I’m using RBAC instead. The role I’m using is Azure AI User, and it’s assigned to the system-managed identity of Azure AI Search service we created earlier.

Azure AI user RBAC role assignment to the Azure AI Search service instance.

If you wanted to use a user‑assigned managed identity instead, you would need to add a field like this:

{
    "authIdentity": {
        "@odata.type": "#Microsoft.Azure.Search.DataUserAssignedIdentity",
        "userAssignedIdentity": "/subscriptions/<subscription_id>/resourcegroups/<resource_group>/providers/Microsoft.ManagedIdentity/userAssignedIdentities/<user-assigned-managed-identity-name>"
    }
}

Our skillset is ready to be invoked… but how do we actually tell Azure AI Search to invoke it, you may ask.

The answer lies in the indexer definition, so let’s focus on that now!

Creating an Index

Before we will move to the indexer definition let’s create an index first. This is the same index definition as the one we used in the previous post so let me just paste it here one more time:

{
  "@odata.etag": "\"0x8DE4AAB5FE224AD\"",
  "name": "vector-search-index-with-vectorizer",
  "purviewEnabled": false,
  "fields": [
    {
      "name": "id",
      "type": "Edm.String",
      "searchable": false,
      "filterable": false,
      "retrievable": true,
      "stored": true,
      "sortable": false,
      "facetable": false,
      "key": true,
      "synonymMaps": []
    },
    {
      "name": "Phrase",
      "type": "Edm.String",
      "searchable": false,
      "filterable": false,
      "retrievable": true,
      "stored": true,
      "sortable": false,
      "facetable": false,
      "key": false,
      "synonymMaps": []
    },
    {
      "name": "Tags",
      "type": "Collection(Edm.String)",
      "searchable": false,
      "filterable": true,
      "retrievable": true,
      "stored": true,
      "sortable": false,
      "facetable": false,
      "key": false,
      "synonymMaps": []
    },
    {
      "name": "Vector",
      "type": "Collection(Edm.Single)",
      "searchable": true,
      "filterable": false,
      "retrievable": false,
      "stored": true,
      "sortable": false,
      "facetable": false,
      "key": false,
      "dimensions": 1536,
      "vectorSearchProfile": "vector-profile-01",
      "synonymMaps": []
    }
  ],
  "scoringProfiles": [],
  "suggesters": [],
  "analyzers": [],
  "normalizers": [],
  "tokenizers": [],
  "tokenFilters": [],
  "charFilters": [],
  "similarity": {
    "@odata.type": "#Microsoft.Azure.Search.BM25Similarity"
  },
  "vectorSearch": {
    "algorithms": [
      {
        "name": "hnsw-algorithm",
        "kind": "hnsw",
        "hnswParameters": {
          "metric": "cosine",
          "m": 4,
          "efConstruction": 400,
          "efSearch": 500
        }
      }
    ],
    "profiles": [
      {
        "name": "vector-profile-01",
        "algorithm": "hnsw-algorithm",
        "vectorizer": "openai-vectorizer"
      }
    ],
    "vectorizers": [
      {
        "name": "openai-vectorizer",
        "kind": "azureOpenAI",
        "azureOpenAIParameters": {
          "resourceUri": "https://azure-foundry-001-resource.cognitiveservices.azure.com",
          "deploymentId": "text-embedding-3-small",
          "modelName": "text-embedding-3-small"
        }
      }
    ],
    "compressions": []
  }
}

This schema has already been discussed, so let’s focus on what matters most in the context of this post: the modelName of our azureOpenAI vectorizer.

It uses the same text-embedding-3-small embedding model as our skillset and I’m repeating this once more because it’s crucial to remember!

Creating an Indexer

If you look at the conceptual diagram of integrated vectorization in Azure AI Search again, you’ll notice that the indexer acts as a connector between the various components and that design is clearly reflected in the indexer’s JSON definition:

{
  "@odata.context": "https://deployed-in-azure-aisearch-eastus2.search.windows.net/$metadata#indexers/$entity",
  "@odata.etag": "\"0x8DE4ACB3AA0A4D8\"",
  "name": "integrated-vectorization-indexer",
  "description": null,
  "dataSourceName": "integrated-vectorization-test-data-cosmosdb",
  "skillsetName": "skillset-generate-embeddings",
  "targetIndexName": "vector-search-index-with-vectorizer",
  "disabled": null,
  "schedule": null,
  "parameters": null,
  "fieldMappings": [],
  "outputFieldMappings": [
    {
      "sourceFieldName": "/document/Vector",
      "targetFieldName": "Vector",
      "mappingFunction": null
    }
  ],
  "cache": null,
  "encryptionKey": null
}

Let’s discuss 4 fields which I consider the most relevant:

  • dataSourceName – the name of the data source.
  • skillsetName – the name of the skillset responsible for generating embeddings.
  • targetIndexName – the destination index for the indexing process.
  • outputFieldMappings – ❗❗❗we MUST instruct the indexer to take the value generated by our skillset and assign it to the Vector field in the index. Without this mapping, it simply won’t work!

Demo

Let’s first check whether our indexer runs successfully when we trigger it:

Azure AI Search indexer with integrated vectorization run status.

It works!

So now let’s switch to the C# app. As you might expect, there’s only one change compared to the previous post which is the removal of the indexing logic. Everything else stays the same including the search function (which uses VectorizableTextQuery which you already know from the previous post):

private async Task<IReadOnlyCollection<AiSearchVectorSearchResult>> FindSimilarItemsAsync(string keyword, int topK)
{
    var searchOptions = new SearchOptions
    {
        VectorSearch = new VectorSearchOptions
        {
            Queries =
            {
                new VectorizableTextQuery(keyword)
                {
                    KNearestNeighborsCount = topK,
                    Fields = { nameof(AiSearchVectorSearchDocumentModel.Vector) }
                }
            },
        },
        Select =
        {
            nameof(AiSearchVectorSearchDocumentModel.id),
            nameof(AiSearchVectorSearchDocumentModel.Phrase),
            nameof(AiSearchVectorSearchDocumentModel.Tags)
        },
        Size = topK
    };

    var response = await _searchClient.SearchAsync<AiSearchVectorSearchDocumentModel>(searchText: null, searchOptions);

    var results = new List<AiSearchVectorSearchResult>(capacity: topK);
    await foreach (var searchResult in response.Value.GetResultsAsync())
    {
        results.Add(new AiSearchVectorSearchResult
        {
            id = searchResult.Document.id,
            Phrase = searchResult.Document.Phrase,
            Tags = searchResult.Document.Tags,
            SimilarityScore = searchResult.Score.GetValueOrDefault()
        });
    }

    return results;
}

Below are the results when I invoke the AiSearchIntegratedVectorizationExample example:

Top 5 similar items to "Mars":
- [Tag:Mars] Mars exploration: 0.80
- [Tag:Mars] Mars atmosphere: 0.79
- [Tag:Curiosity Rover] Mars rover: 0.78
- [Tag:Curiosity Rover] Mars Science Laboratory: 0.77
- [Tag:Mars] Martian surface: 0.76

Top 5 similar items to "Apollo 11":
- [Tag:Apollo 11] Moon landing mission: 0.71
- [Tag:Neil Armstrong] First man on the Moon: 0.69
- [Tag:Neil Armstrong] Buzz Aldrin: 0.68
- [Tag:Apollo 11] NASA 1969: 0.65
- [Tag:Curiosity Rover] Rover landing: 0.65

Top 5 similar items to "Neil Armstrong":
- [Tag:Neil Armstrong] Buzz Aldrin: 0.75
- [Tag:Neil Armstrong] First man on the Moon: 0.73
- [Tag:Apollo 11] Moon landing mission: 0.70
- [Tag:Neil Armstrong] Astronaut: 0.69
- [Tag:Neil Armstrong] NASA astronaut corps: 0.65

Top 5 similar items to "Curiosity Rover":
- [Tag:Curiosity Rover] Mars rover: 0.80
- [Tag:Curiosity Rover] Mars Science Laboratory: 0.72
- [Tag:Curiosity Rover] Rover landing: 0.71
- [Tag:Mars] Mars exploration: 0.71
- [Tag:Mars] Red Planet: 0.66

The results are exactly the same as in the previous example but I hope that after reading this series, you’re not surprised! If the embedding model stayed the same and we haven’t changed our index definition, then the output must remain the same.

Limitations

If I encouraged you even a little to try integrated vectorization in Azure AI Search, then I’m glad but it’s also fair to warn you that there are some serious limitations you should be aware of!

Region limitation

You cannot use this feature in all Azure regions. So before you decide to go this route, make sure that the region you’re targeting actually supports it (you can check it here).

To mitigate this challenge, you may consider deploying the Azure AI Search service to the nearest region that supports the feature. That approach can work, but remember that the ideal setup is when your data source, embedding model, and search service all reside in the same Azure region!

Indexing interval limitation

A skillset cannot exist on its own, it must be attached to an indexer. This introduces a significant limitation: the smallest interval you can configure for an indexer is 5 minutes. Keep this in mind, because I can easily imagine scenarios where such a delay is simply not acceptable. In such case you may also consider a hybrid-approach.

Summary

I hope you found this post helpful and that it clarified how integrated vectorization in Azure AI Search works. We covered a lot of ground from setting up a data source with RBAC, to creating a skillset that generates embeddings automatically, to wiring everything together through an indexer. You also saw how the C# application becomes simpler once embedding generation is fully offloaded to the service.

The key takeaway is simple: keep your embedding model consistent across indexing and querying. And before adopting this pattern, make sure you’re aware of the regional and indexing‑interval limitations.

If this post encouraged you to explore integrated vectorization in Azure AI Search even a bit, I’m really glad.

Thanks for reading and see you in the next post!

Categorized in:

AI Services,