初心者のためのElasticsearchその2 -いろいろな検索-

2018.09.22

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

その1ではセットアップから基本的なクエリについて紹介しました。 今回はESで最も使用するであろう、いろいろな検索をしてみます。 ※本稿で使用する環境については前回記事を参照してください

また、今回esで日本語を扱うためにkuromojiプラグインを使用するので、 このへんとかを参考にインストールしておきましょう。

ちなみに転置インデックスとかanalyzer/tokenizerとか検索のしくみとかについてはここではふれません。

サンプルデータを用意

まずはサンプル用データを用意します。 user-bulk.jsonという名前でjsonファイルを作成しましょう。 これを元にいろいろ検索してみます。

{"index":{"_index":"user_index","_type":"_doc","_id":"1"}}
{"name": "鈴木 太郎","married":false,"hobby":["読書","スポーツ"],"gender":"male","email": "suzuki.taro@example.com","birth": 19800101,"address":{"city": "東京都","detail": "江戸川区1-1-1"}}
{"index":{"_index":"user_index","_type":"_doc","_id":"2"}}
{"name": "山田 花子","married":false,"hobby":["映画鑑賞","スポーツ"],"gender":"female","email": "yamada.hanako@example.com","birth": 19860303,"address":{"city": "東京都","detail": "新宿区1-2-3"}}
{"index":{"_index":"user_index","_type":"_doc","_id":"3"}}
{"name": "中村 修太","married":true,"hobby":["音楽鑑賞"],"gender":"male","email": "nakamura.shuta@example.com","birth": 19790910,"address":{"city": "埼玉県","detail": "さいたま市4-5-6"}}
{"index":{"_index":"user_index","_type":"_doc","_id":"4"}}
{"name": "マイケル J raccoon","married":false,"hobby":["読書"],"gender":"male","email": "mike@sample.com","birth": 20001020,"address":{"city": "京都府","detail": "京都市8-8-8"}}
{"index":{"_index":"user_index","_type":"_doc","_id":"5"}}
{"name": "浪速 五右衛門","married":false,"hobby":["園芸","料理"],"gender":"male","email": "goemon@sample.com","birth": 19800725,"address":{"city": "北海道","detail": "札幌市5-4-2"}}
{"index":{"_index":"user_index","_type":"_doc","_id":"6"}}
{"name": "吉永 由香里","married":true,"hobby":["読書","映画鑑賞"],"gender":"female","email": "yoshinaga.yukari@yoshinaga.com","birth": 19991115,"address":{"city": "滋賀県","detail": "大津市4-7-1"}}
{"index":{"_index":"user_index","_type":"_doc","_id":"7"}}
{"name": "アレン パーカー","married":true,"hobby":["料理"],"gender":"male","email": "aren@classmethod.jp","birth": 20030722,"address":{"city": "沖縄県","detail": "那覇市9-7-6"}}
{"index":{"_index":"user_index","_type":"_doc","_id":"8"}}
{"name": "我那覇 太郎","married":false,"hobby":["スポーツ"],"gender":"male","email": "ganaha@example.jp","birth": 19790329,"address":{"city": "沖縄県","detail": "那覇市1-2-3"}}
{"index":{"_index":"user_index","_type":"_doc","_id":"9"}}
{"name": "米田 洋介","married":true,"hobby":["園芸","音楽鑑賞"],"gender":"male","email": "kome@classmethod.jp","birth": 19650603,"address":{"city": "新潟県","detail": "新潟市3-5-7"}}
{"index":{"_index":"user_index","_type":"_doc","_id":"10"}}
{"name": "明 太子","married":false,"hobby":["料理","スポーツ"],"gender":"female","email": "mentaiko@classmethod.jp","birth": 20020123,"address":{"city": "福岡県","detail": "博多市7-6-5"}}

jsonファイルは_bulkオペレーションを使えば登録できます。

% curl -H "Content-Type: application/json" -XPOST "localhost:9200/user_index/_bulk" --data-binary @user-bulk.json

これで、user_indexというインデックスにサンプルデータが登録されました。

いろいろな条件で検索してみる

では実際に検索してみます。前回同様、kibanaのdev toolをつかいます。 今回使用するインデックスはuser_indexだけなので、検索するURLは http://localhost:9200/user_index/_search となります。 もし複数のインデックスを横断して検索する場合には、 http://localhost:9200/_search となります。

match_all - すべてのドキュメントをかえす

最初は、インデックスから全てのドキュメントを検索するクエリを実行してみましょう。

GET /user_index/_search
{
"query": { "match_all": {} }
}

esでは、Query DSLというJson形式の言語を使って検索を実行します。 このクエリのqueryはクエリを定義しており、match_allで指定したインデックス内のドキュメントをすべて検索しています。 ちなみに、かえすドキュメントのサイズはデフォルトで10件になってます。

from/size - 件数の指定

SQLと同じ要領でfrom/sizeを指定できます。

  • size: 検索結果で取得する数。デフォルトは10。
  • from: スキップする検索結果数を指定します。デフォルトは0です。
GET /user_index/_search
{
"query": {
"match_all": {}
},
"from" : 3,
"size" : 5
}

上記クエリならドキュメントの4件目から5件分を取得します。

sort- ソート

取得したドキュメントを並び替えます。 asc/descを指定してソート可能です。

GET /user_index/_search
{
"query": {
"match_all": {}
},
"sort" : [{"_id":"asc"}]
}

*フィールドに複数の値(数値)がはいっている場合、modeを指定すれば合計値や平均値でソートすることも可能

_source - 取得するフィールドを指定

_sourceでフィールドを指定することで、取得するフィールドを絞ることができます。 パフォーマンスや通信量に影響するので、やみくもに全フィールドを取得しないようにしましょう。

GET /user_index/_search
{
"query": {
"match_all": {}
},
"_source" : ["name","birth"]
}

レスポンス例は↓のようになります。

・・・
"hits": [
{
"_index": "user_index",
"_type": "_doc",
"_id": "5",
"_score": 1,
"_source": {
"name": "浪速 五右衛門",
"birth": "198007025"
}
},
・・・

match - 条件にマッチ(Full text queries)

全文検索においてもっともよく使用すると思われる検索方法です。 これは、対象のフィールドに指定した文字列で検索を行います。 指定した検索文字列は解析(Analyze)されて検索されます。

下記クエリでは、address.cityフィールドで「東京都」というワードにマッチするデータを検索します。

GET /user_index/_search
{
"query": {
"match": { "address.city" : "東京都"}
},
"_source" : ["name","address"]
}

レスポンスをみると3件の結果がかえってきました。

・・・
"hits": [
{
"_index": "user_index",
"_type": "_doc",
"_id": "1",
"_score": 2.0794415,
"_source": {
"address": {
"city": "東京都",
"detail": "江戸川区1-1-1"
},
"name": "鈴木 太郎"
}
},
{
"_index": "user_index",
"_type": "_doc",
"_id": "2",
"_score": 1.9208364,
"_source": {
"address": {
"city": "東京都",
"detail": "新宿区1-2-3"
},
"name": "山田 花子"
}
},
{
"_index": "user_index",
"_type": "_doc",
"_id": "4",
"_score": 0.94000727,
"_source": {
"address": {
"city": "京都府",
"detail": "京都市8-8-8"
},
"name": "マイケル J raccoon"
}
}
]
・・・

address.cityが東京都のドキュメントだけヒットするかと思いましたが、 京都府のマイケルもヒットしています。 これは、address.cityがmatchクエリによって指定されているからです。 matchはFull text queriesなので完全に一致していなくてもヒットします。 実際には下記のように_analyzeを使用すれば、検索条件がどのように分割されているかわかります。

GET user_index/_analyze
{
"field" : "address.city",
"text" : "東京都"
}

現状の設定では「東京都」は3つのトークンに分割されて検索されているので、 京都府のドキュメントがヒットしています。

{
"tokens": [
{
"token": "東",
"start_offset": 0,
"end_offset": 1,
"type": "",
"position": 0
},
{
"token": "京",
"start_offset": 1,
"end_offset": 2,
"type": "",
"position": 1
},
{
"token": "都",
"start_offset": 2,
"end_offset": 3,
"type": "",
"position": 2
}
]
}

*なお、N-gramとか形態素解析とかについて確認したいかたはこちら

matchクエリ内のフレーズはスペース区切りで入力すれば、語句のいずれかにマッチした場合に取得できます。 これをいずれかでなくどちらも含む場合に取得したい場合、match_phrase(フレーズ検索)を使います。

GET /user_index/_search
{
"query": {
"match_phrase": { "email" : "goemon sample.com"}
},
"_source" : ["name","address"]
}

この場合、emailにgoemonかつsample.comを含んでいるデータが取得されます。

複数のフィールドにまたがって検索したい場合、multi_matchを使います。 下記クエリの場合、nameまたはaddress.cityに東京という語句がマッチする場合に結果を返します。

GET /user_index/_search
{
"query": {
"multi_match": {
"fields": [ "name", "address.city"],
"query": "東京"
}
},
"_source" : ["name","address"]
}

term/terms - 条件にマッチ(Term level queries)

matchクエリと同様、termクエリも指定した値で絞り込むためのクエリです。 こちらはTerm level queriesなので言語処理されません。 termを使うフィールドは、IDやBOOL型のフィールド、もしくはnot_analyzedな文字列型フィールドでよく使用されます。

GET /user_index/_search
{
"query": {
"term": {
"_id": "1"
}
},
"_source" : ["name"]
}

not_analyzedでないフィールドでtermを使って完全一致検索をしたい場合、 インデックスされるときもAnalyzeされないkeywordフィールドを指定することで検索可能になります。

GET user_index/_search
{
"query": {
"term": {
"name.keyword": "我那覇 太郎" // ← *.keywordを指定して完全一致検索
}
}
}

また、term条件を複数指定したい場合はtermsを使います。 下記条件で検索すると、id = 1とid = 2のデータがヒットします。

GET /user_index/_search
{
"query": {
"terms": {
"_id": ["1","2"]
}
},
"_source" : ["name"]
}

filter - スコアに影響しない検索の条件

クエリで取得した結果に対して絞り込みを行うための機能です。 filterの検索結果では、スコアが計算されません。 通常、full text searchかスコアに影響するような条件があるときはクエリを使い、それ以外ではfilterを使うのが一般的のようです。 また、よく使うfilterは自動でキャッシュされるらしいです。

下記クエリでは、住所が東京都であるユーザーをfull text searchで検索し、hobbyがスポーツの人をフィルタリングします。

GET user_index/_search
{
"query" : {
"bool":{
"must":[{ "match": { "address.city":"東京都"}}],
"filter":[{"match": {"hobby": "スポーツ"}}]
}
}
}

基本的には全文検索やスコアに関係する検索はQuery contextでおこない、 それ以外はFilter contextでおこなうという方針でいきましょう。

range - 範囲の検索条件

rangeを使えば 範囲を指定することができます。rangeと以下の条件を組み合わせて範囲を指定します。

  • gte : 以上
  • lte : 以下
  • gt : より大きい
  • lt : 未満

例えば、下記クエリではbirthが19800101以上20000101以下のデータにマッチします。

GET user_index/_search
{
"query" : {
"bool":{
"must":[],
"filter":{
"range" : {
"birth": {
"gte": 19800101, "lte" : 20000101
}
}
}
}
}
}

bool - クエリをAND/OR/NOTで組み合わせ

複数のクエリ条件を組み合わせるためにはboolクエリを使います。

AND条件のクエリ AND条件はmustをつかって表現します。 下記クエリはhobbyがスポーツかつgenderがmaleのデータにヒットします。

GET user_index/_search
{
"query": {
"bool" : {
"must" : [
{ "match" : { "hobby" : "スポーツ" }},
{ "match" : { "gender" : "male" }}
]
}
}
}

これをSQLで表現すると、こんな感じになります。

select * from user_index where hobby = 'スポーツ' and gender = 'male';

OR条件のクエリ OR条件はshouldをつかって表現します。 下記クエリはhobbyがスポーツまたはgenderがmaleのデータにヒットします。

GET user_index/_search
{
"query": {
"bool" : {
"should" : [
{ "match" : { "hobby" : "スポーツ" }},
{ "match" : { "gender" : "male" }}
]
}
}
}

SQLで表現すると、こんな感じになります。

select * from user_index where hobby in ('スポーツ') or gender = 'male';

NOT条件のクエリ NOT条件はmust_notをつかって表現します。 下記クエリはhobbyにスポーツを含んでいないユーザーにヒットします。

GET user_index/_search
{
"query": {
"bool" : {
"must_not" : [
{ "match" : { "hobby" : "スポーツ" }}
]
}
}
}

SQLで表現すると、こんな感じになります。

select * from user_index where hobby not in ('スポーツ');

もう少し複雑な条件のクエリを書いてみます。 下のクエリは、「hobbyがスポーツかつgenderがfemale または birthが19790101以上19791231以下でないユーザー」を検索します。

GET user_index/_search
{
"query": {
"bool" : {
"should" : [
{
"bool":{
"must" : [
{ "match" : { "hobby" : "スポーツ" }},
{ "match" : { "gender" : "female" }}
]
}
},
{
"bool": {
"must_not" : {
"range" : {
"birth" : {"gte": 19790101, "lte": 19791231}
}
}
}
}
]
}
}
}

sqlだとこんなイメージです。

select * from user_index
where
(hobby in ('スポーツ') and gender = 'female')
or
(birth not between 19790101 and 19791231) ;

exists - フィールドの有無を判定

Elasticsearchはスキーマレスなので、ドキュメントによってフィールドが違う場合があります。 指定したフィールドがドキュメントに存在するかどうか確認する場合にはexistsクエリを使います。

GET user_index/_search
{
"query": {
"bool": {
"must": {
"exists": {
"field": "name"
}
}
}
}
}

このクエリではnameフィールドを持つドキュメントがヒットします。 逆にフィールドをもっていないドキュメントをヒットさせたい場合、mustでなくmust_notを使います。

まとめ

今回はいろいろな検索条件について試してみました。 紹介した以外にもいろいろなクエリがあるので、ドキュメント等ご確認ください。