Files
progcode/editorial/agent-rewrites/232.json
T

8 lines
20 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 232,
"slug": "editorial-2021-07-field-search-indexing",
"title": "Документ сохранён, но не найден: как отделить задержку индекса от ошибки запроса",
"excerpt": "Карточка уже открывается, но поиск не возвращает новый документ. Разбираем путь от source до видимой выдачи, проверяем version и refresh и выбираем безопасное действие без удаления исходной записи.",
"contentHtml": "<p>Симптом на стенде знаком: оператор сохраняет новый товар: карточка открывается по id, но поиск по названию возвращает пустой список. Через несколько минут товар появляется сам. Сценарий выглядит временным, но его цена вполне материальна: пользователь не находит доступный товар, оператор запускает дорогую повторную индексацию, а удаление и повторное создание записи стирают след исходного состояния. После такого вмешательства уже трудно понять, не была ли причина в запросе, фильтре или правах.</p>\n<p>Рабочее правило такое: source и search отвечают на разные вопросы. Успешное чтение записи по id доказывает, что источник существует. Оно не доказывает, что текущая версия уже попала в видимую поисковую проекцию. Пустой search до refresh может быть нормальной задержкой. Пустой search после подтверждения видимости указывает на другой участок: поле, анализатор, filter, scope или права.</p>\n<h2>Механизм: четыре границы вместо одного «индекса»</h2>\n<p>Для диагностики удобно разложить путь документа на четыре границы: <code>source</code> хранит исходную запись и её version; <code>ingest</code> принимает событие на построение проекции; <code>pending</code> содержит подготовленную версию, которую ещё не видит обычный search; <code>visible</code> — снимок, по которому выполняется запрос. Между границами есть переходы. Каждый переход нужно связывать с id, version, временем и результатом.</p>\n<p>Это абстрактная модель потока, а не четыре обязательных статуса Elasticsearch. В конкретной системе <code>ingest</code> может быть очередью, воркером или Ingest Node pipeline, а <code>pending</code> — сообщением в очереди, staging-индексом или буфером до refresh. Эти детали нельзя угадывать по одному пустому ответу: их нужно сопоставить с реальной схемой доставки.</p>\n<p>Предположим, source содержит документ <code>product-42</code> с версией <code>7</code>. Событие получает ключ <code>product-42:7</code>. Повторная постановка с тем же ключом не должна создавать вторую работу, если это правило закреплено в контракте обработчика. После обработки версия 7 может находиться в pending, но search всё ещё вернёт ноль. Только после перехода видимости запрос получает право увидеть эту версию. Если та же версия уже видима в том же индексе и с тем же routing, refresh больше не объясняет пустой результат.</p>\n<pre><code>source(version=7)\n -&gt; ingest(key=&quot;product-42:7&quot;)\n -&gt; pending(version=7)\n -&gt; refresh\n -&gt; visible(version=7)\n -&gt; search(query, scope, filter)\n\nПравило диагностики:\nsource read != search hit</code></pre>\n<p>Version связывает источник, работу и проекцию, но сама по себе не делает очередь идемпотентной. Ключ <code>id:version</code> — это договорённость приложения: её должен поддерживать consumer или хранилище событий. В Elasticsearch не следует автоматически принимать поле source version за служебное <code>_version</code>: внутренний номер документа и версия из исходной системы решают разные задачи. Для защиты от устаревших изменений подходят внешний versioning или optimistic concurrency control с <code>if_seq_no</code> и <code>if_primary_term</code> — выбор зависит от того, кто владеет порядком изменений.</p>\n<h2>Учебный пример: duplicate, stale и refresh</h2>\n<p>Ниже — минимальная модель без Elasticsearch, базы и сетевых задержек. Она намеренно показывает три проверяемых свойства: одинаковый ключ не создаёт вторую работу, старая версия не заменяет новую, а поиск видит принятую версию только после <code>refresh</code>. Код можно сохранить как файл <code>demo.mjs</code> и запустить командой <code>node demo.mjs</code>; внешние пакеты не нужны.</p>\n<pre><code>const source = new Map([\n ['product-42', { id: 'product-42', version: 7, title: 'Свежие яблоки' }],\n]);\nconst pending = new Map();\nconst visible = new Map();\nconst acceptedKeys = new Set();\n\nfunction ingest(doc) {\n const key = doc.id + ':' + doc.version;\n if (acceptedKeys.has(key)) return { status: 'duplicate', key };\n\n const current = pending.get(doc.id) ?? visible.get(doc.id);\n if (current &amp;&amp; current.version &gt;= doc.version) {\n return { status: 'stale', key };\n }\n\n acceptedKeys.add(key);\n pending.set(doc.id, doc);\n return { status: 'queued', key };\n}\n\nfunction refresh() {\n for (const [id, doc] of pending) {\n const current = visible.get(id);\n if (!current || current.version &lt;= doc.version) visible.set(id, doc);\n pending.delete(id);\n }\n}\n\nfunction search(term) {\n return [...visible.values()].filter((doc) =&gt;\n doc.title.toLowerCase().includes(term.toLowerCase()),\n );\n}\n\nconsole.log(search('яблоки')); // []\nconsole.log(ingest(source.get('product-42'))); // queued, product-42:7\nconsole.log(search('яблоки')); // []\nrefresh();\nconsole.log(search('яблоки')); // [{ id: 'product-42', version: 7, ... }]\nconsole.log(ingest(source.get('product-42'))); // duplicate\nconsole.log(ingest({ id: 'product-42', version: 6, title: 'Старые яблоки' })); // stale\nconsole.log(visible.get('product-42').version); // 7</code></pre>\n<p>Здесь <code>visible</code> — не внутренний объект Elasticsearch, а тестовый снимок. В реальном Elasticsearch операция записи с <code>refresh=false</code> не обязана сразу менять поисковую выдачу; <code>refresh=wait_for</code> ждёт очередного обновления, а <code>refresh=true</code> принудительно обновляет затронутые shards и требует оценки нагрузки. Поэтому вызов <code>visible.set</code> нельзя переносить в production как готовую команду. Он нужен, чтобы не смешивать две проверки: «проекция построена» и «проекция доступна этому запросу».</p>\n<figure><img src=\"/assets/editorial/2021/search-indexing-diagnosis-2021.svg\" alt=\"Дерево диагностики: source, ingest, pending, refresh и контракт query\"><figcaption>Проверяйте путь от source к query по границам. Если текущая version уже видима, переходите к контракту запроса.</figcaption></figure>\n<h2>Симптом → причина → проверка → действие</h2>\n<table><thead><tr><th>Симптом</th><th>Причина</th><th>Проверка</th><th>Действие</th></tr></thead><tbody><tr><td>По id source отсутствует</td><td>Ошибка записи или неверный id</td><td>Сверить id, version и результат сохранения</td><td>Остановиться. Не создавать копию до проверки write path</td></tr><tr><td>Source есть, ingest не подтверждён</td><td>Работа не поставлена или потеряна</td><td>Найти ключ <code>id:version</code> и результат постановки</td><td>Сохранить evidence и проверить очередь по её policy</td></tr><tr><td>Pending содержит текущую version, search пуст</td><td>Проекция ещё не стала видимой</td><td>Сравнить время обработки и refresh или признак видимости</td><td>Применить согласованную policy. Не удалять source</td></tr><tr><td>Visible содержит старую version</td><td>Устаревшая работа или нарушение порядка</td><td>Сравнить source.version, event.version и visible.version</td><td>Поставить текущую version идемпотентно и сохранить старый след</td></tr><tr><td>Visible содержит текущую version, search пуст</td><td>Ошибка query contract</td><td>Проверить поле, analyzer, filter, scope, routing и права</td><td>Исправить только найденный участок и повторить исходный query</td></tr></tbody></table>\n<p>Таблица задаёт порядок, а не заменяет доказательство. Один пустой ответ не говорит, где произошёл сбой. Если начать с фильтра, можно не заметить, что ingest вообще не принял version. Если сразу вызвать широкий reindex, можно получить хороший результат без понимания причины. Такой результат не защищает следующий релиз.</p>\n<h2>Как отличить задержку видимости от ошибки запроса</h2>\n<p>Сначала зафиксируйте точный query: индекс или alias, поле, текст, фильтры, сортировку, routing и права. Затем получите source по id и запишите версию. В Elasticsearch Get API по умолчанию работает в realtime-режиме и не зависит от refresh rate индекса, тогда как обычный search видит изменения после refresh. Поэтому результат Get нельзя использовать как доказательство готовности поисковой выдачи.</p>\n<p>После Get найдите ту же версию в состоянии проекции и проверьте, что это тот же индекс, alias, shard и routing. Если версия находится только в pending, причина ещё находится до refresh. Если текущая версия действительно видима, проверьте mapping, анализатор, нормализацию, filter, scope и права. При использовании служебной версии запроса сохраните также <code>_seq_no</code> и <code>_primary_term</code>, чтобы отличить конфликт обновления от задержки поиска.</p>\n<p>Проверяйте отрицательный путь отдельно. Для документа version 7 ожидайте пустой search до подтверждённого refresh. После refresh ожидайте ровно один hit с version 7. Для повторного ingest с ключом <code>product-42:7</code> ожидайте подавление дубликата. Для старой version 6 ожидайте отказ от отката видимого документа. Эти ожидания проверяют механизм, но не обещают SLA, hit rate или поведение кластера.</p>\n<h2>Порядок действий</h2>\n<ol><li>Запишите id, source version, точный query, индекс или alias, параметры фильтра и время наблюдения. Уберите из evidence секреты и лишние персональные данные.</li><li>Проверьте source по id. Если записи нет, остановите поисковое расследование и выясните write path. Не создавайте дубликат для проверки.</li><li>Проверьте ключ ingest в формате <code>id:version</code>. Сопоставьте постановку, обработку и повторные попытки. Не называйте успешной постановкой сам факт отправки запроса.</li><li>Найдите version в pending или видимой проекции. Если текущая version ждёт refresh, зафиксируйте время и примените только заранее выбранную policy.</li><li>Если видима старая version, сравните порядок версий и найдите источник старой работы. Повторную постановку делайте идемпотентной и не стирайте исходное evidence.</li><li>Если видима текущая version, проверьте query contract: поле, mapping, нормализацию текста, analyzer, filter, scope, routing и права.</li><li>Повторите исходный query без дополнительных изменений. Сравните id и version в результате. Затем добавьте проверку на отрицательный путь в тест или runbook проекта.</li></ol>\n<h2>Ограничения</h2>\n<p>Эта схема не моделирует shards, replicas, alias, persistence, конкуренцию, сбои сети, очередь с гарантией доставки, права доступа и анализатор конкретного движка. Она не даёт точного срока, за который документ обязан появиться в поиске. Термин «near real time» не означает мгновенную видимость. Реальную задержку нужно измерять в собственной конфигурации по timestamps и результатам фиксированного query.</p>\n<p>Не каждый пустой поиск требует refresh. Явное обновление может увеличить нагрузку и замедлить indexing. Не каждый устаревший hit исправляется повторной постановкой: причиной может быть задержанная старая работа, неверный alias или фильтр. Не каждый найденный source можно показывать всем: query обязан соблюдать scope и права. Если эти границы не записаны, оператор легко превращает временный обход в постоянную нагрузку.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Диагностика готова, когда для одного тестового документа можно показать непрерывную историю <code>id → version → ingest key → pending или visible → точный query → результат</code>. В истории есть отрицательный случай до refresh, положительный случай после него, подавление дубликата и отказ от отката старой version. Исправление считается доказанным только тогда, когда исходный query возвращает ожидаемый id и текущую version, а запись не удаляли ради получения этого результата.</p>\n<p>Для production этого критерия недостаточно: добавьте метрики задержки, ошибки ingest и долю документов, которые не достигают видимости в допустимое время. Но порядок расследования останется тем же. Сначала защитите source и восстановите цепочку фактов. Потом меняйте тот слой, который не выполнил свой контракт.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://www.elastic.co/docs/manage-data/data-store/near-real-time-search\" target=\"_blank\" rel=\"noopener\">Elastic: Near real-time search</a> — объясняет сегменты Lucene, refresh и то, почему изменения не всегда видны search сразу.</li><li><a href=\"https://www.elastic.co/docs/reference/elasticsearch/rest-apis/refresh-parameter\" target=\"_blank\" rel=\"noopener\">Elastic: The refresh parameter</a> — описывает <code>refresh=false</code>, <code>refresh=wait_for</code>, <code>refresh=true</code> и стоимость принудительного обновления.</li><li><a href=\"https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-get\" target=\"_blank\" rel=\"noopener\">Elastic: Get a document by its ID</a> — фиксирует realtime-поведение Get API и поля <code>_version</code>, <code>_seq_no</code> и <code>_primary_term</code>.</li><li><a href=\"https://www.elastic.co/docs/reference/elasticsearch/rest-apis/optimistic-concurrency-control\" target=\"_blank\" rel=\"noopener\">Elastic: Optimistic concurrency control</a> — объясняет защиту от устаревших изменений через sequence number и primary term.</li></ul>"
}