Семантический поиск за ~150 строк: embeddings, pgvector и локальная модель без API-ключей

Семантический поиск на своих данных упирается в облачные embedding API: ключи, квоты, утечка текста наружу. Материал @dev48v на Dev.to описывает другой путь — компактный поисковик на embeddings и Postgres pgvector в Node без API-ключей.
Локальный inference: embeddings без облачного биллинга
Векторные представления текста — базовый кирпич для RAG, агентов с памятью и любого «поиска по смыслу». Здесь за эмбеддинги отвечает модель all-MiniLM-L6-v2 в варианте Xenova/all-MiniLM-L6-v2 через библиотеку Transformers.js (@xenova/transformers ^2.17.2). Каждый фрагмент превращается в вектор из 384 чисел; в схеме Postgres колонка объявлена как vector(384) — размерность должна совпадать с моделью.
Инференс идёт в Node 20+, без отправки данных в облако. При первом запуске модель скачивается (~25 MB) и кэшируется на машине. Вызов extractor настроен с pooling: "mean" и normalize: true, чтобы векторы были готовы к cosine distance.
Локальный стек снимает зависимость от биллинга embedding-провайдеров — удобная отправная точка для прототипа AI-фичи на своих документах.
Демо-проект лицензирован под MIT; исходники — в репозитории dev48v/pgvector-from-zero.
Индекс на pgvector: Wikipedia как демо-корпус
Postgres с расширением pgvector поднимается из Docker-образа pgvector/pgvector:pg16; по README автор отмечает готовность БД примерно за ~5 с после docker compose up -d. Корпус для демо набирается из публичного Wikipedia REST API — эндпоинт /page/random/summary, без аутентификации.
По умолчанию сидируется ~300 статей (SEED_LIMIT, дефолт 300). Для каждой эмбеддится строка title + summary — один вектор на статью, без чанкинга длинных текстов. Перед повторным сидом таблица articles очищается (TRUNCATE … RESTART IDENTITY). Фильтруются disambiguation-страницы и короткие extract (< 40 символов); дедупликация по title, верхняя граница попыток — count × 8.
Для ускорения поиска создаётся HNSW-индекс на колонке embedding с vector_cosine_ops — approximate nearest neighbour поверх векторов.
docker compose up -d
npm run seed
npm run dev
После npm run dev веб-интерфейс доступен на http://localhost:3000.
Cosine search вместо keyword match
Запрос пользователя тоже превращается в embedding той же моделью. В Postgres/pgvector близость считается оператором <=> (cosine distance); ранжирование — ORDER BY embedding <=> $query, similarity как 1 - distance.
На корпусе из 300 статей для запроса «animals that live in the ocean» автор приводит иллюстративные score:
| Результат | Similarity |
|---|---|
| Blue whale | 92.1% |
| Coral reef | 88.4% |
| Sea otter | 85.0% |
Другие примеры из материала — без чисел: «famous battles in history» возвращает наполеоновские сражения и древние осады; «how the brain works» — нейронауку с «neuron»/«cortex», хотя буквальных слов запроса в текстах нет. Формальных бенчмарков (latency, recall@k, сравнение с keyword baseline) в первоисточнике нет — это ограничение демо, а не продакшен-отчёт.
Смысл в том, что поиск ловит тему, а не совпадение токенов — ровно то, что отличает embedding-пайплайн от классического LIKE или полнотекстового индекса.
Два шага RAG, которые уже работают
Автор явно вписывает проект в цепочку Retrieval-Augmented Generation (RAG):
- эмбеддить документы и хранить векторы — реализовано;
- эмбеддить вопрос и искать ближайшие фрагменты — реализовано;
- передать найденное LLM с инструкцией отвечать только по контексту — следующий шаг, в репозитории не сделан.
Это рабочие первые два звена retrieval-слоя: без них агент или чат-бот не получит релевантные куски из вашей базы знаний. В README перечислены направления «going further»: answer-слой с LLM, чанкинг длинных документов, альтернативные операторы pgvector (<#>, <->).
Пост входит в серию TechFromZero — Day 45, формат «one new technology every day, built from scratch, every line explained». Полный каталог предыдущих дней в проверенных источниках не раскрыт; читать материал стоит как самостоятельный мини-проект по векторному поиску. Время чтения на Dev.to — около 3 минут; код рассчитан на разбор «с нуля».
Как повторить стек у себя
Минимальный набор зависимостей из package.json репозитория:
| Компонент | Версия |
|---|---|
@xenova/transformers |
^2.17.2 |
pg |
^8.13.1 |
express |
^4.21.2 |
TypeScript / tsx |
^5.7.2 / ^4.19.2 |
Клонирование и запуск по инструкции из поста:
git clone https://github.com/dev48v/pgvector-from-zero.git
cd pgvector-from-zero
npm install
docker compose up -d
npm run seed
npm run dev
Для продакшена понадобятся чанкинг, обновление индекса и слой ответа LLM — но как лабораторный стенд для embeddings + pgvector без облачных ключей связка уже собирается за вечер. Если вы проектируете AI-фичу с поиском по своим данным, такой локальный контур быстрее отвечает на вопрос «как это ощущается на реальных векторах», чем абстрактное чтение про RAG.
Источники
- @dev48v — I Built a Search Engine That Understands Meaning — in ~150 Lines, Zero API Keys (опубликовано 13 июня 2026)
- Репозиторий dev48v/pgvector-from-zero