Укрощение строптивого: Как мы научили WordPress искать быстро и точно

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

Тёмные времена LIKE

История плагина началась в те далёкие времена, когда Wordpress ещё не появился. В то время у меня была небольшая команда и мы клепали сайты на заказ. В качестве бэкенда использовался Plain PHP и куча наработанных библиотек собственного производства, из которых собирались сайты для клиентов. В принципе, почти весь функционал удавалось сделать довольно быстро, по шаблону, но самую большую попаболь всегда вызывал поиск, особенно полнотекстовый. Данные в проектах клиентов разнились по структуре, по количеству и названию полей в таблицах и поэтому поиск по данным - это всегда была кастомная история на одну-пару недель как минимум.

Пример поиска строки в сериализованных данных с помощью LIKE o_O
Пример поиска строки в сериализованных данных с помощью LIKE o_O

Если вы немного знакомы с тем, как работает MySQL, то наверняка знаете, что полнотекстовый поиск в MySQL - это проблема. На самом деле есть оператор LIKE, который ищет подстроки, но работает он ОЧЕНЬ медленно. Позднее появилась продвинутая версия - MATCH....AGAINST, использующая специальный тип индекса - FULLTEXT index. Оно работает намного лучше и быстрее, но без специальных "подкруток" конфигурации ищет только слова длиннее 3 символов, очень странно считает релевантность и всё ещё медленное для больших объёмов текста. Всё ещё больше усложняется, когда текстовые данные хранятся в БД не в чистом виде, а, например, в виде JSON или сериализованы в serialize(). Да, мы делали хитрые конструкции LIKE и для этого, но универсального способа не существовало.

Магия kwIndex

Однажды как лучик яркого солнца на нас свалилась бесплатная и небольшая (но хитроумная) библиотечка под названием kwIndex. Вот что она делала: она принимала на вход текстовые данные, разбивала их на отдельные слова и сохраняла результат в трёх таблицах БД (всё это называлось индекс): documents, words и vectors.

В таблице documents, собственно, были записаны ID документов, которые содержались в индексе. В таблице words - список абсолютно всех слов и всех вариаций, которые встретились во всех документах. А в таблице vectors было простое соответствие ID слов и ID документов.

Таким образом, чтобы найти документы по заданным словам, MySQL делал простые и быстрые операции: искал ID слов в таблице words, и уже по этим ID находил вектора и из векторов узнавал - в каких документах эти слова встречаются. Мы взяли эту библиотечку за основу, модернизировали её и стали активно использовать в наших проектах.

Это было похоже на магию: вместо того, чтобы городить огород из LIKE-конструкций, мы помещали все нужные нам тексты в индекс и потом запускали метод search(). Скорость поиска и качество по тем временам было феноменальным. Поиск по 10 Мбт самых разных текстов совершался за время, меньшее 1 секунды!

Переезд на WordPress

Современный вид страницы настроек плагина
Современный вид страницы настроек плагина

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

В один прекрасный день пришла очередь и нашей быстрой индексирующей библиотеки полнотекстового поиска.

К счастью, Wordpress устроен так, что практически все записи в нём размещаются в таблице wp_posts (можно назвать её "позвоночник"), а все остальные таблицы прикрепляются своими данными к ней и так или иначе связаны с какой-либо конкретной строкой в wp_posts. Например, в таблице wp_postmeta содержатся мета-данные постов, также есть таблицы для категорий, таксономий, свойств пользователей и прочее (наверняка вы всё это уже и так знаете).

И да, в Wordpress есть, конечно же, стандартный поиск, предоставленный мощным классом WP_Query(). В нём огромное множество параметров, но увы, полнотекстовый поиск в нем ограничен только поиском по заголовкам (post_title) и основному содержимому статьи (post_content). Реализовано это всё через тот же допотопный LIKE, и поэтому результаты поиска мягко говоря получаются не ахти :)

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

Рождение Плагина

Но у нас же есть наша продвинутая библиотека поиска! И мы адаптировали её для Wordpress. Причём сперва это была именно библиотека, только через год мы “догадались” переделать её, чтобы можно было просто устанавливать как плагин.

И вот как этот плагин WP Fast Total Search функционирует:

Во-первых: он прицепляется к хукам Wordpress "save_post" и нескольким другим, чтобы отслеживать момент изменения отдельных публикаций. По этому событию плагин собирает всю текстовую информацию из самого поста, из его мета-полей, из таксономий и отовсюду, откуда мы захотим (главное, чтобы информация относилась к посту) и помещает её в наш старый знакомый индекс в трёх таблицах (но уже прокачанный к тому времени). Таким образом, в индексе всегда хранится актуальная информация.

Во-вторых: плагин прицепляется к хукам "pre_get_posts" и некоторым другим, которые находятся внутри WP_Query, чтобы модифицировать результирующий MySQL запрос под себя, а именно - заменить простой и тупой LIKE на быстрый поиск по индексу. Таким образом, теперь при полнотекстовых запросах WP_Query обращается к нашему плагину, который очень быстро предоставляет список ID постов, в которых встречаются искомые слова или фразы, а кроме того, вычисляет корректную релевантность каждого. И поскольку не только ядро Wordpress, но и 99% всех тем и плагинов используют WP_Query для поиска, все они начинают использовать быстрый поиск нашего плагина!

Ну и в третьих: плагин перехватывает хук "get_excerpt" и заменяет обрезанный до 55 слов от начала контент публикации, на функциональный виджет, в котором выводятся именно те фразы из текста, в которых были встречены искомые слова (получается что-то наподобие вывода как в Google Search), а совсем недавно добавили возможность кликнуть по любой фразе, чтобы перейти на страницу публикации и эта фраза будет подсвечена в тексте. По-моему, это удобно.

Забавно, что только спустя 5 лет мы "догадались" выложить наш плагин в официальный репозиторий Wordpress, и это дало большой толчок к его развитию, поскольку пользователи стали задавать вопросы и предлагать различные улучшения - и мы всё это потихоньку внедряем.

Так выглядят результаты поиска на сайте одного из наших пользователей
Так выглядят результаты поиска на сайте одного из наших пользователей
Ещё пример результатов поиска в ру-сегменте
Ещё пример результатов поиска в ру-сегменте

Новое время и планы на будущее

Так за более чем 6 лет наш плагин "оброс" функционалом: например, сейчас можно настраивать "веса" различных частей публикаций, например, давать заголовку больший вес, чем тексту статьи. Каждый кусочек публикации (например, любое мета-поле) можно складывать в свой "кластер" индекса и задавать ему свой собственный вес. Это влияет на расчёт релевантности. Совсем недавно добавили возможность уменьшать релевантность для старых публикаций - дико полезная фича для новостных сайтов и сайтов объявлений.

Также добавили много настроек в индексирование - например, можно исключать из индекса системные записи или записи, находящиеся в "черновиках". Можно индексировать содержимое shortcode'ов, которые вставлены в текст статьи, теперь автоматически исключается нерелевантный “мусор” - JS скрипты, HTML комментарии и CSS сниппеты. Добавлена "убер" фича - извлечение текста из файлов, которые подключены к публикациям или же поиск непосредственно по Media файлам. При этом в результатах поиска появляются даже фразы из содержимого файлов.

"Умные" виджеты результатов поиска теперь тоже можно кастомизировать - настраивать CSS стили, цвета и всё такое.

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

Про сам алгоритм поиска тут рассказывать не буду - если интересно - подробно распишу в следующей статье. Тут отмечу только то, что это уже совсем не тот трёхтабличный индекс, с которого всё начиналось. Пришлось придумать массу ухищрений, чтобы сделать алгоритм быстрее и при этом не потерять в качестве. Добавили бонус релевантности для фраз (например, если в поиске два слова и более, то такие документы находятся в результатах выше), добавили промежуточное кэширование векторов - это увеличивает скорость поиска. В планах внедрение стемминга и логических операндов в поиск (AND и OR, скобок).

Сейчас плагин легко переваривает 300к публикаций среднего размера, при этом время поиска обычно не превышает 1-2 секунды. Но мы не останавливаемся. У нас есть пользователи с почти миллионом публикаций и они периодически бурчат, что поиск медленный, 4-5 секунд!. Это мотивирует нас искать новые алгоритмы - например, сейчас прорабатываем возможность использовать Apache Solr или Elasticsearch как индексирующий движок для плагина - причём это будет дополнение “из коробки” с быстрой настройкой.

Недостатки

Ну и я не написал о недостатках.

Во-первых, плагин совершенно точно может конфликтовать с другими плагинами и темами - таких ОЧЕНЬ немного уже, но встречаются.

Во-вторых, размер индексных таблиц иногда кошмарит пользователей, потому что если у вас 30 000 статей и в каждой 2 000 слов, можете сами посчитать количество векторов (60 миллионов). Пользователи заходят в phpMyAdmin, смотрят на это и начинают хвататься за голову. Но ведь MySQL как раз и сделан для того, чтобы работать с большими таблицами и это никак не мешает. И даже если таблица на диске занимает несколько гигабайт, это не проблема (если только у вас не совсем дешёвый шаред хостинг с ограничением на размер БД).

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

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

Подытожим

Кого заинтересовала тема полнотекстового поиска в Wordpress - пишите, обсудим и прокомментируем. Также я планирую написать ещё 2-3 статьи на тему этого плагина. Напишите в комментариях - какие моменты вы хотели бы узнать поподробнее.

А вот с маркетингом у нас не очень хорошо, поэтому мы делаем ставку на партнёрскую программу. В прошлом году выплатили аж 6000 USD. Но это всё не касается бесплатной версии плагина, о которой я тут пишу - она доступна всегда, для всех. Платную версию покупают только те, кто хотел бы получить уберфичу поиска по файлам, ну и личную техническую консультацию по настройке поиска. Хотя в принципе платную версию можно получить и бесплатно - можно скачать оценочную версию на 7 дней и через 7 дней скачать ещё одну и повторять до бесконечности :) В комплекте там тот же самый Pro плагин. Есть у нас три чудака - клиента, которым не лень делать это каждую неделю :)

Спасибо, что дочитали и хорошего вам дня!

22
Начать дискуссию