О чём речь
Если вы делаете 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);
Что происходит внутри:
-
Текст нарезается на чанки согласно
ChunkingOptions. -
Для каждого чанка генерируется эмбеддинг через
embeddingModel. -
Чанки сохраняются в индекс вместе со ссылкой на родительский документ.
-
Метаданные распространяются на все чанки.
Если нужно добавить несколько документов за раз (быстрее, чем по одному):
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/