Photon: создание движка поиска и ранжирования с нуля
Как специализированные форматы данных, пакетное чтение и отделение построения индекса от обслуживания запросов улучшают поиск на базе ИИ.
Введение
ИИ-нативный поисковый стек Perplexity извлекает и ранжирует страницы-кандидаты, выводя наиболее релевантный контент для обоснования ответов моделью. Первоначально этот этап работал на открытом движке, который мы адаптировали под нашу систему. Однако по мере роста нашего индекса и рабочих нагрузок мы уперлись в жесткие ограничения по стоимости, хвосту задержек и времени восстановления узлов, которые не могли разрешить в рамках существующей сторонней платформы.
Чтобы преодолеть эти трудности, мы создали Photon — новый собственный движок поиска и ранжирования. Это продолжает наш подход к созданию ИИ-ориентированной поисковой инфраструктуры с нуля, как мы это сделали с CobbleDB, нашим распределенным хранилищем «ключ — значение» для обслуживания записей страниц, и нашим Search API.
Photon использует компактные форматы данных для чтения и декодирования только того, что требуется для каждого запроса, с кэшированием с учетом пакетов и асинхронным чтением для сокращения обращений к диску и перекрытия ожиданий. Построение индекса выполняется отдельно от обслуживания запросов: оно обновляет по одной группе обслуживания за раз и прогревает ее кэши перед тем, как направлять в нее живые запросы.
Photon теперь управляет нашим конвейером поиска, а также служит основой для нового быстрого пресета для нашего Search API с задержкой одного поискового вызова 160 мс при p50 и 230 мс при p95. На шести публичных бенчмарках быстрый пресет снизил оценочные затраты на модель плюс поиск на задачу на 68%, сохранив при этом совокупное качество задач, сопоставимое с пресетом по умолчанию. Мы выпускаем его сегодня, чтобы разработчики получили доступ к этим улучшениям.
Требования к движку поиска и ранжирования
Для каждого запроса движок поиска и ранжирования ищет по большому распределенному индексу, чтобы выбрать и упорядочить небольшой набор релевантных страниц в рамках лимита задержки поискового стека. Система также должна поддерживать индекс в актуальном состоянии по мере поступления новых страниц и изменения существующих, не позволяя этой работе замедлять живые запросы.
Мы оценили несколько вариантов с открытым исходным кодом и выбрали зрелый универсальный движок поиска и ранжирования, отвечающий нашим первоначальным потребностям. Затем мы адаптировали его под нашу архитектуру и рабочие нагрузки, сохранив стабильную задержку и пропускную способность в масштабах продакшна.
В чем предыдущая система не дотягивала до требований
Мы поддерживали собственный форк для адаптации движка под наш растущий индекс, но устранение ключевых ограничений все сильнее требовало редизайна больших частей платформы. Несколько ограничений оказалось трудно преодолеть без фундаментальных архитектурных изменений:
1. Затраты на масштабирование. Со временем растет как количество хранимых нами документов, так и объем данных для каждого документа, которые нам нужно извлекать и ранжировать. Такое масштабирование создавало нагрузку на наше железо, особенно на объем доступной оперативной памяти. Для контроля затрат нам потребовались более компактные представления данных и более точный контроль над тем, что остается в памяти, а что считывается с диска по требованию.
2. Плохой хвост задержек. На продакшн-трафике время отклика p99 старой системы составляло около 800 мс. Набор данных превышал доступный объем ОЗУ, поэтому мы не могли использовать mlock, чтобы удерживать его целиком в памяти. Обращение к данным вне памяти могло вызывать серьезные страничные прерывания (page faults), останавливая выполнение запросов, пока операционная система производила чтение с диска.
Движок также периодически объединял свой основной дисковый индекс с меньшими индексами, сбрасываемыми из памяти. Во время этого процесса слияния дискового индекса количество страничных прерываний возрастало еще сильнее, а задержка p99 достигала примерно 1,2 с на 10–15 минут. По мере прогрева кэшей частота страничных прерываний и задержка уменьшались, что указывало на то, что холодное чтение было основным источником всплесков. Предварительная загрузка данных и прогрев индекса уменьшили всплески, но не смогли должным образом решить проблему высокого хвоста задержек.
3. Долгое восстановление и синхронизация. В предыдущей системе развертывание дополнительного кластера и синхронизация его данных могли занимать больше недели. Синхронизация также требовалась при добавлении групп узлов для хранения дополнительных копий данных или при восстановлении после сбоев узлов. Во время восстановления большая доля поисковых ответов оказывалась частичной, что снижало показатели качества финального ответа.
В конечном итоге мы пришли к выводу, что проще и дешевле создать систему с нуля, чем продолжать адаптировать и поддерживать наш форк. Опираясь на опыт эксплуатации предыдущей системы, небольшая команда инженеров совместно с роем постоянных кодинг-агентов разработала Photon.
Место Photon в стеке поиска
Photon разработан для двух основных рабочих процессов: обслуживания поисковых запросов и приема обновлений из конвейера обработки.
Запрос от высокоуровневой службы поиска попадает к брокеру Photon через балансировщик нагрузки. Брокер выбирает группу шардов и пересылает запрос каждому шарду, отслеживая тайм-ауты и отправляя дополнительные запросы при необходимости.
Каждый шард выполняет поиск, первоначальное ранжирование и ранжирование второго этапа, а затем возвращает лучших кандидатов брокеру. Брокер объединяет этих кандидатов в единый отранжированный список и извлекает ключевые поля документов для выбранных страниц из соответствующих шардов. Затем он возвращает эти результаты в высокоуровневую службу поиска, которая собирает содержимое страниц, необходимое агенту для генерации обоснованного ответа.
Обновления индекса идут по отдельному пути. Pillar, наш слой публикации, подготавливает данные и сохраняет их в таблицах YTsaurus. Индексаторы преобразуют эти таблицы в структуры индекса, которые шарды могут загружать для поиска. Контроллер координирует подготовку данных, построение индекса и развертывание новых версий индекса в группах обслуживания. Перед обработкой живого трафика каждый шард проигрывает реальные поисковые запросы для прогрева своих кэшей.
Проектирование Photon под наши рабочие процессы
Три проектных решения делают Photon хорошо подходящим для нашей рабочей нагрузки: компактные представления данных, кэширование и асинхронное дисковое чтение, а также отделение обновлений индекса от обслуживания запросов.
Представления данных для поиска и ранжирования
Поиск и ранжирование обращаются к данным документов по-разному. Лексический поиск начинается с терминов запроса и находит содержащие их документы. Затем ранжированию требуется подробная информация о каждом выбранном документе, включая частоты терминов и позиции.
Photon использует два взаимодополняющих представления для поддержки этих шаблонов доступа. Инвертированный индекс хранит список совпадающих документов для каждого термина, называемый списком публикаций, для поддержки отбора кандидатов. Отдельные записи для каждого документа предоставляют данные, необходимые для ранжирования. Мы также оптимизируем хранение и обход списков публикаций для улучшения отбора в рамках фиксированного бюджета работы.
Специализированные представления данных позволяют более эффективно хранить ключевые данные. Более компактный инвертированный индекс уменьшает объем данных, читаемых или перемещаемых на один запрос, улучшая задержку за счет исключения ненужных операций ввода-вывода. Эти форматы также позволяют проверять наличие документа в списке без его полного декодирования — это свойство используется нашей логикой обхода для контроля объема работы запроса в рамках фиксированного бюджета. Оптимизация записей для каждого документа позволяет хранить больше данных на документ для ранжирования, а также снижает затраты на масштабирование, связанные с нашей предыдущей системой.
Инвертированный индекс
Для каждого термина инвертированный индекс хранит список публикаций. Эти списки могут содержать как горстку документов, так и миллионы, поэтому ни один единственный формат не подходит для каждого списка. Photon выбирает формат на основе количества документов, содержащих термин.
Короткие списки используют встроенное представление, которое помещается в пределах одной страницы и не требует табличной таблицы блоков. Более длинные списки, не помещающиеся во встроенный формат, делятся на блоки, охватывающие фиксированные диапазоны идентификаторов документов. Представление каждого блока зависит от его плотности: разреженные блоки хранят отсортированные массивы смещений ID документов, а плотные блоки используют битовые карты. В разреженных блоках применяется быстрый поиск (galloping search), который продвигается с возрастающими шагами для пропуска областей списка. Плотные блоки позволяют выполнять проверки принадлежности путем поиска бита, соответствующего документу.
Частота запросов термина определяет расположение его списка публикаций на диске. Часто запрашиваемые списки хранятся рядом друг с другом, концентрируя горячие данные в меньшей области хранения. Внутри каждого списка документы упорядочены по отдельному сигналу полезности, чтобы более перспективные кандидаты встречались раньше.
Обход списков в рамках бюджета работы
Отбор кандидатов требует обхода списков публикаций для поиска потенциально релевантных документов. Исчерпывающее сканирование каждого списка обходится дорого, поэтому Photon использует алгоритм WAND-подобного типа, который обходит лишь некоторые списки, при этом учитывая совпадения в остальных.
Алгоритм делит списки на две группы. Управляющие списки продвигают обход и предлагают документы-кандидаты. Списки проверки вносят свой вклад в оценки этих кандидатов, но сами по себе кандидатов не генерируют.
Оценка кандидатов объединяет взвешенное покрытие терминов запроса и частотную составляющую с нормализацией по длине документа и ограничением прироста от повторяющихся терминов. Photon сначала проверяет наличие терминов в массивах и битовых картах, чтобы ограничить максимально возможный очковый балл каждого кандидата. Он считывает точные частоты терминов только для тех кандидатов, которые все еще могут преодолеть порог отбора.
Отбор затем происходит в три шага:
- Обход: Начните со стробого требования к покрытию запроса. Если найдено слишком мало кандидатов, повторите с ослабленным требованием.
- Удержание: Кандидаты удерживаются в ограниченном наборе с использованием оценки, объединяющей покрытие и частоту. По мере улучшения собранных топ-кандидатов порог допуска повышается.
- Финальный отбор: Окончательные результаты для прохода выбираются из сохраненного набора на основе частотных оценок. Каждое поле выполняет собственный проход, а результаты объединяются с использованием весов полей.
Алгоритм формирует набор релевантных документов-кандидатов для использования в дальнейшем ранжировании.
Docblob
После выбора страниц-кандидатов фазе ранжирования требуются частоты терминов, покрытие полей, а также расположения и близость вхождений. Получение этих данных через инвертированный индекс потребовало бы рассредоточенных обращений ко многим парам «термин — документ». В Photon мы храним компактную запись для каждого документа под названием docblob, которая содержит частоты терминов, поля, в которых появился термин, и его местонахождение в документе. В результате ранжирование кандидата требует лишь одного обращения на документ.
Формат создан таким образом, чтобы избегать избыточного декодирования. Внутри docblob поля группируются по текстовому анализатору. Каждая группа хранит отсортированный список терминов, закодированный с помощью метода Элиаса — Фано, поэтому термин запроса можно искать напрямую без предварительного развертывания всего списка. Маска полей отмечает места вхождения термина, счетчик частоты отслеживает их количество, а фактические позиции хранятся в отдельных блоках для каждой пары «термин — поле» в виде разностей позиций. Декодируются только блоки тех терминов, которые действительно совпали, в то время как все остальное в записи остается в сжатом формате.
Такое выборочное декодирование позволяет docblob поддерживать хранение большего объема данных на документ без пропорционального роста затрат. Увеличение размера записи не приводит к увеличению объема работы на один поиск, поскольку ранжирование всегда затрагивает только те части, которые относятся к совпавшим терминам, а не всю запись целиком.
Пакетное чтение и работа с памятью
Даже с более эффективными представлениями данных ОЗУ все равно заканчивается раньше остальных ресурсов, поэтому нам часто требуется доступ к данным, хранящимся на диске. Photon использует асинхронное дисковое чтение и кэш, оптимизированный для пакетного чтения, чтобы ускорить эти обращения, сокращая время ожидания на один запрос. Это вносит вклад в значительное улучшение хвоста задержек по сравнению с предыдущей системой.
Пересечение дисковых ожиданий
Ранжирование требует получения признаков сотни документов, расположенных в разных частях файла. Наша предыдущая система использовала mmap для отображения файла в виртуальное адресное пространство, чтобы его можно было читать через указатели памяти. Однако обращение к странице, отсутствующей в памяти, по-прежнему требовало чтения с диска. Тред был вынужден ждать данные перед продолжением вычислений, и это время ожидания накапливалось, когда процессу требовалось читать холодные записи последовательно.
Photon использует тот факт, что смещения записей известны заранее, поэтому мы можем передавать несколько операций чтения одновременно. Это управляется реактором: выделенные рабочие треды отправляют пакеты через io_uring и передают прочитанные данные вызывающему коду. Каждый тред поддерживает собственный io_uring; пакеты отправляются в наименее загруженный реактор. Предварительно зарегистрированные дескрипторы файлов и пулы буферов снижают накладные расходы на повторяющиеся операции чтения. Такое асинхронное выполнение позволяет совмещать ожидание нескольких операций по времени, снижая хвост задержек.
Сокращение количества обращений к дискам
Перед отправкой операций чтения Photon проверяет весь пакет в кэше оперативной памяти. На диск отправляются только запросы для отсутствующих записей; прочитанные данные затем заполняют кэш.
Кэш спроектирован для пакетного чтения. А именно:
- При попадании не происходит отдельного выделения памяти. Вместо этого данные копируются в буфер, предварительно подготовленный вызывающим кодом.
- Читатель не захватывает блокировки. Вместо этого он проверяет версию данных и повторяет попытку в случае конфликта с записью.
- В нем используется вытеснение CLOCK вместо необходимости обновлять общий список LRU при каждом чтении.
- В нем используется упреждающая выборка в кэш CPU, при которой метаданные и данные для будущих записей запрашиваются заранее.
Эти оптимизации снижают задержку без ущерба для производительности на интересующих нас рабочих нагрузках.
Построение и обновление индекса отдельно от обслуживания
В Photon построение индекса и обслуживание запросов выполняются на разных узлах. Индексаторы преобразуют данные в готовые структуры хранения, в то время как узлы обслуживания читают их и выполняют поиск и ранжирование. В результате построение индекса не конкурирует с поисковыми запросами за CPU, а ресурсы для этих задач могут масштабироваться независимо.
Это разделение устраняет узкие места восстановления и синхронизации, присутствовавшие в предыдущей реализации. Поскольку построение индекса происходило внутри того же кластера, который обслуживал запросы, восстановление вышедшего из строя узла или добавление новой копии данных могло означать переиндексацию документов на месте — процесс, занимавший дни и увеличивавший количество частичных ответов во время восстановления. Photon убирает эту связь: индексаторы работают как распределенные задачи, которые создают версионированные, готовые к обслуживанию структуры индексов независимо от узлов обслуживания, поэтому сборка следующей версии индекса больше не конкурирует с живым трафиком.
Подготовка и распределенная сборка
Построение начинается с выбора документов для активного индекса и создания исходного экспорта. Контроллер записывает исходную версию, входные таблицы и ожидаемое количество шардов. MapReduce-задачи в YTsaurus подготавливают текст, метаданные, фрагменты, признаки и предварительно вычисленные эмбеддинги в версионированные таблицы, разделенные по шардам. Каждый индексатор стримит подготовленную таблицу, строит структуры для своего шарда и объединяет текстовые сегменты. Готовый индекс вместе с описанием его компонентов выгружается в объектное хранилище.
Количество шардов и число одновременно работающих индексаторов настраиваются независимо, поэтому сборка может выполняться в несколько волн, либо можно выделить больше машин для параллельной работы. Распределенная подготовка и параллельные индексаторы позволяют собрать весь веб-индекс всего за несколько часов, что значительно превосходит предыдущую систему.
Развертывание новой версии
Перед развертыванием контроллер проверяет готовность нового индекса. Если данные неполные, развертывание автоматически блокируется.
При наличии нескольких групп обслуживания контроллер обновляет их по одной за раз. Сначала он удаляет группу из маршрутизации брокера и переводит ее на новую версию индекса. Перед обслуживанием запросов шарды проходят отдельный этап прогрева на основе поисковых логов, который предварительно заполняет начальное состояние и делает переход на реальный трафик более плавным. После прогрева контроллер проверяет состояние индекса и готовность, а затем возвращает группу в маршрутизацию. Остальные группы в это время продолжают обслуживать запросы. Поскольку состояние работы сохраняется, перезапуск не заставляет начинать сборку заново: контроллер может переподключиться к уже выполняющейся подготовке, использовать готовый результат или продолжить прерванную ротацию с сохраненного шага.
Время обновления данных различается в зависимости от поисковых вертикалей. Для вертикалей с высокими требованиями к свежести доставка документов занимает считанные минуты. Дополнительную копию данных можно развернуть примерно за полчаса; готовые индексы загружаются и привязываются к шардам без повторного индексирования исходных документов.
Миграция и валидация в нашем поисковом стеке
Нашей первой целью было заменить старую систему движком, который сохранял бы функциональность и качество поиска, одновременно сокращая время отклика. Мы валидировали Photon поэтапно, начав с воспроизводимых нагрузочных тестов и офлайн-оценок качества перед переходом на живой трафик. Теперь Photon обеспечивает поиск и ранжирование в нашем продакшн-стеке поиска с существенно меньшим временем отклика, чем старая система.
Нагрузочное тестирование и оценки
Для тестирования производительности мы создали инструмент нагрузочного тестирования, который воспроизводит запросы из наших поисковых логов с настраиваемой частотой поступления. Он сравнивает результаты между запусками и применяет статистические тесты для оценки того, являются ли наблюдаемые различия в производительности стабильными. Отдельно мы оценили Photon и старую систему на одних и тех же наборах данных в наших внутренних системах офлайн-оценки для проверки на регрессии качества.
После этих проверок мы одновременно направили живые запросы в оба движка, используя теневой трафик. Это позволило нам сравнить их производительность и результаты на продакшн-нагрузках до изменения того движка, который обслуживает пользователей. Затем мы включили Photon для небольших подмножеств пользователей и постепенно расширяли развертывание, постоянно отслеживая качество и производительность. Первая версия прошла приемочное тестирование без регрессий качества, и мы продолжили развертывание, пока Photon не стал обслуживать весь продакшн-трафик.
Измеренные улучшения в продакшне
В нашем рабочем процессе в продакшне время отклика p99 упало примерно с 800 мс в старой системе до примерно 65 мс в Photon. Это измерение охватывает все этапы внутри Photon, но исключает последующие этапы в высокоуровневом поисковом стеке. Мы также больше не наблюдаем скачков задержки, ранее связанных с переключением версий индекса.
Photon использует примерно на 20% меньше эквивалентных серверов обслуживания, чем узлы контента старой системы, при этом храня примерно в 2,5 раза больше данных на документ. После миграции мы использовали эти дополнительные данные для улучшения качества ранжирования по сравнению со старой системой.
Photon также уменьшает объем данных, которые должны постоянно находиться в памяти. Закрепление того же набора данных Photon в оперативной памяти с помощью mlock потребовало бы примерно в 4,6 раза больше резидентной памяти, чем Photon использует в настоящее время.
Оценка быстрого пресета Search API
Мы также использовали Photon в качестве основы для нового быстрого пресета для нашего Search API, разработанного для рабочих процессов, чувствительных к задержкам. Согласно нашим измерениям, задержка одного поискового вызова для быстрого пресета составляет 160 мс при p50 и 230 мс при p95, что ниже или сопоставимо с показателями, заявляемыми другими провайдерами Search API, хотя сообщаемые перцентили хвоста различаются.
Мы оценили быстрый пресет на шести агентных бенчмарках: WideSearch, BrowseComp, DSQA, FRAMES, SEAL-0 и SEAL-Hard. Среди протестированных конфигураций он показал наименьшую оценочную общую стоимость модели и поиска и самую высокую совокупную оценку задач.
Быстрый пресет объединяет Photon с ранжированием, оптимизированным для агентных рабочих процессов, обеспечивая сопоставимую производительность с пресетом по умолчанию при существенно меньших затратах. Тем не менее, здесь есть компромисс в более широком качестве поиска. Быстрый пресет использует для ранжирования меньше вычислительных ресурсов, чем пресет по умолчанию. На внутренних бенчмарках, охватывающих запросы с длинным хвостом, широкое покрытие и разнообразие результатов, он набирает на 0,24 пункта меньше по релевантности и примерно на 3 процентных пункта меньше по доступности ответов. Мы считаем это разумным компромиссом для агентных рабочих процессов, так как собственные рассуждения и знания модели помогают компенсировать эту разницу.
Мы рекомендуем быстрый пресет для повседневных агентных задач. Для более сложных или неоднозначных вопросов, не требующих срочного ответа, пресет по умолчанию обеспечивает более надежное качество, оправдывая более высокие задержки и стоимость.
Заключение
Мы планомерно переводим наш поисковый стек на собственную инфраструктуру на базе Rust. Замена открытого движка на Photon, наш кастомный движок поиска и ранжирования, — это последний шаг. Photon дает нам полный контроль над представлениями данных, организацией чтения и развертыванием новых индексов, позволяя оптимизировать каждый слой под наши рабочие нагрузки.
Photon обеспечил ускорение поиска во всех продуктах Perplexity и лег в основу нашего нового быстрого пресета в Search API. После миграции задержка поиска и ранжирования p99 упала примерно с 800 мс до 65 мс. Наши компактные форматы данных также позволяют хранить в 2,5 раза больше данных на документ, предоставляя поиску и ранжированию доступ к более богатой информации.
Быстрый пресет обеспечивает задержку одного поискового вызова 160 мс при p50 и 230 мс при p95. По результатам оценки на шести агентных бенчмарках он снизил оценочные затраты на модель плюс поиск на задачу на 68% по сравнению с пресетом по умолчанию при сохранении сопоставимого общего качества задач. Эти результаты демонстрируют практические преимущества инфраструктуры, созданной с учетом наших поисковых рабочих нагрузок, как для продуктов Perplexity, так и для разработчиков, использующих наше API.
Мы продолжаем улучшать производительность Photon и эффективность хранения.Наша цель — поддерживать больший индекс и более богатые данные документов на том же железе, сохраняя при этом высокую скорость поиска по мере роста наших рабочих нагрузок.
Цитирование
@online{perplexity2026Photon,
author = {{Perplexity} and Maksim Morozov and Bogdan Savko and Ruslan Kovalev and Aleksei Bykov and Artur Zayats and Denis Bykov and Nikita Kireev and Jarrod Bryan and Claire Beamer and Jeremy Yang},
title = {{Photon: Building a retrieval and ranking engine from scratch}},
date = {2026-09-24},
url = {https://www.perplexity.ai/hub/blog/photon}
}