Photon: Изграждане на двигател за извличане и класиране от нулата
Как специализираните формати на данни, пакетните четения и разделянето на изграждането на индекса от обслужването на заявки подобряват AI-ориентираното търсене.
Въведение
AI-нативният стек за търсене на Perplexity извлича и класира кандидат-страници, извеждайки на повърхността най-релевантното съдържание, за да може моделът да обоснове своите отговори. Първоначално този етап се изпълняваше на двигател с отворен код, който адаптирахме към нашата система. Въпреки това, с разрастването на нашия индекс и работни натоварвания, стигнахме твърди лимити по отношение на разходите, опашната латентност и времето за възстановяване на възлите, които не успяхме да разрешим в рамките на съществуващата сторонна платформа.
За да преодолеем тези предизвикателства, изградихме Photon – нов собствен двигател за извличане и класиране. Това продължава нашия подход за изграждане на AI-ориентирана инфраструктура за търсене от нулата, както направихме с CobbleDB – нашето разпределено хранилище ключ-стойност за обслужване на записи на страници, и нашия Search API.
Photon използва компактни формати на данните за четене и декомпресиране само на това, което всяка заявка изисква, с кеширане, съобразено с партидите, и асинхронни четения за намаляване на достъпа до диск и припокриване на чаканията. Изграждането на индекса се изпълнява отделно от обслужването на заявки, като актуализира една обслужваща група наведнъж и загрява нейните кешове преди насочването на активни заявки към нея.
Photon вече захранва нашия тръбопровод за търсене и е също така основата на нов бърз пресет за нашия Search API, с латентност при единична заявка за търсене от 160 ms при p50 и 230 ms при p95. В шест публични бенчмарка бързият пресет намали прогнозните разходи за модел плюс търсене на задача с 68%, като същевременно запази общо качество на задачите, съпоставимо със стандартния пресет. Публикуваме го днес, за да могат разработчиците да получат достъп до тези подобрения.
Какво изисква двигателят за извличане и класиране
За всяка заявка двигателят за извличане и класиране търси в голям, разпределен индекс, за да избере и подреди малък набор от релевантни страници в рамките на бюджета за латентност на стека за търсене. Системата трябва също така да поддържа индекса актуален с постъпването на нови страници и промяната на съществуващите, без да позволява тази работа да забавя активните заявки.
Оценихме няколко опции с отворен код и избрахме зрял, общопредназначен двигател за извличане и класиране, който отговаряше на нашите първоначални нужди. След това го адаптирахме към нашата архитектура и работни натоварвания, поддържайки стабилна латентност и пропускателна способност в производствен мащаб.
Къде се провали предишната ни система
Поддържахме собствен форк, за да адаптираме двигателя към нашия разширяващ се индекс, но справянето с ключовите ограничения все повече изискваше преработване на големи части от платформата. Няколко ограничения се оказаха трудни за разрешаване без фундаментални архитектурни промени:
1. Разходи за мащабиране. Както броят на документите, които съхраняваме, така и количеството данни за документ, които трябва да извлечем и класираме, нарастват с времето. Това мащабиране оказва натиск върху нашия хардуер, особено върху наличното количество RAM памет. За да контролираме разходите, се нуждаехме от по-компактни представяния на данните и по-фин контрол върху това какво остава в паметта и какво се чете от диска при поискване.
2. Лоша опашна латентност. При производствен трафик времето за реакция p99 на старата система беше около 800 ms. Наборът от данни надвишаваше наличната RAM памет, така че не можахме да използваме mlock, за да го запазим изцяло в паметта. Достъпът до нерезидентни данни можеше да предизвика сериозни грешки в страниците (page faults), което забавяше изпълнението на заявките, докато операционната система чете от диска.
Двигателят също така периодично обединяваше своя основен дисков индекс с по-малки индекси, изчистени от паметта. По време на този процес на сливане на дисковия индекс броят на грешките в страниците се покачваше още повече, като латентността p99 достигаше приблизително 1,2 s за 10–15 минути. С загряването на кешовете честотата на грешките в страниците и латентността намаляваха, което подсказваше, че студените четения са основен приносител за пиковете. Предварителното зареждане на данни и загряването на индекса намалиха пиковете, но не разрешиха адекватно високата опашна латентност.
3. Продължително възстановяване и синхронизация. В предишната система разполагането на допълнителен клъстер и синхронизирането на неговите данни можеше да отнеме повече от седмица. Синхронизация също се изискваше при добавяне на групи от възли за съхранение на допълнителни копия на данните или при възстановяване след отпадане на възли. По време на възстановяването по-голям дял от отговорите при търсене бяха частични, което понижаваше оценката за качеството на крайния отговор.
В крайна сметка стигнахме до извода, че ще бъде по-просто и евтино да построим всичко от нулата, отколкото да продължим да адаптираме и поддържаме нашия форк. Възползвайки се от нашия опит в експлоатацията на предишната система, малък инженерен екип работи заедно с рояк от постоянни Агенти за кодиране, за да изгради Photon.
Къде се вписва Photon в стек за търсене
Photon е проектиран за два основни работни процеса: обслужване на заявки за търсене и поглъщане на актуализации от тръбопровода за обработка.
Заявка от услугата за търсене от по-високо ниво достига до брокера на Photon през балансьор на натоварването. Брокерът избира група от шардове и препраща заявката към всеки шард, като следи за изтичане на времето за изчакване (timeouts) и издава допълнителни заявки при необходимост.
Всеки шард извършва извличане, първоначално класиране и класиране на втория етап, след което връща най-добрите си кандидати на брокера. Брокерът обединява тези кандидати в единен класиран списък и извлича ключови полета на документите за избраните страници от съответните шардове. След това той връща тези резултати на услугата за търсене от по-високо ниво, която събира съдържанието на страниците, необходимо на агента за генериране на обоснован отговор.
Актуализациите на индекса следват отделен път. Pillar, нашият слой за публикуване, подготвя данните и ги съхранява в таблици на YTsaurus. Индексаторите преобразуват тези таблици в индексни структури, които шардовете могат да заредят за търсене. Контролерът координира подготовката на данните, изграждането на индекса и разпоръчването на нови версии на индекса към обслужващите групи. Преди да обработи активен трафик, всеки шард възпроизвежда реални заявки за търсене, за да загрее своите кешове.
Проектиране на Photon за нашите работни процеси
Три дизайнерски решения правят Photon изключително подходящ за нашето работно натоварване: компактни представяния на данни, кеширане и асинхронни дискови четения, както и отделяне на актуализациите на индекса от обслужването на заявки.
Представяне на данните за извличане и класиране
Извличането и класирането достъпват данните на документите по различен начин. Лексикалното извличане започва с термини от заявката и намира документи, които ги съдържат. След това класирането се нуждае от подробна информация за всеки избран документ, включително честоти и позиции на термините.
Photon използва две допълващи се представяния в подкрепа на тези модели на достъп. Инвертираният индекс съхранява списък от съвпадащи документи за всеки термин, наричан постов списък (posting list), за да поддържа избора на кандидати. Отделни записи за документ предоставят данните, необходими за класирането. Ние също така оптимизираме начина, по който постинговите списъци се съхраняват и обхождат, за да подобрим избора в рамките на фиксиран работен бюджет.
Специализираните представяния на данни ни позволяват да съхраняваме ключови данни по-ефективно. По-компактният инвертиран индекс намалява количеството данни, които се четат или преместват на заявка, подобрявайки латентността чрез елиминиране на ненужни I/O операции. Тези формати също така правят възможно проверането дали даден документ се появява в списък, без да се декомпресира напълно – свойство, на което разчита нашата логика за обхождане, за да контролира колко работа свършва дадена заявка при фиксиран бюджет. Оптимизирането на записите за документ ни позволява да съхраняваме повече данни за документ за целите на класирането, като същевременно намалява разходите за мащабиране, свързани с нашата предишна система.
Инвертираният индекс
За всеки термин инвертираният индекс съхранява постов списък. Тези списъци могат да варират от шепа документи до милиони, така че никой единичен формат не работи добре за всеки списък. Photon избира формата въз основа на броя документи, съдържащи термина.
Краттките списъци използват вградено представяне, което се побира в рамките на една страница и не изисква таблица с блокове. По-дългите списъци, които не се побират в вградения формат, се разделят на блокове, покриващи фиксирани диапазони от идентификатори на документи. Представянето на всеки блок зависи от неговата плътност: разредените блокове съхраняват сортирани масиви от отмествания на ID на документи, докато плътните блокове използват битови масиви (bitmap). Разредените блокове използват галопиращо търсене, което напредва с все по-големи стъпки, за да пропуска региони от списъка. Плътните блокове позволяват проверки за членство чрез справка с бита, съответстващ на документ.
Честотата на заявките за даден термин определя къде на диска е разположен неговият постов списък. Често търсените списъци се съхраняват близо един до друг, концентрирайки горещите данни в по-малък регион за съхранение. В рамките на всеки списък документите се подреждат по отделен сигнал за полезност, така че по-обещаващите кандидати да бъдат срещани по-рано.
Обхождане на списъци в рамките на работен бюджет
Изборът на кандидати изисква обхождане на постингови списъци за намиране на потенциално релевантни документи. Изчерпателното сканиране на всеки списък е скъпо, затова Photon използва алгоритъм, подобен на WAND, който обхожда само някои списъци, като същевременно отчита съвпаденията в останалите.
Алгоритъмът разделя списъците на две групи. Водещите списъци придвижват обхождането напред и предлагат кандидат-документи. Пробните списъци допринасят за резултатите на тези кандидати, но самите те не генерират кандидати.
Оценяването на кандидатите комбинира претеглено покритие на термините от заявката и принос от честотата, с нормализиране спрямо дължината на документа и таван на печалбите от повтарящи се термини. Photon първо проверява присъствието на термините в масивите и битовите масиви, за да ограничи максималния възможен резултат на всеки кандидат. Той чете точните честоти на термините само за кандидати, които все още могат да покрият прага за избор.
След това изборът протича в три стъпки:
- Обхождане: Започнете със стриктно изискване за покритие на заявката. Ако бъдат намерени твърде малко кандидати, повторете с разхлабено изискване.
- Запазване (Retention): Кандидатите се съдържат в ограничен набор, използвайки оценка, която комбинира покритие и честота. С подобряването на събраните топ кандидати прагът за допускане се повишава.
- Краен избор: Крайните резултати за прохода се избират от запазения набор въз основа на резултатите за честота. Всяко поле изпълнява собствен проход и резултатите се обединяват с помощта на тегла на полетата.
Алгоритъмът създава набор от кандидати от релевантни документи, които да бъдат използвани в последващото класиране.
Docblob
След като бъдат избрани кандидат-страниците, фазата на класиране се нуждае от честоти на термините, покритие на полетата, както и местоположения и близост на появяванията. Получаването на тези данни чрез инвертирания индекс би изисквало разпръснати достъпи до множество двойки термин-документ. В Photon съхраняваме компактен запис за документ, наречен docblob, който съдържа честоти на термините, полета, в които се е появил терминът, и къде в документа се е появил. В резултат на това класирането на кандидат изисква само една заявка за търсене (lookup) на документ.
Форматът е изграден така, че да избягва декомпресирането на повече от необходимото. Вътре в docblob полетата са групурани по текстов анализатор. Всяка група съхранява сортиран списък от термини, кодиран с Elias-Fano, така че термин от заявката да може да бъде намерен директно, без първо да се разширява целият списък. Маска на полето отбелязва къде се е появил терминът, брояч на честотата следи колко често, а действителните позиции се намират в отделни блокове за всяка двойка термин-поле, кодирани като разлики в позициите. Декомпресират се само блоковете за термини, които реално съвпадат, докато всичко останало в записа остава в компресиран формат.
Това избирателно декодиране позволява на docblob да поддържа съхранението на повече данни за документ без пропорционално увеличение на разходите. Разрастването на записа не принуждава към повече работа при всяко търсене, защото класирането докосва само частите, които са от значение за съответстващите термини, вместо целия запис.
Пакетни четения и работа с паметта
Дори с по-ефективните представяния на данните паметта RAM все пак свършва преди другите ресурси, така че често се налага да достъпваме данни, съхранени на диска. Photon използва асинхронни дискови четения и кеш, оптимизиран за пакетни четения, за да ускори тези достъпи, намалявайки времето за чакане на заявка. Това допринася за значително подобрение на опашната латентност в сравнение с предишната система.
Припокриване на дисковите чакания
Класирането изисква получаване на характеристиките на сто документа, разположени в различни части на файл. Нашата предишна система използваше mmap за мапване на файла във виртуалното адресно Пространство, така че да може да се чете чрез указатели в паметта. Въпреки това достъпът до страница, която отсъстваше от паметта, все пак изискваше дисково четене. Темата трябваше да изчака данните, преди да продължи изчислението, и това време на изчакване се натрупваше, когато процесът трябваше да чете студени записи последователно.
Photon се възползва от факта, че отместванията на записите са предварително известни, така че можем да подаваме няколко четения едновременно. Това се управлява от реактора: специализирани работни теми подават пакети чрез io_uring и предават прочетените данни на извикващия код. Всяка тема поддържа собствен io_uring; пакетите се изпращат към най-малко натоварения реактор. Предварително регистрираните файлови дескриптори и пулове от буфери намаляват овърхеда от повтарящи се четения. Това асинхронно изпълнение позволява чакането за няколко операции да се припокрие, което намалява опашната латентност.
Намаляване на броя на достъпите до дисковете
Преди да подаде четени заявки, Photon проверява цялата партида в кеша в паметта. Само заявките за липсващи записи отиват към диска; след това прочетените данни попълват кеша.
Кешът е проектиран за пакетни четения. По-конкретно:
- Няма отделно заделяне (allocation) при попадение. Вместо това данните се копират в буфер, подготвен предварително от извикващия код.
- Няма придобиване на заключение (lock) от четеца. Вместо това той проверява версията на данните и опитва отново, ако тя е в конфликт със запис.
- Той използва CLOCK освобождаване (eviction) вместо изискване за актуализации на споделен LRU списък при всяко четене.
- Той използва предварително извличане (prefetching) на CPU кеша, при което метаданните и данните за предстоящи записи се заявяват предварително.
Тези оптимизации намаляват латентността, без да влошават производителността при работните натоварвания, които ни вълнуват.
Изграждане и актуализиране на индекса отделно от обслужването
В Photon изграждането на индекса и обслужването на заявки се изпълняват на различни възли. Индексаторите преобразуват данните в готови структури за съхранение, докато обслужващите възли ги четат и извършват извличане и класиране. В резултат на това изграждането на индекса не се конкурира със заявките за търсене за CPU ресурс, а ресурсите за тези задачи могат да бъдат мащабирани независимо.
Това разделяне решава тесните места при възстановяването и синхронизацията, съществуващи в предишната реализация. Тъй като изграждането на индекса се изпълняваше в същия клъстер, който обслужваше заявки, възстановяването на дефектирал възел или добавянето на ново копие на данните можеше да означава повторно индексиране на документи на място – процес, който отнемаше дни и увеличаваше частичните отговори по време на възстановяването. Photon премахва тази обвързаност; индексаторите се изпълняват като разпределени задачи, които произвеждат версионни, готови за обслужване индексни структури независимо от обслужващите възли, така че изграждането на следващата версия на индекса вече не се конкурира с активния трафик.
Подготовка и разпределено изграждане
Изграждането започва с избор на документи за активния индекс и създаване на източника за експортиране. Контролерът записва версията на източника, входните таблици и очаквания брой шардове. MapReduce задачите в YTsaurus подготвят текст, метаданни, фрагменти, характеристики и предварително изчислени вграждания във версионни таблици, партиционирани по шард. Всеки индексатор стриймва подготвената таблица, изгражда структурите за своя шард и обединява текстовите сегменти. Готовият индекс, заедно с описание на неговите компоненти, се качва в обектно хранилище.
Броят на шардовете и броят на едновременно работещите индексатори се конфигурират независимо, така че една сборка може да се изпълни в няколко вълни или да се заделят повече машини за работа в паралел. Разпределената подготовка и паралелните индексатори позволяват пълният уеб индекс да бъде изграден за брой часове от порядъка на едноцифрено число, което е значително подобрение спрямо предишната система.
Внедряване на нова версия
Преди внедряването контролерът проверява дали новият индекс е готов. Ако данните са непълни, внедряването се блокира автоматично.
Когато има множество обслужващи групи, контролерът ги актуализира една по една. Първо той премахва група от маршрутизирането на брокера и я стартира с новата версия на индекса. Преди да обслужват заявки, шардовете преминават през отделен етап на загряване въз основа на дневниците на търсенията, което предварително попълва началното състояние и прави прехода към реалния трафик по-плавен. След загряването контролерът проверява състоянието и готовността на индекса, след което връща групата към маршрутизирането. Другите групи продължават да обслужват заявки през това време. Тъй като състоянието на работа е запазено, рестартирането не принуждава изграждането да започва отначало; контролерът може да се прикачи отново към подготовка, която вече е в ход, да използва повторно готов резултат или да продължи прекъсната ротация от запазената стъпка.
Времената за актуализиране на данните се различават в различните вертикали за търсене. За вертикали с високи изисквания за актуалност доставката на документи отнема едноцифрено число минути. Допълнително копие на данните може да бъде разположено за около половин час; готовите индекси се зареждат и прикачват към шардовете без повторно индексиране на изходните документи.
Миграция и валидация в нашия стек за търсене
Нашата първа цел беше да заменим старата система с двигател, който запазва функционалността и качеството на търсенето, като същевременно намалява времената за отговор. Валидирахме Photon на етапи, започвайки с възпроизводими тестове на натоварването и офлайн оценки на качеството, преди да преминем към активен трафик. Photon вече захранва извличането и класирането в нашия производствен стек за търсене, със съществено по-ниски времена за отговор от старата система.
Тестване на натоварването и оценки
За тестване на производителността изградихме инструмент за тестване на натоварването, който възпроизвежда заявки от нашите дневници за търсене при конфигурируеми честоти на пристигане. Той сравнява резултатите в рамките на различните изпълнения и прилага статистически тестове, за да оцени дали наблюдаваните разлики в производителността са постоянни. Отделно оценихме Photon и старата система върху едни и същи набори от данни в нашите вътрешни офлайн системи за оценка, за да проверим за регресии в качеството.
След тези проверки изпратихме активни заявки едновременно към двата двигателя, използвайки сенчест трафик (shadow traffic). Това ни позволи да сравним тяхната производителност и резултати при производствени натоварвания, преди да променим кой двигател обслужва потребителите. След това активирахме Photon за малки подмножества от потребители и постепенно разширихме внедряването, като през цялото време наблюдавахме качеството и производителността. Първата версия премине тестовете за приемане без регресии в качеството и продължихме внедряването, докато Photon започна да обслужва целия производствен трафик.
Измерени подобрения в производството
В нашия производствен работен процес времето за отговор p99 спадна от приблизително 800 ms в старата система до приблизително 65 ms в Photon. Това измерване обхваща всички етапи вътре в Photon, но изключва последващите етапи в стека за търсене от по-високо ниво. Също така вече не наблюдале пикове в латентността, свързани преди това с превключването на версиите на индекса.
Photon използва приблизително с 20% по-малко еквивалентни обслужващи машини от съдържателните възли на старата система, като същевременно съхранява приблизително 2,5 пъти повече данни за документ. След миграцията използвахме тези допълнителни данни, за да подобрим качеството на класирането извън това на старата система.
Photon също така намалява количеството данни, които трябва да останат в паметта. Фиксирането на същия набор от данни на Photon в RAM с mlock би изисквало приблизително 4,6 пъти повече резидентна памет, отколкото Photon използва в момента.
Оценка на бързия пресет на Search API
Използвахме Photon и като основа за нов бърз пресет за нашия Search API, проектиран за работни натоварвания, чувствителни към латентността. Според нашите измервания латентността при единична заявка за търсене за бързи пресет е 160 ms при p50 и 230 ms при p95, по-ниска или съпоставима със самопредставените числа на други доставчици на Search API, въпреки че докладваните процентили на опашката се различават.
Оценихме бързия пресет на шест агентни бенчмарка: WideSearch, BrowseComp, DSQA, FRAMES, SEAL-0 и SEAL-Hard. В тези оценки той постигна най-ниските прогнозни общи разходи за модел плюс търсене и най-високия общ резултат на задачите сред тестваните конфигурации.
Бързият пресет съчетава Photon с класиране, оптимизирано за агентни работни процеси, осигурявайки съпоставима производителност със стандартния пресет при съществено по-ниски разходи. Въпреки това той води до компромис в по-общото качество на търсенето. Бързият пресет използва по-малко изчислителна мощ за класиране в сравнение със стандартния пресет. На вътрешни бенчмаркове, обхващащи дългоопашати заявки, широко покритие и разнообразие на резултатите, той постига 0,24 пункта по-ниска релевантност и приблизително 3 процентни пункта по-ниска наличност на отговорите. Вярваме, че това е разумен компромис за агентните работни процеси, тъй като собствените разсъждения и познания на модела могат да помогнат за компенсиране на разликата.
Препоръчваме бързия пресет за ежедневни агентни задачи. За по-предизвикателни или двусмислени въпроси, които са по-малко чувствителни към времето, стандартният пресет предлага по-голяма надеждност на качеството, оправдавайки по-високата си латентност и разходи.
Заключение
Постепенно прехвърляме нашия стек за търсене върху собствена инфраструктура на базата на Rust. Замяната на двигателя с отворен код с Photon – нашия персонализиран двигател за извличане и класиране, е най-новата стъпка. Photon ни дава пълен контрол върху представянето на данните, начина на организиране на четенето и внедряването на нови индекси, което ни позволява да оптимизираме всеки слой за нашите работни натоварвания.
Photon позволи по-бързо търсене в продуктите на Perplexity и предоставя основата за нашия нов бърз пресет в Search API. След миграцията латентността на извличането и класирането p99 спадна от приблизително 800 ms до 65 ms. Нашите компактни формати на данни също ни позволяват да съхраняваме 2,5 пъти повече данни за документ, осигурявайки на извличането и класирането достъп до по-богата информация за документите.
Бързият пресет осигурява латентност при единична заявка за търсене от 160 ms при p50 и 230 ms при 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}
}