はじめに
RailsからElasticsearchのドキュメントを操作する方法について紹介していきます。
elasticsearch-railsを使うことが前提の記事になります。
また、本記事で出てくるサンプルや環境は、RailsとElasticsearchで検索機能をつくり色々試してみる - その1:サンプルアプリケーションの作成をもとにしています。
このサンプルどうなっているんだなどがあった場合はそちらを参照いただければと思います。
ドキュメント操作
Elasticsearchに登録されているデータをドキュメントといいます。このドキュメントに対して、読み取り、登録、更新などのCRUD操作を行っていきます。
Create(登録)
import
まとめてデータを登録するにはimport
が使えます。
importのデフォルトの挙動としては、モデルに対応するテーブルのデータを全て登録します。
# Mangaモデルに対応するデータをすべてimport
Manga.import
条件を指定したimport
特定の条件のレコードのみ登録したいような場合はscope
を使うことができます。
included do
include Elasticsearch::Model
# 1時間以内に更新があったレコードを取得するscopeを追加
scope :recent, lambda {
where("updated_at > ?", 1.hour.ago)
}
スコープを使う場合は以下のように scope: {scope名}
を引数に渡すことで可能です。
Manga.import scope: 'recent'
index_document
1件づつ登録する場合は、index_document
が使えます。すでに登録済みの場合はドキュメントを更新します。
manga = Manga.last
manga.__elasticsearch__.index_document
登録済みのドキュメントをどうやって判定しているか
登録済みの場合はドキュメントを更新すると書きましたが、そもそもElasticsearchの場合どのように登録済みか判定しているかのか。
Each document has an _id that uniquely identifies it, which is indexed so that documents can be looked up either with the GET API or the ids query.
によると_id
でユニークかどうかを判断できるようです。
ただし、elastisearch6.0以前の場合は、一つのドキュメントに複数のタイプを設定することができたため、type
と_id
を#で結合した文字列を_uid
として、_uid
がユニークかどうかを表していたようです。
Railsの話に戻ってくると、elasticsearch-railsでドキュメントを登録する場合、デフォルトの挙動はモデルが持つid
をelasticsearchの_id
として登録されます。そのため、モデル側のidと同じ_id
が登録済みの場合は更新、そうでない場合は新規作成の挙動となるようです。
※モデル側にidのフィールドがない場合はどのような挙動をするかは試せてないです。
Read(取得・検索)
search
search
メソッドで取得・検索ができます。
Manga.__elasticsearch__.search({条件})
条件には検索したい「ワード」そのものか「クエリDSL※」のハッシュ(to_hashに応答するインスタンス)を渡すことができます。
※クエリDSLのドキュメント
# 例) ワード
Manga.__elasticsearch__.search('ゆるキャン')
# 例) クエリDSLを使う
Manga.__elasticsearch__.search({query: { match: { title: 'ゆるキャン'} }})
単純な検索であれば検索ワードを渡すのみでも十分に使えますが、実運用ではクエリDSLを使って色々カスタマイズしていくことが多いと思います。
Update(更新)
update_document
update_document
でmodelの更新内容をElastichsearchに反映させることができます。
manga = Manga.last
manga.update(title: "タイトル updated")
# update_documentでElasticsearchに更新内容を反映させる
manga.__elasticsearch__.update_document
=> {"_index"=>"es_manga_development", "_type"=>"_doc", "_id"=>"14", "_version"=>7, "result"=>"noop", "_shards"=>{"total"=>0, "successful"=>0, "failed"=>0}}
# 更新結果の確認
response = Manga.__elasticsearch__.search({query: { match: { id: 14} }})
response.results.first._source.title
Manga Search (7.8ms) {index: "es_manga_development", type: "_doc", body: {query: {match: {id: 14}}}}
=> "タイトル updated"
manga.update(title: "タイトル updated2")
# もう一度更新すると _version がインクリメントされる
manga.__elasticsearch__.update_document
=> {"_index"=>"es_manga_development", "_type"=>"_doc", "_id"=>"14", "_version"=>8, "result"=>"noop", "_shards"=>{"total"=>0, "successful"=>0, "failed"=>0}}
オプションを指定することができます。(指定可能なオプションはドキュメントを参照)
manga.__elasticsearch__.update_document(retry_on_conflict: 3)
update_document_attributes
update_document_attributesで更新するフィールドを指定して更新することができます。
manga = Manga.last
manga.title
=> "タイトル before update"
# update_document_attributesで更新したいフィールドを指定して更新
manga.__elasticsearch__.update_document_attributes(title: "タイトル updated with attribute")
# 結果を確認
response = Manga.__elasticsearch__.search({query: { match: { id: 14} }})
response.results.first._source.title
=> "タイトル updated with attribute"
# DBへの保存は行っていないため更新されていない
manga.reload.title
=> "タイトル before update"
gemのソースを確認すると、attributesに指定したものをbodyにそのまま設定しているため、attributesに渡したフィールドのみ更新されるようです。
def update_document_attributes(attributes, options={})
client.update(
{ index: index_name,
type: document_type,
id: self.id,
body: { doc: attributes } }.merge(options) # attributesをdocに設定している
)
end
update_document_attributes
もupdate_document
と同じオプションが指定できます。
Delete(削除)
delete_document
削除はdelete_document
で行います。
manga = Manga.last
manga.__elasticsearch__.delete_document
=> {"_index"=>"es_manga_development",
"_type"=>"_doc",
"_id"=>"14",
"_version"=>11,
"result"=>"deleted",
"_shards"=>{"total"=>2, "successful"=>1, "failed"=>0},
"_seq_no"=>10,
"_primary_term"=>2}
optionも指定できます。(指定可能なオプションはドキュメントを参照)
manga.__elasticsearch__.delete_document(refresh: true)