Создание микрослужбы для индексации больших объемов контента на лету и получения точных результатов

Сказать, что у Course Hero большая библиотека контента, было бы преуменьшением. Course Hero содержит сотни миллионов документов, вопросов и других учебных материалов. Пользователи находят этот контент различными способами, например, в поисковых системах, таких как Google.

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

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

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

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

Мы оценили такие решения, как ElasticSearch, AWS Open Search и Algolia. Все они предназначены для поиска по миллионам документов, но не для поиска в отдельном документе. Они также требуют, чтобы мы предварительно индексировали всю нашу библиотеку контента, а это затраты, которые мы не хотим платить вперед, особенно не зная, найдут ли наши пользователи достаточную ценность, чтобы оправдать это.

Нам нужен сервис, который может принимать пользовательский запрос для данного документа, загружать индекс для этого документа (или индексировать документ на лету) и возвращать результаты, и все это в течение секунды или двух.

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

Во время нашей первоначальной проверки концепции мы определили, что сможем загружать, индексировать и возвращать соответствующие результаты в течение 1–2 секунд (p95) для ~ 90% нашей библиотеки документов. Это означает, что нам нужно предварительно проиндексировать только самые большие 10% наших документов, что сэкономит много времени и денег.

Архитектура службы выглядит следующим образом:

У нас есть серверные рабочие процессы AWS Simple Queue Service (SQS), которые получают уведомления каждый раз при загрузке документа. Эти рабочие процессы определяют, достаточно ли велик документ для его предварительной индексации, и если да, то начинают процесс.

Когда поступает запрос на определенный документ, наша служба documentsearch REST/gRPC проверяет, загружен ли уже этот индекс в память. Если это так, он сразу же подает результаты. Если нет, то он проверяет, существует ли индекс в S3, и если да, то скачивает и загружает индекс, снова возвращая результаты.

Если это новый документ, который никогда не индексировался, то служба загрузит исходный документ, загрузит его в индекс и обработает результаты, одновременно загружая этот индекс в S3, чтобы его можно было использовать в будущее.

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

Наконец, мы запускаем наш сервис внутри Kubernetes, используя Istio в качестве нашей сервисной сетки. Istio предоставляет встроенную поддержку закрепления, когда мы можем направлять запросы на один и тот же документ в один и тот же экземпляр нашего сервиса. При поиске с вводом текста мы ожидаем, что быстрые запросы к одному и тому же документу будут поступать каждые несколько секунд по мере того, как пользователь будет вводить текст. Первоначальный результат может составлять 1–2 секунды, но все последующие запросы должны обслуживаться в течение нескольких мс, поскольку эти запросы попадают в службу, индекс которой уже находится в памяти.

Благодаря этой архитектуре мы можем обслуживать результаты поиска с индексацией «на лету» для 90% нашей библиотеки и предварительно индексировать все, что может быть слишком большим, обеспечивая нашим пользователям быстрый поиск.

Если решение сложных технических задач, подобных этой, вас интересует, мы набираем!