Внедрение Elasticsearch с Ruby on Rails для расширенного поиска
Elasticsearch — это поисковый движок, который позволяет в реальном времени работать с огромными объемами данных. Он основан на Lucene и предлагает не только полнотекстовый поиск, но и сложные запросы к данным, включая агрегацию.
Ruby on Rails — это фреймворк, который делает акцент на скорости и простоте разработки. Используя принципы convention over configuration и DRY, Rails позволяет сосредоточиться на уникальной логике приложения, минимизируя количество шаблонного кода.
В статье рассмсотрим как использовать Elasticsearch вместе с Ruby on Rails для реализации поиска внутри приложения.
Установка и настройка
Скачаем с официального сайта Elasticsearch и следуя инструкциям установки для определенной ОС.
p.s elasticsearch требует Java
Для интеграции Elasticsearch с Rails приложением нужно добавить в Gemfile
строки:
gem 'elasticsearch-model'
gem 'elasticsearch-rails'
После чего юзаем команду bundle install
и гемы устанавливаются в проект.
После установки гемов настраивается подключение к Elasticsearch. Это можно сделать, создав инициализатор в config/initializers
с именем elasticsearch.rb
и добавив в него следующий код:
Elasticsearch::Model.client = Elasticsearch::Client.new(host: 'localhost:9200')
Проверяем, что Elasticsearch запущен и доступен по указанному адресу в нашем случае localhost:9200
.
Для использования Elasticsearch с моделями Rails, включаются модули Elasticsearch::Model
и Elasticsearch::Model::Callbacks
в модели, которые нужноиндексировать. Например:
class Article < ApplicationRecord
include Elasticsearch::Model
include Elasticsearch::Model::Callbacks
end
Здесь автоматически синхронизируем модель с индексом Elasticsearch при создании, обновлении или удалении записей.
Основные функции
Для индексации модели в Elasticsearch нужно включить модули Elasticsearch::Model
и, опционально, Elasticsearch::Model::Callbacks
в модель Rails:
class Article < ApplicationRecord
include Elasticsearch::Model
include Elasticsearch::Model::Callbacks
end
Для адекватной работы необходимо настроить маппинги — это описания того, как данные должны быть индексированы и храниться:
class Article
include Elasticsearch::Model
include Elasticsearch::Model::Callbacks
settings index: { number_of_shards: 1 } do
mappings dynamic: 'false' do
indexes :title, type: 'text', analyzer: 'english'
indexes :content, type: 'text', analyzer: 'english'
indexes :published_at, type: 'date', format: 'strict_date_optional_time||epoch_millis'
end
end
def as_indexed_json(options={})
as_json(only: [:title, :content, :published_at])
end
end
Настраиваем индекс с одним шардом, отключаем динамическое создание маппингов и определяем маппинги для полей title
, content
и published_at
. Также определяем метод as_indexed_json
, который указывает, какие атрибуты модели должны быть сериализованы для индексации.
После настройки модели можно индексировать существующие данные, используя rake задачи или написав кастомный скрипт:
Article.find_each do |article|
article.__elasticsearch__.index_document
end
Код перебирает каждую статью в БД и индексирует ее в Elasticsearch.
После индексации данных можно использовать возможности поиска Elasticsearch для поиска и анализа данных:
response = Article.search('котики')
response.records.each do |record|
puts record.title
end
Здесь делаем поиск по статьям, содержащим фразу «котики», и выводим найденных котиков.
Когда данные в модели изменяются, Elasticsearch-Model автоматически синхронизирует эти изменения с соответствующим индексом в Elasticsearch. Если нужно вручную обновить или удалить индексированные данные, можно юзать методы update_document
и delete_document
.
article = Article.find(1)
article.title = "Updated Title"
article.save # автоматически обновляет документ в Elasticsearch
article.delete # автоматически удаляет документ из Elasticsearch
Прочие возможности поиска
Существует множество других вариаций реализации поиска, например можно сделать поиск по нескольким полям с разной важностью:
response = Article.search(query: {
multi_match: {
query: 'cats',
fields: ['title^10', 'content^2', 'tags'],
type: 'best_fields',
tie_breaker: 0.3
}
})
Запрос ищет фразу «cats» в полях title
, content
и tags
модели Article
, причем полю title
придается наивысший приоритет ^10
, полю content
— меньший приоритет ^2
, а tags
используется без специфического веса. Параметр tie_breaker
помогает управлять релевантностью результатов при совпадении в нескольких полях.
При определении индекса можно указать использование анализаторов, которые предварительно обрабатывают текст перед его индексацией:
response = Article.search(size: 0, aggs: {
popular_tags: {
terms: {
field: 'tags'
}
}
})
Для полей title
и content
используется анализатор my_custom_analyzer
, который преобразует текст в нижний регистр и удаляет акценты
Можно использовать фаззи-поиск для исправления опечаток:
response = Article.search(query: {
fuzzy: {
title: {
value: 'cats',
fuzziness: 2
}
}
})
Запрос ищет слова, похожие на «cats» в поле title
, допуская до двух ошибок в слове.
Предположим, у нас есть интернет-магазин, и мы хотим позволить пользователям фильтровать продукты по ценовым категориям:
response = Product.search(size: 0, aggs: {
price_ranges: {
range: {
field: 'price',
ranges: [
{ to: 50 },
{ from: 50, to: 100 },
{ from: 100 }
]
}
}
})
Запрос создает фасеты для продуктов в трех ценовых диапазонах: до 50, от 50 до 100, и более 100.
Elasticsearch также поддерживает геопространственные запросы, позволяя искать объекты в определенном радиусе от заданной точки
Например, можно искать все магазины в радиусе 10 километров от текущего местоположения пользователя:
response = Store.search(query: {
bool: {
must: {
match_all: {}
},
filter: {
geo_distance: {
distance: "10km",
location: {
lat: 40.715,
lon: -73.988
}
}
}
}
})
Можно искать объекты, находящиеся внутри определенной географической области, заданной многоугольником:
response = Property.search(query: {
geo_polygon: {
location: {
points: [
{ lat: 40.73, lon: -74.1 },
{ lat: 40.73, lon: -73.99 },
{ lat: 40.74, lon: -74.1 },
{ lat: 40.74, lon: -73.99 }
]
}
}
})
Географический хэш представляет собой способ кодирования информации о местоположении в короткую строку символов, это может быть юзабельно для быстрого поиска объектов внутри определенной области.
response = Event.search(query: {
geo_bounding_box: {
location: {
top_left: {
lat: 40.73,
lon: -74.1
},
bottom_right: {
lat: 40.01,
lon: -71.12
}
}
}
})
Геопространственные агрегации позволяют анализировать данные на основе их местоположения, например, подсчитывая количество объектов в разных регионах:
response = Visitor.search(size: 0, aggs: {
regions: {
geo_hash_grid: {
field: "location",
precision: 3
},
aggs: {
top_hits: {
top_hits: {
_source: {
includes: [ "name", "location" ]
},
size: 10
}
}
}
}
})
Elasticsearch в коннекте с Ruby on Rails, позволяют удовлетворять сложные запросы пользователей и обрабатывать большие объемы информации.
Больше практических инструментов вы сможете изучить в рамках онлайн-курсов от моих друзей из OTUS. С подробным каталогом курсов можно ознакомиться по ссылке.