Вместо интро
Сегодня мы расскажем о нашем опыте создания своего поиска для репозиториев в GitHub, почти похожее на то, которое там и используется, но немного другое.
Для затравки - посмотрите, что у нас получилось на нашем репозитории -> Демка
Можно проиндексировать и свой, для этого нужно немного подождать завершения процесса индексации -> https://github.manticoresearch.com/, а пока идет индексация - добро пожаловать под кат.
Диспозиция
Для того, чтобы показать все классные решения, которые делают Manticore Search, на наш взгляд, лучшим решением для полнотекстового поиска, осталось выбрать применение на практике, на котором, собственно, и будем показывать - вариантов много, и сразу на ум приходит примерно такой список: интернет-магазины, блоги, новостные агрегаторы, базы данных книг, фильмов и прочего человеческого достояния (варианты таких применений можно поискать у нас в блоге).
Посовещавшись между собой, пришли к интересному выводу - таких примеров масса, и объяснять на них очень просто и удобно, но полностью они не раскрывают весь потенциал нашей системы, которая, в свою очередь, полностью покрывает потребности вышеперечисленных применений. А вот поиск в репозитории? А вот поиск в нашем репозитории был бы интересным, в первую очередь нам самим, ведь некоторые пожелания к удобству UX стандартного GitHub’а уже накопились. Вызов был принят и вот результат - не простая искусственная демка высосанная из пальца, а удобный, и самое главное - быстрый инструмент для полнотекстового поиска в репозиториях, задачах, комментариях и прочих прелестях на GitHub с пиковой скоростью ответа, порой, в 30 раз превышающей исходный поисковик. Ну и в процессе внедрения мы, естественно, заботливо все задокументировали.
Поехали
Концепт проекта прост как и все гениальное - грузим выбранные репозитории из Гитхаба в базу Manticore и включаем на полную катушку индексацию для работы полнотекстового поиска. Для не сильного контраста с оригиналом мы мимикрировали наш UI под дизайн неповторимого оригинала, но все же некоторый свой UX мы туда включили:
- опции фильтрации (у нас их просто больше)
- добавили сортировки (опять же, у нас то они есть и много)
- ну и вишенка на торте - беcшовная выдача результата на странице (пагинация прокрутки)
Маленький, но гордый MVP
Разработка демо может показаться сложной задачей, особенно когда запас времени не безграничен. Мы оперлись на проверенное сочетание PHP для Backend и JavaScript для Frontend — с добавлением немного SEO-дружественной гибридной магии. Почему PHP? Потому что наша команда имеет больше опыта в PHP, чем в других языках программирования, хотя то же самое можно реализовать и на любом другом, для которого у Manticore есть клиент: PHP, Python, JavaScript, Typescript, Java, Elixir, Go ну и через mysql или JSON интерфейс естественно. В случае с PHP инициализация таблицы, поиск и вывод результатов выглядят так:
<?php
use Manticoresearch\\Client;
$client = new Client(['host' => 'localhost', 'port' => 9308]);
$index = $client->index('repo');
$docs = $index->search('bug')->get();
foreach ($docs as $doc) {
var_dump($doc->getId(), $doc->getData());
}
Т.е. вы создаёте клиент Manticore, выбираете нужную таблицу, отправляете запрос на поиск и, вуаля, результаты уже у вас. Подробно обсуждать клиент Manticore Search для PHP мы здесь не будем, но если хотите с ним познакомиться поближе, вот ссылка на репозиторий Manticore Search PHP Client.
Кстати, тут лежит гайд, как вы можете написать свой плагин PHP для Manticore Search, написанном на C++
Итак, на данный момент MVP уже имеет в себе некоторый функционал:
- Извлекает данные из GitHub.
- Поддерживает очередь репозиториев для обработки.
- Может отправлять уведомления по электронной почте.
Для удобства всё взаимодействие с Manticore Search сосредоточено в одном файле — Manticore.php. Это также может быть полезным для тех, кто изучает различные системы управления данными и рассматривает их сравнение в перспективе.
Сучки и задоринки, за которые мы зацепились
Работая над нашим демо, помимо реализации упомянутых выше тривиальных вещей, мы столкнулись с несколькими неожиданными для нас интересными задачами, которые вы тоже можете встретить в своих проектах.
Релевантность результатов поиска при комбинировании двух таблиц
Ключевым аспектом любой поисковой системы является релевантность её результатов, а одна из наших сильных сторон - возможность эффективно управлять релевантностью результатов прямо “из коробки”. Система использует классические методы ранжирования на основе алгоритма BM25, которые упорядочивают результаты по частоте и значимости ключевых слов в документах и запросах, учитывая также нормализацию длины текстового поля, в котором найдено совпадение. Это позволяет начать работу без необходимости настройки сложных конфигураций или разработки собственных алгоритмов. Подробнее можно прочитать у нас в документации.
При написании этого примера мы столкнулись с проблемой организации комбинированного поиска по задачам и комментариям на GitHub. Исходно мы разделили данные на две отдельные таблицы в Manticore: одну для задач, другую для комментариев. После изучения механизмов ранжирования мы решили реализовать алгоритм Rank-Biased Precision (RBP), который позволяет нам эффективно объединять результаты из этих двух разных источников.
Manticore Search предоставляет поле «score», которое можно получить с помощью $doc→getScore()
из клиента PHP и использовать в качестве показателя веса для дальнейшего комбинирования. Код здесь.
В итоге мы не только обеспечиваем высокую релевантность результатов в режиме Plug-n-Play, но и используем RBP для объединения данных из двух источников, что значительно повышает эффективность поиска!
Отображение диапазонов
В сфере поисковых технологий базовый поиск часто не удовлетворяет всем потребностям пользователей, в частности если требуется использовать фильтры для уточнения результатов. В Manticore Search, как и в других поисковых системах, реализация простых фильтров, например - по диапазону или равенству, выполняется просто. Однако уже группировка результатов по условным диапазонам для других систем уже задача из разряда не тривиальных, то в случае с Manticore Search мы постарались сделать этот процесс более удобным.
Сейчас задача состоит в том, чтобы позволить пользователям выбирать предопределённые диапазоны и применять соответствующие фильтры без необходимости хранения или кэширования дополнительных данных. Например, если нужно отфильтровать задачи по количеству комментариев: ≤ 5, между 5 и 10 и ≥ 10. Manticore Search упрощает этот процесс с помощью функции INTERVAL()
. Мы в демо разработали метод, который генерирует желаемые диапазоны вместе с подсчетом элементов, попадающих в каждый диапазон.
Лучше один раз увидеть, чем сто раз услышать - псевдокод для лучшего понимания:
$client = static::client();
$index = $client->index('issue');
$search = $index->search('');
$range = implode(',', $values);
$facets = $search
->limit(0)
->filter('repo_id', $repoId)
->expression('range', "INTERVAL(comments, $range)")
->facet('range', 'counters', sizeof($values) + 1)
->get()
->getFacets();
Полный же код можно посмотреть тут.
Применение фильтров
Следующий шаг включает в себя фильтрацию результатов. Это достигается с помощью использования фильтра gt
(больше, чем) в сочетании с условием or
. Ниже приведено упрощенное представление кода:
$search->filter('comments', 'gt', 0, Search::FILTER_AND);
$search->filter('comments', 'lte', 3, Search::FILTER_OR);
Полный код можно посмотреть здесь.
Сортировка по реакциям
При использовании поиска на GitHub вы можете заметить, что платформа не предоставляет возможности отображения и фильтрации по реакциям пользователей. В свою очередь, нам в некоторых случаях определение задач с наибольшим количеством реакций может быть крайне полезным, ведь таким способом легко выявить наиболее востребованные функции, которые стоит добавить в проект. В таких ситуациях возможность сортировки по реакциям сложно недооценить.
Первым шагом в этом процессе будет сохранение данных о реакциях. API GitHub предоставляет эту информацию в удобном формате JSON-объекта:
{
"url": "https://api.github.com/repos/ClickHouse/ClickHouse/issues/35407/reactions",
"total_count": 0,
"+1": 0,
"-1": 0,
"laugh": 0,
"hooray": 0,
"confused": 0,
"heart": 0,
"rocket": 0,
"eyes": 0
}
Прекрасно, это то, что нам замечательно подходит! В арсенале Manticore есть соответствующий тип данных для JSON. Следовательно, мы можем без проблем использовать полученные данные без дополнительных манипуляций с ними. Теперь определимся с сортировкой, будем ли мы сортировать по отдельным полям или возьмем сумму значений нескольких? Можно и не выбирать - Manticore Search тут обоюдоострый и оба варианта нам доступны. Будем хранить JSON прямо так, и используем следующий код для активации сортировки:
$search->expression(
'positive_reactions',
'integer(reactions.`+1`) + integer(reactions.hooray) + integer(reactions.heart) + integer(reactions.rocket)'
);
Полную реализацию сортировки вы можете найти в исходных кодах, доступных здесь.
Как видно из кода, мы используем функцию expression()
клиента PHP Manticore Search для доступа к полям JSON посредством нотации точки. Этот метод позволяет избежать необходимости кэшировать счетчики или выполнять дополнительные расчёты. Создав поле JSON и обратившись к нему через выражения, вы сможете поддерживать высокую скорость обработки данных и минимизировать накладные расходы, связанные с механизмами кэширования.
Фасетный поиск
Функции поиска и фильтрации являются ключевыми элементами любой продвинутой поисковой системы. Однако часто возникают сложности с получением актуальных счетчиков. В системах на основе MySQL, например, быстрые операции подсчета обычно требуют использования индексов, которые могут значительно увеличить размер базы данных и усложнить работу в условиях высоких нагрузок. Решением становится кэширование и последующая корректировка счетчиков.
В Manticore нам не нужно об этом думать и мы можем получить счетчики быстро и просто без необходимости добавлять дополнительный слой кэширования и усложнять архитектуру приложения.
Чтобы отобразить счетчики на странице фильтров, мы используем те же самые фильтры, что и для поиска. Мы добавляем лишь дополнительный запрос для фасетов, который выполняется всего за несколько миллисекунд. Такой подход позволяет получать точные и актуальные данные по группам практически без задержек и дополнительных затрат. Ниже представлен краткий фрагмент кода на PHP, демонстрирующий, как это реализовать:
$facets = $search
->limit(0) // Мы заинтересованы только в подсчетах, поэтому результаты не нужны
->filter('repo_id', $repoId) // Фильтруем по идентификатору репозитория
->expression('open', 'if(closed_at=0,1,0)') // В open будет, открыта ли задача
->facet('open', 'counters', 2) // Получаем данные о количестве открытых и закрытых задач
->get() // Выполняем поисковый запрос и получаем результаты
->getFacets(); // Извлекаем данные о фасетах из результатов
Для заинтересованных в полной реализации, полный фрагмент кода доступен на GitHub.
Рассмотрим процесс более подробно: мы устанавливаем лимит равным нулю, потому что наша основная цель — получение счетчиков, а не сами результаты поиска. Затем мы фильтруем данные по ID репозитория и применяем условие для группировки по полю if(closed_at=0,1,0)
. Таким образом такая группировка предоставляет нам счетчики как для открытых, так и для закрытых задач. Как видим в Manticore Search задача получения подсчетов решается практически “из коробки” в пять строк кода. Эффективно (эффектно), не правда ли?
Вместо заключения
Делая демо для демонстрации функциональности Manticore Search мы немного увлеклись и реализовали полноценный инструмент для нашей собственной работы, позволяющий нам быстрее и точнее реагировать на события в нашем репозитории, комментарии пользователей и их реакции. Помимо прямого использования собственного решения мы еще и нашли пару доработок для еще более удобного и качественного применения нашего движка и базы. Краткий список приятных результатов:
- Поиск теперь занимает 5-10 мс, против более 200 у оригинала на GitHub.
- Комментарии также в поиске, найти потерянный теперь дело одного запроса.
- Задачи имеют свой псевдо-рейтинг на базе реакций пользователей, который легко применяется в сортировке результата.
- Фильтрация в коментариях кроме поиска в них теперь дает и возможнсть выборки по заданным условиям.
Для полного погружения в код - добро пожаловать на наш гит, детали, как утащить к себе: https://github.com/manticoresoftware/manticore-github-issue-search.
В данный момент в демо уже реализован векторный поиск на основе ML модели, и весь код уже доступен в репозитории, но это тема отдельной статьи…
Следите за обновлениями тут, в GitHub и подписывайтесь на нас в Twitter.
Мы будем рады увидеть ваши отзывы и предложения по улучшению проекта в разделах issues и обсуждениях на GitHub.
До встречи!