$shibayu36->blog;

クラスター株式会社のソフトウェアエンジニアです。エンジニアリングや読書などについて書いています。

知識ゼロからElasticsearchを実践で使えるようになろう!

 以前少しだけElasticsearchを触った時に、自分流Elasticsearch入門 - $shibayu36->blog; というElasticsearchに入門した時のメモをまとめていた。しかし、その頃はElasticsearchを使って完全に一人で一つの機能を作るというところまではいけなかった。

 最近になってまたElasticsearchを一から導入する仕事をすることになった。この時以前自分がまとめた記事を読みながらやっていたのだが、実践で一から導入するためにはこの記事だけでは知識が足りなかった。

 そこで、前の記事の知識をベースに、一から導入するために少しずつ学んでいき、自分のブログにまとめるなどのことをしてきたので、今回はその締めくくりとして、知識ゼロからElasticsearchを使えるようになるために学習したことについて書いておきたいと思う。

今回書くこと・書かないこと

 今回はアプリケーションエンジニアが、アプリケーションの機能をElasticsearchを使って作れるようになるための知識について書く。

 逆にインフラの知識については書かない。それは僕がElasticsearchの運用について担当しなかったため。インフラのことは誰か書いて欲しい。

Elasticsearchの概念を学ぶ

 Elasticsearchには非常に多くの概念がある。例えば、クラスタ・ノード・インデックス・タイプ・ドキュメントの関係とは何かや、マッピングやアナライザとは何か、クエリとフィルタの違いは何かなどだ。この辺りを理解しないと使えないので、まずそれを再度勉強しなおした。


 この時に一番参考になったブログは以下のブログ。

code46.hatenablog.com

 この記事はElasticsearchを始めようと思った時に最初に見るチュートリアルとして最適だ。これを読むだけで、先ほど書いた概念や簡単な使い方などを学ぶことが出来る。僕は今回もう一度Elasticsearchを使うにあたって、まずこの記事を読み返してから始めた。


 また他にも@johtaniさんが最近発表されていた次のプレゼン資料が簡単な概念や使い方を追いかけるのに参考になった。

speakerdeck.com

全文検索技術の基本を知る

 Elasticsearchの概念を知ったとしても、全文検索の基本も知っておかないとうまくElasticsearchを使いこなせない。そこで再度全文検索の基本を学び直した。

 参考になったのは先ほどの「Elasticsearchの始め方」の資料だ。この53ページ目辺りから始まる「全文検索とは?」の部分の解説が非常に分かりやすい。この資料から、全文検索の基本の考え方やN-gram形態素解析などについて学ぶことができた。


 他にも正規化やストップワードといった概念もある。こちらについては次の資料が参考になった。

www.atmarkit.co.jp


 また、自分なりに全文検索の技術をシンプルに理解しようと次の記事をまとめた。こちらも参考になると嬉しい。

blog.shibayu36.org

実践の知識を学ぶ

 さて、ここまでは以前Elasticsearchを使っていた時にも学んでいた。しかし今回実際に使ってみようとすると、実践するための知識が全く足りなかった。

 実践で使うためには少なくとも次のことをしなければならない。

  • インデックス定義を設計する
  • 作ったインデックスに対してドキュメントの作成をする
  • 作ったドキュメントを検索する

 これらが出来るようにインデックス定義の設計、ドキュメントの作成・更新・削除処理、検索APIやQuery DSLなどを学ぶ必要があった。

インデックス定義の設計を学ぶ

 インデックス定義の設計については、いろいろ調べて、自分なりにどのような手順で設計すれば良いかまとめた。以下の記事が参考になると嬉しい。

blog.shibayu36.org


 また、最初に紹介したElasticsearchチュートリアル - 不可視点 がインデックス定義の設計の実践にもなっているので参考になる。

ドキュメントの作成・更新・削除処理

 インデックスの定義ができたら、続いてそれに対してドキュメントを作成などをする必要がある。そこでドキュメントの作成・更新・削除処理について学んだ。

 これは単にAPIの使い方を学べば良いだけなので、Document APIに関する公式ドキュメントを読むと良いだろう。

www.elastic.co

Search APIやQuery DSLを学ぶ

 ドキュメントの作成ができたら、あとは検索をどのようにすれば良いか学ぶだけだ。検索をするにはSearch APIやQuery DSLを学ぶ必要がある。

 Search APIの基本的な使い方についてはそこまで難しくない。次のSearch APIに関する公式ドキュメントを読んでおけば簡単に理解が出来る。

www.elastic.co


 問題はSearch APIに指定するQuery DSLだ。指定できるQuery DSLが膨大にあるので、何から学んだら良いか分からない状態になってしまう。

 英語のドキュメントであれば、The Definitive GuideのFull-Body Searchの章を読むのが良さそうだった。

 日本語の方は、

あたりが参考になるだろう。本当は入門としてSQLとメインのQuery DSLの対比がまとまっている資料があればわかりやすいのだけど、そのようなものがなかったので残念。


 他にも検索するときは、ソートをどのようにするかも重要である。これは公式のRequest body search | Elasticsearch Guide [8.0] | Elastic を読めば大体分かるだろう。また、何度も参照するが、一番最初に紹介したElasticsearchチュートリアル - 不可視点 もわかりやすい。

Elasticsearchの公式ドキュメントの引き方を学ぶ

 ここまで来たらとりあえず実践でElasticsearchを使えるようにはなっていると思う。あとは自分のやりたいことに応じて必要なことを学んでいけばよいだろう。必要なことを学ぶにはやはり公式のドキュメントを見るのが一番なので、最後に公式のドキュメントの引き方を学ぶと良い。

 ドキュメントの引き方を学ぶには、Elasticsearchの実践の記事を読み、少しでもわからないことがあれば分かるまで公式のドキュメントを引くということをやるのが良い。例えば次の資料を読みながら、分からないところを全部公式のドキュメントから調べてみると、大体公式のドキュメントの引き方が分かる。

まとめ

 今回は最近自分がずっとElasticsearchを勉強していた締めくくりとして、アプリケーションエンジニアが知識ゼロからElasticsearchを使えるようになるために勉強したことについてまとめてみた。Elasticsearchは覚えなければならないことが非常に多い一方で、日本語で体系的に教えてくれる資料がないので、この記事が参考になれば嬉しい。

 またこれまでずっとElasticsearchについて勉強してまとめていたことは、 http://blog.shibayu36.org/archive/category/elasticsearch に書いてあるので、こちらも参考になれば。

2016/09/05 15:25 更新

@johtaniさんからスライドが大分古いので新しい方を参照したほうが良い、という指摘をもらったので、新しい方のスライドに変更し、若干の調整を加えました。

Elasticsearchのインデックス定義を設計する手順

 Elasticsearchを使おうとすると、まずアプリケーションの仕様にしたがってインデックス定義やマッピング定義を設計しなければならない。これはMySQLを使っていてスキーマを考えるフェーズに相当する。

 この時、考えることが非常に多く、いろいろなドキュメントを参照し設計したので、今回はその手順について書いていきたいと思う。

 インデックスやマッピングが何かという話は、次の記事を参考に。

 また対象とするElasticsearchのversionは記事執筆時点の安定版の2.3.5とする。

今回サンプルとする例

 実際のプロジェクトを参考例にすることは流石にできないので、今回はブログの記事を検索するという例で説明する。Elasticsearchにはブログの記事の情報を入れ、全文検索やフィルタリングができるようにしたい。

 ブログのアーキテクチャとしては、単純に1つのブログに複数の記事が紐付いているというものを考える。記事にはタイトル、本文があり、カテゴリが複数付けられる。またPVやブックマーク数などがデータとしてあるとする。

インデックスやマッピングを設計する手順

 僕が考えるときは、以下のように設計をしていった。

  • Elasticsearchでやりたいことを決める
  • フィールドとその型を決める
  • 全文検索のためにどのようにアナライズするか考える
  • その他細かい検討を行う
  • 最終的なインデックス定義のJSONを作る

Elasticsearchでやりたいことを決める

 何をやりたいかを明確にしておかなければ、インデックスをきれいに設計できないと考えた。そこでインデックスを設計する前にやりたいことを考えてみる。例えば次のものが考えられる。

  • (1) ユーザーが検索ワードを入れることで、タイトル・本文・カテゴリからいい感じに全文検索できる
  • (2) 特定のブログに絞り込んで検索ができる
  • (3) 公開されている記事のみに絞り込む
  • (4) 人気順に並べる
  • (5) 投稿順に並べる
  • (6) カテゴリ名の完全一致で検索する
  • etc...

 (1)は例えば「Ruby 設計」とかで調べた時に、タイトル・本文・カテゴリに「Ruby」と「設計」が含まれるものを検索するといったもの。いい感じにというのは、例えば「Ruby 設計」という検索クエリで、「Ruby入門」「アプリケーション設計」というカテゴリが付いている記事もヒットさせたいとか、大文字小文字関係なしにヒットさせたいなどがある。(2)〜(5)については普通の要求である。(6)に関しては、例えばあるカテゴリがついた記事一覧を出したい時などに利用したい。


 ひとまず今回はやりたいことはこれだけとして、インデックス定義の設計に入る。

フィールドとその型を決める

 やりたいことが決まれば、次にどのような情報をElasticsearchに入れるかや、それぞれのフィールドの型を決めることができる。今回のやりたいこと(1)〜(6)を踏まえて、フィールドとその型を決めてみる。どのような型が利用できるかは、Field data types | Elasticsearch Guide [8.0] | Elastic を参考にする。

 まずどのような情報が必要か考える。

  • (1)から、タイトル、本文、カテゴリを入れる必要がある
    • これらは全文検索をするために何かしら処理を加えなければならない
    • 処理については「全文検索のために、どのようにアナライズするか考える」で説明する
  • (2)から、記事が紐づくブログのIDを含める必要がある
  • (3)から記事のステータスが必要
  • (4)から人気順に必要な情報、今回はPVとブックマーク数を含める必要がある
  • (5)から投稿日時が必要である
  • (6)はカテゴリのフィールドがあればいいように見えるが、実は(1)のために利用するフィールドを(6)で使い回すことが出来ないので、別のフィールドが必要となる
    • 後で説明する


 入れるべき情報は分かったので、次にそれぞれの型を考える。

  • タイトル、本文は全文検索のために何かしら処理を加えたstring型となる
  • カテゴリは複数つけられるので、stringの配列となる
    • 全文検索のために何かしら処理を加える必要がある
    • ただし、Elasticsearchではarray型というのは存在せず 、あるフィールドには複数のデータを入れられるため、string型にしておく
  • ブログのIDはlong
  • 記事のステータスはstring
    • 公開状態published、下書き状態draftがあるとする
  • PVはinteger
  • ブックマーク数はinteger
  • 投稿日時はdateで、formatにdate_time_no_millisを利用する


 以上から、フィールドと型が決まった。

  • title : string, 全文検索のためanalyzerの定義が必要
  • content : string, 全文検索のためanalyzerの定義が必要
  • categories : string, 全文検索のためanalyzerの定義が必要
  • blog_id : long
  • status : string
  • pv : integer
  • bookmark_count : integer
  • published_at : date, date_time_no_millis

全文検索のためにどのようにアナライズするか考える

 フィールドと型が決まったが、title、content、categoriesには「全文検索のためanalyzerの定義が必要」と書いてあり、ここについてはまだ決まっていない。そこで次に全文検索のためにどのようにアナライズするか考えてみる。

 (1)のユースケースでは「いい感じに」という漠然としたワードを使ったので、もう少し具体化する。

  • 日本語として検索したい
  • 大文字や小文字を気にせず検索したい
    • RUBY」と検索しても「Ruby」や「ruby」を含む記事がヒットして欲しい
  • 全角半角を気にせず検索したい
    • 「データベース」と検索しても、「データベース」を含む記事がヒットして欲しい
  • 1つのカテゴリの一部分に含まれていても検索したい
    • Ruby」で検索しても、「Ruby入門」カテゴリがついた記事がヒットして欲しい
  • 「に」「が」のような、日本語の助詞がヒットすると精度が悪くなるので、省きたい


 これらを満たすように、Analyzerを定義してみる。Analyzerの概念については他の記事を参考にして欲しい。以前ElasticsearchのAnalyzerを理解するため全文検索の仕組みをシンプルに考える - $shibayu36->blog;でまとめたものや、Analysis and Analyzers | Elasticsearch: The Definitive Guide [2.x] | Elasticが参考になるだろう。


 大文字小文字や全角半角を気にせず検索するためには、正規化を行う必要がある。全角半角が含まれた状態でトークナイズされると、「データベース」というワードは「データ」「ベース」と分割されてしまい、意図しない結果となるので、トークナイズより前に行う必要がある。これはICU Analysis Pluginという便利なものがあり、この中のICU Normalization Character Filterを利用すれば実現できる。このプラグインだけで、NFKC正規化と大文字小文字の正規化を行ってくれるため、大文字小文字全角半角の正規化ができる。

 また全文検索のため、トークナイズを行う必要がある。日本語の全文検索のためにはkuromoji analyzerというプラグインがある。これのkuromoji_tokenizerを利用すれば日本語の品詞単位でトークナイズできる。また検索しやすさのためにsearch modeというオプションを利用する。

 最後にトークナイズ結果から助詞を省きたい。kuromoji_part_of_speechを利用し、トークナイズ後に適用することで助詞を省くことができる。

 この手順はそのままCharacter filters, Tokenizer, Token filtersの概念にマッチするので、最終的に次の処理を行えば全文検索の準備ができる。

  • Character filters : icu_normalizer
  • Tokenizer : kuromoji_tokenizer, search mode
  • Token filters : kuromoji_part_of_speech

 処理にかけると、例えば「Ruby入門のブログ記事」のようなタイトルは、「ruby」「入門」「ブログ」「記事」のように分割され、「Ruby」や「ブログ」のような検索ワードにヒットするようになる。

カテゴリ名の完全一致で検索するために

 前述のとおり、(6)のユースケースを満たすためには、実はcategoriesだけではうまくいかない。なぜなら、「Ruby入門」「アプリケーション設計」というカテゴリが付いている記事の場合、先ほどの全文検索のための処理を通すと「ruby」「入門」「アプリケーション」「設計」のように分割されてしまい、結果として「Rubyアプリケーション」と検索してもヒットするようになってしまうためである。

 そこで、カテゴリ名の完全一致で検索するためには、全文検索用の処理を行わないフィールドを別に作っておく必要がある。これはカテゴリ検索のためのフィールドを役割ごとに二つ作ることで解決する。

  • categories : string, 全文検索のための処理を行わない
  • analyzed_categories : string, 全文検索のための処理を行う


 これにより、「Ruby入門」「アプリケーション設計」というカテゴリがあった場合

  • categories : 「Ruby入門」「アプリケーション設計」
  • analyzed_categories : 「ruby」「入門」「アプリケーション」「設計」

のようにデータが保存され、完全一致の時はcategories、全文検索の時はanalyzed_categoriesを利用するとうまく検索出来るようになる。

その他細かい検討を行う

 ここまでで大体のインデックスを設計することができた。あとは細かい点を検討する。

  • Dynamic Mappingの無効化
  • _sourceフィールドの無効化
  • インデックスにalias名を付けるか

Dynamic Mappingの無効化

 Dynamic Mapping | Elasticsearch: The Definitive Guide [2.x] | Elastic にあるように、デフォルトではDynamic Mappingという機能が有効化されている。Dynamic Mappingとはインデックス定義にないフィールド情報が送られてきたら自動でフィールドが作られる機能である。これの要不要を検討する。

 基本的にはインデックス定義をちゃんと作るアプリケーションでは、無効化しておいたほうが良いと思う。もしログ基盤を作る場合、はじめから内部に入れる情報を知ることは出来ないので、そのような場合のみ有効化すると良さそう。

 検討した結果、Dynamic Mapping機能は無効化にし、かつもし間違ったフィールド名を渡した時にエラーとなるように、strictモードにすることにした。

_sourceフィールドの無効化

 _source field | Elasticsearch Guide [8.0] | Elastic にあるように、デフォルトではドキュメントを作る時に渡したフィールド全てが_sourceフィールドに保存されている。もし非常に大きなドキュメントをインデックスする場合、_sourceフィールドを保存しているとデータ容量の点で問題が起こる可能性がある。そこで_sourceフィールドの要不要を検討する。


 _sourceフィールドを無効にするとデータ容量の問題は緩和されるが、その代わり様々なデメリットが存在する。例えばあるドキュメントの差分更新ができなくなる(update APIが使えなくなる)、ハイライト機能が使えなくなる、reindex APIが使えなくなるなどである。公式は、_sourceフィールドを無効にする前に圧縮オプションの見直しをすることをおすすめしている。

 もしそれでも_sourceフィールドを無効にしたい場合、store というオプションを併用し、必要なフィールドだけ保存すると良いだろう。


 今回は_sourceフィールドの無効化によるデメリットが大きいと考え、有効のままにしておく。

インデックスにalias名を付けるか

 Index Aliasesのドキュメントにあるとおり、インデックスにはalias名を付けることができる。alias名を付けることにより、停止せずに再インデックスして切り替えるなどといったことができるようになる。

 基本的には今後Elasticsearchを使うなら、alias名を付けておいたほうが良いと感じる。例えばblog-20160830のようにインデックス定義を作った日付をインデックスの実態としておき、alias名としてblogと付けておく。このようにすれば、もしインデックス定義を更新したいときはblog-20160911のように新しくインデックス定義を作り、blogというaliasの向き先をこちらに変えるということができる。

 他にもalias名を作ることで、Elasticsearch インデックス・エイリアス. Elasticsearch Index Aliases —… | by Kunihiko Kido | Hello! Elasticsearch. | Mediumに紹介されているようなことが可能になる。


 結論としてalias名を付けたほうが良いという結論になったが、今回は説明を簡単にするため、alias名は付けないこととする。

最終的なインデックス定義のJSONを作る

 ここまででインデックス定義が決まった。

 ブログ記事をインデックスするので、インデックス名はblog、タイプ名はentryとする。

 entryタイプのフィールドと型は次のとおり。

  • title : string, 全文検索のためのanalyzerで処理
  • content : string, 全文検索のためanalyzerで処理
  • categories : string
  • analyzed_categories : string, 全文検索のためanalyzerで処理
  • blog_id : long
  • status : string
  • pv : integer
  • bookmark_count : integer
  • published_at : date, date_time_no_millis


 全文検索のためのanalyzer設計は以下のとおり。

  • Character filters : icu_normalizer
  • Tokenizer : kuromoji_tokenizerのsearch mode
  • Token filters : kuromoji_part_of_speech


 そしてオプションとしてDynamic Mappingを無効化する。


 最終的なインデックス定義のJSONは以下のようになった。

{
  "settings": {
    "index": {
      "analysis": {
        "tokenizer": {
          "ja_text_tokenizer": {
            "type": "kuromoji_tokenizer",
            "mode": "search"
          }
        },
        "analyzer": {
          "ja_text_analyzer": {
            "tokenizer": "ja_text_tokenizer",
            "type": "custom",
            "char_filter": [
              "icu_normalizer"
            ],
            "filter": [
              "kuromoji_part_of_speech"
            ]
          }
        }
      }
    }
  },
  "mappings": {
    "entry": {
      "dynamic": "strict",
      "properties": {
        "title": {
          "type": "string",
          "analyzer": "ja_text_analyzer"
        },
        "content": {
          "type": "string",
          "analyzer": "ja_text_analyzer"
        },
        "categories": {
          "type": "string",
          "index": "not_analyzed",
          "copy_to": "analyzed_categories"
        },
        "analyzed_categories": {
          "type": "string",
          "analyzer": "ja_text_analyzer"
        },
        "blog_id": {
          "type": "long"
        },
        "status": {
          "type": "string",
          "index": "not_analyzed"
        },
        "pv": {
          "type": "integer"
        },
        "bookmark_count": {
          "type": "integer"
        },
        "published_at": {
          "type": "date",
          "format": "date_time_no_millis"
        }
      }
    }
  }
}

 ほとんど先ほど説明したとおりだが、categoriesのフィールド定義のindex not_analyzedとcopy_toというオプションだけ説明をしていない。

 not_analyzedというのはstring型の全文検索のための処理をしないようにするオプション。デフォルトで勝手に全文検索の処理を行ってしまうので、明示的にnot_analyzedを指定している。

 またcategoriesとanalyzed_categoriesの両方のフィールドを作るために、copy_toというオプションを利用している。これによりcategoriesフィールドにデータを入れるだけで、analyzed_categoriesにもコピーされる。


 あとはこれで実際にブログ用インデックスを作ってみて、ドキュメントを試しに入れ、結果を見てみると良い。

 上記jsonをblog-mapping.jsonというファイルに保存した上で、Elasticsearchに定義を作る。

$ curl -XPOST http://localhost:9200/blog -d @blog-mapping.json

 次に以下のようにしてドキュメントを一つ作る。

$ curl -XPOST http://localhost:9201/blog/entry/1 -d '{
  "title": "Rubyによるアプリケーション設計入門",
  "content": "この記事ではRubyによるアプリケーション設計に入門するための情報を書きます。",
  "categories": ["Ruby入門", "アプリケーション設計"],
  "blog_id": 12345,
  "status": "published",
  "pv": 98765,
  "bookmark_count": 123,
  "published_at": "2016-08-30T12:34:56+09:00"
}'

 最後に実際にドキュメントを検索し、どのようにインデックスされているか確認する。

$ curl -XGET "http://localhost:9200/blog/entry/_search?pretty=1" -d'
{
   "query": {
       "match_all": {}
   },
   "fielddata_fields": ["title", "content", "categories", "analyzed_categories"]
}'

 すると以下の結果が返ってきて、意図通りにインデックスされていることが分かる。

{
  "took" : 123,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 1.0,
    "hits" : [ {
      "_index" : "blog",
      "_type" : "entry",
      "_id" : "1",
      "_score" : 1.0,
      "_source" : {
        "title" : "Rubyによるアプリケーション設計入門",
        "content" : "この記事ではRubyによるアプリケーション設計に入門するための情報を書きます。",
        "categories" : [ "Ruby入門", "アプリケーション設計" ],
        "blog_id" : 12345,
        "status" : "published",
        "pv" : 98765,
        "bookmark_count" : 123,
        "published_at" : "2016-08-30T12:34:56+09:00"
      },
      "fields" : {
        "categories" : [ "Ruby入門", "アプリケーション設計" ],
        "analyzed_categories" : [ "ruby", "アプリケーション", "入門", "設計" ],
        "title" : [ "ruby", "アプリケーション", "入門", "設計" ],
        "content" : [ "ruby", "この", "する", "ため", "アプリケーション", "入門", "情報", "書き", "記事", "設計" ]
      }
    } ]
  }
}

まとめ

 今回はElasticsearchのインデックス定義を設計する手順についてまとめてみた。基本的にはこの手順をベースに、やりたいことに従って、フィールドを増やす、別のタイプを作る、全文検索のための処理をチューニングするなどを行っていくと良いと思う。

JavaScriptのPromiseの概念について学習した時に参考にしたもの

JavaScriptのPromiseを使った実装をコードレビューしていたのだけど、やってみたら自分があんまりPromiseについて理解できていなかったことに気づいた。特にこれまでjQueryajaxjQuery Deferredをなんとなく使っていたのだが、Promiseの根本的な概念がjQueryの巨大な仕様の中に埋もれていて、自分の中でjQueryの機能とPromiseの概念を分離して正しく言語化出来ていなかった。

そこでPromiseの根本的な概念を再学習したので、何を参考に勉強したかを書いておく。


PromiseについてはとにかくJavaScript Promiseの本を読めば大体把握できる。このドキュメントを読めば

  • Promiseの概念
  • ES6 Promiseの利用方法
  • Deferredとの関係性

などといったことを知ることができる。

個人的には次の部分が特に参考になった。

  • thenでもrejectする
    • thenに渡すcallbackで返す値がPromiseの時にどうなるか説明されている
  • DeferredとPromise
    • DeferredとPromiseの関係性が説明されている
    • Deferredの簡易実装が紹介されているので非常に理解しやすい


これを読んだ後、爆速でわかるjQuery.Deferred超入門 - Yahoo! JAPAN Tech Blogを読みなおした。これを読むときは、先ほどのドキュメントで学んだPromiseの根本的な概念とjQueryのDeferredの機能との対応を意識しておくと、理解しやすかった。


大体この二つのドキュメントを読んだらPromiseについて再学習することが出来た。さらに学びたければ自分でPromiseの簡易実装をしてみると良いかもしれない。