D4S.Indexer.Domain 1.0.29

dotnet add package D4S.Indexer.Domain --version 1.0.29
                    
NuGet\Install-Package D4S.Indexer.Domain -Version 1.0.29
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="D4S.Indexer.Domain" Version="1.0.29" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="D4S.Indexer.Domain" Version="1.0.29" />
                    
Directory.Packages.props
<PackageReference Include="D4S.Indexer.Domain" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add D4S.Indexer.Domain --version 1.0.29
                    
#r "nuget: D4S.Indexer.Domain, 1.0.29"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package D4S.Indexer.Domain@1.0.29
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=D4S.Indexer.Domain&version=1.0.29
                    
Install as a Cake Addin
#tool nuget:?package=D4S.Indexer.Domain&version=1.0.29
                    
Install as a Cake Tool

D4S.Indexer

Document indexing library for Azure AI Search: extracts text, generates vector embeddings, and uploads searchable chunks.

Quick start

var indexer = IndexerBuilder.Create("my-index")
    .WithAzureAISearch(options =>
    {
        options.Endpoint = searchEndpoint;   // Uri
        options.ApiKey = searchKey;
    })
    .WithMicrosoftFoundry(options =>
    {
        options.Endpoint = foundryEndpoint;  // Uri
        options.ApiKey = foundryKey;
        options.EmbeddingOptions.Deployment = embeddingDeployment;
        options.EmbeddingOptions.Dimensions = embeddingDimensions;
    })
    .WithLocalFiles(options => options.Path = "./documents")
    .WithFileMetadataFields()
    .WithLoggerFactory(loggerFactory)
    .Build();

var result = await indexer.IndexAsync();

WithLoggerFactory is required: pass the container's ILoggerFactory in a hosted app, or one from LoggerFactory.Create(...) in a console app.

See src/samples/ for working examples (local files, SharePoint, OCR, agentic retrieval).

Architecture

D4S.Indexer.Domain          Entities, abstractions (interfaces)
D4S.Indexer.Application     Orchestration (DocumentIndexerService, DocumentExtractor)
D4S.Indexer.Infrastructure  Azure implementations, builder, processors, sources
Interface Purpose
IDocumentSource Enumerates documents from a data source
IDocumentProcessor Extracts text/metadata from a document
IEmbeddingService Generates vector embeddings
ISearchIndexService Manages the index (CRUD on chunks)
ITextChunker Splits text into chunks
IOcrService / IKeywordExtractor OCR for scans / AI keyword extraction

Built-in sources: local filesystem, multi-site SharePoint (PnP Core). Built-in processors: PDF, DOCX, XLSX, PPTX, TXT/Markdown.

Indexing modes

  • Full (default): all documents fetched from every source; documents missing from the source list are deleted from the index.
  • Delta (.WithDeltaMode()): only changed/new/deleted documents are provided; deletion is driven by DocumentMetadata.DeletedDate (set it and pass null for GetContentAsync). No implicit cleanup.

Both modes compare LastModifiedDate against the index to skip unchanged documents.

Reindexing after a schema change

Change detection tracks document content. When the schema changes and the documents do not — a new custom field, a different BaseUrl, a newly added ConfigureMetadata transform — every document still looks untouched, so the run skips them all and reports N skipped, 0 errors while the new field stays empty on every chunk.

Use .ForceReindex() for the run that follows such a change:

IndexerBuilder.Create("my-index")
    // …
    .AddCustomField("CiteAs", CustomFieldType.String, filterable: true)
    .ConfigureMetadata(meta => meta with { CustomFields = LookupCitation(meta) })
    .ForceReindex()          // re-extract everything once, into the existing index
    .Build();

Chunks are replaced document by document, so the index stays queryable throughout and the knowledge base/knowledge source on top of it are untouched. A field that has to be removed or retyped still needs the index itself to be recreated, which Azure only allows after the knowledge base and knowledge source referencing it are deleted, in that order.

A document is only cleared from the index once its re-extraction has actually produced chunks. If the extraction yields no text (a scan with OCR disabled) or fails, the previously indexed chunks are left in place and the document is counted under No Text or Errors — stale content is a better outcome than a document silently vanishing from a corpus-wide reindex.

Driving the flag from configuration

ForceReindex is a one-off: you want it on for the run after a schema change and off afterwards. Pass the value in rather than calling .ForceReindex() unconditionally, so switching it means changing a setting and restarting instead of editing code:

// appsettings.json / environment (Indexer__ForceReindex=true)
.ForceReindex(configuration.GetValue<bool>("Indexer:ForceReindex"))
// plain console app reading the environment directly (see src/samples/LocalFiles)
.ForceReindex(bool.TryParse(Environment.GetEnvironmentVariable("FORCE_REINDEX"), out var f) && f)

false is the default, so an unset variable leaves a normal run untouched. The same shape works for any of the boolean builder toggles — WithDeltaMode(value), ContinueOnError(value). The library takes no dependency on IConfiguration: every setting arrives through the builder, so reading it is the host's job.

Note that ForceReindex re-extracts and re-embeds every document, so it costs a full indexing run in OCR and embedding calls. It is not meant to be left on.

Builder options

IndexerBuilder.Create("index-name")
    // Required
    .WithAzureAISearch(opts => { opts.Endpoint = …; opts.ApiKey = …; })
    .WithMicrosoftFoundry(opts => { opts.Endpoint = …; opts.ApiKey = …;
                                    opts.EmbeddingOptions.Deployment = …;
                                    opts.EmbeddingOptions.Dimensions = …; })
    .WithLoggerFactory(loggerFactory)
    // Sources (at least one)
    .WithLocalFiles(opts => { opts.Path = …; opts.FileExtensions = […]; opts.BaseUrl = …; })
    .WithSharePointMultiSite(spOptions, contextFactory)
    .WithCustomDocumentSource(mySource)                        // instance — no DI container needed
    .WithCustomDocumentSource(loggerFactory => new MySource(…)) // factory, like the built-in sources
    .WithCustomDocumentSource<T>(serviceProvider, serviceKey)  // keyed DI registration
    // Optional
    .WithDeltaMode()
    .ForceReindex()                                            // ignore LastModifiedDate for one run
    .WithFileMetadataFields()
    .WithChunkReferences()                                     // pageNumber / sectionName fields
    .WithChunkSize(maxSize: 1000, overlap: 200)
    .WithBatchSize(50)
    .WithVectorCompression()
    .WithKeywordExtraction(maxKeywords: 10)                    // uses MicrosoftFoundry GptOptions
    .WithAzureDocumentIntelligence(opts => { … })              // OCR
    .WithCustomOcrService(customOcrService)                    // overrides Azure OCR
    .WithCustomDocumentProcessor(myProcessor)                  // instance
    .WithCustomDocumentProcessor(loggerFactory => new MyProcessor(…))  // factory
    .WithCustomDocumentProcessor<T>(serviceProvider, serviceKey)
    .ContinueOnError(true)
    .Filter(meta => meta.Extension == ".pdf")
    .ConfigureMetadata(meta => meta with { CustomFields = … })       // after extraction
    .ConfigureMetadataBeforeFilter(meta => meta with { … })          // during the scan, before Filter
    .AddCustomField("Status", CustomFieldType.String, filterable: true)
    .AddIndexFieldsFromAttributes<MyModel>()
    .Build();

Attaching your own fields to a document

ConfigureMetadata is the hook for this — you do not need a custom document source to add fields. It runs for every document from every source (local, SharePoint, custom), and whatever it puts in CustomFields is written to the matching index fields:

IndexerBuilder.Create("my-index")
    .WithLocalFiles(opts => opts.Path = "./documents")

    // 1. declare the field in the index schema
    .AddCustomField("CiteAs", CustomFieldType.String, filterable: true)

    // 2. populate it per document
    .ConfigureMetadata(meta => meta with
    {
        CustomFields = new Dictionary<string, object>
        {
            ["CiteAs"] = LookupCitation(meta.FileMetadata?.FileName)
        }
    })
    .Build();

Declared filterable fields are usable as OData filters by anything querying the index, including the filter parameter of the search tool in D4S.KnowledgeRetrieval.MCP.

Note that added fields do not change LastModifiedDate, so pair the first run with .ForceReindex().

Filtering on the fields you added

By default the transform runs after the document's content has been extracted — late enough to read values found inside the file, but after Filter has already decided. So this does not work:

.ConfigureMetadata(meta => meta with { CustomFields = Classify(meta) })
.Filter(meta => meta.CustomFields["DocType"] == "peer-reviewed")   // ← never matches

ConfigureMetadataBeforeFilter is the same transform moved ahead of the filter:

.ConfigureMetadataBeforeFilter(meta => meta with { CustomFields = Classify(meta) })
.Filter(meta => meta.CustomFields.GetValueOrDefault("DocType")?.ToString() == "peer-reviewed")

static Dictionary<string, object> Classify(DocumentMetadata meta) => new() { ["DocType"] = Lookup(meta) };

What it costs: at that point the file has not been opened, so the metadata is exactly what the source produced — file name, path, URL, extension, size, dates, plus any custom or filter fields the source supplied. Two values are not there yet: the author read from the document's properties, and the keywords from the processor or the AI extractor. A transform that needs those belongs in the default (post-extraction) stage.

Both stages can be used at once — they are independent slots, so call one method for each. The pre-filter stage runs first and what it writes is carried through to the index and visible to the later stage.

Fields meant only for filtering, never for the index, can also come from the source itself via DocumentMetadata.FilterFields — that is how the SharePoint source exposes list columns through FilterColumns.

To replace Azure Document Intelligence, pass the custom implementation directly:

services.AddSingleton<IOcrService, MyOcrService>();
var customOcrService = serviceProvider.GetRequiredService<IOcrService>();

var indexer = IndexerBuilder.Create("index-name")
    // Required services and document sources...
    .WithCustomOcrService(customOcrService)
    .Build();

The registered IOcrService takes precedence if WithAzureDocumentIntelligence(...) is also configured.

OCR is chosen per extension, not per document. Whenever an OCR service is configured, every document whose extension it declares in SupportedExtensions goes through it — for Azure Document Intelligence that is .pdf, the image formats, and .docx / .xlsx / .pptx. A corpus of text-native documents with one scan in it therefore pays OCR on all of them. Until the choice becomes per document, narrow it by running the scanned files as a separate pass in delta mode, or supply a WithCustomOcrService implementation that declares only the extensions you want routed.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on D4S.Indexer.Domain:

Package Downloads
D4S.Indexer.Application

Application services and configuration for D4S Indexer.

D4S.Indexer

D4S document indexer for Azure AI Search and RAG workflows.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.29 70 9/2/2026
1.0.28 148 7/30/2026
1.0.25 122 7/21/2026
1.0.21 146 6/25/2026
1.0.20 133 6/5/2026
1.0.19 124 6/3/2026
1.0.18 132 5/12/2026
1.0.17 130 5/8/2026
1.0.16 123 5/6/2026