Перейти к содержанию

Явные mappings для текста и точных значений

Подготовленный JSON ещё не определяет, как движок будет искать его поля. Заголовок нужно разобрать на слова, раздел сравнивать точно, а дату понимать как дату. В этом уроке создадим явные mappings — описание типов и способов индексирования. Получим индекс library-v1, который принимает договор проекта и отклоняет случайное расширение данных.

Полная схема находится в index.json итогового архива. Она включает анализатор следующего урока; здесь рассматриваем типы и ограничения полей. Команду создания читатель выполняет только в собственной учебной среде. Автор не запускал OpenSearch, поэтому успешный ответ и пример ошибки ниже обозначены ожидаемыми.

Разные способы прочитать строку

Поле text предназначено для поиска по разобранному тексту. Движок применяет анализатор и индексирует получившиеся термы. Поле keyword сохраняет значение для точного сравнения, сортировки и группировки. Обе записи могут исходить из одинаковой строки JSON, но решают разные задачи. Документация mappings описывает явное назначение типов.

Для «HTTP и кеширование» хотим находить документ по словам и их русским формам. Поэтому title имеет тип text. Для section: backend хотим выбирать конкретную рубрику: значение имеет тип keyword. Если сделать раздел обычным текстом, название из нескольких слов может разбиться на части, и фильтр станет означать другой договор.

Следующий листинг — фрагмент объекта mappings.properties из полного index.json, а не самостоятельный запрос создания:

{
  "doc_id": {"type": "keyword"},
  "title": {
    "type": "text",
    "analyzer": "library_ru",
    "fields": {"exact": {"type": "keyword", "normalizer": "library_lower"}}
  },
  "section": {"type": "keyword"},
  "tags": {"type": "keyword"},
  "published": {"type": "boolean"},
  "revision": {"type": "long"},
  "updated_at": {"type": "date"}
}

Подполе title.exact индексирует тот же исходный заголовок иначе. В исходнике не появляется второй заголовок, и редактор не обязан поддерживать его отдельно. Через title выполняем полнотекстовый поиск, через title.exact при необходимости сравниваем целую строку с нормализацией регистра. Такая связь называется multi-field.

Нормализатор library_lower переводит точное значение в нижний регистр. Он не выполняет русский стемминг и не превращает заголовок в несколько токенов. Поэтому точное совпадение с «http и кеширование» не равнозначно совпадению с «кеширование». Это различие полезно объяснять через ожидаемый запрос, а не через слово «точный» без уточнения поля.

Массив меток и предметная схема

Метки tags приходят массивом строк. Для него не нужен отдельный тип «массив»: каждое значение индексируется как значение одного поля выбранного типа. Документ с http и cache относится к обеим группам. Позже сумма счётчиков по меткам может оказаться больше количества документов, поскольку один материал входит в несколько групп.

Номер редакции хранится как long, а не строка. Это позволит сравнивать числа без лексикографического эффекта, при котором строка «10» располагается перед «2». Тип date нужен для дат, но он не проверяет предметную правильность изменения: дата будущего времени технически может быть допустимой. Редакционная дисциплина и типизация решают разные проблемы.

Установим dynamic: strict. При появлении неизвестного поля движок отклонит документ. Сценарий выглядит так: загрузчик случайно добавил editor_notes; пакет дошёл до сервера; сервер не расширяет mapping сам, а возвращает ошибку этой операции. Это второй слой защиты после проверки исходного корпуса, особенно полезный при появлении нового импортёра.

Строгий mapping не означает полной проверки бизнес-договора. Он не узнает, что ссылка относится именно к этому материалу, что редакция увеличена корректно или что раздел действительно существует в каталоге. Некоторые преобразования типов также зависят от параметров mappings. Поэтому оставляем предварительную проверку загрузчика и не заменяем её единственным сетевым отказом.

Создание физического индекса

Команда управления читает весь index.json и отправляет PUT /library-v1. Ниже приведена команда для читателя; настройки подключения описаны в README:

python manage.py create library-v1

Ожидаемый успешный ответ содержит acknowledged: true и имя созданного индекса. Повторное создание того же физического имени не является обновлением схемы: сервер должен вернуть отказ о существующем индексе. Поэтому не используем такую команду как универсальную идемпотентную миграцию. Нужно сначала определить текущее состояние, а не гасить любое исключение.

В учебных settings выбран один primary shard и ноль replicas. Это уменьшает число понятий при первом запуске на одном узле. Отсутствие реплики является ограничением устойчивости, а не оптимальной настройкой для рабочего поиска. Состояние green при такой конфигурации не означает наличие второй копии данных; позже разберём эту разницу в эксплуатации.

Имя library-v1 обозначает физическую версию схемы. Приложение впоследствии будет читать alias library-read, чтобы не зависеть от этого имени. Пока важно только не спутать версию схемы индекса с редакцией отдельного материала: v1 относится к способу хранения всех записей, revision: 1 — к истории article-107.

Почему тип не исправляется задним числом

Предположим, revision случайно был создан как keyword, а затем в него поступили документы. Простая замена типа в mapping не перепишет уже подготовленные структуры так, будто число всегда было числом. Для содержательного изменения типа потребуется новый индекс и перенос документов с желаемой схемой. Это одна из причин описывать mapping до массовой загрузки.

Добавление нового поля и изменение существующего типа — разные операции. Новое поле не создаёт автоматически значения для старых документов. Новый multi-field тоже не гарантирует, что уже проиндексированные записи внезапно получат его термы без повторной обработки. Когда разберём reindex, вернёмся к разнице между схемой, _source и физическими поисковыми структурами.

Обратите внимание на _source: это сохранённая исходная JSON-проекция. Она понадобится для выдачи полей, подсветки и переноса. В этом проекте её не отключаем. Экономия места через отказ от _source требует отдельного разбора того, откуда затем брать исходное содержимое. Markdown остаётся основным источником, но некоторые операции движка читают именно сохранённую проекцию.

Ожидаемое состояние после урока — пустой физический индекс с предсказуемой схемой. Материалы ещё не загружены, поэтому отсутствие результатов поиска не является ошибкой анализа или ранжирования. Для самостоятельного разбора сопоставьте каждое поле корпуса с его будущей операцией: слово ищем, метку фильтруем, дату сравниваем, идентификатор используем для записи. Далее настроим русский анализ и проверим границы обработки технических обозначений.

Оглавление серии