まず、dockerでElasticsearchとKibanaの環境を構築します。 docker-composeで書くと以下のような設定になります。
version: '3.1'
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:6.2.4
environment:
- discovery.type=single-node
ports:
- 9200:9200
kibana:
image: docker.elastic.co/kibana/kibana:6.2.4
links:
- elasticsearch:elasticsearch
ports:
- 5601:5601
起動したらElasticsearchにアクセスできることを確認。
$ curl localhost:9200
{
"name": "_5S39MV",
"cluster_name": "docker-cluster",
"cluster_uuid": "sqH8NB2KSrOOpkt2O9YTNg",
"version": {
"number": "6.2.4",
"build_hash": "ccec39f",
"build_date": "2018-04-12T20:37:28.497551Z",
"build_snapshot": false,
"lucene_version": "7.2.1",
"minimum_wire_compatibility_version": "5.6.0",
"minimum_index_compatibility_version": "5.0.0"
},
"tagline": "You Know, for Search"
}
マッピングの作成
マッピングはRDBでいうスキーマのようなものです。 ElasticsearchはJSONを投入すると自動でマッピングを作成してくれますが、 実際本番で使う場合はちゃんとマッピングを定義した方が良いそうなのでマッピングの作成から始めます。
インデックスに以下のような形のデータを登録したいとします。
{
"id": "動画ID",
"title": "動画タイトル",
"release_date": "リリース日",
"category": "カテゴリ",
"group_name": "グループ名",
"member_names": ["メンバー名", "メンバー名"],
"setlist": ["楽曲名", "楽曲名"],
"delivery": {
"start": "YYYY-MM-DD HH:MM:SS",
"end": "YYYY-MM-DD HH:MM:SS"
},
"favorites": "お気に入り登録数"
}
その場合、マッピングは以下のように登録します。 用途に合わせて型やAnalyzerの設定を行う必要がありますがここでは省略します。
Elasticsearchではこちらに載っているデータ型が利用可能です。
PUT /videos/_mapping/hoge
{
"properties": {
"id": {
"type": "integer"
},
"title": {
"type": "text"
},
"release_date": {
"type": "date",
"format": "yyyy-MM-dd"
},
"category": {
"type": "keyword"
},
"group_name": {
"type": "keyword"
},
"member_names": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"setlist": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"delivery": {
"properties": {
"start": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss"
},
"end": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss"
}
}
},
"favorites": {
"type": "integer"
}
}
}
クエリ検索とフィルター
_search に対して条件を投げることで検索できます。
should が OR 条件、must が AND 条件、 must_not が NOT
filter が WHERE みたいなものです。
GET /videos/hoge/_search
{
"query": {
"bool": {
"must": [
{"bool": {"should": [
{"match": {"setlist.keyword": "楽曲1"}},
{"match": {"group_name": "アーティスト"}}
]}},
{"bool": {"must_not": [
{"match": {"setlist.keyword": "楽曲2"}}
]}},
{"bool": {"must": [
{"match": {"category": "コンサート"}}
]}}
],
"filter": {"range": {
"release_date": {
"gte": "2010-01-01",
"lte": "2012-01-01"
}
}}
}
},
"_source": ["group_name", "category", "release_date"],
"size": 3
}
結果
{
"took": 9,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 30,
"max_score": 6.005598,
"hits": [
{
"_index": "videos",
"_type": "hoge",
"_id": "894",
"_score": 6.005598,
"_source": {
"release_date": "2011-03-19",
"group_name": "アーティスト",
"category": "コンサート"
}
},
{
"_index": "videos",
"_type": "hoge",
"_id": "120",
"_score": 5.314138,
"_source": {
"release_date": "2011-10-15",
"group_name": "アーティスト",
"category": "コンサート"
}
},
{
"_index": "videos",
"_type": "hoge",
"_id": "906",
"_score": 4.9989195,
"_source": {
"release_date": "2010-12-22",
"group_name": "歌手",
"category": "コンサート"
}
}
]
}
}
SQLで書くとこのような感じでしょうか。
must と filter はどちらもAND条件なのですが filter は score に影響を与えない点が違うそうです。
SELECT
group_name,
category,
release_date
FROM
videos
WHERE
(「楽曲1」 in setlist OR group_name = "アーティスト")
AND
NOT (「楽曲2」 in setlist)
AND
category = "コンサート"
AND
release_date BETWEEN "2010-01-01" AND "2012-01-01"
LIMIT 3;
検索結果の重み付け
Elasticsearchはデフォルトで
score の高い順にソートされて返ってきます。
こちらをチューニングすることでよりニーズに合った検索結果を返すことができるようになります。
function_score と field_value_factor を使用することでフィールドの値で重み付けを行うことができます。
functions 部分は score = favorites x 0.1 x 5 と同じような感じです。
boost は条件にマッチした場合にscoreを加算する機能で boost: 2 の場合はscoreが2倍になります。
GET /videos/hoge/_search
{
"query": {
"function_score": {
"query": {
"bool": {"should": [
{"match": {"group_name": {
"query": "アーティスト",
"boost": 2
}}},
{"match": {"group_name": {
"query": "歌手",
"boost": 1.5
}}}
]}
},
"functions": [
{
"field_value_factor": {
"field": "favorites",
"factor": 0.2
},
"weight": 5
}
]
}
},
"_source": ["group_name", "favorites"],
"size": 3
}
結果
{
"took": 2,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 1291,
"max_score": 1331.5466,
"hits": [
{
"_index": "videos",
"_type": "hoge",
"_id": "223",
"_score": 1331.5466,
"_source": {
"favorites": "642",
"group_name": "アーティスト"
}
},
{
"_index": "videos",
"_type": "hoge",
"_id": "216",
"_score": 1123.427,
"_source": {
"favorites": "285",
"group_name": "歌手"
}
},
{
"_index": "videos",
"_type": "hoge",
"_id": "238",
"_score": 1024.5858,
"_source": {
"favorites": "494",
"group_name": "アーティスト"
}
}
]
}
}
まとめ
簡単な検索と
score を調整する方法を少し理解できました。
今のところ元のscoreの計算方法が謎ですが、
元scoreの調整、マッピングの最適化、ユーザーによって重み付けを変えるクエリの設計などにより
複雑な条件でもニーズにマッチした結果が素早く返せるようになるのかなと思いました。
参考リンク
松木佑徒