AppBrew tech blog

AppBrewのエンジニアチームの日々です

Elasticsearchの基本的な知識について

はじめまして、AppBrewでコスメを中心としたコミュニティである「LIPS」のアプリや サーバーサイドを担当しています吉野です。

弊社ではあるコスメの/あるユーザーの投稿を探す というようなニーズを満たすために 全文検索エンジンとしてElasticsearchを使用しています。

そこで、いくつか学んだ点などがあったため、 Elasticsearch基礎編とElasticSearch+Railsの運用編の二本立てて書きたいと思います。

参考:https://www.elastic.co/guide/en/elasticsearch/guide/current/index.html

Elasticsearchとは

通常RDBで柔軟な検索をしようとすると、例えば以下のようなあえて非正規化したようなTableが必要となります。

id hira_name kana_name eiji_name
1      よしのかつき ヨシノカツキ    yoshinokatsuki

これでは非常に変更に弱く、カラムもいくつも増やす必要があり、実用的とは言えません。 我々のぞむインターフェイスとしては以下のような感じですよね。

f:id:yosshi0774:20180314000037p:plain

ここでいうところの、 なんかいい感じにしてくれるやつ がElasticsearch です。

もう少し詳しく言うと(よく言われている言葉を使わせて貰えば) ビジネス要件をJSONに落とし込むだけで全文検索が可能となる というやつです。

この記事ではElasticsearch基礎知識についてまとめたいと思います。

Elasticsearchの基礎知識

まず、上記の通りElasticsearchはDBのようにデータを保持し、扱うものの 通常のDBのような形ではないというわけです。それではどのような形でデータを扱い、やりとりするのでしょうか?

Elasticsearchとのふれあい方について

ElasticsearchはSQLの代わりに RESTful APIJSON により データの追加削除を行っています。

つまり、エンドポイントを知っていて、JSONが用意できれば どんなデバイスからでも利用することができるということですね。 (弊社ではすべてRailsサーバーを介して行われていますが)

ちなみにRDBとは用語が異なり以下のような用語の対応となっています。

RDB Elasticsearch
Database Index
Table (Mapping)Type
Column Field
Record Document

このような言葉遣いなため、Elasticsearchを扱う話題では index という言葉にいろいろ意味があったりしますが、品詞の違いや表記法の違いなどで区別しています。

  • index(名詞)
    • これは上記の通りDatabaseを指し、 慣例的にindexes , indices と書きます。
  • index(動詞)
    • これは新しいDocumentを追加すること(RDBでいうところのRecordを追加すること)を指します。こちらはindexする,indexingするなどと言うかもしれません。

また、columnとfieldが対応しているというからにはそのfieldがどんなものであるか?(railsでいうところのschema.rb)を定義するものはElasticsearchに存在し、 そのfieldはどのような型でどのようなAnalyzer(後述)を使用するか? ということを決めることができます。

ちなみにElasticsearchでは以下のような一般的な型を使用できるようになっています。

String: string Whole number: byte, short, integer, long Floating-point: float, double Boolean: boolean Date: date

参考: https://www.elastic.co/guide/en/elasticsearch/guide/2.x/mapping-intro.html

これを踏まえ弊社の投稿に関するmapping定義を見てみましょう。

[5] pry(main)> Post.__elasticsearch__.mappings
=> #<Elasticsearch::Model::Indexing::Mappings:0x007fa69dd41fc0
 @mapping=
  {:content=>{:analyzer=>"kuromoji", :type=>"string"},
   :n_content=>{:analyzer=>"ngram_analyzer", :type=>"string"},
   :like_count=>{:type=>"integer"},
   :media_status=>{:type=>"integer"},
   :info=>{:analyzer=>"ngram_analyzer", :type=>"string"},
   :products=>{:type=>"nested", :properties=>{:name=>{:analyzer=>"ngram_analyzer", :type=>"string"}, :id=>{:type=>"integer"}}},
   :brands=>{:type=>"nested", :properties=>{:name=>{:analyzer=>"ngram_analyzer", :type=>"string"}, :variant_name=>{:analyzer=>"ngram_analyzer", :type=>"string"}, :id=>{:type=>"integer"}}},
   :categories=>{:type=>"nested", :properties=>{:name=>{:analyzer=>"ngram_analyzer", :type=>"string"}, :id=>{:type=>"integer"}}},
   :tags=>{:type=>"nested", :properties=>{:name=>{:analyzer=>"keyword", :index=>"not_analyzed", :type=>"string"}, :id=>{:type=>"integer"}}},
   :user=>
    {:type=>"object",
     :properties=>
      {:name=>{:analyzer=>"ngram_analyzer", :type=>"string"},
       :nickname=>{:analyzer=>"kuromoji", :type=>"string"},
       :skin_type=>{:analyzer=>"kuromoji", :type=>"string"},
       :age_type=>{:analyzer=>"kuromoji", :type=>"string"}}},
   :published_at=>{:type=>"date"}},
 @options={:dynamic=>"false"},
 @type="post">

細かい話は置いておき、content(本文)はstringでkuromoji analyzerを使用しており、like_count(いいね数)はintegerで定義されているということがわかりますね。

用語の紹介はこれくらいにして、Elasticsearchがなにをしてくれているのか ということについて見ていきましょう。

Elasticsearchを使うと何が嬉しいの?

Elasticsearchは検索に留まらずログ解析などとしても利用可能ですが、 検索の部分に限った話をすれば嬉しいことは以下の2点です。

それぞれについて見ていきましょう

Analyzerによる正則化形態素解析

Analyzerとはその名の通り解析してくれるものであるわけですが、 以下の3つの要素から成り立ちます。

  • Character Filters
  • Tokenizer
  • Token Filters

再掲となりますがAnalyzerは図でいうところの ①データベースにあるデータを登録 の部分において、検索しやすいいい感じにしてくれるものです。

f:id:yosshi0774:20180314000037p:plain

それでは各要素の役割について例を挙げて見ていきましょう。

f:id:yosshi0774:20180314000055p:plain

上図のように各要素では

  • Character Filter
    • カタカナや特殊文字を所定の形に変換するフィルタ
  • Tokenizer
    • 形態素への分割、空白区切り、句読点区切りなどの規則に則り,文字列をtokenに変換する。
  • Token Filters
    • ノイズとなるようなtoken( 助詞や句読点 )を除くフィルタ

といったような役割があることがわかり、それぞれの要素(特にTokenizer)の設計が鍵となってくるわけですが、幸いなことに各言語においてAnalyzerはいくつか存在し、日本語においてはkuromoji というAnalyzerが存在します。 これにより莫大な構文解析済みコーパスなどを持っていなくても簡単に文字列を解析することが可能となっています。良い世の中ですね。

さて、以上のようにAnalyzerによる処理を介してRDBの各データはmappingに従い Elasticsearchへと保存されるわけですが、それらがどのように扱われるのでしょうか?


ちょっと雑談

実際、検索のためにanalyzerを使用しようとすると、一つのfield(column)に対して様々なAnalyzeをしたいというケースがありますが、もちろんこれらは可能です。 例えば文章を日本語でもtokenizeしたいし、ローマ字でもtokenizeしたいというようなケースです。 うまくAnalyzerを組み合わせることでfuzzyな検索にも対応できるというわけです。 それでは本文に戻りましょう。


転置インデックスによる高速な検索

tokenizeされた語句達は転置インデックスという索引方法により有用に扱うことができ、 最初の図でいうところの

②適当に検索 ③スコアリングしてデータを返してくれる

を実現するための索引方法ともいえます。

例えば、↑で使った例文(仮にid1としましょう)に加え 運転免許とるなら、合宿免許wa◯!! という例文があり [運転, 免許, とる, 合宿, wa◯] と解析されたとしましょう。(idは2としましょう。) この時の転置インデックスをすると、

token ids
合宿 1,2
免許 1,2
wa◯ 1,2
社長 1
トン 1
0.08 1
とる 2
運転 2

のようになるわけですね。 これがどのように使われるかをみるために、例えば[社長 免許 wa◯] というクエリで検索されたとしましょう。すると下の表のように集計されます。

token ids include?
合宿 1,2 false
免許 1,2 true
wa◯ 1,2 true
社長 1 true
トン 1 false
0.08 1 false
とる 2 false
運転 2 false

あくまでイメージですがこの例ではtrueが多いidのものから引っかかる というわけです。 すなはち今回の場合はスコアとしてid:1>id:2という関係になるわけですね。

まとめ

少し長くなりましたが、Elasticsearchがelasticにsearchしてくれることはおわかりいただけたかと思います。 以上のことをまとめると

①indexesを作成し ②analyzerを用意し ③mappingを定義し ④データを投入すれば

そこそこ快適な検索ライフがおくれるということになります。 このElasticsearchと弊社のRailsプロジェクトをどのように結びつけているか どう運用しているかについては次の記事で書こうかと思います。