Search
Write a publication
Pull to refresh

Подсветка текста в html-документе с помощью Nokogiri

Хочу поделиться небольшим расширением для Nokogiri — библиотекой Hairaito для подстветки текста в html-документе.

Тривиальный пример использования:

doc = Nokogiri::XML('<body>abc def ghi</body>')
doc.highlight(['def'])
doc.to_html # => '<body>abc <span class="snippet-part snippet-start" data-snippet-id="0">def</span> ghi</body>'

Под катом описание изначальной проблемы, поиск её решения и ссылки по теме.

Предыстория


На проекте возникла необходимость реализации поиска по тексту html-документа с настройками морфологии. Исторически сложилось, что для этих целей используется Sphinx. Для связки с ruby существует гем Thinking Sphinx. Он поддерживает функцию подстветки результатов поиска BuildExcerpts через объект ThinkingSphinx::Excerpter.

Судя по документации, для подстветки в режиме полного текста используется параметр html_strip_mode: :retain, он сохраняет исходную html структуру. При этом, как оказалось, теряется функционал поиска с параметром html_strip: true из преднастроенного индекса.

Поясню на примере. Есть фрагмент документа:

<div>
Мама <i>мыла</i> раму
</div>

Если мы хотим искать по фразе целиком «Мама мыла раму» и используем html_strip_mode: :index для подстветки в тексте, то Sphinx удаляет inline-тег i, оставляя толькое его содержимое (документация), и подсвечивает весь сниппет. При этом теряется оригинальное выделение слова «мыла» курсивом.

Если же мы используем html_strip_mode: :retain, то Sphinx ведет себя иначе — не удаляет inline-теги при поиске. При этом он уже не находит фразу целиком. Т.е. имеет место ложно отрицательный результат поиска. Оба варианта посчитали неподходящими.

Поиск решения


Просмотрев документацию Sphinx и ThinkingSphinx, я не нашел решения этой проблемы «из коробки». Вопрос на официальном форуме поискового движка так же не дал результатов.

Далее возникла следующая идея. Можно использовать ThinkingSphinx::Excerpter с параметром html_strip_mode: :index для получения полного списка найденных сниппетов с учетом всех настроек морфологии в индексе. После этого нужно найти в оригинальном документе все вхождения каждого сниппета и выделить их, но уже не средствами Sphinx.

У этого подхода сразу есть очевидный минус — это медленнее нежели бы это сделал сам Sphinx (по наблюдениям, сравнительных тестов не проводил). Однако других рабочих вариантов за короткий срок я не увидел.

Оставалось решить как именно подсвечивать найденные сниппеты. Рассмотрел варианты сделать это на фронте — нагуглил библиотеки jquery-highlight и rangy. Первая не подошла по функционалу, т.к. не умеет подсветку слов «разделенных» inline-тегами. Вторая довольно успешно справилась непосредственно с «качеством» подсветки (метод findText, см. документацию). Однако при росте количества сниппетов время выполнения сильно увеличивалось, браузер начинал подвисать, полностью отжирая одно ядро. Вынести в Web Workers не получилось, т.к. взаимодействие с DOM.

Результат


Итогом стало решение делать подсветку на бэке. Беглый поиск не дал результатов — готовых решений найдено не было. Поэтому я решил написать собственное расширение для Nokogiri. Гем назвал «Hairaito», что в переводе с японского на английский дает «highlight». На русский переводится как «основной момент». Искренне надеюсь, что знатоки японского не вспомнят какой-нибудь неблагозвучный вариант перевода этого слова.

Hairaito умеет подсвечивать текст в html/xml, есть возможность настройки тегов и css-классов для подстветки, нумерации результатов.

Код проекта и примеры использования доступны на github.
Также гем опубликован на rubygems.

Буду рад услышать комментарии. Возможно, кто-нибудь знает более элегантное решение начальной проблемы.

Ссылки по теме:

Sphinx Search
Thinking Sphinx
Nokogiri
jquery-highlight
rangy
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.