ボクココ

個人開発に関するテックブログ

Elasticsearch Rails でサジェストを実現(続き)

ども、@kimihom です。

前回の Elasticsearch Rails の記事で、"サジェスト機能を実現する"とあったが補足事項があったので追記する。

ハマった問題

前回の記事を元に、実際に以下の rake タスクを実行すれば、確かにインポートしたデータはサジェストしてくれる。

bundle exec rake environment elasticsearch:import:all FORCE=y

しかし、なぜかモデルを更新するとその更新した値がサジェストとして出てこなくなる問題が起きることがわかった。つまり、前回の記事の状態だとサジェストとして機能するのは一括インポートしたデータと、新規作成したデータのみが正しく表示されることになってしまう。これではマズイ。

問題の原因

こりゃあおかしい、と Elasticsearch Rails のソースコードを読むと、update_documentメソッドが以下の通りになっていた。

        def update_document(options={})
          if changed_attributes = self.instance_variable_get(:@__changed_attributes)
            attributes = if respond_to?(:as_indexed_json)
              self.as_indexed_json.select { |k,v| changed_attributes.keys.map(&:to_s).include? k.to_s }
            else
              changed_attributes
            end

            client.update(
              { index: index_name,
                type:  document_type,
                id:    self.id,
                body:  { doc: attributes } }.merge(options)
            )
          else
            index_document(options)
          end
        end

つまり、更新したフィールドだけが Elasticsearch の更新対象となり、サジェストの部分が更新されないような作りになっていたのだ。とはいえ、元の elasticsearch-rails のソースを書き換えることはしたくないので、 merge(options) をうまく利用することで解決した。ちなみに上記の self__elasticsearch__であることに注意。

問題の解決

Elasticsearch-rails のコールバックをカスタムコールバックにし、以下のようなコードに変更した。以下は自分の Rails ソースのモデル内のコード。

    after_commit on: [:create] do
      __elasticsearch__.index_document
    end

    after_commit on: [:update] do
      if changed_attributes = __elasticsearch__.instance_variable_get(:@__changed_attributes)
        hash = self.as_indexed_json.select { |k,v| changed_attributes.keys.map(&:to_s).include? k.to_s } 
        doc = { suggest_field: {input: [self.name, self.email, self.address]}}.merge(hash)
        __elasticsearch__.update_document({body: {doc: doc}})
      end
    end

    after_commit on: [:destroy] do
      __elasticsearch__.delete_document
    end

これにより、include Elasticsearch::Model::Callbacks は消すことになる。doc = { suggest_field: {input: [self.name, self.email, self.address]}}.merge(hash) を追加したいがためにこのようなコードになった。何はともあれ、これでモデルを更新した時にサジェストの方も無事に更新されるようになった。

終わりに

import によってサジェストがしっかりと動いていたから、アップデートの時だけ動かないのはソースを読めば確実に直せると思ったので、今回はいきなりソースを読んで解決した。

落としてきたgemソースから grep すれば簡単に対象の場所は見つかるし、困った時はソースを読んで解決できる力はあるととても便利だと感じる。

こういうトリッキーなことしちゃうと今後のGemアップデートで動かなくなる可能性が高くなるから、それだけ注意しないといけない。ということでソースを読んでハックするのはできる限り控えめにしていきたいところだ。