function escapeHtml(value) { return String(value) .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } function paragraph(text) { return '
' + text + '
'; } function heading(text) { return '' + escapeHtml(Array.isArray(lines) ? lines.join('\n') : lines) + '';
}
function figure(src, alt, caption) {
return 'duplicate-ingest-suppressed, а не создают вторую учебную работу. Это узкий инвариант. Он не доказывает идемпотентность реального producer, очереди или API, потому что в модели нет сети, базы и конкурирующих процессов. Но именно такой маленький тест помогает заранее назвать ключ и границу его хранения, которые в проекте придётся сохранять и наблюдать.'),
codeBlock(ingestCode),
figure(
'/assets/editorial/2021/search-indexing-pipeline-2021.svg',
'Вертикальная схема учебного пути документа: source-of-truth сохраняет version 7, ingest подавляет повторный ключ, pending index ещё не участвует в query, refresh переносит документ в visible index, после чего query находит одну версию',
'Учебный путь записи: сохранение источника и видимость в выдаче разделены явным переходом refresh.',
),
heading('Бюджет задержки строится из наблюдаемых точек, а не из чужого значения'),
paragraph('Вместо формулы «поиск должен обновляться быстро» полезно записать четыре точки: savedAtMs, enqueuedAtMs, indexedAtMs и visibleAtMs. Их разность показывает, в каком отрезке находится отставание. В fixture источник сохранён на 100, ingest поставлен на 110, проекция подготовлена на 125, а refresh сделан на 160. Разница между сохранением и видимостью равна 60 условным миллисекундам. Это арифметика теста, не реальная задержка и не целевой бюджет для какого-либо кластера.'),
paragraph('Когда эти точки известны, команда может договориться о реакции без ложной точности. Для обычной выдачи допустимо показать состояние ожидания и измерить следующую проверку. Для сценария, который обязан читать свою запись, потребуется другой путь: например, чтение источника или явно выбранный режим ожидания. Выбирать его надо по цене ожидания, нагрузке и поведению конкретной версии движка. Историческая документация Elasticsearch 7.13 описывает refresh как переход, делающий операции доступными search, и отдельно предупреждает, что search работает near-real-time. Это описание механизма, а не переносимый SLA.'),
codeBlock(freshnessContractCode),
paragraph('В реальном проекте сама единица бюджета тоже зависит от вопроса. Иногда нужно измерить время от публикации до первой видимости. Иногда важнее число текущих pending-версий или доля запросов, где карточка не прошла фильтр. Нельзя смешивать их в один «лаг поиска». Один показатель отвечает на вопрос о pipeline, другой — о запросе, третий — о содержимом. Сначала выбираем один пользовательский случай, затем оставляем у него минимальный набор времени, version и ключа события.'),
heading('Fixture показывает stale выдачу без настоящего движка'),
paragraph('Сквозная fixture сначала сохраняет документ в source-of-truth и ставит ingest. Затем второй вызов постановки с тем же id и version подавляется. После обработки document лежит в pending index, но searchVisible ещё возвращает ноль попаданий. Это и есть контролируемая stale выдача: источник существует, проекция подготовлена, но переход видимости не выполнен. Только после явного refreshSearch тот же запрос получает одну карточку с version 7.'),
paragraph('Такой пример важен именно своей скромностью. Он не строит индекс, не анализирует русский текст так же, как поисковый движок, не вызывает HTTP и не имитирует shard. Его результат проверяем: assertions фиксируют сохранение источника, подавление дубликата, пустой поиск до refresh, видимый текущий version после refresh, арифметику 60 и неразрушающий диагноз. Если один из этих результатов перестать выполняться после правки модели, Node завершится ошибкой.'),
codeBlock(fixtureCode),
heading('Маршрут для обычной выдачи'),
paragraph('Маршрут остаётся линейным: симптом — source уже сохранён, а query пуст; причина ищется в одном из переходов id:version; проверка фиксирует version и точки времени; действие выбирается только для найденной границы. Такой порядок не подменяет отставание видимости повторным сохранением карточки.'),
orderedList([
'Выбрать одну выдачу и один объект: например, поиск опубликованной статьи по заголовку. Не начинать с общего слова «индекс».',
'Записать source id и version, затем определить, какой ключ представляет постановку для этой версии.',
'Сохранить точки savedAtMs, enqueuedAtMs, indexedAtMs и visibleAtMs там, где они реально доступны. Не подставлять значения из fixture в рабочие логи.',
'Проверить один отрицательный путь: source уже существует, а query до refresh не находит документ. Это отличает ожидание видимости от потери source.',
'Для одинакового id:version повторить постановку и убедиться, что наблюдаемое действие не создаёт второй независимый ingest.',
'Выбрать реакцию для превышения проектного бюджета: ждать следующего планового перехода, показывать статус или запускать заранее согласованную проверку. Не начинать с удаления источника.',
'После исправления снова проверить query, version и время видимости. Если запрос всё ещё пустой, перейти к фильтру, области поиска и анализу текста, а не повторять refresh бесконечно.',
]),
heading('Что остаётся за границей этой заметки'),
paragraph('Elasticsearch 7.13 документирует, что refresh управляет видимостью для search, а Index API имеет собственные параметры refresh и versioning. Эти сведения нужны, чтобы не считать успешный index request универсальным признаком видимости. Но учебная Map не является проекцией внутреннего устройства Elastic и не может подтвердить поведение любого индекса, alias, реплики или настройки interval. Значения 100, 110, 125 и 160 выбраны только для детерминированного теста.'),
paragraph('В пакете не запускаются Elasticsearch, база, брокер, HTTP, браузер, CI и deployment. Здесь нет настоящего SLA, лога нагрузки, данных пользователей или обещания мгновенного поиска. Следующий проверяемый шаг в своём проекте — взять один безопасный документ, записать его id и version, сравнить момент сохранения с моментом поиска и отдельно проверить политику refresh выбранной исторической версии движка. Тогда симптом станет маршрутом, а не поводом создавать дубликаты.'),
],
[elasticNrt715, elasticIndex715, elasticRefresh715],
);
const mechanismArticle = createRevision(
{
slug: 'editorial-2021-07-mechanism-search-indexing',
title: 'Индексация и поиск: почему запись не означает видимость',
categories: ['Поиск', 'Данные', 'Механизм'],
cover: '/assets/editorial/2021/search-indexing-lag-budget-2021.svg',
excerpt: 'Разбираем четыре состояния одной карточки — source, ingest, pending index и visible index. Показываем, где появляется stale выдача, как version защищает учебную проекцию и почему refresh не равен сохранению.',
readingMinutes: 17,
},
[
paragraph('Ошибка начинается с неверного вывода: запрос на сохранение завершился успешно, значит пользователь уже увидит новую запись в текстовом поиске. Когда этот вывод не срабатывает, команда добавляет повторный запрос, принудительный refresh или второй кеш, не зная, на каком переходе пропала ожидаемая версия. Цена — не только лишняя нагрузка. Появляются два объяснения одной карточки: source считает актуальной version 7, а выдача показывает version 6 или ничего.'),
paragraph('Разложим механизм на четыре простых состояния. Source-of-truth владеет содержимым и version. Ingest хранит намерение построить проекцию. Pending index содержит уже подготовленный документ, который ещё не участвует в учебном query. Visible index — снимок, из которого query возвращает попадания. Все четыре состояния в этой статье — Map внутри Node. Они специально не являются ни базой, ни поисковым движком, ни broker, ни договором доставки. Их задача — сделать спор о видимости проверяемым.'),
heading('У одного документа несколько моментов готовности'),
paragraph('Документ может быть сохранён и при этом не готов для всех читателей. Для карточки по прямой ссылке достаточно источника. Для фонового обработчика может быть достаточно id и version в ingest. Для полнотекстового поиска нужна готовая видимая проекция. Эти результаты нельзя свести к одному булеву saved, потому что у них разные владельцы и разные доказательства. Если интерфейс обещает «опубликовано», а поиск живёт отдельным переходом, это нужно назвать прямо в контракте.'),
dataTable(
'Состояния учебной проекции и их границы',
['Состояние', 'Владелец в модели', 'Что уже доказано', 'Чего ещё нет'],
[
['source-of-truth', 'доменная запись', 'id, version и текст сохранены', 'нет признака, что search видел документ'],
['ingest queue', 'постановка проекции', 'есть одна задача id:version', 'нет признака, что проекция применена'],
['pending index', 'индексатор', 'актуальная версия подготовлена для перехода', 'query ещё возвращает старый visible snapshot'],
['visible index', 'контур поиска', 'query может вернуть version в данном учебном снимке', 'нет доказательства о другом фильтре, alias или среде'],
],
),
paragraph('Эта схема не означает, что в каждом проекте нужны четыре отдельные технологии. Иногда source и ingestion живут в одном приложении, иногда данные приходят из другого сервиса. Важно другое: у каждого перехода есть наблюдаемое условие. Если source.version равна 7, а event.version равна 6, обработчик не должен выдавать старую задачу за актуальную. Если pending.version равна 7, а query пустой, надо проверять видимость, а не содержимое источника.'),
heading('Ingest должен передавать не только id, но и версию'),
paragraph('Один id показывает, о какой карточке идёт речь, но не отвечает на вопрос о порядке обновлений. Поэтому учебное событие содержит id и version. Ключ id:version даёт простую границу повтору: две одинаковые постановки становятся одним наблюдаемым намерением. Это не защита от всех гонок. Например, в модели нет параллельного producer и нет durable queue. Но если код не может объяснить, что будет при втором вызове с тем же ключом, он уже не готов к реальному переходу между компонентами.'),
codeBlock(ingestCode),
paragraph('После извлечения события индексатор читает текущий source и сравнивает version. Если источник уже изменился, старое событие получает состояние stale-ingest-skipped и не кладёт устаревший текст в pending index. У этой ветки одна цель: не перепутать текущую доменную запись с задержавшейся работой. Она не заменяет optimistic concurrency control или внешнее versioning настоящего движка. В Elasticsearch 7.13 Index API действительно документирует собственную модель versioning; переносить её параметры в этот пример было бы ложным сходством.'),
codeBlock(versionCode),
heading('Refresh меняет видимость, а не историю источника'),
paragraph('После успешного индексирования fixture не копирует документ сразу в visible index. Она оставляет его в pending index и запускает тот же query. Результат нулевой, хотя source существует и event был обработан. Это намеренное место stale выдачи. Оно показывает, почему фраза «документ в индексе» может быть недостаточной: нужно уточнить, в каком именно состоянии и для какого вида чтения.'),
paragraph('Затем refreshSearch переносит pending-документ в видимую проекцию. В учебной модели этот вызов явный, синхронный и не имеет стоимости. В реальном Elastic refresh — понятие конкретного движка и конкретной версии: документация 7.13 описывает его как механизм, который делает операции с индексом доступными search, а параметры Index API отдельно различают отсутствие действий, ожидание refresh и его запрос. Из этого не следует, что explicit refresh нужно ставить в каждый write-path или что он даёт одинаковую цену на любой нагрузке.'),
codeBlock(refreshCode),
figure(
'/assets/editorial/2021/search-indexing-lag-budget-2021.svg',
'Схема учебного бюджета задержки: источник сохранён в точке 100, ingest поставлен в 110, pending index подготовлен в 125, refresh завершён в 160, query до refresh видит ноль попаданий, после него — текущую version; подпись отмечает, что 60 условных миллисекунд не являются SLA',
'Временная шкала fixture: наблюдаемая задержка складывается из переходов, а не называется «лагом поиска» без доказательств.',
),
heading('Search и прямое чтение отвечают на разные вопросы'),
paragraph('Иногда расследование осложняет то, что один способ чтения уже видит обновление, а другой ещё нет. В документации Elasticsearch 7.13 Get API по умолчанию обозначен как realtime и не зависит от момента, когда данные становятся видимыми search. Это полезная историческая граница: успешное чтение по id и успешный текстовый query могут требовать разного доказательства. Но нельзя переносить этот факт в любую архитектуру. В fixture source Map не эмулирует Get API, а visible Map не эмулирует индекс Elastic; модель лишь делает различие явным.'),
paragraph('Практический вывод короткий: в отчёте о сбое нужно указать, каким именно чтением найден документ. «Карточка открылась» и «поиск вернул карточку с нужным фильтром» — два разных факта. Первый проверяет source path. Второй проверяет query path, который включает видимость, область поиска, поля, анализ текста и фильтры. Когда эти факты склеены, повторная индексация маскирует проблему вместо того, чтобы найти её участок.'),
heading('Fixture проверяет переходы, а не рисует счастливую схему'),
paragraph('В runSearchIndexingFixture() один документ с version 7 сначала сохраняется в source. Первая постановка создаёт ingest-задачу, вторая с тем же ключом подавляется. До index и до refresh поиск по слову «свежести» пуст. После обработки pending index содержит документ, а безопасный диагноз говорит awaiting-refresh и запрещает destructive action. После refresh query возвращает ровно один hit с version 7; диагноз перемещается в query-contract.'),
codeBlock(fixtureCode),
paragraph('Assertions не проверяют скорость Elastic, устойчивость storage или поведение production. Они проверяют только то, что обещает сама модель: порядок этапов, один ingest-key, отсутствие попадания до refresh, одно попадание после него, сохранение version и арифметику 60 условных миллисекунд. Если добавить вторую версию, alias или анализатор, нужно сначала расширить fixture и назвать новый инвариант. Нельзя выдать текущий маленький прогон за тест распределённой поисковой системы.'),
heading('Маршрут от записи к видимому query'),
paragraph('Симптом здесь один: search не подтверждает ожидаемую version. Причина может быть только в source, ingest, pending-проекции, переходе видимости или самом query. Проверка идёт в этом порядке, а действие относится к найденной границе. Так forced refresh не становится универсальным ответом на любое пустое попадание.'),
orderedList([
'Зафиксировать id, version и конкретный запрос, который должен найти документ. «Он где-то не ищется» не является проверяемым симптомом.',
'Проверить source-of-truth: нужная версия действительно сохранена и доступна тому пути чтения, который заявлен контрактом.',
'Проверить ingest key id:version. Повторный вызов должен быть различим как duplicate или как новая версия, а не превращаться в анонимную вторую задачу.',
'Проверить, какую version принял индексатор. При несоответствии source.version и event.version не продолжать с устаревшим payload.',
'Проверить, находится ли документ в промежуточном состоянии ожидания видимости. Сохранить точки времени, прежде чем менять refresh policy.',
'После согласованного перехода видимости повторить тот же query и сравнить id, version, scope и фильтры. Одного найденного текста недостаточно, если запрос пользователя другой.',
'Если версия стала видимой, но query пуст, перейти к контракту запроса. Если нет — исправлять участок ingest или refresh с обратимым планом, а не удалять источник.',
]),
heading('Ограничения и историческая рамка'),
paragraph('Эта модель намеренно не хранит documents на диске, не создаёт сегменты, не выбирает refresh interval и не знает ничего о репликах. Она не обещает near-real-time как точную задержку. Значения времени — только входные данные для assertion. Исторические источники Elastic 7.13 нужны, чтобы правильно разделить index request, refresh и search visibility, но фактическое поведение проекта зависит от версии, настройки индекса, нагрузки, прав, routing и способа запроса.'),
paragraph('Следующий шаг — не включить опцию по чужой рекомендации, а проверить один поток собственной системы. Нужны id, version, timestamp и один фиксированный query. Затем можно решить, где хранить evidence, кто владеет свежестью и какая реакция допустима при отставании. Такая последовательность оставляет границу между источником и поиском понятной и не заставляет авторизованный write-path отвечать за всё поведение выдачи.'),
],
[elasticNrt715, elasticIndex715, elasticGet715, elasticRefresh715],
);
const fieldArticle = createRevision(
{
slug: 'editorial-2021-07-field-search-indexing',
title: 'Документ сохранён, но не найден: безопасная диагностика поиска',
categories: ['Поиск', 'Данные', 'Разбор'],
cover: '/assets/editorial/2021/search-indexing-diagnosis-2021.svg',
excerpt: 'Пошаговый разбор случая, когда source существует, а поисковая выдача пуста или устарела: какие доказательства собрать, как отличить ожидание refresh от ошибки query и почему безопаснее не удалять документ первым действием.',
readingMinutes: 16,
},
[
paragraph('Проблема выглядит так: документ сохранён, карточка открывается, но поиск не находит его по ожидаемому слову. Самая дорогая реакция в этот момент — сразу удалить запись, создать её заново или запустить широкую повторную индексацию. Так можно потерять исходную version, сделать новый event неотличимым от старого и стереть доказательство того, что источник вообще был исправен. Сначала нужен короткий отчёт: какой id, какая version, какой query, где именно документ уже виден и в какой момент это наблюдалось.'),
paragraph('Ниже — полевой маршрут для одного учебного документа. Он не запускает Elasticsearch, БД, broker, HTTP или реальный reindex. Сквозная fixture хранит source, ingest, pending index и visible index в памяти. До refresh source уже содержит version 7, а query пуст. Диагностика возвращает awaiting-refresh и действие без удаления. После refresh тот же query находит version 7, и следующий вопрос меняется: не «где документ», а «совпадает ли контракт запроса с тем, что ищет пользователь».'),
heading('Сначала собираем доказательство, которое переживёт исправление'),
paragraph('Минимальная карточка инцидента должна содержать document id, source version, ключ постановки, текст и параметры query, время наблюдения и точку pipeline, где сделана проверка. Если есть доступ к источнику, записываем именно version, а не только текст заголовка: одинаковый заголовок может принадлежать двум разным состояниям. Если есть event, сохраняем id:version, а не только строку «поставили в очередь». Такой набор позволяет повторить проверку после изменения настройки и не спутать новую работу с исходным симптомом.'),
codeBlock(evidenceCode),
paragraph('Доказательство не должно содержать персональные данные, секреты или полный пользовательский запрос, если они не нужны для причины. Для учебной fixture достаточно id, version, одного слова поиска и четырех моментов времени. В реальном проекте к ним добавляются только те поля, которые разрешено собирать и которые отвечают на конкретную ветку: index target, alias, filter, rights, analyzer или timestamp. Чем шире бессмысленный лог, тем труднее увидеть отличие source path от query path.'),
dataTable(
'Диагностика «сохранён, но не найден»: симптом не равен причине',
['Наблюдение', 'Вероятная граница', 'Контрольная проверка', 'Rollback-safe действие'],
[
['source отсутствует по id', 'write 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, поле и анализ текста', 'изменять запрос или проекцию точечно и с обратимым шагом'],
],
),
paragraph('Таблица нужна не для угадывания причины по одному признаку. Она удерживает порядок: сначала доказываем, что source существует; затем выясняем судьбу id:version; только после этого обсуждаем refresh; и лишь потом меняем query или mapping. Если начать с последнего пункта, можно создать новый индекс и всё равно не заметить, что event не был принят. Если начать с удаления, можно лишиться единственной версии, с которой можно сравнить результат.'),
heading('Когда source существует, а выдача stale, не подменяем диагноз reindex'),
paragraph('В fixture после обработки ingest документ лежит в pending index. Source уже подтверждён, ключ постановки был один, version совпала. Тем не менее поиск по слову «свежести» возвращает ноль. Этот факт не доказывает, что индекс сломан. Он доказывает только то, что visible snapshot ещё не получил документ. diagnoseVisibility возвращает awaiting-refresh, отмечает version и запрещает destructive action. Так оператор может измерить промежуток и применить заранее выбранную policy, не изобретая новую запись.'),
codeBlock(diagnosisCode),
paragraph('Здесь важно отделить контролируемый эксперимент от рабочего решения. В модели explicit refresh — одна функция и она всегда завершает переход. В реальной системе тот же термин имеет цену, версионные особенности и границы охвата. Документация Elasticsearch 7.13 пишет, что refresh делает операции доступными search, но не превращает source read и text query в один маршрут. Поэтому безопасный вопрос звучит так: «какой evidence показывает, что нужная version должна была стать видимой именно для этого query?»'),
figure(
'/assets/editorial/2021/search-indexing-diagnosis-2021.svg',
'Дерево безопасной диагностики: сначала проверить source и version, затем key ingest, pending index и evidence refresh; если текущая version уже видима, перейти к scope, filter и текстовому анализу; на всех ветках запрещено удалять source без подтверждённой причины',
'Маршрут расследования: каждое действие сохраняет исходный документ и оставляет следующий проверяемый факт.',
),
heading('Проверяем контракт query после доказанной видимости'),
paragraph('Если visible index уже содержит current version, а поиск пуст, повторять ingest бессмысленно. Нужно зафиксировать фактический query: в каком поле ищем, какой filter ограничивает набор, в каком scope находится документ, как нормализуется текст и не исключают ли права запись из выдачи. В fixture query упрощён до поиска подстроки по title и body. Он не моделирует stemming, токенизацию, synonyms, routing, alias или permissions. Поэтому его успешный hit не является тестом реального анализатора.'),
paragraph('Историческая документация Elasticsearch 7.13 полезна здесь ещё одной границей: Get API по умолчанию realtime и не зависит от момента, когда данные становятся видимы search. В движке это позволяет различать «документ можно прочитать по id» и «документ найден обычным search». Но нельзя использовать это как оправдание для пропуска проверки query. Пользователь обычно видит именно выдачу с её фильтрами, а не внутреннее чтение по id. В отчёте должны быть оба факта, если оба важны.'),
heading('Одна fixture, два безопасных состояния диагностики'),
paragraph('Сквозной тест создаёт один 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. Это означает, что модель не смешивает два вопроса в один.'),
codeBlock(fixtureCode),
paragraph('Assertions фиксируют каждый заявленный вывод. Есть source document, duplicate enqueue подавлен до и после consume, запрос до индексирования и до refresh пуст, refresh показывает одну текущую version, задержка вычислена как 60 условных миллисекунд, а обе диагностические ветки неразрушающие. Если кто-то изменит порядок и перенесёт pending-документ в visible раньше refresh, assertion stale выдачи станет ложным. Так review получает не только текстовый вывод, но и маленькую проверку причинной цепочки.'),
heading('Rollback-safe маршрут для оператора'),
paragraph('Симптом — сохранённая запись отсутствует в заданной выдаче. Причину не угадываем: сначала source, затем id:version и видимость, после этого query. Каждая проверка оставляет evidence, а действие обратимо: источник не удаляется, повторная постановка привязана к текущей version, а изменение запроса проверяется исходным запросом.'),
orderedList([
'Сохранить id, source version, event key, точный query и время наблюдения. Не исправлять систему до появления этого минимального следа.',
'Проверить source-of-truth по id. Если его нет, остановить дальнейшие поисковые действия и выяснить write path; не создавать дубликат «для проверки».',
'Если source есть, проверить состояние постановки id:version. Повторная постановка допустима только как явно идемпотентный шаг, а не как новый анонимный event.',
'Если проекция ждёт видимости, зафиксировать pending version и точки времени. Использовать только согласованную project policy или локальный controlled test; не распространять его на все записи.',
'Если visible version отстаёт, сравнить version источника и event, затем поставить именно текущую version с сохранением старого evidence. Не стирать прежнюю запись до проверки результата.',
'Если visible version актуальна, проверить scope, filter, поле, нормализацию текста и права query. Не возвращаться к refresh, пока этот контракт не проверен.',
'После точечной правки повторить исходный query, записать результат и добавить сценарий в fixture или интеграционный тест проекта. Откатить изменение можно по сохранённому evidence, а не по памяти о симптоме.',
]),
heading('Границы, версия и следующий шаг'),
paragraph('Сама по себе фраза «документ проиндексирован» не даёт права менять источник или объявлять инцидент закрытым. В Elastic 7.13 есть отдельные механизмы Index API, refresh и Get API; их реальные параметры, стоимость и поведение определяются развернутой версией и настройками. Наша fixture намеренно не заявляет ничего о shard, replica, alias, interval, persistence, concurrency, permissions или продуктивном логе. Она делает один безопасный вывод: сначала найти участок между source и query, затем применять обратимое действие.'),
paragraph('Следующий шаг для своего проекта — выбрать один тестовый документ без чувствительных данных, пройти его по той же карточке evidence и сравнить source version с результатом одного фиксированного search. Если между ними есть промежуток, зафиксируйте владельца и реакцию. Если проекция уже актуальна, переключите расследование на query contract. Такой порядок бережёт источник, не обещает мгновенную видимость и оставляет после исправления воспроизводимый способ проверки.'),
],
[elasticNrt715, elasticIndex715, elasticGet715, elasticRefresh715],
);
export const revisions = [practiceArticle, mechanismArticle, fieldArticle]
.map(({ proseLength, ...revision }) => revision);
if (process.argv.includes('--print-revisions')) {
process.stdout.write(JSON.stringify(revisions));
} else if (process.argv.includes('--verify-fixture')) {
process.stdout.write(JSON.stringify(runSearchIndexingFixture(), null, 2) + '\n');
}