Как вы думаете, сколько попыток найти технический регламент или старый постмортем в корпоративной вики заканчиваются закрытой вкладкой и отчаянным вопросом в Slack? Спойлер: сильно за шестьдесят процентов. Вы платите за энтерпрайзные подписки, принуждаете команду документировать каждый инцидент, а в итоге ваши ведущие инженеры работают дорогостоящими маршрутизаторами для вопросов поддержки. Мы в Morana Labs строим индустриальный ИИ, внедряем edge-вычисления и крутим reinforcement learning под жесточайшим high-load исключительно на железе клиента, без слива данных в публичные облака. И я вижу этот паттерн повсеместно: у компании может быть идеальная серверная инфраструктура, но семантический поиск по базе знаний либо отсутствует, либо собран из хайповых фреймворков так, что лучше бы его не трогали.
Почти все делают корпоративный поиск неправильно. Бизнес свято верит, что достаточно накатить дефолтный плагин поверх системы управления знаниями. Разработчики на волне ИИ-лихорадки верят, что нужно просто взять эмбеддинги, нарезать документы кусками и бросить в векторную базу. И те, и другие получают на выходе нерелевантный мусор. Семантика в изоляции — это гарантированный провал под нагрузкой реальных задач.
Семантический поиск по базе знаний: почему классика и векторы одинаково слепы
Чтобы понять глубину проблемы, нужно посмотреть на то, как работают два противоположных подхода. Классический поиск по ключевым словам строится на инвертированных индексах и алгоритмах семейства TF-IDF, чаще всего это BM25. Он ищет точные совпадения. Если инженер поддержки вбивает запрос «клиент не может оплатить картой», а в документации проблема описана как «таймаут биллингового шлюза при обработке транзакции», BM25 вернет ноль результатов. Слова просто не пересекаются. Машина не понимает смысла, она считает частоту токенов. Из-за этого люди вынуждены играть в угадайку, пытаясь подобрать формулировку, которую использовал автор статьи три года назад.
Именно эта боль породила массовый исход в векторные базы данных. Идея звучит красиво: мы прогоняем все документы через нейросеть, превращаем текст в многомерные массивы чисел (эмбеддинги) и ищем по косинусному расстоянию. Теперь «не проходит оплата» и «ошибка биллинга» находятся близко в векторном пространстве. Смысл пойман, руководство в восторге. Но эйфория длится ровно до первого технического запроса.
Представьте, что DevOps ищет конкретную ошибку «OOMKilled pod redis-cache-7b94» или специфический код статуса «ERR-7392». Нейросетевая модель размазывает такие точные идентификаторы по всему пространству. Для нее «ERR-7392» — это просто абстрактный шум, похожий на тысячу других логов. Векторный поиск вывалит вам десятки страниц с рассуждениями о потреблении памяти, о редисе, о проблемах оркестрации, но ту самую единственную страницу, где описан воркэраунд для конкретного кода ошибки, он задвинет на сотую позицию. Векторы великолепно улавливают концепции, но они абсолютно беспомощны там, где требуется точное, посимвольное совпадение аббревиатур, артикулов или трейсов.
Гибридная архитектура: скрещиваем смысл и точные термины
Единственная жизнеспособная архитектура для корпоративного поиска — это жесткий гибрид. Вы не можете выбирать между лексическим и семантическим поиском, вам нужны оба, работающие параллельно. Мы пропускаем запрос пользователя через два независимых пайплайна: один бьет по индексу BM25, другой превращает запрос в вектор и идет в HNSW-индекс векторной базы.
Затем наступает самый сложный этап — слияние результатов. Нельзя просто перемешать два массива ссылок. Оценки релевантности (scores) у BM25 могут улетать в сотни и тысячи, тогда как косинусное расстояние вектора всегда лежит в пределах от минус единицы до единицы. Они математически несопоставимы. Для решения этой задачи используется алгоритм Reciprocal Rank Fusion (RRF). Он игнорирует абсолютные скоры и смотрит только на позицию документа в каждой из выдач.
def reciprocal_rank_fusion(bm25_results, vector_results, k=60):
fused_scores = {}
for rank, doc_id in enumerate(bm25_results, start=1):
fused_scores[doc_id] = fused_scores.get(doc_id, 0.0) + 1.0 / (k + rank)
for rank, doc_id in enumerate(vector_results, start=1):
fused_scores[doc_id] = fused_scores.get(doc_id, 0.0) + 1.0 / (k + rank)
# Сортировка по убыванию нового скора
return sorted(fused_scores.items(), key=lambda item: item[1], reverse=True)Константа `k` здесь предотвращает ситуацию, когда документ, занявший первое место в одном поиске и отсутствующий в другом, полностью подавляет документ, который стабильно занял третьи места в обеих выдачах. На практике гибридный поиск с RRF закрывает обе боли: если запрос концептуальный, его вытянет векторная часть, если в запросе есть UUID транзакции — BM25 гвоздями прибьет нужный лог к вершине выдачи.
Разрозненные источники и проклятие контроля доступа
Если вы думаете, что настройка пайплайна ранжирования — это самая большая проблема, значит, вы еще не выкатывали этот поиск в энтерпрайз. В корпоративной среде данные размазаны по зоопарку систем: Confluence, Jira, GitLab, Zendesk, внутренние таск-трекеры. Но главная катастрофа — это права доступа. Архитектура, которая игнорирует ACL (Access Control List), — это дыра в безопасности, за которую вас уволят.
Условный разработчик Вася имеет доступ к репозиториям бэкенда, но не имеет доступа к финансовым документам HR. Вы индексируете всё. Вася делает невинный семантический запрос про архитектуру сервиса, а нейросеть, найдя смысловое совпадение, услужливо подтягивает ему в выдачу кусок строго конфиденциального документа о планируемом сокращении штата, лежащего в закрытом спейсе топ-менеджмента.
Обычные базы данных умеют быстро фильтровать по ролям. Векторные индексы на базе графов HNSW так не умеют по своей природе. Если вы сначала найдете топ-100 близких векторов, а потом начнете отбрасывать те, к которым у пользователя нет доступа (пост-фильтрация), может оказаться, что доступ есть только к двум документам. Выдача пуста, пользователь бесится. Единственный путь — пре-фильтрация на уровне обхода графа, когда векторная база проверяет битовые маски прав доступа прямо во время вычисления расстояний. Это чудовищно бьет по latency, если не настроить шардирование индексов по крупным группам доступа, но другого способа отдавать защищенный контент просто не существует. Вы либо платите за железо, обеспечивающее быструю пре-фильтрацию, либо сливаете коммерческую тайну собственным стажерам.
Экономика: сколько стоит некомпетентность корпоративного поиска
Технические решения не существуют в вакууме, они измеряются деньгами. Когда операционный директор спрашивает, зачем тратить ресурсы на разработку собственного гибридного поискового движка, я показываю простую математику. Час работы сеньора стоит дорого. Каждый раз, когда поддержка эскалирует тикет на вторую или третью линию только потому, что они не смогли найти готовый регламент по ключевым словам, компания теряет деньги. Каждый раз, когда разработчик тратит половину дня на написание утилиты, которая уже полгода лежит в соседнем репозитории, компания теряет деньги.
Адекватный поиск, который понимает и смысл проблемы, и точные имена сервисов, срезает до четырех часов потерь в неделю на каждого инженера. Для штата в сто технарей это колоссальные суммы, которые сгорают ежемесячно из-за кривого инструментария. Мы разворачиваем такие системы полностью on-premise, потому что отправлять внутреннюю документацию и регламенты инфобеза через API сторонним LLM-провайдерам — это управленческая халатность. И этот подход окупается в первом же квартале. Люди просто перестают дергать друг друга за рукав, находя ответы за секунды. Это и есть та самая инженерия, которая приносит пользу, в отличие от слепого поклонения хайпу вокруг голых языковых моделей.