Основные выводы
Оптимизация индексирования данных и структуры хранения может существенно сократить время выборки и повысить эффективность использования хранилища.
Категоризация и приоритизация релевантных данных на основе конкретных факторов, таких как местоположение или время доставки, повышают точность и скорость обработки запросов.
Методы шардинга, например геошардинг, позволяют сбалансировать нагрузку на систему и повысить эффективность поиска в сложных и масштабных системах.
Параллельная обработка запросов и поддержка различных типов сопоставления одновременно повышают производительность поиска и релевантность результатов.
Обеспечение согласованности между различными точками входа (или каналами взаимодействия) способствует более плавному и интуитивно понятному пользовательскому опыту.
Будучи разработчиками, мы постоянно стремимся создавать системы, которые не просто работают, но и отличаются эффективностью и масштабируемостью. В мире, где пользователи ожидают всё более быстрые и точные результаты, оптимизация производительности поиска становится ключевым приоритетом в современной разработке приложений.
Эта статья основана на нашем выступлении на конференции QCon San Francisco 2024, где мы рассмотрели эволюцию подходов к индексированию данных, их извлечению и ранжированию. Для платформ вроде Uber Eats, обрабатывающих сложные запросы на больших объёмах данных, оптимизация поиска — это серьёзный вызов, требующий продвинутых стратегий: индексирования, шардинга и параллельной обработки запросов.
Сложность поисковых систем продолжает расти, и необходимость соблюдения баланса между скоростью, релевантностью и масштабируемостью становится как никогда актуальной. В этой статье мы рассматриваем ключевые техники таких оптимизаций и их влияние на пользовательский опыт и производительность системы.
Расширение выбора в Uber Eats: подход nX
Формирование ассортимента в Uber Eats — это сложная задача, зависящая от контекста. Для команды онбординга и операционного отдела успех измеряется количеством подключённых ресторанов и магазинов. Для потребителей понятие «выбор» может означать совершенно разное: кто-то ставит в приоритет скорость доставки, кто-то ищет любимые заведения или возможности открыть для себя что-то новое. Задача заключается в том, чтобы учесть это разнообразие ожиданий и при этом обеспечить бесшовный опыт поиска и выбора.
Помимо концептуальных аспектов выбора, существуют и серьёзные технические препятствия. По мере того как бизнес Uber Eats рос, особенно во время и после пандемии, платформа начала предлагать не только доставку из ресторанов, но и товары из продуктовых и розничных магазинов, а также доставку посылок и отдельных предметов. Это расширение добавило значительную сложность в инфраструктуру ранжирования и рекомендаций.
Одним из ключевых отличий в масштабировании ассортимента стало различие между подключением ресторанов и продуктовых магазинов. Меню ресторанов обычно содержит 20–30 позиций, в то время как ассортимент продуктового магазина может превышать 100 тысяч товарных единиц (SKU). Такое разнообразие требует продвинутой системы индексирования, чтобы эффективно предлагать пользователям релевантные варианты.
Другим важным шагом в стратегии Uber Eats стало расширение географии доставки. Ранее заказы были возможны только из ресторанов в радиусе 10–15 минут. Сейчас платформа позволяет оформлять заказы с доставкой в пределах почти часа пути. Например, пользователь в одном городе может заказать еду или товары из ресторана или магазина в другом городе — и получить их с доставкой через Uber Eats. Такое расширение радиуса создаёт дополнительные технические вызовы, особенно по части логистики и выполнения заказов.
Основной фокус стратегии Uber Eats в области ассортимента — максимизировать количество доступных вариантов на всех поверхностях обнаружения внутри приложения. Персонализация, хотя и важна, является отдельной задачей, требующей особых решений. Постоянно совершенствуя технологии индексирования, ранжирования и рекомендаций, Uber Eats стремится к созданию более полного и динамичного ассортимента, чтобы пользователи могли быстро и удобно находить то, что им нужно: будь то любимый ресторан, новый гастрономический опыт или срочный товар из продуктового магазина.
Точки взаимодействия, такие как главная лента, поиск, рекомендации и реклама, играют ключевую роль в соединении пользователей с доступными вариантами. Лента является основным входом для заказов и включает карусели на основе пользовательской истории, списки магазинов и блоки с акциями и кухнями. Функциональность поиска охватывает рестораны, блюда и кухни, а рекомендации помогают находить похожие или альтернативные варианты в режиме реального времени. Рекламные блоки повышают видимость заведений, помогая продавцам эффективнее привлекать релевантную аудиторию. Обеспечение согласованности между всеми этими поверхностями критически важно для создания цельного и интуитивного пользовательского опыта.

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

Уровень выборки оптимизирует полноту поиска (recall), извлекая широкий спектр потенциально подходящих заведений, которые затем уточняются по релевантности с помощью системы ранжирования. Первичное ранжирование фокусируется на точности (precision) — используется лексическое сопоставление, чтобы соотнести пользовательские запросы с найденными документами. Затем подключается слой обогащения (hydration), в который встроена бизнес-логика: учитываются такие факторы, как акции, преимущества по подписке и расчётное время доставки. Наконец, вторичное ранжирование персонализирует результаты, основываясь на истории заказов пользователя и коэффициентах конверсии, чтобы выдать наиболее релевантные варианты.
По мере расширения ассортимента Uber Eats столкнулся с серьёзными проблемами масштабирования. Первая попытка увеличить количество заведений, извлекаемых на этапе выборки, привела к четырёхкратному росту задержек, что потребовало глубокого анализа неэффективности процессов загрузки данных, обработки запросов и ранжирования.

Одной из ключевых проблем стали процессы индексирования и обработки запросов. Uber Eats использует шестиугольную геолокационную разметку H3 для определения зон доставки, однако на этапе загрузки данных заведения некорректно классифицировались как «близкие» или «далёкие». Эта ошибка приводила к сбоям в ранжировании: заведения, фактически находящиеся поблизости, получали заниженный приоритет.


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

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

Чтобы решить эти проблемы, Uber Eats пришлось доработать стратегии индексирования, извлечения и ранжирования. Основное внимание было сосредоточено на поиске баланса между масштабом и эффективностью, чтобы платформа могла отображать наиболее релевантные заведения без ущерба для производительности. Эти оптимизации сыграли ключевую роль в поддержании плавного пользовательского опыта на фоне растущего ассортимента Uber Eats.
Платформа поиска Uber Eats
Поисковая платформа, лежащая в основе Uber Eats и обрабатывающая десятки миллионов запросов ежедневно, построена на Apache Lucene и использует архитектуру Lambda для загрузки данных. Эта структура включает пакетную загрузку с помощью Spark и потоковую загрузку через стриминговый путь, что обеспечивает актуальность поисковой выдачи.

Одной из ключевых особенностей является приоритетно-ориентированная загрузка, позволяющая обрабатывать запросы с высоким приоритетом в первую очередь и тем самым поддерживать свежесть данных.
Кроме того, Uber активно применяет геошардинг для оптимизации геопространственных сценариев поиска. Индивидуальные схемы индексации и специализированные операторы запросов повышают эффективность за счёт использования оффлайн-ранжирования документов и преждевременного завершения (early termination) запросов для ускорения обработки.
Архитектура поисковой платформы включает три основных компонента:
1. Пакетный конвейер индексирования (Batch Indexing Pipeline) — Spark-задания обрабатывают данные, преобразуют их в поисковые документы, разбивают на шарды и формируют Lucene-индексы, которые затем сохраняются в объектном хранилище.
2. Потоковая/оперативная загрузка (Streaming/Real-Time Updates Path) — обновления поступают через стриминговый сервис, который сопоставляет документы с определёнными партициями Kafka для оперативного обновления. Каждая партиция Kafka соответствует одному шардy. Kafka выполняет роль журнала предзаписи (write-ahead log), что обеспечивает стабильную работу при скачках нагрузки, приоритетную загрузку, репликацию и отказоустойчивость.
3. Стек обработки запросов (Serving Stack) — узел поиска (searcher node) извлекает индексы из хранилища, догоняет обновления из стрима и выполняет запросы. Stateless-агрегатор направляет поисковые запросы к соответствующим узлам поиска, управляет разветвлением запросов (fanout) и агрегирует результаты перед возвратом пользователю.
Техники шардинга
Эффективное управление геопространственными поисковыми запросами имеет критическое значение для Uber Eats, поскольку пользователи чаще всего ищут рестораны или магазины поблизости. Для этого Uber Eats использует геошардинг — технику, при которой все релевантные данные для конкретной локации хранятся в одном шардe. Такой подход снижает накладные расходы на запросы и устраняет неэффективность, возникающую при извлечении и агрегации данных из нескольких шардов. Кроме того, геошардинг позволяет выполнять первичный этап ранжирования непосредственно на узлах данных, что повышает скорость и точность.
В Uber Eats применяются два основных метода геошардинга: широтный шардинг (latitude sharding) и шардинг по шестиугольникам (hex sharding).
Широтный шардинг делит земную поверхность на горизонтальные полосы, каждая из которых представляет собой отдельный шард. Диапазоны шардов рассчитываются оффлайн с помощью Spark-заданий: сначала карта разбивается на тысячи узких широтных полос, затем соседние полосы объединяются в шарды приблизительно одинакового размера. Документы, попадающие на границы шардов, индексируются в обоих соседних шардах, чтобы избежать потери результатов.
Одним из ключевых преимуществ широтного шардинга является его способность эффективно распределять нагрузку между разными часовыми поясами. Поскольку активность пользователей Uber Eats следует за так называемым «солнечным графиком» (высокий спрос днём, низкий — ночью), такой способ помогает избежать перегрузки отдельных шардов. Однако в густонаселённых городских районах шарды могут становиться несбалансированными, что приводит к задержкам в индексировании и увеличению времени обработки запросов.

Для решения этих проблем Uber Eats также использует шардинг по шестиугольникам (hex sharding), основанный на геопространственной системе индексирования H3. В отличие от широтного шардинга, который делит мир на полосы, hex sharding структурирует данные в виде шестиугольных тайлов (ячеек) различной детализации.
Выбор подходящего размера шестиугольника имеет решающее значение. Uber, как правило, использует уровни H3 size 2 или 3, чтобы достичь оптимального баланса между эффективностью и точностью.
Как и в случае с широтным шардингом, hex sharding включает буферные зоны, чтобы документы, находящиеся возле границ шардов, индексировались в нескольких смежных ячейках. Это предотвращает пропуски в результатах поиска.
Ключевое преимущество hex sharding — равномерное распределение шардов, особенно в плотных городских агломерациях, где широтный подход даёт сбои и создаёт дисбаланс.

Комбинируя эти две техники, Uber Eats оптимизирует свою поисковую инфраструктуру, обеспечивая быстрые, точные и масштабируемые геопространственные запросы — вне зависимости от того, откуда пользователь оформляет заказ.
Для повышения производительности поиска в Uber Eats было реализовано множество улучшений, основанных на анализе шаблонов запросов и переработке структуры данных с целью повысить полноту выборки (recall) и одновременно снизить задержки (latency).
Одним из ключевых улучшений стало создание специализированных структур данных, адаптированных под разные сценарии использования — например, доставка еды и поиск товаров в магазинах. Благодаря согласованию структуры данных с характером запросов повысилась эффективность выборки, удалось сократить лишние вычисления.
Ещё одной важной оптимизацией стало индексирование расчётного времени доставки (Estimated Time of Delivery, ETD), что позволило разделить поисковое пространство на непересекающиеся диапазоны и обрабатывать их параллельно. Это ускорило выполнение запросов при сохранении точности ранжирования.

Кроме того, такие задачи, как различение близких и удалённых заведений, были смещены ближе к началу пайплайна индексирования. Это позволило сократить объём вычислений во время выполнения запроса и повысить общую производительность. Эти улучшения существенно повысили эффективность поиска, обеспечив более быстрые и релевантные результаты при сохранении масштабируемости.
Улучшение структуры данных для более быстрых и масштабируемых запросов
Uber Eats повысил эффективность поиска за счёт оптимизации структуры хранения данных, приведя её в соответствие с характерными шаблонами запросов. Это дало значительное снижение задержек и улучшение масштабируемости.
Индекс Eats был переработан таким образом, чтобы сначала группировать рестораны по городам, затем — по конкретным заведениям, а уже внутри них — по позициям меню. Такой подход позволил быстро отсеивать нерелевантные города, существенно снижая объём лишней обработки.
Кроме того, поскольку Lucene использует дельта-кодирование, группировка схожих атрибутов рядом позволила повысить эффективность сжатия, что привело к сокращению размера индекса на 20%.

Для поиска продуктов в магазинах потребовалась немного иная стратегия из-за значительно большего объёма ассортимента по сравнению с ресторанами.
Магазины ранжировались на основе офлайн-коэффициентов конверсии, а товары внутри каждого магазина группировались вместе. Такая структура позволила системе устанавливать лимит на количество извлекаемых результатов от одного магазина, предотвращая доминирование выдачи одной торговой точки и обеспечивая более разнообразные результаты от разных поставщиков.
Этот подход особенно хорошо работает при запросах с обобщёнными ключевыми словами, такими как «курица», когда из одного магазина может приходить несколько тысяч совпадений.

Эти оптимизации структуры данных привели к снижению задержек при выборке на 60%, сократив среднее время обработки запроса с 145 до 60 миллисекунд.
Дополнительно, сортировка документов позволила значительно ускорить поиск по индексу: время выборки одного документа сократилось с 10–60 микросекунд до менее чем 5 микросекунд.
В совокупности это дало 50% улучшение P95-задержек, обеспечив гораздо более быстрый и плавный пользовательский опыт.

Оптимизация индексирования ETA в Uber
Uber улучшил индексирование ETA (Estimated Time of Arrival — расчётное время прибытия), чтобы повысить эффективность поиска, сократить задержки и увеличить полноту выборки. Для этого в платформу поиска была добавлена метаинформация о зонах доставки ресторанов и оценках времени доставки.
Изначально в системе отсутствовали данные о взаимных расстояниях между зонами доставки (шестиугольниками), из-за чего ранжирующим алгоритмам было сложно адекватно оценивать удалённые рестораны.
Чтобы решить эту проблему, Uber сгруппировал времена доставки в фиксированные диапазоны и начал индексировать рестораны в соответствии с их близостью к каждому шестиугольнику.

Такой подход повлёк за собой компромисс между затратами на хранение и эффективностью обработки. Рестораны приходилось сохранять в индексе несколько раз, если они попадали в разные диапазоны времени доставки для различных шестиугольников. Несмотря на рост объёма хранимых данных, это дало значительный прирост в скорости выполнения запросов и позволило существенно быстрее извлекать релевантные результаты.
Uber протестировал альтернативные методы индексирования, включая BKD-деревья и извлечение через gRPC, но в итоге пришёл к выводу, что предварительный расчёт связей и их хранение в индексе обеспечивает наилучшую производительность.
Новая система позволила параллелизовать выполнение запросов: один поисковый запрос теперь может запускать несколько подзапросов по разным диапазонам ETA без увеличения задержки. В результате задержка при выполнении запроса снизилась на 50%, а полнота выборки выросла — алгоритмы ранжирования получили доступ к большему числу релевантных ресторанов без потерь в производительности.

Ещё одним важным улучшением стало управление заведениями, которые можно найти, но в которых нельзя заказать доставку. Иногда рестораны отображаются в поиске, но не принимают заказы по разным причинам: отсутствие доступных курьеров, географическое ограничение или текущее время суток.
Вместо того чтобы определять возможность доставки во время выполнения запроса, Uber предварительно рассчитывает доставляемость на этапе загрузки данных. Это позволяет системе быстро различать рестораны, которые действительно доступны для заказа, и те, которые можно только просматривать. Такой подход значительно улучшает пользовательский опыт.
Ключевые аспекты оптимизации поиска в Uber
Оптимизация поиска и выборки в Uber демонстрирует, насколько важны грамотно организованное индексирование, эффективные стратегии шардинга и приёмы параллелизации. Первоначально проблемы с производительностью были вызваны неэффективным хранением и выборкой данных — запросы обрабатывали огромные массивы документов, распределённые по множеству регионов.
Проанализировав шаблоны запросов, Uber перестроил структуру индекса, сделав упор на кластеризацию по городам и заведениям, что позволило значительно ускорить получение релевантных результатов.
Эта реорганизация сократила время выборки более чем на 50% и улучшила коэффициенты сжатия, сделав систему хранения данных более эффективной.
Введение индексирования ETA дополнительно улучшило процесс ранжирования: система стала «штрафовать» отдалённые заведения и поднимать в выдаче те, что находятся в пределах оптимального радиуса доставки.
Благодаря стратегическому подходу к загрузке и индексированию данных Uber добился оптимального баланса между полнотой выборки и скоростью, обеспечив пользователям более релевантные и быстрые результаты.

Помимо перестройки структуры индекса, Uber сосредоточился на переносе сложной логики обработки fallback-сценариев с уровня выполнения запросов на уровень загрузки данных (ingestion layer). Это позволило сократить ненужные вычисления в момент выполнения запроса и сделало поиск более лёгким и быстрым.
Использование параллельных диапазонных запросов дало возможность расширить пространство выборки без роста задержек, увеличив разнообразие результатов при сохранении высокой скорости отклика.
Дополнительную оптимизацию принесло исключение неэффективных операций — например, ранее тестовые заведения обрабатывались вместе с продуктивными, что искусственно увеличивало время отклика.
Благодаря использованию непересекающихся подзапросов, Uber смог максимально распараллелить выполнение и одновременно запускать различные типы сопоставлений: точные, нечеткие и частичные. Это дало значительный прирост в эффективности.
Заключение
Реализация этих оптимизаций потребовала широкой межкомандной координации — команды, отвечающие за поиск, ленту, рекламу и рекомендации, должны были синхронизировать изменения, затрагивающие всю экосистему поиска.
Процесс был непростым: потребовались месяцы бенчмаркинга, тестирования и тонкой настройки. Но результаты оказались по-настоящему трансформационными: задержки значительно сократились, полнота выборки улучшилась, а масштабируемость системы выросла, создав прочную основу для более отзывчивого и интуитивного пользовательского поиска в Uber Eats.
По мере того как Uber продолжает глобальное расширение и развитие своей платформы, эти улучшения подчёркивают важность решений, основанных на данных, эффективной архитектуры систем и непрерывной итерации. Постоянно пересматривая и оптимизируя свои решения, Uber остаётся гибкой и конкурентоспособной компанией в быстро меняющемся мире масштабируемых распределённых систем, обеспечивая всё более бесшовный опыт для пользователей и поддерживая рост продуктового предложения.
Понимание архитектуры — это не про красивые диаграммы. Это про выбор конкретных технологий под конкретные ограничения. Если вы строите распределённые системы, боретесь с задержками или отлаживаете поток данных — вот два открытых урока, которые помогут принимать технические решения:
11 августа, 20:00
RabbitMQ против Kafka — что выбрать для вашей структуры: сравнение и лучшие практики28 августа, 18:00
Практическое введение в Apache Spark: первые шаги в обработке больших данных
Кроме того, пройдите вступительное тестирование, чтобы оценить свой уровень и узнать, подойдет ли вам программа курса "Highload Architect".