---
title: "Vector search in .NET"
url: https://recursiveai.org/oneindex/cookbook/vector-search-dotnet/
description: "Semantic vector search in C# with OneIndex: embed text locally with the bundled bge-small-en-v1.5 model, store the vectors in an HNSW index and find the nearest documents by meaning, with filters. No server, no Python, no API key. Real output."
version: "0.4.0-preview.2"
---

# Vector search in .NET

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.

```shell
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`:

```csharp
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](https://recursiveai.org/oneindex/docs/getting-started/) · [Vectors](https://recursiveai.org/oneindex/docs/vectors/) · [Automatic embedding](https://recursiveai.org/oneindex/docs/auto-embedding/) · [Models](https://recursiveai.org/oneindex/docs/models/) · [All recipes](https://recursiveai.org/oneindex/cookbook/)
