【入門】Elasticsearch とは?インストールから query の使い方

Elasticsearch

Elasticsearch とは、複数のファイルから特定の文字列を検索する分散型エンジンです。

具体的には google 検索や、github のソースコード検索のような全文検索 (後述) が行えます。

スポンサーリンク

初めに

本記事は、以下のビッグデータ分析基盤シリーズの Elasticsearch 編です。

スポンサーリンク

全文検索とは

全文検索とは、複数のファイルから特定の文字列を検索することです。

全文検索には、主に以下の2つの手法があります。

grep 型

grep 型とは、複数のファイルを上から順番に検索する方法です。
ファイルの数が増えると検索速度が大幅に低下する特徴があります。

UNIX の文字列検索コマンド grep がこれに当たります。

索引 (インデックス) 型

索引 (インデックス) とは、「ある単語を含むファイル一覧」です。

本の索引 (インデックス) の例 (https://www.tkp.co.jp/service/edit/index_up)

例えば、本の巻末には「ある単語含むページ一覧」を表す索引 (インデックス) があります。

同様に、全文検索には「ある単語を含むファイル一覧」を表す索引 (インデックス) があります。

索引 (インデックス) 型とは、あらかじめ単語に対して索引 (インデックス) を作成しておくことで、ファイルの検索速度を向上する方法です。

索引型の全文検索を利用した例は以下のとおりです。

  • Elasticsearch 検索:複数のファイル (ドキュメント) から一致する文字列を検索
  • Google 検索:複数のファイル (Webページ) から一致する文字列を検索
  • GitHub のソースコード検索:複数のファイル (ソースコード) から一致する文字列を検索

インデックスの例

以下の2つのドキュメントを例に、インデックスを作成してみます。

  • ドキュメント1:I Like search engine.
  • ドキュメント2:I search keywords by google.

ドキュメントに単語が含まれる場合1単語が含まれない場合0とすると、インデックスは以下のとおりです。

Ilikesearchenginekeywordsbygoogle
ドキュメント11111000
ドキュメント21010111

なお、一般的なデータベースでは行単位で検索します。(列単位で検索できません)

そのため "google" を含むドキュメントを検索する時、全ての行をスキャンする必要があります。
つまりドキュメントの数が増えるほど、検索速度が低下します。

この問題を解決するために、転置インデックスを利用します。

転置インデックスの例

転置インデックスは、次のようにインデックスの転置行列(行と列を入れ替えたもの)です。

ドキュメント1ドキュメント2
I11
like10
search11
engine10
keywords01
by01
google01

転置インデックスから "google" を含むドキュメントを検索する場合、ドキュメントの数に関わらず1行スキャンするだけです。

スポンサーリンク

用途 (できること)・利用例

Elasticsearch の主な利用用途は以下の2つです。

サービスのドキュメントを検索するために利用できます。企業での利用例は以下のとおりです。

異常検知

アクセスログなどから異常を検知します。企業での利用例は以下のとおりです。

Elastic Stack とは

Elastic Stack (ELK) は Elasticsearch と以下の3つのオープンソースプロジェクトのことです。

Kibana

Kibana とは、Elasticsearch 内のドキュメントを可視化する BI ツールです。

以下の公式で紹介されている GIF 画像を見るのがわかりやすいです。

https://www.elastic.co/jp/what-is/kibana-reporting
Logstash

Logstash とは、Elasticsearch に送信するデータの形式を変換するツールです。

ETL の「Transform」、「 Load」(データを変換し Elasticsearch にロード) する役割です。

ちなみに、Logstash の代わりに Fluentd を利用する構成を EFK と言います。
( [ELK= Elasticsearch, Logstash, Kibana], [EFK = Elasticsearch, Fluentd, Kibana])

Fluentd の詳細については、以下の記事をご覧ください。

Beats

Beats とは、サーバーから Elasticsearch や Logstash にデータを転送するシッパーです。

ETL の「Extract」(サーバーからデータの抽出) する役割です。

Elasticsearch のアーキテクチャ

Elasticsearch のアーキテクチャについて次の2つに分けて説明します。

論理的なアーキテクチャと用語

Elasticsearch の論理的な概念の全体図は以下のとおりです。

論理的な概念

各用語の意味は以下のとおりです。

用語説明
フィールドKey-Value (名前と値の組) のこと
各フィールドはデータの種類を表す [データ型] を持つ (一覧はこちら)
ドキュメントフィールドの集合のこと
ドキュメントの実態は JSON オブジェクト
インデックスドキュメントの集合のこと
ドキュメントをインデックスに格納する時、転置インデックスを作成
マッピングフィールドの [データ型] や転置インデックスの設定 (一覧はこちら)
マッピングは1つのインデックスに1つ

物理的なアーキテクチャと用語

Elasticsearch の物理的な概念の全体図は以下のとおりです。

なお、赤枠は Elasticsearch の論理的な概念である [インデックス] です。

物理的な概念と論理的な概念の関連
用語説明
シャードインデックスを分割したデータ
・実体は Lucene インデックスファイル
・検索処理はシャード単位で分散/並列化
プライマリーシャード書き込み処理を行うシャード
レプリカシャードプライマリーシャードのコピー (レプリケーション)
検索処理の負荷分散や、データのバックアップとして利用
ノード1台の Elasticsearch サーバーのこと
ノードは複数のシャードを持つ
クラスター複数のノードの集合のこと

なお、ノードには次の4種類の属性が存在し、1つのノードが複数の属性を持つことも可能です。

ノードの種類役割
Master ノードクラスターのメタデータなどを管理するノード
・Master ノードはクラスターに1台のみ
・マスターノードに昇格可能なノードは Master-eligible と呼ばれるが、Master ノードでは無い
Data ノードデータを格納・リクエストの処理(検索や集計など)をするノード
Ingest ノードデータの変換や加工し、Data ノードに格納するノード
Logstash と同じ役割
Coordinating ノードリクエストをルーティングするノード
・Data ノードもリクエストはルーティング可能
・Data ノードにルーティング作業の負荷を掛けたくない場合に、ルーティング処理専用のノードとして設定

Elasticsearch のインストール

Elasticsearch のインストール方法について、次の2つを紹介します。

なお、docker については以下の記事をご覧ください。

docker でインストール

docker を利用して、Elasticsearch をインストールする方法を紹介します。

Elasticsearch コンテナを作成

version: '3'
services:
  elasticsearch:
    image: elasticsearch:7.12.1
    container_name: elasticsearch
    environment:
      - discovery.type=single-node
    ports:
      - 9200:9200
  kibana:
    image: kibana:7.12.1
    ports:
      - "5601:5601"
    environment:
      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200

これでシングルノードの Elasticsearch が立ち上がります。

curl localhost:9200
{
  "name" : "123456789abc",
  "cluster_name" : "docker-cluster",
  "cluster_uuid" : "1234567890abcdefghijkl",
  "version" : {
    "number" : "7.12.1",
    "build_flavor" : "default",
    "build_type" : "docker",
    "build_hash" : "1234567891456789123456789",
    "build_date" : "2021-04-20T20:56:39.040728659Z",
    "build_snapshot" : false,
    "lucene_version" : "8.8.0",
    "minimum_wire_compatibility_version" : "6.8.0",
    "minimum_index_compatibility_version" : "6.0.0-beta1"
  },
  "tagline" : "You Know, for Search"
}

起動に成功している場合は、上記のような結果が返ってきます。

※ 「curl: (56) Recv failure: Connection reset by peer」が返ってくる場合は初期化処理が完了するまでお待ちください。

Kibana コンテナへのアクセス

先ほどの docker-compose で Kibana コンテナも合わせて起動しています。

Kibana コンテナにアクセスするために、ブラウザで http://localhost:5601 を開きます。

docker 無しでインストール

docker を利用せずに、Elasticsearch をインストールする方法を紹介します。

本記事では Amazon Linux 2 でインストールする方法を紹介します。
その他の OS については以下のドキュメントをご覧ください。

Elasticsearch インストール

Installing Elasticsearch | Elasticsearch Guide [7.15] | Elastic

Kibana インストール

Kibanaのインストール | Kibanaユーザーガイド [5.4] | Elastic

Javaのインストール

Elasticsearch を実行するには Java 8 が必要なので、Java 8 をインストールします。

sudo yum install java-1.8.0-openjdk -y
sudo yum install java-1.8.0-openjdk-devel -y
java -version
openjdk version "1.8.0_252"
OpenJDK Runtime Environment (build 1.8.0_252-b09)
OpenJDK 64-Bit Server VM (build 25.252-b09, mixed mode)

openjdk version "1.8****" 以上であれば OK です。

Elasticsearch のインストール

次に Elasticsearch をインストールします。

sudo rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch
sudo vim /etc/yum.repos.d/elasticsearch.repo
[elasticsearch]
name=Elasticsearch repository for 7.x packages
baseurl=https://artifacts.elastic.co/packages/7.x/yum
gpgcheck=1
gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch
enabled=0
autorefresh=1
type=rpm-md
sudo yum install --enablerepo=elasticsearch elasticsearch -y
sudo systemctl start elasticsearch
curl localhost:9200
{
  "name" : "***",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "***",
  "version" : {
    "number" : "7.10.0",
    "build_flavor" : "default",
    "build_type" : "rpm",
    "build_hash" : "***",
    "build_date" : "2020-11-09T21:30:33.964949Z",
    "build_snapshot" : false,
    "lucene_version" : "8.7.0",
    "minimum_wire_compatibility_version" : "6.8.0",
    "minimum_index_compatibility_version" : "6.0.0-beta1"
  },
  "tagline" : "You Know, for Search"
}

Kibana のインストール

Kibana も合わせてインストールします。(kibana を利用しない場合は飛ばしてOKです。)

sudo vim /etc/yum.repos.d/kibana.repo
[kibana-7.x]
name=Kibana repository for 7.x packages
baseurl=https://artifacts.elastic.co/packages/7.x/yum
gpgcheck=1
gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch
enabled=1
autorefresh=1
type=rpm-md
sudo yum install kibana -y
sudo vim /etc/kibana/kibana.yml
server.host: "0.0.0.0"
sudo systemctl start kibana

Kibana が稼働していることを確認するために、以下のどちらかの URL にブラウザからアクセスします。

Elasticsearch が稼働するサーバーのブラウザからアクセスする場合
localhost:5601

■リモートホストのブラウザからアクセスする場合
<Elasticsearch サーバーの IP アドレス>:5601

以下のページが表示されれば成功です。(この記事は Kibana バージョン 7.10.0です)

アクセスできない場合

Elastic Kibana v7.3.0でSystemctrlを用いた起動ができず再起動を繰り返す。 - szkhaven.com
自分的メモです。 v6.7を使用してたのですが、そろそろv7に上げよう 続きを読む

Elasticsearch の使い方

Elasticsearch は以下のようなツールを使い、REST API で操作します。

REST API って何?という方は、以下の記事をご覧ください。

また、Kibana の Dev Tools は以下の手順で開けます。

Dev Tools の開き方(kibana 7.10.0 の場合)

今回は現時点で最新のメジャーバージョンである Elasticsearch 7.x の使い方を説明します。(Elsticsearch はメジャーバージョンが変化すると、後方互換性がなくなります。)

CRUD 操作 + Bulk API

ElasticsearchREST API では、ドキュメントに対して以下のような CRUD 操作が可能です。

REST API説明対応する CRUD
Index APIインデックスにドキュメントを追加Create
Get APIインデックスのドキュメントを取得Read
Update APIインデックスのドキュメントを更新Update
Delete APIインデックスのドキュメントを削除Delete

Index API (Create)

Index API では、インデックスにドキュメントを追加します。

curl の場合
curl localhost:9200/2020-11-tweet/_doc/1?pretty -XPUT -H "Content-Type: application/json" -d '
{
  "date":"2020/11/01 09:00 JST",
  "Tweet":"ツイッターをはじめました。",
  "User ID":"hoge"
}'
kibana の DevTools の場合
PUT /2020-11-tweet/_doc/1
{
  "date":"2020/11/01 09:00 JST",
  "Tweet":"ツイッターをはじめました。",
  "User ID":"hoge"
}

右上の三角ボタンで実行します。

補足 データストリームへドキュメントを追加する場合

データストリームとは、複数のインデックスを1つにまとめたものです。

データストリームの詳細は以下のドキュメントに記載がございます。

Data streams | Elasticsearch Guide [8.3] | Elastic

データストリームにドキュメントを追加するためには、以下の REST API を利用します。

PUT /tweet/_create/1
{
  "date":"2020/11/01 09:00 JST",
  "Tweet":"ツイッターをはじめました。",
  "User ID":"hoge"
}

Get API (Read)

Get API では、インデックスからドキュメントを取得します。

curl の場合
curl localhost:9200/2020-11-tweet/_doc/1?pretty
{
  (中略)
  "_source" : {
    "date" : "2020/11/01 09:00 JST",
    "Tweet" : "ツイッターをはじめました。",
    "User ID" : "hoge"
  }
}
kibana の DevTools の場合
GET /2020-11-tweet/_doc/1
{
  (中略)
  "_source" : {
    "date" : "2020/11/01 09:00 JST",
    "Tweet" : "ツイッターをはじめました。",
    "User ID" : "hoge"
  }
}

Update API (Update)

Update API では、インデックスのドキュメントを更新します。

curl の場合
curl localhost:9200/2020-11-tweet/_doc/1?pretty -XPOST -H "Content-Type: application/json" -d '
{
  "date":"2020/11/01 09:00 JST",
  "Tweet":"ドキュメントを更新したよ。",
  "User ID":"hoge"
}'
curl localhost:9200/2020-11-tweet/_doc/1?pretty
{
(中略)
  "_version" : 2,
  "_source" : {
    "date" : "2020/11/01 09:00 JST",
    "Tweet" : "ドキュメントを更新したよ。",
    "User ID" : "hoge"
  }

"Tweet" フィールドの値が更新され、_version が2に変化していることがわかります。

kibana の DevTools の場合
POST /2020-11-tweet/_doc/1
{
  "date":"2020/11/01 09:00 JST",
  "Tweet":"ドキュメントを更新したよ。",
  "User ID":"hoge"
}
GET /2020-11-tweet/_doc/1
{
(中略)
  "_version" : 2,
  "_source" : {
    "date" : "2020/11/01 09:00 JST",
    "Tweet" : "ドキュメントを更新したよ。",
    "User ID" : "hoge"
  }

"Tweet" フィールドの値が更新され、_version が2に変化していることがわかります。

POST <index>/_update/<_id> を使う場合

POST /2020-11-tweet/_update/1
{
  "doc": {
    "Tweet":"ドキュメントを一部更新したよ。"
  }
}
GET /2020-11-tweet/_doc/1
{
(中略)
  "_source" : {
    "date" : "2020/11/01 09:00 JST",
    "Tweet" : "ドキュメントを一部更新したよ。",
    "User ID" : "hoge"
  }

Delete API (Delete)

Delete API では、インデックスのドキュメントを削除します。

curl の場合
curl localhost:9200/2020-11-tweet/_doc/1?pretty -XDELETE
curl localhost:9200/2020-11-tweet/_doc/1?pretty
{
  "_index" : "2020-11-tweet",
  "_type" : "_doc",
  "_id" : "1",
  "found" : false
}

"found" が false となりました。("_source" のフィールドが削除されました。)

kibana の DevTools の場合
DELETE /2020-11-tweet/_doc/1
GET /2020-11-tweet/_doc/1
{
  "_index" : "2020-11-tweet",
  "_type" : "_doc",
  "_id" : "1",
  "found" : false
}

found" が false となりました。("_source" のフィールドが削除されました。)

Bulk API (一括処理)

Bulk API では、1回の API 呼び出しでインデックスのドキュメントを複数回操作できます。

なお、Bulk API で利用可能な操作は以下のとおりです。

Bulk API の操作説明
Createインデックスが存在しない場合、ドキュメントを作成
Indexインデックスのドキュメントを全て更新 (置き換え)
Updateインデックスのドキュメントを一部更新
Deleteインデックスのドキュメントを削除
curl の場合

Bulk API で操作する内容を記載した json ファイルを作成します。

{ "index" : { "_index" : "test", "_id" : "1" } }
{ "field1" : "value1" }
{ "delete" : { "_index" : "test", "_id" : "2" } }
{ "create" : { "_index" : "test", "_id" : "3" } }
{ "field1" : "value3" }
{ "update" : {"_id" : "1", "_index" : "test"} }
{ "doc" : {"field2" : "value2"} }
curl localhost:9200/_bulk?pretty -XPOST --data-binary @bulk.json -H "Content-Type: application/json"
kibana の DevTools の場合
POST _bulk
{ "index" : { "_index" : "test", "_id" : "1" } }
{ "field1" : "value1" }
{ "delete" : { "_index" : "test", "_id" : "2" } }
{ "create" : { "_index" : "test", "_id" : "3" } }
{ "field1" : "value3" }
{ "update" : {"_id" : "1", "_index" : "test"} }
{ "doc" : {"field2" : "value2"} }

マッピング管理 + テンプレート

マッピングとはフィールドのデータ型を定義したものです。

マッピングを作成、追加する方法は以下の4つです。

なお、一度作成したマッピングは修正できません。(追加はできます)

マッピングを修正するには、インデックスを作成し直す必要があります。

自動でマッピングを作成

Elasticsearch は、インデックスにドキュメントを作成する時、自動でマッピングを作成します。

PUT /mapping_test/_doc/1
{
  "date":"2020/11/01 09:00:00+0900"
}
GET /mapping_test
   "mappings" : {
      "properties" : {
        "date" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256

結果を見ると、次のマッピングが定義されていることがわかります。

キーデータ型
datetext
date.keywordkeyword
text = 単語ごとに分けた転置インデックスを持つ全文検索用
keyword = 完全一致検索用

"date" キーのデータ型は text 型で登録されていますが、date 型で登録したい場合もあります。

この場合は、手動でマッピングを作成する必要があります。

手動でマッピングを作成

手動でマッピングを作成する方法は以下のとおりです。

PUT /mapping_test2
{
  "mappings": {
    "properties": {
      "date":    { 
        "type": "date",
        "format": "yyyy/MM/dd HH:mm:ssZ"
      }
    }
  }
}
GET /mapping_test2
    "mappings" : {
      "properties" : {
        "date" : {
          "type" : "date",
          "format" : "yyyy/MM/dd HH:mm:ssZ"

無事 date キーが date 型でマッピングされていることがわかります。

マッピングで指定したフォーマットと異なるドキュメントを作成するとどうなる?

マッピングで指定したフォーマットと異なるドキュメントを作成するにエラーとなります。

試しに設定したフォーマットと異なるフォーマットでドキュメントを作成します。

  • 設定したフォーマット = yyyy/MM/dd HH:mm:ssZ
  • 検証するフォーマット = yyyy/MM/dd HH:mm:Z
PUT /mapping_test2/_doc/1
{
  "date":"2020/11/01 09:00+0900"
}
      "reason" : "failed to parse date field [2020/11/01 09:00+0900] with format [yyyy/MM/dd HH:mm:ssZ]",

設定したフォーマットと異なるので、パースエラーが発生します。

次にマッピングで指定したフォーマットに従ってドキュメントを作成

PUT /mapping_test2/_doc/2
{
  "date":"2020/11/01 09:00:00+0900"
}

ドキュメントの作成に成功しました。

インデックステンプレート

インデックステンプレートとは、インデックスの設定 (マッピングなど) の雛形です。
これにより、複数のインデックスで何度もマッピングを設定せずに済みます。

では "test*" インデックスの作成時に利用するテンプレートを作成してみます。

PUT /_template/test_template
{
  "index_patterns": "test*",
  "mappings": {
    "properties": {
      "date":    { 
        "type": "date",
        "format": "yyyy/MM/dd HH:mm:ssZ"
      }
    }
  }
}

上記のテンプレートでは、"date" キーのデータ型を定義しています。

それでは、テンプレートの動作を確認してみます。

PUT /test1
GET /test1
    "mappings" : {
      "properties" : {
        "date" : {
          "type" : "date",
          "format" : "yyyy/MM/dd HH:mm:ssZ"
        }
      }
    }

テンプレートで指定した format で date フィールドが登録されています。

このインデックステンプレートは test* (test1、test2、test3....) の全てのインデックスに対して、自動でマッピングを設定します。

既存のマッピングに追加

既存のマッピングに新しいフィールドを追加したい場合は mapping API を利用します。

test1 インデックスのマッピングに additional_field フィールドを追加します。

GET /test1
    "mappings" : {
      "properties" : {
        "date" : {
          "type" : "date",
          "format" : "yyyy/MM/dd HH:mm:ssZ"
        }
      }
    }
PUT /test1/_mapping
{
  "properties":{
    "additional_field":{
      "type":"text"
    }
  }
}
GET /test1
    "mappings" : {
      "properties" : {
        "additional_field" : {
          "type" : "text"
        },
        "date" : {
          "type" : "date",
          "format" : "yyyy/MM/dd HH:mm:ssZ"

additional_field フィールドが追加されています。

ドキュメントの検索 (search API)

Elasticsearch では search API を利用してインデックス内にあるドキュメントを検索します。

search API では、クエリ DSL (Domain Specific Language) を利用して、「4種類の検索条件」と「ソート」を指定可能です。

以降では、以下のドキュメントを用いて search API の検索方法を紹介します。

POST /_bulk
{ "index" : { "_index" : "demo_search", "_id" : "1" } }
{ "text" : "This is Elasticsearch test." }
{ "index" : { "_index" : "demo_search", "_id" : "2" } }
{ "text" : "Elasticsearch is God." }
{ "index" : { "_index" : "demo_search", "_id" : "3" } }
{ "text" : "This is a pen." }

条件無しで検索

search API を利用して作成した demo_search インデックスのドキュメントを検索します。

GET /demo_search/_search
    "hits" : [
      {
        "_index" : "demo_search",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "text" : "This is Elasticsearch test."
        }
      },
      {
        "_index" : "demo_search",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.0,
        "_source" : {
          "text" : "Elasticsearch is God."
        }
      },
      {
        "_index" : "demo_search",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 1.0,
        "_source" : {
          "text" : "This is a pen."
        }
      }
    ]

作成した3つのドキュメントがすべて検索できています。

match クエリ (全文検索)

match クエリは、転置インデックスを利用して全文検索を行うクエリです。

match クエリでは、データ型は「text」のフィールドに対して全文検索を行います。

これは、「text」が単語ごとに転置インデックスを作成しているためです。

These fields are analyzed, that is they are passed through an analyzer to convert the string into a list of individual terms before being indexed.

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

match クエリを利用して全文検索をしてみます。

GET /demo_search/_search
{
  "query":{
    "match": {
      "text": "Elasticsearch"
    } 
  }
}
    "hits" : [
        (省略)
        "_source" : {
          "text" : "Elasticsearch is God."
        }
        (省略)
        "_source" : {
          "text" : "This is Elasticsearch test."
        }
      }
match クエリで AND 検索

AND 検索で、"Elasticsearch" と "this" の両方が含まれるドキュメントを検索します。

GET /demo_search/_search
{
  "query":{
    "match": {
      "text": {
        "query": "Elasticsearch this",
        "operator":"AND"
      }
    } 
  }
}
    "max_score" : 1.3988109,
    "hits" : [
        (省略)
        "_source" : {
          "text" : "This is Elasticsearch test."
match_phrase クエリ

match_phrase クエリとは、単語の順序が一致するドキュメントだけを出力するクエリです。

GET /demo_search/_search
{
  "query":{
    "match_phrase": {
      "text": "Elasticsearch test"
    } 
  }
}
    "hits" : [
        (省略)
        "_source" : {
          "text" : "This is Elasticsearch test."
        }
      }
GET /demo_search/_search
{
  "query":{
    "match_phrase": {
      "text":{
        "query": "test Elasticsearch"
      }
    } 
  }
}
"hits" : [ ]

Term クエリ (完全一致検索)

Term クエリとは、完全一致検索を行うクエリです。

Term クエリでは、データ型が「keyword」のフィールドに対して完全一致検索を行います。
(text 型は、単語ごとに転置インデックスを作成しているので、完全一致検索ができない)

Keyword fields are often used in sortingaggregations, and term-level queries, such as term.

https://www.elastic.co/guide/en/elasticsearch/reference/current/keyword.html
GET /demo_search/_search
{
  "query":{
    "term": {
      "text.keyword": "This is Elasticsearch test."
    } 
  }
}
      {
        "_source" : {
          "text" : "This is Elasticsearch test."
        }
      }

完全一致検索の失敗例

GET /demo_search/_search
{
  "query":{
    "term": {
      "text.keyword": "Elasticsearch"
    } 
  }
}
"hits" : [ ]

"Elasticsearch" は "This is Elasticsearch test." と異なるので、完全一致検索にヒットしないです。

Terms クエリ

Terms とは Term の複数形、つまり複数の完全一致検索をするクエリです。

GET /demo_search/_search
{
  "query":{
    "terms": {
      "text.keyword":["This is Elasticsearch test.","This is a pen."]
    } 
  }
}
"_source" : {
          "text" : "This is Elasticsearch test."
        }
        (中略)
        "_source" : {
          "text" : "This is a pen."
        }

Range クエリ (範囲検索)

Range クエリとは、指定した値を元に範囲検索するクエリです。

検証用の test_range インデックスを作成します。

POST /_bulk
{ "index" : { "_index" : "test_range", "_id" : "1" } }
{ "size" : -1 }
{ "index" : { "_index" : "test_range", "_id" : "2" } }
{ "size" : 5 }
{ "index" : { "_index" : "test_range", "_id" : "3" } }
{ "size" : 10 }

準備ができたので、Range クエリを紹介します。

GET /test_range/_search
{
  "query":{
    "range": {
      "size":{
        "gte": "0",
        "lte": "9"
      }
    } 
  }
}
    "hits" : [
      {
        "_index" : "test_range",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.0,
        "_source" : {
          "size" : 5
        }
      }

0以上、9以下の size フィールドの値を持つドキュメントを検索できました。

bool クエリ (複合検索)

bool クエリとは、match クエリ、term クエリ、range クエリに対して AND, OR, NOT を実行するクエリです。

なお、bool クエリには、以下の4種類のクエリが存在します。

まずは検証のため、検証用の demo_bool インデックスを作成します。

POST /_bulk
{ "index" : { "_index" : "demo_bool", "_id" : "1" } }
{ "text" : "This is Elasticsearch test.","id":1 }
{ "index" : { "_index" : "demo_bool", "_id" : "2" } }
{ "text" : "Elasticsearch is God.","id":2 }
{ "index" : { "_index" : "demo_bool", "_id" : "3" } }
{ "text" : "This is a pen.","id":3 }

準備ができたので、ここから bool クエリを紹介していきます。

must クエリ

must クエリとは、AND 条件を表すクエリです。

GET /demo_bool/_search
{
  "query":{
    "bool": {
      "must":[
        {"match":{"text":"Elasticsearch"}},
        {"range":{"id":{"lte":"1"}}}
      ]
    } 
  }
}
        "_source" : {
          "text" : "This is Elasticsearch test.",
          "id" : 1
        }

「text = "Elasticsearch" を含む」AND「id = 1以下」のドキュメントを検索できています。

should クエリ

should クエリとは、OR 条件を表すクエリです。

GET /demo_bool/_search
{
  "query":{
    "bool": {
      "should":[
        {"match":{"text":"Elasticsearch"}},
        {"range":{"id":{"lte":"1"}}}
      ]
    } 
  }
}
    "hits" : [
      {
        "_index" : "demo_bool",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.453151,
        "_source" : {
          "text" : "This is Elasticsearch test.",
          "id" : 1
        }
      },
      {
        "_index" : "demo_bool",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.5077718,
        "_source" : {
          "text" : "Elasticsearch is God.",
          "id" : 2
        }
      }

「text = "Elasticsearch" を含む」OR「id = 1以下」のドキュメントを検索できています。

なお、"text", "id" の両方の条件を満たす上のほうが "_score" の値が高くなります。

must_not クエリ

must_not クエリとは、NOT 条件を表すクエリです。

GET /demo_bool/_search
{
  "query":{
    "bool": {
      "must_not":[
        {"match":{"text":"Elasticsearch"}},
        {"range":{"id":{"lte":"1"}}}
      ]
    } 
  }
}
    "hits" : [
      {
        "_index" : "demo_bool",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 0.0,
        "_source" : {
          "text" : "This is a pen.",
          "id" : 3
        }
      }

「text = "Elasticsearch" を含む」ではない AND「id = 1以下」ではないドキュメントを検索できました。

つまり、"_score" が 0 のドキュメントです。

filter クエリ

fileter クエリとは、指定したドキュメント以外を検索対象から外すクエリです。

GET /demo_bool/_search
{
  "query":{
    "bool": {
      "must": [
        {"match":{"text":"Elasticsearch"}}
      ], 
      "filter":[
        {"range":{"id":{"lte":"1"}}}
      ]
    } 
  }
}
    "hits" : [
      {
        "_index" : "demo_bool",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.45315093,
        "_source" : {
          "text" : "This is Elasticsearch test.",
          "id" : 1
        }
      }

"text" = "Elasticsearch is God."、 "id" = 2 が検索結果から除外されています。

sort クエリ

sort クエリは、クエリの結果を指定したフィールドでソートします。

GET /demo_bool/_search
{
  "sort": [
    {
      "id": {
        "order": "desc"
      }
    }
  ]
}
"_source" : {
          "text" : "This is a pen.",
          "id" : 3
        },
        "sort" : [
          3
        ]
        (中略)
        "_source" : {
          "text" : "Elasticsearch is God.",
          "id" : 2
        },
        "sort" : [
          2
        ]
        (中略)
        "_source" : {
          "text" : "This is Elasticsearch test.",
          "id" : 1
        },
        "sort" : [
          1
        ]

id フィールドの値を用いて、降順 (desc) となっています。

日本語検索 (Analyzer の設定)

Analyzer とは、Text Analysis (後述) を実行するためのルールです。

Text Analysis とは、トークナイズ (文章を単語に分解) したり、正規化 (Dog, dogs, doggy 等を dog に置き換え) たりする処理です。

デフォルトの Analyzer では、日本語をトークナイズできないため、日本語をうまく検索できません。そのため、Analyzer を適切に設定する必要があります。

デフォルトの Analyzer を使用して日本語を検索した場合どうなるか

POST demo_standard_analyzer/_doc
{
  "text":"今年の東京都の予算が決まる。"
}

上記のドキュメントに対して「京都」という単語で検索してみます。

GET demo_standard_analyzer/_search
{
  "query": {
    "match": {
      "text": "京都"
    }
  }
}

残念ながらヒットしてしまいました。

「京都」という単語で「東京都」のドキュメントがヒットするのは使いにくいですね。「東京都」の「京都」の部分にヒットしてしまったのでしょうか?

いいえ。現実はもっと酷いことになっています。

GET demo_standard_analyzer/_search
{
  "query": {
    "match": {
      "text": "まほうつかい"
    }
  }
}
      {
        "_index" : "demo_standard_analyzer",
        "_type" : "_doc",
        "_id" : "jRQ06nUBRDMYXy6O16gl",
        "_score" : 0.2876821,
        "_source" : {
          "text" : "今年の東京都の予算が決まる。"
        }
      }

なんと「まほうつかい」で「今年の東京都の予算が決まる。」がヒットします。

この原因を探るため、デフォルトの Analyzer のトークナイズ結果を確認します。

POST demo_standard_analyzer/_analyze
{
  "text":"今年の東京都の予算が決まる"
}
    {
      "token" : "今",
      "start_offset" : 0,
      "end_offset" : 1,
      "type" : "<IDEOGRAPHIC>",
      "position" : 0
    },
    {
      "token" : "年",
      "start_offset" : 1,
      "end_offset" : 2,
      "type" : "<IDEOGRAPHIC>",
      "position" : 1
    },
    {
      "token" : "の",
      "start_offset" : 2,
      "end_offset" : 3,
      "type" : "<HIRAGANA>",
      "position" : 2
    },
    {
      "token" : "東",
      "start_offset" : 3,
      "end_offset" : 4,
      "type" : "<IDEOGRAPHIC>",
      "position" : 3
    },
    {
      "token" : "京",
      "start_offset" : 4,
      "end_offset" : 5,
      "type" : "<IDEOGRAPHIC>",
      "position" : 4
    },
    {
      "token" : "都",
      "start_offset" : 5,
      "end_offset" : 6,
      "type" : "<IDEOGRAPHIC>",
      "position" : 5
    },
    {
      "token" : "の",
      "start_offset" : 6,
      "end_offset" : 7,
      "type" : "<HIRAGANA>",
      "position" : 6
    },
    {
      "token" : "予",
      "start_offset" : 7,
      "end_offset" : 8,
      "type" : "<IDEOGRAPHIC>",
      "position" : 7
    },
    {
      "token" : "算",
      "start_offset" : 8,
      "end_offset" : 9,
      "type" : "<IDEOGRAPHIC>",
      "position" : 8
    },
    {
      "token" : "が",
      "start_offset" : 9,
      "end_offset" : 10,
      "type" : "<HIRAGANA>",
      "position" : 9
    },
    {
      "token" : "決",
      "start_offset" : 10,
      "end_offset" : 11,
      "type" : "<IDEOGRAPHIC>",
      "position" : 10
    },
    {
      "token" : "ま",
      "start_offset" : 11,
      "end_offset" : 12,
      "type" : "<HIRAGANA>",
      "position" : 11
    },
    {
      "token" : "る",
      "start_offset" : 12,
      "end_offset" : 13,
      "type" : "<HIRAGANA>",
      "position" : 12
    }

Standard Analyzer は文章を1文字ずつに区切っているだけです。

そのため、以下の文字が一致しています。

  • 「決まる」の「ま」
  • 「まほうつかい」の「ま」

そのため、「まほうつかい」という単語で「今年の東京都の予算が決まる」がヒットしました。

これでは流石に使い物にならないのでちゃんと日本語に対応した Analyzer で文章を単語に分解しましょう。

Analyzer のルールは、次の3つの要素から構成されます。

Analyzer の要素説明
Char Filter文章を変換
Tokenizer文章を Token(単語)に分割
Token filterToken (単語) を変換

Analyzer の構成要素

Analyzer は次の3つの要素から構成されます。

Analyzer の要素説明
Char Filter文章をトークナイズしやすいように変換
Tokenizer文章を Token(単語)に分割
Token filterToken (単語) を正規化等のために変換
Char Filter --> Tokenizer --> Token filter の順

Analyzer は3つの要素を上から順番に処理します。

Char Filter (文章を変換)
Char Filter説明
HTML Strip Character FilterHTML のタグを除去
Pattern replace character filter正規表現で置換
Tokenizer (文章を Token [= 単語] に分割)
Tokenizer説明
Standard Tokenizerデフォルトのトークナイザー
kuromoji_tokenizer日本語用のトークナイザー
Token filter (Token [= 単語] を変換)
Token Filter説明
Lower case Token Filterトークンをすべて小文字に変換"Google" で "google" も検索
Stop Token Filterストップワードを設定助詞を検索から除外
Stemmer Token Filterステミングを設定「swims」と「swimming」を「swim」に変換
Synonym Token Filterシノニムを設定「ググる」と「検索する」を「検索」に変換

Kuromoji Analyzer (日本語検索)

ここからは、形態素解析を利用して日本語をトークナイズするプラグインである kuromoji Analysis Plugin を利用して、日本語を検索をする方法を紹介します。

なお、kuromoji Analysis Pluginkuromoji analyzer は以下の要素から構成されます。

Analyzerkuromoji analyzer の構成要素
Char Filter無し
Tokenizerkuromoji_tokenizer
Token Filterkuromoji_baseform token filter 
kuromoji_part_of_speech token filter
cjk_width token filter
ja_stop token filter
kuromoji_stemmer token filter
lowercase token filter
kuromoji Analysis Plugin をインストール
FROM docker.elastic.co/elasticsearch/elasticsearch:7.12.1
RUN /usr/share/elasticsearch/bin/elasticsearch-plugin install analysis-kuromoji
version: '3'
services:
  elasticsearch:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: elasticsearch
    environment:
      - discovery.type=single-node
    ports:
      - 9200:9200
  kibana:
    image: kibana:7.12.1
    ports:
      - "5601:5601"
    environment:
      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200

※既に Elasticsearch, kibana コンテナを起動してる場合は、docker-compose down を実行してください。

docker を利用しない方法

sudo /usr/share/elasticsearch/bin/elasticsearch-plugin install analysis-kuromoji
sudo systemctl restart elasticsearch
Kuromoji Analyzer の利用方法
PUT demo_analyzer
{
  "mappings": {
    "properties": {
      "text":{
        "type": "text",
        "analyzer": "kuromoji"
      }
    }
  }
}

次に demo_analyzer インデックスでドキュメントを作成し、日本語検索を行います。

POST demo_analyzer/_doc
{
  "text":"東京都の予算が決まる。"
}
GET demo_analyzer/_search
{
  "query": {
    "match": {
      "text": "東京都"
    }
  }
}
    "hits" : [
      {
        "_score" : 0.5753642,
        "_source" : {
          "text" : "東京都の予算が決まる。"
        }

無事に日本語で検索できるようになりました。

形態素解析の利点

「京都」を検索してみます。

通常の部分一致検索であれば「東京都」が引っかかり、検索のノイズとなります。

GET demo_analyzer/_search
{
  "query": {
    "match": {
      "text": "京都"
    }
  }
}

京都」という単語で「東京都」がヒットしません。正しく別の単語と認識してくれています。

どのように日本語が形態素解析されているか確認します。

GET demo_analyzer/_analyze
{
  "analyzer": "kuromoji", 
  "text":"今年の東京都の予算が決まる。"
}
{
  "tokens" : [
    {
      "token" : "今年",
      "type" : "word",
    },
    {
      "token" : "東京",
      "type" : "word",
    },
    {
      "token" : "都",
      "type" : "word",
    },
    {
      "token" : "予算",
      "type" : "word",
    },
    {
      "token" : "決まる",
      "type" : "word",
    }
  ]
}

「東京」と「都」で分割しているため、「東京都」が「京都」に引っかかりません。

Custom Analyzer を作成

Custom Analyzer は、自分で以下の3つの要素を指定して Analyzer を作成できます。
Char Filter
Tokenizer
Token filter

例えば、以下のような Custom Analyzer を作成します。

Analyzer の要素利用する要素やりたいこと
Char Filterhtml_stripHTML の <p> タグを除去
Tokenizerkuromoji_tokenizer日本語の文章を Token (単語) に変換
Token filterfilter (stopwords)"今年"、"の"、"が" を除去
PUT custome_analyze
{
  "settings": {
    "analysis": {
      "analyzer": {
        "original_analyze":{
          "char_filter":["html_strip"],
          "tokenizer":"kuromoji_tokenizer",
          "filter":["my_stop"]
        }
      },
      "filter":{
        "my_stop":{
          "type":"stop",
          "stopwords":["今年","の","が"]  
        }
      }
    }
  }
}

作成した Analyzer を使用してみます。

POST  custome_analyze/_analyze
{
  "analyzer": "original_analyze",
  "text": "<p>今年の東京都の予算が決まる。</p>"
}
{
  "tokens" : [
    {
      "token" : "東京",
      "start_offset" : 6,
      "end_offset" : 8,
      "type" : "word",
      "position" : 2
    },
    {
      "token" : "都",
      "start_offset" : 8,
      "end_offset" : 9,
      "type" : "word",
      "position" : 3
    },
    {
      "token" : "予算",
      "start_offset" : 10,
      "end_offset" : 12,
      "type" : "word",
      "position" : 5
    },
    {
      "token" : "決まる",
      "start_offset" : 13,
      "end_offset" : 16,
      "type" : "word",
      "position" : 7
    }
  ]
}

カスタム Analyzer "original_analyze" で定義したとおりの動作をしています。

集計・分類 (Aggregations)

Aggregations には次の2種類が存在します。

以下の "demo_agg" インデックスを利用して、Aggregations の動きを確認します。

POST /_bulk
{ "index" : { "_index" : "demo_agg", "_id" : "1" } }
{ "text" : "This is Elasticsearch test.","num":1 }
{ "index" : { "_index" : "demo_agg", "_id" : "2" } }
{ "text" : "Elasticsearch is God.","num":2 }
{ "index" : { "_index" : "demo_agg", "_id" : "3" } }
{ "text" : "This is a pen.","num":3 }
{ "index" : { "_index" : "demo_agg", "_id" : "4" } }
{ "text" : "I have a pen.","num":2 }

集計 (Metrics Aggregations)

Metrics Aggregations は、集計したドキュメントから指定の方法でメトリクスを計算します

メトリクスの指定できる計算方法の例は以下です。

ここでは、Avg Aggregation (平均) を利用してみます。

GET demo_agg/_search
{
  "size": 0,
  "aggs": {
    "hoge_name": {
      "avg": {
        "field":"num"
      }
    }
  }
}
  "aggregations" : {
    "hoge_name" : {
      "value" : 2.0
    }
  }

"num" フィールドの値は [1, 2, 3, 2] なので、平均の 2.0 を正しく計算できています。

分類 (Buckets Aggregations)

Buckets Aggregations とは、フィールドの値を分類し、各バケットに格納します。

Buckets Aggregations として、次の2種類を紹介します。

Range aggregation

Range aggregation とは、指定した範囲の値を格納するバケットを作成する方法です。

例として、次の2つのバケットを作成します。

  • num フォールドの値が [0 ~ 2] のドキュメントを格納するバケット
  • num フォールドの値が [2 以上] のドキュメントを格納するバケット
GET demo_agg/_search
{
  "size": 0,
  "aggs": {
    "bucket_name": {
      "range": {
        "field":"num",
        "ranges": [
          {
            "from": 0, 
            "to":2
          },
          {
            "from": 2
          }
        ]
      }
    }
  }
}
  "aggregations" : {
    "bucket_name" : {
      "buckets" : [
        {
          "key" : "0.0-2.0",
          "from" : 0.0,
          "to" : 2.0,
          "doc_count" : 1
        },
        {
          "key" : "2.0-*",
          "from" : 2.0,
          "doc_count" : 3
        }
      ]
    }

次の結果が確認できます。

  • num フォールドの値が [0 ~ 2] を格納するバケット: 1つのドキュメント (num = [1])
  • num フォールドの値が [2 以上] を格納するバケット: 3つのドキュメント (num = [2, 3, 2])
Histogram aggregation

Histogram aggregation とは、等間隔でバケットを作成する方法です。

1000 間隔でバケットを分けたヒストグラムの例

"num" フィールドを、1間隔でバケットを作成した例は、以下のとおりです。

GET demo_agg/_search
{
  "size": 0,
  "aggs": {
    "bucket_name": {
      "histogram": {
        "field":"num",
        "interval": 1
      }
    }
  }
}
  "aggregations" : {
    "bucket_name" : {
      "buckets" : [
        {
          "key" : 1.0,
          "doc_count" : 1
        },
        {
          "key" : 2.0,
          "doc_count" : 2
        },
        {
          "key" : 3.0,
          "doc_count" : 1
        }
      ]

num の値が1ごとにバケットが作成されていることがわかります。

Dynamic index settings の変更

Dynamic index settings とは、Elasticsearch が稼働中に変更可能な設定です。

今回は、index.number_of_replicas を Update index settings API で変更してみます。

index.number_of_replicas

The number of replicas each primary shard has. Defaults to 1.

https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules.html#dynamic-index-settings
PUT /my-index-000001
GET /my-index-000001
    "settings" : {
      "index" : {
        (中略)
        "number_of_replicas" : "1",
PUT /my-index-000001/_settings
{
  "index" : {
    "number_of_replicas" : 2
  }
}

上記のように、ピリオドごとに { } でネストします。

GET /my-index-000001
    "settings" : {
      "index" : {
        (中略)
        "number_of_replicas" : "2",

index.number_of_replicas が2に変更できています。

関連記事

ビッグデータ分析基盤入門シリーズの続きは以下です。


参考文献

公式ドキュメント

Elasticsearch Guide [7.15] | Elastic