この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
Elasticsearch 初学者の中村です。
前回はElasticsearchで検索する方法を学びました。 今回は検索するためにElasticsearchがどのようにデータを管理しているか、転置インデックスやマッピング、アナライザについて書いてみます。
その2はこちら
転置インデックス
Elasticsearchではドキュメントをindexする際、文字列を解析して転置インデックスを作成します。
転置インデックスとはRDBのインデックスと似て本の索引のような情報です。文字列に出現する単語を解析しそれがどのドキュメントに現れるのかをまとめます。
- Alice and Bob exchange keys.
- Charlie and Bob do some math.
例えば上記2つの文字列で転置インデックスを作ると
alice
は1番のドキュメントに含まれるbob
は1, 2番のドキュメントに含まれるcharlie
は2番のドキュメントに含まれるkey
は1番のドキュメントに...
という形で、単語とそれが含まれるドキュメントIDを紐付けていきます。検索のときには検索キーワードと転置インデックスを突き合わせて、単語が存在するドキュメントIDを特定します。
アナライザ
上記例で単語が小文字になっていたり、keys
がkey
になっていましたが誤字ではなくElasticsearchの処理によるものです。
転置インデックス作成前に走る処理をアナライザ(analyzer)と言います。
アナライザは3つのコンポーネントで構成されています。
- Character Filter
- Tokenizer
- Token Filter
Tokenizerが単語を抽出し分かち書きするコンポーネントで、Character Filter, Token FilterはTokenizerの前後の処理です。 Elasticsearchでは標準でいくつか用意されていますが、用途に応じて独自に定義したりプラグインを導入することも可能です。
アナライザの動きはAnalize APIで確認することが出来ます。
Character Filter
分かち書きの前処理になります。
HTMLタグを除去したり、文字列を置換したりすることが出来ます。
たとえばHTMLタグを除去、 色色
という文字列を 色々
に変換。というCharacter Filterを設定して <b>人生色色</b>
という文字列がどうなるか見てみます。
リクエスト
GET _analyze
{
"char_filter": [
"html_strip",
{
"type" : "mapping",
"mappings" : [
"色色 => 色々"
]
}
],
"text": "<b>人生色色</b>"
}
レスポンス
{
"tokens" : [
{
"token" : "人生色々",
"start_offset" : 0,
"end_offset" : 4,
"type" : "word",
"position" : 0
}
]
}
HTMLタグと文字列の置換を行えました。 この処理により以降のTokenizerやToken Filterで単語を扱いやすくしたり転置インデックスの精度をあげることが出来ます。
Tokenizer
Character Filterにより整形された文字列を分かち書きするコンポーネントです。 Tokenizerは重要で様々な種類が提供されているので、利用ケースによって適切なものを選択してください。
https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-tokenizers.html
今回はElasticsearchが用意しているTokenizerのStandard tokenizerを利用してみましょう。
The standard tokenizer provides grammar based tokenization (based on the Unicode Text Segmentation algorithm, as specified in Unicode Standard Annex #29) and works well for most languages.
文法ベースのtokenizerで大体の言語で利用できるようです。
リクエスト
GET _analyze
{
"analyzer": "standard",
"text": "tom&jerry are good friends."
}
レスポンス
{
"tokens" : [
{
"token" : "tom",
"start_offset" : 0,
"end_offset" : 3,
"type" : "<ALPHANUM>",
"position" : 0
},
{
"token" : "jerry",
"start_offset" : 4,
"end_offset" : 9,
"type" : "<ALPHANUM>",
"position" : 1
},
{
"token" : "are",
"start_offset" : 10,
"end_offset" : 13,
"type" : "<ALPHANUM>",
"position" : 2
},
{
"token" : "good",
"start_offset" : 14,
"end_offset" : 18,
"type" : "<ALPHANUM>",
"position" : 3
},
{
"token" : "friends",
"start_offset" : 19,
"end_offset" : 26,
"type" : "<ALPHANUM>",
"position" : 4
}
]
}
tom
jerry
are
good
friends
とキレイに単語毎に別けることができました。
tom&jerry
と入力してもきちんと分けてくれます。
日本語を扱う場合
日本語を分かち書きはそのままでは上手くいきません。
トムとジェリーはとても仲良しです
という短文をStandard analyzerで実行すると
トム
と
ジェリー
は
と
て
も
仲
良
し
で
す
と分かち書きされてしまいます。
Standard analyzerは英語であれば単語やスペース毎に区切れたりしますが、日本語の分かち書きには向かないようです。
日本語の場合には辞書等を用いて、意味のある単語で区切る形態素解析を行う必要があります。 Elasticsearchではそのためのプラグインkuromojiを提供しています。
https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html
sudo bin/elasticsearch-plugin install analysis-kuromoji
Elasticsearchをインストールしたディレクトリで上記コマンドを実行するだけで利用が可能です。
tokenizer
に kuromoji_tokenizer
を指定して先ほどと同じ文字列を解析してみましょう。
リクエスト
GET _analyze
{
"tokenizer": "kuromoji_tokenizer",
"text": "トムとジェリーはとても仲良しです"
}
レスポンス
{
"tokens" : [
{
"token" : "トム",
"start_offset" : 0,
"end_offset" : 2,
"type" : "word",
"position" : 0
},
{
"token" : "と",
"start_offset" : 2,
"end_offset" : 3,
"type" : "word",
"position" : 1
},
{
"token" : "ジェリー",
"start_offset" : 3,
"end_offset" : 7,
"type" : "word",
"position" : 2
},
{
"token" : "は",
"start_offset" : 7,
"end_offset" : 8,
"type" : "word",
"position" : 3
},
{
"token" : "とても",
"start_offset" : 8,
"end_offset" : 11,
"type" : "word",
"position" : 4
},
{
"token" : "仲良し",
"start_offset" : 11,
"end_offset" : 14,
"type" : "word",
"position" : 5
},
{
"token" : "です",
"start_offset" : 14,
"end_offset" : 16,
"type" : "word",
"position" : 6
}
]
}
トム
と
ジェリー
は
とても
仲良し
です
と日本語の単語単位で分かち書きをしてくれました。
kuromojiにはtokenizerだけでなく、Character Filter, Token Filterも用意されています。
リクエスト
GET _analyze
{
"char_filter": ["kuromoji_iteration_mark"],
"tokenizer": "kuromoji_tokenizer",
"filter": [
"kuromoji_baseform",
"kuromoji_part_of_speech"
],
"text": "日々ネズミを捕まえています"
}
レスポンス
{
"tokens" : [
{
"token" : "日日",
"start_offset" : 0,
"end_offset" : 2,
"type" : "word",
"position" : 0
},
{
"token" : "ネズミ",
"start_offset" : 2,
"end_offset" : 5,
"type" : "word",
"position" : 1
},
{
"token" : "捕まえる",
"start_offset" : 6,
"end_offset" : 9,
"type" : "word",
"position" : 3
},
{
"token" : "いる",
"start_offset" : 10,
"end_offset" : 11,
"type" : "word",
"position" : 5
}
]
}
日々
の繰り返し文字を日日
と変換してくれる Character Filterkuromoji_iteration_mark
捕まえて
を捕まえる
にまるめてくれるkuromoji_baseform
を
等の品詞を除外してくれるkuromoji_part_of_speech
用途に合わせて様々な設定をすることが出来ます。
GET _analyze
{
"analyzer": "kuromoji",
"text": "日々ネズミを捕まえています"
}
analyzer
に kuromoji
を設定することで細かく指定しないで使うことも可能です。
Token Filter
形態素解析して分かち書きした単語への後処理になります。
.filter
に単語を小文字化する lowercase
、 品詞を除外する stop
を指定してみます。
リクエスト
GET _analyze
{
"tokenizer": "standard",
"filter": ["lowercase", "stop" ],
"text": "To Be Or Not To Be, That Is The Question"
}
レスポンス
{
"tokens" : [
{
"token" : "question",
"start_offset" : 32,
"end_offset" : 40,
"type" : "<ALPHANUM>",
"position" : 9
}
]
}
to
be
or
not
等の品詞は除外され、最終的には小文字化された question
のみが残りました。
.filter
の順序を stop
lowercase
の順にした場合、To
Be
等の大文字では品詞が除外されないため結果が異なるので、実行順序に気をつけましょう。
Token Filterの種類については公式をご参考ください。
アナライザのまとめ
Character Filter → Tokenizer → Token Filter と処理を通した単語とドキュメントIDを元に転置インデックスが作成されます。
アナライザを通すことにより、不要な単語を覗いたり語句をまるめたりすることで精度の高い全文検索が可能になります。
検索クエリ
アナライザはドキュメントをindexする時だけでなく、検索APIを呼んだ時の検索キーワードに対しても実行されます。
Elasticsearch入門 転置インデックス
の検索した際には、アナライザによって elasticsearch
入門
転置インデックス
の形に言語処理され、転置インデックスと突き合わせを行います。
マッピング
その1では、Elasticsearchのマッピングについて少しだけ言及していました。
各フィールドには型を定義することができ、それをmappingと言います。Elasticsearchではマッピングが未指定の場合、documentの値から自動で型を定義してくれます。
マッピングはドキュメントのスキーマ定義であり、フィールド名や型、どのアナライザを利用するか等を保持するメタ情報です。
型
Elasticsearchでマッピング出来る型は、公式をご参考ください。
主要なのとしては以下があります。
- boolean
true
false
の真偽値
- long, integer, short, byte, double, float
- 数値
- object
- JSONオブジェクト
- ip
- IPアドレス(IPv4, IPv6)
- geo_point, geo_shape
- 地理情報
- text
- 部分一致に利用する文字列
- アナライザを通す
- keyword
- 完全一致に利用する文字列
- アナライザを通さない
- メールアドレス、URL、タグ等で利用
定義リクエスト
ブログ情報を格納するインデックス blogs
を定義してみましょう。
リクエスト
PUT blogs
{
"mappings": {
"properties": {
"content" : {
"type": "text",
"analyzer": "kuromoji"
},
"url" : {
"type": "keyword"
},
"timestamp": {
"type": "date"
},
"author" : {
"type": "text",
"fields": {
"raw" : {
"type" : "keyword"
}
}
}
}
}
}
このリクエストによって blogs
インデックスにマッピングを作成しました。
content
フィールドはtext型で、日本語の記事内容が入る前提なのでアナライザにkuromojiを指定url
フィールドは完全一致でしか利用しないため、型はkeywordtimestamp
フィールドにはタイムスタンプを入力する想定でdate型
author
フィールドは著者名で検索する場合を想定しtext型にしたのですが、完全一致を利用するケースも想定されるため author.raw
をkeyword型にしました。
このように1つのフィールドで複数の使い方が想定される場合、マルチフィールドを定義することができます。
マッピングを定義したのでドキュメントをindexしてみましょう。
リクエスト
PUT blogs/_bulk
{"index" : {"_id" : 1}}
{"content" : "Elasticsearchやってみた", "url" : "http://test.example.com", "author" : "中村", "timestamp" : 111}
{"index" : {"_id" : 2}}
{"content" : "Elasticsearchのアナライザやマッピングを勉強してみた", "url" : "http://test1.example.com", "author" : "NKMR", "timestamp" : 2}
{"index" : {"_id" : 3}}
{"content" : "池袋のサウナに行ってみたレポ", "url" : "https://sauna.example.com", "author" : "saunakamura", "timestamp" : 3, "is_sauna" : true}
indexが成功したので、今度はマッピングを取得してみます。
リクエスト
GET blogs/_mapping
レスポンス
{
"blogs" : {
"mappings" : {
"properties" : {
"author" : {
"type" : "text",
"fields" : {
"raw" : {
"type" : "keyword"
}
}
},
"content" : {
"type" : "text",
"analyzer" : "kuromoji"
},
"is_sauna" : {
"type" : "boolean"
},
"timestamp" : {
"type" : "date"
},
"url" : {
"type" : "keyword"
}
}
}
}
}
自分で定義したマッピング情報と、新しく追加したフィールド情報が返ってきました。
日本語で検索して無事にHITしました。
Elasticsearchでは、既存のマッピングを後から変更することが難しいです。 なので、利用用途をしっかり洗い出し定義しましょう。
最後に
今回はマッピングやアナライザ等、Elasticsearchのメタ情報や仕組みを勉強しました。
次回はクラスタやノードについて書いていこうと思います。