OpenSearch調査

概要

OpenSearch(ElasticSearch)について調査して、知識を深める

OpenSearchって?

AWS と Elastic社が争って、AWSで提供するElasticSearchのサービスは、OpenSearchという名前になってる。

codezine.jp

Elasticsearch 7.10.2 から派生したので、ほとんどElasticSearchと一緒のことできる。

SQLが使える SQL を用いた Amazon OpenSearch Service のデータのクエリを行う - Amazon OpenSearch Service

Localで試してみたい

この記事通りにすると簡単に試せる!

OpenSearchをローカル環境でDockerを利用して構築する | DevelopersIO

ElasticSearchって?

こちらの記事読めばOK!

Elasticsearch入門 の記事一覧 | DevelopersIO

ちなみに、Elastic社が提供してるElasticStackというサービスの組み合わせがあって、これ使うとログ分析が捗るらしい(試したことしかない)

Elastic Stack = Elasticsearch、Kibana、Beats、Logstash | Elastic

競合は?

Algolia, Solr, Groonga など

どういう時に使う?

ElasticSearchは全文検索エンジンであるので、文字を検索したいときに使う。

N-gram形態素解析という二つの方法が利用できる。詳しい説明は下のサイトがわかりやすかった。

第6回 N-gramと形態素解析との比較 | gihyo.jp

ちなみに、MySQLPostgreSQLn-gramは既にできる。日本語の形態素解析Mecabを使えばできるっぽい。

なので、既にRDBMSを使ってる場合は、まずはRDBMSでやり切る方が先そう。

一方で、データが増えてきたり、スコアリングとか、ベクトル検索みたいな機能を使いたい時は良さそう?(RDBMSでもできるのかもしれないけど、知識がなくてわからない)

Elasticsearchの機能一覧 | Elastic

ベクトル検索だと、この記事がすごく勉強になる。ベクトルを保存して、類似度を検索できる。

Elasticsearchを用いて類似度ベクトル検索をやってみてわかったこと - ログミーTech

スコアリングは使ったことがあって、検索文字とどれだけ一致してるかを返してくれる。

function_scoreっていうのを使って、(PV数 x 1 + お気に入り数 x 10 + 一致度) でソートするみたいな順でデータを取得できたりできる。

RDBMSでなく、DynamoDBを永続化層と使っている場合は、ElasticSearchを検索に使う方法は結構記事が出てくるから一般的っぽい。

DynamoDBとElasticsearchをエンプラで本気で使ってみた2018年を非機能面で振り返る - Qiita

実際にIndexを作ってみよう!

Indexがテーブル設計。indexにmappingを設定するのだが、これが非常に重要となる。

とりあえずやってみる

Product {
  id: number
  manufactureId: number
  name: string
  description: string
}

Manufacture {
  id: number
  name: string
}

Event {
  eventMasterId: number
  userId: number
  timestamp: datetime
}

ざっくりやるとこんな感じ?

PUT /products
{
  "mappings": {
    "properties": {
      "productId": {
        "type": "integer"
      },
      // nameは完全一致も、部分一致でも検索したいので、text型とkeyword型の両方を作成する。
      "name": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }
      },
      // descriptionはkuromojiで日本語での形態素解析での検索を可能に
      "description": {
        "type": "text",
        "analyzer": "kuromoji"
      },
      "manufacture": {
        "properties": {
          "manufactureId": {
            "type": "integer"
          },
          // 企業名は完全一致での検索しかされないと判断して、keyword型のみ利用
          "name": {
            "type": "keyword"
          }
        }
      },
      // event情報も、productsのindexに紐づけることで、プロダクト検索を容易にする。
      // nestedを使うことで、それぞれの子要素に対する検索を可能にする
      "events": {
        "type": "nested",
        "properties": {
          "eventMasterId": {
            "type": "integer"
          },
          "userId": {
            "type": "integer"
          },
          "timestamp": {
            "type": "date"
          }
        }
      }
    }
  }
}

こんな感じで検索できる

GET /products/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "name.keyword": "商品A"
          }
        },
        {
          "nested": {
            "path": "events",
            "query": {
              "bool": {
                "filter": [
                  {
                    "term": {
                      "events.eventMasterId": "1"
                    }
                  }
                ]
              }
            }
          }
        }
      ]
    }
  }
}

懸念点

OpenSearchのDocumentには見つからなかったが、ElasticSearchのDocumentには、nestedの中のObjectが10000を上限とするように推奨されている。

なので、10000過ぎたらtimestampが古いものから削除するとかしても良いが、上手く検索できない。

パターン1

イベント定義ID毎に、最終イベント発生日と、イベントを発生したUserIdの一覧を登録するようにする。

これで、例えば直近でイベントAが発生した商品検索はできるし、ユーザーAがイベントAを発生した商品検索もできる。

だが、これだと指定の日時でイベントを発生した商品の検索ができない。

{
  "events": {
    "type": "nested",
    "properties": {
      "eventMasterId": {
        "type": "integer"
      },
      "lastEventedAt": {
        "type": "date"
      },
      "userIds": {
        "type": "keyword"
      }
    }
  }
}

パターン2

日付毎に、発生イベントの配列と、イベントを発生したユーザーの一覧を持たせる。

一定の期間毎にデータは削除していく。

これで、大体の検索はできそう。だが、指定期間にX回イベントが発生したかという検索はできない。

{
  "events": {
    "type": "nested",
    "properties": {
      "date": {
        "type": "date"
      },
      "eventMasterIds": {
        "type": "keyword"
      },
      "userIds": {
        "type": "keyword"
      }
    }
  }
}

パターン3

日付でgroupして、もう一回nestedにするようにしてみた。

おそらく、イベント発生を検索条件に入れる場合は、先に日付で絞り込みするだろうから、絞り込んだ後に、回数で検索できる。

思いつく感じはこれで網羅できたが、本当にこれでいけるのだろうか...

あと、そもそもイベントの更新があるたびにupdateするのは、非効率なので1日1回の更新とかになりそうだが、大丈夫?

{
  "events": {
    "type": "nested",
    "properties": {
      "date": {
        "type": "date"
      },
      "occurs": {
        "type": "nested",
        "properties": {
          "eventMasterId": {
            "type": "integer"
          },
          "count": {
            "type": "integer"
          },
          "userIds": {
            "type": "keyword"
          }
        }
      }
    }
  }
}

パターン4

新しくeventだけのindexを作っちゃって、Joinをする。

Join field type | Elasticsearch Guide [8.5] | Elastic

これが一番簡単そうだし自然だが、安易にリレーションを使うのはパフォーマンスの問題があるとのこと。

これだけならおそらく大丈夫そうだが、パフォーマンステストはしたほうが良さそう。

リレーショナルモデルを複製するために、複数レベルのリレーションを使用することはお勧めしません。各レベルのリレーションは、クエリ時にメモリと計算のオーバーヘッドを増加させる。検索性能を上げるには、データを非正規化するのがよいでしょう。

まとめ

色々調べたけど、実際にどんなmappingを作るのがベストか分からない...

更新頻度を考えつつ、どういう検索をしたいのかというのをちゃんと考えてから作る必要がありそう。

RedshiftなどのDWHとの使い分け

ElasticSearchは、リアルタイムに近い検索や全文検索が可能ですが、Redshiftほどに大量の時系列データを扱うことは厳しい。億単位のデータを瞬時に返そうとすると流石に勝てないやってなっちゃうっぽい。

一方で、RedshiftはInsertやUpdateが得意じゃない(ちなみにBigQueryはUpdateができない)ので、リアルタイムな更新が求められる場合は辛い。一方で、億単位のデータでも簡単にレスポンスが数秒で返ってくる。