├── .rspec ├── lib ├── algolia │ ├── version.rb │ ├── error.rb │ ├── webmock.rb │ ├── protocol.rb │ ├── client.rb │ └── index.rb └── algoliasearch.rb ├── .gitignore ├── Gemfile ├── .travis.yml ├── spec ├── mock_spec.rb ├── spec_helper.rb ├── stub_spec.rb └── client_spec.rb ├── Rakefile ├── LICENSE.txt ├── algoliasearch.gemspec ├── ChangeLog ├── Gemfile.lock └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /lib/algolia/version.rb: -------------------------------------------------------------------------------- 1 | module Algolia 2 | VERSION = "1.2.11" 3 | end 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## MAC OS 2 | .DS_Store 3 | 4 | ## TEXTMATE 5 | *.tmproj 6 | tmtags 7 | 8 | ## EMACS 9 | *~ 10 | \#* 11 | .\#* 12 | 13 | ## VIM 14 | *.swp 15 | 16 | ## PROJECT::GENERAL 17 | coverage 18 | rdoc 19 | pkg 20 | .rvmrc 21 | 22 | ## PROJECT::SPECIFIC 23 | Gemfile.lock 24 | spec/integration_spec_conf.rb 25 | data.sqlite3 26 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gem 'httpclient', '~> 2.3' 4 | gem 'json', '>= 1.5.1' 5 | gem 'rubysl', '~> 2.0', :platform => :rbx 6 | 7 | group :development do 8 | gem 'coveralls' 9 | gem 'travis' 10 | gem 'rake' 11 | gem 'rdoc' 12 | end 13 | 14 | group :test do 15 | gem 'rspec', '>= 2.5.0' 16 | gem 'autotest' 17 | gem 'autotest-fsevent' 18 | gem 'redgreen' 19 | gem 'autotest-growl' 20 | gem 'webmock' 21 | gem 'simplecov' 22 | gem 'mime-types', '< 2.0' 23 | end 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | branches: 3 | only: 4 | - master 5 | rvm: 6 | - 1.8.7 7 | - 2.1.0 8 | - 2.0.0 9 | - 1.9.3 10 | - jruby 11 | - rbx-2 12 | install: 13 | - bundle install 14 | env: 15 | global: 16 | - secure: OFKp0J81VrKXfpQZWkwSfsG/knN4tr45yDAGekFy4FKcA6Ra6EqDUzcY1MVrkKq9YRUgLJRfjgqF3Ei0cm2qMo1dy5XUG/fY8upTVdQK2Nm1qfbjnh70+vnYpHidHg9pEI8QROwsM7n19QoT16j1af4UinGFE/HBiJpuXcABuLw= 17 | - secure: Jkc/DnS1knCf0fO3qUxaExoEPEEuTFcmJGP/DkOZ5tnMSro8pjgzXIIzMaQSoRO4B7Hv0o7NzNTqNV6/KgB3jLlsj4dQtjkTnZ21Uk0sah5wZpWyC6QhtuIv1yA2utIsNLygU9WDoyD+V9EwuYrLNone1zSGIclAjlPO4xCYo6E= 18 | -------------------------------------------------------------------------------- /lib/algoliasearch.rb: -------------------------------------------------------------------------------- 1 | ## ---------------------------------------------------------------------- 2 | ## 3 | ## Ruby client for algolia.com 4 | ## A quick library for playing with algolia.com's REST API for object storage. 5 | ## Thanks to Sylvain Utard for the initial version of the library (sylvain.utard@gmail.com) 6 | ## ---------------------------------------------------------------------- 7 | require "rubygems" 8 | require "bundler/setup" 9 | 10 | require 'json' 11 | require 'httpclient' 12 | require 'date' 13 | require 'cgi' 14 | 15 | cwd = Pathname(__FILE__).dirname 16 | $:.unshift(cwd.to_s) unless $:.include?(cwd.to_s) || $:.include?(cwd.expand_path.to_s) 17 | 18 | require 'algolia/index' 19 | -------------------------------------------------------------------------------- /spec/mock_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper')) 2 | 3 | require 'algolia/webmock' 4 | 5 | describe 'With a mocked client' do 6 | 7 | before(:each) do 8 | WebMock.enable! 9 | Thread.current[:algolia_hosts] = nil # reset session objects 10 | end 11 | 12 | it "should add a simple object" do 13 | index = Algolia::Index.new("friends") 14 | index.add_object!({ :name => "John Doe", :email => "john@doe.org" }) 15 | index.search('').should == {} # mocked 16 | index.list_user_keys 17 | index.browse 18 | index.clear 19 | index.delete 20 | end 21 | 22 | after(:each) do 23 | WebMock.disable! 24 | end 25 | 26 | end 27 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | 2 | if ENV['COVERAGE'] 3 | require 'simplecov' 4 | SimpleCov.start 5 | end 6 | 7 | if ENV['TRAVIS'] && Object.const_defined?(:RUBY_ENGINE) && RUBY_ENGINE == "ruby" 8 | require 'coveralls' 9 | Coveralls.wear! 10 | end 11 | 12 | require 'rubygems' 13 | Bundler.setup :test 14 | 15 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 16 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 17 | 18 | require 'algoliasearch' 19 | require 'rspec' 20 | 21 | raise "missing ALGOLIA_APPLICATION_ID or ALGOLIA_API_KEY environment variables" if ENV['ALGOLIA_APPLICATION_ID'].nil? || ENV['ALGOLIA_API_KEY'].nil? 22 | Algolia.init :application_id => ENV['ALGOLIA_APPLICATION_ID'], :api_key => ENV['ALGOLIA_API_KEY'] 23 | 24 | RSpec.configure do |c| 25 | c.mock_with :rspec 26 | end 27 | -------------------------------------------------------------------------------- /lib/algolia/error.rb: -------------------------------------------------------------------------------- 1 | module Algolia 2 | 3 | # Base exception class for errors thrown by the Algolia 4 | # client library. AlgoliaError will be raised by any 5 | # network operation if Algolia.init() has not been called. 6 | class AlgoliaError < StandardError #Exception ... why? A:http://www.skorks.com/2009/09/ruby-exceptions-and-exception-handling/ 7 | end 8 | 9 | # An exception class raised when the REST API returns an error. 10 | # The error code and message will be parsed out of the HTTP response, 11 | # which is also included in the response attribute. 12 | class AlgoliaProtocolError < AlgoliaError 13 | attr_accessor :code 14 | attr_accessor :message 15 | 16 | def initialize(code, message) 17 | self.code = code 18 | self.message = message 19 | super("#{self.code}: #{self.message}") 20 | end 21 | end 22 | 23 | end -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'rubygems' 4 | require 'bundler' 5 | begin 6 | Bundler.setup(:default, :development) 7 | rescue Bundler::BundlerError => e 8 | $stderr.puts e.message 9 | $stderr.puts "Run `bundle install` to install missing gems" 10 | exit e.status_code 11 | end 12 | require 'rake' 13 | 14 | require File.expand_path('../lib/algolia/version', __FILE__) 15 | 16 | require 'rake/testtask' 17 | Rake::TestTask.new(:test) do |test| 18 | test.libs << 'lib' << 'test' 19 | test.pattern = 'test/**/test_*.rb' 20 | test.verbose = true 21 | end 22 | 23 | require 'rdoc/task' 24 | Rake::RDocTask.new do |rdoc| 25 | version = Algolia::VERSION 26 | rdoc.rdoc_dir = 'rdoc' 27 | rdoc.title = "algoliasearch #{version}" 28 | rdoc.rdoc_files.include('README*') 29 | rdoc.rdoc_files.include('lib/**/*.rb') 30 | end 31 | 32 | require 'rspec/core/rake_task' 33 | RSpec::Core::RakeTask.new(:spec) 34 | 35 | task :default => :spec 36 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Algolia 4 | http://www.algolia.com/ 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. -------------------------------------------------------------------------------- /algoliasearch.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | # stub: algoliasearch 1.2.0 ruby lib 6 | 7 | Gem::Specification.new do |s| 8 | s.name = "algoliasearch" 9 | s.version = "1.2.11" 10 | 11 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 12 | s.require_paths = ["lib"] 13 | s.authors = ["Algolia"] 14 | s.date = "2014-08-22" 15 | s.description = "A simple Ruby client for the algolia.com REST API" 16 | s.email = "contact@algolia.com" 17 | s.extra_rdoc_files = [ 18 | "ChangeLog", 19 | "LICENSE.txt", 20 | "README.md" 21 | ] 22 | s.files = [ 23 | ".rspec", 24 | ".travis.yml", 25 | "ChangeLog", 26 | "Gemfile", 27 | "Gemfile.lock", 28 | "LICENSE.txt", 29 | "README.md", 30 | "Rakefile", 31 | "algoliasearch.gemspec", 32 | "contacts.json", 33 | "lib/algolia/client.rb", 34 | "lib/algolia/error.rb", 35 | "lib/algolia/index.rb", 36 | "lib/algolia/protocol.rb", 37 | "lib/algolia/version.rb", 38 | "lib/algolia/webmock.rb", 39 | "lib/algoliasearch.rb", 40 | "resources/ca-bundle.crt", 41 | "spec/client_spec.rb", 42 | "spec/mock_spec.rb", 43 | "spec/spec_helper.rb", 44 | "spec/stub_spec.rb" 45 | ] 46 | s.homepage = "http://github.com/algolia/algoliasearch-client-ruby" 47 | s.licenses = ["MIT"] 48 | s.rubygems_version = "2.2.1" 49 | s.summary = "A simple Ruby client for the algolia.com REST API" 50 | 51 | if s.respond_to? :specification_version then 52 | s.specification_version = 4 53 | 54 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 55 | s.add_runtime_dependency(%q, ["~> 2.3"]) 56 | s.add_runtime_dependency(%q, [">= 1.5.1"]) 57 | s.add_development_dependency "travis" 58 | s.add_development_dependency "rake" 59 | s.add_development_dependency "rdoc" 60 | else 61 | s.add_dependency(%q, ["~> 2.3"]) 62 | s.add_dependency(%q, [">= 1.5.1"]) 63 | end 64 | else 65 | s.add_dependency(%q, ["~> 2.3"]) 66 | s.add_dependency(%q, [">= 1.5.1"]) 67 | end 68 | end 69 | 70 | -------------------------------------------------------------------------------- /spec/stub_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper')) 2 | 3 | require 'webmock' 4 | 5 | describe 'With a rate limited client' do 6 | 7 | before(:each) do 8 | WebMock.enable! 9 | Thread.current[:algolia_hosts] = nil 10 | end 11 | 12 | it "should pass the right headers" do 13 | WebMock.stub_request(:get, %r{https://.*\.algolia\.io/1/indexes/friends\?query=.*}). 14 | with(:headers => {'Content-Type'=>'application/json; charset=utf-8', 'User-Agent'=>"Algolia for Ruby #{Algolia::VERSION}", 'X-Algolia-Api-Key'=>ENV['ALGOLIA_API_KEY'], 'X-Algolia-Application-Id'=>ENV['ALGOLIA_APPLICATION_ID'], 'X-Forwarded-Api-Key'=>'ratelimitapikey', 'X-Forwarded-For'=>'1.2.3.4'}). 15 | to_return(:status => 200, :body => "{ \"hits\": [], \"fakeAttribute\": 1 }", :headers => {}) 16 | Algolia.enable_rate_limit_forward ENV['ALGOLIA_API_KEY'], "1.2.3.4", "ratelimitapikey" 17 | index = Algolia::Index.new("friends") 18 | index.search('foo')['fakeAttribute'].should == 1 19 | index.search('bar')['fakeAttribute'].should == 1 20 | end 21 | 22 | it "should use original headers" do 23 | WebMock.stub_request(:get, %r{https://.*\.algolia\.io/1/indexes/friends\?query=.*}). 24 | with(:headers => {'Content-Type'=>'application/json; charset=utf-8', 'User-Agent'=>"Algolia for Ruby #{Algolia::VERSION}", 'X-Algolia-Api-Key'=>ENV['ALGOLIA_API_KEY'], 'X-Algolia-Application-Id'=>ENV['ALGOLIA_APPLICATION_ID'] }). 25 | to_return(:status => 200, :body => "{ \"hits\": [], \"fakeAttribute\": 2 }", :headers => {}) 26 | Algolia.disable_rate_limit_forward 27 | index = Algolia::Index.new("friends") 28 | index.search('bar')['fakeAttribute'].should == 2 29 | end 30 | 31 | it "should pass the right headers in the scope" do 32 | WebMock.stub_request(:get, %r{https://.*\.algolia\.io/1/indexes/friends\?query=.*}). 33 | with(:headers => {'Content-Type'=>'application/json; charset=utf-8', 'User-Agent'=>"Algolia for Ruby #{Algolia::VERSION}", 'X-Algolia-Api-Key'=>ENV['ALGOLIA_API_KEY'], 'X-Algolia-Application-Id'=>ENV['ALGOLIA_APPLICATION_ID'], 'X-Forwarded-Api-Key'=>'ratelimitapikey', 'X-Forwarded-For'=>'1.2.3.4'}). 34 | to_return(:status => 200, :body => "{ \"hits\": [], \"fakeAttribute\": 1 }", :headers => {}) 35 | Algolia.with_rate_limits "1.2.3.4", "ratelimitapikey" do 36 | index = Algolia::Index.new("friends") 37 | index.search('foo')['fakeAttribute'].should == 1 38 | index.search('bar')['fakeAttribute'].should == 1 39 | end 40 | end 41 | 42 | after(:each) do 43 | WebMock.disable! 44 | end 45 | 46 | end 47 | -------------------------------------------------------------------------------- /lib/algolia/webmock.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require 'webmock' 3 | rescue LoadError 4 | puts 'WebMock was not found, please add "gem \'webmock\'" to your Gemfile.' 5 | exit 1 6 | end 7 | 8 | # disable by default 9 | WebMock.disable! 10 | 11 | # list indexes 12 | WebMock.stub_request(:get, /.*\.algolia\.io\/1\/indexes/).to_return(:body => '{ "items": [] }') 13 | # query index 14 | WebMock.stub_request(:get, /.*\.algolia\.io\/1\/indexes\/[^\/]+/).to_return(:body => '{}') 15 | WebMock.stub_request(:post, /.*\.algolia\.io\/1\/indexes\/[^\/]+\/query/).to_return(:body => '{}') 16 | # delete index 17 | WebMock.stub_request(:delete, /.*\.algolia\.io\/1\/indexes\/[^\/]+/).to_return(:body => '{ "taskID": 42 }') 18 | # clear index 19 | WebMock.stub_request(:post, /.*\.algolia\.io\/1\/indexes\/[^\/]+\/clear/).to_return(:body => '{ "taskID": 42 }') 20 | # add object 21 | WebMock.stub_request(:post, /.*\.algolia\.io\/1\/indexes\/[^\/]+/).to_return(:body => '{ "taskID": 42 }') 22 | # save object 23 | WebMock.stub_request(:put, /.*\.algolia\.io\/1\/indexes\/[^\/]+\/[^\/]+/).to_return(:body => '{ "taskID": 42 }') 24 | # partial update 25 | WebMock.stub_request(:put, /.*\.algolia\.io\/1\/indexes\/[^\/]+\/[^\/]+\/partial/).to_return(:body => '{ "taskID": 42 }') 26 | # get object 27 | WebMock.stub_request(:get, /.*\.algolia\.io\/1\/indexes\/[^\/]+\/[^\/]+/).to_return(:body => '{}') 28 | # delete object 29 | WebMock.stub_request(:delete, /.*\.algolia\.io\/1\/indexes\/[^\/]+\/[^\/]+/).to_return(:body => '{ "taskID": 42 }') 30 | # batch 31 | WebMock.stub_request(:post, /.*\.algolia\.io\/1\/indexes\/[^\/]+\/batch/).to_return(:body => '{ "taskID": 42 }') 32 | # settings 33 | WebMock.stub_request(:get, /.*\.algolia\.io\/1\/indexes\/[^\/]+\/settings/).to_return(:body => '{}') 34 | WebMock.stub_request(:put, /.*\.algolia\.io\/1\/indexes\/[^\/]+\/settings/).to_return(:body => '{ "taskID": 42 }') 35 | # browse 36 | WebMock.stub_request(:get, /.*\.algolia\.io\/1\/indexes\/[^\/]+\/browse/).to_return(:body => '{}') 37 | # operations 38 | WebMock.stub_request(:post, /.*\.algolia\.io\/1\/indexes\/[^\/]+\/operation/).to_return(:body => '{ "taskID": 42 }') 39 | # tasks 40 | WebMock.stub_request(:get, /.*\.algolia\.io\/1\/indexes\/[^\/]+\/task\/[^\/]+/).to_return(:body => '{ "status": "published" }') 41 | # index keys 42 | WebMock.stub_request(:post, /.*\.algolia\.io\/1\/indexes\/[^\/]+\/keys/).to_return(:body => '{ }') 43 | WebMock.stub_request(:get, /.*\.algolia\.io\/1\/indexes\/[^\/]+\/keys/).to_return(:body => '{ "keys": [] }') 44 | # global keys 45 | WebMock.stub_request(:post, /.*\.algolia\.io\/1\/keys/).to_return(:body => '{ }') 46 | WebMock.stub_request(:get, /.*\.algolia\.io\/1\/keys/).to_return(:body => '{ "keys": [] }') 47 | WebMock.stub_request(:get, /.*\.algolia\.io\/1\/keys\/[^\/]+/).to_return(:body => '{ }') 48 | WebMock.stub_request(:delete, /.*\.algolia\.io\/1\/keys\/[^\/]+/).to_return(:body => '{ }') 49 | -------------------------------------------------------------------------------- /lib/algolia/protocol.rb: -------------------------------------------------------------------------------- 1 | require 'cgi' 2 | 3 | module Algolia 4 | # A module which encapsulates the specifics of Algolia's REST API. 5 | module Protocol 6 | 7 | # Basics 8 | 9 | # The version of the REST API implemented by this module. 10 | VERSION = 1 11 | 12 | # HTTP Headers 13 | # ---------------------------------------- 14 | 15 | # The HTTP header used for passing your application ID to the 16 | # Algolia API. 17 | HEADER_APP_ID = "X-Algolia-Application-Id" 18 | 19 | # The HTTP header used for passing your API key to the 20 | # Algolia API. 21 | HEADER_API_KEY = "X-Algolia-API-Key" 22 | 23 | HEADER_FORWARDED_IP = "X-Forwarded-For" 24 | 25 | HEADER_FORWARDED_API_KEY = "X-Forwarded-API-Key" 26 | 27 | # HTTP ERROR CODES 28 | # ---------------------------------------- 29 | 30 | ERROR_TIMEOUT = 124 31 | ERROR_UNAVAILABLE = 503 32 | 33 | # URI Helpers 34 | # ---------------------------------------- 35 | 36 | # Construct a uri to list available indexes 37 | def Protocol.indexes_uri 38 | "/#{VERSION}/indexes" 39 | end 40 | 41 | def Protocol.multiple_queries_uri 42 | "/#{VERSION}/indexes/*/queries" 43 | end 44 | 45 | def Protocol.objects_uri 46 | "/#{VERSION}/indexes/*/objects" 47 | end 48 | 49 | # Construct a uri referencing a given Algolia index 50 | def Protocol.index_uri(index) 51 | "/#{VERSION}/indexes/#{CGI.escape(index)}" 52 | end 53 | 54 | def Protocol.batch_uri(index) 55 | "#{index_uri(index)}/batch" 56 | end 57 | 58 | def Protocol.index_operation_uri(index) 59 | "#{index_uri(index)}/operation" 60 | end 61 | 62 | def Protocol.task_uri(index, task_id) 63 | "#{index_uri(index)}/task/#{task_id}" 64 | end 65 | 66 | def Protocol.object_uri(index, object_id, params = {}) 67 | params = params.nil? || params.size == 0 ? "" : "?#{to_query(params)}" 68 | "#{index_uri(index)}/#{CGI.escape(object_id.to_s)}#{params}" 69 | end 70 | 71 | def Protocol.search_uri(index, query, params = {}) 72 | params = params.nil? || params.size == 0 ? "" : "&#{to_query(params)}" 73 | "#{index_uri(index)}?query=#{CGI.escape(query)}&#{params}" 74 | end 75 | 76 | def Protocol.browse_uri(index, params = {}) 77 | params = params.nil? || params.size == 0 ? "" : "?#{to_query(params)}" 78 | "#{index_uri(index)}/browse#{params}" 79 | end 80 | 81 | def Protocol.partial_object_uri(index, object_id) 82 | "#{index_uri(index)}/#{CGI.escape(object_id)}/partial" 83 | end 84 | 85 | def Protocol.settings_uri(index) 86 | "#{index_uri(index)}/settings" 87 | end 88 | 89 | def Protocol.clear_uri(index) 90 | "#{index_uri(index)}/clear" 91 | end 92 | 93 | def Protocol.logs(offset, length, only_errors = false) 94 | "/#{VERSION}/logs?offset=#{offset}&length=#{length}&onlyErrors=#{only_errors}" 95 | end 96 | 97 | def Protocol.keys_uri 98 | "/#{VERSION}/keys" 99 | end 100 | 101 | def Protocol.key_uri(key) 102 | "/#{VERSION}/keys/#{key}" 103 | end 104 | 105 | def Protocol.index_key_uri(index, key) 106 | "#{index_uri(index)}/keys/#{key}" 107 | end 108 | 109 | def Protocol.index_keys_uri(index) 110 | "#{index_uri(index)}/keys" 111 | end 112 | 113 | def Protocol.to_query(params) 114 | params.map do |k, v| 115 | "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" 116 | end.join('&') 117 | end 118 | 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | 3 | 2014-08-22 1.2.10 4 | 5 | * Using Digest to remove "Digest::Digest is deprecated; Use Digest" warning (author: @dglancy) 6 | 7 | 2014-07-10 1.2.9 8 | 9 | * Expose connect_timeout, receive_timeout and send_timeout 10 | * Add new 'delete_by_query' method to delete all objects matching a specific query 11 | * Add new 'get_objects' method to retrieve a list of objects from a single API call 12 | * Add a helper to perform disjunctive faceting 13 | 14 | 2014-03-27 1.2.8 15 | 16 | * Catch all exceptions before retrying with another host 17 | 18 | 2014-03-24 1.2.7 19 | 20 | * Ruby 1.8 compatibility 21 | 22 | 2014-03-19 1.2.6 23 | 24 | * Raise an exception if no APPLICATION_ID is provided 25 | * Ability to get last API call errors 26 | * Ability to send multiple queries using a single API call 27 | * Secured API keys generation is now based on secured HMAC-SHA256 28 | 29 | 2014-02-24 1.2.5 30 | 31 | * Ability to generate secured API key from a list of tags + optional user_token 32 | * Ability to specify a list of indexes targeted by the user key 33 | 34 | 2014-02-21 1.2.4 35 | 36 | * Added delete_objects 37 | 38 | 2014-02-10 1.2.3 39 | 40 | * add_object: POST request if objectID is nil OR empty 41 | 42 | 2014-01-11 1.2.2 43 | 44 | * Expose batch requests 45 | 46 | 2014-01-07 1.2.1 47 | 48 | * Removed 'jeweler' since it doesn't support platform specific deps (see https://github.com/technicalpickles/jeweler/issues/170) 49 | 50 | 2014-01-07 1.2.0 51 | 52 | * Removed 'curb' dependency and switched on 'httpclient' to avoid fork-safety issue (see issue #5) 53 | 54 | 2014-01-06 1.1.18 55 | 56 | * Fixed batch request builder (broken since last refactoring) 57 | 58 | 2014-01-02 1.1.17 59 | 60 | * Ability to use IP rate limit behind a proxy forwarding the end-user's IP 61 | * Add documentation for the new 'attributeForDistinct' setting and 'distinct' search parameter 62 | 63 | 2013-12-16 1.1.16 64 | 65 | * Add arguments type-checking 66 | * Normalize save_object/partial_update/add_object signatures 67 | * Force dependencies versions 68 | 69 | 2013-12-16 1.1.15 70 | 71 | * Embed ca-bundle.crt 72 | 73 | 2013-12-11 1.1.14 74 | 75 | * Added index.add_user_key(acls, validity, rate_limit, maxApiCalls) 76 | 77 | 2013-12-10 1.1.13 78 | 79 | * WebMock integration 80 | 81 | 2013-12-05 1.1.12 82 | 83 | * Add browse command 84 | 85 | 2013-11-29 1.1.11 86 | 87 | * Remove rubysl (rbx required dependencies) 88 | 89 | 2013-11-29 1.1.10 90 | 91 | * Fixed gzip handling bug 92 | 93 | 2013-11-28 1.1.9 94 | 95 | * Added gzip support 96 | 97 | 2013-11-26 1.1.8 98 | 99 | * Add partial_update_objects method 100 | 101 | 2013-11-08 1.1.7 102 | 103 | * Search params: encode array-based parameters (tagFilters, facetFilters, ...) 104 | 105 | 2013-11-07 1.1.6 106 | 107 | * Index: clear and clear! methods can now be used the delete the whole content of an index 108 | * User keys: plug new maxQueriesPerIPPerHour and maxHitsPerQuery parameters 109 | 110 | 2013-10-17 1.1.5 111 | 112 | * Version is now part of the user-agent 113 | 114 | 2013-10-17 1.1.4 115 | 116 | * Fixed wait_task not sleeping at all 117 | 118 | 2013-10-15 1.1.3 119 | 120 | * Fixed thread-safety issues. - Curl sessions are now thread-local 121 | 122 | 2013-10-02 1.1.2 123 | 124 | * Fixed instance/class method conflict 125 | 126 | 2013-10-01 1.1.1 127 | 128 | * Updated documentation 129 | * Plug copy/move index 130 | 131 | 2013-09-17 1.1.0 132 | 133 | * Initial import 134 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | ZenTest (4.10.0) 5 | addressable (2.3.6) 6 | autotest (4.4.6) 7 | ZenTest (>= 4.4.1) 8 | autotest-fsevent (0.2.9) 9 | sys-uname 10 | autotest-growl (0.2.16) 11 | backports (3.6.0) 12 | coderay (1.1.0) 13 | coveralls (0.7.0) 14 | multi_json (~> 1.3) 15 | rest-client 16 | simplecov (>= 0.7) 17 | term-ansicolor 18 | thor 19 | crack (0.4.2) 20 | safe_yaml (~> 1.0.0) 21 | diff-lcs (1.2.5) 22 | docile (1.1.3) 23 | ethon (0.7.0) 24 | ffi (>= 1.3.0) 25 | faraday (0.9.0) 26 | multipart-post (>= 1.2, < 3) 27 | faraday_middleware (0.9.1) 28 | faraday (>= 0.7.4, < 0.10) 29 | ffi (1.9.3) 30 | ffi (1.9.3-java) 31 | ffi2-generators (0.1.1) 32 | gh (0.13.2) 33 | addressable 34 | backports 35 | faraday (~> 0.8) 36 | multi_json (~> 1.0) 37 | net-http-persistent (>= 2.7) 38 | net-http-pipeline 39 | highline (1.6.21) 40 | httpclient (2.3.4.1) 41 | json (1.8.1) 42 | json (1.8.1-java) 43 | launchy (2.4.2) 44 | addressable (~> 2.3) 45 | launchy (2.4.2-java) 46 | addressable (~> 2.3) 47 | spoon (~> 0.0.1) 48 | method_source (0.8.2) 49 | mime-types (1.25.1) 50 | multi_json (1.10.1) 51 | multipart-post (2.0.0) 52 | net-http-persistent (2.9.4) 53 | net-http-pipeline (1.0.1) 54 | pry (0.9.12.6) 55 | coderay (~> 1.0) 56 | method_source (~> 0.8) 57 | slop (~> 3.4) 58 | pry (0.9.12.6-java) 59 | coderay (~> 1.0) 60 | method_source (~> 0.8) 61 | slop (~> 3.4) 62 | spoon (~> 0.0) 63 | pusher-client (0.6.0) 64 | json 65 | websocket (~> 1.0) 66 | rake (10.3.2) 67 | rdoc (4.1.1) 68 | json (~> 1.4) 69 | redgreen (1.2.2) 70 | rest-client (1.6.7) 71 | mime-types (>= 1.16) 72 | rspec (3.0.0) 73 | rspec-core (~> 3.0.0) 74 | rspec-expectations (~> 3.0.0) 75 | rspec-mocks (~> 3.0.0) 76 | rspec-core (3.0.0) 77 | rspec-support (~> 3.0.0) 78 | rspec-expectations (3.0.0) 79 | diff-lcs (>= 1.2.0, < 2.0) 80 | rspec-support (~> 3.0.0) 81 | rspec-mocks (3.0.0) 82 | rspec-support (~> 3.0.0) 83 | rspec-support (3.0.0) 84 | rubysl (2.0.15) 85 | rubysl-abbrev (~> 2.0) 86 | rubysl-base64 (~> 2.0) 87 | rubysl-benchmark (~> 2.0) 88 | rubysl-bigdecimal (~> 2.0) 89 | rubysl-cgi (~> 2.0) 90 | rubysl-cgi-session (~> 2.0) 91 | rubysl-cmath (~> 2.0) 92 | rubysl-complex (~> 2.0) 93 | rubysl-continuation (~> 2.0) 94 | rubysl-coverage (~> 2.0) 95 | rubysl-csv (~> 2.0) 96 | rubysl-curses (~> 2.0) 97 | rubysl-date (~> 2.0) 98 | rubysl-delegate (~> 2.0) 99 | rubysl-digest (~> 2.0) 100 | rubysl-drb (~> 2.0) 101 | rubysl-e2mmap (~> 2.0) 102 | rubysl-english (~> 2.0) 103 | rubysl-enumerator (~> 2.0) 104 | rubysl-erb (~> 2.0) 105 | rubysl-etc (~> 2.0) 106 | rubysl-expect (~> 2.0) 107 | rubysl-fcntl (~> 2.0) 108 | rubysl-fiber (~> 2.0) 109 | rubysl-fileutils (~> 2.0) 110 | rubysl-find (~> 2.0) 111 | rubysl-forwardable (~> 2.0) 112 | rubysl-getoptlong (~> 2.0) 113 | rubysl-gserver (~> 2.0) 114 | rubysl-io-console (~> 2.0) 115 | rubysl-io-nonblock (~> 2.0) 116 | rubysl-io-wait (~> 2.0) 117 | rubysl-ipaddr (~> 2.0) 118 | rubysl-irb (~> 2.0) 119 | rubysl-logger (~> 2.0) 120 | rubysl-mathn (~> 2.0) 121 | rubysl-matrix (~> 2.0) 122 | rubysl-mkmf (~> 2.0) 123 | rubysl-monitor (~> 2.0) 124 | rubysl-mutex_m (~> 2.0) 125 | rubysl-net-ftp (~> 2.0) 126 | rubysl-net-http (~> 2.0) 127 | rubysl-net-imap (~> 2.0) 128 | rubysl-net-pop (~> 2.0) 129 | rubysl-net-protocol (~> 2.0) 130 | rubysl-net-smtp (~> 2.0) 131 | rubysl-net-telnet (~> 2.0) 132 | rubysl-nkf (~> 2.0) 133 | rubysl-observer (~> 2.0) 134 | rubysl-open-uri (~> 2.0) 135 | rubysl-open3 (~> 2.0) 136 | rubysl-openssl (~> 2.0) 137 | rubysl-optparse (~> 2.0) 138 | rubysl-ostruct (~> 2.0) 139 | rubysl-pathname (~> 2.0) 140 | rubysl-prettyprint (~> 2.0) 141 | rubysl-prime (~> 2.0) 142 | rubysl-profile (~> 2.0) 143 | rubysl-profiler (~> 2.0) 144 | rubysl-pstore (~> 2.0) 145 | rubysl-pty (~> 2.0) 146 | rubysl-rational (~> 2.0) 147 | rubysl-readline (~> 2.0) 148 | rubysl-resolv (~> 2.0) 149 | rubysl-rexml (~> 2.0) 150 | rubysl-rinda (~> 2.0) 151 | rubysl-rss (~> 2.0) 152 | rubysl-scanf (~> 2.0) 153 | rubysl-securerandom (~> 2.0) 154 | rubysl-set (~> 2.0) 155 | rubysl-shellwords (~> 2.0) 156 | rubysl-singleton (~> 2.0) 157 | rubysl-socket (~> 2.0) 158 | rubysl-stringio (~> 2.0) 159 | rubysl-strscan (~> 2.0) 160 | rubysl-sync (~> 2.0) 161 | rubysl-syslog (~> 2.0) 162 | rubysl-tempfile (~> 2.0) 163 | rubysl-thread (~> 2.0) 164 | rubysl-thwait (~> 2.0) 165 | rubysl-time (~> 2.0) 166 | rubysl-timeout (~> 2.0) 167 | rubysl-tmpdir (~> 2.0) 168 | rubysl-tsort (~> 2.0) 169 | rubysl-un (~> 2.0) 170 | rubysl-uri (~> 2.0) 171 | rubysl-weakref (~> 2.0) 172 | rubysl-webrick (~> 2.0) 173 | rubysl-xmlrpc (~> 2.0) 174 | rubysl-yaml (~> 2.0) 175 | rubysl-zlib (~> 2.0) 176 | rubysl-abbrev (2.0.4) 177 | rubysl-base64 (2.0.0) 178 | rubysl-benchmark (2.0.1) 179 | rubysl-bigdecimal (2.0.2) 180 | rubysl-cgi (2.0.1) 181 | rubysl-cgi-session (2.0.1) 182 | rubysl-cmath (2.0.0) 183 | rubysl-complex (2.0.0) 184 | rubysl-continuation (2.0.0) 185 | rubysl-coverage (2.0.3) 186 | rubysl-csv (2.0.2) 187 | rubysl-english (~> 2.0) 188 | rubysl-curses (2.0.1) 189 | rubysl-date (2.0.6) 190 | rubysl-delegate (2.0.1) 191 | rubysl-digest (2.0.3) 192 | rubysl-drb (2.0.1) 193 | rubysl-e2mmap (2.0.0) 194 | rubysl-english (2.0.0) 195 | rubysl-enumerator (2.0.0) 196 | rubysl-erb (2.0.1) 197 | rubysl-etc (2.0.3) 198 | ffi2-generators (~> 0.1) 199 | rubysl-expect (2.0.0) 200 | rubysl-fcntl (2.0.4) 201 | ffi2-generators (~> 0.1) 202 | rubysl-fiber (2.0.0) 203 | rubysl-fileutils (2.0.3) 204 | rubysl-find (2.0.1) 205 | rubysl-forwardable (2.0.1) 206 | rubysl-getoptlong (2.0.0) 207 | rubysl-gserver (2.0.0) 208 | rubysl-socket (~> 2.0) 209 | rubysl-thread (~> 2.0) 210 | rubysl-io-console (2.0.0) 211 | rubysl-io-nonblock (2.0.0) 212 | rubysl-io-wait (2.0.0) 213 | rubysl-ipaddr (2.0.0) 214 | rubysl-irb (2.0.4) 215 | rubysl-e2mmap (~> 2.0) 216 | rubysl-mathn (~> 2.0) 217 | rubysl-readline (~> 2.0) 218 | rubysl-thread (~> 2.0) 219 | rubysl-logger (2.0.0) 220 | rubysl-mathn (2.0.0) 221 | rubysl-matrix (2.1.0) 222 | rubysl-e2mmap (~> 2.0) 223 | rubysl-mkmf (2.0.1) 224 | rubysl-fileutils (~> 2.0) 225 | rubysl-shellwords (~> 2.0) 226 | rubysl-monitor (2.0.0) 227 | rubysl-mutex_m (2.0.0) 228 | rubysl-net-ftp (2.0.1) 229 | rubysl-net-http (2.0.4) 230 | rubysl-cgi (~> 2.0) 231 | rubysl-erb (~> 2.0) 232 | rubysl-singleton (~> 2.0) 233 | rubysl-net-imap (2.0.1) 234 | rubysl-net-pop (2.0.1) 235 | rubysl-net-protocol (2.0.1) 236 | rubysl-net-smtp (2.0.1) 237 | rubysl-net-telnet (2.0.0) 238 | rubysl-nkf (2.0.1) 239 | rubysl-observer (2.0.0) 240 | rubysl-open-uri (2.0.0) 241 | rubysl-open3 (2.0.0) 242 | rubysl-openssl (2.1.0) 243 | rubysl-optparse (2.0.1) 244 | rubysl-shellwords (~> 2.0) 245 | rubysl-ostruct (2.0.4) 246 | rubysl-pathname (2.0.0) 247 | rubysl-prettyprint (2.0.3) 248 | rubysl-prime (2.0.1) 249 | rubysl-profile (2.0.0) 250 | rubysl-profiler (2.0.1) 251 | rubysl-pstore (2.0.0) 252 | rubysl-pty (2.0.2) 253 | rubysl-rational (2.0.1) 254 | rubysl-readline (2.0.2) 255 | rubysl-resolv (2.1.0) 256 | rubysl-rexml (2.0.2) 257 | rubysl-rinda (2.0.1) 258 | rubysl-rss (2.0.0) 259 | rubysl-scanf (2.0.0) 260 | rubysl-securerandom (2.0.0) 261 | rubysl-set (2.0.1) 262 | rubysl-shellwords (2.0.0) 263 | rubysl-singleton (2.0.0) 264 | rubysl-socket (2.0.1) 265 | rubysl-stringio (2.0.0) 266 | rubysl-strscan (2.0.0) 267 | rubysl-sync (2.0.0) 268 | rubysl-syslog (2.0.1) 269 | ffi2-generators (~> 0.1) 270 | rubysl-tempfile (2.0.1) 271 | rubysl-thread (2.0.2) 272 | rubysl-thwait (2.0.0) 273 | rubysl-time (2.0.3) 274 | rubysl-timeout (2.0.0) 275 | rubysl-tmpdir (2.0.1) 276 | rubysl-tsort (2.0.1) 277 | rubysl-un (2.0.0) 278 | rubysl-fileutils (~> 2.0) 279 | rubysl-optparse (~> 2.0) 280 | rubysl-uri (2.0.0) 281 | rubysl-weakref (2.0.0) 282 | rubysl-webrick (2.0.0) 283 | rubysl-xmlrpc (2.0.0) 284 | rubysl-yaml (2.0.4) 285 | rubysl-zlib (2.0.1) 286 | safe_yaml (1.0.3) 287 | simplecov (0.8.2) 288 | docile (~> 1.1.0) 289 | multi_json 290 | simplecov-html (~> 0.8.0) 291 | simplecov-html (0.8.0) 292 | slop (3.5.0) 293 | spoon (0.0.4) 294 | ffi 295 | sys-uname (0.9.2) 296 | ffi (>= 1.0.0) 297 | term-ansicolor (1.3.0) 298 | tins (~> 1.0) 299 | thor (0.19.1) 300 | tins (1.3.0) 301 | travis (1.6.11) 302 | addressable (~> 2.3) 303 | backports 304 | faraday (~> 0.9) 305 | faraday_middleware (~> 0.9) 306 | gh (~> 0.13) 307 | highline (~> 1.6) 308 | launchy (~> 2.1) 309 | pry (~> 0.9) 310 | pusher-client (~> 0.4) 311 | typhoeus (~> 0.6, >= 0.6.8) 312 | typhoeus (0.6.8) 313 | ethon (>= 0.7.0) 314 | webmock (1.18.0) 315 | addressable (>= 2.3.6) 316 | crack (>= 0.3.2) 317 | websocket (1.1.4) 318 | 319 | PLATFORMS 320 | java 321 | ruby 322 | 323 | DEPENDENCIES 324 | autotest 325 | autotest-fsevent 326 | autotest-growl 327 | coveralls 328 | httpclient (~> 2.3) 329 | json (>= 1.5.1) 330 | mime-types (< 2.0) 331 | rake 332 | rdoc 333 | redgreen 334 | rspec (>= 2.5.0) 335 | rubysl (~> 2.0) 336 | simplecov 337 | travis 338 | webmock 339 | -------------------------------------------------------------------------------- /lib/algolia/client.rb: -------------------------------------------------------------------------------- 1 | require 'algolia/protocol' 2 | require 'algolia/error' 3 | require 'algolia/version' 4 | require 'json' 5 | require 'zlib' 6 | require 'openssl' 7 | 8 | module Algolia 9 | 10 | # A class which encapsulates the HTTPS communication with the Algolia 11 | # API server. Uses the HTTPClient library for low-level HTTP communication. 12 | class Client 13 | attr_reader :ssl, :hosts, :application_id, :api_key, :headers, :connect_timeout, :send_timeout, :receive_timeout, :search_timeout 14 | 15 | 16 | def initialize(data = {}) 17 | @ssl = data[:ssl].nil? ? true : data[:ssl] 18 | @application_id = data[:application_id] 19 | @api_key = data[:api_key] 20 | @hosts = (data[:hosts] || 1.upto(3).map { |i| "#{@application_id}-#{i}.algolia.io" }).shuffle 21 | @connect_timeout = data[:connect_timeout] 22 | @send_timeout = data[:send_timeout] 23 | @receive_timeout = data[:receive_timeout] 24 | @search_timeout = data[:search_timeout] 25 | @headers = { 26 | Protocol::HEADER_API_KEY => api_key, 27 | Protocol::HEADER_APP_ID => application_id, 28 | 'Content-Type' => 'application/json; charset=utf-8', 29 | 'User-Agent' => "Algolia for Ruby #{::Algolia::VERSION}" 30 | } 31 | end 32 | 33 | # Perform an HTTP request for the given uri and method 34 | # with common basic response handling. Will raise a 35 | # AlgoliaProtocolError if the response has an error status code, 36 | # and will return the parsed JSON body on success, if there is one. 37 | def request(uri, method, data = nil, timeout = nil) 38 | exceptions = [] 39 | thread_local_hosts.each do |host| 40 | begin 41 | return perform_request(host[:session], host[:base_url] + uri, method, data, timeout) 42 | rescue AlgoliaProtocolError => e 43 | raise if e.code != Protocol::ERROR_TIMEOUT and e.code != Protocol::ERROR_UNAVAILABLE 44 | exceptions << e 45 | rescue => e 46 | exceptions << e 47 | end 48 | end 49 | raise AlgoliaProtocolError.new(0, "Cannot reach any host: #{exceptions.map { |e| e.to_s }.join(', ')}") 50 | end 51 | 52 | def get(uri, timeout = nil) 53 | request(uri, :GET, nil, timeout) 54 | end 55 | 56 | def post(uri, body = {}, timeout = nil) 57 | request(uri, :POST, body, timeout) 58 | end 59 | 60 | def put(uri, body = {}, timeout = nil) 61 | request(uri, :PUT, body) 62 | end 63 | 64 | def delete(uri, timeout = nil) 65 | request(uri, :DELETE) 66 | end 67 | 68 | private 69 | 70 | # this method returns a thread-local array of sessions 71 | def thread_local_hosts 72 | Thread.current[:algolia_hosts] ||= hosts.map do |host| 73 | hinfo = { 74 | :base_url => "http#{@ssl ? 's' : ''}://#{host}", 75 | :session => HTTPClient.new 76 | } 77 | hinfo[:session].transparent_gzip_decompression = true 78 | hinfo[:session].connect_timeout = @connect_timeout if @connect_timeout 79 | hinfo[:session].send_timeout = @send_timeout if @send_timeout 80 | hinfo[:session].receive_timeout = @receive_timeout if @receive_timeout 81 | hinfo[:session].ssl_config.add_trust_ca File.join(File.dirname(__FILE__), '..', '..', 'resources', 'ca-bundle.crt') 82 | hinfo 83 | end 84 | end 85 | 86 | private 87 | def perform_request(session, url, method, data, timeout) 88 | original_send_timeout = session.send_timeout 89 | original_receive_timeout = session.receive_timeout 90 | begin 91 | session.send_timeout = session.receive_timeout = timeout if timeout 92 | response = case method 93 | when :GET 94 | session.get(url, { :header => @headers }) 95 | when :POST 96 | session.post(url, { :body => data, :header => @headers }) 97 | when :PUT 98 | session.put(url, { :body => data, :header => @headers }) 99 | when :DELETE 100 | session.delete(url, { :header => @headers }) 101 | end 102 | if response.code >= 400 || response.code < 200 103 | raise AlgoliaProtocolError.new(response.code, "Cannot #{method} to #{url}: #{response.content} (#{response.code})") 104 | end 105 | return JSON.parse(response.content) 106 | ensure 107 | session.send_timeout = original_send_timeout 108 | session.receive_timeout = original_receive_timeout 109 | end 110 | end 111 | 112 | end 113 | 114 | # Module methods 115 | # ------------------------------------------------------------ 116 | 117 | # A singleton client 118 | # Always use Algolia.client to retrieve the client object. 119 | @@client = nil 120 | 121 | # Initialize the singleton instance of Client which is used 122 | # by all API methods. 123 | def Algolia.init(options = {}) 124 | application_id = ENV["ALGOLIA_API_ID"] || ENV["ALGOLIA_APPLICATION_ID"] 125 | api_key = ENV["ALGOLIA_REST_API_KEY"] || ENV['ALGOLIA_API_KEY'] 126 | 127 | defaulted = { :application_id => application_id, :api_key => api_key } 128 | defaulted.merge!(options) 129 | 130 | raise ArgumentError.new("No APPLICATION_ID provided, please set :application_id") if defaulted[:application_id].nil? 131 | 132 | @@client = Client.new(defaulted) 133 | end 134 | 135 | # 136 | # Allow to use IP rate limit when you have a proxy between end-user and Algolia. 137 | # This option will set the X-Forwarded-For HTTP header with the client IP and the X-Forwarded-API-Key with the API Key having rate limits. 138 | # @param admin_api_key the admin API Key you can find in your dashboard 139 | # @param end_user_ip the end user IP (you can use both IPV4 or IPV6 syntax) 140 | # @param rate_limit_api_key the API key on which you have a rate limit 141 | # 142 | def Algolia.enable_rate_limit_forward(admin_api_key, end_user_ip, rate_limit_api_key) 143 | Algolia.client.headers[Protocol::HEADER_API_KEY] = admin_api_key 144 | Algolia.client.headers[Protocol::HEADER_FORWARDED_IP] = end_user_ip 145 | Algolia.client.headers[Protocol::HEADER_FORWARDED_API_KEY] = rate_limit_api_key 146 | end 147 | 148 | # 149 | # Disable IP rate limit enabled with enableRateLimitForward() function 150 | # 151 | def Algolia.disable_rate_limit_forward 152 | Algolia.client.headers[Protocol::HEADER_API_KEY] = Algolia.client.api_key 153 | Algolia.client.headers.delete(Protocol::HEADER_FORWARDED_IP) 154 | Algolia.client.headers.delete(Protocol::HEADER_FORWARDED_API_KEY) 155 | end 156 | 157 | # 158 | # Convenience method thats wraps enable_rate_limit_forward/disable_rate_limit_forward 159 | # 160 | def Algolia.with_rate_limits(end_user_ip, rate_limit_api_key, &block) 161 | Algolia.enable_rate_limit_forward(Algolia.client.api_key, end_user_ip, rate_limit_api_key) 162 | begin 163 | yield 164 | ensure 165 | Algolia.disable_rate_limit_forward 166 | end 167 | end 168 | 169 | # 170 | # Generate a secured and public API Key from a list of tagFilters and an 171 | # optional user token identifying the current user 172 | # 173 | # @param private_api_key your private API Key 174 | # @param tag_filters the list of tags applied to the query (used as security) 175 | # @param user_token an optional token identifying the current user 176 | # 177 | def Algolia.generate_secured_api_key(private_api_key, tag_filters, user_token = nil) 178 | if tag_filters.is_a?(Array) 179 | tag_filters = tag_filters.map { |t| t.is_a?(Array) ? "(#{t.join(',')})" : t }.join(',') 180 | end 181 | raise ArgumentError.new('Attribute "tag_filters" must be a list of tags') if !tag_filters.is_a?(String) 182 | OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), private_api_key, "#{tag_filters}#{user_token.to_s}") 183 | end 184 | 185 | # 186 | # This method allows to query multiple indexes with one API call 187 | # 188 | # @param queries the array of hash representing the query and associated index name 189 | # @param index_name_key the name of the key used to fetch the index_name (:index_name by default) 190 | # 191 | def Algolia.multiple_queries(queries, index_name_key = :index_name) 192 | requests = { 193 | :requests => queries.map do |query| 194 | indexName = query.delete(index_name_key) || query.delete(index_name_key.to_s) 195 | encoded_params = Hash[query.map { |k,v| [k.to_s, v.is_a?(Array) ? v.to_json : v] }] 196 | { :indexName => indexName, :params => Protocol.to_query(encoded_params) } 197 | end 198 | } 199 | Algolia.client.post(Protocol.multiple_queries_uri, requests.to_json, Algolia.client.search_timeout) 200 | end 201 | 202 | # 203 | # List all existing indexes 204 | # return an Answer object with answer in the form 205 | # {"items": [{ "name": "contacts", "createdAt": "2013-01-18T15:33:13.556Z"}, 206 | # {"name": "notes", "createdAt": "2013-01-18T15:33:13.556Z"}]} 207 | # 208 | def Algolia.list_indexes 209 | Algolia.client.get(Protocol.indexes_uri) 210 | end 211 | 212 | # 213 | # Move an existing index. 214 | # @param src_index the name of index to copy. 215 | # @param dst_index the new index name that will contains a copy of srcIndexName (destination will be overriten if it already exist). 216 | # 217 | def Algolia.move_index(src_index, dst_index) 218 | request = {"operation" => "move", "destination" => dst_index}; 219 | Algolia.client.post(Protocol.index_operation_uri(src_index), request.to_json) 220 | end 221 | 222 | # 223 | # Move an existing index and wait until the move has been processed 224 | # @param src_index the name of index to copy. 225 | # @param dst_index the new index name that will contains a copy of srcIndexName (destination will be overriten if it already exist). 226 | # 227 | def Algolia.move_index!(src_index, dst_index) 228 | res = Algolia.move_index(src_index, dst_index) 229 | Index.new(dst_index).wait_task(res['taskID']) 230 | res 231 | end 232 | 233 | # 234 | # Copy an existing index. 235 | # @param src_index the name of index to copy. 236 | # @param dst_index the new index name that will contains a copy of srcIndexName (destination will be overriten if it already exist). 237 | # 238 | def Algolia.copy_index(src_index, dst_index) 239 | request = {"operation" => "copy", "destination" => dst_index}; 240 | Algolia.client.post(Protocol.index_operation_uri(src_index), request.to_json) 241 | end 242 | 243 | # 244 | # Copy an existing index and wait until the copy has been processed. 245 | # @param src_index the name of index to copy. 246 | # @param dst_index the new index name that will contains a copy of srcIndexName (destination will be overriten if it already exist). 247 | # 248 | def Algolia.copy_index!(src_index, dst_index) 249 | res = Algolia.copy_index(src_index, dst_index) 250 | Index.new(dst_index).wait_task(res['taskID']) 251 | res 252 | end 253 | 254 | # Delete an index 255 | # 256 | def delete_index(name) 257 | Index.new(name).delete 258 | end 259 | 260 | # Delete an index and wait until the deletion has been processed. 261 | # 262 | def delete_index!(name) 263 | Index.new(name).delete! 264 | end 265 | 266 | # 267 | # Return last logs entries. 268 | # 269 | # @param offset Specify the first entry to retrieve (0-based, 0 is the most recent log entry). 270 | # @param length Specify the maximum number of entries to retrieve starting at offset. Maximum allowed value: 1000. 271 | # 272 | def Algolia.get_logs(offset = 0, length = 10, only_errors = false) 273 | Algolia.client.get(Protocol.logs(offset, length, only_errors)) 274 | end 275 | 276 | # List all existing user keys with their associated ACLs 277 | def Algolia.list_user_keys 278 | Algolia.client.get(Protocol.keys_uri) 279 | end 280 | 281 | # Get ACL of a user key 282 | def Algolia.get_user_key(key) 283 | Algolia.client.get(Protocol.key_uri(key)) 284 | end 285 | 286 | # 287 | # Create a new user key 288 | # 289 | # @param acls the list of ACL for this key. Defined by an array of strings that 290 | # can contains the following values: 291 | # - search: allow to search (https and http) 292 | # - addObject: allows to add a new object in the index (https only) 293 | # - updateObject : allows to change content of an existing object (https only) 294 | # - deleteObject : allows to delete an existing object (https only) 295 | # - deleteIndex : allows to delete index content (https only) 296 | # - settings : allows to get index settings (https only) 297 | # - editSettings : allows to change index settings (https only) 298 | # @param validity the number of seconds after which the key will be automatically removed (0 means no time limit for this key) 299 | # @param maxQueriesPerIPPerHour the maximum number of API calls allowed from an IP address per hour (0 means unlimited) 300 | # @param maxHitsPerQuery the maximum number of hits this API key can retrieve in one call (0 means unlimited) 301 | # @param indexes the optional list of targeted indexes 302 | # 303 | def Algolia.add_user_key(acls, validity = 0, maxQueriesPerIPPerHour = 0, maxHitsPerQuery = 0, indexes = nil) 304 | params = { 305 | :acl => acls, 306 | :validity => validity.to_i, 307 | :maxQueriesPerIPPerHour => maxQueriesPerIPPerHour.to_i, 308 | :maxHitsPerQuery => maxHitsPerQuery.to_i 309 | } 310 | params[:indexes] = indexes if indexes 311 | Algolia.client.post(Protocol.keys_uri, params.to_json) 312 | end 313 | 314 | # 315 | # Update a user key 316 | # 317 | # @param acls the list of ACL for this key. Defined by an array of strings that 318 | # can contains the following values: 319 | # - search: allow to search (https and http) 320 | # - addObject: allows to add a new object in the index (https only) 321 | # - updateObject : allows to change content of an existing object (https only) 322 | # - deleteObject : allows to delete an existing object (https only) 323 | # - deleteIndex : allows to delete index content (https only) 324 | # - settings : allows to get index settings (https only) 325 | # - editSettings : allows to change index settings (https only) 326 | # @param validity the number of seconds after which the key will be automatically removed (0 means no time limit for this key) 327 | # @param maxQueriesPerIPPerHour the maximum number of API calls allowed from an IP address per hour (0 means unlimited) 328 | # @param maxHitsPerQuery the maximum number of hits this API key can retrieve in one call (0 means unlimited) 329 | # @param indexes the optional list of targeted indexes 330 | # 331 | def Algolia.update_user_key(key, acls, validity = 0, maxQueriesPerIPPerHour = 0, maxHitsPerQuery = 0, indexes = nil) 332 | params = { 333 | :acl => acls, 334 | :validity => validity.to_i, 335 | :maxQueriesPerIPPerHour => maxQueriesPerIPPerHour.to_i, 336 | :maxHitsPerQuery => maxHitsPerQuery.to_i 337 | } 338 | params[:indexes] = indexes if indexes 339 | Algolia.client.put(Protocol.key_uri(key), params.to_json) 340 | end 341 | 342 | 343 | # Delete an existing user key 344 | def Algolia.delete_user_key(key) 345 | Algolia.client.delete(Protocol.key_uri(key)) 346 | end 347 | 348 | # Used mostly for testing. Lets you delete the api key global vars. 349 | def Algolia.destroy 350 | @@client = nil 351 | self 352 | end 353 | 354 | def Algolia.client 355 | if !@@client 356 | raise AlgoliaError, "API not initialized" 357 | end 358 | @@client 359 | end 360 | 361 | end 362 | -------------------------------------------------------------------------------- /spec/client_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper')) 3 | 4 | # avoid concurrent access to the same index 5 | def safe_index_name(name) 6 | return name if ENV['TRAVIS'].to_s != "true" 7 | id = ENV['TRAVIS_JOB_NUMBER'].split('.').last 8 | "#{name}_travis-#{id}" 9 | end 10 | 11 | def is_include(array, attr, value) 12 | array.each do |elt| 13 | if elt[attr] == value 14 | return true 15 | end 16 | end 17 | return false 18 | end 19 | 20 | describe 'Client' do 21 | before(:all) do 22 | @index = Algolia::Index.new(safe_index_name("àlgol?a")) 23 | @index.delete_index rescue "not fatal" 24 | end 25 | 26 | after(:all) do 27 | @index.delete_index rescue "not fatal" 28 | end 29 | 30 | it "should add a simple object" do 31 | @index.add_object!({ :name => "John Doe", :email => "john@doe.org" }, "1") 32 | res = @index.search("john") 33 | res["hits"].length.should eq(1) 34 | end 35 | 36 | it "should partial update a simple object" do 37 | @index.add_object!({ :name => "John Doe", :email => "john@doe.org" }, "1") 38 | res = @index.search("john") 39 | res["hits"].length.should eq(1) 40 | @index.partial_update_object!({ :name => "Robert Doe"}, "1") 41 | res = @index.search("robert") 42 | res["hits"].length.should eq(1) 43 | end 44 | 45 | it "should add a set of objects" do 46 | @index.add_objects!([ 47 | { :name => "Another", :email => "another1@example.org" }, 48 | { :name => "Another", :email => "another2@example.org" } 49 | ]) 50 | res = @index.search("another") 51 | res["hits"].length.should eq(2) 52 | end 53 | 54 | it "should partial update a simple object" do 55 | @index.add_object!({ :name => "John Doe", :email => "john@doe.org" }, "1") 56 | @index.add_object!({ :name => "John Doe", :email => "john@doe.org" }, "2") 57 | res = @index.search("john") 58 | res["hits"].length.should eq(2) 59 | @index.partial_update_objects!([{ :name => "Robert Doe", :objectID => "1"}, { :name => "Robert Doe", :objectID => "2"}]) 60 | res = @index.search("robert") 61 | res["hits"].length.should eq(2) 62 | end 63 | 64 | it "should save a set of objects with their ids" do 65 | @index.save_object!({ :name => "objectid", :email => "objectid1@example.org", :objectID => 101 }) 66 | res = @index.search("objectid") 67 | res["hits"].length.should eq(1) 68 | end 69 | 70 | it "should save a set of objects with their ids" do 71 | @index.save_objects!([ 72 | { :name => "objectid", :email => "objectid1@example.org", :objectID => 101 }, 73 | { :name => "objectid", :email => "objectid2@example.org", :objectID => 102 } 74 | ]) 75 | res = @index.search("objectid") 76 | res["hits"].length.should eq(2) 77 | end 78 | 79 | it "should throw an exception if invalid argument" do 80 | expect { @index.add_object!([ {:name => "test"} ]) }.to raise_error(ArgumentError) 81 | expect { @index.add_objects!([ [ {:name => "test"} ] ]) }.to raise_error(ArgumentError) 82 | expect { @index.save_object(1) }.to raise_error(ArgumentError) 83 | expect { @index.save_object("test") }.to raise_error(ArgumentError) 84 | expect { @index.save_object({ :objectID => 42 }.to_json) }.to raise_error(ArgumentError) 85 | expect { @index.save_objects([{}, ""]) }.to raise_error(ArgumentError) 86 | expect { @index.save_objects([1]) }.to raise_error(ArgumentError) 87 | expect { @index.save_objects!([1]) }.to raise_error(ArgumentError) 88 | expect { @index.save_object({ :foo => 42 }) }.to raise_error(ArgumentError) # missing objectID 89 | end 90 | 91 | it "should be thread safe" do 92 | threads = [] 93 | 64.times do 94 | t = Thread.new do 95 | 10.times do 96 | res = @index.search("john") 97 | res["hits"].length.should eq(2) 98 | end 99 | end 100 | threads << t 101 | end 102 | threads.each { |t| t.join } 103 | end 104 | 105 | if !defined?(RUBY_ENGINE) || RUBY_ENGINE != 'jruby' 106 | it "should be fork safe" do 107 | 8.times do 108 | Process.fork do 109 | 10.times do 110 | res = @index.search("john") 111 | res["hits"].length.should eq(2) 112 | end 113 | end 114 | end 115 | Process.waitall 116 | end 117 | end 118 | 119 | it "should clear the index" do 120 | @index.clear! 121 | @index.search("")["hits"].length.should eq(0) 122 | end 123 | 124 | it "should have another index after" do 125 | index = Algolia::Index.new(safe_index_name("àlgol?a")) 126 | begin 127 | index.delete_index! 128 | rescue 129 | # friends_2 does not exist 130 | end 131 | res = Algolia.list_indexes 132 | is_include(res['items'], 'name', safe_index_name('àlgol?a')).should eq(false) 133 | index.add_object!({ :name => "Robert" }) 134 | resAfter = Algolia.list_indexes; 135 | is_include(resAfter['items'], 'name', safe_index_name('àlgol?a')).should eq(true) 136 | end 137 | 138 | it "should get a object" do 139 | @index.clear_index 140 | @index.add_object!({:firstname => "Robert"}) 141 | @index.add_object!({:firstname => "Robert2"}) 142 | res = @index.search('') 143 | res["nbHits"].should eq(2) 144 | object = @index.get_object(res['hits'][0]['objectID']) 145 | object['firstname'].should eq(res['hits'][0]['firstname']) 146 | 147 | object = @index.get_object(res['hits'][0]['objectID'], 'firstname') 148 | object['firstname'].should eq(res['hits'][0]['firstname']) 149 | 150 | objects = @index.get_objects([ res['hits'][0]['objectID'], res['hits'][1]['objectID'] ]) 151 | objects.size.should eq(2) 152 | end 153 | 154 | it "should delete the object" do 155 | @index.clear 156 | @index.add_object!({:firstname => "Robert"}) 157 | res = @index.search('') 158 | @index.search('')['nbHits'].should eq(1) 159 | @index.delete_object!(res['hits'][0]['objectID']) 160 | @index.search('')['nbHits'].should eq(0) 161 | end 162 | 163 | it "should delete several objects" do 164 | @index.clear 165 | @index.add_object!({:firstname => "Robert1"}) 166 | @index.add_object!({:firstname => "Robert2"}) 167 | res = @index.search('') 168 | @index.search('')['nbHits'].should eq(2) 169 | @index.delete_objects!(res['hits'].map { |h| h['objectID'] }) 170 | @index.search('')['nbHits'].should eq(0) 171 | end 172 | 173 | it "should delete several objects by query" do 174 | @index.clear 175 | @index.add_object({:firstname => "Robert1"}) 176 | @index.add_object!({:firstname => "Robert2"}) 177 | @index.search('')['nbHits'].should eq(2) 178 | @index.delete_by_query('rob') 179 | @index.search('')['nbHits'].should eq(0) 180 | end 181 | 182 | it "should copy the index" do 183 | index = Algolia::Index.new(safe_index_name("àlgol?à")) 184 | begin 185 | @index.clear_index 186 | Algolia.delete_index index.name 187 | rescue 188 | # friends_2 does not exist 189 | end 190 | 191 | @index.add_object!({:firstname => "Robert"}) 192 | @index.search('')['nbHits'].should eq(1) 193 | 194 | Algolia.copy_index!(safe_index_name("àlgol?a"), safe_index_name("àlgol?à")) 195 | @index.delete_index! 196 | 197 | index.search('')['nbHits'].should eq(1) 198 | index.delete_index! 199 | end 200 | 201 | it "should move the index" do 202 | @index.clear_index rescue "friends does not exist" 203 | index = Algolia::Index.new(safe_index_name("àlgol?à")) 204 | begin 205 | Algolia.delete_index! index.name 206 | rescue 207 | # friends_2 does not exist 208 | end 209 | 210 | @index.add_object!({:firstname => "Robert"}) 211 | @index.search('')['nbHits'].should eq(1) 212 | 213 | Algolia.move_index!(safe_index_name("àlgol?a"), safe_index_name("àlgol?à")) 214 | 215 | index.search('')['nbHits'].should eq(1) 216 | index.delete_index 217 | end 218 | 219 | it "should retrieve the object" do 220 | @index.clear_index rescue "friends does not exist" 221 | @index.add_object!({:firstname => "Robert"}) 222 | 223 | res = @index.browse 224 | 225 | res['hits'].size.should eq(1) 226 | res['hits'][0]['firstname'].should eq("Robert") 227 | end 228 | 229 | it "should get logs" do 230 | res = Algolia.get_logs 231 | 232 | res['logs'].size.should > 0 233 | end 234 | 235 | it "should search on multipleIndex" do 236 | @index.clear_index! rescue "Not fatal" 237 | @index.add_object!({ :name => "John Doe", :email => "john@doe.org" }, "1") 238 | res = Algolia.multiple_queries([{:index_name => safe_index_name("àlgol?a"), "query" => ""}]) 239 | res["results"][0]["hits"].length.should eq(1) 240 | 241 | res = Algolia.multiple_queries([{"indexName" => safe_index_name("àlgol?a"), "query" => ""}], "indexName") 242 | res["results"][0]["hits"].length.should eq(1) 243 | end 244 | 245 | it "shoud accept custom batch" do 246 | @index.clear_index! rescue "Not fatal" 247 | request = { "requests" => [ 248 | { 249 | "action" => "addObject", 250 | "body" => {"firstname" => "Jimmie", 251 | "lastname" => "Barninger"} 252 | }, 253 | { 254 | "action" => "addObject", 255 | "body" => {"firstname" => "Warren", 256 | "lastname" => "Speach"} 257 | }, 258 | { 259 | "action" => "updateObject", 260 | "body" => {"firstname" => "Jimmie", 261 | "lastname" => "Barninger", 262 | "objectID" => "43"} 263 | }, 264 | { 265 | "action" => "updateObject", 266 | "body" => {"firstname" => "Warren", 267 | "lastname" => "Speach"}, 268 | "objectID" => "42" 269 | } 270 | ]} 271 | res = @index.batch!(request) 272 | @index.search('')['nbHits'].should eq(4) 273 | end 274 | 275 | it "should allow an array of tags" do 276 | @index.add_object!({ :name => "P1", :_tags => "t1" }) 277 | @index.add_object!({ :name => "P2", :_tags => "t1" }) 278 | @index.add_object!({ :name => "P3", :_tags => "t2" }) 279 | @index.add_object!({ :name => "P4", :_tags => "t3" }) 280 | @index.add_object!({ :name => "P5", :_tags => ["t3", "t4"] }) 281 | 282 | @index.search("", { :tagFilters => ["t1"] })['hits'].length.should eq(2) # t1 283 | @index.search("", { :tagFilters => ["t1", "t2"] })['hits'].length.should eq(0) # t1 AND t2 284 | @index.search("", { :tagFilters => ["t3", "t4"] })['hits'].length.should eq(1) # t3 AND t4 285 | @index.search("", { :tagFilters => [["t1", "t2"]] })['hits'].length.should eq(3) # t1 OR t2 286 | end 287 | 288 | it "should be facetable" do 289 | @index.clear! 290 | @index.set_settings( { :attributesForFacetting => ["f", "g"] }) 291 | @index.add_object!({ :name => "P1", :f => "f1", :g => "g1" }) 292 | @index.add_object!({ :name => "P2", :f => "f1", :g => "g2" }) 293 | @index.add_object!({ :name => "P3", :f => "f2", :g => "g2" }) 294 | @index.add_object!({ :name => "P4", :f => "f3", :g => "g2" }) 295 | 296 | res = @index.search("", { :facets => "f" }) 297 | res['facets']['f']['f1'].should eq(2) 298 | res['facets']['f']['f2'].should eq(1) 299 | res['facets']['f']['f3'].should eq(1) 300 | 301 | res = @index.search("", { :facets => "f", :facetFilters => ["f:f1"] }) 302 | res['facets']['f']['f1'].should eq(2) 303 | res['facets']['f']['f2'].should be_nil 304 | res['facets']['f']['f3'].should be_nil 305 | 306 | res = @index.search("", { :facets => "f", :facetFilters => ["f:f1", "g:g2"] }) 307 | res['facets']['f']['f1'].should eq(1) 308 | res['facets']['f']['f2'].should be_nil 309 | res['facets']['f']['f3'].should be_nil 310 | 311 | res = @index.search("", { :facets => "f,g", :facetFilters => [["f:f1", "g:g2"]] }) 312 | res['nbHits'].should eq(4) 313 | res['facets']['f']['f1'].should eq(2) 314 | res['facets']['f']['f2'].should eq(1) 315 | res['facets']['f']['f3'].should eq(1) 316 | 317 | res = @index.search("", { :facets => "f,g", :facetFilters => [["f:f1", "g:g2"], "g:g1"] }) 318 | res['nbHits'].should eq(1) 319 | res['facets']['f']['f1'].should eq(1) 320 | res['facets']['f']['f2'].should be_nil 321 | res['facets']['f']['f3'].should be_nil 322 | res['facets']['g']['g1'].should eq(1) 323 | res['facets']['g']['g2'].should be_nil 324 | end 325 | 326 | it "should test keys" do 327 | resIndex = @index.list_user_keys 328 | newIndexKey = @index.add_user_key(['search']) 329 | newIndexKey['key'].should_not eq("") 330 | sleep 2 # no task ID here 331 | resIndexAfter = @index.list_user_keys 332 | is_include(resIndex['keys'], 'value', newIndexKey['key']).should eq(false) 333 | is_include(resIndexAfter['keys'], 'value', newIndexKey['key']).should eq(true) 334 | indexKey = @index.get_user_key(newIndexKey['key']) 335 | indexKey['acl'][0].should eq('search') 336 | @index.update_user_key(newIndexKey['key'], ['addObject']) 337 | sleep 2 # no task ID here 338 | indexKey = @index.get_user_key(newIndexKey['key']) 339 | indexKey['acl'][0].should eq('addObject') 340 | @index.delete_user_key(newIndexKey['key']) 341 | sleep 2 # no task ID here 342 | resIndexEnd = @index.list_user_keys 343 | is_include(resIndexEnd['keys'], 'value', newIndexKey['key']).should eq(false) 344 | 345 | 346 | res = Algolia.list_user_keys 347 | newKey = Algolia.add_user_key(['search']) 348 | newKey['key'].should_not eq("") 349 | sleep 2 # no task ID here 350 | resAfter = Algolia.list_user_keys 351 | is_include(res['keys'], 'value', newKey['key']).should eq(false) 352 | is_include(resAfter['keys'], 'value', newKey['key']).should eq(true) 353 | key = Algolia.get_user_key(newKey['key']) 354 | key['acl'][0].should eq('search') 355 | Algolia.update_user_key(newKey['key'], ['addObject']) 356 | sleep 2 # no task ID here 357 | key = Algolia.get_user_key(newKey['key']) 358 | key['acl'][0].should eq('addObject') 359 | Algolia.delete_user_key(newKey['key']) 360 | sleep 2 # no task ID here 361 | resEnd = Algolia.list_user_keys 362 | is_include(resEnd['keys'], 'value', newKey['key']).should eq(false) 363 | 364 | 365 | end 366 | 367 | it "should check functions" do 368 | @index.get_settings 369 | @index.list_user_keys 370 | Algolia.list_user_keys 371 | 372 | end 373 | 374 | it "should handle slash in objectId" do 375 | @index.clear_index!() 376 | @index.add_object!({:firstname => "Robert", :objectID => "A/go/?a"}) 377 | res = @index.search('') 378 | @index.search("")["nbHits"].should eq(1) 379 | object = @index.get_object(res['hits'][0]['objectID']) 380 | object['firstname'].should eq('Robert') 381 | object = @index.get_object(res['hits'][0]['objectID'], 'firstname') 382 | object['firstname'].should eq('Robert') 383 | 384 | @index.save_object!({:firstname => "George", :objectID => "A/go/?a"}) 385 | res = @index.search('') 386 | @index.search("")["nbHits"].should eq(1) 387 | object = @index.get_object(res['hits'][0]['objectID']) 388 | object['firstname'].should eq('George') 389 | 390 | @index.partial_update_object!({:firstname => "Sylvain", :objectID => "A/go/?a"}) 391 | res = @index.search('') 392 | @index.search("")["nbHits"].should eq(1) 393 | object = @index.get_object(res['hits'][0]['objectID']) 394 | object['firstname'].should eq('Sylvain') 395 | 396 | end 397 | 398 | it "Check attributes list_indexes:" do 399 | res = Algolia::Index.all 400 | res.should have_key('items') 401 | res['items'][0].should have_key('name') 402 | res['items'][0]['name'].should be_a(String) 403 | res['items'][0].should have_key('createdAt') 404 | res['items'][0]['createdAt'].should be_a(String) 405 | res['items'][0].should have_key('updatedAt') 406 | res['items'][0]['updatedAt'].should be_a(String) 407 | res['items'][0].should have_key('entries') 408 | res['items'][0]['entries'].should be_a(Integer) 409 | res['items'][0].should have_key('pendingTask') 410 | [true, false].should include(res['items'][0]['pendingTask']) 411 | end 412 | 413 | it 'Check attributes search : ' do 414 | res = @index.search('') 415 | res.should have_key('hits') 416 | res['hits'].should be_a(Array) 417 | res.should have_key('page') 418 | res['page'].should be_a(Integer) 419 | res.should have_key('nbHits') 420 | res['nbHits'].should be_a(Integer) 421 | res.should have_key('nbPages') 422 | res['nbPages'].should be_a(Integer) 423 | res.should have_key('hitsPerPage') 424 | res['hitsPerPage'].should be_a(Integer) 425 | res.should have_key('processingTimeMS') 426 | res['processingTimeMS'].should be_a(Integer) 427 | res.should have_key('query') 428 | res['query'].should be_a(String) 429 | res.should have_key('params') 430 | res['params'].should be_a(String) 431 | end 432 | 433 | it 'Check attributes delete_index : ' do 434 | index = Algolia::Index.new(safe_index_name("àlgol?à2")) 435 | index.add_object!({ :name => "John Doe", :email => "john@doe.org" }, "1") 436 | task = index.delete_index() 437 | task.should have_key('deletedAt') 438 | task['deletedAt'].should be_a(String) 439 | task.should have_key('taskID') 440 | task['taskID'].should be_a(Integer) 441 | end 442 | 443 | it 'Check attributes clear_index : ' do 444 | task = @index.clear_index 445 | task.should have_key('updatedAt') 446 | task['updatedAt'].should be_a(String) 447 | task.should have_key('taskID') 448 | task['taskID'].should be_a(Integer) 449 | end 450 | 451 | it 'Check attributes add object : ' do 452 | task = @index.add_object({ :name => "John Doe", :email => "john@doe.org" }) 453 | task.should have_key('createdAt') 454 | task['createdAt'].should be_a(String) 455 | task.should have_key('taskID') 456 | task['taskID'].should be_a(Integer) 457 | task.should have_key('objectID') 458 | task['objectID'].should be_a(String) 459 | end 460 | 461 | it 'Check attributes add object id: ' do 462 | task = @index.add_object({ :name => "John Doe", :email => "john@doe.org" }, "1") 463 | task.should have_key('updatedAt') 464 | task['updatedAt'].should be_a(String) 465 | task.should have_key('taskID') 466 | task['taskID'].should be_a(Integer) 467 | #task.to_s.should eq("") 468 | task.should have_key('objectID') 469 | task['objectID'].should be_a(String) 470 | task['objectID'].should eq("1") 471 | end 472 | 473 | it 'Check attributes partial update: ' do 474 | task = @index.partial_update_object({ :name => "John Doe", :email => "john@doe.org" }, "1") 475 | task.should have_key('updatedAt') 476 | task['updatedAt'].should be_a(String) 477 | task.should have_key('taskID') 478 | task['taskID'].should be_a(Integer) 479 | task.should have_key('objectID') 480 | task['objectID'].should be_a(String) 481 | task['objectID'].should eq("1") 482 | end 483 | 484 | it 'Check attributes delete object: ' do 485 | @index.add_object({ :name => "John Doe", :email => "john@doe.org" }, "1") 486 | task = @index.delete_object("1") 487 | task.should have_key('deletedAt') 488 | task['deletedAt'].should be_a(String) 489 | task.should have_key('taskID') 490 | task['taskID'].should be_a(Integer) 491 | end 492 | 493 | it 'Check attributes add objects: ' do 494 | task = @index.add_objects([{ :name => "John Doe", :email => "john@doe.org", :objectID => "1" }]) 495 | task.should have_key('taskID') 496 | task['taskID'].should be_a(Integer) 497 | task.should have_key('objectIDs') 498 | task['objectIDs'].should be_a(Array) 499 | end 500 | 501 | it 'Check attributes browse: ' do 502 | res = @index.browse() 503 | res.should have_key('hits') 504 | res['hits'].should be_a(Array) 505 | res.should have_key('page') 506 | res['page'].should be_a(Integer) 507 | res.should have_key('nbHits') 508 | res['nbHits'].should be_a(Integer) 509 | res.should have_key('nbPages') 510 | res['nbPages'].should be_a(Integer) 511 | res.should have_key('hitsPerPage') 512 | res['hitsPerPage'].should be_a(Integer) 513 | res.should have_key('processingTimeMS') 514 | res['processingTimeMS'].should be_a(Integer) 515 | res.should have_key('query') 516 | res['query'].should be_a(String) 517 | res.should have_key('params') 518 | res['params'].should be_a(String) 519 | end 520 | 521 | it 'Check attributes get settings: ' do 522 | task = @index.set_settings({}) 523 | task.should have_key('taskID') 524 | task['taskID'].should be_a(Integer) 525 | task.should have_key('updatedAt') 526 | task['updatedAt'].should be_a(String) 527 | end 528 | 529 | it 'Check attributes move_index : ' do 530 | index = Algolia::Index.new(safe_index_name("àlgol?à")) 531 | index2 = Algolia::Index.new(safe_index_name("àlgol?à2")) 532 | index2.add_object!({ :name => "John Doe", :email => "john@doe.org" }, "1") 533 | task = Algolia.move_index!(safe_index_name("àlgol?à2"), safe_index_name("àlgol?à")) 534 | task.should have_key('updatedAt') 535 | task['updatedAt'].should be_a(String) 536 | task.should have_key('taskID') 537 | task['taskID'].should be_a(Integer) 538 | index.delete_index 539 | end 540 | 541 | it 'Check attributes copy_index : ' do 542 | index = Algolia::Index.new(safe_index_name("àlgol?à")) 543 | index2 = Algolia::Index.new(safe_index_name("àlgol?à2")) 544 | index2.add_object!({ :name => "John Doe", :email => "john@doe.org" }, "1") 545 | task = Algolia.copy_index!(safe_index_name("àlgol?à2"), safe_index_name("àlgol?à")) 546 | task.should have_key('updatedAt') 547 | task['updatedAt'].should be_a(String) 548 | task.should have_key('taskID') 549 | task['taskID'].should be_a(Integer) 550 | index.delete_index 551 | index2.delete_index 552 | end 553 | 554 | it 'Check attributes wait_task : ' do 555 | task = @index.add_object!({ :name => "John Doe", :email => "john@doe.org" }, "1") 556 | task = Algolia.client.get(Algolia::Protocol.task_uri(safe_index_name("àlgol?a"), task['objectID'])) 557 | task.should have_key('status') 558 | task['status'].should be_a(String) 559 | task.should have_key('pendingTask') 560 | [true, false].should include(task['pendingTask']) 561 | end 562 | 563 | it "Check add keys" do 564 | newIndexKey = @index.add_user_key(['search']) 565 | newIndexKey.should have_key('key') 566 | newIndexKey['key'].should be_a(String) 567 | newIndexKey.should have_key('createdAt') 568 | newIndexKey['createdAt'].should be_a(String) 569 | sleep 2 # no task ID here 570 | resIndex = @index.list_user_keys 571 | resIndex.should have_key('keys') 572 | resIndex['keys'].should be_a(Array) 573 | resIndex['keys'][0].should have_key('value') 574 | resIndex['keys'][0]['value'].should be_a(String) 575 | resIndex['keys'][0].should have_key('acl') 576 | resIndex['keys'][0]['acl'].should be_a(Array) 577 | resIndex['keys'][0].should have_key('validity') 578 | resIndex['keys'][0]['validity'].should be_a(Integer) 579 | indexKey = @index.get_user_key(newIndexKey['key']) 580 | indexKey.should have_key('value') 581 | indexKey['value'].should be_a(String) 582 | indexKey.should have_key('acl') 583 | indexKey['acl'].should be_a(Array) 584 | indexKey.should have_key('validity') 585 | indexKey['validity'].should be_a(Integer) 586 | task = @index.delete_user_key(newIndexKey['key']) 587 | task.should have_key('deletedAt') 588 | task['deletedAt'].should be_a(String) 589 | end 590 | 591 | it 'Check attributes log : ' do 592 | logs = Algolia.get_logs() 593 | logs.should have_key('logs') 594 | logs['logs'].should be_a(Array) 595 | logs['logs'][0].should have_key('timestamp') 596 | logs['logs'][0]['timestamp'].should be_a(String) 597 | logs['logs'][0].should have_key('method') 598 | logs['logs'][0]['method'].should be_a(String) 599 | logs['logs'][0].should have_key('answer_code') 600 | logs['logs'][0]['answer_code'].should be_a(String) 601 | logs['logs'][0].should have_key('query_body') 602 | logs['logs'][0]['query_body'].should be_a(String) 603 | logs['logs'][0].should have_key('answer') 604 | logs['logs'][0]['answer'].should be_a(String) 605 | logs['logs'][0].should have_key('url') 606 | logs['logs'][0]['url'].should be_a(String) 607 | logs['logs'][0].should have_key('ip') 608 | logs['logs'][0]['ip'].should be_a(String) 609 | logs['logs'][0].should have_key('query_headers') 610 | logs['logs'][0]['query_headers'].should be_a(String) 611 | logs['logs'][0].should have_key('sha1') 612 | logs['logs'][0]['sha1'].should be_a(String) 613 | end 614 | 615 | it 'should generate secured api keys' do 616 | key = Algolia.generate_secured_api_key('my_api_key', '(public,user1)') 617 | key.should eq(OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new('sha256'), 'my_api_key', '(public,user1)')) 618 | key = Algolia.generate_secured_api_key('my_api_key', '(public,user1)', 42) 619 | key.should eq(OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new('sha256'), 'my_api_key', '(public,user1)42')) 620 | key = Algolia.generate_secured_api_key('my_api_key', ['public']) 621 | key.should eq(OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new('sha256'), 'my_api_key', 'public')) 622 | key = Algolia.generate_secured_api_key('my_api_key', ['public', ['premium','vip']]) 623 | key.should eq(OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new('sha256'), 'my_api_key', 'public,(premium,vip)')) 624 | end 625 | 626 | it 'Check attributes multipleQueries' do 627 | res = Algolia.multiple_queries([{:index_name => safe_index_name("àlgol?a"), "query" => ""}]) 628 | res.should have_key('results') 629 | res['results'].should be_a(Array) 630 | res['results'][0].should have_key('hits') 631 | res['results'][0]['hits'].should be_a(Array) 632 | res['results'][0].should have_key('page') 633 | res['results'][0]['page'].should be_a(Integer) 634 | res['results'][0].should have_key('nbHits') 635 | res['results'][0]['nbHits'].should be_a(Integer) 636 | res['results'][0].should have_key('nbPages') 637 | res['results'][0]['nbPages'].should be_a(Integer) 638 | res['results'][0].should have_key('hitsPerPage') 639 | res['results'][0]['hitsPerPage'].should be_a(Integer) 640 | res['results'][0].should have_key('processingTimeMS') 641 | res['results'][0]['processingTimeMS'].should be_a(Integer) 642 | res['results'][0].should have_key('query') 643 | res['results'][0]['query'].should be_a(String) 644 | res['results'][0].should have_key('params') 645 | res['results'][0]['params'].should be_a(String) 646 | end 647 | 648 | it 'should handle disjunctive faceting' do 649 | index = Algolia::Index.new(safe_index_name("test_hotels")) 650 | index.set_settings :attributesForFacetting => ['city', 'stars', 'facilities'] 651 | index.clear_index rescue nil 652 | index.add_objects! [ 653 | { :name => 'Hotel A', :stars => '*', :facilities => ['wifi', 'bath', 'spa'], :city => 'Paris' }, 654 | { :name => 'Hotel B', :stars => '*', :facilities => ['wifi'], :city => 'Paris' }, 655 | { :name => 'Hotel C', :stars => '**', :facilities => ['bath'], :city => 'San Francisco' }, 656 | { :name => 'Hotel D', :stars => '****', :facilities => ['spa'], :city => 'Paris' }, 657 | { :name => 'Hotel E', :stars => '****', :facilities => ['spa'], :city => 'New York' }, 658 | ] 659 | 660 | answer = index.search_disjunctive_faceting('h', ['stars', 'facilities'], { :facets => 'city' }) 661 | answer['nbHits'].should eq(5) 662 | answer['facets'].size.should eq(1) 663 | answer['disjunctiveFacets'].size.should eq(2) 664 | 665 | answer = index.search_disjunctive_faceting('h', ['stars', 'facilities'], { :facets => 'city' }, { :stars => ['*'] }) 666 | answer['nbHits'].should eq(2) 667 | answer['facets'].size.should eq(1) 668 | answer['disjunctiveFacets'].size.should eq(2) 669 | answer['disjunctiveFacets']['stars']['*'].should eq(2) 670 | answer['disjunctiveFacets']['stars']['**'].should eq(1) 671 | answer['disjunctiveFacets']['stars']['****'].should eq(2) 672 | 673 | answer = index.search_disjunctive_faceting('h', ['stars', 'facilities'], { :facets => 'city' }, { :stars => ['*'], :city => ['Paris'] }) 674 | answer['nbHits'].should eq(2) 675 | answer['facets'].size.should eq(1) 676 | answer['disjunctiveFacets'].size.should eq(2) 677 | answer['disjunctiveFacets']['stars']['*'].should eq(2) 678 | answer['disjunctiveFacets']['stars']['****'].should eq(1) 679 | 680 | answer = index.search_disjunctive_faceting('h', ['stars', 'facilities'], { :facets => 'city' }, { :stars => ['*', '****'], :city => ['Paris'] }) 681 | answer['nbHits'].should eq(3) 682 | answer['facets'].size.should eq(1) 683 | answer['disjunctiveFacets'].size.should eq(2) 684 | answer['disjunctiveFacets']['stars']['*'].should eq(2) 685 | answer['disjunctiveFacets']['stars']['****'].should eq(1) 686 | end 687 | end 688 | -------------------------------------------------------------------------------- /lib/algolia/index.rb: -------------------------------------------------------------------------------- 1 | require 'algolia/client' 2 | require 'algolia/error' 3 | 4 | module Algolia 5 | 6 | class Index 7 | attr_accessor :name 8 | 9 | def initialize(name) 10 | self.name = name 11 | end 12 | 13 | # Delete an index 14 | # 15 | # return an hash of the form { "deletedAt" => "2013-01-18T15:33:13.556Z", "taskID" => "42" } 16 | def delete 17 | Algolia.client.delete(Protocol.index_uri(name)) 18 | end 19 | alias_method :delete_index, :delete 20 | 21 | # Delete an index and wait until the deletion has been processed 22 | # 23 | # return an hash of the form { "deletedAt" => "2013-01-18T15:33:13.556Z", "taskID" => "42" } 24 | def delete! 25 | res = delete 26 | wait_task(res['taskID']) 27 | res 28 | end 29 | alias_method :delete_index!, :delete! 30 | 31 | # Add an object in this index 32 | # 33 | # @param obj the object to add to the index. 34 | # The object is represented by an associative array 35 | # @param objectID (optional) an objectID you want to attribute to this object 36 | # (if the attribute already exist the old object will be overridden) 37 | def add_object(obj, objectID = nil) 38 | check_object obj 39 | if objectID.nil? || objectID.to_s.empty? 40 | Algolia.client.post(Protocol.index_uri(name), obj.to_json) 41 | else 42 | Algolia.client.put(Protocol.object_uri(name, objectID), obj.to_json) 43 | end 44 | end 45 | 46 | # Add an object in this index and wait end of indexing 47 | # 48 | # @param obj the object to add to the index. 49 | # The object is represented by an associative array 50 | # @param objectID (optional) an objectID you want to attribute to this object 51 | # (if the attribute already exist the old object will be overridden) 52 | def add_object!(obj, objectID = nil) 53 | res = add_object(obj, objectID) 54 | wait_task(res["taskID"]) 55 | return res 56 | end 57 | 58 | # Add several objects in this index 59 | # 60 | # @param objs the array of objects to add inside the index. 61 | # Each object is represented by an associative array 62 | def add_objects(objs) 63 | batch build_batch('addObject', objs, false) 64 | end 65 | 66 | # Add several objects in this index and wait end of indexing 67 | # 68 | # @param objs the array of objects to add inside the index. 69 | # Each object is represented by an associative array 70 | def add_objects!(obj) 71 | res = add_objects(obj) 72 | wait_task(res["taskID"]) 73 | return res 74 | end 75 | 76 | # Search inside the index 77 | # 78 | # @param query the full text query 79 | # @param args (optional) if set, contains an associative array with query parameters: 80 | # - page: (integer) Pagination parameter used to select the page to retrieve. 81 | # Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set page=9 82 | # - hitsPerPage: (integer) Pagination parameter used to select the number of hits per page. Defaults to 20. 83 | # - attributesToRetrieve: a string that contains the list of object attributes you want to retrieve (let you minimize the answer size). 84 | # Attributes are separated with a comma (for example "name,address"). 85 | # You can also use a string array encoding (for example ["name","address"]). 86 | # By default, all attributes are retrieved. You can also use '*' to retrieve all values when an attributesToRetrieve setting is specified for your index. 87 | # - attributesToHighlight: a string that contains the list of attributes you want to highlight according to the query. 88 | # Attributes are separated by a comma. You can also use a string array encoding (for example ["name","address"]). 89 | # If an attribute has no match for the query, the raw value is returned. By default all indexed text attributes are highlighted. 90 | # You can use `*` if you want to highlight all textual attributes. Numerical attributes are not highlighted. 91 | # A matchLevel is returned for each highlighted attribute and can contain: 92 | # - full: if all the query terms were found in the attribute, 93 | # - partial: if only some of the query terms were found, 94 | # - none: if none of the query terms were found. 95 | # - attributesToSnippet: a string that contains the list of attributes to snippet alongside the number of words to return (syntax is `attributeName:nbWords`). 96 | # Attributes are separated by a comma (Example: attributesToSnippet=name:10,content:10). 97 | # You can also use a string array encoding (Example: attributesToSnippet: ["name:10","content:10"]). By default no snippet is computed. 98 | # - minWordSizefor1Typo: the minimum number of characters in a query word to accept one typo in this word. Defaults to 3. 99 | # - minWordSizefor2Typos: the minimum number of characters in a query word to accept two typos in this word. Defaults to 7. 100 | # - getRankingInfo: if set to 1, the result hits will contain ranking information in _rankingInfo attribute. 101 | # - aroundLatLng: search for entries around a given latitude/longitude (specified as two floats separated by a comma). 102 | # For example aroundLatLng=47.316669,5.016670). 103 | # You can specify the maximum distance in meters with the aroundRadius parameter (in meters) and the precision for ranking with aroundPrecision 104 | # (for example if you set aroundPrecision=100, two objects that are distant of less than 100m will be considered as identical for "geo" ranking parameter). 105 | # At indexing, you should specify geoloc of an object with the _geoloc attribute (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}}) 106 | # - insideBoundingBox: search entries inside a given area defined by the two extreme points of a rectangle (defined by 4 floats: p1Lat,p1Lng,p2Lat,p2Lng). 107 | # For example insideBoundingBox=47.3165,4.9665,47.3424,5.0201). 108 | # At indexing, you should specify geoloc of an object with the _geoloc attribute (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}}) 109 | # - numericFilters: a string that contains the list of numeric filters you want to apply separated by a comma. 110 | # The syntax of one filter is `attributeName` followed by `operand` followed by `value`. Supported operands are `<`, `<=`, `=`, `>` and `>=`. 111 | # You can have multiple conditions on one attribute like for example numericFilters=price>100,price<1000. 112 | # You can also use a string array encoding (for example numericFilters: ["price>100","price<1000"]). 113 | # - tagFilters: filter the query by a set of tags. You can AND tags by separating them by commas. 114 | # To OR tags, you must add parentheses. For example, tags=tag1,(tag2,tag3) means tag1 AND (tag2 OR tag3). 115 | # You can also use a string array encoding, for example tagFilters: ["tag1",["tag2","tag3"]] means tag1 AND (tag2 OR tag3). 116 | # At indexing, tags should be added in the _tags** attribute of objects (for example {"_tags":["tag1","tag2"]}). 117 | # - facetFilters: filter the query by a list of facets. 118 | # Facets are separated by commas and each facet is encoded as `attributeName:value`. 119 | # For example: `facetFilters=category:Book,author:John%20Doe`. 120 | # You can also use a string array encoding (for example `["category:Book","author:John%20Doe"]`). 121 | # - facets: List of object attributes that you want to use for faceting. 122 | # Attributes are separated with a comma (for example `"category,author"` ). 123 | # You can also use a JSON string array encoding (for example ["category","author"]). 124 | # Only attributes that have been added in **attributesForFaceting** index setting can be used in this parameter. 125 | # You can also use `*` to perform faceting on all attributes specified in **attributesForFaceting**. 126 | # - queryType: select how the query words are interpreted, it can be one of the following value: 127 | # - prefixAll: all query words are interpreted as prefixes, 128 | # - prefixLast: only the last word is interpreted as a prefix (default behavior), 129 | # - prefixNone: no query word is interpreted as a prefix. This option is not recommended. 130 | # - optionalWords: a string that contains the list of words that should be considered as optional when found in the query. 131 | # The list of words is comma separated. 132 | # - distinct: If set to 1, enable the distinct feature (disabled by default) if the attributeForDistinct index setting is set. 133 | # This feature is similar to the SQL "distinct" keyword: when enabled in a query with the distinct=1 parameter, 134 | # all hits containing a duplicate value for the attributeForDistinct attribute are removed from results. 135 | # For example, if the chosen attribute is show_name and several hits have the same value for show_name, then only the best 136 | # one is kept and others are removed. 137 | def search(query, params = {}) 138 | encoded_params = Hash[params.map { |k,v| [k.to_s, v.is_a?(Array) ? v.to_json : v] }] 139 | Algolia.client.get(Protocol.search_uri(name, query, encoded_params), Algolia.client.search_timeout) 140 | end 141 | 142 | # 143 | # Browse all index content 144 | # 145 | # @param page Pagination parameter used to select the page to retrieve. 146 | # Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set page=9 147 | # @param hitsPerPage: Pagination parameter used to select the number of hits per page. Defaults to 1000. 148 | # 149 | def browse(page = 0, hitsPerPage = 1000) 150 | Algolia.client.get(Protocol.browse_uri(name, {:page => page, :hitsPerPage => hitsPerPage})) 151 | end 152 | 153 | # 154 | # Get an object from this index 155 | # 156 | # @param objectID the unique identifier of the object to retrieve 157 | # @param attributesToRetrieve (optional) if set, contains the list of attributes to retrieve as a string separated by "," 158 | # 159 | def get_object(objectID, attributesToRetrieve = nil) 160 | if attributesToRetrieve.nil? 161 | Algolia.client.get(Protocol.object_uri(name, objectID, nil)) 162 | else 163 | Algolia.client.get(Protocol.object_uri(name, objectID, {:attributes => attributesToRetrieve})) 164 | end 165 | end 166 | 167 | # 168 | # Get a list of objects from this index 169 | # 170 | # @param objectIDs the array of unique identifier of the objects to retrieve 171 | # 172 | def get_objects(objectIDs) 173 | Algolia.client.post(Protocol.objects_uri, { :requests => objectIDs.map { |objectID| { :indexName => name, :objectID => objectID } } }.to_json)['results'] 174 | end 175 | 176 | # Wait the publication of a task on the server. 177 | # All server task are asynchronous and you can check with this method that the task is published. 178 | # 179 | # @param taskID the id of the task returned by server 180 | # @param timeBeforeRetry the time in milliseconds before retry (default = 100ms) 181 | # 182 | def wait_task(taskID, timeBeforeRetry = 100) 183 | loop do 184 | status = Algolia.client.get(Protocol.task_uri(name, taskID))["status"] 185 | if status == "published" 186 | return 187 | end 188 | sleep(timeBeforeRetry.to_f / 1000) 189 | end 190 | end 191 | 192 | # Override the content of an object 193 | # 194 | # @param obj the object to save 195 | # @param objectID the associated objectID, if nil 'obj' must contain an 'objectID' key 196 | # 197 | def save_object(obj, objectID = nil) 198 | Algolia.client.put(Protocol.object_uri(name, get_objectID(obj, objectID)), obj.to_json) 199 | end 200 | 201 | # Override the content of object and wait end of indexing 202 | # 203 | # @param obj the object to save 204 | # @param objectID the associated objectID, if nil 'obj' must contain an 'objectID' key 205 | # 206 | def save_object!(obj, objectID = nil) 207 | res = save_object(obj, objectID) 208 | wait_task(res["taskID"]) 209 | return res 210 | end 211 | 212 | # Override the content of several objects 213 | # 214 | # @param objs the array of objects to save, each object must contain an 'objectID' key 215 | # 216 | def save_objects(objs) 217 | batch build_batch('updateObject', objs, true) 218 | end 219 | 220 | # Override the content of several objects and wait end of indexing 221 | # 222 | # @param objs the array of objects to save, each object must contain an objectID attribute 223 | # 224 | def save_objects!(objs) 225 | res = save_objects(objs) 226 | wait_task(res["taskID"]) 227 | return res 228 | end 229 | 230 | # 231 | # Update partially an object (only update attributes passed in argument) 232 | # 233 | # @param obj the object attributes to override 234 | # @param objectID the associated objectID, if nil 'obj' must contain an 'objectID' key 235 | # 236 | def partial_update_object(obj, objectID = nil) 237 | Algolia.client.post(Protocol.partial_object_uri(name, get_objectID(obj, objectID)), obj.to_json) 238 | end 239 | 240 | # 241 | # Partially Override the content of several objects 242 | # 243 | # @param objs an array of objects to update (each object must contains a objectID attribute) 244 | # 245 | def partial_update_objects(objs) 246 | batch build_batch('partialUpdateObject', objs, true) 247 | end 248 | 249 | # 250 | # Partially Override the content of several objects and wait end of indexing 251 | # 252 | # @param objs an array of objects to update (each object must contains a objectID attribute) 253 | # 254 | def partial_update_objects!(objs) 255 | res = partial_update_objects(objs) 256 | wait_task(res["taskID"]) 257 | return res 258 | end 259 | 260 | # 261 | # Update partially an object (only update attributes passed in argument) and wait indexing 262 | # 263 | # @param obj the attributes to override 264 | # @param objectID the associated objectID, if nil 'obj' must contain an 'objectID' key 265 | # 266 | def partial_update_object!(obj, objectID = nil) 267 | res = partial_update_object(obj, objectID) 268 | wait_task(res["taskID"]) 269 | return res 270 | end 271 | 272 | # 273 | # Delete an object from the index 274 | # 275 | # @param objectID the unique identifier of object to delete 276 | # 277 | def delete_object(objectID) 278 | Algolia.client.delete(Protocol.object_uri(name, objectID)) 279 | end 280 | 281 | # 282 | # Delete an object from the index and wait end of indexing 283 | # 284 | # @param objectID the unique identifier of object to delete 285 | # 286 | def delete_object!(objectID) 287 | res = delete_object(objectID) 288 | wait_task(res["taskID"]) 289 | return res 290 | end 291 | 292 | # 293 | # Delete several objects 294 | # 295 | # @param objs an array of objectIDs 296 | # 297 | def delete_objects(objs) 298 | check_array objs 299 | batch build_batch('deleteObject', objs.map { |objectID| { :objectID => objectID } }, false) 300 | end 301 | 302 | # 303 | # Delete several objects and wait end of indexing 304 | # 305 | # @param objs an array of objectIDs 306 | # 307 | def delete_objects!(objs) 308 | res = delete_objects(objs) 309 | wait_task(res["taskID"]) 310 | return res 311 | end 312 | 313 | # 314 | # Delete all objects matching a query 315 | # 316 | # @param query the query string 317 | # @param params the optional query parameters 318 | # 319 | def delete_by_query(query, params = {}) 320 | params.delete(:hitsPerPage) 321 | params.delete('hitsPerPage') 322 | params.delete(:attributesToRetrieve) 323 | params.delete('attributesToRetrieve') 324 | 325 | params[:hitsPerPage] = 1000 326 | params[:attributesToRetrieve] = ['objectID'] 327 | loop do 328 | res = search(query, params) 329 | break if res['hits'].empty? 330 | res = delete_objects(res['hits'].map { |h| h['objectID'] }) 331 | wait_task res['taskID'] 332 | end 333 | end 334 | 335 | # 336 | # Delete the index content 337 | # 338 | # 339 | def clear 340 | Algolia.client.post(Protocol.clear_uri(name)) 341 | end 342 | alias_method :clear_index, :clear 343 | 344 | # 345 | # Delete the index content and wait end of indexing 346 | # 347 | def clear! 348 | res = clear 349 | wait_task(res["taskID"]) 350 | return res 351 | end 352 | alias_method :clear_index!, :clear! 353 | 354 | # 355 | # Set settings for this index 356 | # 357 | # @param settigns the settings object that can contains : 358 | # - minWordSizefor1Typo: (integer) the minimum number of characters to accept one typo (default = 3). 359 | # - minWordSizefor2Typos: (integer) the minimum number of characters to accept two typos (default = 7). 360 | # - hitsPerPage: (integer) the number of hits per page (default = 10). 361 | # - attributesToRetrieve: (array of strings) default list of attributes to retrieve in objects. 362 | # If set to null, all attributes are retrieved. 363 | # - attributesToHighlight: (array of strings) default list of attributes to highlight. 364 | # If set to null, all indexed attributes are highlighted. 365 | # - attributesToSnippet**: (array of strings) default list of attributes to snippet alongside the number of words to return (syntax is attributeName:nbWords). 366 | # By default no snippet is computed. If set to null, no snippet is computed. 367 | # - attributesToIndex: (array of strings) the list of fields you want to index. 368 | # If set to null, all textual and numerical attributes of your objects are indexed, but you should update it to get optimal results. 369 | # This parameter has two important uses: 370 | # - Limit the attributes to index: For example if you store a binary image in base64, you want to store it and be able to 371 | # retrieve it but you don't want to search in the base64 string. 372 | # - Control part of the ranking*: (see the ranking parameter for full explanation) Matches in attributes at the beginning of 373 | # the list will be considered more important than matches in attributes further down the list. 374 | # In one attribute, matching text at the beginning of the attribute will be considered more important than text after, you can disable 375 | # this behavior if you add your attribute inside `unordered(AttributeName)`, for example attributesToIndex: ["title", "unordered(text)"]. 376 | # - attributesForFaceting: (array of strings) The list of fields you want to use for faceting. 377 | # All strings in the attribute selected for faceting are extracted and added as a facet. If set to null, no attribute is used for faceting. 378 | # - attributeForDistinct: (string) The attribute name used for the Distinct feature. This feature is similar to the SQL "distinct" keyword: when enabled 379 | # in query with the distinct=1 parameter, all hits containing a duplicate value for this attribute are removed from results. 380 | # For example, if the chosen attribute is show_name and several hits have the same value for show_name, then only the best one is kept and others are removed. 381 | # - ranking: (array of strings) controls the way results are sorted. 382 | # We have six available criteria: 383 | # - typo: sort according to number of typos, 384 | # - geo: sort according to decreassing distance when performing a geo-location based search, 385 | # - proximity: sort according to the proximity of query words in hits, 386 | # - attribute: sort according to the order of attributes defined by attributesToIndex, 387 | # - exact: 388 | # - if the user query contains one word: sort objects having an attribute that is exactly the query word before others. 389 | # For example if you search for the "V" TV show, you want to find it with the "V" query and avoid to have all popular TV 390 | # show starting by the v letter before it. 391 | # - if the user query contains multiple words: sort according to the number of words that matched exactly (and not as a prefix). 392 | # - custom: sort according to a user defined formula set in **customRanking** attribute. 393 | # The standard order is ["typo", "geo", "proximity", "attribute", "exact", "custom"] 394 | # - customRanking: (array of strings) lets you specify part of the ranking. 395 | # The syntax of this condition is an array of strings containing attributes prefixed by asc (ascending order) or desc (descending order) operator. 396 | # For example `"customRanking" => ["desc(population)", "asc(name)"]` 397 | # - queryType: Select how the query words are interpreted, it can be one of the following value: 398 | # - prefixAll: all query words are interpreted as prefixes, 399 | # - prefixLast: only the last word is interpreted as a prefix (default behavior), 400 | # - prefixNone: no query word is interpreted as a prefix. This option is not recommended. 401 | # - highlightPreTag: (string) Specify the string that is inserted before the highlighted parts in the query result (default to ""). 402 | # - highlightPostTag: (string) Specify the string that is inserted after the highlighted parts in the query result (default to ""). 403 | # - optionalWords: (array of strings) Specify a list of words that should be considered as optional when found in the query. 404 | # 405 | def set_settings(new_settings) 406 | Algolia.client.put(Protocol.settings_uri(name), new_settings.to_json) 407 | end 408 | 409 | # Get settings of this index 410 | def get_settings 411 | Algolia.client.get(Protocol.settings_uri(name)) 412 | end 413 | 414 | # List all existing user keys with their associated ACLs 415 | def list_user_keys 416 | Algolia.client.get(Protocol.index_keys_uri(name)) 417 | end 418 | 419 | # Get ACL of a user key 420 | def get_user_key(key) 421 | Algolia.client.get(Protocol.index_key_uri(name, key)) 422 | end 423 | 424 | # 425 | # Create a new user key 426 | # 427 | # @param acls the list of ACL for this key. Defined by an array of strings that 428 | # can contains the following values: 429 | # - search: allow to search (https and http) 430 | # - addObject: allows to add a new object in the index (https only) 431 | # - updateObject : allows to change content of an existing object (https only) 432 | # - deleteObject : allows to delete an existing object (https only) 433 | # - deleteIndex : allows to delete index content (https only) 434 | # - settings : allows to get index settings (https only) 435 | # - editSettings : allows to change index settings (https only) 436 | # @param validity the number of seconds after which the key will be automatically removed (0 means no time limit for this key) 437 | # 438 | def add_user_key(acls, validity = 0, maxQueriesPerIPPerHour = 0, maxHitsPerQuery = 0) 439 | Algolia.client.post(Protocol.index_keys_uri(name), {:acl => acls, :validity => validity, :maxQueriesPerIPPerHour => maxQueriesPerIPPerHour.to_i, :maxHitsPerQuery => maxHitsPerQuery.to_i}.to_json) 440 | end 441 | 442 | # 443 | # Update a user key 444 | # 445 | # @param acls the list of ACL for this key. Defined by an array of strings that 446 | # can contains the following values: 447 | # - search: allow to search (https and http) 448 | # - addObject: allows to add a new object in the index (https only) 449 | # - updateObject : allows to change content of an existing object (https only) 450 | # - deleteObject : allows to delete an existing object (https only) 451 | # - deleteIndex : allows to delete index content (https only) 452 | # - settings : allows to get index settings (https only) 453 | # - editSettings : allows to change index settings (https only) 454 | # @param validity the number of seconds after which the key will be automatically removed (0 means no time limit for this key) 455 | # 456 | def update_user_key(key, acls, validity = 0, maxQueriesPerIPPerHour = 0, maxHitsPerQuery = 0) 457 | Algolia.client.put(Protocol.index_key_uri(name, key), {:acl => acls, :validity => validity, :maxQueriesPerIPPerHour => maxQueriesPerIPPerHour.to_i, :maxHitsPerQuery => maxHitsPerQuery.to_i}.to_json) 458 | end 459 | 460 | 461 | # Delete an existing user key 462 | def delete_user_key(key) 463 | Algolia.client.delete(Protocol.index_key_uri(name, key)) 464 | end 465 | 466 | # Send a batch request 467 | def batch(request) 468 | Algolia.client.post(Protocol.batch_uri(name), request.to_json) 469 | end 470 | 471 | # Send a batch request and wait the end of the indexing 472 | def batch!(request) 473 | res = batch(request) 474 | wait_task(res['taskID']) 475 | res 476 | end 477 | 478 | # Perform a search with disjunctive facets generating as many queries as number of disjunctive facets 479 | # 480 | # @param query the query 481 | # @param disjunctive_facets the array of disjunctive facets 482 | # @param params a hash representing the regular query parameters 483 | # @param refinements a hash ("string" -> ["array", "of", "refined", "values"]) representing the current refinements 484 | # ex: { "my_facet1" => ["my_value1", ["my_value2"], "my_disjunctive_facet1" => ["my_value1", "my_value2"] } 485 | def search_disjunctive_faceting(query, disjunctive_facets, params = {}, refinements = {}) 486 | raise ArgumentError.new('Argument "disjunctive_facets" must be a String or an Array') unless disjunctive_facets.is_a?(String) || disjunctive_facets.is_a?(Array) 487 | raise ArgumentError.new('Argument "refinements" must be a Hash of Arrays') if !refinements.is_a?(Hash) || !refinements.select { |k, v| !v.is_a?(Array) }.empty? 488 | 489 | # extract disjunctive facets & associated refinements 490 | disjunctive_facets = disjunctive_facets.split(',') if disjunctive_facets.is_a?(String) 491 | disjunctive_refinements = {} 492 | refinements.each do |k, v| 493 | disjunctive_refinements[k] = v if disjunctive_facets.include?(k) || disjunctive_facets.include?(k.to_s) 494 | end 495 | 496 | # build queries 497 | queries = [] 498 | ## hits + regular facets query 499 | filters = [] 500 | refinements.to_a.each do |k, values| 501 | r = values.map { |v| "#{k}:#{v}" } 502 | if disjunctive_refinements[k.to_s] || disjunctive_refinements[k.to_sym] 503 | # disjunctive refinements are ORed 504 | filters << r 505 | else 506 | # regular refinements are ANDed 507 | filters += r 508 | end 509 | end 510 | queries << params.merge({ :index_name => self.name, :query => query, :facetFilters => filters }) 511 | ## one query per disjunctive facet (use all refinements but the current one + hitsPerPage=1 + single facet) 512 | disjunctive_facets.each do |disjunctive_facet| 513 | filters = [] 514 | refinements.each do |k, values| 515 | if k.to_s != disjunctive_facet.to_s 516 | r = values.map { |v| "#{k}:#{v}" } 517 | if disjunctive_refinements[k.to_s] || disjunctive_refinements[k.to_sym] 518 | # disjunctive refinements are ORed 519 | filters << r 520 | else 521 | # regular refinements are ANDed 522 | filters += r 523 | end 524 | end 525 | end 526 | queries << params.merge({ 527 | :index_name => self.name, 528 | :query => query, 529 | :page => 0, 530 | :hitsPerPage => 1, 531 | :attributesToRetrieve => [], 532 | :attributesToHighlight => [], 533 | :attributesToSnippet => [], 534 | :facets => disjunctive_facet, 535 | :facetFilters => filters 536 | }) 537 | end 538 | answers = Algolia.multiple_queries(queries) 539 | 540 | # aggregate answers 541 | ## first answer stores the hits + regular facets 542 | aggregated_answer = answers['results'][0] 543 | ## others store the disjunctive facets 544 | aggregated_answer['disjunctiveFacets'] = {} 545 | answers['results'].each_with_index do |a, i| 546 | next if i == 0 547 | a['facets'].each do |facet, values| 548 | ## add the facet to the disjunctive facet hash 549 | aggregated_answer['disjunctiveFacets'][facet] = values 550 | ## concatenate missing refinements 551 | (disjunctive_refinements[facet.to_s] || disjunctive_refinements[facet.to_sym] || []).each do |r| 552 | if aggregated_answer['disjunctiveFacets'][facet][r].nil? 553 | aggregated_answer['disjunctiveFacets'][facet][r] = 0 554 | end 555 | end 556 | end 557 | end 558 | 559 | aggregated_answer 560 | end 561 | 562 | # 563 | # Alias of Algolia.list_indexes 564 | # 565 | def Index.all 566 | Algolia.list_indexes 567 | end 568 | 569 | private 570 | def check_array(objs) 571 | raise ArgumentError.new("argument must be an array of objects") if !objs.is_a?(Array) 572 | end 573 | 574 | def check_object(obj, in_array = false) 575 | case obj 576 | when Array 577 | raise ArgumentError.new(in_array ? "argument must be an array of objects" : "argument must not be an array") 578 | when String, Integer, Float, TrueClass, FalseClass, NilClass 579 | raise ArgumentError.new("argument must be an #{'array of' if in_array} object, got: #{obj.inspect}") 580 | else 581 | # ok 582 | end 583 | end 584 | 585 | def get_objectID(obj, objectID = nil) 586 | check_object obj 587 | objectID ||= obj[:objectID] || obj["objectID"] 588 | raise ArgumentError.new("Missing 'objectID'") if objectID.nil? 589 | return objectID 590 | end 591 | 592 | def build_batch(action, objs, with_object_id = false) 593 | check_array objs 594 | { 595 | :requests => objs.map { |obj| 596 | check_object obj, true 597 | h = { :action => action, :body => obj } 598 | h[:objectID] = get_objectID(obj).to_s if with_object_id 599 | h 600 | } 601 | } 602 | end 603 | 604 | end 605 | end 606 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Algolia Search API Client for Ruby 2 | ================== 3 | 4 | 5 | 6 | [Algolia Search](http://www.algolia.com) is a search API that provides hosted full-text, numerical and faceted search. 7 | Algolia’s Search API makes it easy to deliver a great search experience in your apps & websites providing: 8 | 9 | * REST and JSON-based API 10 | * search among infinite attributes from a single searchbox 11 | * instant-search after each keystroke 12 | * relevance & popularity combination 13 | * typo-tolerance in any language 14 | * faceting 15 | * 99.99% SLA 16 | * first-class data security 17 | 18 | This Ruby client let you easily use the Algolia Search API from your backend. It wraps [Algolia's REST API](http://www.algolia.com/doc/rest_api). 19 | 20 | [![Build Status](https://travis-ci.org/algolia/algoliasearch-client-ruby.png?branch=master)](https://travis-ci.org/algolia/algoliasearch-client-ruby) [![Gem Version](https://badge.fury.io/rb/algoliasearch.png)](http://badge.fury.io/rb/algoliasearch) [![Code Climate](https://codeclimate.com/github/algolia/algoliasearch-client-ruby.png)](https://codeclimate.com/github/algolia/algoliasearch-client-ruby) [![Coverage Status](https://coveralls.io/repos/algolia/algoliasearch-client-ruby/badge.png)](https://coveralls.io/r/algolia/algoliasearch-client-ruby) 21 | 22 | 23 | 24 | Table of Content 25 | ================ 26 | **Get started** 27 | 28 | 1. [Setup](#setup) 29 | 1. [Quick Start](#quick-start) 30 | 1. [Online documentation](#documentation) 31 | 1. [Tutorials](#tutorials) 32 | 33 | **Commands reference** 34 | 35 | 1. [Add a new object](#add-a-new-object-in-the-index) 36 | 1. [Update an object](#update-an-existing-object-in-the-index) 37 | 1. [Search](#search) 38 | 1. [Get an object](#get-an-object) 39 | 1. [Delete an object](#delete-an-object) 40 | 1. [Delete by query](#delete-by-query) 41 | 1. [Index settings](#index-settings) 42 | 1. [List indices](#list-indices) 43 | 1. [Delete an index](#delete-an-index) 44 | 1. [Clear an index](#clear-an-index) 45 | 1. [Wait indexing](#wait-indexing) 46 | 1. [Batch writes](#batch-writes) 47 | 1. [Security / User API Keys](#security--user-api-keys) 48 | 1. [Copy or rename an index](#copy-or-rename-an-index) 49 | 1. [Backup / Retrieve all index content](#backup--retrieve-all-index-content) 50 | 1. [Logs](#logs) 51 | 1. [Mock](#mock) 52 | 53 | 54 | 55 | 56 | Setup 57 | ------------- 58 | To setup your project, follow these steps: 59 | 60 | 61 | 62 | 63 | 1. Install AlgoliaSearch using gem install algoliasearch. 64 | 2. Initialize the client with your ApplicationID and API-Key. You can find all of them on [your Algolia account](http://www.algolia.com/users/edit). 65 | 66 | ```ruby 67 | require 'rubygems' 68 | require 'algoliasearch' 69 | 70 | Algolia.init :application_id => "YourApplicationID", 71 | :api_key => "YourAPIKey" 72 | ``` 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | Quick Start 81 | ------------- 82 | 83 | This quick start is a 30 seconds tutorial where you can discover how to index and search objects. 84 | 85 | Without any prior-configuration, you can index [500 contacts](https://github.com/algolia/algoliasearch-client-ruby/blob/master/contacts.json) in the ```contacts``` index with the following code: 86 | ```ruby 87 | index = Algolia::Index.new("contacts") 88 | batch = JSON.parse(File.read("contacts.json")) 89 | index.add_objects(batch) 90 | ``` 91 | 92 | You can then start to search for a contact firstname, lastname, company, ... (even with typos): 93 | ```ruby 94 | # search by firstname 95 | puts index.search('jimmie').to_json 96 | # search a firstname with typo 97 | puts index.search('jimie').to_json 98 | # search for a company 99 | puts index.search('california paint').to_json 100 | # search for a firstname & company 101 | puts index.search('jimmie paint').to_json 102 | ``` 103 | 104 | Settings can be customized to tune the search behavior. For example you can add a custom sort by number of followers to the already good out-of-the-box relevance: 105 | ```ruby 106 | index.set_settings({"customRanking" => ["desc(followers)"]}) 107 | ``` 108 | 109 | You can also configure the list of attributes you want to index by order of importance (first = most important): 110 | ```ruby 111 | index.set_settings({"attributesToIndex" => ["lastname", "firstname", "company", 112 | "email", "city", "address"]}) 113 | ``` 114 | 115 | Since the engine is designed to suggest results as you type, you'll generally search by prefix. In this case the order of attributes is very important to decide which hit is the best: 116 | ```ruby 117 | puts index.search('or').to_json 118 | puts index.search('jim').to_json 119 | ``` 120 | 121 | 122 | **Notes:** If you are building a web application, you may be more interested in using our [JavaScript client](https://github.com/algolia/algoliasearch-client-js) to perform queries. It brings two benefits: 123 | * your users get a better response time by avoiding to go through your servers, 124 | * it will offload your servers of unnecessary tasks. 125 | 126 | ```html 127 | 128 | 144 | ``` 145 | 146 | 147 | 148 | 149 | 150 | 151 | Documentation 152 | ================ 153 | 154 | Check our [online documentation](http://www.algolia.com/doc/guides/ruby): 155 | * [Initial Import](http://www.algolia.com/doc/guides/ruby#InitialImport) 156 | * [Ranking & Relevance](http://www.algolia.com/doc/guides/ruby#RankingRelevance) 157 | * [Indexing](http://www.algolia.com/doc/guides/ruby#Indexing) 158 | * [Search](http://www.algolia.com/doc/guides/ruby#Search) 159 | * [Sorting](http://www.algolia.com/doc/guides/ruby#Sorting) 160 | * [Filtering](http://www.algolia.com/doc/guides/ruby#Filtering) 161 | * [Faceting](http://www.algolia.com/doc/guides/ruby#Faceting) 162 | * [Geo-Search](http://www.algolia.com/doc/guides/ruby#Geo-Search) 163 | * [Security](http://www.algolia.com/doc/guides/ruby#Security) 164 | * [REST API](http://www.algolia.com/doc/rest) 165 | 166 | 167 | Tutorials 168 | ================ 169 | 170 | Check our [tutorials](http://www.algolia.com/doc/tutorials): 171 | * [Searchbar with auto-completion](http://www.algolia.com/doc/tutorials/auto-complete) 172 | * [Searchbar with multi-categories auto-completion](http://www.algolia.com/doc/tutorials/multi-auto-complete) 173 | * [Instant-search](http://www.algolia.com/doc/tutorials/instant-search) 174 | 175 | 176 | 177 | Commands reference 178 | ================== 179 | 180 | 181 | 182 | 183 | 184 | Add a new object in the Index 185 | ------------- 186 | 187 | Each entry in an index has a unique identifier called `objectID`. You have two ways to add en entry in the index: 188 | 189 | 1. Using automatic `objectID` assignement, you will be able to retrieve it in the answer. 190 | 2. Passing your own `objectID` 191 | 192 | You don't need to explicitely create an index, it will be automatically created the first time you add an object. 193 | Objects are schema less, you don't need any configuration to start indexing. The settings section provide details about advanced settings. 194 | 195 | Example with automatic `objectID` assignement: 196 | 197 | ```ruby 198 | res = index.add_object({"firstname" => "Jimmie", 199 | "lastname" => "Barninger"}) 200 | puts "ObjectID=" + res["objectID"] 201 | ``` 202 | 203 | Example with manual `objectID` assignement: 204 | 205 | ```ruby 206 | res = index.add_object({"firstname" => "Jimmie", 207 | "lastname" => "Barninger"}, "myID") 208 | puts "ObjectID=" + res["objectID"] 209 | ``` 210 | 211 | Update an existing object in the Index 212 | ------------- 213 | 214 | You have two options to update an existing object: 215 | 216 | 1. Replace all its attributes. 217 | 2. Replace only some attributes. 218 | 219 | Example to replace all the content of an existing object: 220 | 221 | ```ruby 222 | index.save_object({"firstname" => "Jimmie", 223 | "lastname" => "Barninger", 224 | "city" => "New York", 225 | "objectID" => "myID"}) 226 | ``` 227 | 228 | Example to update only the city attribute of an existing object: 229 | 230 | ```ruby 231 | index.partial_update_object({"city" => "San Francisco", 232 | "objectID" => "myID"}) 233 | ``` 234 | 235 | 236 | 237 | Search 238 | ------------- 239 | 240 | **Opening note:** If you are building a web application, you may be more interested in using our [javascript client](https://github.com/algolia/algoliasearch-client-js) to send queries. It brings two benefits: 241 | * your users get a better response time by avoiding to go through your servers, 242 | * and it will offload your servers of unnecessary tasks. 243 | 244 | 245 | To perform a search, you just need to initialize the index and perform a call to the search function. 246 | 247 | You can use the following optional arguments: 248 | 249 | ### Query parameters 250 | 251 | #### Full Text Search parameters 252 | 253 | * **query**: (string) The instant-search query string, all words of the query are interpreted as prefixes (for example "John Mc" will match "John Mccamey" and "Johnathan Mccamey"). If no query parameter is set, retrieves all objects. 254 | * **queryType**: select how the query words are interpreted, it can be one of the following value: 255 | * **prefixAll**: all query words are interpreted as prefixes, 256 | * **prefixLast**: only the last word is interpreted as a prefix (default behavior), 257 | * **prefixNone**: no query word is interpreted as a prefix. This option is not recommended. 258 | * **removeWordsIfNoResult**: This option to select a strategy to avoid having an empty result page. There is three different option: 259 | * **LastWords**: when a query does not return any result, the last word will be added as optional (the process is repeated with n-1 word, n-2 word, ... until there is results), 260 | * **FirstWords**: when a query does not return any result, the first word will be added as optional (the process is repeated with second word, third word, ... until there is results), 261 | * **None**: No specific processing is done when a query does not return any result (default behavior). 262 | * **typoTolerance**: if set to false, disable the typo-tolerance. Defaults to true. 263 | * **minWordSizefor1Typo**: the minimum number of characters in a query word to accept one typo in this word.
Defaults to 4. 264 | * **minWordSizefor2Typos**: the minimum number of characters in a query word to accept two typos in this word.
Defaults to 8. 265 | * **allowTyposOnNumericTokens**: if set to false, disable typo-tolerance on numeric tokens (numbers). Default to true. 266 | * **ignorePlural**: If set to true, plural won't be considered as a typo (for example car/cars will be considered as equals). Default to false. 267 | * **restrictSearchableAttributes** List of attributes you want to use for textual search (must be a subset of the `attributesToIndex` index setting). Attributes are separated with a comma (for example `"name,address"`), you can also use a JSON string array encoding (for example encodeURIComponent("[\"name\",\"address\"]")). By default, all attributes specified in `attributesToIndex` settings are used to search. 268 | * **advancedSyntax**: Enable the advanced query syntax. Defaults to 0 (false). 269 | * **Phrase query**: a phrase query defines a particular sequence of terms. A phrase query is build by Algolia's query parser for words surrounded by `"`. For example, `"search engine"` will retrieve records having `search` next to `engine` only. Typo-tolerance is _disabled_ on phrase queries. 270 | * **Prohibit operator**: The prohibit operator excludes records that contain the term after the `-` symbol. For example `search -engine` will retrieve records containing `search` but not `engine`. 271 | * **analytics**: If set to false, this query will not be taken into account in analytics feature. Default to true. 272 | * **synonyms**: If set to false, this query will not use synonyms defined in configuration. Default to true. 273 | * **replaceSynonymsInHighlight**: If set to false, words matched via synonyms expansion will not be replaced by the matched synonym in highlight result. Default to true. 274 | * **optionalWords**: a string that contains the list of words that should be considered as optional when found in the query. The list of words is comma separated. 275 | 276 | #### Pagination parameters 277 | 278 | * **page**: (integer) Pagination parameter used to select the page to retrieve.
Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set `page=9` 279 | * **hitsPerPage**: (integer) Pagination parameter used to select the number of hits per page. Defaults to 20. 280 | 281 | #### Geo-search parameters 282 | 283 | * **aroundLatLng**: search for entries around a given latitude/longitude (specified as two floats separated by a comma).
For example `aroundLatLng=47.316669,5.016670`).
You can specify the maximum distance in meters with the **aroundRadius** parameter (in meters) and the precision for ranking with **aroundPrecision** (for example if you set aroundPrecision=100, two objects that are distant of less than 100m will be considered as identical for "geo" ranking parameter).
At indexing, you should specify geoloc of an object with the `_geoloc` attribute (in the form `{"_geoloc":{"lat":48.853409, "lng":2.348800}}`) 284 | 285 | * **aroundLatLngViaIP**: search for entries around a given latitude/longitude (automatically computed from user IP address).
For example `aroundLatLng=47.316669,5.016670`).
You can specify the maximum distance in meters with the **aroundRadius** parameter (in meters) and the precision for ranking with **aroundPrecision** (for example if you set aroundPrecision=100, two objects that are distant of less than 100m will be considered as identical for "geo" ranking parameter).
At indexing, you should specify geoloc of an object with the `_geoloc` attribute (in the form `{"_geoloc":{"lat":48.853409, "lng":2.348800}}`) 286 | 287 | 288 | * **insideBoundingBox**: search entries inside a given area defined by the two extreme points of a rectangle (defined by 4 floats: p1Lat,p1Lng,p2Lat,p2Lng).
For example `insideBoundingBox=47.3165,4.9665,47.3424,5.0201`).
At indexing, you should specify geoloc of an object with the _geoloc attribute (in the form `{"_geoloc":{"lat":48.853409, "lng":2.348800}}`) 289 | 290 | #### Parameters to control results content 291 | 292 | * **attributesToRetrieve**: a string that contains the list of object attributes you want to retrieve (let you minimize the answer size).
Attributes are separated with a comma (for example `"name,address"`), you can also use a string array encoding (for example `["name","address"]` ). By default, all attributes are retrieved. You can also use `*` to retrieve all values when an **attributesToRetrieve** setting is specified for your index. 293 | * **attributesToHighlight**: a string that contains the list of attributes you want to highlight according to the query. Attributes are separated by a comma. You can also use a string array encoding (for example `["name","address"]`). If an attribute has no match for the query, the raw value is returned. By default all indexed text attributes are highlighted. You can use `*` if you want to highlight all textual attributes. Numerical attributes are not highlighted. A matchLevel is returned for each highlighted attribute and can contain: 294 | * **full**: if all the query terms were found in the attribute, 295 | * **partial**: if only some of the query terms were found, 296 | * **none**: if none of the query terms were found. 297 | * **attributesToSnippet**: a string that contains the list of attributes to snippet alongside the number of words to return (syntax is `attributeName:nbWords`). Attributes are separated by a comma (Example: `attributesToSnippet=name:10,content:10`).
You can also use a string array encoding (Example: `attributesToSnippet: ["name:10","content:10"]`). By default no snippet is computed. 298 | * **getRankingInfo**: if set to 1, the result hits will contain ranking information in **_rankingInfo** attribute. 299 | 300 | 301 | #### Numeric search parameters 302 | * **numericFilters**: a string that contains the list of numeric filters you want to apply separated by a comma. The syntax of one filter is `attributeName` followed by `operand` followed by `value`. Supported operands are `<`, `<=`, `=`, `>` and `>=`. 303 | 304 | You can easily perform range queries via the `:` operator (equivalent to combining a `>=` and `<=` operand), for example `numericFilters=price:10 to 1000`. 305 | 306 | You can also mix OR and AND operators. The OR operator is defined with a parenthesis syntax. For example `(code=1 AND (price:[0-100] OR price:[1000-2000]))` translates in `encodeURIComponent("code=1,(price:0 to 10,price:1000 to 2000)")`. 307 | 308 | You can also use a string array encoding (for example `numericFilters: ["price>100","price<1000"]`). 309 | 310 | #### Category search parameters 311 | * **tagFilters**: filter the query by a set of tags. You can AND tags by separating them by commas. To OR tags, you must add parentheses. For example, `tags=tag1,(tag2,tag3)` means *tag1 AND (tag2 OR tag3)*. You can also use a string array encoding, for example `tagFilters: ["tag1",["tag2","tag3"]]` means *tag1 AND (tag2 OR tag3)*.
At indexing, tags should be added in the **_tags** attribute of objects (for example `{"_tags":["tag1","tag2"]}`). 312 | 313 | #### Faceting parameters 314 | * **facetFilters**: filter the query by a list of facets. Facets are separated by commas and each facet is encoded as `attributeName:value`. To OR facets, you must add parentheses. For example: `facetFilters=(category:Book,category:Movie),author:John%20Doe`. You can also use a string array encoding (for example `[["category:Book","category:Movie"],"author:John%20Doe"]`). 315 | * **facets**: List of object attributes that you want to use for faceting.
Attributes are separated with a comma (for example `"category,author"` ). You can also use a JSON string array encoding (for example `["category","author"]` ). Only attributes that have been added in **attributesForFaceting** index setting can be used in this parameter. You can also use `*` to perform faceting on all attributes specified in **attributesForFaceting**. 316 | * **maxValuesPerFacet**: Limit the number of facet values returned for each facet. For example: `maxValuesPerFacet=10` will retrieve max 10 values per facet. 317 | 318 | #### Distinct parameter 319 | * **distinct**: If set to 1, enable the distinct feature (disabled by default) if the `attributeForDistinct` index setting is set. This feature is similar to the SQL "distinct" keyword: when enabled in a query with the `distinct=1` parameter, all hits containing a duplicate value for the attributeForDistinct attribute are removed from results. For example, if the chosen attribute is `show_name` and several hits have the same value for `show_name`, then only the best one is kept and others are removed. 320 | **Note**: This feature is disabled if the query string is empty and there isn't any `tagFilters`, nor any `facetFilters`, nor any `numericFilters` parameters. 321 | 322 | ```ruby 323 | index = Algolia::Index.new("contacts") 324 | res = index.search("query string") 325 | res = index.search("query string", { "attributesToRetrieve" => "firstname,lastname", "hitsPerPage" => 20}) 326 | ``` 327 | 328 | The server response will look like: 329 | 330 | ```javascript 331 | { 332 | "hits": [ 333 | { 334 | "firstname": "Jimmie", 335 | "lastname": "Barninger", 336 | "objectID": "433", 337 | "_highlightResult": { 338 | "firstname": { 339 | "value": "Jimmie", 340 | "matchLevel": "partial" 341 | }, 342 | "lastname": { 343 | "value": "Barninger", 344 | "matchLevel": "none" 345 | }, 346 | "company": { 347 | "value": "California Paint & Wlpaper Str", 348 | "matchLevel": "partial" 349 | } 350 | } 351 | } 352 | ], 353 | "page": 0, 354 | "nbHits": 1, 355 | "nbPages": 1, 356 | "hitsPerPage": 20, 357 | "processingTimeMS": 1, 358 | "query": "jimmie paint", 359 | "params": "query=jimmie+paint&attributesToRetrieve=firstname,lastname&hitsPerPage=50" 360 | } 361 | ``` 362 | 363 | 364 | Multi-queries 365 | -------------- 366 | 367 | You can send multiple queries with a single API call using a batch of queries: 368 | 369 | ```ruby 370 | # perform 3 queries in a single API call: 371 | # - 1st query targets index `categories` 372 | # - 2nd and 3rd queries target index `products` 373 | res = Algolia.multiple_queries([{:index_name => "categories", "query" => my_query_string, "hitsPerPage" => 3} 374 | , {:index_name => "products", "query" => my_query_string, "hitsPerPage" => 3, "tagFilters" => "promotion"} 375 | , {:index_name => "products", "query" => my_query_string, "hitsPerPage" => 10}]) 376 | 377 | puts res["results"] 378 | ``` 379 | 380 | 381 | 382 | 383 | 384 | 385 | Get an object 386 | ------------- 387 | 388 | You can easily retrieve an object using its `objectID` and optionnaly a list of attributes you want to retrieve (using comma as separator): 389 | 390 | ```ruby 391 | # Retrieves all attributes 392 | index.get_object("myID") 393 | # Retrieves firstname and lastname attributes 394 | res = index.get_object("myID", "firstname,lastname") 395 | # Retrieves only the firstname attribute 396 | res = index.get_object("myID", "fistname") 397 | ``` 398 | 399 | You can also retrieve a set of objects: 400 | 401 | ```ruby 402 | res = index.get_objects(["myID", "myID2"]) 403 | ``` 404 | 405 | Delete an object 406 | ------------- 407 | 408 | You can delete an object using its `objectID`: 409 | 410 | ```ruby 411 | index.delete_object("myID") 412 | ``` 413 | 414 | 415 | Delete by query 416 | ------------- 417 | 418 | You can delete all objects matching a single query with the following code. Internally, the API client performs the query, delete all matching hits, wait until the deletions have been applied and so on. 419 | 420 | ```ruby 421 | params = {} 422 | index.delete_by_query("John", params) 423 | ``` 424 | 425 | 426 | Index Settings 427 | ------------- 428 | 429 | You can retrieve all settings using the `get_settings` function. The result will contains the following attributes: 430 | 431 | 432 | #### Indexing parameters 433 | * **attributesToIndex**: (array of strings) the list of fields you want to index.
If set to null, all textual and numerical attributes of your objects are indexed, but you should update it to get optimal results.
This parameter has two important uses: 434 | * *Limit the attributes to index*.
For example if you store a binary image in base64, you want to store it and be able to retrieve it but you don't want to search in the base64 string. 435 | * *Control part of the ranking*.
(see the ranking parameter for full explanation) Matches in attributes at the beginning of the list will be considered more important than matches in attributes further down the list. In one attribute, matching text at the beginning of the attribute will be considered more important than text after, you can disable this behavior if you add your attribute inside `unordered(AttributeName)`, for example `attributesToIndex: ["title", "unordered(text)"]`. 436 | **Notes**: All numerical attributes are automatically indexed as numerical filters. If you don't need filtering on some of your numerical attributes, please consider sending them as strings to speed up the indexing.
437 | You can decide to have the same priority for two attributes by passing them in the same string using comma as separator. For example `title` and `alternative_title` have the same priority in this example, which is different than text priority: `attributesToIndex:["title,alternative_title", "text"]` 438 | * **attributesForFaceting**: (array of strings) The list of fields you want to use for faceting. All strings in the attribute selected for faceting are extracted and added as a facet. If set to null, no attribute is used for faceting. 439 | * **attributeForDistinct**: The attribute name used for the `Distinct` feature. This feature is similar to the SQL "distinct" keyword: when enabled in query with the `distinct=1` parameter, all hits containing a duplicate value for this attribute are removed from results. For example, if the chosen attribute is `show_name` and several hits have the same value for `show_name`, then only the best one is kept and others are removed. **Note**: This feature is disabled if the query string is empty and there isn't any `tagFilters`, nor any `facetFilters`, nor any `numericFilters` parameters. 440 | * **ranking**: (array of strings) controls the way results are sorted.
We have nine available criteria: 441 | * **typo**: sort according to number of typos, 442 | * **geo**: sort according to decreassing distance when performing a geo-location based search, 443 | * **words**: sort according to the number of query words matched by decreasing order. This parameter is useful when you use `optionalWords` query parameter to have results with the most matched words first. 444 | * **proximity**: sort according to the proximity of query words in hits, 445 | * **attribute**: sort according to the order of attributes defined by attributesToIndex, 446 | * **exact**: 447 | * if the user query contains one word: sort objects having an attribute that is exactly the query word before others. For example if you search for the "V" TV show, you want to find it with the "V" query and avoid to have all popular TV show starting by the v letter before it. 448 | * if the user query contains multiple words: sort according to the number of words that matched exactly (and not as a prefix). 449 | * **custom**: sort according to a user defined formula set in **customRanking** attribute. 450 | * **asc(attributeName)**: sort according to a numeric attribute by ascending order. **attributeName** can be the name of any numeric attribute of your records (integer, a double or boolean). 451 | * **desc(attributeName)**: sort according to a numeric attribute by descending order. **attributeName** can be the name of any numeric attribute of your records (integer, a double or boolean).
The standard order is ["typo", "geo", "words", "proximity", "attribute", "exact", "custom"] 452 | * **customRanking**: (array of strings) lets you specify part of the ranking.
The syntax of this condition is an array of strings containing attributes prefixed by asc (ascending order) or desc (descending order) operator. 453 | For example `"customRanking" => ["desc(population)", "asc(name)"]` 454 | * **queryType**: Select how the query words are interpreted, it can be one of the following value: 455 | * **prefixAll**: all query words are interpreted as prefixes, 456 | * **prefixLast**: only the last word is interpreted as a prefix (default behavior), 457 | * **prefixNone**: no query word is interpreted as a prefix. This option is not recommended. 458 | * **slaves**: The list of indices on which you want to replicate all write operations. In order to get response times in milliseconds, we pre-compute part of the ranking during indexing. If you want to use different ranking configurations depending of the use-case, you need to create one index per ranking configuration. This option enables you to perform write operations only on this index, and to automatically update slave indices with the same operations. 459 | * **unretrievableAttributes**: The list of attributes that cannot be retrieved at query time. This feature allow to have an attribute that is used for indexing and/or ranking but cannot be retrieved. Default to null. 460 | * **allowCompressionOfIntegerArray**: Allows compression of big integer arrays. We recommended to store the list of user ID or rights as an integer array and enable this setting. When enabled the integer array are reordered to reach a better compression ratio. Default to false. 461 | 462 | #### Query expansion 463 | * **synonyms**: (array of array of words considered as equals). For example, you may want to retrieve your **black ipad** record when your users are searching for **dark ipad**, even if the **dark** word is not part of the record: so you need to configure **black** as a synonym of **dark**. For example `"synomyms": [ [ "black", "dark" ], [ "small", "little", "mini" ], ... ]`. 464 | * **placeholders**: (hash of array of words). This is an advanced use case to define a token substitutable by a list of words without having the original token searchable. It is defined by a hash associating placeholders to lists of substitutable words. For example `"placeholders": { "": ["1", "2", "3", ..., "9999"]}` placeholder to be able to match all street numbers (we use the `< >` tag syntax to define placeholders in an attribute). For example: 465 | * Push a record with the placeholder: `{ "name" : "Apple Store", "address" : "<streetnumber> Opera street, Paris" }` 466 | * Configure the placeholder in your index settings: `"placeholders": { "" : ["1", "2", "3", "4", "5", ... ], ... }`. 467 | * **disableTypoToleranceOn**: (string array). Specify a list of words on which the automatic typo tolerance will be disabled. 468 | * **altCorrections**: (object array). Specify alternative corrections that you want to consider. Each alternative correction is described by an object containing three attributes: 469 | * **word**: the word to correct 470 | * **correction**: the corrected word 471 | * **nbTypos** the number of typos (1 or 2) that will be considered for the ranking algorithm (1 typo is better than 2 typos) 472 | 473 | For example `"altCorrections": [ { "word" : "foot", "correction": "feet", "nbTypos": 1}, { "word": "feet", "correction": "foot", "nbTypos": 1}].` 474 | 475 | #### Default query parameters (can be overwrite by query) 476 | * **minWordSizefor1Typo**: (integer) the minimum number of characters to accept one typo (default = 4). 477 | * **minWordSizefor2Typos**: (integer) the minimum number of characters to accept two typos (default = 8). 478 | * **hitsPerPage**: (integer) the number of hits per page (default = 10). 479 | * **attributesToRetrieve**: (array of strings) default list of attributes to retrieve in objects. If set to null, all attributes are retrieved. 480 | * **attributesToHighlight**: (array of strings) default list of attributes to highlight. If set to null, all indexed attributes are highlighted. 481 | * **attributesToSnippet**: (array of strings) default list of attributes to snippet alongside the number of words to return (syntax is 'attributeName:nbWords')
By default no snippet is computed. If set to null, no snippet is computed. 482 | * **highlightPreTag**: (string) Specify the string that is inserted before the highlighted parts in the query result (default to "<em>"). 483 | * **highlightPostTag**: (string) Specify the string that is inserted after the highlighted parts in the query result (default to "</em>"). 484 | * **optionalWords**: (array of strings) Specify a list of words that should be considered as optional when found in the query. 485 | 486 | You can easily retrieve settings or update them: 487 | 488 | ```ruby 489 | res = index.get_settings 490 | puts settings.to_json 491 | ``` 492 | 493 | ```ruby 494 | index.set_settings({"customRanking" => ["desc(followers)"]}) 495 | ``` 496 | 497 | List indices 498 | ------------- 499 | You can list all your indices with their associated information (number of entries, disk size, etc.) with the `list_indexes` method: 500 | 501 | ```ruby 502 | Algolia.list_indexes 503 | ``` 504 | 505 | Delete an index 506 | ------------- 507 | You can delete an index using its name: 508 | 509 | ```ruby 510 | index = Algolia::Index.new("contacts") 511 | index.delete_index 512 | ``` 513 | 514 | Clear an index 515 | ------------- 516 | You can delete the index content without removing settings and index specific API keys with the clearIndex command: 517 | 518 | ```ruby 519 | index.clear_index 520 | ``` 521 | 522 | Wait indexing 523 | ------------- 524 | 525 | All write operations return a `taskID` when the job is securely stored on our infrastructure but not when the job is published in your index. Even if it's extremely fast, you can easily ensure indexing is complete using the same method with a `!`. 526 | 527 | For example, to wait for indexing of a new object: 528 | ```ruby 529 | res = index.add_object!({"firstname" => "Jimmie", 530 | "lastname" => "Barninger"}) 531 | ``` 532 | 533 | 534 | If you want to ensure multiple objects have been indexed, you can only check the biggest taskID with `wait_task`. 535 | 536 | Batch writes 537 | ------------- 538 | 539 | You may want to perform multiple operations with one API call to reduce latency. 540 | We expose three methods to perform batch: 541 | * `add_objects`: add an array of object using automatic `objectID` assignement 542 | * `save_objects`: add or update an array of object that contains an `objectID` attribute 543 | * `delete_objects`: delete an array of objectIDs 544 | * `partial_update_objects`: partially update an array of objects that contain an `objectID` attribute (only specified attributes will be updated, other will remain unchanged) 545 | 546 | Example using automatic `objectID` assignement: 547 | ```ruby 548 | res = index.add_objects([{"firstname" => "Jimmie", 549 | "lastname" => "Barninger"}, 550 | {"firstname" => "Warren", 551 | "lastname" => "Speach"}]) 552 | ``` 553 | 554 | Example with user defined `objectID` (add or update): 555 | ```ruby 556 | res = index.save_objects([{"firstname" => "Jimmie", 557 | "lastname" => "Barninger", 558 | "objectID" => "myID1"}, 559 | {"firstname" => "Warren", 560 | "lastname" => "Speach", 561 | "objectID" => "myID2"}]) 562 | ``` 563 | 564 | Example that delete a set of records: 565 | ```ruby 566 | res = index.delete_objects(["myID1", "myID2"]) 567 | ``` 568 | 569 | Example that update only the `firstname` attribute: 570 | ```ruby 571 | res = index.partial_update_objects([{"firstname" => "Jimmie", 572 | "objectID" => "SFO"}, 573 | {"firstname" => "Warren", 574 | "objectID" => "myID2"}]) 575 | ``` 576 | 577 | 578 | 579 | Security / User API Keys 580 | ------------- 581 | 582 | The admin API key provides full control of all your indices. 583 | You can also generate user API keys to control security. 584 | These API keys can be restricted to a set of operations or/and restricted to a given index. 585 | 586 | To list existing keys, you can use `list_user_keys` method: 587 | ```ruby 588 | # Lists global API Keys 589 | Algolia.list_user_keys 590 | # Lists API Keys that can access only to this index 591 | index.list_user_keys 592 | ``` 593 | 594 | Each key is defined by a set of rights that specify the authorized actions. The different rights are: 595 | * **search**: allows to search, 596 | * **browse**: allow to retrieve all index content via the browse API, 597 | * **addObject**: allows to add/update an object in the index, 598 | * **deleteObject**: allows to delete an existing object, 599 | * **deleteIndex**: allows to delete index content, 600 | * **settings**: allows to get index settings, 601 | * **editSettings**: allows to change index settings. 602 | 603 | Example of API Key creation: 604 | ```ruby 605 | # Creates a new global API key that can only perform search actions 606 | res = Algolia.add_user_key(["search"]) 607 | puts res['key'] 608 | # Creates a new API key that can only perform search action on this index 609 | res = index.add_user_key(["search"]) 610 | puts res['key'] 611 | ``` 612 | 613 | You can also create an API Key with advanced restrictions: 614 | 615 | * Add a validity period: the key will be valid only for a specific period of time (in seconds), 616 | * Specify the maximum number of API calls allowed from an IP address per hour. Each time an API call is performed with this key, a check is performed. If the IP at the origin of the call did more than this number of calls in the last hour, a 403 code is returned. Defaults to 0 (no rate limit). This parameter can be used to protect you from attempts at retrieving your entire content by massively querying the index. 617 | 618 | 619 | Note: If you are sending the query through your servers, you must use the `Algolia.with_rate_limits("EndUserIP", "APIKeyWithRateLimit") do ... end` block to enable rate-limit. 620 | 621 | * Specify the maximum number of hits this API key can retrieve in one call. Defaults to 0 (unlimited). This parameter can be used to protect you from attempts at retrieving your entire content by massively querying the index. 622 | * Specify the list of targeted indices, you can target all indices starting by a prefix or finishing by a suffix with the '*' character (for example "dev_*" matches all indices starting by "dev_" and "*_dev" matches all indices finishing by "_dev"). Defaults to all indices if empty of blank. 623 | 624 | ```ruby 625 | # Creates a new global API key that is valid for 300 seconds 626 | res = Algolia.add_user_key(["search"], 300) 627 | puts res['key'] 628 | # Creates a new index specific API key: 629 | # - valid for 300 seconds 630 | # - rate limit of 100 calls per hour per IP 631 | # - maximum of 20 hits 632 | # - valid on 'my_index1' and 'my_index2' 633 | res = index.add_user_key(["search"], 300, 100, 20, ['my_index1', 'my_index2']) 634 | puts res['key'] 635 | ``` 636 | 637 | Update the rights of an existing key: 638 | ```ruby 639 | # Update an existing global API key that is valid for 300 seconds 640 | res = Algolia.update_user_key("myAPIKey", ["search"], 300) 641 | puts res['key'] 642 | # Update an existing index specific API key: 643 | # - valid for 300 seconds 644 | # - rate limit of 100 calls per hour per IP 645 | # - maximum of 20 hits 646 | # - valid on 'my_index1' and 'my_index2' 647 | res = index.update_user_key("myAPIKey", ["search"], 300, 100, 20, ['my_index1', 'my_index2']) 648 | puts res['key'] 649 | ``` 650 | Get the rights of a given key: 651 | ```ruby 652 | # Gets the rights of a global key 653 | Algolia.get_user_key("f420238212c54dcfad07ea0aa6d5c45f") 654 | # Gets the rights of an index specific key 655 | index.get_user_key("71671c38001bf3ac857bc82052485107") 656 | ``` 657 | 658 | Delete an existing key: 659 | ```ruby 660 | # Deletes a global key 661 | Algolia.delete_user_key("f420238212c54dcfad07ea0aa6d5c45f") 662 | # Deletes an index specific key 663 | index.delete_user_key("71671c38001bf3ac857bc82052485107") 664 | ``` 665 | 666 | 667 | 668 | You may have a single index containing per-user data. In that case, all records should be tagged with their associated user_id in order to add a `tagFilters=(public,user_42)` filter at query time to retrieve only what a user has access to. If you're using the [JavaScript client](http://github.com/algolia/algoliasearch-client-js), it will result in a security breach since the user is able to modify the `tagFilters` you've set modifying the code from the browser. To keep using the JavaScript client (recommended for optimal latency) and target secured records, you can generate secured API key from your backend: 669 | 670 | ```ruby 671 | # generate a public API key for user 42. Here, records are tagged with: 672 | # - 'public' if they are visible by all users 673 | # - 'user_XXXX' if they are visible by user XXXX 674 | public_key = Algolia.generate_secured_api_key 'YourSearchOnlyApiKey', '(public,user_42)' 675 | ``` 676 | 677 | This public API key must then be used in your JavaScript code as follow: 678 | 679 | ```javascript 680 | 687 | ``` 688 | 689 | You can mix rate limits and secured API keys setting an extra `user_token` attribute both at API key generation-time and query-time. When set, a uniq user will be identified by her `IP + user_token` instead of only her `IP`. It allows you to restrict a single user to perform maximum `N` API calls per hour, even if she share her `IP` with another user. 690 | 691 | ```ruby 692 | # generate a public API key for user 42. Here, records are tagged with: 693 | # - 'public' if they are visible by all users 694 | # - 'user_XXXX' if they are visible by user XXXX 695 | public_key = Algolia.generate_secured_api_key 'YourRateLimitedApiKey', '(public,user_42)', 'user_42' 696 | ``` 697 | 698 | This public API key must then be used in your JavaScript code as follow: 699 | 700 | ```javascript 701 | 709 | ``` 710 | 711 | 712 | 713 | Copy or rename an index 714 | ------------- 715 | 716 | You can easily copy or rename an existing index using the `copy` and `move` commands. 717 | **Note**: Move and copy commands overwrite destination index. 718 | 719 | ```ruby 720 | # Rename MyIndex in MyIndexNewName 721 | puts Algolia.move_index("MyIndex", "MyIndexNewName") 722 | # Copy MyIndex in MyIndexCopy 723 | puts Algolia.copy_index("MyIndex", "MyIndexCopy") 724 | ``` 725 | 726 | The move command is particularly useful is you want to update a big index atomically from one version to another. For example, if you recreate your index `MyIndex` each night from a database by batch, you just have to: 727 | 1. Import your database in a new index using [batches](#batch-writes). Let's call this new index `MyNewIndex`. 728 | 1. Rename `MyNewIndex` in `MyIndex` using the move command. This will automatically override the old index and new queries will be served on the new one. 729 | 730 | ```ruby 731 | # Rename MyNewIndex in MyIndex (and overwrite it) 732 | puts Algolia.move_index("MyNewIndex", "MyIndex") 733 | ``` 734 | 735 | Backup / Retrieve all index content 736 | ------------- 737 | 738 | You can retrieve all index content for backup purpose or for analytics using the browse method. 739 | This method retrieve 1000 objects by API call and support pagination. 740 | 741 | ```ruby 742 | # Get first page 743 | puts index.browse(0) 744 | # Get second page 745 | puts index.browse(1) 746 | ``` 747 | 748 | Logs 749 | ------------- 750 | 751 | You can retrieve the last logs via this API. Each log entry contains: 752 | * Timestamp in ISO-8601 format 753 | * Client IP 754 | * Request Headers (API-Key is obfuscated) 755 | * Request URL 756 | * Request method 757 | * Request body 758 | * Answer HTTP code 759 | * Answer body 760 | * SHA1 ID of entry 761 | 762 | You can retrieve the logs of your last 1000 API calls and browse them using the offset/length parameters: 763 | * ***offset***: Specify the first entry to retrieve (0-based, 0 is the most recent log entry). Default to 0. 764 | * ***length***: Specify the maximum number of entries to retrieve starting at offset. Defaults to 10. Maximum allowed value: 1000. 765 | 766 | ```ruby 767 | # Get last 10 log entries 768 | puts Algolia.get_logs.to_json 769 | # Get last 100 log entries 770 | puts Algolia.get_logs(0, 100).to_json 771 | # Get last 100 errors 772 | puts Algolia.get_logs(0, 100, true).to_json 773 | ``` 774 | 775 | Mock 776 | ------------- 777 | 778 | For testing purpose, you may want to mock Algolia's API calls. We provide a [WebMock](https://github.com/bblimke/webmock) configuration that you can use including `algolia/webmock`: 779 | 780 | ```ruby 781 | require 'algolia/webmock' 782 | 783 | describe 'With a mocked client' do 784 | 785 | before(:each) do 786 | WebMock.enable! 787 | end 788 | 789 | it "shouldn't perform any API calls here" do 790 | index = Algolia::Index.new("friends") 791 | index.add_object!({ :name => "John Doe", :email => "john@doe.org" }) 792 | index.search('').should == {} # mocked 793 | index.clear_index 794 | index.delete_index 795 | end 796 | 797 | after(:each) do 798 | WebMock.disable! 799 | end 800 | 801 | end 802 | ``` 803 | 804 | 805 | 806 | --------------------------------------------------------------------------------