На работе у меня есть вики из одиннадцати с лишним тысяч Markdown-файлов. Искать в ней что-то точным совпадением строки было почти бесполезно: чтобы найти подстроку, надо уже знать, как она называется. Именно эта боль и привела на работе к Qdrant — векторной базе данных. Долгое время она жила там, в корпоративном контуре. А теперь мне понадобилась та же штука дома.
Что такое Qdrant по смыслу, а не по определению
Формально Qdrant — это векторная база данных. Это определение ничего не объясняет, поэтому лучше начать с противоположного.
Обычная база данных ищет по совпадению. Ты спрашиваешь «дай строки, где есть слово retry» — и получаешь ровно те строки, где есть слово retry. Если автор кода назвал функцию with_backoff, поиск по retry её не найдёт, хотя смысл там тот же самый. Обычная база умеет сравнивать символы. Смысл она не видит.
Векторная база умеет другое. Она хранит не строки, а векторы — числовые описания смысла. Текст прогоняется через модель эмбеддингов, и на выходе получается массив чисел фиксированной длины: тысяча двадцать четыре числа, семьсот шестьдесят восемь — как повезёт с моделью. У этих чисел есть полезное свойство: близкие по смыслу тексты дают близкие векторы. Не по буквам — по значению.
Поэтому Qdrant отвечает не на вопрос «где эта подстрока», а на вопрос «что здесь про это». Ты приносишь ему «открыть соединение с базой», он за секунду находит функцию, где написано sqlite3.connect. Ты приносишь «проверить пароль пользователя», он достаёт функцию, где написано hashlib.sha256. Слов из запроса в этих функциях нет ни одного.
Коротко: Qdrant — это место, где текст лежит в виде смысла, а не в виде букв. И искать по нему можно смыслом.
Зачем она была нужна в рабочих процессах
Вики на одиннадцать тысяч файлов — это документация Confluence, выгруженная в Markdown, плюс личные заметки Obsidian. Структура была иерархическая: сначала строился многоуровневый индекс, где папки сворачивались в узлы, узлы — в один финальный INDEX.md. Это работало как оглавление. Но оглавление отвечает на вопрос «где это лежит», а не «где это описано».
Дальше начиналась та же ловушка, что и с grep. Чтобы найти в такой вики порядок согласования заявки, нужно было помнить, в каком разделе он лежит и как там названы пункты. Достаточно было не помнить точного слова — и ты бродишь по папкам руками.
Что дал Qdrant в этом рабочем процессе:
- Поиск по значению, а не по названию. Запрос «аналитика данных» находил
документы, где этими словами и не пахнет, — но тема та же.
- Навигацию от смысла к файлу. Сначала находилось несколько десятков релевантных
кусков с путями, дальше иерархический индекс подсказывал, в какой раздел сходить за полным текстом.
- Инкрементность. У каждого файла считался хеш; при повторном запуске
перечитывались только изменившиеся. Одиннадцать тысяч файлов с кэшем обрабатывались за секунды вместо многих минут.
Но главная причина, по которой Qdrant вообще прижился в рабочих процессах, шире одной вики. Кроме документации в индексе был нужен код. И не какой-то один — а сразу два потока, которые на работе не пересекаются: свои проекты и чужие.
Свои — это то, что ведёт моя команда: инструменты, регламенты, автоматизация. Чужие — код соседних команд и смежных контуров, который я читаю и с которым стыкуюсь, но никогда не писал. Сотни тысяч строк, написанные разными людьми в разное время, в разных стилях и с разными соглашениями об именах.
То есть в одном индексе сошлись два очень разных источника: код своего и чужого контура плюс тысячи Markdown-файлов документации. Разные форматы, разные авторы, разное время — а вопрос к ним один и тот же: «где это описано».
И вот здесь grep перестаёт быть инструментом в принципе. Пока пишешь сам — помнишь имена. Но чужой код ты не писал, и его имён ты не выбирал: там process_payload, у нас handle_batch, а задача одна и та же. Помнишь задачу, а не функцию: «где у нас уже валидировали входящий пакет», «кто согласовывает отклонения», «в каком модуле ретраят запрос к внешней системе», «есть ли у соседей готовый коннектор к Confluence». Такой вопрос grep не понимает — он умеет сравнивать только символы. Документация и код, свои проекты и чужие — всё это сводится к одному механизму поиска по смыслу.
Важная деталь того рабочего прототипа: там стояли хеш-псевдо-эмбеддинги размерности 384 — заглушка вместо настоящей модели. Это было сделано осознанно, чтобы проверить сам конвейер: индексацию, хранение, поиск. По смыслу такие векторы не работают — это просто детерминированный способ превратить строку в набор чисел. Об этом в проекте прямо написано в заметках: для продакшена модель надо заменять на настоящую. Именно эту замену я и делаю теперь дома.
Почему это понадобилось дома
Домашний воркспейс перестал быть одним проектом. Там сейчас монорепо с сабмодулями и примерно двадцать вложенных репозиториев: деплой и мониторинг песочницы, движок блога, каталог метаданных, эксперименты, BI-фреймворк, отдельный векторный индексатор для вики, куча исследований и заметок вокруг. Файлы, конфиги, документация, скрипты — всё вперемешку, и всё на одной машине.
И тут в полный рост вылезает та же самая проблема, только теперь без корпоративного контура.
grep отлично отвечает на вопрос, когда ты знаешь строку. Есть точное слово из ошибки — grep найдёт мгновенно. Есть имя функции — найдёт. Но половина моих вопросов к собственному коду устроена иначе: я помню смысл, а не буквы.
Хочется спросить «где обрабатывается ретрай запроса», а в коде это _with_backoff. Хочется спросить «где собираются метрики диска», а файл называется disk_usage_exporter.py. Хочется вспомнить, в каком из двадцати проектов я уже решал похожую задачу, — и вот тут grep бессилен полностью, потому что я не знаю, какие слова там подставить. Приходится угадывать имена: пробегать по трём-четырём синонимам, читать найденные файлы целиком, чтобы понять, то это или не то.
Это и есть та боль, из-за которой Qdrant вернулся в мою жизнь — уже на личный ноутбук. Много проектов и исследований, а простым грепом близкое по смыслу не находится.
Что я поднял
Схема получилась целиком локальная. Наружу не уходит ничего, лимитов и биллинга нет.
- Эмбеддинги — Ollama. Модель
mxbai-embed-large, тысяча двадцать четыре
размерности. Локальный сервер на 127.0.0.1:11434.
- Векторное хранилище — Qdrant. Образ в Docker, порт
6333, данные в
локальном каталоге. Сервис с политикой перезапуска unless-stopped, то есть поднимается сам после перезагрузки.
- Потребитель — среда, в которой я работаю. У неё есть встроенная индексация
кодовой базы: она режет файлы на смысловые блоки, шлёт их на эмбеддинги и складывает векторы в Qdrant. Мне остаётся только указать провайдера и адреса.
Тут стоит сказать про модели отдельно, потому что я сравнивал их не по описаниям, а по делу.
| Модель | Размерность | Где живёт | Роль |
|---|---|---|---|
mxbai-embed-large | 1024 | Ollama | основной вариант, лучшее качество |
nomic-embed-text | 768 | Ollama | быстрее, дешевле по времени |
text-embedding-nomic-embed-text-v1.5 | 768 | LM Studio | то же, но в другой среде |
Размерность — это не «больше значит лучше». Это просто длина вектора. Важно другое: размерность, которую выдаёт модель, должна совпадать с размерностью коллекции в Qdrant. Не совпала — коллекция не примет точку. Поэтому при смене модели коллекцию надо пересобирать, а не «переключить настройку».
Что показали проверки
Я не люблю верить конфигам. Поэтому после настройки прогнал всё руками и записал цифры.
Эмбеддинги отвечают. mxbai-embed-large вернул вектор из 1024 чисел, nomic-embed-text — из 768. Батч из шестидесяти фрагментов кода (это размер пачки по умолчанию) прошёл целиком: 60 фрагментов — 60 векторов, за одну секунду и полсекунды соответственно. Длинный вход на две с лишним тысячи токенов тоже прошёл без ошибок — а это не мелочь: у локальных серверов эмбеддингов на базе llama.cpp бывает известная беда с размером микробатча, из-за которой индексация встаёт с внутренней ошибкой. Здесь её нет.
Векторный поиск работает. Я взял три маленьких файла, отправил их в Qdrant и спросил «как обрабатывается аутентификация пользователя». Ответ пришёл со скором 0.6969 и указал ровно на нужный файл. Для сравнения, два других файла набрали 0.38 — то есть разделение чёткое, а не «всё об одном и том же».
Индексация проходит целиком. Собрал тестовый проект из четырёх файлов, включил индексацию и дождался состояния «выполнено»: 4 из 4 файлов, индекс актуален. В Qdrant появилась коллекция на 8 точек с векторами размерности 1024. Внутри каждой точки — не только вектор, но и полезная нагрузка: путь к файлу, сам фрагмент кода, начальная и конечная строка, хеш файла. То есть поиск возвращает не «где-то в проекте», а конкретный кусок с точными границами.
Смысловой поиск попадает. Дальше — самое интересное. Запросы формулировались без единого слова из кода:
| Запрос | Куда попал | Скор |
|---|---|---|
| открыть соединение с базой | src/db.py | 0.7536 |
| проверить пароль пользователя | src/auth.py | 0.7313 |
| убрать запись из кэша | src/cache.py | 0.7187 |
Ни в одном из этих файлов нет слов из запроса. Слова открыть, проверить, убрать там не встречаются вообще. Индекс нашёл их по значению.
Инкремент работает. Я дописал в проект пятый файл и подождал. Наблюдатель подхватил изменение, переиндексировал один файл, число точек в коллекции выросло с 8 до 9, и новый фрагмент сразу нашёлся поиском. Пересборки всего проекта не произошло.
Хранилище можно и не поднимать в Docker. Второй вариант — встроенная LanceDB, файловая, без отдельного сервера. Прогнал тот же проект на ней: 5 из 5 файлов, два файла данных на диске. Работает. Ограничение есть только у старых процессоров Intel, у меня Apple Silicon — поэтому оба варианта доступны, и Docker нужен исключительно ради Qdrant.
Две вещи, на которых я застрял
Обе оказались не в конфиге, а в самом механизме. Опишу, потому что наступить на них легко.
Первая: индексация требует явного согласия на проект. Даже когда провайдер эмбеддингов настроен и база отвечает, индексация не стартует сама. Есть отдельное согласие на уровне проекта, оно хранится в состоянии рабочей среды и по умолчанию пустое. Пока его не дашь, статус честно отвечает: индексация отключена для этого проекта, включите её в настройках. Я потратил на это время, потому что искал ошибку в конфигурации, а надо было просто нажать тумблер.
Вторая: провайдер эмбеддингов без ключа. В конфиге стоял внешний провайдер, а ключа к нему не было ни в файле, ни в переменных окружения. Настройка выглядела заполненной и при этом гарантированно нерабочей. Классика: конфиг валиден, а запрос упадёт на первом же вызове. Ровно поэтому я и не верю конфигам — верю прогону.
Обе поломки роднит одно: индексация проваливается молча и выглядит как «настроено, но не работает». Ни крика, ни падения — просто статус «отключено».
Чего ждать не стоит
Чтобы не создавать ложных ожиданий, сразу о границах.
- Это не заменяет grep и glob. Точный идентификатор, текст ошибки, номер строки
— по-прежнему быстрее искать подстрокой. Индекс может промахнуться на редком имени, а grep не промахивается никогда.
- Это не улучшает сам диалог с моделью. Модель эмбеддингов не пишет код и не
участвует в ответах. Она только строит векторы. Польза приходит через поиск, а не через «ум».
- Первый проход по большому репозиторию небыстрый. Мой воркспейс — это
десятки проектов; первичная индексация займёт заметное время. Дальше только инкремент: изменившиеся файлы.
- Поиск видит один проект за раз. Это рабочее пространство, а не весь диск.
Лежащие рядом репозитории, не открытые в workspace, поиску недоступны — до них всё так же придётся идти абсолютным путём.
- Есть скрытая цена зависимости. Эмбеддинги и хранилище — это два сервиса,
которые должны быть подняты. Не подняты — индексация переходит в состояние ошибки, а поиск молча возвращает пустоту. Локальность бесплатна по деньгам, но не бесплатна по вниманию.
Что в итоге
Qdrant на работе решал одну узкую задачу: дать вики из одиннадцати тысяч файлов поиск по значению, потому что по названию она уже не искалась. Дома задача оказалась шире и по сути той же: проектов и исследований стало столько, что точная подстрока перестала быть рабочим инструментом. Grep отвечает на вопрос «где эта строка». Он не отвечает на вопрос «где это по смыслу» — и никогда не отвечал.
И масштаб тут не только домашний. Сотни тысяч строк кода своей команды и соседних команд — это ровно тот случай, где grep принципиально не работает: код писали не ты и не сегодня, имён ты не помнишь, а помнишь задачу. Векторный индекс отвечает именно на это. Разница только в цене — на рабочем контуре за это платит сервер, а дома за это платит ноутбук теплом и шумом.
Практический вывод для себя я формулирую так: векторная база — это не замена поиска, а вторая его половина. Grep остаётся для точного, вектор — для смыслового. Когда ты сам писал весь этот код и разложил его по папкам, кажется, что помнишь всё. Спустя полгода и двадцать проектов выясняется, что помнишь смысл, а не имена. Векторная база хранит именно смысл — и потому работает.
Загрузка…