ボクココ

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

Elasticsearch と Rails のデータ同期方法

ども、@kimihom です。

全文検索の仕組みとして Elasticsearch を使ってサービスを運用している。Elasticsearch と Rails を使っている上で考慮しなきゃいけないデータ同期の方法について、それぞれのメリット/デメリットを紹介した上で最終的な提案まで記す。

本記事は Elasticsearch-Rails を利用した場合となるので、基本的な使用方法などはリンク先を参照いただければ幸いだ。

GitHub - elastic/elasticsearch-rails: Elasticsearch integrations for ActiveModel/Record and Ruby on Rails

では ActiveRecord と Elasticsearch でのデータ同期方法についていくつかご紹介しよう。

方法1. ActiveRecordコールバックによる同期

まず一番簡単な elasticsearch-model を使った ActiveRecord コールバックの方法だ。これは、ActiveRecord オブジェクトで 作成/更新/削除 されたタイミングで Elasticsearch のデータもそれぞれ更新する方法である。

class Article < ActiveRecord::Base
  include Elasticsearch::Model

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

  after_commit on: [:update] do
    __elasticsearch__.update_document
  end

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

ほとんど 登録/更新処理のないサービスの場合だったら、この方法で十分だろう。まずは ActiveRecord コールバックで問題ないか試しにやってみるといいと思う。

この方法のデメリットは2つある。1つは ActiveRecord オブジェクトの作成/更新のたびに Elasticsearch との接続が走ってしまって1つ1つのリクエストが重くなる点だ。当然 Rails サーバー側と Elasticsearch のサーバーは分けることになると思うので、その間のネットワーク接続で処理を待たされることになる。 2つめは ActiveRecord 限定で使っていかないといけない点だ。例えば裏側で PostgreSQL を使っていて、生の SQL でレコードをアップデートしても Elasticsearch 側に反映させることができない。あくまで #update#create などの ActiveRecord のメソッドを呼ばない限り Elasticsearch に登録されない。これは運用時に結構ネックになることがあるので気をつけたいところだ。

方法2. バッチ処理で一括取り込みを行う

もう一つの方法は elasticsearch-rails を使って一括取り込みを行う方法だ。

これであれば、前者のパフォーマンス問題と、生SQL で同期できない問題を解決できる。具体的には、以下の rake タスクを定期的に実行するような形となる。

bundle exec rake environment elasticsearch:import:all

これは、include Elasticsearch::Model が読み込まれた ActiveRecord モデルを一括で読み込み、その全データを Elasticsearch に投入することを意味する。全データなので、これが1万件くらいあると、当然無駄に時間がかかってしまう。 そして時間がかかるということで 1時間に1回の実行にしてしまうと、その間に登録されたデータは検索対象に入らなくなってしまう。この問題をなんとか解決したいところだ。

方法3. コールバックと バッチ処理の混合技

最終的なオススメは、上記2つを混合させる方法である。方法2で紹介した rake タスクは、対象の scope を指定することが可能だ。

bundle exec rake environment elasticsearch:import:all SCOPE='recent'

ActiveRecord 側で今回適当に用意した recent スコープを定義してあげる。

class Article < ActiveRecord::Base
  include Elasticsearch::Model

  scope :recent, -> {where("updated_at > ?", 1.hour.ago}

end

これで更新日時が 1時間以内のデータを一括で取ってきて、それだけを Elasticsearch に登録することが可能だ。しかし、この方法をしてしまうと、データ削除に対応できない という新たな問題が発生する。方法2の一括読み込みの方法では削除されてもデータ同期ができたが、今回は差分だけなので削除したレコードを取ってこれないのである。

てことで、削除の時だけ 方法1のコールバックを活用しよう。今回の結論としては以下のような形となる。

class Article < ActiveRecord::Base
  include Elasticsearch::Model

  scope :recent, -> {where("updated_at > ?", 1.hour.ago}

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

そして以下のコマンドを10分に1回などの Cron で回すようにしよう。

bundle exec rake environment elasticsearch:import:all SCOPE='recent'

1時間前の更新日時にしたのは、その間で Elasticsearch の同期に失敗しても、10分に1回なら6回のチャンスを与えるためにそうした。 import 自体は重複しても上書きするだけなので、確実にデータ同期する上でもこの方法で良いと思う。そしてこの方法なら、PostgreSQL の生 SQL でレコードをアップデートしたとしても、updated_at を正しく更新さえずれば Elasticsearch のデータ同期対象となる点も魅力的だ。 ※外部SQLでの削除 or ActiveRecord#delete(_all) 等は同期無理なのでやらないでね。

終わりに

今回は Elasticsearch Rails を使った活用方法をご案内した。

最終的には方法3をオススメしたけども、これはデータがリアルタイムに反映されるってわけでもないので、これでも完璧とは言えないとは思う。リアルタイムが求められるサービスなら、方法1 の同期処理を ActiveJob でバックグラウンド実行すること(方法4)を検討する価値もあるだろう。

本記事が Elasticsearch と Rails を使っている方に参考になれば幸いだ。

Rails を学んだ後に学びたいオススメ技術

ども、@kimihom です。

たまには Rails 初心者向けのコンテンツでも書こうと思い立った。 Rails を本なりなんなりで勉強した後、どういった技術を学ぶべきなのか。私のオススメする順番でご紹介していこう。

対象者

主にWeb アプリを作りたいと思っている Rails エンジニア。今回は スマホアプリとか フロントエンドの話はしないことにする。Bundler を扱えて、Rails の AssetPipeline, ActiveRecord 周りはそれなりに理解したけど、そのあとどうするのっていうくらいな方。

Heroku

Rails ならこれというくらいの定番。 最近では Rails の本にでも Heroku が登場することが多くなってきた。 Heroku を使えば、ローカルで開発した Rails アプリを公開して運用することができる。せっかく Rails でアプリ作れるようになったら、そのグレートなサービスをみんなに見てもらいたいところ。Heroku を使えば 5分もかからずに公開することができてしまう。 Heroku は Rubyist を Hero にしてくれるのだ。

メールをどうする? ログやアラートの管理は?HTTPS にするには? そういった Web の共通の課題は全て Heroku アドオンが解決してくれる。私たちはポチッと使うアドオンをクリックするだけで便利な機能を利用することが可能だ。 Chef? Ansible? Capistrano? そんなこと全く知らなくて OK。そんな所にコードを書く時間があったら、サービスの改善に時間を使おう。Heroku と その周辺のアドオンがインフラのあらゆる悩みを解決してくれるのだ。

Heroku はアメリカにサーバーがあるからレイテンシが〜。そんな声は無視しよう。実際に使えばわかるが、Heroku を使って遅いと感じることはほとんどない。Rails 初学者に必要なのは、自分たちの作りたいサービスがどんどん出来ていくその過程と、その実現のしやすさだ。それを実感していく中でどうやったら Heroku x Rails で速くするかは自ずと学んでいくことだろう。

AWS の一部

AWS は最近どんどん新しいサービスを出してきているが、Heroku を使っていれば知らなければならないことはそんなに多くない。私がオススメする以下の AWS を最低限マスターしておくと良いだろう。

  • S3。画像などのファイルのアップロードや閲覧などの静的ファイルを置く場所として有用。
  • Route 53。ドメイン周りの管理をしてくれる。独自ドメインを運用していきたい時に有用。
  • Lambda。 Rails の一部のコードを外に切り出してレスポンスを早めたり、定期的に処理をさせたい時に有用。
  • Cognito。 Web や スマホアプリからAWSサービスにアクセスする際に有用。
  • IAM。 AWS の権限周りの管理。

このくらいだ。他は類似した Heroku のアドオンを使ったほうが手っ取り早いし安上がりに済む場合が多い。他の AWS サービスは概要を知っておくっくらいで、後に回して良いだろう。

Redis

Redis を"キャッシュストアだ"と思うだけでは Redis の本当の素晴らしさを分かっていない。Redis は非常に柔軟な KVS でいてとても高速だ。Redis は PostgreSQL などの RDB では実現が難しいような痒い所に手が届く素晴らしい技術である。

例えば、一時的にデータを持っておきたいんだけど一定時間後に削除していいようなデータがあったとしよう。RDBでは定期的に削除するバッチ処理などを実装しなければならないが、Redis であればタイムアウトを設定することで勝手に消えてくれる。Key に自由な値を設けることで、RDB さながらの実装を Redis だけで実現できてしまう。この詳細については Redis の本を学ぶとイメージがつきやすいだろう。

Redis入門 インメモリKVSによる高速データ管理 : Josiah L. Carlson, 長尾高弘 : 本 : Amazon.co.jp

Heroku Redis として標準のアドオンとして提供されているので、 KVS であれば Redis という選択は賢い選択と言えるだろう。

Elasticsearch

たいていのサービスでは"検索"機能を実現したいということになるだろう。 Heroku Postgres では日本語の全文検索をサポートしていないので、 Elasitcsearch を使う決断をすることにいずれなる。Rails と Elasitcsearch を簡単に扱えるような Gem があるので、概要をさらっとドキュメントを読んで Sense を使いながらデータの出し入れと検索ができるようになればひとまずは OK 。日本語のドキュメントが全然なくて最初は苦戦するかもしれないが、Elasticsearch に限って言えば実際にインストールして使ってみながら改善していくスタイルでいいと思う。 Elasticsearch 周りの話はこのブログでもたまに取り扱ってるので気になる方はタグの Elasticsearch からざっと眺めてもらえれば幸いだ。

Elasticsearch 自体は Heroku のアドオンとして豊富に取り扱っているので好きに選べば良い。AWS でもいいけど。

終わりに

一気に書いていったが、まず一番大事なのは基本であると改めて書いておきたい。Rails より HTML/CSS/JavaScript のフロントエンド。フロントエンドよりも HTTP やCookie、セッションなどの基本的な概念だ。というのも Web の性質 をしっかりと理解しないと Rails コードにおいて簡単にレールを踏み外して、最高にカオスなコードを書き散らかす初学者が後を絶たないからだ。Rails を学んだらもう一度 HTTPの基本から始まり、 Rails の ActiveRecord, ActionView, ActionController などをしっかりと理解しておきたいところ。テストコードが自然に書けるようになれれば、 Rails エンジニアと名乗ることができよう。

今回紹介した技術はその基礎があって初めて応用できる分野とも言える。基礎を吹っ飛ばしていきなりこういう技術に手を出すのもいいけど、結局 Rails の基礎が足りないといつか気づいて後戻りするだけだから、ルートとしてはどちらでもいいのかもしれない。

エンジニアを成長させる一番の材料は「好奇心」だ。あの技術はどれを使ったら実現できるのだろう?というアンテナを常にめぐらせ、それに飛び込む。そうした思いっ切りの良さこそがいいエンジニアになるための条件である。是非とも好奇心のあるエンジニアの仲間になって世界を変えるサービスを作って欲しい。同じ思いを持つ私からの僅かながらのメッセージである。

Elasticsearch Rails で 全文検索とサジェスト機能を実現

ども、@kimihom です。

前回の記事で、Elasticsearch Rails の導入周りの調査内容をレポートした。

今回は調査内容をもとに、Elasticsearch と Rails を組み合わせて実装したので実践編としてまとめてみる。

Elasticsearch と Rails 間のデータ同期に関して

まず懸念事項のデータ同期に関する話。それなりにパフォーマンスを気にするのであれば、Resque や Sidekiq を用いてバックグラウンドに回して ActiveRecord のコールバックを活用してElasticsearch と 同期を取るのだけども、バックグラウンドジョブの実装は割とトラップが多く、その程度でバックグラウンドジョブを実現する価値があるのかだいぶ悩んだ。以下の記事が参考になった。

attracie.hatenablog.com

確かに実際に導入するのは簡単なんだけど、プロセスやメモリの管理をしっかりと考えないといけないし、実際に問題が起きた時に検知できて解決できるような仕組みを導入しないといけない。そこまでのことをしてまで導入するかどうかというのは一度冷静になって考えるべき、という意見はとても納得のいくものだった。

現時点でそこまで負荷のかかるサービスでもないし、今回はバックグラウンド処理は見送って、同期処理でやってみることにした。そうなると実装がかなりシンプルになる。

mapping などのデータ定義を考えなければ、modelに1個モジュールをincludeするだけで良くなる。これでデータのCRUD時に勝手に Elasitcsearch のデータもmapping定義に合わせて一緒に更新してくれる。実際にやってみたところそこまで動作が遅くなったという感じはしなかったので、この方法でひとまず正解だったと思っている。具体的にはモデルに以下を include すればおk。

    include Elasticsearch::Model
    include Elasticsearch::Model::Callbacks

これからのサンプルコードは全て Model 内に書くか、 Concern で切り出してそのモジュール内に書くかで対応できる。

Elasticsearch サービス

では Elasticsearch を使おうとなった時に、どのサービスを使えばいいのだろうか?自分でEC2などで建てる方法ももちろんあるだろうが、私は最も手軽な外部サービスを利用することにした。

そこで選んだのが、 Bonsai というサービスだ。テスト環境は無料から始められる。本番の Elasticsearch には $50 かかるが、実際に同様の環境を構築しなければならない時間やサーバコストを考えると、全然気にならない値段だ。

Bonsai に登録したら Elasticsearch の URL がゲットできるので、 Rails の config/initializers/bonsai.rb で初期化しよう。

Elasticsearch::Model.client = Elasticsearch::Client.new url: ENV['BONSAI_URL']

tokenizer, filter, analyzer

まず Elasticsearch Rails でぎょっとするのがスキーマ定義だろう。

私なりの理解で説明すると、tokenizer で文章をどのように区切るかという方法を定義し、filter でその区切った文字をどのように整形して保存するかを定義する。そして analyzer でどのtokenizerを使ってfilterで整形するかを定義する。最終的に各フィールドがどの analyzer で保存するかを決める必要がある。それを踏まえた上で、以下のサンプルコードを紹介する。参考にしたのはこちらのQiitaより。もろもろ調べた感じ、こちらが一番近いものになった。

    index_name "myapp"

    settings index: {
      analysis: {
        tokenizer: {
          kuromoji: {
            type: 'kuromoji_tokenizer'
          },
          ngram_tokenizer: {
            type: "nGram",
            min_gram: 3,
            max_gram: 11,
            token_chars: ['digit']
          }
        },
        filter: {
          pos_filter: {
            type:     'kuromoji_part_of_speech',
            stoptags: ['助詞-格助詞-一般', '助詞-終助詞'],
          },
          greek_lowercase_filter: {
            type:     'lowercase',
            language: 'greek',
          }
        },
        analyzer: {
          kuromoji_analyzer: {
            type:      'custom',
            tokenizer: 'kuromoji_tokenizer',
            filter:    ['kuromoji_baseform', 'pos_filter', 'greek_lowercase_filter', 'cjk_width'],
          },
          number_analyzer: {
            tokenizer: 'ngram_tokenizer'
          }
        }
      }
    } do
      mapping _source: { enabled: true }, _all: { enabled: true, analyzer: "kuromoji_analyzer" } do
        indexes :external_id, type: 'long', index: 'not_analyzed'
        indexes :name, type: 'string', analyzer: 'kuromoji_analyzer'
        indexes :text, type: 'string', analyzer: 'kuromoji_analyzer'
        indexes :number, type: 'string', analyzer: 'number_analyzer'
        self.instance_eval do # hack for including "suggest context"
          context = {external_id: {type: "category"}}
          @mapping[:suggest_field] = {type: "completion", context: context}
        end
      end
    end

tokenizer では kuromojingram を利用する。

kuromoji は日本語をいい感じに単語で区切ってくれて、それをキーワードとして分割してくれる。んでそれを'kuromoji_baseform', 'pos_filter', 'greek_lowercase_filter', 'cjk_width' などでより綺麗な形に整形して保存する。

対して nGram は min_gram から max_gram までを文字を分割して保存する方法だ。今回は番号検索とかでこちらを利用した。1234 って数字があった時、 min_gram 2, max_gram 3 の場合、12, 23, 34, 123, 234 って感じで保存してくれる。そんで 123 って検索した時にこれがヒットするようになるというわけだ。

デフォルトで使える各 tokenizer, filter, analyzer などは公式ドキュメントに全てまとまっている。やはり細かいオプションなどはこうしたところしか載っていないので必ず読むことになるだろう。

mapping メソッドにて、Elasticsearch に保存するキー名を定義する。基本的に Rails モデルで定義したカラム名のうち、検索で使いたいものだけ登録する形になると思う。ここで上部に定義した analyzer を指定してあげることで保存、検索にそのアナライザを使って検索してくれることになる。

今回は サジェスト 機能も実装したので、suggest_field も定義した。Completion Suggesterの公式ドキュメントを見ながら設定した。今回の仕様として、external_id 毎に検索のサジェストも分けるようにしたかったので、Elasticsearch の Context Suggester というのを同時に併用して実現した。これを使えば サジェスト機能でも where のような絞り込みをした上でサジェストしてくれるようになる。

そんで Elasticsearch Rails のソースコードを見ると、Context Suggesterを実現する上で必要な contextをセットできなかったので、instance_eval を使って強引に設定した。もっといい方法があるのかもしれないが、見た感じやり方がなかった。

以上でスキーマ定義が完了する。実際は上記設定を何度も変えて、データをインポートしながらちゃんと検索で出てくるのかをテストすることになる。

as_indexed_json の定義

続いて ActiveRecord と Elasitcsearch の関連付けを行う。 ActiveRecord カラムと Elasticsearch のキーは違うことになることが多いわけで、以下のようなメソッドを定義してあげる。

    INDEX_FIELDS = %w(external_id name text number).freeze
    SUGGEST_FIELDS = %w( name ).freeze
    def as_indexed_json(options={})
      indexed_json = self.as_json.select { |k, _| INDEX_FIELDS.include?(k) }
      indexed_json.merge!({
        suggest_field: {
          input: indexed_json.select { |k, _| SUGGEST_FIELDS.include?(k) }.values,
          context: {
            external_id: self.external_id
          }
        }
      })
    end

モデルに関連づいた key-value 形式のハッシュと、 suggest_field のついたハッシュが組み合わさったような形になる。 suggest_field は配列が指定できるので、サジェスト項目の候補を複数いれておくことも可能だ。

データインポート

さて、これらの定義を終えると次は DB -> ActiveRecord -> Elasticsearch でデータをインポートすることになる。

lib/tasks/elasticsearch.rakeに以下を追記。

require 'elasticsearch/rails/tasks/import'

これで rake タスクが追加される。以下を実行することで一括インポートが実行される。

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

あとは ローカルで起動した KibanaSense を起動して、データのクエリを投げて遊んでみよう。Sense の URL は Bonsai で登録した Elasitcsearch の URL に変更する。クエリの種類に関しては他サイトや公式サイト 見ながら組み立てよう。基本的に SQLでできることは Elasitcsearch でもだいたいできる。 LIKE 句は Elasticsearch だと tokenizer の分類になることに注意。LIKE をできるようにするために Elasitcsearch を入れているので当然のことだ。

Rails 側で検索、サジェスト

公式ドキュメントを読めば書いてあるけど一応載せておく。

    def self.search external_id, keyword
      query = { query: { and: [
        { constant_score: { filter: { term: { external_id: external_id } } } },
        { query_string: { query: keyword, fields: ["number", "name", "text"] } }
      ] } }
      Elasticsearch::Model.search( query ).records.to_a
    end

    def self.suggest external_id, keyword
      query = { suggest: {
        text: keyword,
        completion: {
          field: "suggest_field",
          size: 10,
          context: { external_id: external_id }
      } } }
      __elasticsearch__.client.suggest(body: query, index: INDEX_NAME)["suggest"].to_a.first.to_hash["options"]
    end

上記の クエリは、 where で 外部id をあらかじめ絞った場合 の検索だ。

その他知っておきたいこと

Sense でのクエリの書き方は知っておいたほうがためになる。といっても REST で body が JSON のを投げるだけなので Elasitcsearch の書き方を知るだけだが。例えばマッピング定義を確認したい時は、 GET my_index/_mapping で確認できる。いちいち Rails でコード書いてテストみたいなのは時間がかかるから、 Sense で動いたら Rails コードに落とし込むってやり方がいいかと思われる。

今回は ActiveRecord の saveupdate, destroy が呼ばれた時のコールバックで Elasitcsearch に登録している。つまり、直接生の SQL を書いていたり、外部から別で SQL を呼んでデータを挿入しているといった場合には、別途 Elasticsearch にもデータを更新することを忘れないようにしよう。不整合が起きる原因となる。

DELETE WHERE みたいなことがしたくなるのだけども、Elasticsearch 2 系からは DELETE Query が廃止されたようだ。なので定期的に再インポートしたり、Elasticsearch Rails の records メソッドを使って Elasitcsearch にはあるけど ActiveRecord にはないデータは取ってこないといったような工夫で対応しよう。

追記 (2016/5/9)

上記の状態でサジェストを実現すると、モデルを更新した時にサジェストが反映されない問題が起きる。その対応について以下の記事にまとめた。

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

終わりに

Elasticsearch の公式ドキュメントをゼロから読んでいたのだが、Elasticsearch に関しては実践から始めた方がより理解できる気がした。割と感覚的に実装していけるのがこの技術の良さなのかもな。それからこだわりたい部分をしっかりと学んでいけば大丈夫。

この記事を書くまでにあらゆるサイトを巡って調査し、公式ドキュメントを読み、実現することができた。次の私のような方がこの記事で得られるものがあれば幸いである。

こうして技術というのは発展していくのかな、とも最近感じるようになったので、頑張って書いてみた。

Elasticsearch Rails の調査レポート ~実運用へ向けて~

ども、@kimihom です。

今回は Ruby on Rails で Elasticsearch を使う方法について調査したので報告しよう。

特に ActiveRecord と Elasticsearch をどう連携させるのか、そこら辺を詳しく書こうと思う。

elasticsearch-rails の利用

さて、 Rails で Elasitcsearch を利用する場合、上記の gem を利用することになるが、この gem は何者なのか自分の理解でまとめる。

  • Elasticsearch での index, type の指定
  • ActiveRecord 上のオブジェクトで Elasticsearch へ保存するフィールドを指定
  • ActiveRecord(or Mongoid) でのデータを Elasticsearch に一括インポート
  • ActiveRecord でオブジェクトを作成・更新・削除したタイミングでデータを同期
  • Elasticsearch で検索

この Gem は3つに分かれるが、ActiveRecord を使う場合は、

gem 'elasticsearch-model', git: 'git://github.com/elasticsearch/elasticsearch-rails.git'
gem 'elasticsearch-rails', git: 'git://github.com/elasticsearch/elasticsearch-rails.git'

の2つの gem を利用する。elasticsearch-model が今回重要なコアとなる ActiveRecord の拡張だ。 elasticsearch-rails は インポートに便利な rake タスクの提供やログの出力の設定などが実装されている。

elasticsearch-model で検索する

さて、細かな設定方法はドキュメントを読めばいいのだが、特に導入において重要な点を以下に挙げる。

ActiveRecord と Elasticsearch の同期

どうやって ActiveRecord と Elasticsearch を同期させるか、は私自身、とても悩んでいたことだった。実際に elasticsearch-model を見てみると、幾つかのやり方があるように見える。まず一番シンプルなのは、 ActiveRecord の create, update, destroy のコールバックを用いて、そのタイミングで Elasticsearch にリクエストを送り、データを同期させる方法だ。

class Article < ActiveRecord::Base
  include Elasticsearch::Model

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

  after_commit on: [:update] do
    __elasticsearch__.update_document 
  end

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

こうすれば基本的に ActiveRecord でデータが変われば Elasticsearch も変わるようになる。ただし、その分リクエストが発生するので、大量アクセスのあるサービスの場合は Resque などを利用する必要がある。

少なくともこれで ActiveRecord の CRUD時は Elasticsearch と同期させた運用が可能になる。しかし自分の環境の場合、 ActiveRecord を介さないで PostgreSQL に保存する場合があり、その時にどうやって Elasticsearch へデータを入れるかは未だ調査が必要な点だ。

Elasticsearch を ActiveRecord::Relation で扱う

例えば、 Article.search("hoge") とした時に、 その戻りがActiveRecord::Relationだと、where でのさらなる絞り込みやページング、グルーピングなどが可能になる。シンプルに Article.search("hoge").recordsrecordsを付与すると変換してくれる。これにより、 Elasticsearch ではデータが残っていたけども、ActiveRecord ではデータが削除されていたので、それは取得しない、ということが可能になる。Elasticsearch で ids を取得し、さらに ActiveRecord でその ids で検索を内部で動作させるからだ。これは2回リクエストを送るというやや冗長な感じになるが、データが必ず存在することを担保することが可能だ。

インデックスの作成とデータインポート

インデックスの作成はまずプログラムで実行させるようなコードを書く必要がある。

      client = Elasticsearch::Client.new host: 'localhost:9200', logger: Logger
      client.indices.delete index: index_name rescue nil
      client.indices.create(index: index_name,
                            body: {
                                settings: settings.to_hash,
                                mappings: mappings.to_hash
                            })

こんな感じのを rake タスクなり model なりにうまい具合に記入し(上記コードじゃ動かないので変数にうまく値を入れる)、インデックスを作成しよう。そのあと、以下を lib/tasks/elasticsearch.rake に記入しよう。

# lib/tasks/elasticsearch.rake
require 'elasticsearch/rails/tasks/import'

これだけで bundle exec rake -D elasticsearch が打てるようになり、このコマンドで ActiveRecord から Elasticsearch へインポートする方法が出来上がる。例えば Article に Elasticsearch の定義をしたならば、以下のような形でインポートが可能だ。

bundle exec rake environment elasticsearch:import:model CLASS='Article'

一括でサクッとインポートできるので大変便利なコマンドだ。

終わりに

私自身、まだ調査段階のレポートであるため、実際に Elasticsearch Rails な環境で運用していくとまた違う問題などが発生すると思う。適宜本ブログで報告していきたい。

Elasticsearch のクエリとフィルタについて

ども、@kimihomです。

今回は Elasticsearch の Query と Filter について自分の理解した範囲で書いてみることにする。

クエリ と フィルタ の違い

本家の英語ドキュメントを読むと、以下のように書かれている。

As a general rule, use query clauses for full-text search or for any condition that should affect the relevance score, and use filter clauses for everything else.

軽く訳すと、一般的なルールとして、フルテキスト検索か関連するスコアに影響する条件があるときは クエリ を使い、それ以外はフィルタを使う。 とある。どの程度、検索結果に関連性が高いかを示すスコアが大事な時は query を使うという感じか。普通の LIKE 検索程度だったら filter でも大丈夫なようだ。

この違いを知っておくと、割と Elasticsearch で投げる構文を理解しやすかった。

検索のサンプル

まずはデータを投入しよう。 Kibana にある Sense ってのをプラグインとしてインストールすると、実験が色々できて理解が深まった。 Kibana の Zip を本家サイトからDLし、以下のプラグインを入れるコマンドを打つことで導入できる。

$./bin/kibana plugin --install elastic/sense
./bin/kibana

# Server running at http://0.0.0.0:5601 ...

まずはデータ投入。

# Insert some example docs
PUT /test/test/1
{
  "folder": "inbox",
  "email": "Big opportunity"
}

PUT /test/test/2
{
  "folder": "spam",
  "email": "Business opportunity"
}

PUT /test/test/3
{
  "folder": "inbox",
  "email": "Urgent business proposal"
}

これらをフィルタとクエリでいろいろな条件で取ってくるとする。

すべて取得

この時はクエリしか使わないので、トップに query と指定してその直下に条件を書くだけで良い。

GET /_search
{
    "query": {
        "match_all": {}
    }
}

この場合 インデックス と タイプ を指定していないので、Elasticsearch に保存したすべてのデータを取ってくることになる。限定したい場合は GET /index/_search とか、 GET /index/type/_search などのようにパスで区切って限定できる。てか GET にリクエストボディを入れてリクエストを送るってのがなかなか新鮮。普通のHTTPのGETとかだとやらないやり方だ。

ただ、冗長だけどこうやってかけることも知っておくといいと思った。

GET /_search
{
  "query": {
    "filtered": {
      "query": {
        "match_all": {}
      }
    }
  }
}

それはこの後書く フィルタ と組み合わせる時に必要な書き方である。

クエリとフィルタでの取得

さて、上記データでクエリとフィルタ合わせ技はこんな感じになる。

GET /_search
{
  "query": {
    "filtered": {
      "query": {
        "match": {
          "email": "business opportunity"
        }
      },
      "filter": {
        "term": {
          "folder": "inbox"
        }
      }
    }
  }
}

クエリの match は businessopportunity があればヒットするけど、 フィルタの term は一致してないとヒットしない。クエリとフィルタを合わせ技で書くときは、クエリの指定は query.filtered.query のように query が2回出てくることになるようだ。ここら辺が初心者にはちょっと気持ち悪かったけど、慣れなのかな。

終わりに

今回はめちゃめちゃシンプルなところだけ書いてみた。もっと細かい指定ができるけど、この基本さえ押さえておけば、あとは他の種類を知るだけなので入り口の記事としては有用かと思う。

本家ドキュメントが割とボリュームあって読み終えるには時間がかかりそうだが、じっくりとブログに理解したことを書きながら読み進めようと思う。

Elasticsearch に挫折し、もう一度勉強し始めた話

ども、@kimihomです。

最近は新しい技術勉強って事で Elasticsearch を勉強している。それなりのWebやアプリを持つようになると、ほぼ100%必要になってくるのが"検索"。 しっかりとマスターして、より良いデータ検索と分析ができるようになりたいと思い勉強を始めた。

勉強方法

今までは本とか、日本語のブログとかで勉強していたんだけど、全く頭に入ってこなかった。 日本語であまり整備されていない気がする。そんで最初のElasticsearch 学習は挫折した。

んでもう一度検索が必要になりそうだったので興味を持って再勉強することに。んで最近思うのは、学ぶのに一番いいのは本家ドキュメントを読むことだということ。最新版の情報はやはりオフィシャルに行くしかない。Elasticsearch に関して言えば、本格的な電子書籍としてWebで無償で公開してくれているので、これを読まない手はない。英語だけど・・。

てことで今回のまとめもこの英語ドキュメントからメモとして残しておきたいところをピックアップする。

概要

大規模なデータでも素早い全文検索とデータ解析を可能にする。Wikipedia の膨大なドキュメント検索も Elasticsearch で動いているのだそう。

そもそも全文検索についてなんだけど、SQLの基本を学んだ人は LIKEというキーワードを知っているかと思う。このLIKEなんだけど、%を両サイドで入れる(検索する)と、インデックスが効かなくなっちゃうのよね。これを日本語検索でなんとかするには PostgreSQL とかでは追加のプラグイン(PGRoonga)などを入れる必要がある。Heroku では現在対応していない模様。#herokujpハッシュタグにこのことを呟けば検討してくれるみたいだけど。それでも集計や検索のパフォーマンスにそんなに困ってなければ、困るようになるまで RDBだけの運用とかにしておくのも手だ。なぜなら、検索や集約の為にデータを RDB、Elasticsearch それぞれに保存する必要があるからだ。これらの運用がやはり問題に感じる。この点いろいろ試行錯誤してみんなやってるみたいだ。

高機能な検索とか集計をしたいのであれば、Elasticsearch 導入を検討してみよう。私の場合どうせ今後困ることになるのなら、早めに勉強しておこうという感覚で Elasticsearch を学ぶことにした。

Elasticsearch, Kibana, Sense のインストール

Kibana ってのは Elasticsearch の UI 部分を提供している。Elasticsearch 単体で REST でアクセスすればデータ操作できるけど、Web上でコンソールっぽくいじったり表示したいなら便利っぽい。 Swagger のなかの SwaggerUI みたいなもんか。 あと Sense ってのは REST アクセスをWeb上で手軽に操作できるもの。オフィシャルドキュメントを読むならこれは入れておいたほうがいい。理解度が変わってくる。

インストール方法はそれぞれググって zip ダウンロードして 展開するだけで動くっぽい。楽でいいね。あとサービスが大きくなってレプリケーションとかシャーディングとかの大規模 Elasticsearch にしても基本的な操作はほとんど同じらしい。これもいいね。

用語と基本操作

まず、用語。これは覚えておかないとな。

Relational DB  ⇒ Databases ⇒ Tables ⇒ Rows      ⇒ Columns
Elasticsearch  ⇒ Indices   ⇒ Types  ⇒ Documents ⇒ Fields

indexの複数形はindices らしい。初めて知ったw

んで、URL は基本 index名/type名/document_id となっていてシンプル。各リソースに対して GET,POST, PUT, DELETE すればCRUD(Create, Read, Update, Delete)操作が可能だ。 idは PUT で指定すれば、そのidベースで作られ、POSTで作ればランダムな文字列のidが返されるようだ。

ドキュメントを読む限り、Elasticsearch は 純粋な REST API だ。 GET で情報の取得・検索、 PUT で情報の Upsert, POST で作成, DELETE で削除。 部分的な更新はPOSTで _update ってのを付け加えるみたい。

あと bulk 操作も提供されている。一気にデータを投入したいとかいった場合に使える。この時は割と特徴的な構文だった。これに関してはリンク先で。 Cheaper in Bulk

終わりに

まだまだ読み途中で一番大事な検索の章とか全く読めてないんだけど、読んだ内容のアウトプットとしてこのブログに書いていこうと思う。

いつかは自分のサービスにうまく導入できるようになるといいな。