Time Flies

fckey's Tech Blog

Elasticsearch 101

Elasticsearchを使う機会があったのでBasicsをメモ。

Elasticsearchとは?

ElasticsearchApacheプロジェクトの一つである検索用サーバでApache Luceneの上に構築されている。RESTfulなインターフェイスで分散型の構成や全文検索が可能であり、基本的に取り入れるデータの定義、検索クエリ共にJSONで記述される。

仕組み?

ESの導入や基本的な構成はこのあたりのページを見たらだいたいわかると思う。

http://code46.hatenablog.com/entry/2014/01/21/115620 http://engineer.wantedly.com/2014/02/25/elasticsearch-at-wantedly-1.html

基本的にESで重要なのは以下の2点である。

  1. 取り込むデータの定義
  2. 検索の詳細の定義

これらの中でも特にキモとなりそうな点を以下にメモ。

使用方法?

以下では実際にElasticsearchを動かしながらIndexの設定と簡単なクエリの発行を行う。 なお、今回使用した全てのJSONここにある。

取り込むデータの定義

Elasticsearchのインデックスは基本的にはスキーマレスではあるが、取り込むデータのフィールドの定義やdelimiterをmappingにて定義しなけれな成らない。 以下ではデータ整形とフィールド定義の2つのパートに分けて説明する。

データの整形フォーマットの定義

まずここでは取り込むデータの整形フォーマットの定義を行う。以下のJSONmappingの前半部分である。

整形方法の定義はanalyzerに記述され、このanalyzertokenizerfilter の組み合わせによって定義される。 tokenizerは文字列の分割方法を定義し、以下の例にあるngram_tokenは最小2文字、最大3文字のngramを利用する。 filterは分割後の文字列の整形処理を定義するのだが今回は利用しなかった。 analyzertokenizerfilterを組み合わせて複数定義することが可能であり、それぞれのfieldで異なるanalyzerを利用することが可能である。

  "settings": {
    "analysis": {
      "analyzer": {
        "ngram_analyzer": {
          "tokenizer": "ngram_token"
        }
      },
      "tokenizer": {
        "ngram_token": {
          "type": "nGram",
          "min_gram": "2",
          "max_gram": "3",
          "token_chars": [
            "letter",
            "digit"
          ]
        }
      }
    }
  }
フィールドの定義

以下のmappingの後半ではインデックスとして登録される各フィールドのデータ型、使用するanalyzerを定義する。

今回作成するmappingの"sushiya"では、それぞれのフィールドは寿司屋の名前、住所、緯度経度情報を保持する。 ここでは、nameはスペースごとに文字列を分割するデフォルトanalyzerである”whitespace”を使用し、addressに対しては上記にて定義したngam_analyzerを使用した。

 "mappings": {
    "sushiya": {
      "properties": {
        "name": {
          "type": "string",
          "analyzer": "whitespace"
        },
        "address": {
          "type": "string",
          "analyzer": "ngram_analyzer"
        },
        "location": {
          "type": "geo_point",
          "store": "yes"
        }
      }
    }
  }

以上にて定義した寿司屋情報保持用のmappingをsample_mapping_sushi.jsonとして保存し、以下のコマンドにてrestrantインデックスとして定義する。 curl -XPOST localhost:9200/restaurant -d @sample_mapping_sushi.json

データの登録

今回はお気に入りの寿司屋を以下のように定義し登録する。_bulkはまとめてデータを登録するためのパラメータである。

curl -XPOST localhost:9200/_bulk -d ' 
{ "index": { "_index": "restaurant", "_type": "sushiya", "_id": "1" } }
{ "id": "1", "name": "鮨水谷", "location" : [35.66836,139.761106], "address" :"東京都中央区銀座8-7-7" }
{"index": { "_index": "restaurant", "_type": "sushiya", "_id": "2" } }
{ "id": "2", "name": "すきばやし次郎", "location": [35.672614,139.764037], "address" : "東京都中央区銀座4-2-15" }
{ "index": { "_index": "restaurant", "_type": "sushiya", "_id": "3" } }
{ "id": "3", "name": "久兵衛", "location": [35.672614,139.764037], "address" : "東京都中央区銀座8-7-6" }
{ "index": { "_index": "restaurant", "_type": "sushiya", "_id": "4" } }
{ "id": "4", "name": "かねさか", "location": [35.667989,139.762643], "address" :"東京都中央区銀座8-10-3"}
{ "index": { "_index": "restaurant", "_type": "sushiya", "_id": "5" } }
{ "id": "5", "name": "鮨 なかむら", "location": [35.662347,139.729366], "address" : "港区六本木7丁目17−16" }
'

ここでデータの定義について軽く説明すると以下の通りである。

{ "index": <-インデックスの情報について
    { "_index": "restaurant",  <-どのインデックスを利用するか
      "_type": "sushiya",  <-どのマッピングをを利用するか
      "_id": "1"          <-keyとなる任意の文字列
    } }
{ "id": "1", <-先に定義したidと一致するように定義
 "name": "鮨水谷", <-ミシュラン三ツ星(高い)
"location" : [35.66836,139.761106], <-geo_point typeの形式に沿った緯度経度情報
"address" :"東京都中央区銀座8-7-7"   <-銀座は高値の象徴
}

検索の詳細の定義

Elasticsearchの検索はqueryfilterによって行い, 抑えるべき点は

  • queryはscoreに影響する。
  • filterはscoreへの影響はないがキャッシングの有無の選択が可能

という点である。詳しくは公式のリファレンスを追って欲しい。

クエリの定義

ここではまず、簡潔なクエリの定義を行う。以下のクエリは鮨という単語を全てのフィールドに対して検索している。

{
  "query" : {
    "simple_query_string" : {
      "query": "鮨",
      "fields": ["_all"],
      "default_operator": "and"
    }
  }
}  "default_operator": "or"
}

そしてその返り値が以下の通りである。店名に"鮨”をもつ水谷となかむらが含まれている。

  "took" : 4,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 2,
    "max_score" : 0.067124054,
    "hits" : [ {
      "_index" : "restaurant",
      "_type" : "sushiya",
      "_id" : "1",
      "_score" : 0.067124054,
      "_source":{ "id": "1", "name": "鮨水谷", "location" : [35.66836,139.761106], "address" :"東京都中央区銀座8-7-7" }
    }, {
      "_index" : "restaurant",
      "_type" : "sushiya",
      "_id" : "5",
      "_score" : 0.067124054,
      "_source":{ "id": "5", "name": "鮨 なかむら", "location": [35.662347,139.729366], "address" : "港区六本木7丁目17−16" }
    } ]
  }
}
Scoreの定義

さらに、Elasticsearchでは検索スコアの重み付けがクエリ内で可能である。

見ての通り、以下のクエリでは店名に”鮨”が入っていればscoreを10倍し、場所が一定範囲内(六本木駅から2km)の場合はスコアが100倍される。

また他にもアクセスカウントによるsortの定義や、自身で定義した計算式によるscoreの計算も可能であり、非常に柔軟なクエリを記述する事ができる。

{
  "query" : {
    "function_score" : {
      "query" : {
        "simple_query_string" : {
          "query": "鮨 銀座 六本木",
          "fields": ["_all"],
          "default_operator": "or"
         }
      },
      "score_mode": "multiply",
      "boost_mode": "multiply",
      "functions" : [
        {
          "filter" : { "term" : { "name" : "鮨"}},
          "boost_factor" : 10
        },
        {
          "filter" : { "geo_distance" : { "distance" : "2km", "location" : [35.66, 139.73] }},
          "boost_factor" : 100
        }
      ]
    }
  }
}
"sort" : [{ "access_count" : {"order" : "desc", "missing" : "_last"}}]
"script_score" : {
        "script" : "doc[\"access_count\"].value * 100"
      }

おわりに

以上に上げたとおり、Elasticsearchはデータの取り込みもクエリの組み立ても非常に容易に出来る。

最近良く見られるのがElasticsearchとKibanaの組み合わせであり、例えばfulentdで得られたログ情報をElasticsearchに取り込むことでログの整形を容易にし、Kibanaで可視化することでログの分析が簡単に行える。

また、クローリング用のプラグインや、日本語の形態素解析用のプラグインが既に出回っており、Elasticsearchを取り巻く環境は確実にに発展している。 Elasticsearchは簡単に設定ができ強力であるため新しく検索エンジンが必要になった場合には使用をおすすめしたいフレームワークである。

高速スケーラブル検索エンジン ElasticSearch Server (アスキー書籍)

高速スケーラブル検索エンジン ElasticSearch Server (アスキー書籍)

Apache Lucene 入門 ~Java・オープンソース・全文検索システムの構築

Apache Lucene 入門 ~Java・オープンソース・全文検索システムの構築