Recursive AI

Rebuild a search index in C#

OneIndex is a free, open-source search library for .NET from Recursive AI. MIT or Apache-2.0, preview, runs in-process.

Rebuild searchable records in a new index directory. OneIndex does not import another engine’s index files.

Terminal
dotnet add package RecursiveAI.OneIndex --prerelease

Requires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.

C#
using OneIndex;
using var index =
    SearchIndex.Open<Product>("new-index");
index.Upsert(new Product
    { Id = "p1",
      Title = "Wireless headphones" });
index.Commit();
foreach (var hit in index.Search("wireless"))
    Console.WriteLine(hit.Id);
public class Product
{
    [SearchId]
    public string Id { get; set; } = "";
    [FullText]
    public string Title { get; set; } = "";
}
Text
p1

Prints p1 after importing one product.

Unzip the example, then dotnet run.

The full program

This example uses preview 0.4.0-preview.2; APIs and file formats may change between previews.

The full program imports four source records into products-index. These in-code records stand in for your database rows or exported stored values. It searches two words, filters category and price, sorts by price, and prints highlighted titles and facet counts.

C#
using OneIndex;
using var index = SearchIndex.Open<Product>("products-index");
index.Upsert(
[
    new() { Id = "p1", Title = "Wireless headphones", Price = 89, Category = "audio",
            Body = "Over-ear wireless headphones with noise cancelling." },
    new() { Id = "p2", Title = "Wireless earbuds", Price = 59, Category = "audio",
            Body = "In-ear headphones, wireless, with a charging case." },
    new() { Id = "p3", Title = "Studio headphones", Price = 149, Category = "audio",
            Body = "Wired headphones for mixing." },
    new() { Id = "p4", Title = "Wireless mouse", Price = 25, Category = "computers",
            Body = "A wireless mouse with a USB receiver." },
]);
index.Commit();
 
var results = index.Search("wireless headphones")
    .Where(p => p.Category == "audio" && p.Price < 100)
    .OrderBy(p => p.Price)
    .Facets(p => p.Category)
    .Take(10)
    .Execute();
foreach (var hit in results)
{
    var title = hit.Highlights.TryGetValue("Title", out var f)
        ? f[0].Marked() : hit.Document.Title;
    Console.WriteLine($"{hit.Id}  {hit.Document.Price,4}  {title}");
}
foreach (var v in results.Facets["Category"].Values)
    Console.WriteLine($"{v.Value} = {v.Count}");
 
public class Product
{
    [SearchId]
    public string Id { get; set; } = "";
    [FullText(Boost = 2)]
    public string Title { get; set; } = "";
    [FullText(Language = "en")]
    public string Body { get; set; } = "";
    [Filterable, Sortable]
    public double Price { get; set; }
    [Keyword, Facet]
    public string Category { get; set; } = "";
}

On the first run, dotnet run prints:

Text
p2    59  <em>Wireless</em> earbuds
p1    89  <em>Wireless</em> <em>headphones</em>
audio = 2

A second writer on the same directory throws IndexLockedException; import into a separate new directory.

Variations

Limit the returned page. Replace the loop in the snippet at the top with:

C#
foreach (var hit in index.Search("wireless").Take(1))
    Console.WriteLine(hit.Id);
Text
p1

Full-text search · Vector search · Hybrid search · EF Core · Folder search


The short answer

Rebuild a OneIndex search index from your source documents, not by copying another engine’s segment files. Define a class with a stable [SearchId], mark its searchable and filterable fields, upsert the records into a new directory and commit. The snippet at the top rebuilds one record; the full program indexes a small product catalog with price filtering, sorting, facets and highlighted titles.

This is a data-and-query migration, not an API compatibility layer. Keep the existing search available while you build and check the new index. OneIndex is a preview, so keep the original source documents available for future rebuilds too. The mapping documentation explains how the class becomes a schema.

How do I migrate a search index in C#?

Begin with the source of truth: database rows, files or an export of stored document values. Assign stable IDs so a repeated import replaces documents rather than creating duplicates. A search index is a derived representation of that data; tokens and postings alone are not a substitute for the original records.

The sample’s Product class declares a boosted full-text title, an English-analyzed body, numeric price filtering and sorting, and a keyword category with facet counts. Those declarations are the migration plan for this catalog. Decide what every old field means before importing it: prose, exact code, numeric filter, display-only source or sort key.

Open a new directory and import into it. Do not point OneIndex at another engine’s index files or try to rename their extensions. This preview does not ship a general index-format importer. The short example supplies the record directly in code so the migration step has no hidden export file; in a real application, that record comes from your existing data source.

How do I preserve search behavior when rebuilding an index?

Write down representative queries and the expected business behavior before changing the implementation. In the product example, “wireless headphones” must match both terms, category must be audio, price must be below 100, and the results must be cheapest first. Those are distinct requirements, expressed by Search, Where and OrderBy rather than a single opaque query string.

OneIndex’s user-text search requires every word unless joined by OR. It supports quoted phrases, exclusions, a last-word prefix and typo tolerance. That behavior might differ from an existing search box. Use the strict parser only when the interface deliberately accepts its field syntax; it is not the default search-box contract.

Text analysis and scoring also deserve separate checks. OneIndex uses BM25F and its English analysis uses stemming. An old ranking score is not a scale to preserve numerically. Compare which documents appear and in what order, including ties, phrases, inflected words and missing terms. See analysis and scoring.

Can I rebuild a search index without losing updates?

The index API supplies replacement-by-ID and committed visibility; coordinating a live migration still belongs to the application. Take a consistent source snapshot, retain changes made during the import, and apply those changes before switching reads to the new directory. A one-time loop over records is not by itself a live change-capture mechanism.

Commit publishes the imported changes for searches. Only one writer can own an index directory, so do not run two independent importers against the same path. Use separate directories for an old and a new index, and keep rollback possible until the new read path has been checked. See processes for writer ownership.

Facet totals and pagination can also expose unnoticed differences. The full program returns category counts with its filtered query. The default hit total is a lower bound unless CountExactly() is requested. If the old UI promised an exact count, make that an explicit requirement instead of comparing a lower bound with an exact value.

All oneindex recipes