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

## はじめに
RailsからElasticsearchのドキュメントを操作する方法について紹介していきます。
[elasticsearch-rails](https://github.com/elastic/elasticsearch-rails/tree/master)を使うことが前提の記事になります。

また、本記事で出てくるサンプルや環境は、[RailsとElasticsearchで検索機能をつくり色々試してみる - その１：サンプルアプリケーションの作成](https://qiita.com/yamashun/items/6ecaa6f161b4cf283db3)をもとにしています。
このサンプルどうなっているんだなどがあった場合はそちらを参照いただければと思います。

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

## Create（登録）
### import
まとめてデータを登録するには`import`が使えます。
importのデフォルトの挙動としては、モデルに対応するテーブルのデータを全て登録します。

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

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

```rb:app/models/concerns/manga_searchable.rb
  included do
    include Elasticsearch::Model

    # 1時間以内に更新があったレコードを取得するscopeを追加
    scope :recent, lambda {
      where("updated_at > ?", 1.hour.ago)
    }
```

スコープを使う場合は以下のように `scope: {scope名}`を引数に渡すことで可能です。

```rb
Manga.import scope: 'recent'
```

### index_document
1件づつ登録する場合は、`index_document`が使えます。すでに登録済みの場合はドキュメントを更新します。

```rb
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`メソッドで取得・検索ができます。

```rb
Manga.__elasticsearch__.search({条件})
```

条件には検索したい「ワード」そのものか「クエリDSL※」のハッシュ（to_hashに応答するインスタンス）を渡すことができます。
※[クエリDSLのドキュメント](https://www.elastic.co/guide/en/elasticsearch/reference/6.5/query-dsl.html)

```rb
# 例） ワード
Manga.__elasticsearch__.search('ゆるキャン')

# 例） クエリDSLを使う
Manga.__elasticsearch__.search({query: { match: { title: 'ゆるキャン'} }})
```

単純な検索であれば検索ワードを渡すのみでも十分に使えますが、実運用ではクエリDSLを使って色々カスタマイズしていくことが多いと思います。


## Update（更新）
### update_document
`update_document`でmodelの更新内容をElastichsearchに反映させることができます。

```rb
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}}
```

オプションを指定することができます。（指定可能なオプションは[ドキュメント](https://www.rubydoc.info/gems/elasticsearch-api/Elasticsearch/API/Actions:update)を参照）

```rb
manga.__elasticsearch__.update_document(retry_on_conflict: 3)
```

### update_document_attributes
update_document_attributesで更新するフィールドを指定して更新することができます。

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

```rb: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_attributes`も`update_document`と同じオプションが指定できます。

## Delete（削除）
### delete_document
削除は`delete_document`で行います。

```rb
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も指定できます。（指定可能なオプションは[ドキュメント](http://rubydoc.info/gems/elasticsearch-api/Elasticsearch/API/Actions:delete)を参照）

```rb
manga.__elasticsearch__.delete_document(refresh: true)
```