- BrainTools - https://www.braintools.ru -
Если вы делаете 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-файла.
Встраиваемая БД идеально ложится в такой сценарий. Все данные — на локальном диске, поиск — в оперативной памяти [1].
Сценарий 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 и работаете с ним.
dotnet add package VectorRAG.Net --version 0.1.17
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("---");
}
Комбинирует семантическое сходство с ключевыми словами:
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 уникальных родительских документов, каждый — с наивысшей оценкой среди своих чанков.
Библиотека содержит вспомогательный класс 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-прослойку, латентность вырастет.
// 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);
}
}
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 [2]
Github (бенчмарки) – https://github.com/likeslines-maker/VectorRAG.Net [3]
Библиотека доступна для бесплатного тестирования в любых объёмах. Никаких скрытых платежей, триальных периодов и ограничений.
VectorRAG.Net — это встраиваемая векторная БД для .NET, которая:
работает без сети и внешних сервисов;
показывает микросекундные задержки;
минимально аллоцирует в горячем пути;
поддерживает гибридный поиск (вектор + BM25);
умеет сама нарезать документы на чанки;
сохраняется и загружается из файлов;
даёт метрики для мониторинга.
Если ваш сценарий требует низкой латентности, автономности или предсказуемой производительности — попробуйте эту библиотеку. Она не пытается заменить облачные сервисы, а решает задачу там, где внешние БД избыточны.
Автор: arhip1986
Источник [4]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/30906
URLs in this post:
[1] памяти: http://www.braintools.ru/article/4140
[2] https://www.nuget.org/packages/VectorRAG.Net: https://www.nuget.org/packages/VectorRAG.Net
[3] https://github.com/likeslines-maker/VectorRAG.Net: https://github.com/likeslines-maker/VectorRAG.Net
[4] Источник: https://habr.com/ru/articles/1040774/?utm_campaign=1040774&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.