Главная / Статьи / Гибридный поиск для домашней лаборатории: Qdrant RRF, BM25 и свой чанкинг

Гибридный поиск для домашней лаборатории: Qdrant RRF, BM25 и свой чанкинг

Самостоятельно размещённый ассистент для хоумлаба режет на чанки заметки, таблицы, диаграммы Draw.io и логи Loki, а затем ищет по плотным и разреженным векторам с RRF и переранжированием кросс-энкодером.

1386 слов

Перерывать полсотни markdown-файлов или смотреть docker logs в трёх окнах терминала ради одного IP-адреса — утомительный способ управлять хоумлабом. Заметки — это хорошо. Мгновенные ответы от гибридного векторного поиска — лучше. В этом разборе собирается полностью самостоятельно размещённый ассистент, который индексирует заметки из Nextcloud, сетевые диаграммы Draw.io и живые потоки логов Grafana Loki. Основное внимание — механике: как данные режутся на чанки, как ведут себя эмбеддинги с точки зрения математики и как Qdrant находит и объединяет результаты.

Парадокс документации

В этой лаборатории работают многоузловой гипервизор Proxmox, файрвол OPNsense, маршрутизирующий несколько VLAN, и десятки Docker-контейнеров вроде Jellyfin, Radarr, Prowlarr, Grafana и Home Assistant. Со временем документация расползлась по четырём местам:

  1. Сетевые заметки и страницы-инструкции в Nextcloud Collectives, хранящиеся как файлы .md.
  2. Инвентаризация железа и таблицы IP-адресов в Excel-файлах .xlsx.
  3. Топологические схемы, нарисованные в Draw.io и сохранённые как файлы .drawio.
  4. Живые логи контейнеров, которые собирает 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-контейнера в стойке.