Дилерский центр, отключённый от сети, и 200‑миллисекундный timeout
Заказчик — сеть автосалонов. Их RAG-чатбот для техников висит на Azure Cognitive Search, эмбеддинги считает OpenAI. Всё работает… пока не глохнет интернет. А он глохнет — серверная в подвале старого здания, LTE модем падает раз в день.
Чатбот превращается в тыкву. 200 мс на поиск по 50 тысячам документов превращаются в 8 секунд с кучей повторных попыток. Техники матерятся, менеджер звонит мне: «Сделай так, чтобы работало, даже если DNS умер».
В тот момент я понял: внешняя векторная БД — это хорошо, но не для всех. А для .NET‑стека нужна встраиваемая штука, которая живёт прямо в процессе приложения. Не надо ставить Docker, открывать порты, согласовывать firewall. Открыл файл — и погнал.
Почему не Redis, не Qdrant, не Elasticsearch?
Ответ простой: они не встраиваются. Любая отдельная база — это точка отказа, лишний поход по сети, extra latency. Для офлайн-сценариев это смерть. А ещё инженерные боли: надо поднимать кластер, настраивать репликацию, бэкапы.
Конечно, если у вас high‑load на миллиард векторов — берите Qdrant или Pinecone. Но если у вас 10–500 K документов и приложение должно держаться на коленке — встраиваемая БД справится не хуже. Как показывает практика 2026 года, для многих production‑сценариев локальный поиск с SQLite даёт сопоставимую точность при меньшей цене.
Что выбрать для .NET 8 в 2026 году?
Вариантов несколько, но я остановился на SQLite с расширением sqlite-vec (версия 0.7.2, стабильная на май 2026). Оно встраивается через Microsoft.Data.Sqlite, не требует нативного кода (благодаря пакету sqlite-vec-net). Альтернативы — DuckDB (тоже встраивается, но тяжеловеснее для .NET) или FAISS через P/Invoke, но это уже не БД.
Спойлер: sqlite-vec использует HNSW‑индексы на диске, даёт косинусную близость и поддерживает hybrid search через FTS5. Всё внутри одного файла — удобно для бэкапов и деплоя.
Зачем здесь SQLite? А затем, что база данных и работа с ней уже хорошо знакомы .NET‑разработчику. Не надо учить новый API. INSERT INTO embeddings VALUES (@id, @vector) — и поехали. Но есть нюанс: нативная поддержка векторов в SQLite появилась только с расширением. До этого люди хранили JSON-строки и мучались с вычислением расстояния на клиенте. (Когда SQL и векторный поиск дерутся за ваши данные — в статье как раз показаны грабли такого подхода.)
Бенчмарки: 50K документов, 1K запросов, и никакой магии
Я замерял на MacBook M3 Pro (файл БД на SSD). Использовал эмбеддинги размером 384 (модель all‑MiniLM‑L6‑v2, конвертированный в ONNX и запущенный через ML.NET).
| Сценарий | Латентность (p50) | Латентность (p99) | Точность@5 |
|---|---|---|---|
| SQLite + грубая сила (без индекса) | 45 ms | 220 ms | 87% |
| SQLite + HNSW (M=16, efConstruction=200) | 3 ms | 12 ms | 84% |
| Гибрид: HNSW + FTS5 (keyword boost) | 8 ms | 35 ms | 92% |
8 мс на гибрид — это в 25 раз быстрее, чем поход в облачную БД. И никакой сети. Даже если вы используете VectorDBZ для отладки, локальный файл можно открыть и посмотреть глазами.
Ошибка новичка: не строить HNSW-индекс сразу. Если вы вставили 50K векторов без индекса — полный скан на 220 мс. Индекс надо строить после вставки всех данных. Иначе sqlite-vec будет перестраивать его при каждом INSERT — тормоза дикие.
Пошаговая реализация: от NuGet до первого поиска
1 Ставим пакеты
dotnet add package Microsoft.Data.Sqlite --version 8.0.4
dotnet add package sqlite-vec-net --version 0.7.2
2 Инициализируем БД с векторной функцией
using Microsoft.Data.Sqlite;
using SqliteVec;
var conn = new SqliteConnection("Data Source=rag.vec.db");
conn.Open();
conn.EnableExtensions();
conn.LoadExtension("vec0"); // загружаем sqlite-vec
3 Создаём виртуальную таблицу для векторов
conn.Execute(@"
CREATE VIRTUAL TABLE IF NOT EXISTS docs_vectors USING vec0(
embedding float[384] distance_metric=cosine,
+id INTEGER PRIMARY KEY,
+content TEXT,
+source TEXT
);
");
// индекс FTS5 для гибридного поиска
conn.Execute(@"
CREATE VIRTUAL TABLE IF NOT EXISTS docs_fts USING fts5(
id UNINDEXED, content, source
);
");
4 Вставляем данные (батч по 1000)
// каждый эмбеддинг — byte[] или float[], сериализуем в бинарный blob
var embedding = GetEmbedding(text); // float[384]
var blob = SqliteVec.Vector.ToBlob(embedding);
conn.Execute(@"
INSERT INTO docs_vectors (embedding, id, content, source)
VALUES (@emb, @id, @content, @source)
", new {
emb = blob,
id = docId,
content = doc.Content,
source = doc.Source
});
5 Гибридный поиск (векторы + ключевые слова)
var queryEmb = GetEmbedding(userQuery);
var queryBlob = SqliteVec.Vector.ToBlob(queryEmb);
// Сначала точный топ-10 по векторам, потом ранжируем с FTS
var sql = @"
SELECT v.id, v.content, v.source, v.distance,
rank FROM (
SELECT id, content, source, distance
FROM docs_vectors
WHERE embedding MATCH @qemb
ORDER BY distance
LIMIT 10
) v
LEFT JOIN (
SELECT id, rank
FROM docs_fts
WHERE docs_fts MATCH @ftsQuery
) f ON v.id = f.id
ORDER BY COALESCE(f.rank, 0) * 0.3 + v.distance * 0.7
LIMIT 5
";
var topDocs = conn.Query(sql, new { qemb = queryBlob, ftsQuery = userQuery }).ToList();
Готово! 20 строк кода — и у вас RAG-движок, который не зависит от интернета. Браузерный RAG для юристов идёт ещё дальше — весь пайплайн в браузере, но у нас .NET для десктопных и серверных сценариев.
Когда встраиваемая БД сломается (и что делать)
Не всё так радужно. Вот грабли, на которые я наступал:
- Многопоточность. SQLite не любит конкурентную запись. Решение — очередь запросов на вставку (один писатель, много читателей).
- Размер индекса. HNSW-индекс для 500K векторов съедает ~800 МБ. Если память критична — смотрите на Anchor Engine V5: memory graph вместо векторного индекса даёт 2 мс поиск при 10% памяти.
- Точность гибрида. Веса подбираются эмпирически. Универсальной формулы нет. Тестируйте на своих данных.
- Обновление эмбеддингов. При изменении документа нужно пересчитать вектор и обновить обе таблицы — иначе будет рассинхрон. Используйте общую транзакцию.
Риторический вопрос: а не проще ли взять стороннюю библиотеку?
Есть пакет Microsoft.KernelMemory — он умеет хранить векторы в SQLite, но внутри он поднимает свой pipeline. Для простых сценариев — оверхед. А ещё многие разработчики жалуются на странные баги с индексами. Я предпочитаю контролировать каждый байт.
Кстати, векторный RAG проваливается на сложных документах — если документы длинные, один эмбеддинг плохо их описывает. Решение — PageIndex без эмбеддингов, который разбивает документ на страницы и индексирует каждую. Но это уже другая история.
Вместо заключения: куда копать, если SQLite не тянет?
Если данных сотни гигабайт — переходите на DuckDB (он тоже встраивается, но требует .NET 8 NativeAOT для бесшовной интеграции) или на Lighthouse (новый векторный движок, оптимизированный под ARM, 2025–2026). Но для 90% .NET‑проектов SQLite + sqlite-vec — это золотой стандарт. Бесплатно, приватно, без сети.
Попробуйте переписать свой RAG-чатбот на in‑process векторной БД. Потом отключите интернет. И увидите, как он продолжает работать. Это ощущение стоит всех строк кода.