DarkRiDDeR16 мин

Документ сохранён, но не найден: безопасная диагностика поиска

ПоискДанныеРазбор

Проблема выглядит так: документ сохранён, карточка открывается, но поиск не находит его по ожидаемому слову. Самая дорогая реакция в этот момент — сразу удалить запись, создать её заново или запустить широкую повторную индексацию. Так можно потерять исходную version, сделать новый event неотличимым от старого и стереть доказательство того, что источник вообще был исправен. Сначала нужен короткий отчёт: какой id, какая version, какой query, где именно документ уже виден и в какой момент это наблюдалось.

Ниже — полевой маршрут для одного учебного документа. Он не запускает Elasticsearch, БД, broker, HTTP или реальный reindex. Сквозная fixture хранит source, ingest, pending index и visible index в памяти. До refresh source уже содержит version 7, а query пуст. Диагностика возвращает awaiting-refresh и действие без удаления. После refresh тот же query находит version 7, и следующий вопрос меняется: не «где документ», а «совпадает ли контракт запроса с тем, что ищет пользователь».

Сначала собираем доказательство, которое переживёт исправление

Минимальная карточка инцидента должна содержать document id, source version, ключ постановки, текст и параметры query, время наблюдения и точку pipeline, где сделана проверка. Если есть доступ к источнику, записываем именно version, а не только текст заголовка: одинаковый заголовок может принадлежать двум разным состояниям. Если есть event, сохраняем id:version, а не только строку «поставили в очередь». Такой набор позволяет повторить проверку после изменения настройки и не спутать новую работу с исходным симптомом.

const evidence = {
  documentId: "article-2021-07-42",
  sourceVersion: 7,
  eventKey: "article-2021-07-42:7",
  query: "свежести",
  observedAt: ["saved", "enqueued", "indexed", "visible"],
  destructiveAction: false,
};

// Без этой записи reindex легко стирает различие между причиной и следствием.

Доказательство не должно содержать персональные данные, секреты или полный пользовательский запрос, если они не нужны для причины. Для учебной fixture достаточно id, version, одного слова поиска и четырех моментов времени. В реальном проекте к ним добавляются только те поля, которые разрешено собирать и которые отвечают на конкретную ветку: index target, alias, filter, rights, analyzer или timestamp. Чем шире бессмысленный лог, тем труднее увидеть отличие source path от query path.

Диагностика «сохранён, но не найден»: симптом не равен причине
НаблюдениеВероятная границаКонтрольная проверкаRollback-safe действие
source отсутствует по idwrite path или неверный idсверить id, version и результат сохраненияостановиться; не создавать копию до подтверждения источника
source есть, event ожидаетingestнайти key id:version и время постановкисохранить evidence и наблюдать обработку, не менять source
pending version есть, query пустпереход видимостисравнить indexedAtMs с visibleAtMs или признаком refreshследовать проектной policy либо локальному controlled test
visible version стараяпорядок версийсравнить source.version и visible.versionпоставить текущую version идемпотентно, сохранив старый след
visible version текущая, query пустконтракт queryпроверить scope, filter, поле и анализ текстаизменять запрос или проекцию точечно и с обратимым шагом

Таблица нужна не для угадывания причины по одному признаку. Она удерживает порядок: сначала доказываем, что source существует; затем выясняем судьбу id:version; только после этого обсуждаем refresh; и лишь потом меняем query или mapping. Если начать с последнего пункта, можно создать новый индекс и всё равно не заметить, что event не был принят. Если начать с удаления, можно лишиться единственной версии, с которой можно сравнить результат.

Когда source существует, а выдача stale, не подменяем диагноз reindex

В fixture после обработки ingest документ лежит в pending index. Source уже подтверждён, ключ постановки был один, version совпала. Тем не менее поиск по слову «свежести» возвращает ноль. Этот факт не доказывает, что индекс сломан. Он доказывает только то, что visible snapshot ещё не получил документ. diagnoseVisibility возвращает awaiting-refresh, отмечает version и запрещает destructive action. Так оператор может измерить промежуток и применить заранее выбранную policy, не изобретая новую запись.

const report = diagnoseVisibility("article-2021-07-42");

if (report.stage === "awaiting-refresh") {
  // Сохраняем version и точки времени; не удаляем source и не создаём копию.
  console.log(report.action);
}

if (report.stage === "query-contract") {
  // Проверяем scope, filter и анализ текста до повторной индексации.
  console.log(report.action);
}

Здесь важно отделить контролируемый эксперимент от рабочего решения. В модели explicit refresh — одна функция и она всегда завершает переход. В реальной системе тот же термин имеет цену, версионные особенности и границы охвата. Документация Elasticsearch 7.13 пишет, что refresh делает операции доступными search, но не превращает source read и text query в один маршрут. Поэтому безопасный вопрос звучит так: «какой evidence показывает, что нужная version должна была стать видимой именно для этого query?»

Дерево безопасной диагностики: сначала проверить source и version, затем key ingest, pending index и evidence refresh; если текущая version уже видима, перейти к scope, filter и текстовому анализу; на всех ветках запрещено удалять source без подтверждённой причины
Маршрут расследования: каждое действие сохраняет исходный документ и оставляет следующий проверяемый факт.

Проверяем контракт query после доказанной видимости

Если visible index уже содержит current version, а поиск пуст, повторять ingest бессмысленно. Нужно зафиксировать фактический query: в каком поле ищем, какой filter ограничивает набор, в каком scope находится документ, как нормализуется текст и не исключают ли права запись из выдачи. В fixture query упрощён до поиска подстроки по title и body. Он не моделирует stemming, токенизацию, synonyms, routing, alias или permissions. Поэтому его успешный hit не является тестом реального анализатора.

Историческая документация Elasticsearch 7.13 полезна здесь ещё одной границей: Get API по умолчанию realtime и не зависит от момента, когда данные становятся видимы search. В движке это позволяет различать «документ можно прочитать по id» и «документ найден обычным search». Но нельзя использовать это как оправдание для пропуска проверки query. Пользователь обычно видит именно выдачу с её фильтрами, а не внутреннее чтение по id. В отчёте должны быть оба факта, если оба важны.

Одна fixture, два безопасных состояния диагностики

Сквозной тест создаёт один source document с version 7. Первая постановка ingest возвращает ingest-enqueued; повторная до consume и повторная после него с тем же ключом возвращают duplicate-ingest-suppressed. До refresh diagnosis указывает на pending state и не предлагает delete. После refresh query возвращает один hit с current version, а diagnosis переводит расследование в query-contract. Это не означает, что любой пропавший документ нужно ждать до refresh. Это означает, что модель не смешивает два вопроса в один.

const fixture = runSearchIndexingFixture();
if (!Object.values(fixture.assertions).every(Boolean)) {
  throw new Error("search fixture failed");
}

fixture.beforeRefreshSearch.hitCount; // 0: source уже сохранён, search ещё stale
fixture.afterRefreshSearch.hitCount;  // 1: тот же version стал видимым
fixture.observedLagMs;                // 60: учебная арифметика, не SLA

Assertions фиксируют каждый заявленный вывод. Есть source document, duplicate enqueue подавлен до и после consume, запрос до индексирования и до refresh пуст, refresh показывает одну текущую version, задержка вычислена как 60 условных миллисекунд, а обе диагностические ветки неразрушающие. Если кто-то изменит порядок и перенесёт pending-документ в visible раньше refresh, assertion stale выдачи станет ложным. Так review получает не только текстовый вывод, но и маленькую проверку причинной цепочки.

Rollback-safe маршрут для оператора

Симптом — сохранённая запись отсутствует в заданной выдаче. Причину не угадываем: сначала source, затем id:version и видимость, после этого query. Каждая проверка оставляет evidence, а действие обратимо: источник не удаляется, повторная постановка привязана к текущей version, а изменение запроса проверяется исходным запросом.

  1. Сохранить id, source version, event key, точный query и время наблюдения. Не исправлять систему до появления этого минимального следа.
  2. Проверить source-of-truth по id. Если его нет, остановить дальнейшие поисковые действия и выяснить write path; не создавать дубликат «для проверки».
  3. Если source есть, проверить состояние постановки id:version. Повторная постановка допустима только как явно идемпотентный шаг, а не как новый анонимный event.
  4. Если проекция ждёт видимости, зафиксировать pending version и точки времени. Использовать только согласованную project policy или локальный controlled test; не распространять его на все записи.
  5. Если visible version отстаёт, сравнить version источника и event, затем поставить именно текущую version с сохранением старого evidence. Не стирать прежнюю запись до проверки результата.
  6. Если visible version актуальна, проверить scope, filter, поле, нормализацию текста и права query. Не возвращаться к refresh, пока этот контракт не проверен.
  7. После точечной правки повторить исходный query, записать результат и добавить сценарий в fixture или интеграционный тест проекта. Откатить изменение можно по сохранённому evidence, а не по памяти о симптоме.

Границы, версия и следующий шаг

Сама по себе фраза «документ проиндексирован» не даёт права менять источник или объявлять инцидент закрытым. В Elastic 7.13 есть отдельные механизмы Index API, refresh и Get API; их реальные параметры, стоимость и поведение определяются развернутой версией и настройками. Наша fixture намеренно не заявляет ничего о shard, replica, alias, interval, persistence, concurrency, permissions или продуктивном логе. Она делает один безопасный вывод: сначала найти участок между source и query, затем применять обратимое действие.

Следующий шаг для своего проекта — выбрать один тестовый документ без чувствительных данных, пройти его по той же карточке evidence и сравнить source version с результатом одного фиксированного search. Если между ними есть промежуток, зафиксируйте владельца и реакцию. Если проекция уже актуальна, переключите расследование на query contract. Такой порядок бережёт источник, не обещает мгновенную видимость и оставляет после исправления воспроизводимый способ проверки.

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

  • Elasticsearch 7.13: Near real-time search — историческая документация линии 7.13: refresh делает операции с индексом доступными для поиска; описание near-real-time не является SLA учебной модели
  • Elasticsearch 7.13: Index API — источник для параметра refresh и раздела versioning; fixture не вызывает API и не задаёт настройки движка
  • Elasticsearch 7.13: Get API — документация различает realtime GET по умолчанию и момент, когда данные видны search; это граница для диагностики, а не модель хранилища в статье
  • Elasticsearch 7.13: The refresh parameter — историческое описание значений false, true и wait_for и их влияния на видимость операции для search; решение о режиме зависит от нагрузки и контракта проекта