Публичный gateway и пределы поискового запроса
До сих пор поиск вызывался через команду управления. Теперь предоставим локальную страницу с HTTP-gateway. Результат урока — небольшой внешний договор, который преобразует разрешённые параметры в фиксированный запрос, скрывает детали подключения и ограничивает работу движка. При этом ясно обозначим предел: в проекте нет системы входа и полноценного публичного развёртывания.
Полные файлы server.py и web/index.html включены в итоговый архив. Сервер привязан к 127.0.0.1:8092; автор его не запускал и браузерный результат не наблюдал. Механизм серверных ролей самого движка описан в документации пользователей и ролей OpenSearch.
Gateway владеет поисковым договором
Браузер отправляет GET /api/search с q, section, tag, offset и size. Gateway проверяет имена параметров и запрещает повторение одного параметра. Если URL содержит две q, нельзя случайно выбрать «первую» в одном слое и «последнюю» в другом. Явный отказ делает интерпретацию запроса единственной и облегчает диагностику.
Параметры преобразуются в build_query. На этой стороне остаются обязательная видимость, список полей, агрегации и пределы подсветки. Внешний клиент не может передать другой индекс или убрать условие published. Это ещё не достаточная защита рабочего сервиса, но правильная граница между намерением поиска и внутренним API движка.
Фрагмент полного обработчика показывает место проверки; params получен через parse_qs:
if set(params) - {'q', 'section', 'tag', 'offset', 'size'}:
raise ValueError('Unknown query parameter')
if any(len(values) != 1 for values in params.values()):
raise ValueError('Repeated query parameter')
query = build_query(text, section, tag, offset, size)
В полном файле используются локальная функция value и преобразование offset/size в целые числа. Листинг обозначен фрагментом, чтобы не скрывать происхождение переменных. Ошибка входа получает ответ 400, ошибка связи или неполный ответ движка — 503 с коротким сообщением. Не отдаём браузеру сырой ответ OpenSearch с внутренними именами и содержимым отказавших записей.
Пределы являются частью интерфейса
Размер страницы ограничен пятьюдесятью карточками, смещение — пятьюстами. Эти небольшие значения выбраны для учебного просмотра. Они не получены измерением нагрузки и не доказывают, что рабочий кластер выдержит произвольное число одновременно поступающих запросов. В реальном окружении нужны ограничения частоты и наблюдение за стоимостью запросов.
На первом этапе используем from/size. Такой просмотр не сохраняет единый снимок: между страницами индекс может измениться, и карточки сдвинутся. Сортировка по score и doc_id упорядочивает каждый ответ, но не замораживает корпус. Это различие описано в документации пагинации.
Для глубокой устойчивой последовательности можно использовать PIT и search_after, но они требуют другого договора курсора и управления временем жизни контекста. В этой серии не добавляем их как случайный параметр к существующей форме. Передача произвольного search_after из браузера без согласования сортировки и снимка не создаёт корректной пагинации.
Также задаём timeout: 2s в запросе движка и тайм-аут транспорта пять секунд. Это разные пределы: первый относится к поисковой операции, второй к ожиданию HTTP-вызова Python. Они не являются универсальным дедлайном всех слоёв приложения. Если downstream занят или сеть ведёт себя иначе, важно понимать, какой слой сообщил отказ и какое состояние известно.
Секреты не переходят в страницу
Учётные данные OpenSearch читаются из переменных окружения процесса. Страница не содержит их в JavaScript и не обращается напрямую к кластеру. Транспорт использует проверку сертификата через стандартный SSL-контекст и при необходимости явный CA-файл. В исходниках нет переключателя, который безусловно отключает проверку TLS ради удобства примера.
Для отдельной локальной среды разрешён HTTP только с localhost или 127.0.0.1. При другом адресе требуется HTTPS. Это ограниченная проверка формы адреса, а не полноценная политика сетевой инфраструктуры. Она помогает не перенести очевидно открытое соединение на удалённый сервер, но не заменяет управление сертификатами, сетевыми доступами и секретами рабочего окружения.
Права gateway и импортёра должны различаться. Поиску нужны чтение известного alias и связанные поисковые действия; созданию индекса, reindex и переключению aliases нужны более широкие права отдельного оператора. В итоговом проекте оба инструмента используют один транспортный модуль, но запускаются отдельными процессами с соответствующей конфигурацией. Одинаковый код клиента не требует одинаковой учётной записи.
Приложение не создаёт пользователей OpenSearch автоматически. Это зависит от выбранного окружения и действующей системы аутентификации. README направляет читателя к официальному описанию ролей и просит ограничить учебные права известными индексами. Не выдаём демонстрационного администратора за рекомендуемую учётную запись публичного поиска.
Публичная видимость и настоящая авторизация
Поле audience: staff присутствует в учебном корпусе, чтобы показать обязательное исключение служебных материалов. Gateway всегда строит публичный запрос. Никакой параметр role не разрешён. Но если кто-то получает прямой доступ к кластеру с широкими правами, прикладной фильтр gateway его не ограничивает. Поэтому доступ к движку должен быть отдельной защищённой границей.
Режим сотрудников потребовал бы входа, доверенного серверного сопоставления пользователя и разрешённых групп, а также проверки всех способов чтения. Выдача карточек, фасеты, фрагменты и прямое получение документа должны подчиняться одной политике. Добавить выпадающий список «public/staff» в браузер и считать задачу решённой нельзя: браузер контролируется самим посетителем.
Ожидаемые состояния HTTP-контракта:
Корректная строка → карточки и фасеты
Пустая строка или неизвестный параметр → 400
Превышенный size/offset → 400
Потеря связи или неполный поиск → 503
Служебный материал → отсутствует в публичных данных
Это договор ответов для будущего наблюдения, а не выполненный сетевой прогон. Простой HTTPServer обслуживает запросы последовательно и не предназначен для производственной нагрузки. Внешнее развёртывание потребует другого серверного окружения, ограничения входящих соединений и реальной авторизации там, где она нужна продукту.
Форма ответа тоже ограничивает стоимость клиента. В public_response остаются два фрагмента на карточку, а полный body после выбора фрагмента не передаётся браузеру. При этом транспорт между gateway и движком всё ещё получает body для запасного текста. Если документы вырастут, полезно подготовить отдельное короткое поле описания и запрашивать его вместо всего тела. Это изменение проекции требует согласовать mapping и повторный импорт, а не только убрать ключ из внешнего JSON.
Gateway устанавливает no-store для ответа поиска. Так локальная демонстрация не сохраняет результат в обычном HTTP-кеше без отдельного решения о ключах и видимости. Будущий кеш должен учитывать все параметры и права. Иначе быстрый ответ одного контекста может оказаться неверным для другого.
После урока интерфейс связан с движком через понятный шлюз. Для упражнения представьте два одинаковых параметра q и объясните, почему отказ предпочтительнее неоднозначного выбора. Затем сопоставьте право читать library-read с правом переключать aliases. В заключительном уроке подготовим сопровождение индекса и отличим готовый исходник от проверенного работающего сервиса.