Overview
NuGet.Services.AzureSearch is the central library that implements everything related to NuGet’s Azure AI Search integration. It defines the document models for two indexes (a search index for user-facing queries and a hijack index for legacy V2 look-ups), the pipelines that populate and update those indexes, and the service layer that executes queries against them. Consuming executable jobs import this library and wire it into their own entry points, but all of the indexing and query logic lives here.
The library supports three distinct data pipelines. Db2AzureSearch performs the initial full bootstrap of both indexes by reading all package registrations from the NuGet Gallery SQL database (or optionally from Kusto), creating or replacing the Azure Search indexes and the blob-based auxiliary data files, and writing the initial cursor position so that the incremental pipeline can pick up from the right point. Catalog2AzureSearch runs continuously as a catalog collector, reading NuGet catalog commits, converting each commit into a set of Azure Search index actions, and pushing those actions in batches; it maintains a durable cursor in blob storage to track progress. Auxiliary2AzureSearch runs as a periodic job that reads owner, download-count, and verified-package data from the Gallery database and the NuGet statistics pipeline, diffs each data set against the previously indexed snapshot stored in blob storage, and pushes only the changed documents to the search index.
A fourth subsystem, the SearchService namespace, provides the runtime query path used by the NuGet search web API. It translates incoming V2, V3, and autocomplete requests into Azure Search queries, applies NuGet-specific text analysis (camel-case splitting, separator tokenization, exact-match boosting), executes queries against the appropriate index, and maps the results back to the API response shapes. Download counts, owner lists, verified status, and popularity-transfer adjustments are kept in memory via AuxiliaryDataCache and are applied at query time by SearchResponseBuilder.
Role in System
BatchPusher which queues index actions for both indexes, pushes them in configurable batches (default 1000 documents), and writes per-package-ID version list blobs to blob storage after all documents for that ID have been flushed.
Dual Azure Search Indexes
A search index holds one document per package ID per
SearchFilters combination (stable, prerelease, SemVer 2) and is used for V3 and non-hijack V2 queries. A hijack index holds one document per package ID+version and is used for legacy V2 look-ups by exact version.Three Indexing Pipelines
Db2AzureSearch bootstraps the indexes from scratch; Catalog2AzureSearch keeps them current via the NuGet catalog; Auxiliary2AzureSearch keeps download counts, owners, and verified status in sync from out-of-band data sources.
Blob-Backed Version Lists
For every package ID, a JSON blob in Azure Storage records the known versions and their properties. The
VersionLists class uses this data to compute which version is the latest under each SearchFilters combination and to determine exactly which search index documents need to change when a version is added, updated, or deleted.Auxiliary Data Cache
At query time the search service holds download counts, verified-package flags, and popularity-transfer data in memory via
AuxiliaryDataCache. This avoids hitting storage on every query and is refreshed periodically by a background reloader.Key Files and Classes
Dependencies
NuGet Package References
Internal Project References
Notable Patterns and Implementation Details
Two-index architecture. Every package version gets a document in the hijack index (one document per ID+version), while the search index holds one document per package ID per
SearchFilters combination. The hijack index exists exclusively to support legacy V2 queries that ask for a specific version; the search index supports all other query types. AzureSearchService routes requests to the correct index based on the IgnoreFilter flag on the V2 request.Version list blobs as shared state. Because a single package ID can map to up to four search-index documents (one per
SearchFilters), the indexing pipeline needs to know the complete ordered version list to compute which document represents the “latest” version for each filter. This state is kept as a JSON blob per package ID in Azure Blob Storage and read/written by VersionListDataClient using ETag-based optimistic concurrency. VersionLists.ApplyChanges recomputes which index documents need to change for both indexes whenever a version is added, updated, or deleted.Batch splitting on HTTP 413.
BatchPusher.IndexAsync catches RequestFailedException with status 413 (Request Entity Too Large) and recursively splits the batch in half, retrying each half independently. This handles cases where document payloads (e.g. packages with very long version lists or descriptions) make a full batch exceed Azure Search’s request size limit.Download score pre-computation. Rather than sorting by raw download count (which would require a large boosting range in the scoring profile),
DocumentUtilities.GetDownloadScore computes a normalized log-scale score at index time. The DefaultScoringProfile applies a MagnitudeScoringFunction against this DownloadScore field with a configurable DownloadScoreBoost multiplier, keeping the effective boost range manageable while still heavily favouring popular packages.