Posted at

RailsとElasticsearchで検索機能をつくり色々試してみる - ドキュメントの操作


はじめに

RailsからElasticsearchのドキュメントを操作する方法について紹介していきます。

elasticsearch-railsを使うことが前提の記事になります。

また、本記事で出てくるサンプルや環境は、RailsとElasticsearchで検索機能をつくり色々試してみる - その1:サンプルアプリケーションの作成をもとにしています。

このサンプルどうなっているんだなどがあった場合はそちらを参照いただければと思います。


ドキュメント操作

Elasticsearchに登録されているデータをドキュメントといいます。このドキュメントに対して、読み取り、登録、更新などのCRUD操作を行っていきます。


Create(登録)


import

まとめてデータを登録するにはimportが使えます。

importのデフォルトの挙動としては、モデルに対応するテーブルのデータを全て登録します。

# Mangaモデルに対応するデータをすべてimport

Manga.import


条件を指定したimport

特定の条件のレコードのみ登録したいような場合はscopeを使うことができます。


app/models/concerns/manga_searchable.rb

  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の場合どのように登録済みか判定しているかのか。

https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-id-field.html


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に渡したフィールドのみ更新されるようです。


elasticsearch-model/lib/elasticsearch/model/indexing.rb

       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_attributesupdate_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)