Recursive AI

Vector search in .NET

All oneindex recipes

Mark a float[] property [Vector(From = nameof(Text))] and OneIndex embeds the text when you Upsert, with a bundled English model that runs in your process. Then embed the question with the same model and call SearchVector: you get the documents nearest in meaning, even when they share no words with it.

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

RecursiveAI.OneIndex.Models carries the model weights inside the package (a 134 MB download from NuGet, once). Nothing is fetched at run time.

Put this in Program.cs and run dotnet run:

C#
using OneIndex;
using OneIndex.Models;
using OneIndex.Vectors;
 
var embedder = BgeSmallEmbedder.UseAsDefault();    // bge-small-en-v1.5, 384 dimensions
 
using var index = SearchIndex.Open<Note>("notes-index");
index.Upsert(
[
    new() { Id = "a", Topic = "hardware",
            Text = "Overheating notebook? Clean the fan and vents." },
    new() { Id = "b", Topic = "hardware",
            Text = "A swollen battery must be replaced at once." },
    new() { Id = "c", Topic = "cooking",
            Text = "Let bread dough rest overnight in the fridge." },
    new() { Id = "d", Topic = "software",
            Text = "Disable startup programs so Windows boots faster." },
    new() { Id = "e", Topic = "software",
            Text = "Update graphics drivers to fix screen flicker." },
    new() { Id = "f", Topic = "travel",
            Text = "Pack a universal plug adapter for trips abroad." },
]);                                                // each Text is embedded here
index.Commit();
 
float[] question = embedder.Embed("my laptop gets too hot", EmbeddingRole.Query);
foreach (var hit in index.SearchVector(n => n.Embedding, question, neighbors: 2))
    Console.WriteLine($"{hit.Id}  {hit.Score:F3}  {hit.Document.Text}");
 
Console.WriteLine();
question = embedder.Embed("PC takes ages to turn on", EmbeddingRole.Query);
var search = index.SearchVector(n => n.Embedding, question, neighbors: 2)
                  .Where(n => n.Topic == "software");   // filter before neighbors
foreach (var hit in search)
    Console.WriteLine($"{hit.Id}  {hit.Score:F3}  {hit.Document.Text}");
 
public class Note
{
    [SearchId] public string Id { get; set; } = "";
    [FullText] public string Text { get; set; } = "";
    [Keyword, Filterable] public string Topic { get; set; } = "";
    [Vector(From = nameof(Text))] public float[]? Embedding { get; set; }
}
Text
a  0.873  Overheating notebook? Clean the fan and vents.
e  0.788  Update graphics drivers to fix screen flicker.
 
d  0.835  Disable startup programs so Windows boots faster.
e  0.809  Update graphics drivers to fix screen flicker.
  • BgeSmallEmbedder.UseAsDefault() loads the bundled model and makes it the embedder new indexes open with. Nothing registers itself: without this line (or an embedder passed to SearchIndex.Open<T>(path, embedder)), no text is embedded.
  • [Vector(From = nameof(Text))] embeds Text at Upsert with the document role. The length (384) comes from the model.
  • The question is embedded with EmbeddingRole.Query, which adds the instruction the model expects for queries.
  • SearchVector(member, vector, neighbors) returns the nearest documents by cosine similarity, highest score first. Where(...) picks the neighbors among documents that pass, so a filtered search still fills its page.
  • Each document stores its embedding beside a hash of the model and the text, so re-upserting unchanged text, reopening and merging never embed again.

Limits

  • A preview (0.4.0-preview.2): APIs and file formats may still change between previews.
  • The bundled embedder is English. RecursiveAI.OneIndex.Models.Multilingual has multilingual-e5-small (around 100 languages; its int8 weights are 120 MB).
  • Embedding runs on the CPU in the thread that calls Upsert, so a large first load takes time. The model reads at most its input limit and cuts longer text; split long documents into passages.
  • Small collections are searched exactly; larger ones use an HNSW graph, which is approximate (recall below 1.0 at a given setting).
  • Model licenses: bge-small-en-v1.5 is MIT. The weights are the model authors’ and are distributed under that license; the training data’s terms are not conferred on them.

Getting started · Vectors · Automatic embedding · Models · All recipes