На работе у меня есть вики из одиннадцати с лишним тысяч 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-large1024Ollamaосновной вариант, лучшее качество
nomic-embed-text768Ollamaбыстрее, дешевле по времени
text-embedding-nomic-embed-text-v1.5768LM 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.py0.7536
проверить пароль пользователяsrc/auth.py0.7313
убрать запись из кэшаsrc/cache.py0.7187

Ни в одном из этих файлов нет слов из запроса. Слова открыть, проверить, убрать там не встречаются вообще. Индекс нашёл их по значению.

Инкремент работает. Я дописал в проект пятый файл и подождал. Наблюдатель подхватил изменение, переиндексировал один файл, число точек в коллекции выросло с 8 до 9, и новый фрагмент сразу нашёлся поиском. Пересборки всего проекта не произошло.

Хранилище можно и не поднимать в Docker. Второй вариант — встроенная LanceDB, файловая, без отдельного сервера. Прогнал тот же проект на ней: 5 из 5 файлов, два файла данных на диске. Работает. Ограничение есть только у старых процессоров Intel, у меня Apple Silicon — поэтому оба варианта доступны, и Docker нужен исключительно ради Qdrant.

Две вещи, на которых я застрял

Обе оказались не в конфиге, а в самом механизме. Опишу, потому что наступить на них легко.

Первая: индексация требует явного согласия на проект. Даже когда провайдер эмбеддингов настроен и база отвечает, индексация не стартует сама. Есть отдельное согласие на уровне проекта, оно хранится в состоянии рабочей среды и по умолчанию пустое. Пока его не дашь, статус честно отвечает: индексация отключена для этого проекта, включите её в настройках. Я потратил на это время, потому что искал ошибку в конфигурации, а надо было просто нажать тумблер.

Вторая: провайдер эмбеддингов без ключа. В конфиге стоял внешний провайдер, а ключа к нему не было ни в файле, ни в переменных окружения. Настройка выглядела заполненной и при этом гарантированно нерабочей. Классика: конфиг валиден, а запрос упадёт на первом же вызове. Ровно поэтому я и не верю конфигам — верю прогону.

Обе поломки роднит одно: индексация проваливается молча и выглядит как «настроено, но не работает». Ни крика, ни падения — просто статус «отключено».

Чего ждать не стоит

Чтобы не создавать ложных ожиданий, сразу о границах.

  • Это не заменяет grep и glob. Точный идентификатор, текст ошибки, номер строки

— по-прежнему быстрее искать подстрокой. Индекс может промахнуться на редком имени, а grep не промахивается никогда.

  • Это не улучшает сам диалог с моделью. Модель эмбеддингов не пишет код и не

участвует в ответах. Она только строит векторы. Польза приходит через поиск, а не через «ум».

  • Первый проход по большому репозиторию небыстрый. Мой воркспейс — это

десятки проектов; первичная индексация займёт заметное время. Дальше только инкремент: изменившиеся файлы.

  • Поиск видит один проект за раз. Это рабочее пространство, а не весь диск.

Лежащие рядом репозитории, не открытые в workspace, поиску недоступны — до них всё так же придётся идти абсолютным путём.

  • Есть скрытая цена зависимости. Эмбеддинги и хранилище — это два сервиса,

которые должны быть подняты. Не подняты — индексация переходит в состояние ошибки, а поиск молча возвращает пустоту. Локальность бесплатна по деньгам, но не бесплатна по вниманию.

Что в итоге

Qdrant на работе решал одну узкую задачу: дать вики из одиннадцати тысяч файлов поиск по значению, потому что по названию она уже не искалась. Дома задача оказалась шире и по сути той же: проектов и исследований стало столько, что точная подстрока перестала быть рабочим инструментом. Grep отвечает на вопрос «где эта строка». Он не отвечает на вопрос «где это по смыслу» — и никогда не отвечал.

И масштаб тут не только домашний. Сотни тысяч строк кода своей команды и соседних команд — это ровно тот случай, где grep принципиально не работает: код писали не ты и не сегодня, имён ты не помнишь, а помнишь задачу. Векторный индекс отвечает именно на это. Разница только в цене — на рабочем контуре за это платит сервер, а дома за это платит ноутбук теплом и шумом.

Практический вывод для себя я формулирую так: векторная база — это не замена поиска, а вторая его половина. Grep остаётся для точного, вектор — для смыслового. Когда ты сам писал весь этот код и разложил его по папкам, кажется, что помнишь всё. Спустя полгода и двадцать проектов выясняется, что помнишь смысл, а не имена. Векторная база хранит именно смысл — и потому работает.