🛒

Elasticsearchのことがよくわからないあなたと私がざっくり理解するための記事

に公開

はじめに

ながすなりです。

私はElasticsearchのことをほとんど知りません!知らないので、調べよう!と思って、ついでなので記事におこしながら、調べれば同じような悩みを抱えるブラザー達の助けにもなるだろうと思って記事におこしてます。なので、誤っているところ理解が曖昧なところあると思いますが、あくまでざっくりと理解するためのなので悪しからず。

想定読者としては、Elasticsearchのことはよく知らないけど、ちゃんと理解はしたいなぁ...って人です。誤解を恐れずに言ってしまえば、私と同じくらいの知識レベルの方々です!あのさぁ...例えはわかりやすくするために出すんだよ?と元カノに言われたことを思い出しました。どうでも良いですね。

本記事のゴールとしては

Elasticsearch全くわからん
↓
Elasticsearch完全に理解した(ガンギマリ)

となることです。本記事を読み終わる頃には目がバキバキになっていることと思います。

Elasticsearchってなんなのさ

簡単に言ってしまえば、OSSの分散型検索エンジンです。

Elasticsearchを使えば、あらゆる形やサイズのデータをほぼリアルタイムで検索、インデックス付け、保存、分析することができます。[1]

Elasticsearchのドキュメントにこう書いてありました。おおそうかそうか。そうなんだ!...とはならないですよね。この文章の中にもツッコミどころがたくさんです。

  • あらゆる形って?
  • ほぼリアルタイムって?本当に?遅延がないってこと?
  • インデックス付けすると何がいいの?

などなど不十分だと私は思うわけですね。ですがこの辺は後々わかることもあると思うので、とりあえずこの定義で一旦納得しておきましょう。本記事でいくつかは解決することと思います。

検索ならRDBでよくね?

そう思った方...私と知識レベルが一緒です。RDBだって名前で検索できるし、絞り込みだってできるし、部分一致もやろうと思えばできるし、別にRDBでいいじゃん。RDB and SQLでいいじゃん!そう私も思っていましたが、どうやらElasticsearchの方が良い部分があるようです。

  1. 全文検索の性能が断然良い
  2. 多様な検索機能

主な差別化ポイントはここだと思います。Wikipediaさんによると、全文検索は複数の文書(ファイル)から特定の文字列を検索すること[2]を指すようです。
...それって部分一致と何が違うん?勘の良いガキはそんな疑問を持ちますね?chatGPTさんに聞くと


🔸 部分一致検索(LIKE)

WHERE text LIKE '%パン%'

→ "パン" が含まれてる行は取れる。
でも パン屋 も パンチ も パンケーキ も同じように扱われる。

🔹 全文検索(Elasticsearch)
「パンケーキ」は「パン」と「ケーキ」に分かれる(=トークナイズ)

類義語や形態素解析を設定すれば「ホットケーキ」もヒット対象にできる

出現頻度や前後関係をスコア化して、より関連性が高い順に並べる


部分一致だと機械的に(どちらも機械ですが)、一致したら全て並べるということしかできませんが、全文検索なら文字の意味や関連度という観点で判断できるのです。

RDBの検索よりもより柔軟に対応できるということだと思っています。私の理解だと、高度な検索が必要になるような場面、例えばECサイトの商品検索とか要素が色々あって複雑なものを検索したいときに使うものと理解しました。

とりあえず触ってみることにする

なんとなくどういうものかわかったので、実際に手を動かして触ってみましょう。こんな感じのdocker-composeを用意しました。chatGPTくんが。ただバージョンは調べて、最新版に変えてあります。この記事が出来立てほやほやの状態で見ている方は、このまま使えば良いと思いますが、そうでなければこの辺を参考にしてくださいな。

https://hub.docker.com//elasticsearch
https://hub.docker.com/
/kibana

services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.17.4
    container_name: elasticsearch
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false  # セキュリティ無効化(簡易起動用)
    ports:
      - "9200:9200"
    volumes:
      - es-data:/usr/share/elasticsearch/data

  kibana:
    image: docker.elastic.co/kibana/kibana:8.17.4
    container_name: kibana
    environment:
      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
    ports:
      - "5601:5601"
    depends_on:
      - elasticsearch

volumes:
  es-data:

今回はdockerにElasticsearchをのせ、Kibanaという可視化ツールも入れてみることにしましょう。ちなみにElasticsearchもKibanaもdockerイメージめちゃくちゃ重いです。pullするのに結構時間かかったので、コーヒーを淹れてくるといいと思います。

さぁ準備ができたらいつもの通り、このコマンドで実行しましょう!

docker compose up

以下にアクセスすれば、Kibanaの画面が見られますね!

http://localhost:5601

Kibanaのホーム画像

使い方はよくわかってないのですが、とりあえずElasticsearchを押してみましょう。そして下の方にConsoleと書いてあるバーがあるので、そこをクリックするとコンソールが出てきます。これをいじいじしてみましょう。

元々サンプルとして書いてあるコマンドを触ってみましょう!

PUT

実行するコマンド

PUT /my-index

結果

{
  "acknowledged": true,
  "shards_acknowledged": true,
  "index": "my-index"
}
POST

実行するコマンド

POST /my-index/_doc
{
    "id": "park_rocky-mountain",
    "title": "Rocky Mountain",
    "description": "Bisected north to south by the Continental Divide, this portion of the Rockies has ecosystems varying from over 150 riparian lakes to montane and subalpine forests to treeless alpine tundra."
}

結果

{
  "_index": "my-index",
  "_id": "IkP_KJYBCAyC2aJ6JdXr",
  "_version": 1,
  "result": "created",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 0,
  "_primary_term": 1
}
GET

実行したコマンド

GET /my-index/_search?q="rocky mountain"

結果

{
  "took": 15,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 0.5753642,
    "hits": [
      {
        "_index": "my-index",
        "_id": "IkP_KJYBCAyC2aJ6JdXr",
        "_score": 0.5753642,
        "_source": {
          "id": "park_rocky-mountain",
          "title": "Rocky Mountain",
          "description": "Bisected north to south by the Continental Divide, this portion of the Rockies has ecosystems varying from over 150 riparian lakes to montane and subalpine forests to treeless alpine tundra."
        }
      }
    ]
  }
}

さっと使ってみた感想としてはRestfulな感じで、慣れ親しんでる形で直感的に操作できるのはありがたい。Restfulなのだと認識すれば、きっとDELETEもあるんだろうなぁとか予想が立ったりするのでわかりやすいですね。

またGETのところで注目して欲しいのは、q="rocky mountain"の部分と"score": 0.5753642,の部分ですね!完全一致ではなくて、関連度で検索しているのがみて取れます。ここがRDSを使った検索?よりも強いところですね。

データ構造をざっくり理解しよう!

作ってちょっと触ってみたはいいもののよく分からずに触っているので、インデックスとかなんやねん?RDBのインデックスとは違うんかい!と私がなっているので、RDBと比較しながら整理していきましょう。

Elasticsearch RDB
インデックス テーブル
ドキュメント レコード(行)
フィールド カラム(列)

MongoDBなどのドキュメント指向のデータベースなどを使用したことがある方なら、要らぬ説明かもしれませんね。

そう何を隠そうElasticsearchは

ドキュメント指向データベースなのです![3]

NoSQLデータベースかどうかは微妙なラインっぽいです。[3:1]

なんとなくデータ構造がわかったところで、今度は自分の意思でデータを登録してみましょう。さっきみたいに用意されたものではなくて、自分の作ったものをPOSTしてみましょう。私は助手(gptくん)に造らせたものを登録してみます。

POST /my-index/_doc
{
  "title": "Elasticsearch入門",
  "author": "たろう",
  "year": 2024
}

注目すべきはさっきと構造が違うところですね。idもないですし、同じものはtitleくらいです。これを同じインデックスに投入してみました。これでも問題なく登録できました。ドキュメント指向データベースの強みが生かされてますね。スキーマが柔軟です。

ただ闇雲に作っていいわけがないので、ElasticsearchにはMappingという概念があります。

マッピングとは、文書とそれに含まれるフィールドがどのように格納され、インデックス化されるかを定義するプロセスである。[4]

公式ドキュメントのMappingの定義をdeeplくんに訳してもらいました。ただここで私がつまずいたポイントとして

インデックス化...?インデックスって入れ物では?

と思いましたが、インデックス化とインデックスはどうやら使われ方が違うっぽく、インデックス化=検索用にデータを変換・格納する処理くらいの意味にとらえると良いようです。

ともあれMappingという概念があり、それによってデータ型や検索方法などが決められるようです。ですが、ここ踏み込みすぎるとざっくり理解するというコンセプトを無視しまうので、MappingにはExplicit MappingDynamic Mapping[4:1]の2種類があるよ。くらいに留めておこうと思います。前者が手動で作るMappingで後者が勝手に作られるMappingです。実運用で使うのであれば、Explicit Mappingを使うことにおそらくなるでしょうね。

検索の基本をさっくり学ぶ

なんとなーく、データ構造がわかったところで検索の基本を見ていきましょう。
よく使うクエリ構文をざっくり体験することにしましょう。

主なクエリ構文はこの4つです。

  • match
  • term
  • range
  • bool

事前準備

助手にデータを作ってもらいました。こちらをつっこんで、検索の様子をみましょう。

データ
POST /books/_doc/1
{
  "title": "Elasticsearch入門",
  "author": "たろう",
  "price": 2500,
  "category": "技術"
}

POST /books/_doc/2
{
  "title": "やさしい検索エンジン",
  "author": "はなこ",
  "price": 1800,
  "category": "技術"
}

POST /books/_doc/3
{
  "title": "週末のレシピを検索する",
  "author": "さちこ",
  "price": 1200,
  "category": "料理"
}

match

一番使うことになるだろうmatchですね。全文検索をしたいときに使います。

GET /books/_search
{
  "query": {
    "match": {
      "title": "検索"
    }
  }
}

簡単にこうやって書くこともできるそうですが、あんまり推奨はされてなさそうです。

GET /books/_search?q=title:検索
結果
{
  "took": 2,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 2,
      "relation": "eq"
    },
    "max_score": 0.9011997,
    "hits": [
      {
        "_index": "books",
        "_id": "2",
        "_score": 0.9011997,
        "_source": {
          "title": "やさしい検索エンジン",
          "author": "はなこ",
          "price": 1800,
          "category": "技術"
        }
      },
      {
        "_index": "books",
        "_id": "3",
        "_score": 0.8018838,
        "_source": {
          "title": "週末のレシピを検索する",
          "author": "さちこ",
          "price": 1200,
          "category": "料理"
        }
      }
    ]
  }
}

term

termは文字を完全一致させたいときに使うようです。
ただ注意点としてkeyword型でないと完全一致はできないようです。Mappingが関わることなので、今回は説明を見送りますが、何やら登録する要素には色々型があって、それによってできることや得意なことが変わるくらいの意識を持っておけば良いと思います。

適切にMappingしてあればcategory.keywordkeywordは必要ないようですが、今回は特に設定していないので、つけてあげないと完全一致できないみたいです。興味があれば、ぜひ調べてみてください。

GET /books/_search
{
  "query": {
    "term": {
      "category.keyword": {
        "value": "技術"
      }
    }
  }
}
結果
{
  "took": 2,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 2,
      "relation": "eq"
    },
    "max_score": 0.4700036,
    "hits": [
      {
        "_index": "books",
        "_id": "1",
        "_score": 0.4700036,
        "_source": {
          "title": "Elasticsearch入門",
          "author": "たろう",
          "price": 2500,
          "category": "技術"
        }
      },
      {
        "_index": "books",
        "_id": "2",
        "_score": 0.4700036,
        "_source": {
          "title": "やさしい検索エンジン",
          "author": "はなこ",
          "price": 1800,
          "category": "技術"
        }
      }
    ]
  }
}

range

数値や日付の範囲検索ができます。gtegreater than or equalです。gtlt,lteなんかもあります。

GET /books/_search
{
  "query": {
    "range": {
      "price": {
        "gte": 2000
      }
    }
  }
}

結果
{
  "took": 6,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 1,
    "hits": [
      {
        "_index": "books",
        "_id": "1",
        "_score": 1,
        "_source": {
          "title": "Elasticsearch入門",
          "author": "たろう",
          "price": 2500,
          "category": "技術"
        }
      }
    ]
  }
}

bool

これは複数条件を組み合わせられます。

GET /books/_search
{
  "query": {
    "bool": {
      "must": [
        { "term": { "category.keyword": "技術" }},
        { "range": { "price": { "gte": 2000 }}}
      ]
    }
  }
}
結果
{
  "took": 10,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 1.4700036,
    "hits": [
      {
        "_index": "books",
        "_id": "1",
        "_score": 1.4700036,
        "_source": {
          "title": "Elasticsearch入門",
          "author": "たろう",
          "price": 2500,
          "category": "技術"
        }
      }
    ]
  }
}

主要なものを紹介しましたが、他にもたくさんクエリあるので、興味があれば覗いてみてください。[5]

集計の基本をさっくりと

Elasticsearchの魅力の一つに集計ができることが挙げられます。これを細かくあげても公式ドキュメントの書き写しみたいになりますし、何より上の検索の基本の部分が具体例の列挙みたいになって読んでて面白くなかったので、ここは本当にさっくりと一個だけ例を紹介して、こんな感じかぁ...と雰囲気を感じてもらうだけにします。

どうせなので、前章でやった検索と組み合わせてカテゴリが技術の本だけを対象にした平均価格を出してみましょう。

GET /books/_search
{
  "query": {
    "term": {
      "category.keyword": "技術"
    }
  },
  "aggs": {
    "avg_price": {
      "avg": {
        "field": "price"
      }
    }
  },
  "size": 0
}

想定通り、(1800 + 2500) / 2 = 2150なので意図した値が出ていますね。こういった集計が柔軟にできるところがElasticsearchの強みの一つです。

{
  "took": 3,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 2,
      "relation": "eq"
    },
    "max_score": null,
    "hits": []
  },
  "aggregations": {
    "avg_price": {
      "value": 2150
    }
  }
}

もちろんRDBでも頑張ればできないこともない集計ですし、Elasticsearchでしかできない集計は厳密には存在しないようです。(これは真偽不明)ただRDBでは現実的ではない集計が、Elasticsearchなら現実的にできるということはあるようです。

例えばネストされた構造に対する集計などが挙げられます。RDBだとJOINしてGROUP BYしてHAVINGして...みたいな感じになりがちですが、Elastisearchでは比較的さっくりとできるようです。この部分に関しては、ちょっと調査が足りていないので、曖昧です。また別の機会に一つの記事として出しても良いかもしれません。

フロントエンドからElasticsearchを使ってみよう

イメージつけるために、フロントエンドから使ってみましょう。やること多くてめんどくさいので、そんなに作り込まずに全体の流れをサラッとみて、こういう感じで実装するんだろうなと思って終わりにしたいと思います。

本当は動画を貼り付けようと思ったのですが、gifにしないといけなくて面倒なので、画像を貼り付けます。

作成したフロントエンドの画像
検索結果の画像

正直こんなもの見せられたところで...という感じでしょうが、お許しください。

このフロント実装に関しては、自分が理解したいために作った章で、読者のために書いた章ではないです。ごめんなさい。いや!あたしも理解したい!!!という方は

https://github.com/atuya0726/elastic-search-lab

こちらに用意してあるので、cloneして色々弄ってみてください。

構成

構成としてはフロントエンドがあって、バックエンドがあって、バックエンドからElasticsearchを叩くという感じになりそうです。

本来のシステムならRDBもあって、Kibanaも導入するんですかね?ちょっとKibanaでできることを全て把握しきれているわけではないので、実務で使われるかは謎です。またRDBに追加されたデータをElasticsearchに追加する必要があるので、非同期に同期するメッセージキューを使うかもしれません。(これは想像です)それに速さが必要になったり、大規模なサービスならキャッシュサーバーも欲しいと思うので、大袈裟な構成図はこんな感じになるんですかね?だいぶ妄想や推測に基づくものなのであっているかは分かりません。

AWSに載せるとするならば、キャッシュサーバーとメッセージングキューとElasticsearchとRDBがマネージドのサービスを使えそうですね。それぞれElasticache,SQS,Opensearch,RDSって感じですかね。バックエンド、フロントはECSに乗っける感じですかね。フロントに関してはS3に乗っける可能性もありそうです。

そんな感じで助手に一日10000件の小規模サービス試算してもらったら、以下だそうです。Opensearchが高いですね。値段的な意味ではElasticsearchのような検索エンジンを導入するのはそこそこ必要に迫られないと使わなそうですね。

🟢 最小構成(節約重視):$150〜200 / 月
🟡 中間構成(現実的なスケール対応):$250〜350 / 月
🔴 フルマネージド&冗長構成(商用本番向け):$400〜600 / 月

終わりに

この記事ではElasticsearchを使ったシステム構成の「ざっくり像」を理解することを目的としました。どうでしたでしょうか?なんとなくわかったわ!くらいの理解をしていただければ私としては幸いです。

おさらい

Elasticsearchは、高い検索性能と柔軟なクエリ構造を持ち、RDBとは異なる強みを持つデータストアでございます。単に全文検索ができるだけでなく、複雑なフィルターや集計、ログ可視化、ランキング表示といった用途にも強みを発揮します。

次のステップ

さっくり理解していただけと思うので、きっちり理解したい人は

  • Mappingについてちゃんと学ぶ
  • シャーディングの概念もあるので、その辺り
  • セキュリティ
  • ログとか

この辺りが必要な知識になると思います。というか私が足りてないなと思ったところです。なので参考程度に。

脚注
  1. https://www.elastic.co/guide/en/elasticsearch/reference/current/elasticsearch-intro-what-is-es.html ↩︎

  2. https://ja.wikipedia.org/wiki/全文検索 ↩︎

  3. https://www.elastic.co/blog/found-elasticsearch-as-nosql ↩︎ ↩︎

  4. https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html ↩︎ ↩︎

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

Discussion