LiBz Tech Blog

LiBの開発者ブログ

AWSで怯えず作るEC2 + Amazon Elasticsearch Serviceでハマったところ

tl;dr

  • Amazon Elasticsearch Serviceの導入でハマったところと解決をまとめる
  • ハロウィンに投稿する予定だったのに腹痛で帰宅して出し損ねる
    • でも記事は書き直さない
  • なにかツッコミあればコメントお願いします

簡単に自己紹介

はじめまして。LiBでサーバーサイドエンジニアとして働いている外丸(とまる)です。31歳独身です。

趣味はロードバイクと読書です。土日の過ごし方は、最近は起業とマーケティングの本を読むのにハマってます。

怯えながら記事を書きます

弊社は渋谷にあるのですが、ここ渋谷はまさにハロウィン真っ只中です。

先日は軽トラックがひっくり返され、今日はビル火事です。

ゾンビ達がオフィスを襲撃するんじゃないかと、実は怯えながらいま記事を書いています。 怖いのでサクッと書いて帰ろうと思います。

かゆ…うま…

すかさず技術的な話

今回は弊社が提供している転職サービス「LiBzCAREER(以下LC)」へ Elasticsearchを導入したお話です。LC本番環境はAWS上に構築されていてRailsで実装しています。

なぜやったのか

LCではレジュメ検索や求人検索など検索機能をいくつか提供しています。

元々RailsからActiveRecord経由でMySQLに正規表現を投げていました。 ですがこれ、というかご想像の通り、テキスト検索が 遅かった のです。

大学名やプログラミング言語名など、テキスト検索では様々なリストが投げられる想定ですが、例えば10単語前後投げられた場合、その他色々な条件によっては元々の構成で10s~20sかかってました。msではなくsです。これはマズい。

置き換え先を検討

置き換え先の候補はこんな感じです。

  • Amazon CloudSearch
  • Elacticsearch( on EC2)
  • Amazon Elasticsearch Service

目指したいことは「テキスト検索の高速化」と「運用時コストが少ない」の2点でした。

後者の理由で、自前でElasticsearchをEC2上に構成するのは早々に諦めました。起動スクリプトやTerraformなど書くコストも高そうだったし(よくわかってない)。やっぱりマネージドが良い。

残った2択から消去法でAmazon Elasticsearch Serviceを採用した。これはデータ構造の制約から決めました。

  • 1対多構造を含む
  • Parent-Child構造を含む

いざ導入

先人が導入まわりの書物をすでに残してくれてるので大枠は省きます。ググればだいたい解決できる(はず)。

ただ「なんで記事書いた?」と偉い人から怒られるので、比較的新しめな下記環境でハマったこととその解決について書きます。

  • Elasticsearch 6.1.1
  • elasticsearch-rails 6.0.0
  • elasticsearch-model 6.0.0
  • elasticsearch-extensions 0.0.27
  • Rails 5.0.5

1. 親子関係

ググると Parent-Child type を使うと幸せになれるよ とちょこちょこ出てきますがこれは公式で非推奨でした。

Mapping types will be completely removed in Elasticsearch 7.0.0.

7.0.0でtypeの削除が決定しています。また、Elasticsearch6系以上ではindexあたりに作成できるtypeは1つとなっています。知らずに実装して辛い思いをしました。

ということで逃げ方

join data typenested data type を使う

弊社では nested data type を使って実装しました。 あとから知りましたが、nested data typeを使ってindex した場合nest されたdocument は内部的に別のdocumentとしてindex されるため、データサイズに懸念がありそうです。

2. チューニング

リリース後に修正コストがかかりそうなものを優先してチューニングした。あとで難しい球を打たなくて済むよう早め早めに打つ。

query よりfilter を使う

検索条件の設定はquery やfilter を使いますが、スコア計算が不要な場合、積極的にfilter を使用するのがよいようです。スコア計算にかかるコストが省かれるためらしい。

なるべくbool 条件を使う

filter のcacheの仕組みから基本的にbool 条件で比較したほうが早いようです。 ただし、Geo, Script or Numeric_range filter を使う場合は And/Or/Not を条件に使用したほうが例外的に早いです。

※ 古い記事なのでいまもこの仕様なのかは怪しい。

All About Elasticsearch Filter BitSets | Elastic

_source を使わない

データサイズ縮小のため、highlightなど使わない場合はfalse にしたほうが良いようです。

3. クラスター状態

これ最初知らずに設定やindex 周りを疑ってました。。実はデフォルト(ノードクラスタ数1)だと確実に黄色なんですね。プライマリは割り当てられているが、レプリカを割り当てる先がない場合におきてしまう。

4. Elasticsearchの非同期実行

after_commitのタイミングで実行。

after_commit ->(record) { ElasticsearchWorker.perform_async('index', User.__elasticsearch__.field(record), User.__elasticsearch__.index_name) }, on: [:create, :update]
after_commit ->(record) { ElasticsearchWorker.perform_async('delete', User.__elasticsearch__.field(record), User.__elasticsearch__.index_name) }, on: :destroy

sidekiq で更新。

Elasticsearch::Model.client.index index: index_name, type: 'user', id: record.id, body: record.__elasticsearch__.as_indexed_json

これでほぼほぼうまくいきました。

でもうまくいったと思ったら

当然といえばそうなんですが。。

after_commit はActiveRecord のコールバックなので、ActiveRecord を介さないDB更新が走っても Elasticsearch の同期は走りません

たとえば、このようなActiveRecord のカラムに直接代入するような更新をした場合、直接MySQL のupdate が走り、after_commit は呼ばれません。

@active_record_user.name = args[:name]

以上でおしまい

ハマったところのまとめでした。

ちょいちょいハマりながらもelasticsearchを導入できて一安心。 遅かったレジュメ検索も数倍早くなり、お客様へ価値をすばやく提供できる状態になったと思っています。よかったよかった。

おつかれさまでした。さっさと帰ります。

最後に

弊社では毎月パーティーを行っています。 ゾンビは襲撃してきませんでしたが、今月はかぼちゃとスパイダーマンと、エイリアンが一人誘拐して来てくれました。

新しいエンジニア仲間なのですが、増えて嬉しいです。

彼らも今後記事を書いてくれるはずなので、お楽しみに!

f:id:kazunosuket:20181101212555j:plain