В Netflix было сделано много нового со времён выхода предыдущих материалов, посвящённых роли тех, кто отвечает за направление Content Engineering, в реализации поиска по нашему федеративному графу (federated graph). А именно, в первой статье мы идентифицировали проблему и рассказали об использовании инфраструктуры индексирования данных, а во второй мы углубились в вопрос о том, как мы пользуемся очередями. Мы дали доступ к Studio Search для всех инженеров компании, а не только для тех, кто занимается направлением Content Engineering, и переименовали этот проект в Graph Search. С Graph Search интегрировано более 100 приложений. В рамках этой системы поддерживается примерно 50 индексов. Мы продолжаем расширять её функционал. Как было обещано в предыдущем материале, здесь мы расскажем о том, как мы, объединив усилия с одной из команд, отвечающих за Studio Engineering, создавали обратный поиск (reverse search). Обратный поиск переворачивает с ног на голову стандартный подход к выполнению запросов: вместо того, чтобы искать документы, которые соответствуют запросу, он направлен на поиск запросов, соответствующих документу.

Введение

Тиффани работает в Netflix координатором окончательного монтажа видеоматериалов. Она курирует почти дюжину фильмов, находящихся на разных стадиях подготовки к съёмкам, съёмки и окончательного монтажа. Тиффани и её команда работают с различными партнёрами разного профиля, в число которых входят подразделения Legal, Creative и Title Launch Management. Она следит за ходом работы над фильмами и за их состоянием.

Поэтому Тиффани подписывается на уведомления и на события календаря, имеющие отношения к определённым вопросам, требующим повышенного внимания. Например — это могут быть «фильмы, которые снимают в Мехико, в которых не назначен актёр на главную роль», или «фильмы, которые могут оказаться не готовы к их дате выхода в прокат».

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

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

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

Решение

Система Graph Search основана на Elasticsearch, где имеются в точности те возможности, которые нам нужны:

  • Поля типа percolator, которые можно использовать для индексирования запросов Elasticsearch.

  • Запросы типа percolate, которые могут быть использованы для определения того, какие индексированные запросы соответствуют некоему входному документу.

https://miro.medium.com/v2/resize:fit:700/1*GCZRoNqT8seObcUFzYthXg.png
Обычный поиск и обратный поиск

Можно выполнять поиск, используя запрос вроде «фильмы на испанском языке, снятые в Мехико» и возвращая пользователю документы, соответствующие этому запросу (один — для Roma, один — для Familia). А запрос типа percolate берёт документ (для Roma) и возвращает поисковые запросы, которые соответствуют документу. Например — «фильмы на испанском языке» и «драмы, в сценариях которых даны указания для актёров».

Мы представили этот функционал в виде возможности сохранять запросы, названной SavedSearch. Она представляет собой фильтр, постоянно хранимый в существующем индексе.

type SavedSearch {
  id: ID!
  filter: String
  index: SearchIndex!
}

Этот фильтр, написанный на предметно-ориентированном языке (Domain-Specific Language, DSL) Graph Search, конвертируется в запрос Elasticsearch и индексируется в поле типа percolator. В разделе о языке запросов этой статьи можно узнать подробности о DSL Graph Search, и о том, зачем мы его создали, вместо того, чтобы просто использовать язык запросов Elasticsearch.

Мы назвали процесс поиска подходящих сохранённых поисковых запросов ReverseSearch. Это — самая простая часть нового функционала. В Domain Graph Service (DGS) мы добавили новый резолвер для Graph Search. Он берёт интересующий нас индекс и документы, а потом, выполняя запрос типа percolate, возвращает все сохранённые поисковые запросы, которые соответствуют документу.

"""
Запрос для получения всех зарегистрированных сохранённых поисковых запросов
в заданном индексе на основании предоставленного документа. 
Документ в данном случае — это документ ElasticSearch, сгенерированный на
основе конфигурации индекса.
"""
reverseSearch(
  after: String,
  document: JSON!,
  first: Int!,
  index: SearchIndex!): SavedSearchConnection

Постоянное хранение SavedSearch реализовано в виде новой мутации в DGS Graph Search. Это, в итоге, вызывает индексирование запроса Elasticsearch в поле типа percolator.

"""
Мутация для регистрации и обновления сохранённых поисковых запросов. Их нужно
обновлять каждый раз, когда пользователь уточняет критерии поиска.
"""
upsertSavedSearch(input: UpsertSavedSearchInput!): UpsertSavedSearchPayload

Поддержка полей типа percolator на фундаментальном уровне изменила то, как мы готовим к работе конвейеры индексации данных для Graph Search (подробности смотрите в этой статье). Вместо того, чтобы пользоваться одним конвейером индексации на один индекс Graph Search, мы теперь применяем два конвейера. Один — для индексирования документов, а второй — для индексирования сохранённых поисковых запросов в индексе типа percolate. Мы решили добавить поля типа percolator в отдельный индекс для того чтобы раздельно подстроить производительность индексов для двух типов запросов.

Elasticsearch требует, чтобы индекс типа percolate обладал бы мэппингом соответствующим структуре запросов, которые в нём хранятся. Это значит, что он должен соответствовать мэппингу индекса документов. Шаблоны индекса задают мэппинги, которые применяются при создании новых индексов. Используя параметр index_patterns из системы для работы с шаблонами индексов, мы можем использовать мэппинг для индекса документа в двух индексах. Параметр index_patterns, кроме того, даёт нам простой механизм для добавления полей типа percolator в каждый имеющийся у нас индекс типа percolate.

Пример мэппинга индекса документов

Паттерн индекса — application_*.

{
  "order": 1,
  "index_patterns": ["application_*"],
  "mappings": {
  "properties": {
    "movieTitle": {
      "type": "keyword"
    },
    "isArchived": {
      "type": "boolean"
    }
  }
}

Пример мэппинга индекса типа percolate

Паттерн индекса — *_percolate.

{
  "order": 2,
  "index_patterns": ["*_percolate*"],
  "mappings": {
    "properties": {
      "percolate_query": {
        "type": "percolator"
      }
    }
  }
}

Пример сгенерированного мэппинга

Имя индекса типа percolate — application_v1_percolate.

{
  "application_v1_percolate": {
    "mappings": {
      "_doc": {
        "properties": {
          "movieTitle": {
            "type": "keyword"
          },
          "isArchived": {
            "type": "boolean"
          },
          "percolate_query": {
            "type": "percolator"
          }
        }
      }
    }
  }
}

Конвейер индексирования percolate-данных

Создание индекса типа percolate — это очень просто. Достаточно взять входные данные из мутации GraphQL, перевести их на язык запросов Elasticsearch и проиндексировать. Надо сказать, что версионирование, о котором мы поговорим чуть позже, показало тут свою неприглядную сущность, и несколько всё усложнило. Вот как устроен конвейер индексирования percolate-данных.

https://miro.medium.com/v2/resize:fit:700/1*KSZuvPeOxDOKNrPiNnNvFg.png
Конвейер индексирования данных. Подробности о Data Mesh смотрите здесь.
  1. Когда модифицируются сущности SavedSearch — мы сохраняем их в нашей CockroachDB, а коннектор источника для базы данных Cockroach выдаёт CDC-события.

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

  3. Как уже было сказано, в базе данных хранится фильтр Graph Search, написанный на нашем DSL, который отличается от DSL Elasticsearch. Поэтому мы не можем напрямую проиндексировать события и добавить их в percolate-индекс. Вместо этого мы выдаём мутацию Graph Search DGS. А Graph Search DGS переводит наш DSL в запрос Elasticsearch.

  4. Затем мы индексируем запрос Elasticsearch в виде поля типа percolate в подходящем percolate-индексе.

  5. Возвращаются результаты об успешном или неуспешном индексировании сущности SavedSearch. В случае возникновения ошибки события SavedSearch отправляются в очередь DLQ (Dead Letter Queue). Эти данные можно использовать для решения возникающих проблем. Например — таких, как те, что появляются при удалении из индекса поля, упомянутого в поисковом запросе.

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

https://miro.medium.com/v2/resize:fit:700/1*AnB07zkL_g4a30TUgAHGSA.jpeg
Далия и Форрест из сериала Baby Animal Cam

Предположим, в определение индекса вносится изменение, требующее нового мэппинга, например — при добавлении тега для фильмов с животными. В таком случае Graph Search создаёт новую версию индекса Elasticsearch и добавляет новый конвейер для заполнения этого индекса. Этот новый конвейер читает данные из сжатого топика Kafka в Data Mesh. Именно так мы можем реиндексировать всю совокупность данных, не запрашивая у источников данных повторную отправку всех старых событий. Новый конвейер и старый конвейер работают рядом друг с другом. Это происходит до тех пор, пока новый конвейер не обработает бэклог событий. После этого Graph Search переходит к соответствующей версии индекса, используя псевдонимы индексов Elasticsearch.

Создание нового индекса для документов означает ещё и необходимость создания нового percolate‑индекса для запросов, чтобы они обладали бы единообразным мэппингом индексов. Этот новый percolate‑индекс, кроме того, нужно заполнить при изменениях версий индексов. Именно поэтому конвейер работает так, как работает. Мы, разворачивая новый конвейер percolate‑индексирования, снова можем прибегнуть к сжатым топикам в Data Mesh для реиндексирования всех данных SavedSearch.

https://miro.medium.com/v2/resize:fit:700/1*bAxTCeTOeZ_g4ueiN0cLiQ.png
Мы сохраняем в базе данных предоставленный пользователем DSL-фильтр вместо того, чтобы немедленно переводить его на язык запросов Elasticsearch. Это позволяет нам вносить изменения или исправления при переводе сохранённых DSL-текстов в запросы Elasticsearch. Мы можем развернуть эти изменения, создавая новую версию индекса, так как в ходе этого процесса все сохранённые поисковые запросы будут повторно переведены на язык запросов Elasticsearch.

Другие варианты использования обратного поиска

Мы надеялись, что возможности по обратному поиску в нашем графе, в итоге, найдут применение и в других инженерных командах Netflix. И к нам почти сразу же обратились с проблемой, которую может решить обратный поиск.

Подходы к съёмке фильма могут сильно различаться в зависимости от того, что это за фильм. Один фильм может проходить через некую последовательность шагов, что к другим фильмам неприменимо. Или — некий фильм может нуждаться в планировании каких‑то событий, которые при работе над другим фильмом не нужны. Вместо того, чтобы вручную настраивать рабочий процесс фильма, основываясь на его классификации, у нас должна быть возможность задавать методы классификации фильмов и возможность использовать эти методы для автоматической привязки фильмов к рабочим процессам. Но классификация фильмов — задача непростая. Можно классифицировать фильмы только по их жанрам. Например — это может быть «Экшн» или «Комедия». Но для практического использования, скорее всего, понадобится более сложная классификация. Возможно, она будет определяться жанром, регионом, форматом, языком, или некоей хитрой комбинацией всех этих свойств. Сервис Movie Matching даёт нам способ классификации фильмов на основании любой комбинации подходящих критериев. Внутри этого сервиса подходящие критерии хранятся в виде сущностей ReverseSearch. Для определения того, каким критериям соответствует фильм, документ фильма отправляют в конечную точку, занимающуюся обратным поиском.

Короче говоря — обратный поиск позволяет работать внешнему средству сопоставления критериев. Сейчас это средство используется для работы с критериями фильмов, но, так как любой индекс в Graph Search теперь поддерживает обратный поиск, этот подход можно использовать при работе с любым индексом.

Размышления о будущих применениях обратного поиска: подписки

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

О, а приходите к нам работать? ? ?

Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.

Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.

Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.

Присоединяйтесь к нашей команде

Комментарии (0)