Russian (Pусский) translation by Ilya Nikov (you can also view the original English article)
В этой статье я расскажу вам, как реализовать полнотекстовый поиск с использованием Ruby on Rails и Elasticsearch. В настоящее когда вы вводите поисковую фразу, то вы сразу же получаете как результаты поиска, так и предложения по поиску. Если вы опечатались при поиске, то автоматическая корректировка также является хорошей функцией, как мы можем видеть на таких сайтах, как Google или Facebook.
Для реализации всех этих функций использование только реляционной базы данных, такой как MySQL или Postgres, не достаточно. По этой причине мы используем Elasticsearch, который вы можете рассматривать как базу данных, специально созданную и оптимизированную для поиска. Это решение с открытым исходным кодом, и оно построено поверх Apache Lucene.
Одна из самых приятных особенностей Elasticsearch заключается в том, что она предоставляет свои функции с использованием REST API, поэтому есть библиотеки, которые обрачивают эту функциональность для большинства языков программирования.
Представляем Elasticsearch
Ранее я упомянул, что Elasticsearch похож на базу данных для поиска. Было бы полезно, если бы вы были знакомы с некоторой терминологией вокруг него.
- Field: Поле похоже на пару «ключ-значение». Значение может быть простым значением (string, integer, date) или вложенной структурой, например массивом или объектом. Поле похоже на столбец таблицы в реляционной базе данных.
- Document: документ представляет собой список полей. Это документ JSON, который хранится в Elasticsearch. Это похоже на строку в таблице в реляционной базе данных. Каждый документ хранится в индексе и имеет тип и уникальный идентификатор.
- Type: Тип подобен таблице в реляционной базе данных. Каждый тип имеет список полей, которые могут быть указаны для документов этого типа.
- Index: индекс является эквивалентом реляционной базы данных. Он содержит определение для нескольких типов и хранит несколько документов.
Здесь следует отметить, что в Elasticsearch, когда вы пишете документ в индекс, поля документа анализируются по слову, чтобы сделать поиск простым и быстрым. Elasticsearch также поддерживает геолокацию, поэтому вы можете искать документы, расположенные на определенном расстоянии от определенного места. Именно так Foursquare реализует поиск.
Я хотел бы упомянуть, что Elasticsearch был построен с высокой масштабируемостью, поэтому очень легко создать кластер с несколькими серверами и иметь высокую доступность, даже если некоторые серверы отключаются. Я не собираюсь описывать особенности планирования и развертывания различных типов кластеров в этой статье.
Установка Elasticsearch
Если вы используете Linux, возможно, вы можете установить Elasticsearch из одного из репозиториев. Он доступен в APT и YUM.
Если вы используете Mac, вы можете установить его с помощью Homebrew: brew install elasticsearch
. После установки elasticsearch вы увидите список соответствующих папок в вашем терминале:

Чтобы убедиться, что установка работает, введите текст elasticsearch
в своем терминале, чтобы запустить его. Затем выполните curl localhost: 9200
в вашем терминале, и вы увидите что-то вроде:

Установка Elastic HQ
Elastic HQ - это плагин мониторинга, который мы можем использовать для управления Elasticsearch в браузере, подобно phpMyAdmin для MySQL. Чтобы установить его, просто запустите в своем терминале:
/usr/local/Cellar/elasticsearch/2.2.0_1/libexec/bin/plugin -install royrusso / elasticsearch-HQ
После его установки перейдите в http://localhost:9200/_plugin/hq в своем браузере:

Нажмите Connect, и вы увидите экран, показывающий состояние кластера:

На данный момент, как и следовало ожидать, пока не созданы индексы или документы, но у нас есть наш локальный экземпляр Elasticsearch, установленный и запущенный.
Создание приложения Rails
Я собираюсь создать очень простое приложение Rails, где вы можете добавлять статьи в базу данных, чтобы мы могли выполнять полнотекстовый поиск по ним с помощью Elasticsearch. Начните с создания нового приложения Rails:
rails new elasticsearch-rails
Затем мы создаем новый ресурс статьи:
rails generate scaffold Article title:string text:text
Теперь нам нужно добавить новый корневой маршрут, поэтому мы можем по умолчанию просмотреть список статей. Изменим config/routes.rb:
Rails.application.routes.draw do root to: 'articles#index' resources :articles end
Создайте базу данных, выполнив команду rake db: migrate
. Если вы запустите rails server
, откройте свой браузер, перейдите на localhost:3000 и добавьте несколько статей в базу данных или просто загрузите файл db/seeds.rb с фиктивными данными, которые я создал, поэтому вам не нужно тратить много времени на заполнение форм.
Добавление поиска
Теперь, когда у нас есть небольшое приложение Rails со статьями в базе данных, мы готовы добавить наши функции поиска. Мы собираемся начать с добавления ссылки на официальные Elasticsearch Gems:
gem 'elasticsearch-model' gem 'elasticsearch-rails'
На многих веб-сайтах очень часто есть текстовое поле для поиска в верхнем меню на всех страницах. По этой причине я собираюсь создать форму в app/views/search/_form.html.erb. Как вы можете видеть, я отправляю форму с помощью GET, поэтому легко скопировать и вставить URL для определенного поиска.
<%= form_for :term, url: search_path, method: :get do |form| %> <p> <%= text_field_tag :term, params[:term] %> <%= submit_tag "Search", name: nil %> </p> <% end %>
Добавьте ссылку на форму на основной макет сайта. Изменим app/views/layouts/application.html.erb.
<body> <%= render 'search/form' %> <%= yield %> </body>
Теперь нам также нужен контроллер для выполнения фактического поиска и отображения результатов, поэтому мы создаем его, запуская команду rails g new controller Search
.
class SearchController < ApplicationController def search if params[:term].nil? @articles = [] else @articles = Article.search params[:term] end end end
Как вы можете видеть, я вызываю метод search
в модели Article. Мы еще не определили его, поэтому, если мы попытаемся выполнить поиск в этот момент, мы получим ошибку. Кроме того, мы не добавили маршрут для SearchController в файле config/routes.rb, поэтому давайте сделаем это:
Rails.application.routes.draw do root to: 'articles#index' resources :articles get "search", to: "search#search" end
Если мы посмотрим на документацию для gem elasticsearch-rails, нам нужно включить два модуля в модели, которые мы хотим проиндексировать в Elasticsearch, в нашем случае Article.rb.
require 'elasticsearch/model' class Article < ActiveRecord::Base include Elasticsearch::Model include Elasticsearch::Model::Callbacks end
Первая модель вводит метод поиска, который мы использовали в нашем предыдущем контроллере среди других. Второй модуль интегрируется с обратными вызовами ActiveRecord для индексации каждого экземпляра статьи, которую мы сохраняем в базе данных, а также обновляет индекс, если мы модифицируем или удаляем статью из базы данных. Так что это все прозрачно для нас.
Если вы ранее импортировали данные в базу данных, эти статьи по-прежнему не входят в индекс Elasticsearch; только новые индексируются автоматически. По этой причине мы должны индексировать их вручную, и это легко сделать запустив rails console
. Затем нам нужно только запустить irb (main)> Article.import
.

Теперь мы готовы попробовать функцию поиска. Если я наберу «ruby» и нажму Поиск, то получу такие результаты:

Подсветка поиска
На многих веб-сайтах вы можете увидеть на странице результатов поиска, как подсвечивается термин, который вы искали. Это очень легко сделать, используя Elasticsearch.
Измените app/models/article.rb и измените метод поиска по умолчанию:
def self.search(query) __elasticsearch__.search( { query: { multi_match: { query: query, fields: ['title', 'text'] } }, highlight: { pre_tags: ['<em>'], post_tags: ['</em>'], fields: { title: {}, text: {} } } } ) end
По умолчанию метод search
определяется моделями «elasticsearch-models», а прокси-объект __elasticsearch__ предоставляется для доступа к классу-оболочке для API Elasticsearch. Таким образом, мы можем изменить запрос по умолчанию, используя стандартные параметры JSON, как указано в документации.
Теперь метод поиска будет обертывать результаты, соответствующие запросу с указанными тегами HTML. По этой причине нам также необходимо обновить страницу результатов поиска, чтобы мы могли безопасно отображать HTML-теги. Для этого отредактируйте app/views/search/search.html.erb.
<h1>Search Results</h1> <% if @articles %> <ul class="search_results"> <% @articles.each do |article| %> <li> <h3> <%= link_to article.try(:highlight).try(:title) ? article.highlight.title[0].html_safe : article.title, controller: "articles", action: "show", id: article._id %> </h3> <% if article.try(:highlight).try(:text) %> <% article.highlight.text.each do |snippet| %> <p><%= snippet.html_safe %>...</p> <% end %> <% end %> </li> <% end %> </ul> <% else %> <p>Your search did not match any documents.</p> <% end %>
Добавьте стиль CSS в app/assets/stylesheets/search.scss для нужного тега:
.search_results em { background-color: yellow; font-style: normal; font-weight: bold; }
Попробуйте снова поискать «ruby»:

Как вы можете видеть, легко выделить термин поиска, но не идеально, поскольку нам нужно отправить запрос JSON, как указано в документации Elasticsearch, и у нас нет какой-либо абстракции.
Searchkick Gem
Searchkick gem предоставляется Instacart, и это абстракция поверх официальных gem-ов Elasticsearch. Я собираюсь реорганизовать выделенную функциональность, поэтому мы начинаем с добавления gem 'searchkick'
в gemfile. Первым классом, который нам нужно изменить, является модель Article.rb:
class Article < ActiveRecord::Base searchkick end
Как вы можете видеть, это намного проще. Нам нужно снова переопределить статьи и выполнить команду rake searchkick: reindex CLASS = Article
. Чтобы выделить поисковый запрос, нам нужно передать дополнительный параметр методу поиска из нашего search_controller.rb.
class SearchController < ApplicationController def search if params[:term].nil? @articles = [] else term = params[:term] @articles = Article.search term, fields: [:text], highlight: true end end end
Последним файлом, который нам нужно изменить, является view/search/search.html.erb, поскольку результаты возвращаются в другом формате с помощью поиска:
<h2>Search Results for: <i><%= params[:term] %></i></h2> <% if @articles %> <ul class="search_results"> <% @articles.with_details.each do |article, details| %> <li> <h3> <%= link_to article.title, controller: "articles", action: "show", id: article.id %> </h3> <p><%= details[:highlight][:text].html_safe %>...</p> </li> <% end %> </ul> <% else %> <p>Your search did not match any documents.</p> <% end %>
Теперь пришло время снова запустить приложение и протестировать функцию поиска:

Обратите внимание, что я ввел поисковый запрос «dato». Я сделал это специально, чтобы показать вам, что по умолчанию searchkick настроен для анализа проиндексированного текста.
Автоматические подсказки
Autosuggest или typeahead предсказывает, что пользователь вводит, делая поиск быстрее и проще. Имейте в виду, что, если у вас нет тысяч записей, лучше всего фильтровать на стороне клиента.
Начнем с добавления плагина typeahead, который доступен через gem 'bootstrap-typeahead-rails'
и добавьте его в свой Gemfile. Затем нам нужно добавить JavaScript в app/assets/javascripts/application.js, чтобы при вводе текста в поле поиска появилось несколько предложений.
//= require jquery //= require jquery_ujs //= require turbolinks //= require bootstrap-typeahead-rails //= require_tree . var ready = function() { var engine = new Bloodhound({ datumTokenizer: function(d) { console.log(d); return Bloodhound.tokenizers.whitespace(d.title); }, queryTokenizer: Bloodhound.tokenizers.whitespace, remote: { url: '../search/typeahead/%QUERY' } }); var promise = engine.initialize(); promise .done(function() { console.log('success'); }) .fail(function() { console.log('error') }); $("#term").typeahead(null, { name: "article", displayKey: "title", source: engine.ttAdapter() }) }; $(document).ready(ready); $(document).on('page:load', ready);
Несколько комментариев о предыдущем фрагменте. В последних двух строках, поскольку я не отключил turbolinks, это способ подключить код, который я хочу запустить при загрузке страницы. В первой части скрипта вы можете видеть, что я использую Bloodhound. Это движок typeahead.js, и я также настраиваю конечную точку JSON, чтобы сделать запросы AJAX, чтобы получить предложения. После этого я вызываю initialize()
в движке, и я настраиваю typeahead в текстовом поле поиска, используя свой идентификатор «term».
Теперь нам нужно выполнить бэкэнд-реализацию для предложений, давайте начнем с добавления маршрута, отредактируйте app/config/routes.rb.
Rails.application.routes.draw do root to: 'articles#index' resources :articles get "search", to: "search#search" get 'search/typeahead/:term' => 'search#typeahead' end
Затем я добавлю реализацию на app/controller/search_controller.rb.
def typeahead render json: Article.search(params[:term], { fields: ["title"], limit: 10, load: false, misspellings: {below: 5}, }).map do |article| { title: article.title, value: article.id } end end
Этот метод возвращает результаты поиска для введенного термина с использованием JSON. Я только ищу по названию, но я мог бы указать тело статьи тоже. Я также ограничиваю количество результатов поиска до 10 максимум.
Теперь мы готовы попробовать реализацию typeahead:

Заключение
Как вы можете видеть, использование Elasticsearch с Rails делает поиск наших данных очень простым и быстрым. Здесь я показал вам, как использовать gem-ы низкого уровня, предоставляемые Elasticsearch, а также gem Searchkick, который представляет собой абстракцию, которая скрывает некоторые детали того, как работает Elasticsearch.
В зависимости от ваших конкретных потребностей вы можете использовать Searchkick и быстро и легко выполнять полнотекстовый поиск. С другой стороны, если у вас есть другие сложные запросы, в том числе фильтры или группы, вам может потребоваться больше узнать о деталях языка запросов в Elasticsearch и в конечном итоге использовать gem-ы более низкого уровня, такие как 'elasticsearch-models' и 'elasticsearch-rails'.
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Update me weeklyEnvato Tuts+ tutorials are translated into other languages by our community members—you can be involved too!
Translate this post