Внедрение Elasticsearch с Ruby on Rails для расширенного поиска

8ed5ff22fbad20816171ff3bf1876438.jpg

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. С подробным каталогом курсов можно ознакомиться по ссылке.

© Habrahabr.ru