Суть проблемы
Раньше я работала на проекте N, где главной бизнесовой сущностью было событие. Это событие имеет свое название и еще несколько полей. У нас был реализован поиск по всем событиям, который представлял из себя обычный iLIKE в PostgreSQL.
Когда-то нам пришел запрос от юзеров: событие у нас в системе называется, например, "событие от Ивана Ивановича", а они пытаются вбить в поиск "иван иванович рассказал про X" и не получают никаких результатов.
Данная проблема решается с помощью полнотекстового поиска. Вопрос в том, как его реализовать.
Выбор технологии
Мы хотели сделать все быстро, безболезненно, с минимумом изменений. Для этого рассматривали либо Lucene, либо ElasticSearch. Сравним их:
ElasticSearch:
+ хорошо зарекомендовавшее себя решение. 9/10 проектов, где есть full text search, используют именно его
+ огромное количество фичей и инструментов, которые к тому же отлично документированы
- необходимость поднимать отдельный сервис и обслуживать его
- в последствии необходимость менять код и на бэкэнде, и на фронтэнде
Lucene:
+ не надо поднимать новый сервис и заниматься его обслуживанием
+ не надо менять код на фронтэнде, достаточно просто держать логику полнотекстового поиска на бэкэнде в том сервисе, где у нас реализован поиск по событиям
- у нас было много смежных проектов внутри компании, которые тоже работали с событиями
Именно последний минус и привел нас к тому, что мы выбрали в итоге ElasticSearch. Если бы мы взяли Lucene, нам бы пришлось делать свой собственный сервис по типу ElasticSearch поверх Lucene, чтобы с ним могли общаться другие сервисы в нашей компании. В случае с монолитной архитектурой, думаю, мы бы выбрали Lucene.
После того, как технология была выбрана, мы начали ее итерационно внедрять.
Первая итерация
Собственно, это настройка ElasticSearch - то, что легло на плечи наших DevOps-инженеров, а потом мы должны были написать код, который заставит наш бэкэнд работать с эластиком.
Мы написали модуль, который коннектится к эластику, создает в нем необходимые нам индексы, если они еще не созданы, а потом заполняет их. Заполняли мы их событиями и связанными с ними сущностями - под каждую сущность делали по два индекса (stage и prod).
Сначала у нас была простыня кода под маппинги - у наших сущностей было очень много информации, которую мы хотели хранить в эластике, огромная куча полей. Это привело к тому, что под каждую сущность мы создавали отдельный модуль, в котором лежал маппинг. В итоге, когда нам нужно было добавить новое поле в какую-то таблицу, нам приходилось добавлять его в двух местах - собственно, в саму модель, а потом отдельно в маппинг.
Для того, чтобы решить эту проблему, я написала библиотеку elasticmapper, которая автоматически по модели ORM (в текущей реализации поддерживаются Peewee, DjangoORM, SQLAlchemy, но в следующих версиях планируется расширить этот список) генерирует маппинг для ElasticSearch. Благодаря ей новое поле нужно было добавить только в модель, дальше происходила автоматическая генерация маппинга и код дублировать больше было не нужно - разве что в каких-то редких кейсах, где нужна была более "тонкая" настройка с помощью, например, custom_values или keyword_fields.
Под конец первого двухнедельного релиза у нас было все готово, чтобы начать переписывать наш поиск.
Вторая итерация
Итак, у нас было множество эндпоинтов под сами события и связанные с ними сущности, которые мы также хотели хранить в ElasticSearch. На начальных этапах мы хотели, чтобы фронтэнд ничего не знал про существование ElasticSearch, следовательно ничего не менял у себя в коде.
Для этого мы сделали так, чтобы эти эндпоинты самостоятельно ходили в эластик, а потом просто возвращали на фронт данные из него. То есть мы убрали все походы в PostgreSQL и убрали iLIKE, заменив их на запросы к ElasticSearch.
Фронт по-прежнему ничего не знал об этой подмене, но у наших юзеров уже был видимый результат - теперь они могли вбить "иван иванович рассказывает про Х", "выступление ивановича" и любой другой запрос. В любом случае они получали релевантные результаты поиска, а не 404, как раньше.
Также у нас возникла следующая проблема - переводы. В нашей системе на тот момент уже была поддержка более десяти языков. Надо было сделать так, чтобы другие локализации событий также отдавались через ElasticSearch.
Придумали два способа:
1) Добавить в существующие индексы кучу полей с переводами. Например, было просто field, а стало бы field_ru, field_en, field_fr и т.д.
2) Один язык = один индекс. Существующие индексы остались бы нетронутыми, зато мы наделали бы кучу похожих индексов, но каждый под свою локаль
В первом случае у нас получилось бы огромное количество полей. Я упоминала, что у нас и так их было изначально немало, а в такой реализации их количество увеличилось бы в 10+ раз. Это повлекло бы следующие проблемы:
- более медленный поиск (поиск по маленькому датасету работает быстрее)
- если нам понадобится сделать что-то специфичное для одного или нескольких языков, например, добавить какое-то поле, то весь индекс придется реиндексировать. В случае с разными индексами под разные языки этого можно было бы избежать
- плохое масштабирование. Гораздо проще масштабироваться, если вы имеете возможность закинуть индекс с французской локализацией на один сервер, а с английской на другой
Учитывая все это, мы воспользовались вторым способом.
Под конец второго спринта наши пользователи получили решение для своей проблемы, с которой изначально пришли к нам.
Третья итерация
Мы передали фронтэндерам все запросы для всех эндпоинтов, которые использовали на бэкэнде для общения с ElasticSearch. Удалили лишние эндпоинты, а фронтэндеры начали напрямую общаться с эластиком. Это был завершающий этап внедрения полнотекстового поиска.
upd: как указали в комментариях, ходить из фронта напрямую в эластик может быть небезопасно. В нашем случае на всех индексах стоит read-only флаг, а в самих индексах нет никакой чувствительной информации, которая могла бы быть интересна для злоумышленников. Если у вас мутабельные и/или чувствительные данные хранятся в эластике, то, конечно же, вам нужна прослойка в виде бэкэнда.
Заключение
Здесь был описан кейс для конкретного проекта. Вероятно, в вашем случае такое решение может не подойти вовсе, либо подойти частично - it depends. Буду рада, если кому-то поможет мой опыт внедрения ElasticSearch в проект. Спасибо за внимание!