
Исходная идея
Часто приходится искать в огромной куче документов какую нибудь частную, специфичную вещь. На данный момент, только лично у меня более 2Gb различных pdf файлов. Зачастую разбросанных не системно. И хотя обычно представляешь где искать, но это отнимает время. Захотелось иметь инструмент ускоряющий поиск.
Для запуска всего этого есть сервер в домашней сетке
AMD Ryzen 7, RTX 5060ti 16Gb, 32Gb ОЗУ
Ubuntu 24.04, ollama v0.12.3. nvidia v580.95.05. CUDA v13.0, docker qdrant/qdrant:latest
Первая мысль: “a не до обучить ли какую ни будь сетку на данных из документов”.
Но первые же эксперименты показали, то даже просто формирования искусственного набора QA (вопрос+ответ) на 16Gb GPU не реально.
Формирование одной записи QA по одному чанку – это 3..10 сек. на каждый чанк нужно хотя бы 5 вопросов. И все надо вычитывать, поскольку каждые 4-я пара QA это либо бред, либо LLM уходит от темы. Для ~1Gb исходных данных это очень большие затраты.
Остался вариант "векторная БД + RAG"
Казалось все просто. Накидать за пару вечеров код, что бы получить векторный поиск по смыслу и прикрутить генерацию LLM по вопросу и найденному чанку. Обычная «наивная» реализация RAG исходя из ограничения аппаратных ресурсов.
Для кода был выбран python (v3.12.3)
Не очень люблю питон. В первую очередь из за отсутствия реализации полноценной многопоточности. Но, в данном случае, используется как скриптовый язык обработки данных и для "домашних" задач сойдет.
Исходя из объективно/субъективных требований выбрал архитектуру
Ollama с вызовом http+json API ('/api/embed' + '/api/generate') напрямую.
Qdrant в виде docker контейнера (быстро и просто). Обращение через http+json api
Хранение документов и картинок страниц на файловой системы и ссылками на них в payload записей Qdrant.
Одностраничный сайт для поиска без идентификации/аутентификации (домашняя сеть же)
Так оно и оказалось. Написать ПО оказалось не долго. Действительно ничего принципиального сложного.
Хотя потратил кучу времени на создание одно страничного сайта.
Под впечатлением статей типа «как я бросил программировать и занялся Вайб-кодингом �� заработал за 1 день несколько миллионов», решил принципиально создать одностраничный примитивный сайт вообще не трогая код и только вымаливая у LLM нужный мне вариант.
Я тряпка.
Я сдался после, наверное, всего 7-8 попыток. Когда объем промта стал близок к объему кода, а изначально сгенерированный LLM более менее приличный шаблон кода стал напоминать клубок переваренной спагетти. Наверное, на колени нужно было встать перед клавиатурой и вымаливать у LLM (не локальной) рабочий вариант. Взял в итоге один из сгенеренных вариантов и доделал руками за 15 минут.
А всего-то нужно было перенаправить в режиме stream поток токенов ответа от ollama через серверную часть сайта напрямую в поле html страницы, что бы была иллюзия интерактивности.
Я не знаю как вайб-кодеры, как они (утверждают "не поправляя код") умудряются создавать что-то более менее сложное, а потом это еще запросами к LLM допиливать. LLM это какой-то генератор банальностей и распространенных ошибок из интернета. Что-то простое - без проблем. Шаг вправо/влево от шаблонных решений и получаешь убедительный бред, который не работает и не может работать.
Но, вылезли первые нежданчики. Ответы в топе были, мягко скажем, не адекватны вопросам.
Ошибка выбора модели для эмбединга
Выбрал модель "embeddinggemma:latest".
Зря.
Модель, выложенная месяц/два назад, не обязательно самая хорошая. Что с ней не так – не знаю. Но время выдачи вектора эмбединга в ollama у нее не предсказуемое. От 0.2сек до 30 сек. Обработка чанков для тестового набора документов (300Mb PDF) заняла 4:28:39. В отличие от, например mxbai-embed-large:latest которая выполнила то же самое за 0:04:49. Хотя именно эта ошибка выбора сподвигла на дальнейшие эксперименты.
Внимательно просмотрев код, данные чанков, логи и не найдя явных ошибок ни в логике ни в данных, решил начать с нуля. Т.е. с примеров на «причесанных данных», которые приводятся в Интернете и идеально работают.
Вдруг я что-то упустил, сразу пытавшись загрузить все что мне нужно в векторную БД и не получив ожидаемый результат на реальных данных. Отложил в сторонку реальные документы и сделал песочницу.
Эксперименты в песочнице с разными моделями
В статьях постоянно приводят же примеры прямо в jupyter notebook «вот 10 строк залили в chromadb, а вот сразу нашли нужный текст по запросу». Решил пойти по шагам от этого. Что бы понять почему на реальных данных все так красиво не работает.
Данные для тестов нагенерил конечно не сам (LLM), но вычитал и явного бреда там нет. (https://github.com/mmMikeKn/local-vector-db/tree/main/py_tests/tests_local)
Как оказалось, выбор модели эмбеддинга имеет решающее значение. И не нужно никому доверять в оценках модели. Нужно проводить тесты самому. Графическое отображение результатов тестов по моделям показался более наглядным чем просто обобщенные цифры.
-
Точка на диаграмме – это чанк в списке ответа на поиск. Оси: X - номер вопроса, Y - дистанция между вектором вопроса и вектором найденного чанков в топе выдачи.
Зеленый цвет – найденный чанк прямо соответствует вопросу (в тестовых данных). Остальные точки серые – это другие чанки в списке найденных. Просто для наглядности что бы оценить их "дистанцию" до вопроса.
Красный цвет – ни один найденный чанк не соответствует вопросу.
Признаки наилучшего поиска на диаграмме
Зеленая точка лежит ниже всех и максимально близко к 0 (ось Y). Т.е. "дистанция" между вектором эмбеддинга вопроса и ответа минимальная.
Серые точки лежат заметно выше зеленой – хорошая дифференциация ответов в списке найденных ответов.
Красный точек нет (ну или очень мало)
Результаты всем моделям можно посмотреть в https://github.com/mmMikeKn/local-vector-db/tree/main/py_tests/tests_local/plt
Все тесты сделаны на одном и том же наборе данных
Модель embeddinggemma:latest
Несмотря на то, что модель свежая, но она отвратительная по качеству. Размер 621 MB (размер привожу в том, сколько фактически займет GPU памяти. Для меня это важней, чем количество параметров), контекст 2048, вектор 768. Время расчета эмбеддинга не предсказуемо в широких пределах. От 300ms до 30 сек (!). Почему? Не знаю. Не вникал и не интересно. Есть из чего выбирать.
Диаграммы тестов


Модель (русскоязычная) evilfreelancer/FRIDA:latest
Просто не грузится в ollama с ошибкой INFO source=sched.go:438 msg="Load failed" model=/usr/share/ollama/.ollama/models/blobs/sha256-b94dd828625ffee6bd86723e18c5bf8b7a452d71d8d981dde45a974134c8bb03 error="llama runner process has terminated: exit status 2". Хотя находится в списке доступных для ollama и версия ollama, судя по описанию, подходящая. Не стал разбираться. Тем более, что у нее окно контекста всего 512
Модель evilfreelancer/enbeddrus:latest
Позиционируется как до обученная на русском. Размер 336MB, контекст 512.
Да простят меня авторы модели, но, по моим тестам, оказалась хуже мультиязычных схожего размера.
Диаграммы тестов


На этом закончил эксперименты с «русскими» моделями и занялся мультиязычными. Все они, кроме 'qwen3-embedding' оказались приблизительно похожи +/- по результатам.
Но явно видна корреляция между общим размером модели в памяти GPU (оцениваю по ресурсам GPU) и результатами. Чем больше, тем лучше результат. Что ожидаемо.
После всех экспериментов остановился на
Модель qwen3-embedding:latest
Довольно «тяжелая» модель 4.7 GB с огромным контекстным окном и размерностью вектора в 4096. Основной недостаток – высокое потребление ресурсов GPU. Слышно, как включается вентилятор на карте при обработке всех документов. На остальных моделях такого нет. Но время расчета вектора стабильное в рамках одного и того же размера чанка. Для чанка в 2-3Kb байт (условно) это где-то 1000-1200ms. Но полностью использовать огромное контекстное окно данной модели для данной цели не стоит.
Сильно увеличивается потребление ресурсов GPU на больших данных контекста.
Крупные чанки документа приводят к не релевантным результатам поиска по коротким вопросам.
Диаграммы тестов



В результате модель "qwen3-embedding:latest" и была выбрана мной как рабочая для получения эмбеддинга по всем моим документам.
За ночь (удалил лог не посмотрев время) все обработалось и загрузилось в qdrant.
Мои впечатление и выводы от поиска по векторной БД и RAG
Восторженные статьи замалчивают недостатки как векторного поиска, так и RAG. В области AI вообще тренд "щенячьего восторга". Мне это всегда казалось сомнительным («..и вы то же говорите, что 10 раз за ночь») и собственный опыт это подтверждает. Да <сараказм>AI</сараказм> инструменты. Да удобные. Но вот не прям "ого..ого" с решением вопроса "42".
Контекстный поиск, совсем не заменяет поиск по подстроке
Если вам нужно найти что-либо очень конкретное, например, документ (страницу документа) со строкой вида «Требование 492», то вероятность, того, что страница с этим текстом попадет в топ результатов поиска по вектору контекста близка к 0.
Потому, что поиск по контексту и поиск по подстроке (конкретной) это принципиально разные вещи.
Следующее, что я буду прикручивать – это поиск по подстроке в чанках. К сожалению, qdrant на это плохо ориентирован. Но на 1-2Гб данных в конце концов можно и “grep” поискать. А задача типичная.
Малейшее изменение формулировки вопроса и получите другой набор результатов в топе выдачи. Все красиво на тестовых данных. А когда у вас есть ~100 очень похожих по стилю документов, на близкую тему, в которых полно таблиц, диаграмм, бюрократически/технически жаргонных оборотов и т.п., то нужно как минимум знать, что должно найтись.
Нужно постараться, что бы за несколько попыток поиска с разными формулировками найти нужный документ (хотя бы документ, не говоря уж о нужной странице). И никогда точно не знаешь: не нашлось потому что просто нет, или потому что далеко по контексту для данной модели эмбеддинга.
Все что работает «как бы хорошо» в контекстном поиске по товарам Озона (хотя лично меня он бесит. А вас? Недоделка какая-то, а не поиск), очень плохо работает для задачи поиска в технической документации.
Теперь о RAG
Лично мне, он был не особо нужен. Достаточно было png страниц документов на экране для быстрой оценки и ссылок на документы для скачивания. Но прикрутить было не долго и прикрутил (отдельной кнопкой на странице сайта). Но:
Скармливать весь документ в контекст запроса – не тянет GPU 16Gb. А для облачных LLM быстро токены кончатся, если в контекст передавать весь документ.
-
Чанк, по реальному документу, это, зачастую, обрывки мыслей и с этим LLM не справляется. Что локальная типа Qwen3-Coder-REAP-25B-A3B-MXFP4_MOE-GGUF:latest, что чатботы «больших» LLM.
Несут банальности на которых обучались, игнорируя содержимое чанка.
Впадают в откровенный бред.
Результаты (ну если не фиксировать параметр options.seed в запросе к LLM) будут каждый раз разные. И никогда не будешь знать: каких то данных в ответе LLM нет потому что их вообще нет, или потому что механизм генерации очередного токена в LLM обошел нужное тебе ветвление предсказаний очередного токена.
В общем то, что "говорит" LLM по вопросу + текст чанка меня сильно разочаровало. Хотя возится и подбирать системные промты и прочее не стал.
Даже большие LLM иногда такой убедительный бред несут, когда их спрашиваешь про узкие/частные случаи, которых не было или было мало в исходных данных обучения. А искать приходится не "почему небо голубое?", а информацию которой просто нет в Интернете (служебные документы и т.п.).
Ссылка на проект [GitHub](https://github.com/mmMikeKn/local-vector-db)
Alex-Freeman
Первая ступень с YOKE, аж жабры зачесались)