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 -9of 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.
dotnet new console -o OneIndexStart && cd OneIndexStart dotnet add package RecursiveAI.OneIndex --version 0.4.0-preview.2
Nine packages share one version:
| Package | What it is |
|---|---|
RecursiveAI.OneIndex | the engine and the happy path; no dependencies beyond .NET |
RecursiveAI.OneIndex.Models | bundled embedder (bge-small-en-v1.5), reranker and sparse encoder on OneModel; a 134 MB package |
RecursiveAI.OneIndex.Models.Multilingual | the multilingual-e5-small embedder, around 100 languages; a 112 MB package |
RecursiveAI.OneIndex.Analysis.Asian | Japanese, Chinese, Korean, Thai, Lao, Khmer and Myanmar segmentation |
RecursiveAI.OneIndex.EntityFrameworkCore | keeps an index in step with SaveChanges |
RecursiveAI.OneIndex.VectorData | a vector store for Microsoft.Extensions.VectorData |
RecursiveAI.OneIndex.Files, .Files.Ocr | folder indexing with citations; OCR for scans |
RecursiveAI.OneIndex.Cli | the oneindex tool: index, query, static build |
A first program
Put this in Program.cs and run dotnet run:
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; } = []; }
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 byORmust match, typos are forgiven, and the last word also matches as a prefix.Whereis translated to filters and never run; anything it cannot translate throws and names the part: LINQ filters.Commitwrites 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.TotalHitsis a lower bound unless you addCountExactly().
Next
- Full-text search in C#: typos, facets and highlights.
- Vector search in .NET and hybrid search with reranking, with the bundled models.
- Search over EF Core entities and a folder of files with citations.
- Coming from Lucene.NET: the migration recipe.
- Every topic: the docs, starting with architecture.