Локальный семантический поиск как MCP-сервер: поиск по смыслу в заметках без интернета
Текстовый поиск по конспектам созвонов часто упирается в несовпадение ключевых слов: пользователь помнит слово «найм», а в файле записано «собеседование». Чтобы обойти это, авторы создали собственный MCP-сервер, который выполняет семантический поиск прямо на устройстве, используя локальные модели. Решение полностью автономно: заметки не покидают компьютер, а связь с клиентами типа Claude Code осуществляется через стандартные протоколы без внешних API.
# Локальный семантический поиск как MCP-сервер: нарезка под лимит модели и кэш
Проблема поиска в персональных заметках заключается в разрыве между естественной памятью пользователя и линейным поиском по тексту. Если в конспекте зафиксировано «прошел собеседование», простой поиск по запросу «найм» ничего не выдаст. Передача данных во внешние сервисы для получения эмбеддингов (векторного представления смысла) требует подключения к интернету и отправки конфиденциальной информации. Автор статьи предложил решение в виде автономного MCP-сервера (Model Context Protocol), который работает локально и интегрируется в среду разработки или чат-боты без использования платных API.
Архитектура решения и стек технологий
Основа системы состоит из движка поиска, написанного на Python, и сервера MCP, который предоставляет инструменты для взаимодействия с движком. Стек включает библиотеку sentence-transformers для генерации эмбеддингов и модель paraphrase-multilingual-MiniLM-L12-v2, способную понимать более пятидесяти языков. Модель весит около 120 МБ и работает без необходимости постоянного соединения с сервером.
Сервер предоставляет три основных инструмента: 1. Поиск по смыслу (`search_notes`): возвращает фрагменты текста с оценкой близости к запросу. 2. Список заметок (`list_notes`): отображает файлы вместе с их датой изменения и размером. 3. Чтение заметки (`read_note`): возвращает содержимое файла целиком.
Все инструменты работают в режиме только-чтения, что подтверждается специальными аннотациями в протоколе MCP, сигнализируя клиенту об отсутствии побочных эффектов.
Управленческая нарезка текста под лимит модели
Одной из ключевых технических сложностей является жесткий лимит контекста для некоторых языковых моделей. Модель MiniLM имеет ограничение в 128 токенов на вход. Если попытаться подать длинный абзац, модель «молча» обрежет его, и часть смысла будет потеряна без предупреждения об ошибке.
Для решения этой проблемы автор разработал двухэтапную систему нарезки текста:
1. Разбиение на кирпичики: Исходный текст разбивается на строки или предложения. Если строка слишком длинная, она разделяется по знакам препинания. Если предложение не укладывается в лимит символов (ориентир — 500–600 символов), оно дробится принудительно. 2. Проверка по токенам: Каждый «кирпичик» проверяется через токенизатор. Лимит переводится в количество токенов с учетом запасов под служебные метки модели. Если фрагмент всё равно не влезает, он режется точно по границе токенов через декодирование частей векторов.
При объединении фрагментов в итоговые куски для поиска используется перекрытие в 80 символов между соседними блоками. Это гарантирует, что мысль, разорванная на стыке кусков, не теряется полностью, так как её контекст присутствует в начале следующего фрагмента. Статистика показала, что для русского языка токенный лимит срабатывает раньше символьного, поэтому фрагменты получаются короче целевых 500 символов, но при этом все данные учитываются без потерь.
Кэш, понимающий устаревание данных
Пересчет эмбеддингов для всех заметок при каждом запросе был бы неэффективен. Поэтому автор реализовал кэширование векторов в файле .index/cache.pkl. Однако простое сохранение векторов недостаточно: кэш становится недействительным, если изменится модель, параметры нарезки или сама структура кэша.
Для отслеживания валидности кэша используются несколько механизмов: * Версионирование: в метаданные записывается версия структуры кэша. Увеличение версии означает полную пересборку. * Сопоставление параметров: хранится словарь текущих настроек (имя модели, лимиты нарезки). Если они изменились, старые векторы игнорируются. * Контроль времени изменения: для каждого файла в кэхе сохраняется mtime (время изменения). При переборе файлов на диске пересчитываются только те, чья дата изменится или файла больше нет на диске.
Обновление кэша происходит атомарно через временный файл, чтобы сбой записи не оставил невалидный файл в системе. Если чтение кэша по какой-то причине не удается, сервер автоматически перестраивает индекс заново.
Поиск: сочетание смысла и точных фактов
Чистый семантический поиск, основанный на косинусной близости векторов, отлично находит тексты по теме, но может терять точность при запросе конкретных имен, цифр или редких терминов. Модель, ищущая «акции», может упустить файл с названием компании.
Для исправления этого в алгоритм введен фоллбэк (запасной вариант) — гибридный поиск по ключевым словам. Он активируется в двух случаях: 1. Запрос очень короткий (до двух слов), так как для таких запросов векторное представление может быть слишком обобщенным. 2. Максимальная семантическая близость ниже заданного порога (0.35).
Оценка по ключевым словам учитывает точные совпадения слов и подстроки для длинных слов (от 4 символов), чтобы не ловить случайные вхождения коротких слов в тексте. Итоговый рейтинг фрагмента складывается из веса семантической близости и взвешенной суммы ключевых совпадений. В ответе пользователю возвращается именно чистая близость, чтобы он мог сам оценить достоверность смысла.
Технические ловушки реализации
При разработке MCP-сервера автор выделил три частые ошибки, которых стоит избегать.
Первая ловушка — лишний вывод в стандартный поток (stdout). Протокол MCP передает данные через вход и вывод. Любая лишняя строка в stdout от библиотек или логики (например, прогресс-бары или предупреждения о длине токенов) разрушит формат сообщений и сломает связь с клиентом. Решение: отключить прогресс-бары в библиотеках машинного обучения и направить всю диагностику в stderr (ошибочный поток).
Вторая ловушка — безопасность пути при чтении файлов. Инструмент read_note принимает имя файла от модели, которая могла получить это имя из текста заметки или другого контекста. Слепое чтение по имени файла открывает доступ к любым путям, включая системные директории. Необходимо нормализовать путь и жестко проверить, что он находится внутри рабочей папки с заметками, запретив переходы через ...
Третья ловушка — неоднозначность имен файлов. Если в разных подпапках есть файлы с одинаковыми именами, модель может запросить не тот файл. При ошибке чтения сервер должен предлагать список всех файлов с таким именем, чтобы пользователь или модель могли уточнить относительный путь.
Заключение
Создание локального поискового движка показывает, что сложные функции ИИ могут работать полностью автономно на стороне клиента. Это гарантирует конфиденциальность данных и экономит ресурсы, исключая необходимость в платных токенах. Несмотря на сложность правильной нарезки текста и настройки кэша, итоговое решение предоставляет удобный интерфейс для работы с огромными массивами текстовой информации через привычные инструменты разработки.