🔰

公式ドキュメントをみながら、初心者なりにElasticsearchハンズオン!

2024/12/05に公開

みなさんこんにちは!株式会社スペースマーケットでエンジニアをしているrokioです☺️

弊社では、15分単位でスペースを貸したい方とそのスペースを使いたい方をマッチングするサービスを提供しています。2024年12月現在36,000超のスペースを掲載しており、それを素早く検索するためにElasticsearchを導入しています。

約2ヶ月前にこの会社にWebエンジニアとしてJoinしたのですが、Elasticsearchは名前だけは聞いたことがあるものの、実際に触ったことがなく😅、メンバーの方々に教えてもらいながらキャッチアップし少しずつElasticsearch関連のタスクができるようになってきました!

今回は、公式のドキュメントを参照しながら0からElasticsearchを触ってみます。
そこで得られた、ボトムアップな情報を臨場感たっぷりに記事にしましたので、ポップコーン片手にご覧ください🍟

ElasticSearchって何👀

https://www.elastic.co/jp/elasticsearch

Elasticsearchはオープンソースの分散型RESTful検索・分析エンジン、スケーラブルなデータストア、ユースケースの急増に対処可能なベクトルデータベースの役割を兼務します。データを一元的に格納することで、超高速検索や、関連性の細かな調整、パワフルな分析が大規模に、手軽に実行可能になります。Elastic Stackの心臓部となるプロダクトです。

  • RESTfulなインターフェイスをもっている
  • スケーラブルなデータストアである
  • ベクトルデータベースとして使える

あたりが読み取れます。さらに理解するために早速触ってみましょう!

ローカル環境構築

今回は使い捨てできるように、Docker上にElasticsearch環境を構築したいと思います。公式ブログでやり方が紹介されているのでこちらを参照して進めます💪

https://www.elastic.co/jp/blog/getting-started-with-the-elastic-stack-and-docker-compose

↑の記事ではElasticsearchの他にKibanaやMetricbeatなど、他のサービスのコンテナを作っています。特にKibanaは様々なデータのビジュアライズを担っているようです。
https://www.elastic.co/kibana
一度にたくさん追うのは大変なので、今回はElasticsearchとKibanaに絞って学習を進めることにします。

この記事の通りに丁寧に進めるのが良さそうですが、完成形をGithubにあげてもらっているので有り難く使わせていただきます🙏
https://github.com/elkninja/elastic-stack-docker-part-one

git cloneして、docker compose upしましょう!
※業務でもElasticsearchのDockerコンテナを立ち上げていたので、デフォルトの9200ポートが競合していました。その場合は一旦使っているコンテナをstopしておきましょう。

http://localhost:5601でKibanaにアクセスできます。
cloneしたままの設定だと、Usernameはkibana_systemでPasswordはchangemeですね!

...と思ったらあれ?

ダイアログのメッセージでググったら、こんなページが出てきました
https://discuss.elastic.co/t/you-do-not-have-permission-to-access-the-requested-page-after-elk-docker-compose-tutorial/297171

Usernameはelasticが正解ですね。。。
.envdocker-compose.ymlで設定しているkibana_systemはブラウザでKibanaにログインするためユーザーではなく、Kibana内部で使っているユーザーみたいでした。。。

改めて、UsernameはelasticでPasswordはchangemeですね!
※ブログをちゃんと読んだら画像にそう書いてありました(急がば回れ)

データを入れて検索してみる

今度はこちらのQuick Startを見ながら、進めていきます!
https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started.html

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インデックスに「ドキュメント」を追加しました。
ここまでで、「インデックス」と「ドキュメント」という単語が出てきましたが、一旦整理しておきます。
https://www.elastic.co/guide/en/elasticsearch/reference/current/documents-indices.html

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: マッピングとデータ型を定義する

マッピングという新しい概念が出てきました。ドキュメントを読んでみましょう。
https://www.elastic.co/guide/en/elasticsearch/reference/current/documents-indices.html#elasticsearch-intro-documents-fields-mappings

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のキーを持っているドキュメントを削除するとどうなるのでしょうか?

単一のドキュメントを削除するには

https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete.html

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

dynamicfalseになっていますね。これをtrueにするとDynamic mappingの挙動になるようです。propertiesにキーと型を入れていますね。
ちなみに型のバリエーションは以下に書いてありました↓
https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html

さて、ここでマッピングにない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"に関しては以下に詳細が書いてありそうです。(膨大。。。)
https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html

前節で作った、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"
        }
      }
    }
  }
}

注意したいのはauthorlanguagenameの型です。
これは基本は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が使えて、
https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html

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が使えます。
https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html

GET books/_search
{
  "query": {
    "range": {
      "release_date": {
        "gte": "2000-01-01", 
        "lte": "2023-05-31" 
      }
    }
  }
}

それはそうですね👍(スン)

おわりに

公式ドキュメントをみながら、Elasticsearchの入口あたりをいろいろ触ってみました!
不明瞭なところもありますが、これをベースに学習を進める方がいらっしゃることを願います🙏

スペースマーケットでは経験が薄い技術にチャレンジできる機会がたくさんあります!
気になる方は下のリンクをチェックしてみてください!

スペースマーケット Engineer Blog

Discussion