Встраиваемая векторная БД для RAG на .NET 8: когда внешние сервисы избыточны

от автора

О чём речь

Если вы делаете RAG (Retrieval-Augmented Generation) на .NET, то рано или поздно упираетесь в вопрос: куда складывать эмбеддинги и как быстро искать по ним.

Существующие варианты делятся на два лагеря.

Внешние сервисы (Pinecone, Qdrant, Weaviate) — хороши, но требуют отдельной инфраструктуры. Сеть, авторизация, сериализация, мониторинг. Каждый запрос — это миллисекунды на HTTP. Плюс вы привязываетесь к конкретному облачному провайдеру или контейнеру.

Существующие .NET-решения — часто либо заброшены, либо имеют проблемы с производительностью (избыточные аллокации, медленный ANN, отсутствие гибридного поиска).

Но есть и третий путь: встраиваемая (embedded) векторная БД, которая работает прямо внутри вашего процесса. Никакой сети. Никакого внешнего сервиса. Только ваш код и процессор.

Когда это необходимо

Встраиваемая векторная БД нужна не всегда, но есть сценарии, где она фактически незаменима.

Сценарий 1: Высоконагруженный сервис с требованиями к латентности

Представьте, что вы делаете поиск по внутренней документации для техподдержки. Оператор вводит запрос и должен получить ответ за 50–100 мс. Если каждый поиск идёт через HTTP к внешней БД, 10–20 мс уходит только на транспорт. Плюс обработка в самой БД. Плюс вызов LLM. Суммарное время легко переваливает за 300–400 мс.

С встраиваемой БД транспортных задержек нет. Векторный поиск занимает 15–100 микросекунд. Экономия — на порядки.

Сценарий 2: Десктопное или мобильное приложение

Вы пишете локального ассистента, который работает на ноутбуке пользователя. Нет интернета — нет и внешней БД. Нет возможности поднять Docker-контейнер. Всё должно быть внутри одного exe-файла.

Встраиваемая БД идеально ложится в такой сценарий. Все данные — на локальном диске, поиск — в оперативной памяти.

Сценарий 3: Edge-вычисления и IoT

На устройстве с ограниченными ресурсами (например, на контроллере или Raspberry Pi) нет возможности запускать отдельный сервис. Есть только сам процесс приложения. Встраиваемая БД с минимальным потреблением памяти и процессора — единственный вариант.

Сценарий 4: Офлайн-режим в корпоративных системах

В авиации, медицине, оборонке часто требуется полная автономность. Никаких внешних запросов. Всё должно работать при отключенной сети. Внешняя векторная БД с сетевым доступом по определению не подходит.

Сценарий 5: Прототипирование и тестирование

Когда вы только начинаете делать RAG, хочется быстро попробовать гипотезы, поменять параметры, пересобрать индекс. Бегать к облачной БД и чистить коллекции — муторно. Локальная in-memory база позволяет итерации за секунды, а не минуты.

Что предлагается

VectorRAG.Net 0.1.17 — библиотека для .NET 8.0+, реализующая векторное хранилище с поддержкой:

  • быстрого ANN-поиска (LSH-кандидаты → точный переранжировщик с SIMD);

  • гибридного поиска (вектор + BM25);

  • автоматической нарезки документов на чанки (chunking);

  • фильтрации по метаданным;

  • сохранения и загрузки снэпшотов;

  • runtime-метрик.

Библиотека не требует отдельного сервера или базы данных. Вы просто создаёте экземпляр VectorRAGDatabase и работаете с ним.

Установка

Через .NET CLI

dotnet add package VectorRAG.Net --version 0.1.17

Через Package Manager Console

Install-Package VectorRAG.Net -Version 0.1.17

После установки подключаем пространства имён:

using SlidingRank.FastOps;using VectorRAG.Net;

Создание экземпляра базы данных

Для начала нужно сконфигурировать LSH-индекс — от этого зависит баланс между скоростью поиска и качеством.

// Конфигурация LSH: 24 бэнда, 12 бит на бэнд, максимум 2048 кандидатовvar lshConfig = new EmbeddingLshConfig(    Bands: 24,    BitsPerBand: 12,    MaxCandidates: 2048,    Seed: 1337);

Объяснение параметров:

  • Bands — количество хеш-таблиц. Чем больше, тем точнее, но медленнее.

  • BitsPerBand — длина хеша в битах. Влияет на вероятность коллизий.

  • MaxCandidates — сколько кандидатов LSH возвращает до точного переранжирования.

  • Seed — для воспроизводимости результатов.

Затем создаём опции для самой базы:

var options = new VectorRagDatabaseOptions{    InitialCapacity = 8192,          // Начальная ёмкость для векторов    QueryCacheCapacity = 1000,       // Кэш последних запросов    NormalizeVectorsOnAdd = false,   // Нормализовать векторы при добавлении    NormalizeQueryOnSearch = false,  // Нормализовать запрос перед поиском    DefaultChunking = new ChunkingOptions    {        Strategy = ChunkingStrategy.FixedChars,        ChunkSize = 1000,        ChunkOverlap = 200    }};

Теперь создаём базу:

int dimension = 1536;  // Размерность эмбеддинга (зависит от модели)var db = new VectorRAGDatabase(    dimension: dimension,    lshConfig: lshConfig,    options: options);

Подключение модели эмбеддингов

Библиотека не генерирует эмбеддинги сама — для этого нужно передать реализацию IEmbeddingModel. Самый простой вариант — через OpenAI.

IEmbeddingModel embeddingModel = new OpenAIEmbeddingModel(    apiKey: "sk-...",    model: "text-embedding-3-small",    dimension: 1536);

Для локальных моделей (ONNX, Sentence Transformers) можно реализовать свой адаптер:

public class LocalOnnxEmbeddingModel : IEmbeddingModel{    private readonly YourOnnxModel _model;    public LocalOnnxEmbeddingModel(string modelPath)    {        _model = LoadOnnxModel(modelPath);    }    public async Task<float[]> GenerateEmbeddingAsync(string text)    {        // Вызов ONNX-модели        return await Task.Run(() => _model.Encode(text));    }    public int Dimension => 768;  // Размерность вашей модели}

Добавление документов

Самый простой способ — использовать встроенный чанкинг:

await db.UpsertTextDocumentAsync(    externalId: "faq_000123",    text: File.ReadAllText("faq_000123.txt"),    metadata: new DocumentMetadata     {         Department = "Support",        IsActive = true,        Attributes = new Dictionary<string, object>        {            ["priority"] = "high",            ["language"] = "ru"        }    },    embeddingModel: embeddingModel);

Что происходит внутри:

  1. Текст нарезается на чанки согласно ChunkingOptions.

  2. Для каждого чанка генерируется эмбеддинг через embeddingModel.

  3. Чанки сохраняются в индекс вместе со ссылкой на родительский документ.

  4. Метаданные распространяются на все чанки.

Если нужно добавить несколько документов за раз (быстрее, чем по одному):

var documents = new List<TextDocument>{    new TextDocument("doc_001", "Текст документа 1", new DocumentMetadata { Department = "Sales" }),    new TextDocument("doc_002", "Текст документа 2", new DocumentMetadata { Department = "Support" })};await db.UpsertTextDocumentBatchAsync(documents, embeddingModel);

Для низкоуровневого добавления готовых векторов (без чанкинга):

var vectors = new float[][] { ... };var metadatas = new DocumentMetadata[] { ... };db.Add(vectors, metadatas);

Поиск

Векторный поиск

Сначала генерируем эмбеддинг запроса, затем ищем:

var queryText = "как сбросить пароль?";var queryVector = await embeddingModel.GenerateEmbeddingAsync(queryText);var results = db.Search(queryVector, new SearchOptions{    TopK = 5,    UseHybrid = false});foreach (var result in results){    Console.WriteLine($"Score: {result.Score:F4}");    Console.WriteLine($"Text: {result.Text}");    Console.WriteLine($"Metadata: {result.Metadata.Department}");    Console.WriteLine("---");}

Гибридный поиск (вектор + BM25)

Комбинирует семантическое сходство с ключевыми словами:

var queryText = "сброс пароля";var queryVector = await embeddingModel.GenerateEmbeddingAsync(queryText);var results = db.Search(queryVector, new SearchOptions{    TopK = 5,    UseHybrid = true,    TextQuery = queryText,    Alpha = 0.7f  // 0.7 = вес вектора, 0.3 = вес BM25});
  • Alpha = 1.0 — только векторный поиск.

  • Alpha = 0.0 — только полнотекстовый поиск (BM25).

  • Промежуточные значения — взвешенная сумма нормализованных релевантностей.

Поиск с фильтрацией по метаданным

var results = db.Search(queryVector, new SearchOptions{    TopK = 5,    Filter = md => md.Department == "Support" && md.IsActive});

Фильтрация происходит до переранжирования (на кандидатах от LSH), поэтому не добавляет существенного оверхеда.

Поиск с группировкой по родительскому документу

Если документ был разбит на чанки, можно вернуть не сами чанки, а уникальные родительские документы:

var results = db.Search(queryVector, new SearchOptions{    TopK = 5,    GroupByParentDocument = true});

В этом случае results будет содержать не более 5 уникальных родительских документов, каждый — с наивысшей оценкой среди своих чанков.

RAG-пайплайн: от поиска к промпту

Библиотека содержит вспомогательный класс RAGPipeline для построения контекста из найденных результатов.

var pipeline = new RAGPipeline(embeddingModel);var searchResults = db.Search(queryVector, new SearchOptions{    TopK = 5,    UseHybrid = true,    TextQuery = userQuestion});// Собираем контекст, ограничивая токенамиvar promptContext = pipeline.BuildPromptContext(    results: searchResults,    maxTokens: 3500,    includeMetadata: true);// promptContext — готовая строка для вставки в промптvar finalPrompt = $@"Используя следующий контекст, ответь на вопрос пользователя.Контекст:{promptContext}Вопрос: {userQuestion}Ответ:";// Отправляем finalPrompt в LLM (GPT, Llama, любой другой)

Персистентность

Сохранение базы на диск

await db.SaveAsync("C:/rag_snapshots/db_2026_02_08.vdb");

Файл включает:

  • все векторы;

  • LSH-индексы;

  • метаданные;

  • BM25-индекс (если использовался гибридный поиск).

Загрузка базы с диска

var restoredDb = new VectorRAGDatabase(dimension, lshConfig, options);await restoredDb.LoadAsync("C:/rag_snapshots/db_2026_02_08.vdb");

Автоматические снапшоты

Можно настроить фоновое сохранение, например, через Timer или BackgroundService:

public class SnapshotBackgroundService : BackgroundService{    private readonly VectorRAGDatabase _db;    private readonly string _snapshotPath;    protected override async Task ExecuteAsync(CancellationToken stoppingToken)    {        while (!stoppingToken.IsCancellationRequested)        {            await Task.Delay(TimeSpan.FromHours(1), stoppingToken);            await _db.SaveAsync($"{_snapshotPath}/snapshot_{DateTime.Now:yyyyMMdd_HHmm}.vdb");        }    }}

Метрики и мониторинг

var metrics = db.GetMetrics();Console.WriteLine($"Всего записей: {metrics.RecordsTotal}");Console.WriteLine($"Активных: {metrics.RecordsActive}");Console.WriteLine($"Удалённых: {metrics.RecordsTotal - metrics.RecordsActive}");Console.WriteLine($"Среднее время запроса: {metrics.AvgQueryMs:F2} мс");Console.WriteLine($"Размерность: {metrics.Dimension}");Console.WriteLine($"Ёмкость: {metrics.Capacity}");

Метрики можно экспортировать в Prometheus через prometheus-net:

var gauge = Metrics.CreateGauge("vectordb_active_records", "Active records");gauge.Set(metrics.RecordsActive);

Полный рабочий пример

Соберём всё вместе — от создания базы до ответа LLM:

using SlidingRank.FastOps;using VectorRAG.Net;class Program{    static async Task Main(string[] args)    {        // 1. Конфигурация LSH        var lshConfig = new EmbeddingLshConfig(            Bands: 24,            BitsPerBand: 12,            MaxCandidates: 2048        );        // 2. Опции базы        var options = new VectorRagDatabaseOptions        {            InitialCapacity = 8192,            DefaultChunking = new ChunkingOptions            {                Strategy = ChunkingStrategy.FixedChars,                ChunkSize = 1000,                ChunkOverlap = 200            }        };        // 3. Создаём базу        var db = new VectorRAGDatabase(            dimension: 1536,            lshConfig: lshConfig,            options: options        );        // 4. Подключаем модель эмбеддингов (OpenAI)        var embeddingModel = new OpenAIEmbeddingModel(            apiKey: Environment.GetEnvironmentVariable("OPENAI_API_KEY"),            model: "text-embedding-3-small",            dimension: 1536        );        // 5. Добавляем документы        await db.UpsertTextDocumentAsync(            externalId: "password_reset",            text: File.ReadAllText("./docs/password_reset.txt"),            metadata: new DocumentMetadata { Department = "Support" },            embeddingModel: embeddingModel        );        // 6. Поиск        var question = "Как восстановить доступ к аккаунту?";        var queryVector = await embeddingModel.GenerateEmbeddingAsync(question);                var results = db.Search(queryVector, new SearchOptions        {            TopK = 3,            UseHybrid = true,            TextQuery = question,            Alpha = 0.6f        });        // 7. Собираем промпт        var pipeline = new RAGPipeline(embeddingModel);        var context = pipeline.BuildPromptContext(results, maxTokens: 2000);        // 8. Отправляем в LLM (пример через OpenAI)        var answer = await CallLLM(context, question);                Console.WriteLine($"Ответ: {answer}");    }}

Производительность

Тестовый стенд: Windows 11, Intel Core i5-11400F, .NET 8.0, BenchmarkDotNet 0.15.8

Датасет: 10 000 документов, размерность эмбеддинга — 64 (синтетика для повторяемости). TopK = 5.

Операция

Среднее время

Аллокации

Векторный поиск (TopK=5)

15.15 μs

5.69 KB

Гибридный поиск (вектор + BM25)

116.73 μs

14.85 KB

Из среднего времени векторного поиска получается ~66 000 запросов в секунду на поток. Это синтетика на dim=64. Для реальных эмбеддингов (768, 1536) абсолютные цифры будут ниже, но важнее другое: библиотека не добавляет накладных расходов на сеть, сериализацию или избыточные аллокации.

Примечание: бенчмарки измеряют только in-process вычисления. Если вы добавляете HTTP/gRPC-прослойку, латентность вырастет.

Интеграция в существующий проект

ASP.NET Core + Dependency Injection

// Program.csbuilder.Services.AddSingleton(sp =>{    var lshConfig = new EmbeddingLshConfig(24, 12, 2048);    var options = new VectorRagDatabaseOptions { InitialCapacity = 10000 };        var db = new VectorRAGDatabase(1536, lshConfig, options);        // Загружаем базу из файла, если есть    if (File.Exists("data/database.vdb"))        db.LoadAsync("data/database.vdb").Wait();        return db;});builder.Services.AddSingleton<IEmbeddingModel>(sp =>    new OpenAIEmbeddingModel(apiKey, "text-embedding-3-small", 1536));// Использование в контроллере[ApiController][Route("api/search")]public class SearchController : ControllerBase{    private readonly VectorRAGDatabase _db;    private readonly IEmbeddingModel _embedder;    public SearchController(VectorRAGDatabase db, IEmbeddingModel embedder)    {        _db = db;        _embedder = embedder;    }    [HttpPost]    public async Task<IActionResult> Search([FromBody] SearchRequest request)    {        var vector = await _embedder.GenerateEmbeddingAsync(request.Query);        var results = _db.Search(vector, new SearchOptions { TopK = request.TopK });        return Ok(results);    }}

Windows Service / Linux Daemon

public class RagService : BackgroundService{    private readonly VectorRAGDatabase _db;    private readonly ILogger<RagService> _logger;    public RagService(ILogger<RagService> logger)    {        _logger = logger;        var lshConfig = new EmbeddingLshConfig(24, 12, 2048);        _db = new VectorRAGDatabase(1536, lshConfig);    }    protected override async Task ExecuteAsync(CancellationToken stoppingToken)    {        // Загружаем индекс        await _db.LoadAsync("/var/data/knowledge_base.vdb");                // Фоновое обновление индекса раз в сутки        while (!stoppingToken.IsCancellationRequested)        {            await Task.Delay(TimeSpan.FromDays(1), stoppingToken);            await UpdateIndexAsync();        }    }}

Сравнение с альтернативами

Характеристика

VectorRAG.Net

Pinecone / Qdrant

Другие .NET-библиотеки

Сетевые вызовы

Нет

Есть (HTTP/gRPC)

Нет

Аллокации на запрос

~5-15 KB

10-100 KB+ (JSON)

Часто большие

Средняя латентность (dim=768)

~50-200 μs

5-15 ms

0.5-5 ms

Гибридный поиск

Да

Частично

Редко

Чанкинг

Встроенный

Нужно делать самому

Редко

Персистентность

Файлы снэпшотов

Облачное хранилище

Разнородно

Автономная работа

Да

Нет

Да

Где взять

NuGet: https://www.nuget.org/packages/VectorRAG.Net

Github (бенчмарки) https://github.com/likeslines-maker/VectorRAG.Net

Библиотека доступна для бесплатного тестирования в любых объёмах. Никаких скрытых платежей, триальных периодов и ограничений.

Резюме

VectorRAG.Net — это встраиваемая векторная БД для .NET, которая:

  • работает без сети и внешних сервисов;

  • показывает микросекундные задержки;

  • минимально аллоцирует в горячем пути;

  • поддерживает гибридный поиск (вектор + BM25);

  • умеет сама нарезать документы на чанки;

  • сохраняется и загружается из файлов;

  • даёт метрики для мониторинга.

Если ваш сценарий требует низкой латентности, автономности или предсказуемой производительности — попробуйте эту библиотеку. Она не пытается заменить облачные сервисы, а решает задачу там, где внешние БД избыточны.

ссылка на оригинал статьи https://habr.com/ru/articles/1040774/