Elasticsearch (分散型全文検索エンジン)入門

全文検索

ElastiElasticsearch とは

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

全文検索とは

複数のドキュメント(ファイル)から特定の文字列を検索することを全文検索と言います。

全文検索技術には grep 型と索引(インデックス)型が存在し、Elasticsearch は索引型の全文検索を複数のコンピュータに分散処理させることで、高速な検索を実現します。

grep 型

複数のファイルを順番に検索する方法です。検索対象が増加するにつれて、検索速度が大幅に低下します。UNIX の文字列検索コマンド grep がこれに当たります。

索引(インデックス)型

本の索引と同じように、あらかじめ複数のドキュメントに対してインデックスを作成しておくことで検索速度を向上する方法です。索引型の全文検索を利用した例は以下のとおりです。

  • Elasticsearch のドキュメント検索
  • Google 検索(複数のドキュメント(Webページ)から特定の文字列を検索)
  • GitHub のコード検索(複数のドキュメント(ソースコード)から特定の文字列を検索)

ここで以下の2つのドキュメントを例にして、インデックスを作成した場合の例を紹介します。

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

上記のドキュメントから、ドキュメントに単語が含まれる場合を1、含まれない場合を0とすると、次のようなインデックスが作成可能です。

インデックスの例
Ilikesearchenginekeywordsbygoogle
ドキュメント11111000
ドキュメント21010111

なお、インデックスは左から右への行単位でしか検索できません。そのため、このままでは google という単語を使用しているドキュメントを探す場合は、全ての行をスキャンする必要があります。

そこで Elasticsearch では転置インデックスと呼ばれるインデックスを使用します。転置インデックスは、次のようにインデックスの転置行列(行と列を入れ替えたもの)です。

転置インデックスの例
ドキュメント1ドキュメント2
I11
like10
search11
engine10
keywords01
by01
google01

このため、google という単語が登場するインデックスは「google」という1行だけをスキャンするだけで済みます。

Elasticsearch 利用例

社内外のドキュメント検索

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

異常検知

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

Elastic Stack(Elasticsearch 周辺プロダクト)

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

Kibana

Elasticsearch 内のドキュメントを可視化するいわゆる BI ツールです。以下の公式で紹介されている GIF 画像を見るのがわかりやすいです。

Elasticsearch 公式ページより引用

Logstash

Elasticsearch に送信するデータの形式を変換するツールです。いわゆる ETL ツールの Transform, Load 部分を担います。

アプリケーションサーバーから Elasticsearch サーバーにログを転送する際に、適切なデータ形式に変更するために使用します。

Beats

データを転送するシッパーです。いわゆる ETL の Extract を担います。
アプリケーションサーバーから Logstash や Elasticsearch にデータを送ります。

Elasticsearch のアーキテクチャ

論理的な概念

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

論理的な概念

次に各用語について説明をします。

フィールド

項目名(Key)と値(Value)の組のことです。ファイルシステムで言う”ファイルの中身”に当たります。それぞれの Key は型を持ちます。

  • 文字列を表す text 型
  • 時間を表す date 型など

ドキュメント

フィールドの集合をドキュメントと呼びます。ファイルシステムで言う”ファイル”にあたります。ドキュメントは JSON オブジェクトです。

インデックス

ドキュメントの集合をインデックスと呼びます。ファイルシステムで言う”フォルダ”に当たります。ドキュメントをインデックスに格納する時、転置インデックスを作成します。

マッピング

各フィールドの Key に対する値のデータの型などを定義するものです。マッピングは1つのインデックスに1つ存在します。

上記の「論理的な概念図」に記載されたマッピングでは、"Tweet" キーは "Text" 型と定義しているため、"Tweet" キーの値は、必ず Text 型の文字列となります。

物理的な概念

Elasticsearch の物理的な概念の全体図は以下のとおりです。また、論理的な概念のインデックスとの関わりも合わせて記載しています。

物理的な概念と論理的な概念の関連

ノード

1台の Elasticsearch サーバーのことです。(※1つのOSに複数台の Elasticsearch ノードを起動することは可能です。)

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

  • Master ノード
    • クラスターのメタデータなどを管理するノード
    • Master ノードはクラスターに1台のみ
    • マスターノードに昇格可能なノードは Master-eligible と呼ばれるが、Master ノードでは無い
  • Data ノード
    • 実際のデータを格納するノード
    • リクエストの処理(検索や集計など)を実施
    • リクエストを別のノードにルーティング(別のノードがシャード持っている場合など)
  • Ingest ノード
    • データの変換や加工を実施し、Data ノードに格納
  • Coordinating ノード
    • リクエストをルーティングする(Dataノードもできる)
    • Data ノードにルーティング作業の負荷を掛けたくない場合にルーティング処理専用のノードを用意するため

クラスター

複数のノードの集合です。クラスターに検索処理リクエストを投げると、各ノードに検索処理が分散されます。

シャード

論理的な概念で紹介した「インデックス」のデータを分割し、ノードに保存したデータのことです。これにより、検索処理を各ノードに分散することができます。なお、シャードの実体は Lucene インデックスファイルです。

シャードには次の2種類が存在します。

  • プライマリーシャード
    • データのオリジナルです。
    • 最初に更新処理を行うシャードです。
  • レプリカシャード
    • プライマリーシャードのコピーです。
    • プライマリーシャードの更新が終了すると、レプリケーションされます。
    • 検索負荷の分散や、データのバックアップとして利用します。

Elasticsearch の使い方

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

Elasticsearch のインストール

公式ドキュメントがとてもわかりやすいので公式ドキュメントを見ながらインストールすることを推奨します。

Elasticsearch インストール

Installing Elasticsearch | Elasticsearch Reference [7.10] | Elastic

Kibana インストール

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

本記事では Amazon Linux 2(4.14.171-136.231.amzn2.x86_64)でコピペするだけでインストール可能な方法を掲載しておきます。

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 の基本操作(CRUD)

Elasticsearch は REST API もしくは Kibana の Dev Tools を使用して操作をします。(Kibana は内部で API を叩いています。)

なお、Kibana の Dev Tools の開き方は以下のとおりとなります。

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

Elasticsearch のドキュメントでは、以下のような CRUD 操作が可能です。

  • Create:ドキュメントをインデックスに登録
  • Read:ドキュメントを取得
  • Update:ドキュメントを更新
  • Delete:ドキュメントを削除

以降の CRUD 操作の説明では、REST API を利用するために curl を利用した実行方法、および Kibana の DevTools を利用した実行方法の2種類を記載しますので、お好きな方をご利用ください。

Create ドキュメントの登録

以下のように実行することで Elasticsearch にドキュメントを登録可能です。

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

Kibana の DevTools を利用する場合は、こんな感じでコマンドを貼り付けて、右上の三角ボタンを押します。

Read ドキュメントを取得

Read 操作によって先程作成したドキュメントを取得してみます。

curl localhost:9200/2020-11-tweet/_doc/1
GET /2020-11-tweet/_doc/1
{
  "_index" : "2020-11-tweet",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 1,
  "_seq_no" : 8,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "date" : "2020/11/01 09:00 JST",
    "Tweet" : "ツイッターをはじめました。",
    "User ID" : "hoge"
  }
}

Update:ドキュメントを更新

ドキュメントの内容を更新します。

curl localhost:9200/2020-11-tweet/_doc/1 -XPOST -H "Content-Type: application/json" -d '
{
  "date":"2020/11/01 09:00 JST",
  "Tweet":"ドキュメントを更新したよ。",
  "User ID":"hoge"
}'
POST /2020-11-tweet/_doc/1
{
  "date":"2020/11/01 09:00 JST",
  "Tweet":"ドキュメントを更新したよ。",
  "User ID":"hoge"
}

更新結果を確認する。

curl localhost:9200/2020-11-tweet/_doc/1
GET /2020-11-tweet/_doc/1
{
  "_index" : "2020-11-tweet",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 7,
  "_seq_no" : 14,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "date" : "2020/11/01 09:00 JST",
    "Tweet" : "ドキュメントを更新したよ。",
    "User ID" : "hoge"
  }
}

Delete:ドキュメントを削除

作成したドキュメントを削除します。

curl localhost:9200/2020-11-tweet/_doc/1 -XDELETE
DELETE /2020-11-tweet/_doc/1

ドキュメントが削除されていることを確認します。

curl localhost:9200/2020-11-tweet/_doc/1
GET /2020-11-tweet/_doc/1
{
  "_index" : "2020-11-tweet",
  "_type" : "_doc",
  "_id" : "1",
  "found" : false
}

Elasticsearch のマッピング管理

マッピングは2章で説明したとおりフィールドのKey の型を定義したものです。一度作成したマッピングはフィールドの追加を除き、修正はできません。インデックスを作成し直しです。

Elasticsearch では、以下のようにインデックスにドキュメントを作成した際に自動でマッピングが作成されます。

PUT /mapping_test/_doc/1
{
  "date":"2020/11/01 09:00:00+0900",
  "Tweet":"This is a mapping test."
}
GET /mapping_test
{
  "mapping_test" : {
    "aliases" : { },
    "mappings" : {
      "properties" : {
        "Tweet" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        "date" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        }
      }
    },

上記の結果を見ると、以下のようなマッピングが定義されていることがわかります。(text 型と keyword 型の違いについては後述します。)

  • date, Tweet キーは text 型
  • date.keyword, text.keyword キーは keyword 型

上記で作成したマッピングの date キーは text 型で登録されていますが、date 型で登録したい場合があります。この場合は、手動でマッピングを作成する必要があります。

手動でマッピングを作成する

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

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

無事 date キーが 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",
  "tweet":"This is a mapping test."
}
failed to parse field [date] of type [date] in document with id '1'. Preview of field's value: '2020/11/01 09:00+0900'

想定どおり date フィールドがパースできないよ。と言われます。

それでは、正しいフォーマットでドキュメントを作成してみます。

PUT /mapping_test2/_doc/2
{
  "date":"2020/11/01 09:00:00+0900",
  "tweet":"This is a mapping test."
}

正しいフォーマットでドキュメントを作成すると成功しましたね!

既存のマッピングにフィールドを追加する

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

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

PUT /mapping_test2/_mapping
{
  "properties":{
    "additional_field":{
      "type":"text"
    }
  }
}
GET /mapping_test2
{
  "mapping_test2" : {
    "aliases" : { },
    "mappings" : {
      "properties" : {
        "additional_field" : {
          "type" : "text"
        },
        "date" : {
          "type" : "date",
          "format" : "yyyy/MM/dd HH:mm:ssZ"
        },
        "tweet" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword"
            }
          }
        }
      }
    }

additional_field フィールドが追加されたことを確認できました。

テンプレートを作成する

インデックスごとに同じマッピングを何度も作成することは大変です。これを解決するのがテンプレートです。

テンプレートは指定した名前のインデックスが作成された時、テンプレートのマッピングを用いてインデックスを作成します。テンプレートを作成するには、template API を利用します。

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

上記のテンプレートでは、"index_patterns": "test*" で指定したように、インデックス名のプレフィックスに "test" がある場合、指定したマッピングを作成します。

テンプレートの動作を確認してみます。

PUT /test
GET /test
{
  "test" : {
    "aliases" : { },
    "mappings" : {
      "properties" : {
        "date" : {
          "type" : "date",
          "format" : "yyyy/MM/dd HH:mm:ssZ"
        },
        "tweet" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword"
            }
          }
        }
      }
    },

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

Elasticsearch の検索方法

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

search API のデモを行うために、まずは検索デモ用のドキュメントを3個作成します。

ドキュメントを[Create ドキュメントの登録]を3回繰り返してもいいのですが、bulk 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つのドキュメントがすべて検索できたことがわかります。

search API で検索条件を指定する場合、クエリ DSL と呼ばれる Elasticsearch 固有の言語を利用します。search API で利用するクエリ DSL は以下の4種類があります。

  • match クエリ
  • term 検索クエリ
  • range クエリ
  • bool クエリ

match クエリ

match クエリは転置インデックスを利用して全文検索を行うクエリとなります。text 型フィールドの文字列は、単語に分割され転置インデックスが作成されているため、match クエリに利用可能です。

  • match クエリ
      match クエリは全文検索で利用するクエリです。
      試しに Elasticsearch という単語が含まれるドキュメントを検索します。
      GET /demo_search/_search
      {
        "query":{
          "match": {
            "text": "Elasticsearch"
          } 
        }
      }
          "hits" : [
            {
              "_index" : "demo_search",
              "_type" : "_doc",
              "_id" : "2",
              "_score" : 0.5077718,
              "_source" : {
                "text" : "Elasticsearch is God."
              }
            },
            {
              "_index" : "demo_search",
              "_type" : "_doc",
              "_id" : "1",
              "_score" : 0.45315093,
              "_source" : {
                "text" : "This is Elasticsearch test."
              }
            }

      Elasticsearch が含まれる2つのドキュメントが検索にヒットしていることがわかります。

      次に指定した単語2つに一致するドキュメントを検索します。指定した単語2つの AND を取るには operator に AND を指定します。(デフォルトでは ORです。)

      GET /demo_search/_search
      {
        "query":{
          "match": {
            "text":{
              "query": "Elasticsearch test",
              "operator":"AND"
            }
          } 
        }
      }
          "hits" : [
            {
              "_index" : "demo_search",
              "_type" : "_doc",
              "_id" : "1",
              "_score" : 1.3988109,
              "_source" : {
                "text" : "This is Elasticsearch test."
              }
            }

      ""Elasticsearch"と"test"のどちらも含まれるドキュメントだけが出力されました。

  • match_phrase クエリ

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

    例えば、"Elasticsearch test"の順であれば検索はヒットします。

    GET /demo_search/_search
    {
      "query":{
        "match_phrase": {
          "text":{
            "query": "Elasticsearch test"
          }
        } 
      }
    }
        "hits" : [
          {
            "_index" : "demo_search",
            "_type" : "_doc",
            "_id" : "1",
            "_score" : 1.398811,
            "_source" : {
              "text" : "This is Elasticsearch test."
            }
          }

    しかし"test Elasticsearch"はヒットしません。"test"の後に"Elasticsearch"が続くドキュメントが無いからです。

    GET /demo_search/_search
    {
      "query":{
        "match_phrase": {
          "text":{
            "query": "test Elasticsearch"
          }
        } 
      }
    }
    "hits" : [ ]

Term 検索クエリ

フィールドに対して完全一致検索を実施します。

  • term クエリ

    上述のとおり、フィールドに対して完全一致検索を実施します。

    GET /demo_search/_search
    {
      "query":{
        "term": {
          "text.keyword": "This is Elasticsearch test."
        } 
      }
    }
          {
            "_index" : "demo_search",
            "_type" : "_doc",
            "_id" : "1",
            "_score" : 0.9808291,
            "_source" : {
              "text" : "This is Elasticsearch test."
            }
          }

    以下のように "Elasticsearch" で単語検索することはできません。"This is Elasticsearch test."とは部分一致であり、完全一致ではありません。

    GET /demo_search/_search
    {
      "query":{
        "match": {
          "text": "Elasticsearch"
        } 
      }
    }
    "hits" : [ ]

    text 型のフィールドに Term クエリで検索すると予期せぬ結果となる場合があります。

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

    これは text 型では "This is Elasticsearch test." が形態素解析され、単語に分割されるため、分解した単語と "This is Elasticsearch test." が一致しないためです。

  • Terms

    複数の文字列を完全一致検索するためには、複数系のsを付けた Terms クエリを利用します。

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

Range クエリ

指定した値の範囲検索します。

まずは Range クエリのデモ用に test_range インデックスを作成します。(なお、テンプレートで date フィールドのマッピングを作成していることが前提となりますので、まだ作成していない方はリンク先からテンプレートを作成してください。)

POST /_bulk
{ "index" : { "_index" : "test_range", "_id" : "1" } }
{ "date" : "2000/01/01 09:00:00+0900" }
{ "index" : { "_index" : "test_range", "_id" : "2" } }
{ "date" : "2010/08/01 09:00:00+0900" }
{ "index" : { "_index" : "test_range", "_id" : "3" } }
{ "date" : "2020/11/01 09:00:00+0900" }

準備が完了したので Range クエリを使用して、"2010/01/01 09:00:00+0900" より新しく、"2030/01/01 09:00:00+0900" よりも古いドキュメントを検索します。

GET /test_range/_search
{
  "query":{
    "range": {
      "date":{
        "gte": "2010/01/01 09:00:00+0900",
        "lte": "2030/01/01 09:00:00+0900"
      }
    } 
  }
}
    "hits" : [
      {
        "_index" : "test_range",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.0,
        "_source" : {
          "date" : "2010/08/01 09:00:00+0900"
        }
      },
      {
        "_index" : "test_range",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 1.0,
        "_source" : {
          "date" : "2020/11/01 09:00:00+0900"
        }
      }

"2010/01/01 09:00:00+0900" より新しく、"2030/01/01 09:00:00+0900" よりも古いドキュメントを取得できたことがわかります。

bool クエリ

match クエリ、term クエリ、range クエリを組み合わせて、AND, OR, NOT を取ることができます。

まずは、bool クエリのデモを行うために、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 クエリ

    AND 条件です。text フィールドで "Elasticsearch" と match し、id が 1 以下のドキュメントを検索します。

    GET /demo_bool/_search
    {
      "query":{
        "bool": {
          "must":[
            {"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
            }
          }
  • should クエリ

    OR 条件です。先程と同じ条件で bool クエリを shoud クエリに変更します。

    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
            }
          }

    先程ヒットしていなかった id=2 のドキュメントも "Elasticsearch" が含まれているため、検索結果に現れるようになりました。なお、id,text 両方の条件を満たす上のほうが "_score"の値が高くなります。

  • must_not

    NOT を表します。先程と同じ条件で bool クエリを must_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
            }
          }

    両方の条件を満たさないドキュメントが表示されたことがわかります。条件を全く満たさないので、"_score"は当然 0.0 です。

  • filter

    フィルターで指定されたドキュメント以外は検索対象から外します。外されたドキュメントは、検索結果の"_score"に影響を与えません。

    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
            }
          }

    まず、filter 条件で id フィールドの値が1以下のドキュメントのみを返します。その後、返されたドキュメントから match クエリの内容に一致するドキュメントを検索します。

sort クエリ

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

id フィールドでソートする場合は以下のように記載します。

GET /demo_bool/_search
{
  "sort": [
    {
      "id": {
        "order": "desc"
      }
    }
  ]
}
    "hits" : [
      {
        "_index" : "demo_bool",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : null,
        "_source" : {
          "text" : "This is a pen.",
          "id" : 3
        },
        "sort" : [
          3
        ]
      },
      {
        "_index" : "demo_bool",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : null,
        "_source" : {
          "text" : "Elasticsearch is God.",
          "id" : 2
        },
        "sort" : [
          2
        ]
      },
      {
        "_index" : "demo_bool",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : null,
        "_source" : {
          "text" : "This is Elasticsearch test.",
          "id" : 1
        },
        "sort" : [
          1
        ]
      }

id フィールドの値で降順(desc)となっていることがわかります。

Elasticsearch の Analyzer

Analyzer は文章を単語単位に分割するもののことです。Elasticsearch のデフォルトの Standard analyzer は日本語に対応していません。

日本語検索を正しく行うためには、kuromoji Analysis Plugin と呼ばれる日本語形態素解析用のプラグインを使用して、文章を単語に分割する必要があります。

なぜ、Analyzer を使用して単語に分割する必要があるか

「ドキュメントを正しく検索できないから」これに尽きるのですが、具体例を見たほうがわかりやすいので以下をご覧ください。

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

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

GET demo_standard_analyzer/_search
{
  "query": {
    "match": {
      "text": "京都"
    }
  }
}
    "hits" : [
      {
        "_index" : "demo_standard_analyzer",
        "_type" : "_doc",
        "_id" : "jRQ06nUBRDMYXy6O16gl",
        "_score" : 0.5753642,
        "_source" : {
          "text" : "今年の東京都の予算が決まる。"
        }
      }

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

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

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

なんと「まほうつかい」で「今年の東京都の予算が決まる。」がヒットします。この原因は Analyzer が作成する転置インデックスに起因してます。

analyze API を利用することでドキュメントの文字列が Analyzer によってどのように文章が分解されて、転置インデックスに格納されているのか確認できるため、原因を調査してみます。

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

"token" キーの値を見てわかるとおり、Standard Analyzer は文章を1文字ずつに区切って転置インデックスに格納しているだけです。そのため、以下の文字が一致しています。

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

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

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

kuromoji Analysis Plugin をインストール

日本語に対して形態素解析を行い、単語に分解するプラグインとして有名なものに kuromoji があります。今回はこちらのプラグインをインストールします。

sudo /usr/share/elasticsearch/bin/elasticsearch-plugin install analysis-kuromoji

次に kuromoji プラグインを有効化するために Elasticsearch を再起動します。

sudo systemctl restart elasticsearch

Kuromoji Analyzer を使用する

Analyzer はインデックスのマッピングで設定可能です。

実際に demo_analyzer インデックス で 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" : [ ]

「まほうつかい」で「東京都の予算が決まる。」がヒットするというふざけた事象がなくなりました!

「京都」という単語ではどうでしょうか?こちらは grep 検索をする場合はひっかかりますよね。

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

「京都」という単語で「東京都」がヒットしなくなりました!kuromoji Analyzer ではどのような転置インデックスが作成されているのか確認してみます。

GET demo_analyzer/_analyze
{
  "analyzer": "kuromoji", 
  "text":"今年の東京都の予算が決まる。"
}
{
  "tokens" : [
    {
      "token" : "今年",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "東京",
      "start_offset" : 3,
      "end_offset" : 5,
      "type" : "word",
      "position" : 2
    },
    {
      "token" : "都",
      "start_offset" : 5,
      "end_offset" : 6,
      "type" : "word",
      "position" : 3
    },
    {
      "token" : "予算",
      "start_offset" : 7,
      "end_offset" : 9,
      "type" : "word",
      "position" : 5
    },
    {
      "token" : "決まる",
      "start_offset" : 10,
      "end_offset" : 13,
      "type" : "word",
      "position" : 7
    }
  ]
}

「東京」と「都」で分析してくれているようです。そのため、「東京都」が「京都」にひっかからないようになりました。

Analyzer の構成要素

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

  1. Char Filter 文章を変換する
    • HTML Strip Character Filter
      • HTML のタグを除去します
      • <p>hoge</p> --(変換)--> hoge
  2. Tokenizer 文章を Token(単語)に分割
    • Standard Tokenizer(デフォルトのトークナイザー)
    • Kuromoji Tokenizer(日本語用のトークナイザー)
  3. Tokenfilter Token(単語)を変換する
    • Lower case Token Filter
      • トークンの文字をすべて小文字に変換します
      • Google と google を同一トークンと見なしたい時などに使います
    • Stop Token Filter
      • 使用しないトークンを削除します
      • 助詞の削除などに利用されます
    • Stemmer Token Filter
      • ステミング処理を行います
      • 「楽しい」と「楽しむ」を「楽し」に変換します
    • Synonym Token Filter
      • シノニムを正規化します
      • 「ググる」と「検索する」を同じ「検索」に変換します

Analyzer は3つの要素を上から順番に処理することで文章を Token(単語)に分解し、転置インデックスを作成します。

なお、kuromoji Analyzer は以下の要素で構成されます。

また、Analyzer は上記の3つの要素を組み合わせて、以下のように自分で自由に作成することができます。

PUT custome_analyze
{
  "settings": {
    "analysis": {
      "analyzer": {
        "original_analyze":{
          "char_filter":["html_strip"],
          "tokenizer":"kuromoji_tokenizer",
          "filter":["my_stop"]
        }
      },
      "filter":{
        "my_stop":{
          "type":"stop",
          "stopwords":["今年","の","が"]  
        }
      }
    }
  }
}
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" で定義した以下の処理を反映してます。

  • "html_strip" により、HTML の <p> タグが除去
  • "kuromoji_tokenizer" により、文章から Token(単語)に変換
  • "my_stop" で定義した "stopwords" により、"今年"、"の"、"が"を除去

Elasticsearch の Aggrigation(集計・分類)

Elasticsearch では Aggrigation クエリを使用して値の集計や分類も可能です。

まずは、Aggrigation クエリのデモを行うために "demo_agg" インデックスを作成します。

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

集計(Metrics)

"demo_agg" インデックスに対して、集計を行います。まずは avg クエリを利用して値の平均を取ってみます。

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

value は 2でした。今回平均を求めた type の値は[1,2,3,2]なので、この平均は2となり、正しいことがわかります。なお、"size": 0 は _search API の検索結果0個表示する(つまり表示しない)ということです。この値を増やせば、平均を求めるのに使用した検索結果が確認できます。

その他にも以下の集計を行うことが可能です。

  • sum: 合計値を取得する
  • max: 最大値を取得する
  • min: 最小値を取得する
  • stats: 上記全部の値を取得する
  • cardinary: 値の種類を取得する ※[1,2,3,2]の場合は1,2,3 の3種類

分類(Buckets)

以下のように、指定した条件ごとに Buckets を作成し、該当するドキュメントを Buckets の中に入れることができます。(分類)

GET demo_agg/_search
{
  "size": 0,
  "aggs": {
    "bucket_name": {
      "range": {
        "field":"type",
        "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
        }
      ]
    }

実行結果から、以下のことが確認可能です。

  • "type" フィールドの値が 0 以上 2 未満の Bucket には 1 つのドキュメントが含まれます。(type=[1])
  • "type" フィールドの値が 2 以上の Bucket には 3 つのドキュメントが含まれます。(type=[2,3,2])

他にも histgram クエリを利用して値を等間隔で分類分けすることができます。

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

上記は値が1ごとにドキュメントをそれぞれの Buckets に分類しています。

参考文献

公式ドキュメント最高

Elasticsearch Reference [7.10] | Elastic

Elasticsearch を初めて使う場合は、以下の本が一番わかりやすいと思います。この記事ではかなり参考にしています。

0

コメント

タイトルとURLをコピーしました