patorashのブログ

方向性はまだない

Elasticsearchへの保存を非同期ではなく同期で行う方法

環境情報

DBに保存したらElasticsearchも更新されたい

Railsプロジェクトで、データベースへの保存を行ったら同時にElasticsearchへの反映も行いたい、というケースが出てきました。

Elasticsearchの利用には、elasticsearch-railsを使っているのですが、データの反映は簡単でした。

# Elasticsearchへの保存が失敗してもロールバックしたいので…
class Foo < ApplicationRecord
  def apply!
    ApplicationRecord.transaction do
      # なんやかんかして
      self.applied = true
      self.save!
      self.__elasticsearch__.index_document # => これでElasticsearchにも反映される
    end
  end
end

画面側で確認しても問題なし👍Elasticsearchを使う検索フォームで検索してもOK。

問題発生(テストが落ちる)

しかし、RSpecでテストを書いたら、落ちる…😥

require 'rails_helper'

RSpec.describe Foo, type: :model do
  describe '#apply!' do
    subject { Foo.create!(applied: false) }
    it '反映されること' do
      expect {
        subject.apply!
      }.to change { subject.applied? }.from(false).to(true)
      definition = Elasticsearch::DSL::Search.search
      definition.query do
        must do
          term applied: true
        end
      end
      result = Foo.__elasticsearch__.search(definition).records
      expect(result).to be_present # => ❌存在しないと言われる👻
    end
  end
end

調査

なんとなく、もしかして非同期更新か?と思い、雑に2秒待つようにしたら、テストが通るように!

require 'rails_helper'

RSpec.describe Foo, type: :model do
  describe '#apply!' do
    subject { Foo.create!(applied: false) }
    it '反映されること' do
      expect {
        subject.apply!
      }.to change { subject.applied? }.from(false).to(true)
      sleep(2) # => 2秒待つ…
      definition = Elasticsearch::DSL::Search.search
      definition.query do
        must do
          term applied: true
        end
      end
      result = Foo.__elasticsearch__.search(definition).records
      expect(result).to be_present # => 🟢通った👍
    end
  end
end

とりあえずテストは通るようになったけれど、テストにsleepとか入れたくありません。どうにか同期的にデータ更新できる方法はないものか…とコードを読んでいきます。

https://github.com/elastic/elasticsearch-rails/blob/main/elasticsearch-model/lib/elasticsearch/model/indexing.rb

Yardを読むと、@param options [Hash] Optional arguments for passing to the clientと書いてあるけれど、いやそのHashにどう設定すればいいかがわからん!😠

ググったら、zennの記事がヒットしました。

zenn.dev

コードの感じからすると、Kotlinかなんかだろうか…?とりあえずrefreshオプションがあることがわかりました。

www.elastic.co

解決方法

というわけで、該当箇所を修正。

# apply!メソッドの一部
self.__elasticsearch__.index_document(refresh: true) # => Elasticsearchに同期的に反映

これで、sleepなしでもRSpecが通るようになりました👍

テストの時だけ同期をとるようにしたい

しかし、なぜ非同期でElasticsearch側に反映されるかというと、パフォーマンス向上のためですね。つまり、毎回同期をとるように更新していたら、遅くなります。ループでFoo#apply!メソッドを呼ぶこともあるので、それは避けたい…。 そもそも、普段は即時同期が取れていなければならないほどシビアな条件でもありません。単に、テストの時だけ同期が取れていればいいわけです。

そこで、該当箇所を以下のように修正。

# apply!メソッドの一部
self.__elasticsearch__.index_document(refresh: Rails.env.test?) # => テストの時だけElasticsearchに同期的に反映

これで、テストの時だけElasticsearchへの反映が同期的になりました👏👏👏