Greenという転職サイトで、ElasticSearchを導入して遊んでみようと思います。
具体的には今の求人が持っているデータをElasticSearchに入れてみて実際に開発環境で動かす事をGoalとします。
全文検索とは?
全文検索(Full text search)とは、コンピュータにおいて、複数の文書(ファイ ル)から特定の文字列を検索すること。「ファイル名検索」や「単一ファイル 内の文字列検索」と異なり、「複数文書にまたがって、文書に含まれる全文を 対象とした検索」という意味で使用される。 - 引用:Wikipedia
インデックスの違い
Bツリーインデックスとは?
転置インデックスとは?
- 引用:thinkit
elasticsearchを全文検索サーバとして活用するなら読んでおきたい、6つのブログ記事をピックアップ(追記あり)
見出し語の切り出す方法
上で転置インデックスについて説明しましたが、では索引を作るためにはどういった手段があるでしょうか。
代表的なものだけ簡単に説明します。
形態素解析
これはrubyの人だとmecabを使った事があると思うので、同じみのものですねw
辞書を登録することで、単語を意味のある単位に切ってくれるというものです。
辞書ありきなので、辞書を常にアップデートすることが重要になってきます。
参考)
形態素解析とは?
N-gram
形態素解析は辞書ありきだったのに対して、N-gramは特定の文字数で文字を切っていくタイプです。
下記がイメージ
参考)
N-gramってなんだ
実際に見るのが早いのでanalyzerを実行できるプラグインを入れておきます。
$ /usr/local/bin/plugin -install polyfractal/elasticsearch-inquisitor
実際に下記のURLにアクセス → analyzersを選択
http://localhost:9200/_plugin/inquisitor/#/analyzersz
全文検索エンジンの代表例
最近だと、Apache Solr、elasticsearch、Amazon cloudsearchあたりですね。
下記の記事に比較が書いてますので参考までに。
全文検索システムの比較 - Elasticsearch vs Solr vs Amazon CloudSearch
SolrとElasticsearchを比べてみよう
wantedly、niconicoなど有名サービスがelasticsearchを導入しているところを見ると、
あまり疑わずelasticsearchを導入すればいいかなーと個人的には思ってます。
cross2015でもこの辺の議論はありました。
参考)
CROSS 2015で話をしてきました #cross2015
Elasticsearchの紹介と特徴 CROSS 2015
インストールから設定まで
まずはelasticsearchをインストールします。
詳細はwantedlyのブログに書いているので、そちらを参考にしてください。
elasticsearch-railsというgemが用意されてます。(document)
まずはGemに追加してbundle install
# for elastic search
gem 'elasticsearch-rails'
gem 'elasticsearch-model'
include Elasticsearch::Model を記入するといくつかのメソッドを用意してくれます。
(search、import等)
今回は登録ユーザを検索するので、user modelに読み込ませるmoduleを書いていきます。
require 'active_support/concern'
module JobOffer::Searchable
extend ActiveSupport::Concern
included do
include Elasticsearch::Model
# Customize the index name
index_name "green_application"
# Set up index configuration and mapping
settings index: {
number_of_shards: 1,
number_of_replicas: 0,
analysis: {
filter: {
pos_filter: {
type: 'kuromoji_part_of_speech',
stoptags: ['助詞-格助詞-一般', '助詞-終助詞'],
},
greek_lowercase_filter: {
type: 'lowercase',
language: 'greek',
},
},
tokenizer: {
kuromoji: {
type: 'kuromoji_tokenizer'
},
ngram_tokenizer: {
type: 'nGram',
min_gram: '2',
max_gram: '3',
token_chars: ['letter', 'digit']
}
},
analyzer: {
kuromoji_analyzer: {
type: 'custom',
tokenizer: 'kuromoji_tokenizer',
filter: ['kuromoji_baseform', 'pos_filter', 'greek_lowercase_filter', 'cjk_width'],
},
ngram_analyzer: {
tokenizer: "ngram_tokenizer"
}
}
}
} do
mapping _source: { enabled: true },
_all: { enabled: true, analyzer: "kuromoji_analyzer" } do
indexes :id, type: 'integer', index: 'not_analyzed'
indexes :title, type: 'string', analyzer: 'kuromoji_analyzer'
・・・
end
end
def as_indexed_json(options={})
hash = self.as_json(
include: {
job_types: { only: [:job_type_id] },
・・・
}
)
hash['client_name'] = client.name
hash
end
end
module ClassMethods
def create_index!(options={})
client = __elasticsearch__.client
client.indices.delete index: "green_application" rescue nil if options[:force]
client.indices.create index: "green_application",
body: {
settings: settings.to_hash,
mappings: mappings.to_hash
}
end
end
end
analyzerとは?
インデックスを作成したり、クエリを解析するときにデータをどのように処理するかを指定するための機能。分割方法を定義するtokenizerと、分割後の文字列の整形処理を定義するfilterによって構成されます。
mappingの部分で指定しているプロパティの説明は下記になります。
_source
_sourceプロパティのenabledをtrueにすると、データを全てストアしておきます。
今回はelastic searchの方でidを抽出して、RDBを叩くので一旦falseで問題ないです。
また今回はuserデータにいくつかhas_manyのrelationの子データも検索対象としたいので、
def as_indexed_json(options={})
メソッドで追加します。
データの型や、その他の設定に関しては本家ドキュメント or elasticsearchの本を見ながら理解してもらえれば。
job_offer modelにて上記のmoduleをincludeします
class JobOffer < ActiveRecord::Base
include Joboffer::Searchable
includeをしたら下記を実行してデータをimportさせます。
# 既にindexを作っている場合は下記のコマンドで削除
JobOffer.__elasticsearch__.client.indices.delete index: JobOffer.index_name rescue nil
JobOffer.create_index! force: true
JobOffer.import
ちなみにこの場合でいくと、子要素データに検索をかける事は上手くいきません。。。
例えば、userというmodelにuser_skillsという子要素を配列で突っ込んで検索をかけると、
{
"query": {
"filtered": {
"query": {
"match": { "user_skills.id": 100 }
},
"filter": {
"range": { "user_skills.year": { "gte": 6 }}
}
}
}
}
以下のようなデータも対象となってしまいます。。。
・・・
user_skills: [
{ id: 100, year: 4 },
{ id: 200, year: 8 }
]
求人検索の方であれば、子要素でそこまで細かいfilterは必要ないので、こちらの方がデータ構造としても扱いやすい & 分かりやすいですね。
しかし、今回は子要素で検索をかける必要があるので(それelasticsearch使うのか!?説は一旦おいておいて)、別の手段で構成を創っていきます。
childデータを、elasticsearch-modelのgemを使って入れるやり方はgithubのexampleを参考に。
僕はなぜか上手く行かず。。。
もし上手くimportでparent/childを実現出来ている人がいれば教えてほしい。。。
kuromojiとは?
rubyの代表格mecabと並びjavaベースの検索システムの形態素解析器 kuromoji
mecab同様辞書が命ですねw
参考)
Java製形態素解析器「Kuromoji」を試してみる
Kuromojiは何で研究にあまり使われないのか?
実際に動かしてみる(Sense)
クエリをelasticsearch上で実行するためのツールとしてsenseというものが用意されています。
下記でインストールして下さい。
$ /usr/local/bin/plugin -install elasticsearch/marvel/latest
参考)
ElasticSearch 101 – a getting started tutorial
elasticsearchのディレクトリ構造とSenseの使い方
基本的には sense上でqueryを実施 → railsのコンソール上で実施 → 組み込みという流れが良さそうです。
実際に動かしてみる(rails)
まずは、コンソール上で検索をかけてみる
JobOffer.__elasticsearch__.search(
query: {
term: { "title" : "ruby" }
},
filter: {
term: {
status_id: 1 # open求人
}
},
sort: [
{ point: "desc"}
],
size: 100,
from: 200
).records.to_a
ちなみに.to_aをすると、elasticsearch-railsがdocumentのidを引っこ抜いて、rdb側にidでwhereを実施→配列にして返してくれる!
elasticsearch-rails使って運用する際はsourceはfalseで問題ないですね。
その辺の実装は下記にリンク貼っておきます。
デフォルトのサイズが10件になっているので、sizeを指定することで.to_aで返ってくる要素数を変更できます。fromはoffsetと同じようなイメージです。
query と filterの違い
ほとんど同じ事ができるので最初は戸惑うqueryとfilter
実質はかなり違うので簡単な説明を下記に書いておきます。
- query
全文検索を実施したい際に用いる(キーワード検索等)
relevance scoreに影響有り
- filter
status_idや完全一致で検索をかけられるものに関しては用いる(ステータス検索等)
キャッシュが効くのでパフォーマンス的にはかなり良い
relevance scoreに影響は出ない
どうクエリを生成していくべきか?
大きく2つ考えられる
- 入力情報から動的にクエリを生成
- query-stringを作って一発解決!
実際にやろうと思うと動的のクエリ生成は結構難しい。
検索項目やソートが多くなればなるほど、多様なクエリを作れるようにしないといけない。
一方でquery-stringはある種sqlインジェクション的な危険をはらんでいるw
ググるとQiitaの人が同じような事を書いていた。
Qiitaがquery-stringっぽい構文を自前実装した理由
Qiitaは前者で実装しているらしい。
rubyクライアント向けのOSSも作ってて素敵w
Greenでも本格的に導入するなら、クエリ周りのOSSは作る必要あるな。
一応試しに開発環境にいれるためにクエリ生成を書いてみた。
def build_elasticsearch_query
hash = {}
if keyword
hash[:query] = {
simple_query_string: {
fields: ["abstract", "title", "access"],
query: "#{keyword}",
default_operator: "and"
}
}
end
hash[:filter] = {}
hash[:filter][:and] = []
hash[:filter][:and] << { term: { status_id: 1 } } unless find_closed
hash[:filter][:and] << { range: { salary: { gte: salary_bottom.value } } }
hash
end
簡単に作っただけなので、問題はないが、本格的なクエリになればなるほど条件分岐も増えて大変。。。
さすがに汎用性のある状態で使いたいので、上手くOSSにしたいところ。
elasticsearch関連で使えそうな便利ライブラリ
- elasticsearch-ruby
- Yamabiko (Fluentd based MySQL/MariaDB Replicator for Elasticsearch/Solr)
- searchkick
searchkickは使えてないけど、かなり便利そう!
速攻で検索機能を実装できるsearchkickを調べた(Ruby)
総論
- RDB=遅い→全文検索だ!! 的な雰囲気を出している人がいるが、当たり前ながら得意不得意があるので、全てのアプリケーションで有効というわけではないw
- rails/rubyとのgemも出ているので、取っ付き易い
- ハイライトや予測変換など、使ってないけどキーワード検索をさせる上で面白い機能がいっぱいあった
- ElasticSearch Serverの本が分かりやすいので、とりあえず実運用させる際は買うことをおすすめしますw
参考
「niconicoの検索を支えるElasticsearch」と題して、第7回Elasticsearch勉強会で発表しました
Qiitaがquery-stringっぽい構文を自前実装した理由
Rails ConsoleでActiveRecordをElasticsearchに放り込んでみる
Elasticsearch at CrowdWorks
elasticsearch-rails を使っているときの custom analyzer 設定の書き方
Ruby: Rails から Elastic Search で全文検索を実行
active_record_associations_parent_child.rb
Getting Started with Elasticsearch on Rails
How To Setup Elasticsearch In Your Rails Application In Development
fields not in mapping are included in the search results returned by ElasticSearch
elasticsearch/elasticsearch-rails
elasticsearch-railsのupdateコールバック処理がモヤモヤ
Rails ConsoleでActiveRecordをElasticsearchに放り込んでみる