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

О чём речь

Если вы делаете 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.cs
builder.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);

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

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

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

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

Автор: arhip1986

Источник