Skip to main content

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

Each indexing pipeline shares a common 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.
Popularity transfers. The download-count update pipeline supports a feature where downloads from deprecated or renamed packages can be “transferred” to their successors. DownloadTransferrer applies a configurable transfer percentage so the successor’s effective download count is boosted. This is controlled by both a configuration flag (EnablePopularityTransfers) and a feature flag (IFeatureFlagService.IsPopularityTransferEnabled), allowing it to be toggled without redeployment.
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.
Kusto vs SQL bootstrap. During initial indexing (Db2AzureSearch), the library can read package data from either the NuGet Gallery SQL database or from Azure Data Explorer (Kusto). The choice is made at startup time in DependencyInjectionExtensions: if KustoConnectionString is set in configuration, NewPackageRegistrationFromKustoProducer is used; otherwise NewPackageRegistrationFromDbProducer reads from SQL. This allows faster bootstrap runs by querying Kusto snapshots.
Access-condition concurrency on version lists. When BatchPusher flushes a batch and writes version list blobs, it uses the ETag captured when the blob was originally read. If another process has modified the same blob in the interim, the write fails and the package ID is added to FailedPackageIds. AzureSearchCollectorLogic and UpdateDownloadsCommand both implement retry loops (up to three attempts) to handle this case, re-fetching the latest version list state before retrying.
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.