{ "index": 232, "slug": "editorial-2021-07-field-search-indexing", "title": "Документ сохранён, но не найден: как отделить задержку индекса от ошибки запроса", "excerpt": "Карточка уже открывается, но поиск не возвращает новый документ. Разбираем путь от source до видимой выдачи, проверяем version и refresh и выбираем безопасное действие без удаления исходной записи.", "contentHtml": "

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

\n

Главный тезис прост: source и search отвечают на разные вопросы. Успешное чтение записи по id доказывает, что источник существует. Оно не доказывает, что текущая версия уже попала в поисковую видимую проекцию. Пустой search до refresh может быть нормальной задержкой. Пустой search после подтверждённой видимости указывает на другой участок: поле, анализатор, filter, scope или права.

\n

Механизм: четыре состояния вместо одного «индекса»

\n

Полезно разложить путь документа на состояния. source хранит исходную запись и её version. ingest принимает работу на построение поисковой проекции. pending содержит подготовленную версию, которую ещё не видит обычный search. visible — снимок, по которому выполняется запрос. Между состояниями есть переходы. Каждый переход имеет свой ключ, время и результат.

\n

Предположим, source содержит документ product-42 с version 7. Ingest получает ключ product-42:7. Повторная постановка с тем же ключом не должна создавать вторую работу. После обработки version 7 может оказаться в pending, но search всё ещё вернёт ноль. Только после перехода видимости запрос получает право увидеть эту версию. Если видимая version равна 7, а query по-прежнему пуст, refresh уже не объясняет симптом.

\n
source(version=7)\n  -> ingest(key="product-42:7")\n  -> pending(version=7)\n  -> refresh\n  -> visible(version=7)\n  -> search(query, scope, filter)\n\nПравило диагностики:\nsource read != search hit
\n

Version нужна не для красоты. Она связывает источник, работу и проекцию. Если лог содержит только название товара, две последовательные правки выглядят одинаково. Если лог содержит id:version, можно увидеть, что старая работа не должна перезаписать новую, а повторная постановка относится к той же версии. Timestamp помогает оценить задержку, но не заменяет порядок версий.

\n

Учебный пример: запрос до и после refresh

\n

Ниже — минимальная модель. Она не подключается к Elasticsearch или базе и не описывает производительность. Код показывает только причинную цепочку: документ уже есть в source, затем появляется в pending, но search начинает возвращать его лишь после явного перехода видимости.

\n
const source = new Map();\nconst pending = new Map();\nconst visible = new Map();\n\nsource.set('product-42', {\n  id: 'product-42',\n  version: 7,\n  title: 'Свежие яблоки',\n});\n\npending.set('product-42', source.get('product-42'));\n\nfunction search(term) {\n  return [...visible.values()].filter((doc) =>\n    doc.title.toLowerCase().includes(term.toLowerCase()),\n  );\n}\n\nconsole.log(search('яблоки')); // []\n\nfor (const [id, doc] of pending) {\n  visible.set(id, doc);\n}\n\nconsole.log(search('яблоки')); // [{ id: 'product-42', version: 7, ... }]
\n

В реальном движке переход видимости может происходить автоматически по настройке refresh interval или по явной операции. Его стоимость и охват зависят от версии, размера индекса и нагрузки. Поэтому учебный вызов visible.set нельзя переносить в production как готовую команду. Он нужен, чтобы не смешивать две проверки: «проекция построена» и «проекция доступна этому запросу».

\n
\"Дерево
Проверяйте путь от source к query по границам. Если текущая version уже видима, переходите к контракту запроса.
\n

Симптом → причина → проверка → действие

\n
СимптомПричинаПроверкаДействие
По id source отсутствуетОшибка записи или неверный idСверить id, version и результат сохраненияОстановиться. Не создавать копию до проверки write path
Source есть, ingest не подтверждёнРабота не поставлена или потерянаНайти ключ id:version и результат постановкиСохранить evidence и проверить очередь по её policy
Pending содержит текущую version, search пустПроекция ещё не стала видимойСравнить время обработки и refresh или признак видимостиПрименить согласованную policy. Не удалять source
Visible содержит старую versionУстаревшая работа или нарушение порядкаСравнить source.version, event.version и visible.versionПоставить текущую version идемпотентно и сохранить старый след
Visible содержит текущую version, search пустОшибка query contractПроверить поле, analyzer, filter, scope, routing и праваИсправить только найденный участок и повторить исходный query
\n

Таблица задаёт порядок, а не заменяет доказательство. Один пустой ответ не говорит, где произошёл сбой. Если начать с фильтра, можно не заметить, что ingest вообще не принял version. Если сразу вызвать широкий reindex, можно получить хороший результат без понимания причины. Такой результат не защищает следующий релиз.

\n

Как отличить задержку видимости от ошибки запроса

\n

Сначала зафиксируйте точный query: индекс или alias, поле, текст, фильтры, сортировку, routing и права. Затем получите source по id и запишите version. После этого найдите ту же version в состоянии проекции. Если она находится только в pending, причина ещё находится до refresh. Если она видима, повторная постановка не добавит новых фактов.

\n

Проверяйте отрицательный путь отдельно. Для документа version 7 ожидайте пустой search до подтверждённого refresh. После refresh ожидайте ровно один hit с version 7. Для повторного ingest с ключом product-42:7 ожидайте подавление дубликата. Для старой version 6 ожидайте отказ от отката видимого документа. Эти ожидания проверяют механизм, но не обещают SLA, hit rate или поведение кластера.

\n

Особенно опасно считать Get API и search одним чтением. В Elasticsearch документ можно получить по id раньше, чем он становится доступен обычному поиску. Эта разница полезна для диагностики, но она не доказывает, что пользовательский query исправен. Пользователь видит не внутренний Get, а выдачу с её анализатором, фильтрами и ограничениями доступа.

\n

Порядок действий

\n
  1. Запишите id, source version, точный query, индекс или alias, параметры фильтра и время наблюдения. Уберите из evidence секреты и лишние персональные данные.
  2. Проверьте source по id. Если записи нет, остановите поисковое расследование и выясните write path. Не создавайте дубликат для проверки.
  3. Проверьте ключ ingest в формате id:version. Сопоставьте постановку, обработку и повторные попытки. Не называйте успешной постановкой сам факт отправки запроса.
  4. Найдите version в pending или видимой проекции. Если текущая version ждёт refresh, зафиксируйте время и примените только заранее выбранную policy.
  5. Если видима старая version, сравните порядок версий и найдите источник старой работы. Повторную постановку делайте идемпотентной и не стирайте исходное evidence.
  6. Если видима текущая version, проверьте query contract: поле, mapping, нормализацию текста, analyzer, filter, scope, routing и права.
  7. Повторите исходный query без дополнительных изменений. Сравните id и version в результате. Затем добавьте проверку на отрицательный путь в тест или runbook проекта.
\n

Ограничения

\n

Эта схема не моделирует shards, replicas, alias, persistence, конкуренцию, сбои сети, очередь с гарантией доставки, права доступа и анализатор конкретного движка. Она не даёт точного срока, за который документ обязан появиться в поиске. Термин «near real time» не означает мгновенную видимость. Реальную задержку нужно измерять в собственной конфигурации по timestamps и результатам фиксированного query.

\n

Не каждый пустой поиск требует refresh. Явное обновление может увеличить нагрузку и замедлить indexing. Не каждый устаревший hit исправляется повторной постановкой: причиной может быть задержанная старая работа, неверный alias или фильтр. Не каждый найденный source можно показывать всем: query обязан соблюдать scope и права. Если эти границы не записаны, оператор легко превращает временный обход в постоянную нагрузку.

\n

Проверяемый критерий готовности

\n

Диагностика готова, когда для одного тестового документа можно показать непрерывную историю id → version → ingest key → pending или visible → точный query → результат. В истории есть отрицательный случай до refresh, положительный случай после него, подавление дубликата и отказ от отката старой version. Исправление считается доказанным только тогда, когда исходный query возвращает ожидаемый id и текущую version, а запись не удаляли ради получения этого результата.

\n

Для production этого критерия недостаточно: добавьте метрики задержки, ошибки ingest и долю документов, которые не достигают видимости в допустимое время. Но порядок расследования останется тем же. Сначала защитите source и восстановите цепочку фактов. Потом меняйте тот слой, который не выполнил свой контракт.

\n

Проверяемые источники

\n" }