公式ドキュメントをみながら、初心者なりにElasticsearchハンズオン!
みなさんこんにちは!株式会社スペースマーケットでエンジニアをしているrokioです☺️
弊社では、15分単位でスペースを貸したい方とそのスペースを使いたい方をマッチングするサービスを提供しています。2024年12月現在36,000超のスペースを掲載しており、それを素早く検索するためにElasticsearchを導入しています。
約2ヶ月前にこの会社にWebエンジニアとしてJoinしたのですが、Elasticsearchは名前だけは聞いたことがあるものの、実際に触ったことがなく😅、メンバーの方々に教えてもらいながらキャッチアップし少しずつElasticsearch関連のタスクができるようになってきました!
今回は、公式のドキュメントを参照しながら0からElasticsearchを触ってみます。
そこで得られた、ボトムアップな情報を臨場感たっぷりに記事にしましたので、ポップコーン片手にご覧ください🍟
ElasticSearchって何👀
Elasticsearchはオープンソースの分散型RESTful検索・分析エンジン、スケーラブルなデータストア、ユースケースの急増に対処可能なベクトルデータベースの役割を兼務します。データを一元的に格納することで、超高速検索や、関連性の細かな調整、パワフルな分析が大規模に、手軽に実行可能になります。Elastic Stackの心臓部となるプロダクトです。
- RESTfulなインターフェイスをもっている
- スケーラブルなデータストアである
- ベクトルデータベースとして使える
あたりが読み取れます。さらに理解するために早速触ってみましょう!
ローカル環境構築
今回は使い捨てできるように、Docker上にElasticsearch環境を構築したいと思います。公式ブログでやり方が紹介されているのでこちらを参照して進めます💪
↑の記事ではElasticsearchの他にKibanaやMetricbeatなど、他のサービスのコンテナを作っています。特にKibanaは様々なデータのビジュアライズを担っているようです。
一度にたくさん追うのは大変なので、今回はElasticsearchとKibanaに絞って学習を進めることにします。この記事の通りに丁寧に進めるのが良さそうですが、完成形をGithubにあげてもらっているので有り難く使わせていただきます🙏
git clone
して、docker compose up
しましょう!
※業務でもElasticsearchのDockerコンテナを立ち上げていたので、デフォルトの9200ポートが競合していました。その場合は一旦使っているコンテナをstopしておきましょう。
http://localhost:5601
でKibanaにアクセスできます。
cloneしたままの設定だと、Usernameはkibana_system
でPasswordはchangeme
ですね!
...と思ったらあれ?
ダイアログのメッセージでググったら、こんなページが出てきました
Usernameはelastic
が正解ですね。。。
.env
やdocker-compose.yml
で設定しているkibana_system
はブラウザでKibanaにログインするためユーザーではなく、Kibana内部で使っているユーザーみたいでした。。。
改めて、Usernameはelastic
でPasswordはchangeme
ですね!
※ブログをちゃんと読んだら画像にそう書いてありました(急がば回れ)
データを入れて検索してみる
今度はこちらのQuick Startを見ながら、進めていきます!
Elasticsearchのコンテナにcurlなどでリクエストすればデータ操作できるようですが、せっかくなのでKibanaでやってみます☺️
Kibanaにログインし、左上のハンバーガーアイコンをクリック、メニュー下部のDev Tools
をクリックしてコンソールを開きましょう!
Step1: インデックスを作る
PUT /books
と入力して、「再生」みたいなボタンをクリック or MacならCommand+Enterするとクエリが実行されます。
こうするとbooks
という「インデックス」が作られるようです。インデックス?🤔となりますが先に進みましょう。
(クエリがHTTPリクエストに似ていますね!)
Step2: インデックスにデータを入れる
単一のドキュメントを入れる
POST books/_doc
{
"name": "Snow Crash",
"author": "Neal Stephenson",
"release_date": "1992-06-01",
"page_count": 470
}
複数のドキュメントを一気に入れる
POST /_bulk
{ "index" : { "_index" : "books" } }
{"name": "Revelation Space", "author": "Alastair Reynolds", "release_date": "2000-03-15", "page_count": 585}
{ "index" : { "_index" : "books" } }
{"name": "1984", "author": "George Orwell", "release_date": "1985-06-01", "page_count": 328}
{ "index" : { "_index" : "books" } }
{"name": "Fahrenheit 451", "author": "Ray Bradbury", "release_date": "1953-10-15", "page_count": 227}
{ "index" : { "_index" : "books" } }
{"name": "Brave New World", "author": "Aldous Huxley", "release_date": "1932-06-01", "page_count": 268}
{ "index" : { "_index" : "books" } }
{"name": "The Handmaids Tale", "author": "Margaret Atwood", "release_date": "1985-06-01", "page_count": 311}
Step1で作ったbooks
インデックスに「ドキュメント」を追加しました。
ここまでで、「インデックス」と「ドキュメント」という単語が出てきましたが、一旦整理しておきます。
The index is the fundamental unit of storage in Elasticsearch...
An index is a collection of documents...
A document is a set of fields, which are key-value pairs...
- インデックスはドキュメントの集合
- ドキュメントはフィールド(キーと値のペア)の集合
とあります。ここまでで、books
インデックスにドキュメントを追加していましたが、これを実世界に当てはめると、インデックスは本棚でドキュメントは本といえそうです。
Step3: マッピングとデータ型を定義する
マッピングという新しい概念が出てきました。ドキュメントを読んでみましょう。
A mapping defines the data type for each field,...
マッピングはドキュメントのフィールドの型を定義するもののようです。
そしてマッピングの作り方には2種類あり、
- Dynamic mapping
- Elasticsearchが自動的に作ってくれる。楽だが、特定のユースケースで最適でない結果になるかも。
- Explicit mapping
- あらかじめ手動で定義しておく。インデックス化を完全にコントロールできるので本番運用はこちらがおすすめ。
みたいです。ここで書いた「インデックス化」の"インデックス"とドキュメントの集合である"インデックス"は多分別物です🤔
では実際に動かしてみましょう。
Dynamic mapping
POST /books/_doc
{
"name": "The Great Gatsby",
"author": "F. Scott Fitzgerald",
"release_date": "1925-04-10",
"page_count": 180,
"language": "EN"
}
これまでと違い、language
というキーが増えたドキュメントを追加します。
GET /books/_mapping
このクエリでインデックスのマッピングが確認できるようです。
リクエストの結果は以下の通りです↓
{
"books": {
"mappings": {
"properties": {
"author": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"language": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"name": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"page_count": {
"type": "long"
},
"release_date": {
"type": "date"
}
}
}
}
}
properties
の中にこれまで入れたドキュメントのフィールドを網羅するキーが入っていますね。
ここで、Quick Startから道が逸れますが、実験をします!
まず、現在のマッピングで最初に追加したドキュメントを取得するとどうなるのでしょうか??
GET books/_search
これでインデックスにあるドキュメントを一覧できます。
そうすると...
(略)
"hits": [
{
"_index": "books",
"_id": "mgHocJMBsRl20jYVWLLQ",
"_score": 1,
"_source": {
"name": "Snow Crash",
"author": "Neal Stephenson",
"release_date": "1992-06-01",
"page_count": 470
}
},
{
"_index": "books",
"_id": "0gN-cZMBsRl20jYVNwtR",
"_score": 1,
"_source": {
"name": "The Great Gatsby",
"author": "F. Scott Fitzgerald",
"release_date": "1925-04-10",
"page_count": 180,
"language": "EN"
}
},(以下略)
language
のキーがあるドキュメントと、ないドキュメントが混在しています😅
language
がないドキュメントはせめてlanguage: null
みたいになっていてほしいですが、これだとアプリケーション側でElasticsearchを使うときにキーがあるかどうか疑わないといけないので考慮点が増えて辛いですね。。。
あともう一つ実験したいです!唯一language
のキーを持っているドキュメントを削除するとどうなるのでしょうか?
単一のドキュメントを削除するには
DELETE /<index>/_doc/<_id>
でよく、_idは上の結果から、0gN-cZMBsRl20jYVNwtR
ですね!
ドキュメントを消して、もう一度マッピングを見てみると...
"properties": {
"author": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"language": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"name": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"page_count": {
"type": "long"
},
"release_date": {
"type": "date"
}
}
language
が残っていますね。ここは自動で消えてくれないみたいです。
Explicit mapping
さて脱線しましたが、元に戻して...
以下のクエリでスキーマをあらかじめ定義したインデックスを作れるようです。
PUT /my-explicit-mappings-books
{
"mappings": {
"dynamic": false,
"properties": {
"name": { "type": "text" },
"author": { "type": "text" },
"release_date": { "type": "date", "format": "yyyy-MM-dd" },
"page_count": { "type": "integer" }
}
}
}
dynamic
がfalse
になっていますね。これをtrueにするとDynamic mappingの挙動になるようです。properties
にキーと型を入れていますね。
ちなみに型のバリエーションは以下に書いてありました↓
さて、ここでマッピングにないlanguage
のキーが入ったドキュメントを追加しようとするとどうなるでしょうか?
POST my-explicit-mappings-books/_doc
{
"name": "The Great Gatsby",
"author": "F. Scott Fitzgerald",
"release_date": "1925-04-10",
"page_count": 180,
"language": "EN"
}
結果は
{
"_index": "my-explicit-mappings-books",
"_id": "8gOhcZMBsRl20jYVw331",
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 2,
"_primary_term": 1
}
成功しました😇(エラーが起きてほしい...)
長くなるので省略しますが、今回追加したドキュメントを取得できますし、なんならlanguage
キーも含まれちゃってます💦
マッピングはRDBのスキーマ定義に似た概念だと思っていましたがそうではないみたいです。詳細な調査は今後の課題としたいです。
Step4: データを検索する
脱線したときに使いましたが、インデックスに含まれる全てのドキュメントを取得するクエリを再掲します↓
GET books/_search
絞り込みを行いたいときは上のクエリに条件を足すようです。
GET books/_search
{
"query": {
"match": {
"name": "brave"
}
}
}
結果は以下の通りです↓
{
"took": 0,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 1.3310189,
"hits": [
{
"_index": "books",
"_id": "nQOFcZMBsRl20jYVDiGG",
"_score": 1.3310189,
"_source": {
"name": "Brave New World",
"author": "Aldous Huxley",
"release_date": "1932-06-01",
"page_count": 268
}
}
]
}
}
name
に「brave」が入っているドキュメントがヒットしましたね!(大文字小文字の区別なしですね)
以上で、見ていたQuick Startは終わりになります!
Quick Startはもう1ページありますが、分量が多くなってしまうので、次節で、かいつまんで触ってみようと思います!
もうちょっと検索を深掘りをしてみる
"query"に関しては以下に詳細が書いてありそうです。(膨大。。。)
前節で作った、books
インデックスで色々実験したいと思います!
型に注目した実験を行うので、ここでbooks
のマッピングを再掲します🙋♂️
{
"books": {
"mappings": {
"properties": {
"author": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"language": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"name": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"page_count": {
"type": "long"
},
"release_date": {
"type": "date"
}
}
}
}
}
注意したいのはauthor
、language
、name
の型です。
これは基本はtext型ですが、例えばクエリで"author.keyword"と書くと、keyword型として検索できます。
keyword型で検索
keyword型に対してterm
というキーで検索してみます。
GET /books/_search
{
"query": {
"term": {
"name.keyword": {
"value": "Revelation Space"
}
}
}
}
このクエリだと、name
が"Revelation Space"のドキュメントがヒットしました。
GET /books/_search
{
"query": {
"term": {
"name.keyword": {
- "value": "Revelation Space"
+ "value": "Revelation"
}
}
}
}
とするとヒットしませんでした。完全一致していないとダメなようです。
text型で検索
text型に対してmatch
が使えて、
GET books/_search
{
"query": {
"match": {
"name": "Revelation"
}
}
}
このようにするといわゆる全文検索ができます。
ここで、業務を意識して日本語が含まれるドキュメントで試してみましょう!
POST books/_doc
{
"name": "吾輩は猫である",
"author": "夏目漱石",
"release_date": "1905-10-06",
"page_count": 500
}
POST books/_doc
{
"name": "文芸は男子一生の事業とするに足らざる乎",
"author": "夏目漱石",
"release_date": "1905-10-06",
"page_count": 500
}
※設定値は正確ではありません🙏
GET books/_search
{
"query": {
"match": {
"name": "吾輩は"
}
}
}
とすると
"hits": [
{
"_index": "books",
"_id": "oATqcZMBsRl20jYVMGg_",
"_score": 4.638386,
"_source": {
"name": "吾輩は猫である",
"author": "夏目漱石",
"release_date": "1905-10-06",
"page_count": 500
}
},
{
"_index": "books",
"_id": "1QTucZMBsRl20jYVPHVl",
"_score": 0.68677616,
"_source": {
"name": "文芸は男子一生の事業とするに足らざる乎",
"author": "夏目漱石",
"release_date": "1905-10-06",
"page_count": 500
}
}
]
となり、2つヒットしますが、注目したいのは_score
です。『吾輩は猫である』の方が_score
が高いです。クエリは「吾輩は」なので、より近い名前のドキュメントのスコアが高くなっていますね。
全文検索、すごそう(小並感🐤)
今までRDBしか触っておらず、文字列の検索でもLIKEくらいしか使っていなかったので面白いです!!!
面白いので色々試しました🙋♂️
以降結果を書いていきますが、色々と省略して書きます!
クエリの"match" | 『吾輩は猫である』の"_score" | 『文芸は男子一生の事業とするに足らざる乎』の"_score" |
---|---|---|
我が輩は | 2.6076863 | 0.5861554 |
る | 1.0870833 | 0.9704559 |
あ | 1.520603 | (ノーヒット) |
犬 | (ノーヒット) | (ノーヒット) |
- 「吾輩は」の変換ミスで「我が輩は」になったものは、後半の「輩は」でヒットしてスコアが上がっていますね。
- 「る」はどちらの本にも含まれている文字ですが、『吾輩は猫である』の方がスコアが若干高いです。
- 全く含まれない文字で検索するとヒットしないようです。
「る」のスコアが『吾輩は猫である』のほうが若干高い理由を説明できるようにな゛り゛た゛い゛...!
long型、date型で検索
範囲を指定するような検索はrange
が使えます。
GET books/_search
{
"query": {
"range": {
"release_date": {
"gte": "2000-01-01",
"lte": "2023-05-31"
}
}
}
}
それはそうですね👍(スン)
おわりに
公式ドキュメントをみながら、Elasticsearchの入口あたりをいろいろ触ってみました!
不明瞭なところもありますが、これをベースに学習を進める方がいらっしゃることを願います🙏
スペースマーケットでは経験が薄い技術にチャレンジできる機会がたくさんあります!
気になる方は下のリンクをチェックしてみてください!
スペースを簡単に貸し借りできるサービス「スペースマーケット」のエンジニアによる公式ブログです。 弊社採用技術スタックはこちら -> whatweuse.dev/company/spacemarket
Discussion