人生裏ローテ

通年裏ローテを守って地味に重宝がられる人生を送りたいITエンジニアのブログ

触りながら Elasticsearch を学ぶ 〜データ投入からboolクエリまで〜

Elasticsearch が苦手なので、触りながら勉強してみた。 ( リポジトリ )

触りながら学ぶ Elasticsearch

チュートリアルの対象

投入するデータ

IMDb Datasets を利用する。これにはいくつか理由がある。

  1. 全文検索エンジンの多くは英語のために最適化されており、日本語データセットを利用する場合は設定が必要である
  2. IMDb は無料かつデータ量が多く、かつ映画というトピックは「検索」という題材に適切であること
  3. IMDb Datasets は TSV がダンプされるため、システム構成を単純にできること(RDBを必要としない)

チュートリアルでは、映画人と2000年以降の映画データを検索対象とする。

データ投入

provision.sh で実施している処理を解説する。

function download_imdb_dataset {
    # 代表作、役職、生年が確定している者のみを抽出する
    # gz ファイルを標準出力に渡して gzip -d することでファイルを経由せずに解凍を行う
    # 出力されるのは TSV なので awk で処理できる
    curl 'https://datasets.imdbws.com/name.basics.tsv.gz' -o - | \
      gzip -d | \
      awk -F"\t" '{ if ($6 != "\\N" && $5 != "" && $3 != "\\N") { print $0 }}' > ${C}/rawdata/name.basics.tsv
    # 評価数データはそのまま利用
    curl 'https://datasets.imdbws.com/title.ratings.tsv.gz' | gzip -d > ${C}/rawdata/title.ratings.tsv
    # 2000 年以降に制作された映画を抽出する
    # join コマンドを利用して評価数データを結合する
    curl 'https://datasets.imdbws.com/title.basics.tsv.gz' -o - | \
      gzip -d | \
      awk -F"\t" '{ if ($2 == "movie" && $6 != "\\N" && $6 >= 2000) { print $0 } }' | \
      join -t "    " - ${C}/rawdata/title.ratings.tsv > ${C}/rawdata/title.basics.tsv
}

出力されたデータは以下のようになっている。

name.basics.tsv

nconst    primaryName birthYear   deathYear   primaryProfession   knownForTitles
nm0000001   Fred Astaire    1899    1987    soundtrack,actor,miscellaneous  tt0050419,tt0031983,tt0053137,tt0072308
nm0000002   Lauren Bacall   1924    2014    actress,soundtrack  tt0038355,tt0037382,tt0117057,tt0071877

title.basics.tsv

tt0016906    movie   Frivolinas  Frivolinas  0   2014    \N  80  Comedy,Musical  5.6 15
tt0035423   movie   Kate & Leopold  Kate & Leopold  0   2001    \N  118 Comedy,Fantasy,Romance  6.4 77701
tt0062336   movie   El Tango del Viudo y Su Espejo Deformante   El Tango del Viudo y Su Espejo Deformante   0   2020    \N  70  Drama   6.7 38

データがダウンロードできたら、インデックスを作成する。 ( ドキュメント ) Elasticsearch7 では type がなくなったため、 ES6 以前のインデックスを作成APIは無効。 インデックス作成は PUT メソッドで <ES_HOST>/<INDEX_NAME> に以下のような json を送る。 参考: デフォルトで有効なデータタイプ一覧

{
    "settings": {
        "number_of_shards": "1",
        "number_of_replicas": "1"
    },
    "mappings": {
        "properties": {
            "primary_title": {
                "type": "text"
            },
            "original_title": {
                "type": "text"
            },
            "film_year": {
                "type": "integer"
            },
            "genres": {
                "type": "keyword"
            },
            "average_rating": {
                "type": "double"
            },
            "num_of_votes": {
                "type": "long"
            },
            "persons": {
                "type": "text"
            }
        }
    }
}

シャード数は後から変えられないが、レプリカセット数は変更できる。 やり方については 公式 を参照。

上記では persons は複数の値が入るが、ES側でよしなにしてくれる。

データ投入

convert_title.py で実施している。 ここでのポイントは Bulk API を利用している点。

一括投入の場合は Bulk API を利用するほうが効率が良い。ただし、

{"index":{"_id":"foo"}}
{"name":"bar","category":"baz"}

のように、 コマンド行と内容の行を分ける必要がある。 また、 末尾に \n がないとエラーとなる ので注意が必要。

上記の操作は PUT /<index>/_doc/<_id> と等価である。 Index API は個別の操作でのみ用いる。

触りながら学ぶ Elasticsearch 基本クエリ

インデクシングできたら

以下のAPIで確認してみる

検索APIを追ってみる

text: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html

match クエリ

match クエリは

{
  "query": {
    "match": {
      "<field>": {
        "query": "<value>"
      }
    }
  }
}

のような形式で検索する。たとえば The Bourne Identity で検索してみよう

curl -i -X POST \
   -H "Content-Type:application/json" \
   -d \
'{
  "query": {
    "match": {
      "primary_title": {
        "query": "The Bourne Identity"
      }
    }
  }
}' \
 'http://localhost:9200/movies/_search?pretty'

10000 件以上がヒットするが、検索条件が The OR Bourne OR Identity になっているためである。 AND 検索にするためには以下のようにする。

curl -i -X POST \
   -H "Content-Type:application/json" \
   -d \
'{
  "query": {
    "match": {
      "primary_title": {
        "query": "The Bourne Identity",
        "operator": "AND"
      }
    }
  }
}' \
 'http://localhost:9200/movies/_search?pretty'

こうすることで、ボーン・アイデンティティのみがヒットするようになった、

match_phrase クエリ

AND 検索では不十分な場合がある。たとえば語順が重要な場合がそれである。

極端な例だが、 Michael Fox で AND 検索をすると Micael J. Fox などもヒットする。例を見てみよう。

match query

curl -i -X POST \
   -H "Content-Type:application/json" \
   -d \
'{
  "query": {
    "match": {
      "persons": {
        "query": "michael fox",
        "operator": "AND"
      }
    }
  }
}' \
 'http://localhost:9200/movies/_search?pretty'

マイケル・フォックス氏のみならず、マイケル・レイ・フォックス氏などもヒットしていることがわかる。 フレーズクエリを利用すると以下のようになる。

match_phrase query

curl -i -X POST \
   -H "Content-Type:application/json" \
   -d \
'{
  "query": {
    "match_phrase": {
      "persons": {
        "query": "michael fox"
      }
    }
  }
}' \
 'http://localhost:9200/movies/_search?pretty'

Hard Shoulder というスリラー映画だけヒットした。 この映画にはマイケル・フォックス氏が関わっているようだ。

term クエリ

term クエリは solr でいう fq のような使い方ができる。主に keyword 型の絞り込みに使う。

curl -i -X POST \
   -H "Content-Type:application/json" \
   -d \
'{
  "query": {
    "term": {
      "genres": "Sci-Fi"
    }
  }
}' \
 'http://localhost:9200/movies/_search?pretty'
curl -i -X POST \
   -H "Content-Type:application/json" \
   -d \
'{
  "query": {
    "terms": {
      "genres": ["Sci-Fi", "Animation"]
    }
  }
}' \
 'http://localhost:9200/movies/_search?pretty'

複数のOR検索する場合は terms を使う。

range クエリ

範囲検索を行う。

curl -i -X POST \
   -H "Content-Type:application/json" \
   -d \
'{
  "query": {
    "range": {
      "film_year": {
        "gte": 2010,
        "lte": 2015
      }
    }
  }
}' \
 'http://localhost:9200/movies/_search?pretty'
  • gte : 以上
  • lte : 以下
  • gt : 〜より大きい
  • lt : 〜未満

gt系, lt系は必ずしも両方用意する必要はない. 例は以下

curl -i -X POST \
   -H "Content-Type:application/json" \
   -d \
'{
  "query": {
    "range": {
      "film_year": {
        "gte": 2018
      }
    }
  }
}' \
 'http://localhost:9200/movies/_search?pretty'

日付型は相対計算できる ( 例: now-1w )

触りながら学ぶ Elasticsearch 複合クエリ

複合クエリ

複合クエリは must should must_not filter の4つで構成される。

{
    "query": {
        "bool": {
            "must": [ <queries> ],
            "should": [ <queries> ],
            "must_not": [ <queries> ],
            "filter": [ <queries> ]
        }
    }
}

たとえばフレーズクエリを利用して The Girl with the Dragon Tattoo を検索した場合、2009年のスウェーデン映画と2011年のハリウッド映画の両方がヒットする。

curl -i -X POST \
   -H "Content-Type:application/json" \
   -d \
'{
  "query": {
    "match_phrase": {
      "primary_title": {
        "query": "The Girl with the Dragon Tattoo"
      }
    }
  }
}' \
 'http://localhost:9200/movies/_search?pretty'

2011 年のほうに絞りたいのであれば、 bool クエリを利用する。

curl -i -X POST \
   -H "Content-Type:application/json" \
   -d \
'{
  "query": {
    "bool": {
      "must": [
        {"match_phrase": {"primary_title": {"query": "The Girl with the Dragon Tattoo"}}}
      ],
      "filter": [
        {"term":{"film_year":2011}}
      ]
    }
  }
}' \
 'http://localhost:9200/movies/_search?pretty'

備考: must, should, must_not はスコアに影響するが、 filter はスコアに影響しない。

クエリのソート

たとえば star wars で検索すると、 Saving Star Wars なる謎の映画がトップするので、 IMDb のレビュー数でソートしてみる。

謎のコメディ映画がヒットするクエリ

curl -i -X POST \
   -H "Content-Type:application/json" \
   -d \
'{
  "query": {
    "bool": {
      "must": [
        {"match_phrase": {"primary_title": {"query": "star wars"}}}
      ]
    }
  }
}' \
 'http://localhost:9200/movies/_search?pretty'

ソートしたクエリ

curl -i -X POST \
   -H "Content-Type:application/json" \
   -d \
'{
  "query": {
    "bool": {
      "must": [
        {"match_phrase": {"primary_title": {"query": "star wars"}}}
      ]
    }
  },
  "sort": [
    {"num_of_votes":{"order":"desc"}}
  ]
}' \
 'http://localhost:9200/movies/_search?pretty'

こうするとEP7がトップにヒットする。めでたしめでたし。

スコアソートをしたい場合は _score でソートする。

他の複合クエリ

例として使いやすいためスターウォーズを引き続き使う。

must と must_not は併用できる。下記はマーク・ハミルが出演していないスターウォーズを検索する。

curl -i -X POST \
   -H "Content-Type:application/json" \
   -d \
'{
  "query": {
    "bool": {
      "must": [
        {"match_phrase": {"primary_title": {"query": "star wars"}}}
      ],
      "must_not": [
        {"match": {"persons": {"query": "Mark Hamill"}}}
      ]
    }
  },
  "sort": [
    {"num_of_votes":{"order":"desc"}}
  ]
}' \
 'http://localhost:9200/movies/_search?pretty'

この条件だと、EP3が最上位となる。確かにマークハミルが出ていない。

must と should は併用できない。

 {
  "query": {
    "bool": {
      "must": [
        {"match_phrase": {"primary_title": {"query": "star wars"}}}
      ],
      "should": [
        {"match":{"persons":{"query":"Mark Hamill"}}},
        {"match":{"persons":{"query":"Samuel L. Jackson"}}}
      ],
      "filter": [
        {"term": {"genres": "Sci-Fi"}}
      ]
    }
  },
  "sort": [
    {"_score":{"order":"desc"}}
  ]
}

このようなクエリを送っても、マーク・ハミルサミュエル・L・ジャクソンが重視されるわけではない。