На 31 июля 2026 года строгий аудит проходит 124 из 358 созданных материалов. Остальные 234 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
На 31 июля 2026 года строгий аудит проходит 127 из 358 созданных материалов. Остальные 231 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
Revision-модуль экспортирует только overlay-поля. В нём нет
<code>date</code>, <code>author</code> или подключения к
<code>editorialRevisions</code>; исходные архивные поля останутся владельцем
базовой записи. Команда <code>--print-revisions</code> печатает тот же JSON,
что import-safe export. Команда <code>--verify-fixture</code> выполняет
исключительно локальную детерминированную модель в памяти.
## Проход 1. Структура, объём и голос М4 — пройдено
| Revision | Симптом и цена в начале | Главный вопрос | Объём основного текста |
| --- | --- | --- | --- |
| Практика | Карточка открывается, но поиск пуст или stale; цена — дубликат, потерянная история версии и неверное массовое действие | Как договориться о свежести обычной выдачи до выбора настройки движка | **10 440** знаков body |
| Механизм | Успешное сохранение ошибочно считают признаком видимости; цена — source с version 7 и выдача с version 6 или без попадания | Где проходит граница source, ingest, pending index, refresh и query | **10 255** знаков body |
| Полевой разбор | Source существует, а заданный query пуст; цена — удалить доказательство, создать копию и потерять причину | Как собрать evidence и пройти rollback-safe диагностику | **10 247** знаков body |
- Все тексты находятся в диапазоне 5 000–15 000 знаков. В каждом есть
проблема и стоимость в первых двух абзацах, минимум пять смысловых
разделов, таблица, figure с осмысленным <code>alt</code>/<code>figcaption</code>,
рабочие JS-фрагменты, нумерованный маршрут и раздел ограничений.
- В каждом маршруте названы четыре звена: симптом, граница причины, проверка
version и точек времени, затем действие только для подтверждённого участка.
Это сохраняет прагматичный порядок «симптом → причина → проверка →
действие», а не даёт один универсальный reindex.
- Голос соответствует М4 июля 2021 года: короткая техническая речь, явные
владельцы состояния и условия. Новое умение — отделить хранение от
поисковой проекции и диагностировать их границу. Оно естественно продолжает
материалы о кешах, очередях, event-потоках и согласованности данных.
- Убраны признаки позднего техлидского тона: нет выдуманной команды,
платформы, пользовательских метрик, SLA, инцидента, production-лога или
обещания, что задержка одинаково решается на любой нагрузке.
Вердикт: **пройдено**. Вставленные значения 100, 110, 125, 160 и 60 служат
только детерминированной арифметике fixture. Текст прямо называет их
условными и не переносит в договор production.
## Проход 2. Технические утверждения и сквозная fixture — пройдено
Один неизменяемый документ <code>article-2021-07-42</code> с version 7
проходит сквозной путь:
1. сохраняется в <code>sourceOfTruth</code>;
2. ставится в <code>ingestQueue</code> по ключу <code>id:version</code>;
3. повторная постановка того же ключа получает
<code>duplicate-ingest-suppressed</code>;
4. актуальное событие попадает в <code>pendingIndex</code>;
5. query до refresh возвращает ноль попаданий;
6. <code>refreshSearch</code> переносит ту же version в
<code>visibleIndex</code>;
7. тот же query возвращает один hit, а диагностика меняется с
<code>awaiting-refresh</code> на <code>query-contract</code>.
| Инвариант fixture | Фактическая проверка |
| --- | --- |
| Source сохранён раньше ingest | <code>source-saved → ingest-enqueued</code> входит в утверждённый порядок stages |
| Повторная постановка идемпотентна в границах fixture | повтор до consume и повтор после него возвращают <code>duplicate-ingest-suppressed</code>; accepted key остаётся в учебном receipt-set |
| Source не равен видимости поиска | query пуст до index и после index до refresh |
| Version не теряется при переходе | hit после refresh содержит current version 7 |
| Наблюдаемая задержка не выдана за SLA | <code>visibleAtMs − savedAtMs = 60</code> проверяется как арифметика модели |
| Диагностика не разрушает источник | обе пройденные diagnostic-ветки имеют <code>destructiveAction: false</code> |
| Конечное действие относится к query | после видимости результат — <code>query-contract</code>, а не новый blind reindex |
Fixture содержит десять истинных assertions. Один из них проверяет suppression
повтора как до consume, так и после него; receipt-set остаётся только учебной
in-memory границей. Она не реализует Elasticsearch,
права, persistence, concurrency или реальную policy refresh. Поэтому
<code>Map</code> в статье не назван настройкой поискового движка и не
объявлен доказательством delivery guarantee, near-real-time SLA или
производительности.
Вердикт: **пройдено**. Пример исполняется без внешней инфраструктуры и
проверяет все заявленные результаты: сохранение, ingest, stale выдачу,
refresh, query, idempotent re-enqueue, version и безопасную диагностику.
## Проход 3. Точность, визуалы и ссылки — пройдено
| Утверждение в материалах | Первичный или официальный источник | Проверенная граница |
| --- | --- | --- |
| Линия Elasticsearch 7.13 существовала к июлю 2021 года | [Elastic 7.13.0 released](https://www.elastic.co/blog/whats-new-elastic-7-13-0/) | Официальный анонс датирован 25 мая 2021 года. Поэтому 7.13 — корректная историческая рамка для июльских текстов; 7.15 для неё не используется. |
| В Elasticsearch 7.13 refresh делает операции с индексом доступными search, а near-real-time не означает мгновенную видимость | [Elasticsearch 7.13: Near real-time search](https://www.elastic.co/guide/en/elasticsearch/reference/7.13/near-real-time.html) | Источник описывает поведение линии 7.13, выпущенной до июля 2021 года. Статьи не переносят его интервал, стоимость или режим на fixture и не объявляют SLA. |
| Index API 7.13 отдельно описывает refresh и versioning | [Elasticsearch 7.13: Index API](https://www.elastic.co/guide/en/elasticsearch/reference/7.13/docs-index_.html) | Учебная проверка version не является вызовом API, external versioning или заменой optimistic concurrency control. |
| Get API 7.13 по умолчанию realtime и отделён от момента search visibility | [Elasticsearch 7.13: Get API](https://www.elastic.co/guide/en/elasticsearch/reference/7.13/docs-get.html) | Source Map не моделирует Get API. Факт используется только для объяснения, почему чтение по id и полнотекстовый query требуют отдельных доказательств. |
| Refresh parameter различает отсутствие действия, запрос refresh и ожидание перехода | [Elasticsearch 7.13: The refresh parameter](https://www.elastic.co/guide/en/elasticsearch/reference/7.13/docs-refresh.html) | Пакет не рекомендует включать режим на любой write-path и не делает утверждение о нагрузке конкретного кластера. |
- <code>search-indexing-pipeline-2021.svg</code> показывает источник,
ключ id:version, подавленный duplicate, pending state, stale query,
refresh и найденный current version. На 375 px проверены карточки,
стрелки и нижняя подпись.
- <code>search-indexing-lag-budget-2021.svg</code> показывает именно
учебные точки 100/110/125/160 и явно отмечает 60 как assertion, а не
SLA. На 375 px проверены заголовок, промежуток awaiting-refresh и
заключительная карточка.
- <code>search-indexing-diagnosis-2021.svg</code> показывает дерево
source → ingest → pending → visible → query и действие при отрицательной
ветке. После мобильного рендера нижняя предупреждающая карточка была
разбита на две строки; clipping и horizontal overflow в финальном варианте
не наблюдаются.
- Во всех SVG есть <code>title</code>, <code>desc</code> и
<code>role="img"</code>. Static scan не нашёл <code>script</code>,
<code>foreignObject</code>, внешних href/src или raster data URI.
| Import-safe export и draft gate | PASS: **10 440 / 10 255 / 10 247** знаков body; для трёх slug найдены sections, table, figure, code, route, sources и локальные assets |
| In-memory fixture | PASS: десять assertions истинны; source сохранён, duplicate подавлен до и после consume, stale query наблюдаем до refresh, current version видима после refresh, диагностика неразрушающая |
| <code>xmllint --noout</code> | PASS, все три SVG — корректный XML |
| SVG safety scan | PASS: не найдены <code>script</code>, <code>foreignObject</code>, внешние href/src или raster data URI |
| Sharp mobile preflight | PASS: все три финальных SVG отрендерены в PNG шириной 375 px и просмотрены вручную; clipping, overlap и horizontal overflow внутри схем не обнаружены |
| Scope/self-review | PASS: П41 создаёт только пять перечисленных файлов; не меняет registry, README, <code>articles.json</code>, общие аудиты, чужие файлы и Git |
<code>npm run audit:draft</code> завершилась с code 0. npm напечатал
существующие предупреждения о пользовательских <code>store-dir</code>,
<code>cache-dir</code> и <code>public-hoist-pattern</code>; они не относятся
к П41 и не менялись пакетом.
Намеренно не запускались: strict audit через registry, production build,
<titleid="title">Дерево безопасной диагностики сохранённого, но не найденного документа</title>
<descid="desc">Диагностика начинает с source и version, затем проверяет ingest, pending index, refresh и контракт query. На всех ветках сначала сохраняется evidence, а source не удаляется без подтверждённой причины.</desc>
<titleid="title">Учебная временная шкала задержки между сохранением и видимостью поиска</title>
<descid="desc">Диаграмма показывает четыре условные точки времени: source сохранён на 100, ingest поставлен на 110, pending index подготовлен на 125, refresh даёт видимость на 160. Наблюдаемая разница 60 условных миллисекунд не является SLA.</desc>
<titleid="title">Учебный путь документа от source до поисковой выдачи</title>
<descid="desc">Вертикальная схема показывает сохранение документа version 7, идемпотентную постановку ingest, pending index, stale query до refresh, явный refresh и успешный query после перехода видимости.</desc>
note:'историческая документация линии 7.13: refresh делает операции с индексом доступными для поиска; описание near-real-time не является SLA учебной модели',
note:'документация различает realtime GET по умолчанию и момент, когда данные видны search; это граница для диагностики, а не модель хранилища в статье',
};
constelasticRefresh715={
title:'Elasticsearch 7.13: The refresh parameter',
note:'историческое описание значений false, true и wait_for и их влияния на видимость операции для search; решение о режиме зависит от нагрузки и контракта проекта',
};
exportconstsearchTrainingDocument=Object.freeze({
id:'article-2021-07-42',
version:7,
title:'Контракт свежести выдачи',
body:'Сохранённый документ проходит ingest, индексирование, refresh и только затем становится виден учебному search.',
excerpt:'Сохранённая карточка ещё не обязана быть результатом поиска. Разбираем договор свежести: источник данных, ingest, версия, refresh, наблюдаемая задержка и безопасный маршрут для оператора.',
readingMinutes:16,
},
[
paragraph('Симптом знакомый: карточка уже открывается по прямой ссылке, а поиск по её заголовку возвращает пустую выдачу или старый текст. Цена ошибки не сводится к неудобному поиску. Пользователь повторяет действие, редактор начинает создавать дубликат, а разработчик может запустить повторную индексацию, не зная, была ли исходная запись сохранена и на каком участке она перестала быть видимой. После такой спешки история изменения становится хуже исходного сбоя.'),
paragraph('В июле 2021 года я бы начал не с параметра конкретного поискового движка, а с договора для одной выдачи. Нужно назвать момент, от которого считаем свежесть, момент, когда запись должна участвовать в запросе, и данные, по которым можно отличить обычное ожидание от потери события. Ниже используется один локальный сценарий на JavaScript. Он держит source-of-truth, ingest, pending index и видимую проекцию в памяти. Это не Elasticsearch, не база, не broker и не результат замера в production.'),
heading('Свежесть выдачи — отдельный результат, а не побочный эффект записи'),
paragraph('Сохранение источника и видимость в поиске отвечают на разные вопросы. Источник хранит корректную версию карточки. Поисковая проекция хранит форму, удобную для запроса. Между ними появляется работа: сформировать ingest-задачу, принять нужную версию, обновить индексную проекцию и открыть её для query. Если назвать все эти шаги словом «сохранили», невозможно понять, где искать причину: в записи, в постановке, в обработчике, в refresh или в самом запросе.'),
paragraph('Договор свежести полезно писать рядом с пользовательским сценарием. Например: «после сохранения статьи обычная текстовая выдача должна получить либо видимую текущую версию, либо понятный статус ожидания; редактор не создаёт вторую статью вместо первой». Это не SLA и не числовое обещание по умолчанию. Здесь важнее граница: какую выдачу обсуждаем, какая версия источника является актуальной, какой сигнал подтверждает видимость и кто принимает решение при отставании.'),
dataTable(
'Минимальный договор свежести для одной поисковой выдачи',
['Часть договора','Что фиксируем','Чем проверяем','Чего не обещаем'],
[
['Источник','id и доменная version сохранённой карточки','чтение source-of-truth по id','что query уже видит эту версию'],
['Ingest','ключ id:version и состояние постановки','одна запись задачи для одинакового ключа','что любая повторная постановка создаст новую работу'],
['Индексирование','version, принятая в pending-проекцию','сравнение event.version с source.version','что старая задача может переписать новую версию'],
['Видимость','момент refresh и version в visible-проекции','query плюс visibleAtMs','мгновенную видимость после сохранения'],
['Реакция','допустимое действие при задержке','зафиксированный маршрут диагностики','что delete и reindex всегда безопасны'],
],
),
paragraph('У этой таблицы есть практическая польза: она запрещает спорить о «медленном поиске» без объекта наблюдения. Если проблема относится к фильтру категории, это уже договор query. Если событие не поставлено, это ingest. Если pending-версия есть, а visible ещё нет, это переход видимости. Каждая ветка требует своей проверки и своего владельца. Нельзя лечить их одинаковым повтором сохранения страницы.'),
heading('Сначала отмечаем границу источника и проекции'),
paragraph('Для одной учебной статьи источник содержит устойчивые id, version, title и body. Его version принадлежит доменной записи, а не поисковой строке. Проекция может менять форму текста, поля и стратегию запроса, но не должна изобретать новую версию содержимого. Поэтому ingest получает id и version. Обработчик сверяет их с текущим источником до того, как положит данные в pending index. Эта проверка не делает систему распределённо согласованной; она только не даёт старому учебному событию молча выдать себя за новую карточку.'),
codeBlock(sourceContractCode),
paragraph('Стабильный ключ постановки строится из id и version. Fixture сохраняет принятый ключ отдельно от самой очереди: повтор до consume и повтор после consume получают <code>duplicate-ingest-suppressed</code>, а не создают вторую учебную работу. Это узкий инвариант. Он не доказывает идемпотентность реального producer, очереди или API, потому что в модели нет сети, базы и конкурирующих процессов. Но именно такой маленький тест помогает заранее назвать ключ и границу его хранения, которые в проекте придётся сохранять и наблюдать.'),
'Вертикальная схема учебного пути документа: source-of-truth сохраняет version 7, ingest подавляет повторный ключ, pending index ещё не участвует в query, refresh переносит документ в visible index, после чего query находит одну версию',
'Учебный путь записи: сохранение источника и видимость в выдаче разделены явным переходом refresh.',
),
heading('Бюджет задержки строится из наблюдаемых точек, а не из чужого значения'),
paragraph('Вместо формулы «поиск должен обновляться быстро» полезно записать четыре точки: <code>savedAtMs</code>, <code>enqueuedAtMs</code>, <code>indexedAtMs</code> и <code>visibleAtMs</code>. Их разность показывает, в каком отрезке находится отставание. В 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, но <code>searchVisible</code> ещё возвращает ноль попаданий. Это и есть контролируемая stale выдача: источник существует, проекция подготовлена, но переход видимости не выполнен. Только после явного <code>refreshSearch</code> тот же запрос получает одну карточку с 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 выбранной исторической версии движка. Тогда симптом станет маршрутом, а не поводом создавать дубликаты.'),
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 возвращает попадания. Все четыре состояния в этой статье — <code>Map</code> внутри Node. Они специально не являются ни базой, ни поисковым движком, ни broker, ни договором доставки. Их задача — сделать спор о видимости проверяемым.'),
heading('У одного документа несколько моментов готовности'),
paragraph('Документ может быть сохранён и при этом не готов для всех читателей. Для карточки по прямой ссылке достаточно источника. Для фонового обработчика может быть достаточно id и version в ingest. Для полнотекстового поиска нужна готовая видимая проекция. Эти результаты нельзя свести к одному булеву <code>saved</code>, потому что у них разные владельцы и разные доказательства. Если интерфейс обещает «опубликовано», а поиск живёт отдельным переходом, это нужно назвать прямо в контракте.'),
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. Ключ <code>id:version</code> даёт простую границу повтору: две одинаковые постановки становятся одним наблюдаемым намерением. Это не защита от всех гонок. Например, в модели нет параллельного producer и нет durable queue. Но если код не может объяснить, что будет при втором вызове с тем же ключом, он уже не готов к реальному переходу между компонентами.'),
codeBlock(ingestCode),
paragraph('После извлечения события индексатор читает текущий source и сравнивает version. Если источник уже изменился, старое событие получает состояние <code>stale-ingest-skipped</code> и не кладёт устаревший текст в 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('Затем <code>refreshSearch</code> переносит pending-документ в видимую проекцию. В учебной модели этот вызов явный, синхронный и не имеет стоимости. В реальном Elastic refresh — понятие конкретного движка и конкретной версии: документация 7.13 описывает его как механизм, который делает операции с индексом доступными search, а параметры Index API отдельно различают отсутствие действий, ожидание refresh и его запрос. Из этого не следует, что explicit refresh нужно ставить в каждый write-path или что он даёт одинаковую цену на любой нагрузке.'),
'Схема учебного бюджета задержки: источник сохранён в точке 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('В <code>runSearchIndexingFixture()</code> один документ с version 7 сначала сохраняется в source. Первая постановка создаёт ingest-задачу, вторая с тем же ключом подавляется. До index и до refresh поиск по слову «свежести» пуст. После обработки pending index содержит документ, а безопасный диагноз говорит <code>awaiting-refresh</code> и запрещает destructive action. После refresh query возвращает ровно один hit с version 7; диагноз перемещается в <code>query-contract</code>.'),
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 отвечать за всё поведение выдачи.'),
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 пуст. Диагностика возвращает <code>awaiting-refresh</code> и действие без удаления. После 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(
'Диагностика «сохранён, но не найден»: симптом не равен причине',
['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 ещё не получил документ. <code>diagnoseVisibility</code> возвращает <code>awaiting-refresh</code>, отмечает version и запрещает destructive action. Так оператор может измерить промежуток и применить заранее выбранную policy, не изобретая новую запись.'),
codeBlock(diagnosisCode),
paragraph('Здесь важно отделить контролируемый эксперимент от рабочего решения. В модели 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 без подтверждённой причины',
'Маршрут расследования: каждое действие сохраняет исходный документ и оставляет следующий проверяемый факт.',
),
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 возвращает <code>ingest-enqueued</code>; повторная до consume и повторная после него с тем же ключом возвращают <code>duplicate-ingest-suppressed</code>. До refresh diagnosis указывает на pending state и не предлагает delete. После refresh query возвращает один hit с current version, а diagnosis переводит расследование в <code>query-contract</code>. Это не означает, что любой пропавший документ нужно ждать до 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. Такой порядок бережёт источник, не обещает мгновенную видимость и оставляет после исправления воспроизводимый способ проверки.'),
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.