Анализ алгоритмов без нотации Big O напоминает разговор о скорости без единиц измерения: рассуждения есть, а сравнить нечего. Big O даёт общий язык, на котором инженеры описывают, как время работы или расход памяти растут вместе с размером входных данных. Поисковая индексация служит идеальным полигоном для этого языка, потому что здесь каждая ступень сложности, от линейного обхода до логарифмического спуска по дереву, имеет вполне физическое воплощение в структурах данных и стоимости запросов. Этот материал связывает абстрактные оценки с конкретной механикой индексов и показывает, почему поисковые системы устроены именно так, а не иначе.

Что на самом деле измеряет Big O

Нотация O(f(n)) фиксирует верхнюю границу роста ресурса при увеличении объёма входа n, отбрасывая константы и медленно растущие слагаемые. Алгоритм со сложностью O(n) удвоит время работы при удвоении данных, алгоритм O(n²) учетверит, а алгоритм O(log n) добавит к времени лишь один шаг. Константы отбрасываются не потому, что они неважны на практике, а потому, что они зависят от железа и реализации, тогда как форма роста принадлежит самому алгоритму.

Полезно помнить о смежных обозначениях. Омега описывает нижнюю границу лучшего случая, тета точную асимптотику, когда верх и низ совпадают. В инженерных разговорах почти всегда имеется в виду худший случай Big O, и это разумная перестраховка: индекс обязан отвечать за приемлемое время на любом запросе, а не только на удачном.

Типичная лестница выглядит так: O(1) константный доступ по хешу, O(log n) спуск по сбалансированному дереву, O(n) полный проход, O(n log n) хорошие сортировки, O(n²) и выше зона, куда крупные системы стараются не заглядывать. Разница между ступенями на малых данных едва заметна, но на миллиардах документов она превращается в разницу между миллисекундой и вечностью.

Линейный обход и пределы полного сканирования

Самый честный способ найти документ это прочитать всё подряд и сравнить с запросом. Полное сканирование имеет сложность O(n) по числу документов и работает без всякой подготовки, поэтому для каталога на сотню файлов ничего лучше и не придумать. Утилиты поиска по содержимому до сих пор так и поступают, когда индекс отсутствует.

Проблема в масштабе. При миллиарде документов линейный проход означает миллиард проверок на каждый запрос. Даже при фантастической скорости в миллиард проверок в секунду ответ займёт секунду, а системы общего назначения обслуживают десятки тысяч запросов в секунду. Полное сканирование остаётся уместным как нижний уровень для крошечных сегментов и как эталон корректности, но расти вместе с данными оно не способно.

Из этого тупика и рождается главная идея индексации: потратить ресурсы один раз при записи, чтобы каждый запрос потом обходился дёшево. Индекс это предвычисленная структура, меняющая сложность чтения с O(n) на что-то близкое к O(log n) или даже O(1), за счёт дополнительной памяти и более дорогой вставки. Торговля между записью и чтением это центральная сделка всей темы.

Инвертированный индекс как сердце поиска

Полнотекстовый поиск держится на инвертированном индексе. Вместо отображения от документа к словам строится отображение от слова к списку документов, где оно встречается. Словарь термов обычно хранится в структуре с логарифмическим поиском, а к каждому терму прилагается список постингов с идентификаторами документов и позициями.

Запрос разбирается на термы, по словарю выполняется логарифмический поиск, затем списки пересекаются. Пересечение отсортированных списков идёт за линейное время от суммы их длин, и именно длина редких термов определяет фактическую стоимость запроса. Слово, встречающееся в трёх документах, почти бесплатно, а стоп слово с миллиардом постингов это боль, которую движки лечат ранней отсечкой и эвристиками.

Экономия на длине списков достигается сжатием: разности идентификаторов кодируются переменной длиной, блоки пропусков позволяют перепрыгивать ненужные участки при пересечении. В результате типичный запрос обрабатывает не миллиарды записей, а несколько тысяч, и асимптотика на практике ощущается как почти константная.

Деревья, LSM и цена записи

Базы данных полагаются на B-деревья и их вариант B+ с данными в листьях. Сбалансированное дерево высотой O(log n) гарантирует логарифмический поиск, вставку и удаление, а широкое ветвление подгоняет узлы под страницы диска так, чтобы высота дерева над миллиардом ключей укладывалась в четыре уровня. Каждый уровень это одно обращение к диску, поэтому дерево проектируется вокруг стоимости ввода вывода, а не процессора.

Слабое место дерева это случайная запись: вставка в разные листья рассылает мелкие обращения по всему диску. LSM деревья отвечают на это переносом работы на последовательную запись. Изменения сначала ложатся в журнал и таблицу в памяти, затем сливаются на диск отсортированными блоками, а фоновое уплотнение поддерживает порядок. Поиск становится чуть дороже, потому что приходится заглядывать в несколько уровней, но запись дешевеет на порядки, и именно поэтому журнально структурированные хранилища доминируют там, где пишут много.

Выбор между B-деревом и LSM это выбор того, какую сторону асимптотики подмять под свою нагрузку. Читающая система предпочитает компактное дерево, пишущая выбирает журнал. Реальные движки нередко держат обе конструкции: LSM для свежих данных и преобразование в компактные сегменты для хранения.

Хеши, фильтры и границы константного времени

Хеш таблица обещает O(1) в среднем: вычислил хеш ключа, получил ячейку, готово. Платой служат отсутствие упорядоченности, деградация при коллизиях и дорогое расширение при росте. Для точечных запросов по ключу это непобедимый инструмент, а вот диапазонные запросы хеш не поддерживает вовсе, потому что соседние ключи разлетаются по случайным ячейкам.

Вероятностные структуры идут дальше и обменивают точность на память. Фильтр Блума отвечает на вопрос о наличии элемента за константное время, допуская ложные срабатывания, но никогда не ошибаясь в обратную сторону. Хранилища ставят такой фильтр перед каждым сегментом данных, и запрос к отсутствующему ключу отсекается без чтения диска. В LSM системах это превращает потенциально долгий поиск по уровням в пару обращений к памяти.

Константное время при этом не означает нулевое. При больших n в игру вступают промахи кэша процессора и сетевые задержки, и реальная константа растёт. Грамотные реализации поэтому мыслят не чистой асимптотикой, а полной стоимостью обращения на каждом уровне памяти.

Ранжирование и сложность на этапе выдачи

Найти документы мало, их нужно отсортировать по релевантности. Наивная полная сортировка миллиона совпадений стоила бы O(N log N), поэтому движки применяют выбор топ k через минимальную кучу за O(N log k), где N размер массива совпадений, а k размер выдачи, обычно десятки. Это один из красивых случаев, где асимптотика подгоняется под реальную потребность: никто не перелистывает миллион результатов, значит и сортировать его полностью незачем.

Дополнительное ускорение дают заранее упорядоченные списки постингов по статическому рангу, блоки максимальных оценок и каскадные схемы, где дешёвая модель отбирает тысячу кандидатов, а тяжёлая переранжирует сотню. Каждая ступень каскада заменяет дорогую операцию над большим множеством дешёвой операцией над малым, и суммарная сложность падает на порядки без заметной потери качества.

Векторный поиск добавил ещё один слой: приближённые соседи в графовых индексах наподобие HNSW достигают сублинейного времени с высокой точностью, жертвуя гарантией идеального результата. Это зрелое признание того факта, что за пределами малого масштаба точные методы неподъёмны, и разумная асимптотика важнее педантичной полноты.

Почему большие константы бьют красивую асимптотику

И напоследок стоит сказать о том, о чём нотация молчит умышленно. Квадратичный алгоритм с крошечной константой легко обгоняет линейный с тяжёлой на реальных размерах данных, и разработчики индексов выбирают структуры взвешивая не букву в скобках, а профиль обращений к памяти, сжатие и параллелизм. Дисковый блок в шестнадцать килобайт, считанный за один заход, стоит меньше десятка случайных обращений, и B+ дерево выигрывает у более изящных структур именно на этом.

Практическая мораль проста. Big O задаёт направление и отсекает заведомо проигрышные варианты, но окончательное решение рождается из измерений на целевом оборудовании. Индекс это компромисс, нарисованный асимптотикой и выверенный профилировщиком, и именно поэтому поисковые системы выглядят как пирог из деревьев, хешей, фильтров и каскадов, а не как один чистый алгоритм из учебника.

Индекс как организационное решение. Цена индексного подхода неизбежна: структура обновляется при каждой записи, занимает своё место и расходует вычислительную работу при сканировании изменений. Поэтому служба индексирования в Windows исторически вызывает зависть ресурсов - она работает тогда, когда ресурсы и её нужны. Разумный компакт формулируется так: фоновый ввод индексируется медленно и лениво, а ответ на активный запрос выдаётся мгновенно. Именно этой логике подчиняется USN-журнал и его потребители: если нужны имена, читают MFT напрямую - миллион записей обходится за секунды; если нужно содержимое, идут в инверсный индекс; если нужен вариант «найди всё, где встречается фраза, вне зависимости от формата», тогда уже специальные движки полнотекста со stemming - каждый выбирает под свой класс запроса.

Скрытая квадратичность как профессиональная травма. Самый распространённый провал бытовой производительности - не глупость алгоритма, а скрытая квадратичность в натуральном коде: сравнение каждого записи с каждой в процедурах дедупликации, конкатенация строк в цикле вместо StringBuilder, линейный поиск в ComboBox при наборе символа. Показательно, что все эти патологии полностью невидны на ста строках и нестерпимы на ста тысячах: на этом и живёт слава оптимизаторов, которые приходят «поднимать тормоза». Избавление от них обычно не требует новых структур данных - достаточно заменить линейное сканирование на карту или предварительную сортировку, и скорость возрастает не просто в десять раз, а меняет саму асимптотику. Поэтому разговор о производительности начинается не с языка или железа. а с чтения замера: есть ли в замере горба, который растёт быстрее размера выборки.

Что брать из этой науки в ежедневный обиход. Инженерная дисциплина Big O приводит к нескольким домашним максимам. Индексы оплачиваются сегодня, а экономят всегда; линейные уточнения допустимы на последнем шаге и недопустимы внутри циклов; хеш-таблица помогает там, где важно равенство, дерево там, где важен порядок; и прежде чем звать более быстрый компьютер, стоит посмотреть, не идёт ли речь о скрытой квадратичности в ещё невинно выглядящем хелпере. Тот, кто усвоил эту арифметику, перестаёт удивляться, когда «поиск мгновенный» или «проект тормозит»: он видит немый след асимптотики в обеих картинах.

Практическая этика замера. Обсуждая алгоритм, инженер протоколирует не результат сам по себе, а профиль роста: время при десяти тысячах, ста тысячах и миллионе должно расти предсказуемо по заявленной асимптотике. Любое отклонение от этой кривой делает следующую версию подозрительной, а её выпуск - авантюрой. Команды, которые поиграли с этой дисциплиной один раз, потом уже не возвращаются к отладке догадками, они оставляют в конвейере сборки автоматический тест на регрессию производительности - и обнаруживают скрытую квадратичность до того, как она доедет до продакшена.

Итог остаётся домашним: прежде чем ускорять железо, проверяют асимптотику. Сервер с несколькими миллионами записей вежливо умирает не от возраста, а от запроса, который перестал замечать собственную сложность, и хороший индекс в этом месте стоит массу новых процессоров.