Поисковый индекс как отдельная копия библиотеки
В библиотеке есть собственные Markdown-документы. Редактор меняет заголовок, раздел и текст, а читатель хочет искать слова, выбирать тему и видеть подходящий фрагмент. В проекте «Индекс» вынесем подготовленную поисковую копию в OpenSearch. Результат первого урока — договор о том, какие данные передаём движку, что считаем исходником и как узнаём один материал после переименования.
Предполагается знакомство с HTTP, JSON и небольшими программами Python. Используем Python 3.13 и OpenSearch 3.9.0. Все команды серии предназначены для отдельной учебной среды читателя. При написании статьи сервер, программы и тесты не запускались; описанные ответы являются ожидаемыми. Официальная история версий фиксирует выбранную версию движка.
Источник и поисковая проекция
Возьмём материал article-107 с заголовком «HTTP и кеширование». Его исходник лежит в documents/http-cache.md, а постоянная ссылка указывает на /articles/http-cache.html. В поисковую запись попадут заголовок, основной текст, раздел, метки, видимость и номер редакции. HTML всей страницы не нужен: меню, нижние ссылки и рекламные блоки только добавят повторяющиеся слова.
Назовём такую запись проекцией. Она подготовлена для конкретного способа чтения, а не заменяет редакционный документ. Если индекс потерян, мы создадим его заново и повторно прочитаем Markdown. Если потерян Markdown, наличие поисковой записи ещё не доказывает, что удалось восстановить комментарии редактора, структуру файла и историю правок. Это различие определяет резервное копирование и право изменять данные.
Первый полный пример метаданных использует JSON внутри разделителей Markdown. JSON является допустимой записью объекта в YAML, но наш учебный загрузчик читает только этот узкий формат. Перед телом файла находятся следующие строки:
---
{"doc_id":"article-107","title":"HTTP и кеширование","url":"/articles/http-cache.html","section":"backend","tags":["http","cache"],"identifiers":["http","etag"],"audience":"public","published":true,"revision":1,"updated_at":"2026-10-12T09:00:00Z"}
---
# HTTP и кеширование
Браузер сохраняет представление ресурса и проверяет его актуальность через ETag.
У doc_id и url разные обязанности. Первый идентифицирует материал для обновления и удаления. Второй предназначен для перехода читателя. Переименование файла или исправление адреса не должны автоматически создавать другой документ в поиске. Поэтому идентификатор не вычисляется из текущего заголовка, номера строки или хеша текста.
Стабильность также требует дисциплины. Если вы скопировали файл для нового материала, измените doc_id. Загрузчик отклонит два документа с одинаковым идентификатором до передачи данных серверу. Такой отказ полезнее тихого перезаписывания: иначе видимый результат зависел бы от порядка обхода папки, а ошибка выглядела бы как непредсказуемый поиск.
Какие изменения увидит читатель
В OpenSearch запись имеет идентификатор _id; в нашем проекте он совпадает с doc_id. После изменения заголовка до «HTTP: кеширование ответа» отправим полную новую проекцию под тем же _id. Ожидаемое состояние — один материал с новым заголовком, а не две похожие карточки. Номер revision увеличивает редактор, чтобы изменение можно было сопоставить с исходником.
Для обнаружения изменений загрузчик дополнительно вычисляет content_hash. Это контрольная сумма канонического JSON всей подготовленной записи. Она отвечает на вопрос, поменялось ли содержимое, которое индексируем. Она не определяет предметную идентичность и не доказывает, что документ лучше прежнего. Даже небольшое исправление метки может поменять хеш без изменения основного текста.
Поля published и audience описывают видимость. Документ для сотрудников может существовать в учебном индексе, но публичный запрос обязан исключать его ещё до поиска фрагментов и подсчёта фасетов. Скрывать такую карточку после получения ответа недостаточно: количество совпадений или метки способны раскрыть наличие закрытого материала. Позже ограничение войдёт в серверный шаблон запроса.
Ожидаемый предметный результат первой подготовки:
article-107 → один идентификатор записи
revision: 1 → редакция исходного документа
audience: public → кандидат для публичного поиска
Markdown → проекция JSON → OpenSearch
Это схема результата, а не журнал выполненного импорта. На этом этапе мы ещё не создавали индекс. Сначала нужен договор полей: от него зависят разбор текста, фильтры, сортировка и возможность безопасно изменить схему. Попытка отправить произвольный JSON до такого договора переносит решения в автоматическое определение типов движком.
Почему индекс отстаёт от редактора
Изменение Markdown и обновление OpenSearch являются двумя отдельными действиями. Между ними читатель может получить прежний заголовок. Даже принятая операция индексирования не всегда немедленно видна поисковому запросу: поисковая доступность связана с refresh. Поэтому будем различать сохранение исходника, принятие записи движком и появление редакции в выдаче. Эти состояния нельзя обозначать одной неопределённой фразой «всё обновлено».
Для маленького учебного проекта достаточно последовательного импортёра и локального манифеста. Он сравнит текущие хеши с последним подтверждённым состоянием и отправит изменившиеся записи. При большом редакционном потоке понадобится журнал изменений или отдельная доставка событий. Такой механизм не появляется от самого наличия OpenSearch. Курс RabbitMQ можно изучать отдельно; его события не являются готовыми командами этого импортёра.
Есть ещё важная граница: поисковый индекс не обязан хранить всё, что есть в исходнике. Мы не передаём секреты подключения, черновые заметки редактора и локальные пути. Удаление поля из ответа API не удаляет его с диска движка. Решение о составе проекции принимается до отправки, когда содержимое ещё находится под контролем приложения.
Архив серии содержит один полный итоговый проект, а не четырнадцать независимых снимков. В начальных уроках рассматриваем его отдельные файлы и функции; поздние возможности уже видны в архиве, но вводятся постепенно. Карта этапов в README указывает, какую часть читать сейчас. Благодаря этому имена документов и интерфейсы остаются одинаковыми на протяжении всего курса.
Ещё одна часть договора — владелец поисковой копии. Если редакторский инструмент и отдельный скрипт одновременно меняют одну запись, восстановление из Markdown может отменить чужое изменение. Поэтому в серии все записи и удаления выполняет один импортёр. Gateway только читает. Такая договорённость позволяет объяснить, почему полная замена проекции безопасна для нашего проекта, хотя не является универсальным правилом любой базы.
Индекс также не создаёт опубликованную страницу по значению url. Он хранит ссылку как данные. Если адрес изменён в корпусе, но сайт ещё выдаёт старую страницу, поиск способен направить читателя на отсутствующий маршрут. Согласование адресов относится к выпуску контента, а не к вычислению релевантности. В учебном архиве переходы иллюстрируют этот договор и не обещают существования каждой страницы на локальном gateway.
Чтобы уточнить договор самостоятельно, переименуйте только файл http-cache.md, сохранив метаданные. Предположите, какие поля проекции поменяются. Затем измените только url и увеличьте редакцию. В первом случае запись остаётся прежней, поскольку локальный путь в проекцию не включён; во втором меняется содержимое того же документа. В следующем уроке подготовим полный корпус и добавим проверки перед отправкой.