Elasticsearch 超入門

YAPC::Kansai 2017.03.04 Sat @MOTEXホール
Yuji Shimada (@xaicron)

こんにちはこんにちは

きょうはとーっても便利なミドルウェアであるところの
Elasticsearch を紹介します

今日のアジェンダ

今日はなさないこと

はい

Elasticsearchってなに?

Elasticsearchってなに?

Elasticsearchってなに?

Elasticsearchってなに?

ただのデータストアではない!

ただのデータストアではない!

以下はほんの一部です

ただのデータストアではない!

ただのデータストアではない!

なんか夢の技術っぽいですね!!!

もちろんそんなことはなくて、ちゃんとパフォーマンス出るようにしたり、安定運用するにはそれなりの準備が必要です。

この辺は語ると長いので今回は省略

はい

Elasticsearchで
全文検索をサクッと実現

する前に

そもそも全文検索ってなんぞや

全文検索とは

全文検索とは

つらい😂

そこでElasticsearchですよ!

Elasticsearchの全文検索

Elasticsearchの全文検索

そろそろ文字ばっかりで疲れてきたと思うので実際に試してみましょう!!

Elasticsearchをつかってみる

$ elasticsearch # 立ち上げる
$ curl localhost:9200

Elasticsearchをつかってみる

ここでElasticsearch頻出ワードの紹介

と思っておいてください!実際にはだいぶ違うんですが...

Elasticsearchをつかってみる

APIでは以下のようなURLで値を更新したり、アクセスしたりできる

# indexの情報を取得
$ curl localhost:9200/{index}

# type配下のdocumentの検索
$ curl localhost:9200/{index}/{type}/_search

# 特定のdocumentにアクセス
$ curl localhost:9200/{index}/{type}/{id}

Elasticsearchをつかってみる

スキーマレスなので、いきなり適当にデータをぶっこんでも動く

# documentのidを指定しない場合、自動的にidが振られる
$ curl -XPUT localhost:9200/mytest/user/100 -d '{
    "user_name":"xaicron",
    "user_id":100
}'

これでいきなり作成できるし取得も可能

# pretty をつけるとみやすいよ!
$ curl "localhost:9200/mytest/user/100?pretty"
{
  "_index" : "mytest",
  "_type" : "user",
  "_id" : "100",
  "_version" : 1,
  "found" : true,
  "_source" : {
    "user_name" : "xaicron",
    "user_id" : 100
  }
}

これでこんな感じで検索することが出来ます

$ curl localhost:9200/mytest/user/_search -d '{
  "query": {
    "match": {
      "user_name": "xai"
    }
  }
}'

{
  "took" : 9,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 0.2876821,
    "hits" : [
      {
        "_index" : "mytest",
        "_type" : "user",
        "_id" : "100",
        "_score" : 0.2876821,
        "_source" : {
          "user_name" : "xaicron",
          "user_id" : 100
        }
      }
    ]
  }
}

簡単ですね!!

ただし、何も設定しない状態だといい感じの全文検索ができない

# xai で検索してみるとマッチしない
$ curl 'localhost:9200/mytest/user/_search?q=user_name:xai'
{
  "took": 4,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 0,
    "max_score": null,
    "hits": []
  }
}

自動的に作られた設定がどうなっているかみてみる

$ curl 'localhost:9200/mytest/_mapping/user'
{
  "mytest": {
    "mappings": {
      "user": {
        "properties": {
          "user_id": {
            "type": "long"
          },
          # typeがテキストでインデックスがkeywordになっていることが分かる
          "user_name": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          }
        }
      }
    }
  }
}

というわけで、設定をしていきます
実際にはもうちょっとちゃんとした日本語の文章を検索したいのでデータを用意しつつ、必要なプラグインをインストール

今回は、形態素解析ライブラリーの1つであるところのkuromojiを使ってみるので、以下のコマンドでサクッとインストールします

$ elasticsearch-plugin install analysis-kuromoji

次にAnalyzerの設定をします。
本当は、小文字に全て置換したり、かな変換したりするフィルターもここで設定できるのですが、今回はとりあえずtoknizerのみ設定しておきます。
Analyzerの設定はindexに対して行いますが、一度作成したら基本的には変更不可能なのでちゃんと設計してから実行しましょう。

$ curl -XPUT localhost:9200/kuromoji_test -d '{
  "index": {
    "analysis": {
      "tokenizer": {
        "kuromoji": {
          "type": "kuromoji_tokenizer"
        }
      },
      "analyzer": {
        "analyzer": {
          "type": "custom",
          "tokenizer": "kuromoji"
        }
      }
    }
  }
}'

これでちゃんとkuromojiが使えるようになっているのかテストしてみます。
indexに対して、_analyzeというAPIを叩くと、結果がわかります。
まずはなにも指定しないパターンだと、全部1文字ずつに分割されてしまっているのがわかります。

$ curl -XPOST localhost:9200/kuromoji_test/_analyze -d 'すもももももももものうち'
{
  "tokens": [
    {
      "token": "す",
      "start_offset": 0,
      "end_offset": 1,
      "type": "<HIRAGANA>",
      "position": 0
    },
    {
      "token": "も",
      "start_offset": 1,
      "end_offset": 2,
      "type": "<HIRAGANA>",
      "position": 1
    },
    {
      "token": "も",
      "start_offset": 2,
      "end_offset": 3,
      "type": "<HIRAGANA>",
      "position": 2
    },
    {
      "token": "も",
      "start_offset": 3,
      "end_offset": 4,
      "type": "<HIRAGANA>",
      "position": 3
    },
    {
      "token": "も",
      "start_offset": 4,
      "end_offset": 5,
      "type": "<HIRAGANA>",
      "position": 4
    },
    {
      "token": "も",
      "start_offset": 5,
      "end_offset": 6,
      "type": "<HIRAGANA>",
      "position": 5
    },
    {
      "token": "も",
      "start_offset": 6,
      "end_offset": 7,
      "type": "<HIRAGANA>",
      "position": 6
    },
    {
      "token": "も",
      "start_offset": 7,
      "end_offset": 8,
      "type": "<HIRAGANA>",
      "position": 7
    },
    {
      "token": "も",
      "start_offset": 8,
      "end_offset": 9,
      "type": "<HIRAGANA>",
      "position": 8
    },
    {
      "token": "の",
      "start_offset": 9,
      "end_offset": 10,
      "type": "<HIRAGANA>",
      "position": 9
    },
    {
      "token": "う",
      "start_offset": 10,
      "end_offset": 11,
      "type": "<HIRAGANA>",
      "position": 10
    },
    {
      "token": "ち",
      "start_offset": 11,
      "end_offset": 12,
      "type": "<HIRAGANA>",
      "position": 11
    }
  ]
}

つぎにkuromojiを指定したパターンだと、ちゃんと形態素解析されている風なのがわかります!やったー!

$ curl -XPOST "localhost:9200/kuromoji_test/_analyze?analyzer=kuromoji" -d 'すもももももももものうち'
{
  "tokens": [
    {
      "token": "すもも",
      "start_offset": 0,
      "end_offset": 3,
      "type": "word",
      "position": 0
    },
    {
      "token": "も",
      "start_offset": 3,
      "end_offset": 4,
      "type": "word",
      "position": 1
    },
    {
      "token": "もも",
      "start_offset": 4,
      "end_offset": 6,
      "type": "word",
      "position": 2
    },
    {
      "token": "も",
      "start_offset": 6,
      "end_offset": 7,
      "type": "word",
      "position": 3
    },
    {
      "token": "もも",
      "start_offset": 7,
      "end_offset": 9,
      "type": "word",
      "position": 4
    },
    {
      "token": "の",
      "start_offset": 9,
      "end_offset": 10,
      "type": "word",
      "position": 5
    },
    {
      "token": "うち",
      "start_offset": 10,
      "end_offset": 12,
      "type": "word",
      "position": 6
    }
  ]
}

ここまででkuromojiが使えるようになったので次はデータ型の設定をしましょう
Elasticsearchはスキーマレスなので適当にいれるとデフォルトのanalyzerや型が使われちゃうので、ちゃんと設定してあげる必要があります

_mappingというAPIを利用することで、事前に設定しておくことが出来ます
今回は、wikipediaからてきとうに取ってきたアニメタイトルを使うので以下のような感じにしました

$ curl -XPUT localhost:9200/kuromoji_test/_mapping/anime_title -d'
{
  "properties": {
    "title": {
      "type": "string",
      "index": "analyzed",
      "analyzer": "kuromoji"
    }
  }
}'

_bulk APIで一気にデータを流し込めるので、以下のようなJSONを用意してぶっこみましょう

$ cat anime_title.json
{"index":{}}
{"title":"イーグルサム"}
{"index":{}}
{"title":"家なき子"}
{"index":{}}
{"title":"家なき子レミ"}
{"index":{}}
{"title":"伊賀野カバ丸"}
{"index":{}}
{"title":"いきなりダゴン"}
{"index":{}}
{"title":"イクシオン サーガ DT"}
...

$ curl -XPOST localhost:9200/kuromoji_test/anime_lists/_bulk --data-binary @anime_title.json

ためしにヴァンパイアで検索してみるとこんな感じに!

$ curl localhost:9200/kuromoji_test/anime_title/_search -d '{
  "query" : {
    "match" : { "title" : "ヴァンパイア" }
  }
}'

{
  "took": 13,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 3,
    "max_score": 9.377503,
    "hits": [
      {
        "_index": "kuromoji_test",
        "_type": "anime_title",
        "_id": "AVqWsRzhkeMwOqfvLAY0",
        "_score": 9.377503,
        "_source": {
          "id": 48,
          "title": "ヴァンパイア騎士 ヴァンパイア騎士 Guilty"
        }
      },
      {
        "_index": "kuromoji_test",
        "_type": "anime_title",
        "_id": "AVqWsRzhkeMwOqfvLAY1",
        "_score": 7.0457544,
        "_source": {
          "id": 49,
          "title": "ヴァンパイア騎士 Guilty"
        }
      },
      {
        "_index": "kuromoji_test",
        "_type": "anime_title",
        "_id": "AVqWsRzhkeMwOqfvLAY2",
        "_score": 5.0536017,
        "_source": {
          "id": 50,
          "title": "吸血姫美夕(ヴァンパイアみゆ)"
        }
      }
    ]
  }
}

つぎは「ご注文」でやってみると...

$ curl localhost:9200/kuromoji_test/anime_title/_search -d '{
  "query" : {
    "match" : { "title" : "ご注文" }
  }
}'

{
  "took": 12,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 12,
    "max_score": 9.386059,
    "hits": [
      {
        "_index": "kuromoji_test",
        "_type": "anime_title",
        "_id": "AVqWsRzjkeMwOqfvLAiF",
        "_score": 9.386059,
        "_source": {
          "id": 641,
          "title": "ご注文はうさぎですか??"
        }
      },
      {
        "_index": "kuromoji_test",
        "_type": "anime_title",
        "_id": "AVqWsRzjkeMwOqfvLAiE",
        "_score": 9.313047,
        "_source": {
          "id": 640,
          "title": "ご注文はうさぎですか? ご注文はうさぎですか??"
        }
      },
      {
        "_index": "kuromoji_test",
        "_type": "anime_title",
        "_id": "AVqWsRzikeMwOqfvLAhz",
        "_score": 6.1055107,
        "_source": {
          "id": 623,
          "title": "ご近所物語"
        }
      },
      {
        "_index": "kuromoji_test",
        "_type": "anime_title",
        "_id": "AVqWsRzmkeMwOqfvLAy2",
        "_score": 5.5830846,
        "_source": {
          "id": 1714,
          "title": "姫様ご用心"
        }
      },
...

上位二件は良さそうだけど、なんか下の方が変...?

こういうときは analyzer をためしてみる
まずはうまく行った「ヴァンパイア」
これは良さそう

$ curl "localhost:9200/kuromoji_test/_analyze?analyzer=kuromoji" -d 'ヴァンパイア'
{
  "tokens": [
    {
      "token": "ヴァンパイア",
      "start_offset": 0,
      "end_offset": 6,
      "type": "word",
      "position": 0
    }
  ]
}

つぎは「ご注文」をやってみると、「ご」と「注文」に分かれていることが分かる!

$ curl "localhost:9200/kuromoji_test/_analyze?analyzer=kuromoji" -d 'ご注文'
{
  "tokens": [
    {
      "token": "ご",
      "start_offset": 0,
      "end_offset": 1,
      "type": "word",
      "position": 0
    },
    {
      "token": "注文",
      "start_offset": 1,
      "end_offset": 3,
      "type": "word",
      "position": 1
    }
  ]
}

query の match だと、トークン毎にマッチしたものをスコアリングして、関連度が高そうなものを検索するのでこのようなことになった。
さっきの例だと「ご」がトークンになっているタイトルがひっかかっていた。

実際に関連度の高さで検索したいケースもままあるが、今回のケースだと「入力した文字列」が含まれるものにマッチしてほしい。

Elasticsearchはそんな方法ももちろん用意していて、query_stringというのを使えばよい
これを使うといわゆるキーワードマッチ的なものに

$ curl localhost:9200/kuromoji_test/anime_title/_search -d '{
 "query" : {
  "query_string" : {
    "fields": ["title"],
    "query": "\"ご注文は\"" # <- この文字列にマッチさせたいときはダブルクォーテーションでくくる
  }
 }
}'
{
  "took": 55,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 2,
    "max_score": 12.363281,
    "hits": [
      {
        "_index": "kuromoji_test",
        "_type": "anime_title",
        "_id": "AVqWsRzjkeMwOqfvLAiF",
        "_score": 12.363281,
        "_source": {
          "id": 641,
          "title": "ご注文はうさぎですか??"
        }
      },
      {
        "_index": "kuromoji_test",
        "_type": "anime_title",
        "_id": "AVqWsRzjkeMwOqfvLAiE",
        "_score": 12.267111,
        "_source": {
          "id": 640,
          "title": "ご注文はうさぎですか? ご注文はうさぎですか??"
        }
      }
    ]
  }
}

やった〜!😊

はい

というわけで、Elasticsearchとはなんぞやというところから、軽く全文検索の触りの部分までお話させていただきました

もちろん使い方はこれだけではなくて、ログ解析だったり、ランキング作成だったり、レコメンドだったりいろいろな用途で使えます

全文検索の部分も、文字列の正規化や、辞書の作成だったりいろいろな要素がまだまだてんこ盛りで、本が一冊かけちゃうレベルなので、ちゃんと知りたい人は各自調べてみてくださいね!

サービスで大量のデータをバリバリ扱うだけでなく、手元でも簡単に動かせるので、個人レベルでのちょっとしたデータの解析なんかもサクッとできるようになって夢が広がりんぐです。

実はRDBMSとちがって、ドキュメント指向のシステムなので、そのへんもやりにくかったことが実現しやすくなってたりといろいろといい感じであります。

みんなもElasticsearchつかおうぜ!!

ご清聴ありがとうございました

ご質問タイム

j or →: next

k or ←: prev

h or ↑: list

l or ↓: return

o or ↵: open

? or /: toggle this help