RailsアプリケーションにElasticsearchを追加する
はじめに
普段は個人投資家として投資を行いながら
複数のスタートアップで開発の支援をしたり、リードエンジニアをしています
ElasticSearchとは
全文検索エンジン
全文検索やフィルタリング、ソート、類似度検索、地理空間検索などが可能
Docker周り
ほとんどのプロダクトはDockerで構築されているのでDocker周りの話から
Dockerファイルの作成
FROM docker.elastic.co/elasticsearch/elasticsearch:7.15.1
RUN elasticsearch-plugin install analysis-kuromoji
docker-composeファイルの作成
version: '3.9'
services:
elasticsearch:
build: .
environment:
- "discovery.type=single-node"
ports:
- 9200:9200
volumesなどの細かな設定は割愛
Rails周り
gemのインストール
gem 'elasticsearch-model'
gem 'elasticsearch-rails'
Model周り
Elasticsearchの処理の共通化
elasticsearch-modelで使う処理などを共通化するためconcernsをつくってみる
module ElasticsearchOperator
extend ActiveSupport::Concern
included do
include Elasticsearch::Model
include Elasticsearch::Model::Callbacks # インデックスの自動生成をしない場合は書かない
settings index: default_settings
mappings dynamic: false
end
class_methods do
def index_name
"#{Rails.env}-#{self.name.underscore.pluralize}"
end
def default_settings
{
analysis: {
analyzer: {
kuromoji_analyzer: {
type: 'custom',
tokenizer: 'kuromoji_tokenizer',
filter: [
'kuromoji_baseform', 'pos_filter',
'cjk_width'
]
}
},
filter: {
ja_stop: {
type: 'stop',
stopwords: '_japanese_'
},
kuromoji_number: {
type: 'kuromoji_number'
},
kuromoji_stemmer: {
type: 'kuromoji_stemmer'
},
pos_filter: {
type: 'kuromoji_part_of_speech',
stoptags: ['助詞-格助詞-一般', '助詞-格助詞-引用', '助詞-格助詞-連語']
}
},
tokenizer: {
kuromoji_tokenizer: {
type: 'kuromoji_tokenizer'
}
}
}
}
end
def default_mappings
{
dynamic: false
}
end
def create_index!
self.__elasticsearch__.delete_index! if self.__elasticsearch__.index_exists?
self.__elasticsearch__.create_index! index: index_name
self.__elasticsearch__.refresh_index!
end
end
end
default_settingsメソッド
Elasticsearchのインデックス設定に関するデフォルト値を定義
analysis
テキスト解析に関する設定
analyzer
テキストのトークン化やフィルタリングに使用されるアナライザーを定義
kuromoji_analyzerのようなカスタムアナライザーを定義
tokenizer
テキストをトークンに分割する方法を定義するための設定
kuromoji_tokenizerのように、形態素解析を行うトークナイザーを定義
filter
アナライザーで使用されるフィルターを定義
・ ja_stop
日本語のストップワードを除去するためのフィルター
typeをstopに設定し、stopwordsオプションで日本語のストップワードを指定
・ kuromoji_number
数字を正規化するためのフィルター
typeをkuromoji_numberに設定することで、数字を正規化する
・ kuromoji_stemmer
単語の語幹を抽出するためのフィルタ-
typeをkuromoji_stemmerに設定することで、語幹を抽出する
・ pos_filter
形態素解析において特定の品詞を除去するためのフィルター
typeをkuromoji_part_of_speechに設定し、stoptagsオプションで除去したい品詞を指定
この例では、「助詞-格助詞-一般」、「助詞-格助詞-引用」、「助詞-格助詞-連語」を除去
検索対象のモデル
例としてArticleというモデルの場合
class Article < ApplicationRecord
include ElasticsearchOperator
mappings do
indexes :title, type: 'text', analyzer: 'kuromoji_analyzer'
indexes :body, type: 'text', analyzer: 'kuromoji_analyzer'
end
end
ここに個々のマッピング設定を記載
index設定も変えたい場合は、ElasticsearchOperator.default_settingsに追加すれば可能
インデックスの作成 / 削除
ElasticsearchOperatorにインデックスを作成・削除するメソッドを定義
def create_index!
self.__elasticsearch__.delete_index! if self.__elasticsearch__.index_exists?
self.__elasticsearch__.create_index! index: index_name
self.__elasticsearch__.refresh_index!
end
頻繁にリフレッシュを行うとパフォーマンスに影響が出るため、必要なタイミングでのみ使用する
呼び出し
Article.create_index!
もちろんバッチ処理をつくって検索対象のモデルのインデックスを定期更新するが、割愛
検索
def self.search(query)
search_definition = {
size: 10000, # Elasticsearchのデフォルトのサイズが10件なので調整
query: {
multi_match: {
query: query,
fields: [
'title^2', # titleを2倍に
'body',
.......
],
type: 'most_fields',
fuzziness: 'AUTO'
}
}
}
response = __elasticsearch__.search(search_definition)
article_ids = response.results.map { |result| result._id.to_i }
Article.where(id: product_ids)
end
Article.search('検索ワード')
応用
別テーブルの別カラムの結果をもとに検索したい
例えばArticleの検索機能をつくっているときに、
Publisherという別テーブルのカラムの値をもとにArticleを検索したい時など
仮想のカラム名をつけてindexさせることで可能
def index_with_publisher_data(publisher)
data = as_json.merge(publisher_name: publisher&.name, publisher_name_kana: publisher&.name_kana)
data.delete('id')
__elasticsearch__.index_document({ body: data, id: self.id })
end
Dockerで起動している別リポジトリと共通でElasticsearchを使いたい
プロダクトによってはAPI専用のRailsアプリケーションのリポジトリと
社内運用のための管理画面やバッチ処理などのRailsアプリケーションのリポジトリが存在している場合がある
例えば検索の処理はAPIのリポジトリで行い、
インデックス作成のジョブなどは別のリポジトリで行いたいなどのケース
ネットワークの作成
コンテナ間で通信するためのネットワークを作成する
共通のネットワークがすでにある場合は不要
$ docker network create common_network
ただし、Elasticsearchのコンテナが起動しているリポジトリのほうに
ネットワークの設定がされていなければ追記する
version: '3.9'
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:7.14.1
environment:
- discovery.type=single-node
ports:
- 9200:9200
networks:
- common_network
networks:
common_network:
external: true
Rails側の修正
両方のリポジトリに追加
config/initializers/elasticsearch.rb
require 'elasticsearch/model'
require 'elasticsearch/transport'
config = {
host: ENV['ELASTICSEARCH_HOST'] || 'your_host_or_ip:9200',
user: ENV['ELASTICSEARCH_USER'],
password: ENV['ELASTICSEARCH_PASSWORD'],
transport_options: {
request: { timeout: 5 }
}
}.compact
Elasticsearch::Model.client = Elasticsearch::Client.new(config)
def self.search(query)
search_definition = {
query: {
multi_match: {
query: query,
fields: ['title', 'vendor_name', 'vendor_name_kana'],
type: 'most_fields',
fuzziness: 'AUTO'
}
}
}
__elasticsearch__.search(search_definition)
end
IPアドレスの調べ方
$ docker network inspect common_network
externalネットワーク内でコンテナのIPアドレスを固定する方法もあるが割愛
Discussion