Поисковый сервис и API
Переход к серверному поиску начнём с отдельного индекса на тех же документах. Сайт продолжает отдавать статические страницы, а Meilisearch выполняет поиск через HTTP API. Сначала проверим подготовку данных и ответ движка напрямую, затем следующий урок добавит адаптер для браузера.
Публичные URL остаются прежними. Серверный индекс не должен вычислять их из названий или заставлять переезжать старые уроки. Результатом станет готовый индекс выбранной версии корпуса и понятный договор о запросе и ответе.
Отдельное окружение
Для учебного сервиса используем локальный адрес 127.0.0.1:17700. Он отличается от порта сайта и не предполагает изменение действующего хостинга. Серверный блок требует отдельного окружения, способного запускать процесс Meilisearch.
На дату подготовки курса, 10 октября 2026 года, в официальном списке выпусков указан Meilisearch 1.54.3. Для примера закрепляем этот тег, а не автоматически выбираем будущий latest. Официальные выпуски.
Если вы используете Docker и задали собственный MEILI_MASTER_KEY в окружении, запуск учебного экземпляра может выглядеть так:
docker run --name search-lab-meili -p 127.0.0.1:17700:7700 -e MEILI_MASTER_KEY -v search-lab-meili:/meili_data getmeili/meilisearch:v1.54.3
Команда предназначена для самостоятельной практики. Значение ключа не хранится в статье или публичных файлах. Привязка порта к loopback ограничивает этот экземпляр локальной машиной; публичное размещение требует отдельной настройки доступа.
Том сохраняет данные между запусками. Если учебный контейнер уже создан, повторный docker run с тем же именем не создаст второй экземпляр: используется управление существующим контейнером. Не удаляйте данные только для устранения конфликта имени.
Готовность задачи и приём запроса
Создание индекса, настройки и загрузка документов выполняются асинхронно. Ответ с номером задачи означает приём операции, а не готовность результата. Перед поиском нужно дождаться успешного состояния.
Такой порядок предотвращает случайную проверку пустого индекса сразу после загрузки. Он также позволяет отличить сетевой ответ от результата самой операции. Сведения о задаче доступны через Tasks API.
Если задача завершилась ошибкой, продолжать как будто индекс готов нельзя. Причиной могут быть данные, настройки или доступ. Наш небольшой helper выдаёт общее сообщение, а подробности при самостоятельной диагностике рассматриваются в соответствующей задаче движка.
Подготовка корпуса
Используем manifest и версионный файл предыдущего урока. Каждый выпуск получает отдельное имя индекса articles_ВЕРСИЯ. Это позволяет подготовить новую библиотеку до переключения поискового адаптера.
Создайте meili.py в корне search-lab. Ниже полный файл подготовки и первого запроса:
from pathlib import Path
from urllib.error import HTTPError
from urllib.request import Request, urlopen
import json
import os
import re
import time
ROOT = Path(__file__).resolve().parent
BASE = os.environ.get("MEILI_URL", "http://127.0.0.1:17700").rstrip("/")
KEY = os.environ["MEILI_MASTER_KEY"]
def api(method, path, data=None):
body = None if data is None else json.dumps(data, ensure_ascii=False).encode("utf-8")
request = Request(BASE + path, data=body, method=method, headers={
"Authorization": "Bearer " + KEY,
"Content-Type": "application/json",
})
with urlopen(request, timeout=10) as response:
return json.load(response)
def wait(task):
deadline = time.monotonic() + 300
while time.monotonic() < deadline:
result = api("GET", f"/tasks/{task['taskUid']}")
if result["status"] == "succeeded":
return result
if result["status"] in {"failed", "canceled"}:
raise RuntimeError("Задача индекса не завершилась успешно")
time.sleep(1)
raise TimeoutError("Индекс ещё не готов")
def read_payload():
pointer = json.loads((ROOT / "public" / "manifest.json").read_text(encoding="utf-8"))
if not re.fullmatch(r"documents-[a-f0-9]{16}\.json", pointer["file"]):
raise ValueError("Неверный manifest")
payload = json.loads((ROOT / "public" / pointer["file"]).read_text(encoding="utf-8"))
if payload["schemaVersion"] != 1 or payload["version"] != pointer["version"]:
raise ValueError("Версии не согласованы")
return payload
def prepare(payload):
uid = "articles_" + payload["version"]
try:
api("GET", "/indexes/" + uid)
except HTTPError as error:
if error.code != 404:
raise
wait(api("POST", "/indexes", {"uid": uid, "primaryKey": "id"}))
settings = {
"searchableAttributes": ["title", "headings", "text"],
"filterableAttributes": ["section", "topic"],
"displayedAttributes": ["id", "url", "title", "text", "section", "topic"],
"pagination": {"maxTotalHits": 1000},
"typoTolerance": {"disableOnWords": ["C#", "C++", ".NET"]},
"synonyms": {"js": ["javascript"], "javascript": ["js"]},
}
wait(api("PATCH", "/indexes/" + uid + "/settings", settings))
wait(api("POST", "/indexes/" + uid + "/documents", payload["documents"]))
return uid
if __name__ == "__main__":
payload = read_payload()
uid = prepare(payload)
print("MEILI_INDEX=" + uid)
result = api("POST", "/indexes/" + uid + "/search", {
"q": "XML", "page": 1, "hitsPerPage": 10
})
print(json.dumps({"ids": [doc["id"] for doc in result["hits"]],
"totalHits": result["totalHits"]}, ensure_ascii=False))
Ключ администратора нужен этому редакционному инструменту для изменения индекса. Позднее поисковый адаптер получит отдельный ключ с правом поиска. Браузер не должен получать административное значение.
Поля поиска перечислены в порядке заголовок, подзаголовки, текст. Это настройка движка, а не буквальный перенос наших весов 10, 4 и 1. Meilisearch использует собственные правила релевантности, поэтому результаты нужно сравнить заново.
Фильтры разрешены для section и topic. Без соответствующей настройки запрос по структурированному полю не обязан работать. Отдельные поля отображения ограничивают данные ответа, хотя текст пока остаётся доступен для фрагмента.
Ответ поискового API
Для первого наблюдения helper отправляет XML и получает страницу до десяти результатов. Поле hits содержит документы, а page и hitsPerPage задают пагинацию от единицы. Эти параметры описаны в Search API.
У движка есть окно доступной выдачи. Мы явно ограничили его тысячей результатов. Это не число документов в библиотеке и не обещание просмотреть все двадцать тысяч карточек по одному широкому запросу.
При режиме page и hitsPerPage используются сведения totalHits и totalPages. Их нужно читать вместе с пределом окна. Интерфейс не должен сообщать, что ограниченное количество является доказанным полным числом всех подходящих документов за пределами этого окна.
В учебном корпусе ожидается профильный XML-урок и возможно упоминание XML в Fetch. Конкретный порядок относится к выбранным настройкам движка. Не переносите числовой score браузерной формулы в серверный ответ без реального источника такого значения.
Для пустой строки Meilisearch поддерживает отдельное поведение запроса без терминов. Наш пользовательский адаптер не обязан использовать его: прежняя форма просит ввести запрос. Контракт приложения и возможности движка остаются разными уровнями.
Согласованное переключение
Выведенное MEILI_INDEX обозначает готовый индекс. До успеха всех задач адаптер не должен выбирать его как активный. Если подготовка не удалась, прежний согласованный индекс может продолжать обслуживать запросы.
Наличие нового индекса не переключает сайт автоматически. Для учебного адаптера имя задаётся в окружении. Публичная инфраструктура может использовать другой механизм, но должна сохранять тот же принцип готовности и возможность возврата.
Временные версии требуют политики удаления. Нельзя копить их бесконечно или удалить старую сразу после первого нового ответа. Нужно учитывать завершение сессий, наблюдение и период возможного возврата.
Контрольный набор запросов сохраняется. Сравните качество отдельно от задержек и размера ответа. Совпадение формата документов упрощает сравнение, но не делает алгоритмы одинаковыми.
Первая полезная диагностика начинается с состава индекса. Если поиск не возвращает XML, проверьте, завершилась ли задача загрузки и находится ли запись xml в выбранной версии. Затем рассмотрите поисковые поля и сам запрос. Изменять синонимы до этой проверки преждевременно: отсутствие документа невозможно исправить настройкой релевантности.
При повторной подготовке одинакового корпуса имя индекса сохраняется. Наш helper заново применяет настройки и отправляет те же документы. Это удобно для учебного стенда, но версию самого корпуса нельзя считать версией всех настроек поиска. Изменив порядок полей или словарь синонимов, повторите оценку и сохраните сведения о конфигурации. Для строгого воспроизводимого выпуска понадобится учитывать и её версию. Мы отделяем этот вопрос от стабильного ID статьи: изменение алгоритма не должно менять её публичный адрес.
Теперь подготовлен поисковый движок и понятен смысл его HTTP-ответа. Следующий урок поставит перед ним небольшой адаптер, проверит параметры и вернёт браузеру только тот договор, который нужен интерфейсу.