> ## Content Index
> Fetch the complete content index at: https://www.imaginarium.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# Azure AI Search Index Performance Comparison
- URL: https://www.imaginarium.dev/azure-ai-search-index-performance/
- Published: 2026-11-02T18:31:26.000Z
- Updated: 2026-11-02T18:33:21.000Z
- Author: Batsirai Tambo
- Tags: Azure AI Search

In an effort scrutinise the performance of Azure AI Search, I decided to compare what it be like to observe the difference in confidence search scores and relevance of results based on whether the vector fields utilised whole document vector embeddings or whether chunked vector embeddings where used.

It came from a realisation when I was learning and pushing around Azure AI Search for results, that the results I received could have been polluted by the process of ingesting whole documents into the Azure AI Search Vector Index's vector field and basing my searches based on what was in the entire document. 

Could search quality for specific search queries be improved by attempting to isolate the vector embeddings to just specific parts of the documents that were in focus that we knew we would want to semantically understand and search for?

I took some time to find out just that.

For refence, I used fictional PDF invoices that looked like this example:

[invoice\_42\_color\_B\_242invoice\_42\_color\_B\_242.pdf44 KBdownload-circle](https://www.imaginarium.dev/content/files/2026/11/invoice%5F42%5Fcolor%5FB%5F242.pdf "Download")

## Azure AI Search Indexes and Embeddings

I started initially by creating a whole document ingestion vector field utilising the OpenAI text-embedding-3-small model (living in a deployment in Microsoft Foundry) for vectorising the whole PDF into the vector field per record of documents stored in Azure Blob Storage. 

![](https://storage.ghost.io/c/23/ff/23ffb93f-40bb-4405-983a-69cca9fd1421/content/images/2026/11/image.png)

Bringing forth vector field and built in file fields

This data consisted of approximately 1000 PDF documents of fictional invoices spanning a broad range of products. The custom field here that I made is content\_vector where this is of type EDM.Single , expecting 1536 dimensions per vector embeddings given to it. I also extracted out the built-in fields metadata\_storage\_name and metadata\_storage\_path for displaying the filename and URL path in blob storage so that I could easily see them in search results.

I created an indexing skillset with 1 skill (An AzureOpenAIEmbeddingSkill) to carry out the vectorisation process of converting a whole document into a 1536-dimension vector that would populate the content\_vector defined in my Index. This is how I defined the skillset:

```json
{
  "@odata.etag": "\"0x8DF3808DC75B098\"",
  "name": "invoice-embedding-skillset",
  "skills": [
    {
      "@odata.type": "#Microsoft.Skills.Text.AzureOpenAIEmbeddingSkill",
      "name": "invoice-embedding",
      "context": "/document",
      "resourceUri": "[MyMicrosoft_Foundry_Embedding_Model_Endpoint]",
      "apiKey": "<redacted>",
      "deploymentId": "text-embedding-3-small-batsiinvoices",
      "dimensions": 1536,
      "modelName": "text-embedding-3-small",
      "inputs": [
        {
          "name": "text",
          "source": "/document/content",
          "inputs": []
        }
      ],
      "outputs": [
        {
          "name": "embedding",
          "targetName": "invoiceVector"
        }
      ]
    }
  ]
}
```

Simple Skillset to define an Embedding Skill to vectorise documents into a vector field

The indexer for this case was configured to be the following (notice the definition here of the skillset from above):

```json
{
  "@odata.context": "[azure_ai_search_enpointUrl]/$metadata#indexers/$entity",
  "@odata.etag": "\"0x8DF383C3C47FDD6\"",
  "name": "indexer-1793528687945",
  "description": null,
  "dataSourceName": "azureblob-1793527132374-datasource",
  "skillsetName": "invoice-embedding-skillset", //embedding skill to perform vectorisation
  "targetIndexName": "invoicetextindex",
  "disabled": null,
  "schedule": {
    "interval": "PT5M",
    "startTime": "2026-11-01T10:26:19.289Z"
  },
  "parameters": null,
  "fieldMappings": [],
  "outputFieldMappings": [
    {
      "sourceFieldName": "/document/invoiceVector",
      "targetFieldName": "content_vector",  //name of vector field
      "mappingFunction": null
    }
  ],
  "cache": null,
  "encryptionKey": null
}
```

Whole Document Indexer JSON Configuration

Running the index on this produced fairly reasonable results, being able to semantically understand search queries to my eye. But I thought the search score and quality could be improved through cleaning up what was being stored in the embeddings because I thought that the extra text in the document was outside the scope of what I was interested in searching, which was product information and related data around it. 

## Azure AI Search Indexer Configuration for Selective Data

Now that the skillset is ready to be utilised to extract the data we want, we can specify it as a defined skillset in the new indexer as the following, noting the declaration of the skillset, the allowance for the skillset to read File Data, and the output Mappings to the right field pointing to the vector field:

```json
{
  "@odata.context": "[my_azure_ai_searchEndpoint]/$metadata#indexers/$entity",
  "@odata.etag": "\"0x8DF38C9CE6DF6C4\"",
  "name": "indexer-new",
  "description": null,
  "dataSourceName": "azureblob-1793527132374-datasource", // same datasource
  "skillsetName": "invoice-products-skillset", //skillset just for this data
  "targetIndexName": "newdocumentindex", //new index, same data
  "disabled": null,
  "schedule": {
    "interval": "PT5M",
    "startTime": "2026-11-01T18:47:28.388Z"
  },
  "parameters": {
    "batchSize": 10,
    "maxFailedItems": null,
    "maxFailedItemsPerBatch": null,
    "configuration": {
      "dataToExtract": "contentAndMetadata",
      "allowSkillsetToReadFileData": true // allow raw file data to be exposed
    }
  },
  "fieldMappings": [],
  "outputFieldMappings": [
    {
      "sourceFieldName": "/document/products_vector", 
      "targetFieldName": "products_vector", //output into vector field
      "mappingFunction": null
    }
  ],
  "cache": null,
  "encryptionKey": null
}
```

Indexer JSON for chunked data

## Azure AI Search Skillset Chaining for Enrichment Tree

Now the new question what if the product descriptions could be vectorised excluding all other document details to provide a higher resolution and cleaner embedding vector into the vector field. I created a new Index in the Azure AI Search for the purposes of using a product-description-only vector field to see if results would be better

To be able to do that, I had to think about extracting just the product descriptions from all the documents and be able to parse these through as strings into the Embedding Model in Microsoft Foundry.

And so this process required the following considerations:

- An instance of Azure Document Intelligence to scan and extract text from PDFs on the S0 tier because F0 tier only offers up to 500 page free per month, I was working with 1000 docs in this case. This is configured to use the Prebuilt-invoice layout
- An HTTP Triggered Azure Function for receiving the PDF documents as payloads , calling into Document Intelligence to extract the product descriptions as structured and predictable text
- An Enrichment Tree that consolidates a Custom Web API Skill, the Azure Function and the AzureOpenAI Embedding Skill to create the embeddings

Having a look at the Enrichment Tree:

```json
{
  "@odata.etag": "\"0x8DF38C8E740FBFC\"",
  "name": "invoice-products-skillset",
  "skills": [
    {
      "@odata.type": "#Microsoft.Skills.Custom.WebApiSkill",
      "name": "extract-products",
      "context": "/document",
      "uri": "https://[function_endpoint]/api/products/extract?code=<redacted>",
      "httpMethod": "POST",
      "timeout": "PT3M50S", //give each request a timeout of 3m50s to process
      "batchSize": 1,
      "inputs": [
        {
          "name": "filedata",
          "source": "/document/file_data", //file_data exposes the raw data here
          "inputs": []
        }
      ],
      "outputs": [
        {
          "name": "productDescriptionsText", output the reposnse from Function as this name here
          "targetName": "productDescriptionsText"
        }
      ],
      "httpHeaders": {}
    },
    {
      "@odata.type": "#Microsoft.Skills.Text.AzureOpenAIEmbeddingSkill",
      "name": "embed-products",
      "context": "/document",
      "resourceUri": "[microsoft_foundry_embedding_endpoint]",
      "apiKey": "<redacted>",
      "deploymentId": "text-embedding-3-small-batsiinvoices",
      "dimensions": 1536,
      "modelName": "text-embedding-3-small",
      "inputs": [
        {
          "name": "text",
          "source": "/document/productDescriptionsText", //use input to embedding model from output of Function 
          "inputs": []
        }
      ],
      "outputs": [
        {
          "name": "embedding",
          "targetName": "products_vector" // output of 2 skills as vector values for index
        }
      ]
    }
  ]
}
```

Azure Function to extract PDF product descriptions using Document Intelligence

Then the Azure Function comes into view. It's role is to take in a PDF input within the request and then call Document Intelligence that uses prebuilt-layout for invoices to extract the text in the PDF. This prebuilt layout means the DI instance is better prepared to segment and extract the data we need. It produces an output that is shape-ready for the Azure skillset to carry forward (more on this later). But first let's look at the Function:

```csharp
using System.Net;
using System.Text.Json;
using Azure;
using Azure.AI.DocumentIntelligence;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;

////
 
private readonly DocumentIntelligenceClient _client;
private readonly ILogger<ExtractProductsFunction> _logger;

 public ExtractProductsFunction(
     ILogger<ExtractProductsFunction> logger)
 {
     _logger = logger;

     var endpoint = "[document_intelligence_endpoint]";

     var key = "[document_intelligence_key]";

     _client = new DocumentIntelligenceClient(
         new Uri(endpoint!),
         new AzureKeyCredential(key!));
 }
 
 [Function("ExtractProducts")]
 public async Task<HttpResponseData> Run(
 [HttpTrigger(AuthorizationLevel.Function, "post", Route = "products/extract")]
 HttpRequestData req)
 {
     using var json = await JsonDocument.ParseAsync(req.Body);
     var results = new List<object>();

     foreach (var record in json.RootElement.GetProperty("values").EnumerateArray())
     {
         string recordId = record.GetProperty("recordId").GetString()!;

         try
         {
             // Skill input "filedata" (source /document/file_data) arrives as
             // { "$type": "file", "data": "<base64>" }.
             string base64Pdf = record
                 .GetProperty("data")
                 .GetProperty("filedata")
                 .GetProperty("data")
                 .GetString()!;

             byte[] pdfBytes = Convert.FromBase64String(base64Pdf);

             string text = await ExtractProductTextAsync(pdfBytes);

             results.Add(new
             {
                 recordId,
                 data = new { productDescriptionsText = text },
                 errors = Array.Empty<object>(), 
                 warnings = Array.Empty<object>()
             });
         }
         catch (Exception ex)
         {
             _logger.LogError(ex, "Failed to extract product descriptions for {RecordId}.", recordId);

             // Per-record error so the indexer reports the real cause.
             results.Add(new
             {
                 recordId,
                 data = new { },
                 errors = new[] { new { message = ex.Message } },
                 warnings = Array.Empty<object>()
             });
         }
     }

     var response = req.CreateResponse(HttpStatusCode.OK);
     response.Headers.Add("Content-Type", "application/json");
     await response.WriteStringAsync(JsonSerializer.Serialize(new { values = results }));
     return response;
 }

 private async Task<string> ExtractProductTextAsync(byte[] pdfBytes)
 {
    //using Document Intelligence prebuild invoice layout
     var operation = await _client.AnalyzeDocumentAsync(
         WaitUntil.Completed,
         "prebuilt-layout",
         BinaryData.FromBytes(pdfBytes));

     var products = new List<string>();

     foreach (var table in operation.Value.Tables ?? [])
     {
         var header = table.Cells.FirstOrDefault(cell =>
                          cell.Kind == DocumentTableCellKind.ColumnHeader &&
                          cell.Content.Trim().Equals("Description", StringComparison.OrdinalIgnoreCase))
                      ?? table.Cells.FirstOrDefault(cell =>
                          cell.RowIndex == 0 &&
                          cell.Content.Trim().Equals("Description", StringComparison.OrdinalIgnoreCase));

         if (header == null)
             continue;

         products.AddRange(table.Cells
             .Where(cell =>
                 cell.ColumnIndex == header.ColumnIndex &&
                 cell.RowIndex > header.RowIndex &&
                 cell.Kind != DocumentTableCellKind.ColumnHeader)
             .OrderBy(cell => cell.RowIndex)
             .Select(cell => cell.Content.Trim())
             .Where(value => !string.IsNullOrWhiteSpace(value)));
     }

     var distinct = products.Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
     return distinct.Length == 0 ? string.Empty : $"{string.Join(". ", distinct)}.";
 }
```

Extract Product descriptions with Document Intelligence - Co-authored with Claude and ChatGPT

And so the output shape of this function always come out as this example, ready for Azure OpenAI Embedding Skill as seen in my Enrichment Tree (Skillset Sequence) : 

```json
{
  "values": [
    {
      "recordId": "1",
      "data": {
        "productDescriptionsText": "Electric wine bottle opener\nVintage crystal wine glasses\nWine rack"
      }
    }
  ]
}
```

Output ready for consumption by the Azure OpenAI Embedding Skill

Finally as intended, the Azure OpenAI Embedding Skill consumes the Azure Function output and creates the embeddings.

## Comparing the Azure AI Search Indexes

I decided to take the 2 indexes for a spin and see how they performed on the same data using the term 'house decoration' and taking into the account the highest ranking result:

![](https://storage.ghost.io/c/23/ff/23ffb93f-40bb-4405-983a-69cca9fd1421/content/images/2026/11/image-1.png)

Original Index with whole document embedding vectors

The document invoice\_909\_color\_B\_250.pdf had this to say about "house decorations" with 59% confidence when using whole document embeddings:

![](https://storage.ghost.io/c/23/ff/23ffb93f-40bb-4405-983a-69cca9fd1421/content/images/2026/11/image-3.png)

PDF contents results for "house decorations" when using whole document embeddings with 59% confidence

Next up, the product-description-only embeddings index for the same search term "house decorations" : 

![](https://storage.ghost.io/c/23/ff/23ffb93f-40bb-4405-983a-69cca9fd1421/content/images/2026/11/image-2.png)

Separate index with product description-only embeddings 

The top result invoice\_890\_color\_B\_253.pdf had this to say about the search query:

![](https://storage.ghost.io/c/23/ff/23ffb93f-40bb-4405-983a-69cca9fd1421/content/images/2026/11/image-4.png)

PDF contents results for "house decorations" when using whole segment only embeddings with 64% confidence

What this shows me at least for now is that going finer on specific sections within the documents will help the index perform with more certainty and confidence when searching semantically across the data. My inference is that the results would have even lower confidence score on whole document embeddings especially when documents include undesirable text such as legal disclaimers or other technical notes that are not related to the search scope