Recursive AI

Getting started with OneIndex

All oneindex docs

Install OneIndex 0.4.0-preview.2 for .NET 10, index a few objects and search them, in C# with no server and no native code. OneIndex is an embedded search engine: full-text (BM25F), vector (HNSW) and hybrid search in one in-process library.

Before you use it

  • A preview. In the README’s words: “Everything in the table below works and is tested; APIs and file formats may still change between previews.” Its overview lists what exists and the known gaps.
  • Durability, as the project tests it. A commit that returns survives a crash or kill -9 of your process, and a damaged file fails the open with an error naming it instead of returning wrong results. The project’s own testing ran about 890,000 fault-injection schedules and more than 7,000 process kills with no failed open and no lost commit. Its limits: “On a power cut the index may roll back to an earlier commit, but it is never corrupt”, and the kill and fault-injection campaigns run on Linux and macOS only; Windows runs the full test suite on every change. Network and FUSE file systems are not supported. Durability.
  • Speed and relevance against Lucene.NET are measured in benchmarks and relevance, with their conditions.

Install

The packages target net10.0, so you need the .NET 10 SDK, and dotnet add package needs --prerelease or the exact version.

Terminal
dotnet new console -o OneIndexStart && cd OneIndexStart
dotnet add package RecursiveAI.OneIndex --version 0.4.0-preview.2

Nine packages share one version:

PackageWhat it is
RecursiveAI.OneIndexthe engine and the happy path; no dependencies beyond .NET
RecursiveAI.OneIndex.Modelsbundled embedder (bge-small-en-v1.5), reranker and sparse encoder on OneModel; a 134 MB package
RecursiveAI.OneIndex.Models.Multilingualthe multilingual-e5-small embedder, around 100 languages; a 112 MB package
RecursiveAI.OneIndex.Analysis.AsianJapanese, Chinese, Korean, Thai, Lao, Khmer and Myanmar segmentation
RecursiveAI.OneIndex.EntityFrameworkCorekeeps an index in step with SaveChanges
RecursiveAI.OneIndex.VectorDataa vector store for Microsoft.Extensions.VectorData
RecursiveAI.OneIndex.Files, .Files.Ocrfolder indexing with citations; OCR for scans
RecursiveAI.OneIndex.Clithe oneindex tool: index, query, static build

A first program

Put this in Program.cs and run dotnet run:

C#
using OneIndex;
 
// a directory; SearchIndex.InMemory<Invoice>() keeps it in memory
using var index = SearchIndex.Open<Invoice>("data/search");
index.Upsert(new Invoice { Id = "inv-1", Title = "Overdue notice",
                           Body = "Payment from Acme Corp is overdue.",
                           Modified = DateTime.UtcNow, Tags = ["invoice"] });
index.Upsert(new Invoice { Id = "inv-2", Title = "Draft: overdue reminder",
                           Body = "Acme Corp draft, not sent.",
                           Modified = DateTime.UtcNow, Tags = ["invoice", "draft"] });
index.Commit();                                    // atomic and durable
 
var cutoff = DateTime.UtcNow.AddYears(-1);
// what a user types: words, "phrases", -exclusions
var results = index.Search("overdue \"acme corp\" -draft")
                   .Where(x => x.Tags.Contains("invoice") && x.Modified > cutoff)
                   .Take(10);
foreach (var hit in results)
    Console.WriteLine($"{hit.Id} {hit.Score:F2} {hit.Document.Title}");
Console.WriteLine($"{index.Count} documents");
 
public class Invoice
{
    [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 DateTime Modified { get; set; }
    [Keyword, Facet] public string[] Tags { get; set; } = [];
}
Text
inv-1 0.69 Overdue notice
2 documents
  • A source generator inside the package writes the mapping from the attributes at build time, with no reflection, so it is NativeAOT- and trim-safe. Supported types, diagnostics and highlights: mapping.
  • Search(text) takes what a user types and never throws, whatever is typed: user text. Every word not joined by OR must match, typos are forgiven, and the last word also matches as a prefix.
  • Where is translated to filters and never run; anything it cannot translate throws and names the part: LINQ filters.
  • Commit writes and syncs every new file, then publishes the commit with one atomic rename. Searches see the last commit. One writer per directory; reader processes are fine.
  • TotalHits is a lower bound unless you add CountExactly().

Next