Изменения и удаления через манифест импорта
Первоначальный импорт наполнил поисковую копию. Теперь редактор меняет article-107, добавляет материал и удаляет устаревшую заметку. Повторять весь корпус можно, но полезнее понимать, какие операции необходимы. Результат урока — инкрементальная синхронизация по манифесту последнего полностью подтверждённого импорта.
Используем sync в полном manage.py. Функция работает с одним локальным импортёром и явным физическим индексом. Команды и файловые операции учебного приложения при написании не выполнялись. Ожидаемые списки изменений ниже следуют из алгоритма; они не являются журналом реально обновлённого кластера.
Манифест хранит наше подтверждённое знание
Файл .state/library-v1.json сопоставляет doc_id и content_hash. Это не копия полного текста и не резервная копия OpenSearch. Он отвечает на вопрос, какие подготовленные проекции импортёр считает успешно переданными в данный физический индекс. Имя индекса в имени файла исключает случайное применение состояния v1 к пустому v2.
Текущий корпус сначала читается полностью через documents(). Затем строится новый словарь хешей. Идентификаторы с изменившимся или неизвестным хешем получают операцию index; идентификаторы из прошлого манифеста, отсутствующие в новом корпусе, получают delete. Неизменные документы не требуют записи при условии, что индекс никто не изменял за пределами этого импортёра.
Следующий листинг — центральный фрагмент sync; docs уже содержит весь проверенный корпус, а old прочитан из манифеста:
current = {d['doc_id']: d['content_hash'] for d in docs}
actions = [
('index', d['doc_id'], d)
for d in docs
if old.get(d['doc_id']) != d['content_hash']
]
actions += [
('delete', identity, None)
for identity in old
if identity not in current
]
bulk(target, actions)
Вместо частичного обновления отдельных полей отправляем всю новую проекцию. Если метка исчезла из исходника, она должна исчезнуть и из поисковой записи. Частичный update, который передаёт только присутствующие поля, мог бы случайно оставить старую метку. Полная замена делает связь между исходником и проекцией понятнее.
Отсутствие файла требует законченного чтения
Удаление выводится из разницы двух полных наборов идентификаторов. Если один файл повреждён и загрузчик его пропустил, сокращённый результат нельзя считать новым редакционным состоянием. Поэтому ошибка чтения останавливает синхронизацию до вычисления удалений. Пустой корпус также отклоняется: это предохраняет от ошибочного пути к папке.
Такое правило не отменяет настоящую потребность удалить все документы. Оно выделяет массовое очищение в отдельную операцию, где явно известна причина. В учебном проекте эта операция не автоматизирована. Без такого различия временная недоступность файловой системы могла бы выглядеть точно так же, как решение редактора убрать библиотеку целиком.
Рассмотрим последовательность. В прошлом манифесте присутствуют article-107, article-108 и article-109. Теперь первый имеет новый текст и редакцию, второй не менялся, третий файл удалён. Ожидаются index для первого и delete для третьего. Второй не отправляется. Добавленный article-112 получил бы ещё один index независимо от значения номера редакции других документов.
Хеш реагирует на содержимое всей проекции, включая видимость. Переключение audience с public на staff требует записи даже при неизменном основном тексте. Это важный случай: поисковый индекс должен перестать показывать документ публично. Если сравнивать только body, изменение прав не попало бы в список действий.
Состояние записывается после полного успеха
После bulk создаём временный файл нового манифеста и заменяем прежний файл. До этого момента старое состояние сохраняется. Если одна группа операций завершилась, а следующая отказала, индекс может быть частично изменён, но новый манифест не принят. Следующий запуск повторно рассмотрит все неподтверждённые различия.
Фрагмент сохранения из полной функции:
temporary = state_file.with_suffix('.tmp')
temporary.write_text(json.dumps(current, indent=2), encoding='utf-8')
temporary.replace(state_file)
Замена файла помогает избежать обычного промежуточного чтения наполовину записанного JSON в одной файловой системе. Она не является распределённой транзакцией с OpenSearch и не даёт здесь отдельной гарантии при потере питания. Учебная программа не выполняет fsync-протокол для всех файлов. Для производственной доставки понадобится более строгая модель состояния.
Стабильные операции делают повтор приемлемым: полный index под тем же _id восстанавливает требуемую проекцию, повторный delete достигает отсутствия документа. Но если два импортёра с разными снимками действуют одновременно, старое содержимое может перезаписать новое. Поэтому фиксируем ограничение одного владельца синхронизации, а не приписываем хешу защиту от любой конкуренции.
Локальная блокировка и внешние изменения
sync использует файловую блокировку fcntl. Второй процесс на том же поддерживаемом локальном файловом окружении не должен одновременно войти в критический участок импорта. Блокировка относится к авторскому инструменту и общей папке состояния; она не мешает чужому HTTP-клиенту писать напрямую в индекс. Права доступа должны ограничивать такие независимые изменения отдельно.
Архив ориентирован на Unix-подобную среду с fcntl. На Windows этот модуль отсутствует, поэтому для переноса потребуется заменить механизм блокировки. Это конкретная граница воспроизводимости, а не неопределённое «может не работать». HTTP-транспорт и формат документов при этом не зависят от данной реализации локальной блокировки.
Если индекс удалили и создали заново под тем же именем, прежний манифест станет ложным знанием. Импортёр пропустит документы, которые считает уже записанными. Восстановление требует осознанно убрать соответствующий локальный манифест и выполнить полный импорт в отдельном согласованном режиме. Поэтому имя версии и сохранность индекса входят в эксплуатационный договор.
Ожидаемые состояния после двух последовательных успешных запусков:
Первый запуск после изменения → нужные index/delete, затем новый манифест
Повтор без изменения → actions: 0
Частичный отказ → старый манифест, возможное частичное изменение индекса
Следующий запуск → повтор неподтверждённых действий
Мы не объявляем actions:0 проверкой равенства всего индекса корпусу. Это только отсутствие различий относительно локального манифеста. Если документы менялись извне, нужен отдельный аудит или восстановление. В учебном проекте единственный импортёр делает это предположение явным и понятным.
Удаление файла и временное снятие публикации имеют разные результаты. При удалении doc_id исчезает из полного корпуса, и импортёр отправляет delete. При published:false документ остаётся в индексе, но исключается из публичного query. Это позволяет сохранить известную редакцию для будущего возврата видимости. Какой вариант выбрать, определяет редакционный договор, а не желание уменьшить число операций.
Похожим образом изменение url не меняет doc_id. В следующем импорте заменяется поле ссылки существующего материала. Если вместо этого создать новый идентификатор без удаления старого исходника, поисковая копия закономерно получит два материала. Алгоритм не способен угадать, что редактор хотел переименовать запись, а не добавить самостоятельную статью. Стабильная идентичность остаётся обязанностью подготовки корпуса.
Для самостоятельного разбора удалите метку из исходника, затем представьте отказ последней операции пакета. Опишите запись в индексе и состояние манифеста по отдельности. Теперь можно сменить схему через новый индекс и aliases, не смешивая версию данных с местом чтения приложения.