Rails: 高速リアルタイム検索API「algolia-search-rails」gem README(翻訳)

概要

MITライセンスに基づいて翻訳・公開いたします。


algolia.comより

Rails: algolia-search-rails gem README(翻訳)

Algolia Searchは、最初のキーストロークを入力した時点でリアルタイムで結果を返せる、ホスト型検索エンジンです。

このgemはalgoliasearch-client-rubyを元に作られたもので、Algolia Search APIを自分好みのORMに簡単に統合できます。

Rails 3.x、4.x、5.xはすべてサポート対象です。

algoliasearch-rails-exampleサイトで、autocomplete.jsベースのオートコンプリート機能やInstantSearch.jsベースのインスタント検索結果ページをご覧いただけますので、ご興味がありましたらどうぞ。

API ドキュメント

完全なリファレンスはAlgoliaのWebサイトで参照いただけます。

訳注: 目次は省略しました

セットアップ

インストール方法

gem install algoliasearch-rails

Gemfileに以下を追加します。

gem "algoliasearch-rails"

続いて以下を実行します。

bundle install

設定

config/initializers/algoliasearch.rbファイルを作成し、APPLICATION_IDAPI_KEYをセットアップします。

AlgoliaSearch.configuration = { application_id: 'YourApplicationID', api_key: 'YourAPIKey' }

このgemは、ActiveRecordMongoidSequelと互換性があります。

タイムアウト

初期化時に以下のオプションを設定することで、さまざまなタイムアウトスレッショルドを設定できます。

AlgoliaSearch.configuration = {
  application_id: 'YourApplicationID',
  api_key: 'YourAPIKey',
  connect_timeout: 2,
  receive_timeout: 30,
  send_timeout: 30,
  batch_timeout: 120,
  search_timeout: 5
}

注意

このgemでは、インデックス作成タスクのトリガーにRailsのコールバックを多用しています。after_validationbefore_saveafter_commitといったコールバックをバイパスするメソッドが使われていると、変更がインデックスに反映されません。たとえば、update_attributeメソッドはバリデーションチェックを行いません。アップデート時にバリデーションを行うには、update_attributesをお使いください。

AlgoliaSearchモジュールによって注入されるメソッド名の冒頭にはすべてalgolia_が追加され、それらに関連する短いエイリアス名も追加されます(定義されていない場合)。

Contact.algolia_reindex! # <=> Contact.reindex!

Contact.algolia_search("jon doe") # <=> Contact.search("jon doe")

利用法

インデックスのスキーマ

以下のコードは、Contactインデックスを作成してContactモデルに検索機能を追加します。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch do
    attribute :first_name, :last_name, :email
  end
end

送信する属性を指定する(ここでは:first_name:last_name:emailに限定します)ことも、指定しない(この場合すべての属性が送信される)こともできます。

class Product < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch do
    # すべての属性が送信される
  end
end

add_attributeメソッドを用いて、モデルのすべての属性に加えて別の属性を送信することもできます。

class Product < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch do
    # すべての属性の他にextra_attrも送信される
    add_attribute :extra_attr
  end

  def extra_attr
    "extra_val"
  end
end

関連性の高さ

私たちの提供する設定では、インデックス全体の関連性の高さ(relevancy)をチューニングするさまざまな方法が使えます。その中でも最も重要性が高いのは、「検索可能な属性(searchable attributes)」と、「レコードの人気(record popularity)」を反映するいくつかの属性です。

class Product < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch do
    # Algoliaレコードのビルドに使う属性のリスト
    attributes :title, :subtitle, :description, :likes_count, :seller_name

    # 検索したい属性を`searchableAttributes`設定で定義する
    # (旧attributesToIndex)(ここでは`title`、`subtitle`、`description`)。
    # 重要性の高い順にリストアップする必要がある。
    # `description`に`unordered`とタグ付けすることでその属性のマッチ位置への影響を回避している。
    searchableAttributes ['title', 'subtitle', 'unordered(description)']

    # `customRanking`設定はランキングの基準(criteria)を定義するもので、
    # 2つのレコードのテキスト関連性が等しいかどうかを比較するのに用いられる。
    # これはそのレコードの人気(popularity)を反映する。
    customRanking ['desc(likes_count)']
  end

end

インデックス化

特定のモデルをインデックス化するには、そのクラスで単にreindexを呼び出します。

Product.reindex

すべてのモデルをインデックス化する場合は以下のようにします。

Rails.application.eager_load! # 全モデルが読み込み済みであること(development環境では必須)

algolia_models = ActiveRecord::Base.descendants.select{ |model| model.respond_to?(:reindex) }

algolia_models.each(&:reindex)

フロントエンド検索(リアルタイムエクスペリエンス)

従来の検索ロジックや機能は、バックエンドで実装される傾向がありました。この方法は、ユーザーが検索クエリを手入力して検索を実行し、結果ページにリダイレクトするという検索エクスペリエンスであれば事足りました。

検索をバックエンドで実装する必然性はもはやありません。現実には、ほとんどの場合ネットワークの遅延や処理の遅延が重なってパフォーマンスが悪化します。そこで、私たちが開発したJavaScript API Clientの利用を強くおすすめします。あらゆる検索リクエストをユーザーのブラウザやスマートフォンやクライアントから直接発行することで、トータルの検索遅延を削減しつつ、サーバーの負荷も同時に軽減します。

私たちのJS APIクライアントはgemに組み込まれているので、JavaScriptマニフェストの手頃な場所(Rails 3.1以降ならapplication.jsなど)でalgolia/v3/algoliasearch.minrequireするだけで準備できます。

//= require algolia/v3/algoliasearch.min

あとは以下のようなJavaScriptコードでできます。

var client = algoliasearch(ApplicationID, Search-Only-API-Key);
var index = client.initIndex('YourIndexName');
index.search('something', { hitsPerPage: 10, page: 0 })
  .then(function searchDone(content) {
    console.log(content)
  })
  .catch(function searchFailure(err) {
    console.error(err);
  });

先ごろ(2015年3月)JavaScriptクライアントの新しいバージョン(V3)をリリースしました。V2をお使いの方は移行ガイドをお読みください

バックエンド検索

注意: クエリをサーバーから送信せずにエンドユーザーのブラウザから直接クエリ送信するのであれば、JavaScript API Clientを使うことをおすすめします。

1件の検索はORMに沿ったオブジェクトを返しますが、そのときにデータベースからの再読み込みが発生します。トータルの遅延とサーバーの負荷を削減するためにも、クエリ実行はJavaScript API Clientで行うことをおすすめします。

hits =  Contact.search("jon doe")
p hits
p hits.raw_answer # 元の生JSON answerを取得する

各ORMオブジェクトにはhighlight_result属性が1つずつ追加されます。

hits[0].highlight_result['first_name']['value']

データベースからのオブジェクト再読み込みを行わずにAPIから生JSON answerを取り出したい場合は、次の方法が使えます。

json_answer = Contact.raw_search("jon doe")
p json_answer
p json_answer['hits']
p json_answer['facets']

検索パラメータは、インデックス設定から静的に指定することも、または検索時にsearchメソッドの第2引数でsearch parametersを動的に指定することもできます。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch do
    attribute :first_name, :last_name, :email

    # インデックス設定に保存されているデフォルトの検索パラメータ
    minWordSizefor1Typo 4
    minWordSizefor2Typos 8
    hitsPerPage 42
  end
end
# 動的な検索パラメータ
p Contact.raw_search('jon doe', { hitsPerPage: 5, page: 2 })

バックエンドのページネーション

私たちは、あらゆる検索の実行(すなわちページネーションも)をフロントエンドのJavaScriptで行うことを強くおすすめしていますが、ページネーションのバックエンドとしてwill_paginatekaminariもサポートします。

:will_paginateを用いる場合は以下のように:pagination_backendで指定します。

AlgoliaSearch.configuration = { application_id: 'YourApplicationID', api_key: 'YourAPIKey', pagination_backend: :will_paginate }

これで、searchメソッドを呼び出せばたちどころにページネーションされた結果が表示されます。

# コントローラ
@results = MyModel.search('foo', hitsPerPage: 10)

# ビュー(will_paginateを使う場合)
<%= will_paginate @results %>

# ビュー(kaminariを使う場合)
<%= paginate @results %>

タグ付け

tagsメソッドで以下のようにレコードにタグを追加できます。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch do
    tags ['trusted']
  end
end

以下のように動的な値も使えます。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch do
    tags do
      [first_name.blank? || last_name.blank? ? 'partial' : 'full', has_valid_email? ? 'valid_email' : 'invalid_email']
    end
  end
end

結果セットを特定のタグで絞り込むには、クエリ発行時に{ tagFilters: 'tagvalue' }または{ tagFilters: ['tagvalue1', 'tagvalue2'] }を検索パラメータとして指定します。

ファセット

検索結果でさらにfacetsメソッドを呼ぶことで、ファセットを取得できます。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch do
    # [...]

    # ファセットで使える属性のリストを指定する
    attributesForFaceting [:company, :zip_code]
  end
end
hits = Contact.search('jon doe', { facets: '*' })
p hits                    # ORM-compliant array of objects
p hits.facets             # extra method added to retrieve facets
p hits.facets['company']  # facet values+count of facet 'company'
p hits.facets['zip_code'] # facet values+count of facet 'zip_code'
raw_json = Contact.raw_search('jon doe', { facets: '*' })
p raw_json['facets']

ファセットの検索

以下のようにファセットの値も検索できます。

Product.search_for_facet_values('category', 'Headphones') # {value, highlighted, count}の配列

このメソッドには、クエリで使える任意のパラメータを渡せます。これによって、そのクエリにマッチしそうな結果だけを返すように調整できます。

# 「red Apple products」(およびそれらの個数のみ)を含むカテゴリだけを返す
Product.search_for_facet_values('category', 'phone', {
  query: 'red',
  filters: 'brand:Apple'
}) # 「red Apple products」にリンクするphoneカテゴリの配列

グループ化(group by)

グループ化をdistinctに行う方法について詳しくはこちらをご覧ください。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch do
    # [...]

    # レコードをグループ化する属性を指定する
    # (ここではcompanyでレコードをグループ化する)
    attributeForDistinct "company"
  end
end

地理的な検索(geo-search)

レコードの地理上の位置で絞り込むにはgeolocメソッドを使います。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch do
    geoloc :lat_attr, :lng_attr
  end
end

結果セットをSan Joseの周囲50km以内に絞り込むには、クエリ発行時に{ aroundLatLng: "37.33, -121.89", aroundRadius: 50000 }を検索パラメータとして指定します。

オプション

自動インデックスと非同期実行

インデックスは、レコードが1件保存されるたびに「非同期的に」反映され、レコードが1件削除(destroy)されるたびにインデックスから「非同期に」削除されます。具体的には、ADDやDELETEを伴うネットワーク呼び出しは同期的にAlgolia APIに送信されますが、Algoliaのエンジンでの処理は非同期的に行われます。つまり、直後だと結果が反映されない可能性があります。

自動インデックスやインデックスからの自動削除の設定は、以下のオプションで無効にできます。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch auto_index: false, auto_remove: false do
    attribute :first_name, :last_name, :email
  end
end

自動インデックスを一時的に無効にする

自動インデックスは、without_auto_indexスコープで一時的に無効にできます。これはパフォーマンス上の理由でよく使われます。

Contact.delete_all
Contact.without_auto_index do
  1.upto(10000) { Contact.create! attributes } # このブロック内では自動インデックスが動かない
end
Contact.reindex! # バッチ操作を用いる

キューとバックグラウンドジョブ

自動インデックスや自動削除の処理を設定することで、キューを用いてこれらの処理をバックグラウンド実行できます。デフォルトではActive Job(Rails 4.2以降)のキューが用いられますが、独自のキューイングメカニズムを定義することもできます。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch enqueue: true do # ActiveJobは`algoliasearch`キューでトリガされる
    attribute :first_name, :last_name, :email
  end
end

考慮すべき点

更新や削除をバックグラウンドで行う場合、ジョブの実際の実行時より前のタイミングでデータベースにレコードの削除がコミットされる可能性があります。万一、レコードを削除するためにレコードをデータベースから読み込むと、ActiveRecord#findがRecordNotFoundで失敗します。

このような場合は、ActiveRecordからのレコード読み込みをバイパスしてインデックスを直接操作する方法があります。

class MySidekiqWorker
  def perform(id, remove)
    if remove
      # レコードがデータベースから削除された可能性があれば
      # ActiveRecord#findで読み込めない
      index = Algolia::Index.new("index_name")
      index.delete_object(id)
    else
      # レコードは存在するはず
      c = Contact.find(id)
      c.index!
    end
  end
end

Sidekiqの場合

Sidekiqの場合は次のようにします。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch enqueue: :trigger_sidekiq_worker do
    attribute :first_name, :last_name, :email
  end

  def self.trigger_sidekiq_worker(record, remove)
    MySidekiqWorker.perform_async(record.id, remove)
  end
end

class MySidekiqWorker
  def perform(id, remove)
    if remove
      # レコードがデータベースから削除された可能性があるので
      # ActiveRecord#findで読み込めない
      index = Algolia::Index.new("index_name")
      index.delete_object(id)
    else
      # レコードは存在するはず
      c = Contact.find(id)
      c.index!
    end
  end
end

DelayedJobの場合

delayed_jobの場合は次のようにします。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch enqueue: :trigger_delayed_job do
    attribute :first_name, :last_name, :email
  end

  def self.trigger_delayed_job(record, remove)
    if remove
      record.delay.remove_from_index!
    else
      record.delay.index!
    end
  end
end

同期処理とテストについて

次のオプションを設定することで、インデックス化とインデックスからの削除を同期的に行うよう強制できます(この場合、gemはwait_taskメソッドを呼ぶことで、メソッドから戻ったときにこの操作に対応します)。ただし、この操作は非推奨です(テスト目的を除く)。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch synchronous: true do
    attribute :first_name, :last_name, :email
  end
end

インデックス名をカスタマイズする

デフォルトではクラス名がインデックス名に使われます(「Contact」など)。index_nameオプションでインデックス名をカスタマイズできます。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch index_name: "MyCustomName" do
    attribute :first_name, :last_name, :email
  end
end

インデックス名に環境を追加する

以下のオプションを用いて、Railsの現在の環境をインデックス名の末尾に追加できます。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch per_environment: true do # インデックス名は"Contact_#{Rails.env}"となる
    attribute :first_name, :last_name, :email
  end
end

属性定義のカスタマイズ

複雑な属性値をブロックで指定できます。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch do
    attribute :email
    attribute :full_name do
      "#{first_name} #{last_name}"
    end
    add_attribute :full_name2
  end

  def full_name2
    "#{first_name} #{last_name}"
  end
end

注意: この種のコードを用いて属性を追加で定義すると、その直後から属性の変更をこのgemで検出不可能になってしまいます(このgemではRailsの#{attribute}_changed?メソッドで変更を検出しています)。その結果、レコードの属性が変更されていない場合にもレコードがAPIにプッシュされます。次のように_changed?メソッドを作成することでこの振る舞いを回避できます。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch do
    attribute :email
    attribute :full_name do
      "#{first_name} #{last_name}"
    end
  end

  def full_name_changed?
    first_name_changed? || last_name_changed?
  end
end

ネステッドオブジェクトやネステッドリレーションについて

リレーションシップの定義

追加の属性を定義し、JSONに沿った任意のオブジェクト(配列、ハッシュ、配列とハッシュの組み合わせのいずれか)を返すネステッドオブジェクトを簡単に埋め込むことができます。

class Profile < ActiveRecord::Base
  include AlgoliaSearch

  belongs_to :user
  has_many :specializations

  algoliasearch do
    attribute :user do
      # ネステッド"user"オブジェクトを`name` + `email`に制限
      { name: user.name, email: user.email }
    end
    attribute :public_specializations do
      # public specializationの配列をビルド(`title`と`another_attr`のみを含む)
      specializations.select { |s| s.public? }.map do |s|
        { title: s.title, another_attr: s.another_attr }
      end
    end
  end

end

ネステッドな子オブジェクトの変更を反映させる

Active Recordの場合

Active Recordでは、touchafter_touchで行います。

# app/models/app.rb
class App < ApplicationRecord
  include AlgoliaSearch

  belongs_to :author, class_name: :User
  after_touch :index!

  algoliasearch do
    attribute :title
    attribute :author do
      author.as_json
    end
  end
end

# app/models/user.rb
class User < ApplicationRecord
  # belongs_to関連付けを使う場合は
  # - `touch: true`を使うこと
  # - `after_save`フックは定義しないこと
  has_many :apps, foreign_key: :author_id

  after_save { apps.each(&:touch) }
end
Sequelの場合

Sequelではtouchプラグインで変更を反映できます。

# app/models/app.rb
class App < Sequel::Model
  include AlgoliaSearch

  many_to_one :author, class: :User

  plugin :timestamps
  plugin :touch

  algoliasearch do
    attribute :title
    attribute :author do
      author.to_hash
    end
  end
end

# app/models/user.rb
class User < Sequel::Model
  one_to_many :apps, key: :author_id

  plugin :timestamps
  # この関連付けは利用不可(これはafter_saveをトリガしない)
  plugin :touch

  # ここでtouchされる必要のある関連付けを定義する
  # 効率はよくないが、after_saveをトリガできるようになる
  def touch_associations
    apps.map(&:touch)
  end

  def touch
    super
    touch_associations
  end
end

カスタムobjectID

objectIDは、デフォルトではそのレコードのidに基づきます。:idオプションを指定すればこの振る舞いを変更できます(ただしuniqフィールドを使うこと)。

class UniqUser < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch id: :uniq_name do
  end
end

制約でデータのサブセットのみをインデックス化する

:ifオプションや:unlessオプションを用いて、レコードのインデックス化に制約を追加できます。

これによって、条件付きインデックス化や、ドキュメントごとのインデックス化ができるようになります。

class Post < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch if: :published?, unless: :deleted? do
  end

  def published?
    # [...]
  end

  def deleted?
    # [...]
  end
end

注意: これらの制約を使うと、インデックスをデータベースと同期するために直ちにaddObjects呼び出しやdeleteObjects呼び出しが実行されるようになります。その場合、ステートレスなgemからはオブジェクトが制約とマッチするかどうかを認識できなくなるか、一切マッチしなくなるので、私たちはADD操作やDELETE操作を送信するよう強制しています。_changed?メソッドを作成することでこの振る舞いを変更できます。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch if: :published do
  end

  def published
    # trueかfalseを返す
  end

  def published_changed?
    # 「published」ステートが変更された場合にのみtrueを返す
  end
end

以下のいずれかの方法で、レコードのサブセットをインデックス化できます。

# will generate batch API calls (recommended)
MyModel.where('updated_at > ?', 10.minutes.ago).reindex!
MyModel.index_objects MyModel.limit(5)

サニタイザ

sanitizeオプションで属性をすべてサニタイズできます。属性に含まれるHTMLタグはすべて取り除かれます。

class User < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch per_environment: true, sanitize: true do
    attributes :name, :email, :company
  end
end

Rails 4.2以降をご利用の場合は、rails-html-sanitizerへの依存も必要です。

gem 'rails-html-sanitizer'

UTF-8エンコーディング

force_utf8_encodingオプションで属性をすべて強制的にUTF-8エンコーディングにできます。

class User < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch force_utf8_encoding: true do
    attributes :name, :email, :company
  end
end

注意: このオプションはRuby 1.8と互換性がありません。

例外処理

raise_on_failureオプションで、Algolia APIへのアクセスを試行中にraiseされる可能性のある例外を無効にできます。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  # development環境でのみ例外をraiseする
  algoliasearch raise_on_failure: Rails.env.development? do
    attribute :first_name, :last_name, :email
  end
end

設定例

以下は、実際に使われている設定例です(HN Searchより)。

class Item < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch per_environment: true do
    # the list of attributes sent to Algolia's API
    attribute :created_at, :title, :url, :author, :points, :story_text, :comment_text, :author, :num_comments, :story_id, :story_title

    # integer version of the created_at datetime field, to use numerical filtering
    attribute :created_at_i do
      created_at.to_i
    end

    # `title` is more important than `{story,comment}_text`, `{story,comment}_text` more than `url`, `url` more than `author`
    # btw, do not take into account position in most fields to avoid first word match boost
    searchableAttributes ['unordered(title)', 'unordered(story_text)', 'unordered(comment_text)', 'unordered(url)', 'author']

    # tags used for filtering
    tags do
      [item_type, "author_#{author}", "story_#{story_id}"]
    end

    # use associated number of HN points to sort results (last sort criteria)
    customRanking ['desc(points)', 'desc(num_comments)']

    # google+, $1.5M raises, C#: we love you
    separatorsToIndex '+#$'
  end

  def story_text
    item_type_cd != Item.comment ? text : nil
  end

  def story_title
    comment? && story ? story.title : nil
  end

  def story_url
    comment? && story ? story.url : nil
  end

  def comment_text
    comment? ? text : nil
  end

  def comment?
    item_type_cd == Item.comment
  end

  # [...]
end

インデックス

手動でのインデックス化

index!インスタンスメソッドでインデクス化をトリガできます。

c = Contact.create!(params[:contact])
c.index!

インデックスからの手動削除

remove_from_index!インスタンスメソッドでインデックスからの削除をトリガできます。

c.remove_from_index!
c.destroy

再インデックス化

このgemでは、全オブジェクトの再インデックス化方法を2とおり提供しています。

アトミックな再インデックス化

reindexクラスメソッドは、該当の全オブジェクトを<INDEX_NAME>.tmpという一時インデックスを作成してから、この一時インデックスを(アトミックにインデックス化完了した)最終インデックスに移動することによって、(削除済みオブジェクトも考慮に入れて)全レコードを再インデックス化します。これは、全コンテンツを再インデックス化する最も安全な方法です。

Contact.reindex

注意: インデックス固有のAPIキーを利用している場合は、<INDEX_NAME><INDEX_NAME>.tmpの両方を許可してください。

警告: このようなアトミックな再インデックス化は、モデルのスコープやフィルタがかかっている状態で行うべきではありません。理由は、この操作によってインデックス全体が置き換わり、フィルタされたオブジェクトだけが残ってしまうためです。例: MyModel.where(...).reindexではなくMyModel.where(...).reindex!とすること(末尾の!は必ず付けること!!!)。

正規の再インデックス化

対象の全オブジェクトを(一時インデックスを使わず、除外されたオブジェクトを削除することもなく)インプレースで再インデックス化するには、reindex!クラスメソッドを使います。

Contact.reindex!

インデックスをクリアする

インデックスをクリアするには、clear_index!クラスメソッドを使います。

Contact.clear_index!

背後のインデックスにアクセスする

indexクラスメソッドを呼び出すことで、背後のindexオブジェクトにアクセスできます。

index = Contact.index
# index.get_settings, index.partial_update_object, ...

primary/replica

add_replicaメソッドを使ってreplicaインデックスを定義できます。primary設定をreplicaで継承したい場合はreplicaのブロックでinherit: trueをお使いください。

class Book < ActiveRecord::Base
  attr_protected

  include AlgoliaSearch

  algoliasearch per_environment: true do
    searchableAttributes [:name, :author, :editor]

    # `author`のみで検索する目的でreplicaインデックスを定義する
    add_replica 'Book_by_author', per_environment: true do
      searchableAttributes [:author]
    end

    # 他はメインブロックと同じで並び順だけカスタマイズした
    # replicaインデックスを定義する
    add_replica 'Book_custom_order', inherit: true, per_environment: true do
      customRanking ['asc(rank)']
    end
  end

end

replicaで検索するには以下のコードを使います。

Book.raw_search 'foo bar', replica: 'Book_by_editor'
# または
Book.search 'foo bar', replica: 'Book_by_editor'

単一のインデックスを共有する

1つのインデックスを複数のモデルで共有するのがよいこともあります。これを実装するには、背後のどのモデルでも決してobjectIDがコンフリクトしないようにする必要が生じます。

class Student < ActiveRecord::Base
  attr_protected

  include AlgoliaSearch

  algoliasearch index_name: 'people', id: :algolia_id do
    # [...]
  end

  private
  def algolia_id
    "student_#{id}" # teacherとstudentのIDがコンフリクトしないようにすること
  end
end

class Teacher < ActiveRecord::Base
  attr_protected

  include AlgoliaSearch

  algoliasearch index_name: 'people', id: :algolia_id do
    # [...]
  end

  private
  def algolia_id
    "teacher_#{id}" # teacherとstudentのIDがコンフリクトしないようにすること
  end
end

注意: 複数のモデルを元にした1つのインデックスを対象とする場合、MyModel.reindexは絶対に使わないでください。使うのはMyModel.reindex!だけです。reindexメソッドは、再インデックス化をアトミックに行う目的で一時インデックスを用います。これが使われると、生成されるインデックスにはモデルの現在のレコードしか含まれなくなってしまいます(他のレコードが再インデックス化されません)。

複数のインデックスを対象に設定する

add_indexメソッドを用いることで、1つのレコードを複数のインデックスでインデックス化できます。

class Book < ActiveRecord::Base
  attr_protected

  include AlgoliaSearch

  PUBLIC_INDEX_NAME  = "Book_#{Rails.env}"
  SECURED_INDEX_NAME = "SecuredBook_#{Rails.env}"

  # すべての本を'SECURED_INDEX_NAME'インデックスに保存する
  algoliasearch index_name: SECURED_INDEX_NAME do
    searchableAttributes [:name, :author]
    # securityをタグに変換する
    tags do
      [released ? 'public' : 'private', premium ? 'premium' : 'standard']
    end

    # publicな(つまりreleasedだがpremiumではない)本を
    # 'PUBLIC_INDEX_NAME'インデックスに保存する
    add_index PUBLIC_INDEX_NAME, if: :public? do
      searchableAttributes [:name, :author]
    end
  end

  private
  def public?
    released && !premium
  end

end

追加のインデックスで検索するには、次のコードを使います。

Book.raw_search 'foo bar', index: 'Book_by_editor'
# or
Book.search 'foo bar', index: 'Book_by_editor'

テスト

テストの注意点

specを実行するために、ALGOLIA_APPLICATION_IDALGOLIA_API_KEYの環境変数を設定してください。テストで作成および削除したインデックスは、productionアカウントでは決して使わないでください

可能なら次のようにdisable_indexingオプションを設定し、API呼び出しでインデックス化操作(追加/更新/削除)をすべて無効にしておきましょう。

class User < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch per_environment: true, disable_indexing: Rails.env.test? do
  end
end

class User < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch per_environment: true, disable_indexing: Proc.new { Rails.env.test? || more_complex_condition } do
  end
end

またはAlgolia API呼び出しをモック(mock)にしてもよいでしょう。私たちは、algolia/webmockを使えるサンプル設定をWebMockで提供しています。

require 'algolia/webmock'

describe 'With a mocked client' do

  before(:each) do
    WebMock.enable!
  end

  it "ここでは一切APIを呼び出してはならない" do
    User.create(name: 'My Indexed User')  # モック化済み(APIは呼び出されない)
    User.search('').should == {}          # モック化済み(APIは呼び出されない)
  end

  after(:each) do
    WebMock.disable!
  end

end

おたより発掘

関連記事

インタビュー: 超高速リアルタイム検索APIサービス「Algolia」の作者が語る高速化の秘訣(翻訳)

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の監修および半分程度を翻訳、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れて更新翻訳中。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好きで、Goで書かれたRubyライクなGoby言語のメンテナーでもある。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ

BPSアドベントカレンダー