Гибридный поиск для домашней лаборатории: Qdrant RRF, BM25 и свой чанкинг
Самостоятельно размещённый ассистент для хоумлаба режет на чанки заметки, таблицы, диаграммы Draw.io и логи Loki, а затем ищет по плотным и разреженным векторам с RRF и переранжированием кросс-энкодером.
Перерывать полсотни markdown-файлов или смотреть docker logs в трёх окнах терминала ради одного IP-адреса — утомительный способ управлять хоумлабом. Заметки — это хорошо. Мгновенные ответы от гибридного векторного поиска — лучше. В этом разборе собирается полностью самостоятельно размещённый ассистент, который индексирует заметки из Nextcloud, сетевые диаграммы Draw.io и живые потоки логов Grafana Loki. Основное внимание — механике: как данные режутся на чанки, как ведут себя эмбеддинги с точки зрения математики и как Qdrant находит и объединяет результаты.
Парадокс документации
В этой лаборатории работают многоузловой гипервизор Proxmox, файрвол OPNsense, маршрутизирующий несколько VLAN, и десятки Docker-контейнеров вроде Jellyfin, Radarr, Prowlarr, Grafana и Home Assistant. Со временем документация расползлась по четырём местам:
- Сетевые заметки и страницы-инструкции в Nextcloud Collectives, хранящиеся как файлы
.md. - Инвентаризация железа и таблицы IP-адресов в Excel-файлах
.xlsx. - Топологические схемы, нарисованные в Draw.io и сохранённые как файлы
.drawio. - Живые логи контейнеров, которые собирает Grafana Loki.
Любой инцидент или изменение моста означали ручную рутину: открыть Nextcloud, найти нужную заметку, открыть диаграмму, чтобы проверить подключения интерфейсов, пролистать таблицу, а потом запустить docker logs и посмотреть ошибки контейнеров. Раздражает, и долго.
Публичные чат-боты тут не помогут. Они ничего не знают о приватных подсетях вроде 10.27.30.2, мостах Proxmox вроде vmbr1 или привязках сетевых карт вроде igc0, а на вопросы о частной инфраструктуре уверенно выдумывают чепуху. Поэтому знания должны оставаться локальными и подтягиваться в момент вопроса.
Чанкинг каждого формата по его правилам
Стандартные RAG-схемы режут текст каждые несколько сотен символов или слов. Для данных хоумлаба это губительно. Фраза вроде «Proxmox Host 1 подключён к порту 2» может разойтись по двум чанкам, и связь между сервером и портом пропадёт. Поэтому у каждого формата своя стратегия.
Markdown: разбиение по заголовкам
Документы режутся по структурным границам, то есть по #, ## и ###. Nextcloud Collectives хранит страницы в Markdown, так что заголовки уже на месте. Разбиение по ним сохраняет путь раздела, и чанк — это не оторванный текст, а фрагмент со всей иерархией:
Document: Home Server Setup | Section: Network > Firewalls > OPNsense
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
OPNsense VM 101 is configured with passthrough interface igc0…
Таблицы: строки как предложения
С помощью openpyxl каждая строка с железом извлекается и переписывается в явное утверждение «ключ — значение». Сырые таблицы теряют заголовки столбцов, когда наивный чанкер превращает их в текстовые блоки, а переписанная строка самодостаточна:
Row 4: Device: Proxmox Host 1 | Port: Port 2 | IP: 10.27.10.3 | Connection: Airtel Router
every single row becomes an independently searchable record in vector storage.
Каждая строка становится отдельной записью в векторном хранилище, по которой можно искать.
Draw.io: спрятанный XML
Чтобы читать сетевые диаграммы, не нужны дорогой OCR или компьютерное зрение: файлы .drawio — это просто сжатые XML-строки. Первый шаг делает zlib из Python: декодируем base64, затем распаковываем через zlib.decompress(data, -15). Из полученного XML-дерева элементов <mxCell> извлекаются узлы, устройства и рёбра соединений, которые записываются так:
## Nodes
- Node: Proxmox Host 1 (10.27.10.3)
- Node: OPNsense Firewall VM (101)
## Connections
- Connection: Airtel WAN Router -> Port 2 (igc0) -> OPNsense VM (101)
Соединения получаются точными — без ошибок распознавания и без нагрузки на GPU.
Логи Loki: два уровня
Если залить сырые строки Docker-логов из Loki в векторное хранилище, оно утонет в миллионах бессмысленных INFO-сообщений. Этого позволяет избежать адаптивная двухуровневая схема:
- Уровень 1 — инциденты. Всплески критических ошибок вроде
ETIMEDOUT, HTTP 500, сбоев и падений контейнеров режутся на чанки с точными метками времени, метками контейнеров вроде jellyfin или radarr и стектрейсами. - Уровень 2 — рутинная активность. Обычный «пульс» сворачивается в сводки по 15 минут.
[Tier-1 Incident Event]
Timestamp: 2026–09–14 18:30:12 UTC | Service: jellyfin | Container: jellyfin
Log Event: ERR_FR_REDIRECTION_FAILURE attempting to connect to 10.27.30.2:8096
Два пространства эмбеддингов
Зачем доверять одной модели эмбеддингов, если можно объединить два математических пространства?
Плотные векторы на all-MiniLM-L6-v2
Это шестислойный трансформер MiniLM, выдающий 384-мерные векторы. Релевантность измеряется косинусным сходством между вектором запроса A и вектором чанка B.
Плотные векторы улавливают смысл. Модель понимает, что firewall — концептуальный сосед OPNsense, а хранилище связано с пулом ZFS. Их слабое место — числа, которые сглаживаются. 10.27.30.2 и 10.27.30.3 оказываются почти в одной точке, и для IP-адресов это даёт ошибки точности.
Разреженные векторы на FastEmbed BM25
Разреженная часть использует частоту термина, взвешенную обратной документной частотой, и оценивает совпадения ключевых слов по коллекции документов D для терминов запроса q.
Здесь сила — в точности. Технические токены вроде IP-адресов (10.27.30.2), интерфейсов (igc0, vmbr1), портов (8096) и кодов ошибок (ETIMEDOUT) совпадают идеально. Цена в том, что синоним вместо буквального слова ничего не находит.
Хранение, слияние и переранжирование
Оба представления попадают в базу Qdrant на localhost:6333, в одну коллекцию с именем homelab. Запрос запускает гибридный поиск.
Reciprocal Rank Fusion
Плотные косинусные оценки (скажем, 0,85) и разреженные оценки BM25 (скажем, 14,2) живут в совершенно разных шкалах, так что складывать их напрямую бессмысленно. Вместо этого Qdrant объединяет ранжированные списки кандидатов методом Reciprocal Rank Fusion.
RRF вознаграждает согласие. Документ, который стоит первым в разреженном поиске благодаря точному совпадению с 10.27.30.2 и вторым в плотном благодаря сильной смысловой близости, после слияния взлетает наверх.
Переранжирование кросс-энкодером
Би-энкодеры вроде MiniLM при индексации кодируют запрос и документ по отдельности. Чтобы отсеять ложные срабатывания, десять лучших кандидатов после RRF проходят через кросс-энкодер ms-marco-MiniLM-L-6-v2, который пропускает пару «запрос — документ» через self-attention трансформера вместе.
Чанки с оценкой ниже MIN_RERANK_SCORE = -2.0 отбрасываются. Если шум выбил бы все чанки, конвейер аккуратно откатывается: спекулятивный каскад оставляет кандидата с лучшей оценкой.
Сырой логит кросс-энкодера (z) превращается в понятную вероятность релевантности через сигмоиду.
Разные значения логита дают примерно такие вероятности:
При z = -2.0 вероятность, что чанк релевантен, около 11,9%. Всё, что выше, несёт полезный контекст или хотя бы слабое тематическое пересечение, которое может помочь языковой модели. Всё, что ниже, быстро уходит к нулю — при z = -4.0 это всего 1,8% — и обычно означает загрязнение из чужой области, например всплески сырых Docker-логов, примешанные к вопросу о подсетях.
Query: "What is the OPNsense management IP?"
[Top Retrieved Chunks]
Chunk 1: "OPNsense Firewall VM 101 IP: 10.27.10.1..." ──► Logit: +4.8 (Keep)
Chunk 2: "Proxmox host bridge vmbr0 LAN 10.27.10.3..." ──► Logit: +0.4 (Keep)
Chunk 3: "Grafana Loki log error: connection refused" ──► Logit: -4.2 (Filtered out!)
Без этого порога третий чанк попал бы в контекст модели, и ошибка из лога могла бы сбить её на неверный ответ. Порог -2.0 работает как автоматический вышибала у дверей контекстного окна.
Проверка
Автоматический регрессионный скрипт tests/run_regression.py гоняет систему по тестовому набору из 17 вопросов о хоумлабе:
Ran 3 tests in 1.307s
OK
[Qdrant Connected] Vector DB ready at http://localhost:6333 (Collection: 'homelab').
[Regression Assertion] Hit@1 Accuracy Rate: 100.0% | MRR: 1.0000
```
* **Hit@1 Accuracy Rate**: **100.0%**
* **MRR (Mean Reciprocal Rank)**: **1.0000**
* **Time-to-First-Token (TTFT)**: **640.8 ms**
Всё вместе
Мультиформатный чанкинг (заголовки Markdown, строки Excel, распакованные XML-графы Draw.io и двухуровневые логи Loki), плотные эмбеддинги MiniLM и разреженные BM25, слияние рангов в Qdrant и переранжирование кросс-энкодером вместе дают приватного ассистента на своём железе. Он может ответить на вопрос о любом кабеле, IP-адресе и ошибке Docker-контейнера в стойке.