pairsの検索機能をElasticsearchにリプレースした話

elastic-logo-H-full color

pairsサーバーサイド担当の小島です。

先日インフラチームの松尾よりpairs 検索のElasticsearch導入についての記事があり、その中でも少し触れていたのですが、pairsの検索機能はたびたび速度の問題とそれにかかるコスト等がネックになっていました。

今回私からは、それらを改善するために、検索機能のElasticsearch導入に際してアプリケーションサイドで行ったことをお話したいと思います。

version1.5 から 2.3 へ

pairsではこれまでも検索の一部機能に限りElasticsearchを使用していました。対応当時はAWSのElasticsearch Service を使用していたのですが、当時まだ2系に対応していなかったこともあり、当初は1.5を使用していたので、まずは2.3へのアップデートから行いました。
※ 現在Elasticsearch Serviceは2.3に対応したようです。

※ データマイグレーションはデータ再作成することが前提であり、特に必要もなかったため、行っていません。

検索クエリの最適化

2系でfilteredクエリが非推奨となったので、これまで使用していた検索クエリを書き換えました。

before
curl -XGET "http://es.endpoint/index/user/_search" -d'
{
  "from": 0,
  "query": {
    "filtered": {
      "filter": {
        "bool": {
          "must": [
          {
            "term": {
              "gender": "f"
            }
          },

 ・ ・ ・ ・ ・ ・ ・ ・ ・  省略 ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・

          ]
        }
      }
    }
  }
}
after
curl -XGET "http://es.endpoint/index/user/_search" -d'
{
  "from": 0,
  "query": {
    "bool": {
      "must": [
      {
        "term": {
          "gender": "f"
        }
      },

 ・ ・ ・ ・ ・ ・ ・ ・ ・  省略 ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・

      ]
    }
  }
}

階層が浅くなりシンプルになりました。

Reindex API

2.3に移行して今のところ一番良かったと感じるのがReindex APIを使えるようになったことです。
本番・検証環境でのシャード数やスキーマの変更など、再度インデックスを一から作りなおす場合、以前は検索データ同期用のスクリプトを用意し、MySQLのデータをElasticsearchにbulkで送信する処理を実行させる必要があったのですが、以下のような問題がありました。

  • スクリプトの完了までに2時間程度の時間が必要。
  • 新旧切替時にデータの差分をできるかぎり少なくする事が望ましいので、スクリプトを複数並列で動かすことが必要。
  • 全てのデータが揃うまで、一度完了したスクリプトも再度実行し同期させ続けることが必要。
  • DB・サーバへの負荷なども考慮しなければならなかったため、そのスクリプト用にサーバーを1台用意したりと無駄なコストが発生。

Reindex APIに移行したことによるメリット

  • スクリプトで2時間かかっていたのが、2分以内でインデックス複製が可能に。データの差分も最小限に抑えられる。
  • Elasticsearchだけで解決できるので、同期時にDBが不要に。
  • 以下のようにAPIを叩くだけなので、スクリプトがシンプルに。
curl -XPUT "http://es.endpoint/_reindex" -d'
{"source":{"index":"user_old"},"dest":{"index":"user_new"}}'

逆にデメリットが有るとしたら、Elasticsearch側に負荷が多少かかることくらいでしょうか。

Index構成の変更

インデックスを男女に分ける

インデックスは負荷分散・高速化のため、性別で分けることにしました。

インデックス親子関係の解消

リレーションデータを別インデックスとして持っていましたが、親子関係にすると検索時など、微妙な挙動の変化があり、速度的にもあまり良くなかったので、今回インデックスを統合し、一つのフィールドに配列として持たせるように変更しました。

検索クエリ

検索クエリを秒間1000リクエスト送り、どれほどのパフォーマンスがでるか計測しつつチューニングを行いました。

取得件数

一度の検索で取得する件数ですが、pairsの検索の場合、一度にできる限り多くの数を取得する必要があったため、件数によってどれくらいの速度差があるかを検証しました。

取得件数 平均レスポンス速度 (ms)
1 21
10 29
100 51
1000 275
10000 1483

※ 秒間100リクエストでの平均
10件と100件で約2倍、100件と1000件で約5倍くらいの差がありました。

取得するフィールドを絞る

こちらは当たり前ですが、不要なフィールドは取得しない方がレスポンス速度は上がります。
ただ、フィールドを絞るようにした場合、高負荷環境だとcpuの使用率が4割増くらいになっていたので、そもそもドキュメントが大したサイズではないときは、何も指定せずそのまま取得したほうが良いケースもありそうです。

fieldを指定するのにもいくつかやり方があります。

field指定
# リクエスト
curl -XGET "http://es.endpoint/index/user/_search" -d'
{
  "fields": [
    "birthday",
    "first_name",
    "last_name"
  ],
  "query": {
    "term": {
      "_id": "1"
    }
  }
}'

# レスポンス
{
  "took": 26,
  "timed_out": false,
  "_shards": {
    "total": 7,
    "successful": 7,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 1,
    "hits": [
      {
        "_index": "index",
        "_type": "user",
        "_id": "1",
        "_score": 1,
        "fields": {
          "birthday": [
            "1964-08-07T00:00:00Z"
          ],
          "first_name": [
            "nobita"
          ],
          "last_name": [
            "nobi"
          ]
        }
      }
    ]
  }
}

それぞれ指定したフィールドを配列で返してくれます。

_source指定

_source を使うとドキュメントの構造をそのまま返してくれるので、例えばGolangだとjson.Unmarshalでそのまま構造体にいれられます。

# リクエスト
curl -XGET "http://localhost:9200/pairs_female/user/_search" -d'
{
  "_source": [
    "birthday",
    "first_name",
    "last_name"
  ],
  "query": {
    "term": {
      "_id": "1"
    }
  }
}'

# レスポンス
{
  "took": 11,
  "timed_out": false,
  "_shards": {
    "total": 7,
    "successful": 7,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 1,
    "hits": [
      {
        "_index": "index",
        "_type": "user",
        "_id": "1",
        "_score": 1,
        "_source": {
          "1964-08-07T00:00:00Z"
          "last_name": "nobi",
          "first_name": "nobita"
        }
      }
    ]
  }
}

ファンクションスコアクエリは思ったよりも早い

ファンクションスコアを一部機能で使用していたので、こちらも計測してみました。
はじめに懸念したのが、レスポンス速度が落ちることだったのですが、計測の結果、通常のクエリとそこまで違いはなく、場合によっては速いくらいでした。
もちろん条件によるのでしょうが、シンプルなものならばファンクションスコアを使用してElasticsearch側にロジックを寄せるのも良さそうです。

導入後

簡単に結果ですが、Elasticsearchの導入により、速度・コストともに改善することができました。
速度は、平均レスポンスタイムが 0.2秒程度、最大経過時間で比べると7秒近く改善がみられました。また、コストについては、これまで使用してきたサーバーの台数を半分以上減らすことができました。

おわりに

Elasticsearch導入時は社内に経験者が少なかったため、ほとんど手探り状態で進めていましたが、Elastic社からのサポートとElasticsearchそのものの性能のおかげでそれなりの結果を得ることができました。

まだ設定も簡単にしかいじっていないため、まだまだ改善の余地がありそうです。これからの更なる改善にご期待ください。

  • このエントリーをはてなブックマークに追加

エウレカでは、一緒に働いていただける方を絶賛募集中です。募集中の職種はこちらからご確認ください!皆様のエントリーをお待ちしております!

Recommend

更新ストレスをゼロに!pairsのデザイン業務効率化

第一回 Goもくもく会(ごもく会)を開催しました!