← Back to Tutorials Chapter 16

Azure AI Search: Indexing and Queries

Azure AI Search Overview

Azure AI Search (formerly Azure Cognitive Search) is a cloud-based search-as-a-service solution that delivers full-text search, faceted navigation, and AI-powered enrichment over your content. It acts as a search index that sits on top of your data stores — you push content into it or pull it from supported data sources, and it returns ranked search results with millisecond latency. The service is designed for building search experiences over heterogeneous content, including documents, databases, and blob storage.

Azure AI Search is a key topic in the AI-102 exam under the "Knowledge Mining" domain. You will be tested on index design, data source connections, indexer configuration, query syntax, and how to apply relevance tuning. The service competes with Elasticsearch and Algolia in the broader search market, but its deep integration with Azure data services and its AI enrichment pipeline give it a unique position in the Azure ecosystem.

The service is organized around four core components: search indexes (the schema that defines what fields can be searched, filtered, or sorted), data sources (connections to your content stores), indexers (automatic data import pipelines), and queries (the requests that retrieve and rank results). Understanding how these components interact is essential for both the exam and real-world knowledge mining solutions.

Provisioning a Search Resource

You create an Azure AI Search resource through the Azure portal, CLI, or ARM templates. During provisioning you select a pricing tier that determines storage limits, index count, query throughput, and available features. The Free (F) tier is limited to 50 MB of storage, 3 indexes, and 15,000 documents. It is suitable for prototyping and small demos but not for production workloads. The Basic (B) tier offers 2 GB of storage, 15 indexes, and supports higher query volumes with a shared replica. The Standard (S1, S2, S3) tiers scale from 25 GB up to 1.4 TB per partition, with support for multiple replicas and partitions for high availability and throughput. The Storage Optimized (L1, L2) tiers prioritize large document volumes over query throughput, offering up to 24 TB per partition.

TierStorageMax IndexesReplicasUse Case
Free (F)50 MB31 (shared)Prototyping and demos
Basic (B)2 GB153Small production workloads
Standard S125 GB5012General purpose production
Standard S2100 GB5012Large-scale production
Standard S31.4 TB5012Massive scale workloads
Storage Opt L11 TB106Document-heavy workloads
Storage Opt L224 TB106Extreme document volumes

Scalability is managed through replicas and partitions. Replicas are copies of the search service that handle query load — adding replicas improves query throughput and provides high availability. Partitions divide the index across multiple storage units, allowing larger indexes and faster indexing throughput. For production, Microsoft recommends at least two replicas for SLA coverage (99.9% for Standard tiers) and two partitions for indexes larger than 50 GB.

Data Sources

A data source tells Azure AI Search where your content lives and how to connect to it. The supported data source types include Azure SQL Database, Azure Cosmos DB (SQL, MongoDB, and Gremlin APIs), Azure Blob Storage, Azure Table Storage, Azure Files, and Azure Data Lake Storage Gen2. Each data source is configured with a connection string, authentication credentials (key-based, managed identity, or SQL authentication), and optional container or query parameters. For Cosmos DB, you specify a collection and optionally a query that filters or projects the documents to index. For Blob Storage, you specify a container and optionally a file extension filter to limit which blobs are indexed.

// Creating a data source via REST API
POST https://{search-service}.search.windows.net/datasources?api-version=2024-07-01
Content-Type: application/json
api-key: {admin-key}

{
  "name": "azure-sql-products",
  "type": "azuresql",
  "credentials": {
    "connectionString": "Server=tcp:myserver.database.windows.net;Database=myproductsdb;User ID=myuser;Password=mypass;Trusted_Connection=False;Encrypt=True;"
  },
  "container": {
    "name": "dbo.Products"
  }
}

When using managed identity authentication, you enable a system-assigned or user-assigned managed identity on the search service and grant it read access to the data source. This avoids embedding connection strings in the data source configuration and is the recommended approach for production. You can create a data source through the Azure portal, REST API, Azure SDK, or the Azure CLI.

Search Indexes: Fields and Attributes

A search index is the core schema that defines the structure of your searchable content. Each field in the index has a name, a data type (Edm.String, Edm.Int32, Edm.Double, Edm.Boolean, Edm.DateTimeOffset, Edm.GeographyPoint, or Collection types), and a set of attributes that control how the field participates in search queries. The five key attributes are searchable, filterable, sortable, facetable, and retrievable.

AttributeDescriptionUsed In
searchableFull-text searchable via Lucene analysisKeyword and phrase searches
filterableCan be referenced in $filter expressionsExact match filtering
sortableCan be used as a sort criterionOrdering results by field
facetableCan be used in faceted navigationCategory drill-down UI
retrievableReturned in search resultsField selection in responses

Fields that are searchable are analyzed through a Lucene analyzer that tokenizes the text, applies stemming and stop-word removal, and builds an inverted index for fast lookups. Non-searchable fields are stored as-is and can only be used for filtering, sorting, or faceting. Every index must have exactly one key field of type Edm.String that uniquely identifies each document. Complex types allow nested data structures, such as an "address" object containing street, city, and postalCode subfields.

{
  "name": "product-index",
  "fields": [
    { "name": "id", "type": "Edm.String", "key": true, "retrievable": true },
    { "name": "name", "type": "Edm.String", "searchable": true, "sortable": true, "retrievable": true },
    { "name": "description", "type": "Edm.String", "searchable": true, "retrievable": false },
    { "name": "category", "type": "Edm.String", "filterable": true, "facetable": true, "retrievable": true },
    { "name": "price", "type": "Edm.Double", "filterable": true, "sortable": true, "facetable": true, "retrievable": true },
    { "name": "rating", "type": "Edm.Double", "sortable": true, "filterable": true, "retrievable": true },
    { "name": "tags", "type": "Collection(Edm.String)", "filterable": true, "facetable": true, "retrievable": true },
    { "name": "lastUpdated", "type": "Edm.DateTimeOffset", "sortable": true, "filterable": true, "retrievable": true }
  ]
}

Creating an Index via Portal, REST, and SDK

You can create an index through the Azure portal by navigating to your search service and selecting "Add index" from the Overview blade. The portal provides a form-driven interface where you define field names, types, and attributes. For programmatic access, the REST API offers full control over index definition, as shown in the following Python example using the Azure SDK for Python:

from azure.core.credentials import AzureKeyCredential
from azure.search.documents.indexes import SearchIndexClient
from azure.search.documents.indexes.models import (
    SearchIndex, SearchField, SearchFieldDataType,
    SimpleField, SearchableField
)

endpoint = "https://mysearch.search.windows.net"
admin_key = "YOUR_ADMIN_KEY"

client = SearchIndexClient(endpoint, AzureKeyCredential(admin_key))

fields = [
    SimpleField(name="id", type=SearchFieldDataType.String, key=True),
    SearchableField(name="name", type=SearchFieldDataType.String, sortable=True),
    SearchableField(name="description", type=SearchFieldDataType.String),
    SimpleField(name="category", type=SearchFieldDataType.String,
                filterable=True, facetable=True),
    SimpleField(name="price", type=SearchFieldDataType.Double,
                filterable=True, sortable=True, facetable=True),
    SimpleField(name="rating", type=SearchFieldDataType.Double,
                filterable=True, sortable=True),
]

index = SearchIndex(name="product-index", fields=fields)
result = client.create_index(index)
print(f"Index '{result.name}' created successfully")
// C# - creating an index with Azure SDK for .NET
using Azure;
using Azure.Search.Documents.Indexes;
using Azure.Search.Documents.Indexes.Models;

var endpoint = "https://mysearch.search.windows.net";
var credential = new AzureKeyCredential("YOUR_ADMIN_KEY");

var client = new SearchIndexClient(new Uri(endpoint), credential);

var index = new SearchIndex("product-index")
{
    Fields =
    {
        new SimpleField("id", SearchFieldDataType.String) { IsKey = true },
        new SearchableField("name") { IsSortable = true },
        new SearchableField("description"),
        new SimpleField("category", SearchFieldDataType.String)
            { IsFilterable = true, IsFacetable = true },
        new SimpleField("price", SearchFieldDataType.Double)
            { IsFilterable = true, IsSortable = true, IsFacetable = true },
        new SimpleField("rating", SearchFieldDataType.Double)
            { IsFilterable = true, IsSortable = true }
    }
};

client.CreateIndex(index);
Console.WriteLine("Index created successfully");

When creating indexes, you cannot add new searchable fields or change field attributes after the index is populated. To modify the schema of an existing index with data, you must either create a new index and rebuild it, or add new fields with the "allowIndexingOnNewFields" flag set on the index. Plan your schema carefully before indexing production data.

Indexers: Automated Data Import

An indexer connects a data source to an index and automates the document ingestion process. It handles field mapping (mapping source fields to index fields), scheduled refresh (incremental indexing), and error handling. When you create an indexer, you specify the data source name, target index name, optional field mappings, a schedule interval (such as "every 5 hours"), and how to handle errors. Indexers support two execution modes: on-demand (run once) and scheduled (recurring).

Change tracking is critical for incremental indexing. Azure SQL Database and Cosmos DB support change detection through the built-in change tracking features (SQL's integrated change tracking or Cosmos DB's change feed). Blob Storage uses last-modified timestamps to detect new or updated blobs. When change tracking is not available, the indexer falls back to full re-indexing, which can be expensive for large datasets. For Azure SQL, you must enable change tracking on both the database and the table before the indexer can use it.

// Creating an indexer via REST API
POST https://{search-service}.search.windows.net/indexers?api-version=2024-07-01
Content-Type: application/json
api-key: {admin-key}

{
  "name": "products-indexer",
  "dataSourceName": "azure-sql-products",
  "targetIndexName": "product-index",
  "schedule": {
    "interval": "PT2H"
  },
  "fieldMappings": [
    { "sourceFieldName": "ProductID", "targetFieldName": "id" },
    { "sourceFieldName": "ProductName", "targetFieldName": "name" },
    { "sourceFieldName": "Description", "targetFieldName": "description" }
  ],
  "parameters": {
    "batchSize": 100,
    "maxFailedItems": 10,
    "maxFailedItemsPerBatch": 5
  }
}

Query Syntax: Simple vs. Full Lucene

Azure AI Search supports two query parser modes: simple (default) and full Lucene. The simple syntax supports basic query operators — AND, OR, NOT, + (required), - (excluded), and quoted phrases for exact matching. A simple query like "wireless mouse" +Bluetooth -wired searches for documents containing the phrase "wireless mouse," that must include "Bluetooth," and must not include "wired."

The full Lucene syntax adds advanced operators — ~ for fuzzy search (e.g., bluetooth~ matches "bluetooth," "bluetoot," "bluetch"), ^ for field boosting (e.g., title:laptop^3 boosts matches in the title field by 3x), * and ? for wildcard search (e.g., comput* matches "computer," "computing," "computation"), ~n for proximity search (e.g., "laptop battery"~5 matches documents where the words are within 5 terms of each other), and regular expressions enclosed in /. To use full Lucene syntax, set the queryType parameter to full.

Exam Tip: Simple vs. Full Lucene

The AI-102 exam tests when to use each query type. Use simple syntax for standard keyword searches and metadata filtering — it is faster and sufficient for most e-commerce and content search scenarios. Use full Lucene when you need fuzzy matching for typo-tolerant search, proximity search for phrase matching with word gaps, regex for pattern matching, or field boosting for custom relevance ranking. Remember that full Lucene queries are more resource-intensive and slower, so only use them when the query complexity is justified by the search experience requirements.

Filtering, Sorting, Faceting, and Wildcard Queries

Filters are expressed using the OData $filter parameter with standard comparison operators (eq, ne, gt, lt, ge, le), logical operators (and, or, not), and collection operators (in, any, all). A filter narrows the search space before scoring, which makes it very efficient. For example, $filter=category eq 'Electronics' and price le 1000 restricts results to electronics under $1,000. Sorting is done with $orderby$orderby=rating desc, price asc sorts by rating descending then price ascending. Facets provide aggregated counts for drill-down navigation. A faceted query returns the count of documents in each category — facet=category,count:100 returns up to 100 category values with their document counts. Wildcard queries use * for multi-character and ? for single-character matching, and require the full Lucene query parser.

# Python - full query with filtering, faceting, and sorting
from azure.search.documents import SearchClient
from azure.core.credentials import AzureKeyCredential

client = SearchClient(
    endpoint="https://mysearch.search.windows.net",
    index_name="product-index",
    credential=AzureKeyCredential("YOUR_QUERY_KEY")
)

results = client.search(
    search_text="laptop",
    filter="category eq 'Electronics' and price le 2000",
    order_by=["rating desc", "price asc"],
    facets=["category", "brand"],
    select=["name", "price", "rating", "category"],
    top=10,
    query_type="simple"
)

print(f"Total results: {results.get_count()}")
print("Facets:")
for facet_name, facet_values in results.get_facets().items():
    for value in facet_values:
        print(f"  {value['value']}: {value['count']}")

for doc in results:
    print(f"{doc['name']} - ${doc['price']} - Rating: {doc['rating']}")

Scoring Profiles for Relevance Tuning

Scoring profiles allow you to customize the relevance ranking of search results. By default, Azure AI Search computes a score for each matching document based on term frequency-inverse document frequency (TF-IDF) and the Okapi BM25 algorithm. A scoring profile lets you override this by adding weighted field boosts, magnitude-based functions (boost by recency or numeric value), freshness functions (boost newer documents), tag boosting (boost documents matching specific tags), and distance functions (boost by geographic proximity).

For example, a product search might use a scoring profile that boosts the title field by 5x, boosts recently updated products, and boosts products in the "Featured" category by 2x. Scoring profiles are defined in the index schema and referenced by name in queries. If no scoring profile is specified, the default BM25 scoring is used. You can also override scoring parameters at query time using the scoringParameter option to pass tag values or parameter values to the scoring function.

{
  "name": "product-scoring",
  "text": {
    "weights": {
      "name": 5,
      "description": 2,
      "tags": 3
    }
  },
  "functions": [
    {
      "type": "freshness",
      "fieldName": "lastUpdated",
      "boost": 2,
      "interpolation": "linear",
      "freshness": {
        "boostingDuration": "P30D"
      }
    },
    {
      "type": "magnitude",
      "fieldName": "rating",
      "boost": 1.5,
      "interpolation": "constant",
      "magnitude": {
        "boostingRangeStart": 3,
        "boostingRangeEnd": 5
      }
    }
  ]
}

Exercise: Build a Product Search Index

In this exercise you will provision an Azure AI Search service, create an index, index sample product data, and run queries with filtering and faceting.

  1. Create an Azure AI Search resource at the Basic tier in the Azure portal.
  2. Write a Python script that creates an index named "products" with fields for id, name, description, category, price, rating, tags, and lastUpdated. Set appropriate attributes: searchable on name and description, filterable and facetable on category and tags, sortable on price and rating.
  3. Index at least 20 sample product documents spanning 3-4 categories (Electronics, Home, Clothing, Sports). Use the SearchClient.upload_documents() method.
  4. Run the following queries and print the results:
    • Search for "wireless" in Electronics under $150, sorted by rating descending
    • Same query with a facet on category to see counts per category
    • Use a filter to show only products with rating >= 4
    • Use full Lucene syntax with a fuzzy search term (e.g., "laptopp~")
  5. Define a scoring profile that boosts the name field by 3x and applies a freshness boost for items updated within the last 30 days. Compare the result ordering with and without the scoring profile.

Exam Tips: Index Design and Query Performance

Several index design strategies appear frequently on the exam. Use searchable attributes only on fields that genuinely need full-text search — each searchable field adds to the index size and query latency. Make fields filterable instead of searchable when you only need exact matches. Use facets for categorical drill-down rather than boolean filters because facets provide user-facing navigation counts. When performance is critical, limit the number of searchable fields and use the $select parameter to return only the fields the client needs. Use the query key (read-only key) for client-side queries and reserve the admin key for index management operations. For large datasets, prefer push-based indexing (direct document upload) over pull-based indexing (indexers) when you need real-time freshness, because indexers have a minimum scheduling interval of 5 minutes and each run incurs overhead.

Summary

Azure AI Search provides a managed search service with configurable indexes, automated indexers for popular Azure data sources, and flexible query syntax supporting simple and full Lucene modes. Index schema design determines what queries are possible — field attributes control searchability, filterability, sortability, and faceting. Indexers automate data import and support incremental refresh through change tracking. Scoring profiles fine-tune relevance ranking beyond the default BM25 algorithm. Queries support filtering (OData), sorting, faceted navigation, and advanced Lucene features like fuzzy, proximity, and wildcard search. On the AI-102 exam, focus on index design best practices, choosing between simple and full query syntax, and understanding when filters are more efficient than facets. The next chapter extends these concepts with AI enrichment, where cognitive skills transform and enrich content during the indexing process.