Массовое индексирование — это построение или крупное обновление поискового индекса из большого пакета документов, записей, признаков или векторов. На выходе система получает структуры, по которым потом ищет: списки вхождений, словари терминов, сегменты, шарды, граф HNSW, списки IVF или другой индекс ближайших соседей.
Термин относится к инженерному режиму записи в индекс. Внутри поисковой системы это задача построения и публикации индекса.
Массовое индексирование отличается от точечной записи тем, что система обрабатывает много объектов за один проход и оптимизирует не один запрос add, а весь цикл: чтение источника, разбор, построение сегментов, слияния, публикацию, откат и проверку качества.
Место в поисковой системе
Контур:
- Источник отдает документы: база, очередь, фид, хранилище файлов, журнал изменений.
- Индексатор разбирает документ: извлекает поля, нормализует текст, применяет анализатор, считает признаки, назначает идентификатор.
- Массовое индексирование пишет документы в буфер, сбрасывает буфер в сегменты, строит словари и списки вхождений.
- Система публикует индекс: открывает новый читатель, переключает алиас, делает мягкую или жесткую фиксацию.
- Поисковый слой использует индекс для извлечения кандидатов.
- Ранжировщик получает кандидатов, признаки и статистики корпуса.
Вход:
- документы, записи, товары, страницы, объявления, профили, фрагменты текста;
- формат: JSON, HTML, XML, CSV, Parquet, PDF после извлечения текста, внутренний объект доменной модели;
- объем: от тысяч документов в локальном поиске до миллиардов записей в распределенном индексе;
- частота: разовый полный прогон, ночной батч, микробатчи, переиндексация после смены схемы.
Выход:
- сегменты инвертированного индекса;
- словари терминов;
- списки вхождений;
- doc values и хранилища полей;
- карта удалений;
- шардированный индекс;
- опубликованный читатель или новый алиас;
- служебные метрики: число документов, размер индекса, число сегментов, задержка публикации, долг по слияниям.
Что идет дальше:
- извлечение кандидатов по термам, полям, фильтрам или векторам;
- ранжирование BM25, LambdaMART, нейросетевым ранжировщиком или гибридной схемой;
- переупорядочивание;
- оценка качества;
- A/B-тест;
- мониторинг свежести и ошибок.
Место в рекомендательной системе
В рекомендациях массовое индексирование чаще строит индекс объектов для отбора кандидатов.
Контур:
- Система собирает события: просмотры, клики, покупки, прослушивания, пропуски, добавления в избранное.
- Модель строит векторные представления пользователей и объектов или считает разреженные признаки.
- Индексатор загружает объекты в индекс ближайших соседей или в разреженный поисковый индекс.
- Сервис кандидатов получает пользователя, его вектор или признаки и ищет близкие объекты.
- Скоринговая модель пересчитывает порядок.
- Последний слой применяет ограничения: свежесть, разнообразие, бизнес-правила, частотные лимиты.
Вход:
- идентификатор объекта;
- вектор объекта;
- текстовые поля;
- категориальные признаки;
- числовые признаки;
- ограничения для фильтрации: регион, наличие, возраст, язык, права доступа.
Выход:
- индекс для быстрого отбора кандидатов;
- карта объект → вектор;
- структура для фильтруемого поиска;
- статистики покрытия и свежести.
Здесь массовое индексирование ломает рекомендации иначе, чем текстовый поиск. В текстовом индексе основная проблема — число сегментов, слияния и статистики терминов. В векторном индексе — память, параметры графа, перекос плотности векторов, долг по построению графа и падение Recall@K.
Что именно строит индексатор
Для текстового поиска индексатор обычно строит несколько структур.
Словарь терминов
Словарь хранит уникальные термины после анализа текста:
red
shoes
sale
winter
jacketДля каждого термина система хранит указатель на список документов, где термин встретился.
Списки вхождений
Список вхождений связывает термин с документами:
sale -> [(doc=1, tf=1), (doc=6, tf=2)]
shoes -> [(doc=1, tf=1), (doc=3, tf=1), (doc=4, tf=1), (doc=6, tf=1)]В настоящем индексе список может хранить не только идентификатор документа и частоту, но и позиции терма, смещения, полезную нагрузку, норму поля, блоки пропусков и сжатое представление чисел.
Сегменты
Lucene-подобные системы не переписывают один большой индекс после каждого документа. Они пишут неизменяемые сегменты. Новый пакет документов дает новый сегмент. Потом фоновая политика слияний объединяет мелкие сегменты в крупные.
Это снижает цену записи, но переносит часть стоимости на чтение и обслуживание. Пока сегментов много, запросу надо обращаться к нескольким сегментам. Пока идут слияния, система переписывает данные и тратит диск, процессор и ввод-вывод.
Карта удалений
Удаление в сегментной архитектуре обычно не стирает документ сразу из всех файлов. Система помечает документ как удаленный. Физически он исчезает позже, когда сегмент попадет в слияние.
Из-за этого массовая переиндексация с большим числом обновлений может резко увеличить размер индекса и долг по слияниям.
Значения документов
Для сортировки, фильтров, фасетов и признаков ранжирования индекс хранит столбцовые значения документов. Они не заменяют списки вхождений. Это отдельная структура с отдельной ценой записи и чтения.
История и происхождение
У термина “массовое индексирование” нет одного академического определения. Каноническая задача называется построением индекса.
В классическом информационном поиске задача звучит так: есть корпус, который не помещается в память; надо построить инвертированный индекс. Базовые алгоритмы — блочная сортировка и однопроходное построение в памяти.
Блочная сортировка делит корпус на куски, строит пары:
(term_id, doc_id)потом сортирует их, пишет промежуточные блоки на диск и сливает блоки в итоговый индекс.
Однопроходный вариант строит словарь и списки вхождений сразу внутри блока. Он избегает части лишней сортировки и лучше подходит, когда словарь нельзя заранее зафиксировать.
В раннем Google индексатор уже решал ту же задачу в веб-масштабе. Документы разбирались в наборы вхождений, промежуточные структуры раскладывались по блокам, затем сортировщик перестраивал их в инвертированный индекс.
Следующий слой — распределенное индексирование. Модель по MapReduce:
map(document) -> (term, doc_id)
reduce(term, [doc_id...]) -> postings listЭто способ физически разложить построение индекса по машинам.
Потом появилась отдельная проблема: индекс надо не только строить с нуля, но и постоянно обновлять. Так появились динамические схемы: несколько уровней индекса, буферы, слияния, компромисс между дешевой записью и дорогим чтением.
Современные Lucene, Elasticsearch, OpenSearch и Solr превратили это в эксплуатационную механику: буфер записи, сегменты, слияния, обновление читателей, фиксация, реплики, шарды, алиасы и повторяемая переиндексация из исходного хранилища.
Алгоритмическая основа
Базовый алгоритм для инвертированного индекса
Вход:
- поток документов;
- схема полей;
- анализатор текста;
- лимит памяти;
- политика сброса буфера;
- политика слияний.
Алгоритм:
- Прочитать документ из источника.
- Проверить идентификатор и версию.
- Извлечь индексируемые поля.
- Применить анализатор: токенизация, нормализация, фильтры, стемминг или лемматизация, если они нужны.
- Построить локальные вхождения: термин → документ → частота и позиции.
- Добавить документ в буфер записи.
- При достижении лимита памяти сбросить буфер в новый сегмент.
- Запустить слияние сегментов по политике.
- Опубликовать новый читатель или переключить алиас.
- Проверить счетчики: документов принято, документов опубликовано, ошибок записи, задержка видимости.
Сложность:
- построение пар вхождений: O(T), где T — число токенов;
- сортировка в блочном варианте: O(T log T);
- слияние блоков: O(T) на один проход слияния;
- память: зависит от размера буфера, словаря блока и накопленных вхождений;
- диск: исходный корпус плюс промежуточные блоки плюс итоговый индекс плюс запас под слияния.
Где ломается:
- словарь блока не помещается в память;
- документы слишком большие;
- анализатор резко увеличивает число термов;
- много дублей;
- обновления создают большой слой удалений;
- слияния не успевают за записью;
- не хватает диска под временные файлы;
- шардов слишком много;
- публикация слишком частая;
- источник нельзя повторно прочитать при откате.
Сегментная модель
Сегмент — неизменяемая часть индекса. Запись нового документа не меняет старый сегмент. Она добавляет новый сегмент. Удаление создает маску удалений. Слияние берет несколько сегментов, выкидывает удаленные документы и пишет новый сегмент.
У этой модели жесткий компромисс:
мелкие батчи -> много сегментов -> дешевле свежесть -> дороже поиск и слияния
крупные батчи -> меньше сегментов -> выше пропускная способность -> хуже свежестьПоэтому массовое индексирование нельзя оценивать только по документам в секунду. Быстрый прогон может оставить после себя мусор: тысячи сегментов, тяжелые слияния, высокий хвост задержки поиска и большой расход диска.
Долг по слияниям
Долг по слияниям — объем работы, который система должна выполнить, чтобы привести набор сегментов к нормальному состоянию.
Формула для контроля:
write_amplification = bytes_written_by_merges / bytes_of_new_indexed_dataЕсли write_amplification растет, индексатор переписывает слишком много байтов на каждый байт новых данных.
Еще полезный счетчик:
segment_fanout(query) = число сегментов, к которым обратился запросПосле слияний segment_fanout падает. Список найденных документов может не измениться, но цена запроса станет ниже.
Публикация индекса
- Сброс буфера, фиксация на диск и видимость в поиске — разные события.
- Сброс буфера пишет новый сегмент.
- Мягкая публикация открывает новый читатель, чтобы поиск увидел изменения.
- Жесткая фиксация делает данные устойчивыми после сбоя.
- Переключение алиаса меняет активный индекс для чтения или записи.
Ошибка внедрения: команда меряет скорость записи в индекс, но не меряет задержку видимости. В логах все “записано”, а пользователь в поиске документ еще не видит.
Массовая переиндексация
Переиндексация — частный случай массового индексирования. Она нужна, когда старый индекс нельзя безопасно исправить точечными обновлениями.
Причины:
- изменилась схема полей;
- изменился анализатор;
- изменилась нормализация;
- добавили поле для сортировки или фасета;
- изменили способ построения векторов;
- надо заменить число шардов;
- надо убрать накопленные удаления;
- надо пересчитать признаки ранжирования;
- надо восстановить индекс после повреждения.
Правильная схема:
- Прочитать данные из исходного хранилища, а не из старого индекса.
- Создать новый индекс с новой схемой.
- Прогнать документы через новый индексатор.
- Проверить счетчики и выборочные запросы.
- Сравнить качество и покрытие.
- Переключить алиас чтения.
- Оставить старый индекс на время отката.
- Удалить старый индекс после окна наблюдения.
Индекс не должен быть единственным источником истины. Поисковый индекс — производная структура. Он сжимает, нормализует, отбрасывает часть исходных данных и хранит их в форме, удобной для поиска. Если исходного хранилища нет, переиндексация превращается в восстановление по поврежденной копии.
Массовое индексирование в векторном поиске
Для векторного поиска объектом индексирования становится не документ как текст, а вектор и его служебные поля.
IVF
IVF делит пространство векторов на группы. Сначала система обучает квантизатор на выборке. Потом каждый вектор попадает в один или несколько списков.
Поиск:
- Вектор запроса сравнивается с центрами групп.
- Система выбирает несколько ближайших групп.
- Внутри выбранных групп выполняет точный или приближенный поиск.
- Параметр
nprobeувеличивает число просматриваемых групп.
Компромисс:
больше nprobe -> выше Recall@K -> выше задержка
меньше nprobe -> ниже задержка -> больше пропусковМассовая загрузка IVF ломается, если обучающая выборка не похожа на реальные данные. Тогда списки получаются перекошенными: одни почти пустые, другие огромные. Поиск начинает сканировать слишком много кандидатов.
HNSW
HNSW строит граф близости. Каждый новый вектор подключается к соседям на нескольких уровнях. Параметр M задает число связей. Параметр ef_construction управляет тщательностью построения. Параметр ef влияет на поиск.
Компромисс:
больше M -> выше память -> лучше связность графа
больше ef_construction -> дольше построение -> выше качество графа
больше ef -> выше задержка поиска -> выше Recall@KВ массовом режиме HNSW часто упирается в память и время построения. Если векторов много, а параметры выбраны слишком агрессивно, индексатор забьет память раньше, чем сервис начнет отдавать стабильный поиск.
Отложенное построение векторного индекса
Некоторые векторные базы разрешают сначала загрузить точки без построения полноценного индекса, а потом включить построение. Это ускоряет прием данных, но создает долг.
Пока долг не закрыт:
- часть данных ищется полным сканированием;
- задержка растет;
- Recall@K может отличаться от целевого;
- память занята не только индексом, но и промежуточными сегментами;
- состояние коллекции нельзя считать готовым к нагрузке.
Минимальный пример
Есть шесть документов:
1: red shoes sale
2: winter jacket blue
3: red shoes size guide
4: running shoes red
5: camera lens adapter
6: red shoes sale saleИндексатор принимает документы батчами по два.
После первого батча:
S0 = docs 1, 2После второго:
S1 = docs 3, 4После третьего:
S2 = docs 5, 6Запрос:
sale shoesДо слияния:
sale -> S0: doc 1, S2: doc 6
shoes -> S0: doc 1, S1: doc 3, doc 4, S2: doc 6Запрос читает три сегмента. После слияния все списки вхождений лежат в одном сегменте. Найденные документы те же, но служебная работа меньше.
Это главный смысл сегментной архитектуры: массовая запись дешева, потому что система пишет новые сегменты. Потом она платит за слияние.
Пример на Python
Что проверяем кодом
Код показывает механику сегментов. Батчи создают несколько сегментов. Запрос читает все сегменты. Полное слияние уменьшает число обращений к сегментам, но не меняет найденные документы.
Установка
python --versionВнешние библиотеки не нужны.
Код
from __future__ import annotations
from collections import Counter, defaultdict
from dataclasses import dataclass
import re
from typing import Dict, List, Tuple
TOKEN_RE = re.compile(r"\w+", re.UNICODE)
def tokenize(text: str) -> List[str]:
return TOKEN_RE.findall(text.lower())
@dataclass(frozen=True)
class Document:
doc_id: int
text: str
class Segment:
"""
Неизменяемый сегмент.
postings[term] -> {doc_id: term_frequency}
"""
def __init__(self, docs: List[Document]) -> None:
self.docs = {doc.doc_id: doc for doc in docs}
self.postings: Dict[str, Dict[int, int]] = defaultdict(dict)
for doc in docs:
frequencies = Counter(tokenize(doc.text))
for term, frequency in frequencies.items():
self.postings[term][doc.doc_id] = frequency
class BulkIndex:
"""
Минимальная модель массового индексирования:
- документы приходят батчами;
- каждый сброс буфера пишет новый сегмент;
- поиск читает все сегменты;
- слияние переписывает сегменты в один.
"""
def __init__(self, batch_size: int = 2) -> None:
if batch_size <= 0:
raise ValueError("batch_size must be positive")
self.batch_size = batch_size
self.buffer: List[Document] = []
self.segments: List[Segment] = []
self.source: Dict[int, str] = {}
def add(self, doc: Document) -> None:
if doc.doc_id in self.source:
raise ValueError(f"duplicate document id: {doc.doc_id}")
self.source[doc.doc_id] = doc.text
self.buffer.append(doc)
if len(self.buffer) >= self.batch_size:
self.flush()
def flush(self) -> None:
if not self.buffer:
return
self.segments.append(Segment(self.buffer))
self.buffer = []
def merge_all(self) -> None:
self.flush()
docs = [
Document(doc_id=doc_id, text=text)
for doc_id, text in sorted(self.source.items())
]
self.segments = [Segment(docs)]
def search(self, query: str) -> Tuple[List[Tuple[int, int]], Dict[str, int]]:
self.flush()
scores: Dict[int, int] = defaultdict(int)
segment_hits = 0
postings_read = 0
for term in tokenize(query):
for segment in self.segments:
postings = segment.postings.get(term)
if not postings:
continue
segment_hits += 1
postings_read += len(postings)
for doc_id, frequency in postings.items():
scores[doc_id] += frequency
ranked = sorted(scores.items(), key=lambda x: (-x[1], x[0]))
stats = {
"segments": len(self.segments),
"segment_hits": segment_hits,
"postings_read": postings_read,
"matched_documents": len(scores),
}
return ranked, stats
def main() -> None:
docs = [
Document(1, "red shoes sale"),
Document(2, "winter jacket blue"),
Document(3, "red shoes size guide"),
Document(4, "running shoes red"),
Document(5, "camera lens adapter"),
Document(6, "red shoes sale sale"),
]
index = BulkIndex(batch_size=2)
for doc in docs:
index.add(doc)
before_results, before_stats = index.search("sale shoes")
print("BEFORE MERGE")
print("results:", before_results)
print("stats:", before_stats)
print()
index.merge_all()
after_results, after_stats = index.search("sale shoes")
print("AFTER MERGE")
print("results:", after_results)
print("stats:", after_stats)
if __name__ == "__main__":
main()Вывод
BEFORE MERGE
results: [(6, 3), (1, 2), (3, 1), (4, 1)]
stats: {'segments': 3, 'segment_hits': 5, 'postings_read': 6, 'matched_documents': 4}
AFTER MERGE
results: [(6, 3), (1, 2), (3, 1), (4, 1)]
stats: {'segments': 1, 'segment_hits': 2, 'postings_read': 6, 'matched_documents': 4}Ограничения примера
В коде нет:
- сжатия списков вхождений;
- позиций термов;
- удалений;
- обновлений;
- карты удаленных документов;
- хранения полей;
- doc values;
- шардов;
- реплик;
- сбоя и восстановления;
- параллельной записи;
- фоновых слияний;
- статистик BM25;
- анализаторов с языковой нормализацией;
- векторного индекса.
Код показывает только механику сегментов.
Метрики массового индексирования
Пропускная способность
documents_per_second
tokens_per_second
bytes_per_second
vectors_per_secondЭти метрики отвечают только на вопрос: как быстро система принимает вход. Они не говорят, когда данные станут видимы поиску и сколько долга останется после загрузки.
Задержка видимости
visibility_lag = first_searchable_at - accepted_atДокумент может быть принят индексатором, но не виден в поиске. Для пользователя и ранжировщика важна не запись в журнал, а момент, когда новый документ участвует в извлечении кандидатов.
Покрытие
coverage = indexed_objects / expected_objectsПокрытие надо считать по исходному хранилищу, а не по числу успешно обработанных сообщений. Иначе система может “успешно” проиндексировать все, что до нее дошло, но потерять часть данных на входе.
Ошибки разбора
parse_error_rate = failed_parse_documents / input_documentsОсобенно критично для HTML, PDF, XML и данных из внешних фидов. Ошибка разбора часто выглядит как “документ есть”, но в индексе нет нужного текста.
Долг по слияниям
merge_backlog_bytes
merge_time_seconds
segments_per_shard
write_amplificationЭти метрики показывают, закончилась ли работа на самом деле. Массовая загрузка может завершиться, а индекс еще часами будет сливать сегменты.
Размер индекса
index_size_bytes
index_to_source_ratio = index_size_bytes / source_size_bytesРезкий рост отношения индекса к источнику часто означает одно из четырех:
- много удалений;
- неправильно настроены поля;
- слишком много сохраненных значений;
- анализатор генерирует лишние термы.
Метрики качества поиска
Если массовое индексирование меняет анализатор, схему, поля или векторные представления, надо считать качество.
Для текстового поиска:
NDCG@K
MAP
MRR
Recall@K
Precision@KДля отбора кандидатов:
candidate_recall@K = relevant_candidates_found@K / relevant_candidates_totalДля векторного индекса:
ann_recall@K = |approximate_topK ∩ exact_topK| / KНельзя выкатывать новый индекс только потому, что он построился без ошибок. Если изменилась обработка текста или векторов, надо сравнивать выдачу.
Типовые ошибки внедрения
1. Мерить только скорость записи
docs/s может расти, пока система копит долг по слияниям. Потом поиск начинает тормозить, потому что запросы ходят по большому числу сегментов.
Что делать:
- считать число сегментов на шард;
- считать долг по слияниям;
- смотреть хвосты задержки поиска во время загрузки;
- мерить задержку видимости;
- не считать загрузку завершенной, пока индекс не пришел в рабочее состояние.
2. Слишком часто публиковать изменения
Частая публикация создает мелкие сегменты. Мелкие сегменты увеличивают стоимость поиска и слияний.
Что делать:
- разделить прием данных и видимость в поиске;
- для первичной загрузки временно увеличить интервал публикации;
- после загрузки выполнить контролируемую публикацию;
- проверять свежесть отдельной метрикой.
3. Ставить слишком маленький размер батча
Маленький батч снижает задержку отдельной записи, но ломает массовую загрузку: больше запросов, больше накладных расходов, больше сегментов.
Что делать:
- подбирать размер батча экспериментом;
- искать плато пропускной способности;
- смотреть не среднее время запроса, а полный цикл: запись, слияние, публикация;
- отдельно тестировать ошибки и повторные попытки.
4. Убить диск слияниями
Слияния переписывают данные. Для крупных индексов нужен запас диска. Если места не хватит, индексатор может остановиться в худший момент: новый индекс не готов, старый уже под нагрузкой, откат дорогой.
Что делать:
- считать временный расход диска;
- держать запас под слияния;
- не запускать принудительное слияние без расчета;
- не выполнять массовую переиндексацию на кластере без свободной емкости.
5. Путать переиндексацию с копированием документов
Копирование документов в новый индекс не переносит автоматически схему, анализаторы, число шардов, настройки реплик и политику публикации.
Что делать:
- создавать новый индекс с явной схемой;
- хранить схему в коде или миграциях;
- строить индекс из исходного хранилища;
- проверять настройки до загрузки;
- держать старый индекс для отката.
6. Использовать старый индекс как источник истины
Если система переиндексирует из старого индекса, она наследует его потери: пропущенные поля, старую нормализацию, ошибки разбора, удаленные исходные данные.
Что делать:
- источник истины должен быть вне поискового индекса;
- переиндексация должна быть повторяемой;
- входные данные должны иметь версии;
- индексатор должен уметь восстановиться после сбоя.
7. Игнорировать дубли
Дубли портят статистики терминов, раздувают индекс и ухудшают выдачу. В рекомендациях дубли объектов портят частотные признаки и могут забить кандидатов однотипными результатами.
Что делать:
- считать долю дублей до индексирования;
- выделять канонический объект;
- хранить причину склейки;
- не смешивать технический идентификатор документа и канонический идентификатор сущности.
8. Менять анализатор без оценки качества
Смена токенизации, регистра, стемминга, синонимов или нормализации меняет списки вхождений. После этого BM25 и ранжировщик получают другой набор кандидатов.
Что делать:
- прогонять контрольный набор запросов;
- считать Recall@K и NDCG@K;
- сравнивать распределения длин документов;
- сравнивать частоты терминов;
- проверять запросы с брендами, артикулами, числами и редкими словами.
9. Ошибиться с шардированием
Слишком много мелких шардов создают лишнюю нагрузку на память и процессор. Слишком мало крупных шардов усложняют восстановление и миграции.
Что делать:
- тестировать на реальном профиле запросов;
- считать размер шарда после загрузки;
- смотреть число сегментов внутри шарда;
- проверять время восстановления;
- не выбирать число шардов “на всякий случай”.
10. Не проверять права доступа
Если права доступа индексируются как поля, массовая загрузка должна обновлять их консистентно с документами. Иначе поиск вернет документ пользователю, который не должен его видеть, или скроет документ от того, кто имеет доступ.
Что делать:
- индексировать версию ACL;
- проверять контрольные запросы по ролям;
- логировать причину фильтрации;
- иметь аварийный способ скрыть класс документов.
Отказовые случаи
Индекс построился, но поиск пустой
Частые причины:
- документы ушли не в тот индекс или алиас;
- читатель не обновлен;
- поле не индексируется;
- анализатор удалил все токены;
- запрос ищет по другому полю;
- фильтр прав доступа отрезает все документы.
Индекс построился, но качество упало
Частые причины:
- изменился анализатор;
- изменились веса полей;
- пропали поля ранжирования;
- сломалась нормализация языка;
- дубли изменили статистики;
- часть корпуса не попала в индекс;
- новый векторный индекс имеет низкий Recall@K.
Индексатор быстрый, но поиск тормозит
Частые причины:
- слишком много мелких сегментов;
- слияния конкурируют с запросами за ввод-вывод;
- шардов слишком много;
- кэш постоянно сбрасывается;
- векторный индекс еще не построен;
- фильтры не используют подходящую структуру.
Переиндексация не откатывается
Частые причины:
- старый индекс удалили слишком рано;
- алиас переключили без версии;
- новый индекс строили из старого, а не из источника;
- не сохранили схему старого индекса;
- нет контрольных счетчиков;
- нет списка документов, которые должны быть в индексе.
Проверки перед запуском
Минимальный список:
- входной счетчик документов из источника;
- счетчик принятых документов;
- счетчик опубликованных документов;
- список ошибок разбора;
- доля пустых документов;
- доля дублей;
- распределение длины документов;
- число уникальных терминов;
- размер индекса;
- число сегментов;
- долг по слияниям;
- задержка видимости;
- контрольные запросы;
- сравнение качества со старым индексом;
- план отката;
- запас диска;
- версия схемы;
- версия анализатора;
- версия модели векторов.
Проверки после запуска
После переключения надо смотреть не только ошибки индексатора.
Нужно проверить:
- хвосты задержки поиска;
- рост CPU на поисковых узлах;
- рост ввода-вывода;
- число сегментов;
- размер кэшей;
- долю пустых выдач;
- изменение CTR по классам запросов;
- Recall@K на контрольном наборе;
- NDCG@K на размеченном наборе;
- долю документов без обязательных полей;
- ошибки прав доступа;
- расхождение числа документов с исходным хранилищем.
Связь с техническим SEO
Для сайта “массовое индексирование” часто называют массовым попаданием страниц в индекс поисковика. Это другая задача.
Внутри Google или Яндекса страница проходит:
- обнаружение URL;
- обход;
- загрузку ресурсов;
- рендеринг, если он нужен;
- извлечение текста и ссылок;
- выбор канонического URL;
- проверку запретов;
- решение о включении в индекс;
- участие в ранжировании.
Sitemap не гарантирует индексацию. Он только сообщает поисковику список URL и метаданные. robots.txt не удаляет URL из индекса. Он управляет обходом. Для удаления из выдачи нужны другие механизмы: noindex, HTTP-заголовок, корректный статус или удаление URL через инструменты поисковика.
Технический SEO здесь должен думать не “как отправить много URL”, а как не сломать индексируемость корпуса:
- убрать дубли;
- стабилизировать canonical;
- не закрыть нужные ресурсы от рендеринга;
- не генерировать бесконечные параметрические URL;
- не отдавать пустой HTML для страниц, зависящих от JavaScript;
- не смешивать языковые версии;
- не индексировать внутренний поиск и мусорные фильтры;
- контролировать логи обхода;
- сравнивать число известных URL, обойденных URL, индексируемых URL и страниц в выдаче.
Как это тестировать
Тест пропускной способности
Берется копия реального корпуса. Индексатор прогоняет данные с разными размерами батча.
Смотрим:
docs/s
MB/s
CPU
disk write MB/s
merge backlog
segments per shard
visibility lag
429/503/error rateЦель — найти размер батча, после которого пропускная способность почти не растет, а ошибки и долг по слияниям уже растут.
Тест качества
Берется контрольный набор запросов и оценок релевантности.
Сравниваем старый и новый индекс:
Recall@100
NDCG@10
MRR@10
доля пустых выдач
доля запросов с изменением top-10Если новый индекс меняет кандидатов, нельзя смотреть только финальный NDCG. Надо отдельно считать Recall@K на этапе извлечения кандидатов. Ранжировщик не вернет документ, которого нет среди кандидатов.
Тест свежести
Подаются документы с известными временными метками.
Система измеряет:
accepted_at
flushed_at
published_at
first_searchable_atИтоговая метрика:
P50/P95/P99 visibility_lagСреднее значение бесполезно, если P99 уезжает на минуты или часы.
Тест отката
До запуска надо проверить, что старый индекс можно вернуть.
Минимум:
- алиас чтения можно переключить обратно;
- старый индекс не удален;
- схема старого индекса сохранена;
- контрольные запросы работают;
- данные после переключения не потеряны;
- новая запись не ушла только в новый индекс без плана синхронизации.
Где термин ломается
Термин “массовое индексирование” часто используют слишком широко. В одном разговоре им называют:
- построение инвертированного индекса;
- массовую загрузку документов через API;
- переиндексацию после смены схемы;
- попадание страниц сайта в Google;
- построение векторного индекса;
- пересчет признаков рекомендаций;
- публикацию новой версии индекса.
Это разные операции. У них разные метрики и разные причины отказа.
Правильный вопрос не “как ускорить массовое индексирование”, а:
какой объект индексируем?
какую структуру строим?
что считается успешной публикацией?
какая допустимая задержка видимости?
какой источник истины?
как откатываемся?
какая метрика качества не должна упасть?
какой ресурс является ограничением: память, диск, процессор, сеть, ввод-вывод?Без этих ответов обсуждение быстро превращается в набор советов уровня “увеличьте батч” и “отключите refresh”. Иногда это правильно. Иногда это ломает свежесть, откат или качество.
Подытожим
Массовое индексирование — это не отправка большого числа документов в поисковую систему. Это полный цикл построения поисковой структуры: разбор, запись, сегменты, слияния, публикация, контроль качества и откат.
В текстовом поиске главный риск — накопить сегменты, удаления и долг по слияниям.
В векторном поиске главный риск — построить индекс, который быстро загружается, но дает низкий Recall@K или слишком дорогой поиск.
В техническом SEO главный риск — считать отправку URL в sitemap индексацией. Поисковик может обойти URL, но не включить его в индекс, выбрать другой канонический адрес или не увидеть содержимое после рендеринга.
Нормальная эксплуатационная метрика массового индексирования выглядит не так:
мы загрузили 50 млн документовА так:
50 млн документов прочитаны из источника
49.98 млн опубликованы в индексе
0.02 млн отклонены с известными причинами
P95 задержки видимости — 4 минуты
сегментов на шард — меньше 80
долг по слияниям закрыт
NDCG@10 не упал
Recall@100 не упал
старый индекс доступен для отката