This gem let you easily integrate the Algolia Search API to your favorite ORM. It's based on the algoliasearch-client-ruby gem.
You might be interested in the sample Ruby on Rails application providing a typeahead.js
-based auto-completion and Google
-like instant search: algoliasearch-rails-example.
Get started
- Install
- Setup
- Quick Start
- Options
- Configuration example
- Indexing
- Master/Slave
- Target multiple indexes
- Tags
- Search
- Faceting
- Geo-search
- Typeahead UI
- Caveats
- Note on testing
gem install algoliasearch-rails
If you are using Rails 3, add the gem to your Gemfile
:
gem "algoliasearch-rails"
And run:
bundle install
Create a new file config/initializers/algoliasearch.rb
to setup your APPLICATION_ID
and API_KEY
.
AlgoliaSearch.configuration = { application_id: 'YourApplicationID', api_key: 'YourAPIKey' }
We support both will_paginate and kaminari as pagination backend. For example to use :will_paginate
, specify the :pagination_backend
as follow:
AlgoliaSearch.configuration = { application_id: 'YourApplicationID', api_key: 'YourAPIKey', pagination_backend: :will_paginate }
The following code will create a Contact
index and add search capabilities to your Contact
model:
class Contact < ActiveRecord::Base
include AlgoliaSearch
algoliasearch do
attribute :first_name, :last_name, :email
end
end
You can either specify the attributes to send (here we restricted to :first_name, :last_name, :email
) or not (in that case, all attributes are sent).
class Product < ActiveRecord::Base
include AlgoliaSearch
algoliasearch do
# all attributes will be sent
end
end
You can also use the add_attribute
method, to send all model attributes + extra ones:
class Product < ActiveRecord::Base
include AlgoliaSearch
algoliasearch do
# all attributes + extra_attr will be sent
add_attribute :extra_attr
end
def extra_attr
"extra_val"
end
end
We recommend the usage of our JavaScript API Client to perform queries. The JS API client is part of the gem, just require algolia/algoliasearch.min
somewhere in your JavaScript manifest, for example in application.js
if you are using Rails 3.1+:
//= require algolia/algoliasearch.min
A search returns ORM-compliant objects reloading them from your database.
p Contact.search("jon doe")
If you want to retrieve the raw JSON answer from the API, without re-loading the objects from the database, you can use:
p Contact.raw_search("jon doe")
All methods injected by the AlgoliaSearch
include are prefixed by algolia_
and aliased to the associated short names if they aren't already defined.
Contact.algolia_reindex! # <=> Contact.reindex!
Contact.algolia_search("jon doe") # <=> Contact.search("jon doe")
Each time a record is saved; it will be - asynchronously - indexed. On the other hand, each time a record is destroyed, it will be - asynchronously - removed from the index.
You can disable auto-indexing and auto-removing setting the following options:
class Contact < ActiveRecord::Base
include AlgoliaSearch
algoliasearch auto_index: false, auto_remove: false do
attribute :first_name, :last_name, :email
end
end
You can temporary disable auto-indexing using the without_auto_index
scope. This is often used for performance reason.
Contact.delete_all
Contact.without_auto_index do
1.upto(10000) { Contact.create! attributes } # inside the block, auto indexing task will noop
end
Contact.reindex! # will use batch operations
You can force indexing and removing to be synchronous by setting the following option:
class Contact < ActiveRecord::Base
include AlgoliaSearch
algoliasearch synchronous: true do
attribute :first_name, :last_name, :email
end
end
You can force the index name using the following option:
class Contact < ActiveRecord::Base
include AlgoliaSearch
algoliasearch index_name: "MyCustomName" do
attribute :first_name, :last_name, :email
end
end
You can suffix the index name with the current Rails environment using the following option:
class Contact < ActiveRecord::Base
include AlgoliaSearch
algoliasearch per_environment: true do # index name will be "Contact_#{Rails.env}"
attribute :first_name, :last_name, :email
end
end
You can use a block to specify a complex attribute value
class Contact < ActiveRecord::Base
include AlgoliaSearch
algoliasearch do
attribute :email
attribute :full_name do
"#{first_name} #{last_name}"
end
end
end
By default, the objectID
is based on your record's id
. You can change this behavior specifying the :id
option (be sure to use a uniq field).
class UniqUser < ActiveRecord::Base
include AlgoliaSearch
algoliasearch id: :uniq_name do
end
end
You can add constraints controlling if a record must be indexed by using options the :if
or :unless
options.
class Post < ActiveRecord::Base
include AlgoliaSearch
algoliasearch if: :published?, unless: :deleted? do
end
def published?
# [...]
end
def deleted?
# [...]
end
end
Notes: As soon as you use those constraints, deleteObjects
calls will be performed in order to keep the index synced with the DB (The state-less gem doesn't know if the object don't match your constraints anymore or never matched, so we force DELETE operations, even on never-indexed objects).
You can index a subset of your records using either:
# will generate batch API calls (recommended)
MyModel.where('updated_at > ?', 10.minutes.ago).reindex!
or
MyModel.index_objects MyModel.limit(5)
Here is a real-word configuration example (from 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
attributesToIndex ['unordered(title)', 'unordered(story_text)', 'unordered(comment_text)', 'unordered(url)', 'author', 'created_at_i']
# list of attributes to highlight
attributesToHighlight ['title', 'story_text', 'comment_text', 'url', 'story_url', 'author', 'story_title']
# 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)']
# controls the way results are sorted sorting on the following 4 criteria (one after another)
# I removed the 'exact' match critera (improve 1-words query relevance, doesn't fit HNSearch needs)
ranking ['typo', 'proximity', 'attribute', 'custom']
# 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
You can trigger indexing using the index!
instance method.
c = Contact.create!(params[:contact])
c.index!
And trigger index removing using the remove_from_index!
instance method.
c.remove_from_index!
c.destroy
To safely reindex all your records (index to a temporary index + move the temporary index to the current one atomically), use the reindex
class method:
Contact.reindex
To reindex all your records (in place, without deleting out-dated records), use the reindex!
class method:
Contact.reindex!
To clear an index, use the clear_index!
class method:
Contact.clear_index!
You can define slave indexes using the add_slave
method:
class Book < ActiveRecord::Base
attr_protected
include AlgoliaSearch
algoliasearch per_environment: true do
attributesToIndex [:name, :author, :editor]
# define a slave index to search by `author` only
add_slave 'Book_by_author', per_environment: true do
attributesToIndex [:author]
end
# define a slave index to search by `editor` only
add_slave 'Book_by_editor', per_environment: true do
attributesToIndex [:editor]
end
end
end
You can index a record in several indexes using the add_index
method:
class Book < ActiveRecord::Base
attr_protected
include AlgoliaSearch
PUBLIC_INDEX_NAME = "Book_#{Rails.env}"
SECURED_INDEX_NAME = "SecuredBook_#{Rails.env}"
# store all books in index 'SECURED_INDEX_NAME'
algoliasearch index_name: SECURED_INDEX_NAME do
attributesToIndex [:name, :author]
# convert security to tags
tags do
[released ? 'public' : 'private', premium ? 'premium' : 'standard']
end
# store all 'public' (released and not premium) books in index 'PUBLIC_INDEX_NAME'
add_index PUBLIC_INDEX_NAME, if: :public? do
attributesToIndex [:name, :author]
end
end
private
def public?
released && !premium
end
end
Use the tags
method to add tags to your record:
class Contact < ActiveRecord::Base
include AlgoliaSearch
algoliasearch do
tags ['trusted']
end
end
or using dynamical values:
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
At query time, specify { tagFilters: 'tagvalue' }
or { tagFilters: ['tagvalue1', 'tagvalue2'] }
as search parameters to restrict the result set to specific tags.
Notes: We recommend the usage of our JavaScript API Client to perform queries directly from the end-user browser without going through your server.
A search returns ORM-compliant objects reloading them from your database. We recommend the usage of our JavaScript API Client to perform queries to decrease the overall latency and offload your servers.
hits = Contact.search("jon doe")
p hits
p hits.raw_answer # to get the original JSON raw answer
If you want to retrieve the raw JSON answer from the API, without re-loading the objects from the database, you can use:
json_answer = Contact.raw_search("jon doe")
p json_answer
p json_answer['hits']
p json_answer['facets']
Search parameters can be specified either through the index's settings statically in your model or dynamically at search time specifying search parameters as second argument of the search
method:
class Contact < ActiveRecord::Base
include AlgoliaSearch
algoliasearch do
attribute :first_name, :last_name, :email
# default search parameters stored in the index settings
minWordSizeForApprox1 4
minWordSizeForApprox2 8
hitsPerPage 42
end
end
# dynamical search parameters
p Contact.search("jon doe", { :hitsPerPage => 5, :page => 2 })
Facets can be retrieved calling the extra facets
method of the search answer.
class Contact < ActiveRecord::Base
include AlgoliaSearch
algoliasearch do
# [...]
# specify the list of attributes available for faceting
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']
Use the geoloc
method to localize your record:
class Contact < ActiveRecord::Base
include AlgoliaSearch
algoliasearch do
geoloc :lat_attr, :lng_attr
end
end
At query time, specify { aroundLatLng: "37.33, -121.89", aroundRadius: 50000 }
as search parameters to restrict the result set to 50KM around San Jose.
Require algolia/algoliasearch.min
(see algoliasearch-client-js) and algolia/typeahead.jquery.js
somewhere in your JavaScript manifest, for example in application.js
if you are using Rails 3.1+:
//= require algolia/algoliasearch.min
//= require algolia/typeahead.jquery
We recommend the usage of hogan, a JavaScript templating engine from Twitter.
//= require hogan
Turns any input[type="text"]
element into a typeahead, for example:
<input name="email" placeholder="test@example.org" id="user_email" />
<script type="text/javascript">
$(document).ready(function() {
var client = new AlgoliaSearch('YourApplicationID', 'SearchOnlyApplicationKey');
var template = Hogan.compile('{{{_highlightResult.email.value}}} ({{{_highlightResult.first_name.value}}} {{{_highlightResult.last_name.value}}})');
$('input#user_email').typeahead(null, {
source: client.initIndex('<%= Contact.index_name %>').ttAdapter(),
displayKey: 'email',
templates: {
suggestion: function(hit) {
return template.render(hit);
}
}
});
});
</script>
This gem makes intensive use of Rails' callbacks to trigger the indexing tasks. If you're using methods bypassing after_validation
, before_save
or after_save
callbacks, it will not index your changes. For example: update_attribute
doesn't perform validations checks, to perform validations when updating use update_attributes
.
To run the specs, please set the ALGOLIA_APPLICATION_ID
and ALGOLIA_API_KEY
environment variables. Since the tests are creating and removing indexes, DO NOT use your production account.
You may want to disable all indexing (add, update & delete operations) API calls, you can set the disable_indexing
option:
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
Or you may want to mock Algolia's API calls. We provide a WebMock sample configuration that you can use including algolia/webmock
:
require 'algolia/webmock'
describe 'With a mocked client' do
before(:each) do
WebMock.enable!
end
it "shouldn't perform any API calls here" do
User.create(name: 'My Indexed User') # mocked, no API call performed
User.search('').should == {} # mocked, no API call performed
end
after(:each) do
WebMock.disable!
end
end