diff --git a/.circleci/config.yml b/.circleci/config.yml index 171c83e0..ec150e81 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,6 +6,10 @@ aliases: curl -s https://algoliasearch-client-keygen.herokuapp.com | sh >> $BASH_ENV fi + - &install_sqlite + name: Install SQLite + command: sudo apt-get update && sudo apt-get install -y sqlite3 libsqlite3-dev + - &check_bundler name: Which bundler? command: bundle -v @@ -33,7 +37,7 @@ aliases: references: default_docker_ruby_executor: &default_docker_ruby_executor - image: circleci/ruby:<< parameters.version >> + image: cimg/ruby:<< parameters.version >> environment: BUNDLE_JOBS: 3 BUNDLE_RETRY: 3 @@ -57,6 +61,7 @@ jobs: - *default_docker_ruby_executor steps: - checkout + - run: *install_sqlite - run: *check_bundler - restore_cache: *restore_cache - run: *install_bundler @@ -69,45 +74,16 @@ workflows: ci: jobs: - test: - name: 'Rails 5.1 - Ruby 2.4' - version: '2.4' - rails-version: '5.1' - sequel-version: '5.0' - - test: - name: 'Rails 5.1 - Ruby 2.5' - version: '2.5' - rails-version: '5.1' - sequel-version: '5.0' - - test: - name: 'Rails 5.1 - Ruby 2.6' - version: '2.6' - rails-version: '5.1' - sequel-version: '5.0' - - - - test: - name: 'Rails 6.0 - Ruby 2.5' - version: '2.5' - rails-version: '6.0' - sequel-version: '5.0' - - test: - name: 'Rails 6.0 - Ruby 2.6' - version: '2.6' - rails-version: '6.0' - sequel-version: '5.0' - - test: - name: 'Rails 6.0 - Ruby 2.7' - version: '2.7' - rails-version: '6.0' - sequel-version: '5.0' - - - test: - name: 'Rails 6.1 - Ruby 2.7' - version: '2.7' - rails-version: '6.1' - sequel-version: '5.0' + name: "Ruby << matrix.version >> - Rails << matrix.rails-version >>" + matrix: + parameters: + version: [ '2.5', '2.6', '2.7', '3.0', '3.1' ] + rails-version: [ '6.0', '6.1' ] + sequel-version: [ '5.0' ] - test: - name: 'Rails 6.1 - Ruby 3.0' - version: '3.0' - rails-version: '6.1' - sequel-version: '5.0' + name: "Ruby << matrix.version >> - Rails << matrix.rails-version >>" + matrix: + parameters: + version: ['3.0', '3.1'] + rails-version: ['7.0'] + sequel-version: ['5.0'] \ No newline at end of file diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml new file mode 100644 index 00000000..4c49ba97 --- /dev/null +++ b/.github/workflows/issue.yml @@ -0,0 +1,55 @@ +name: 'Issue sync with Jira' +on: + issues: + types: [opened] + +permissions: + issues: write + contents: read + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - name: Create ticket + uses: actions/github-script@v7 + with: + script: | + const action = context.payload.action; + if (action !== 'opened') { + return; + } + const title = context.payload.issue.title; + const body = context.payload.issue.body; + + const res = await fetch('https://algolia.atlassian.net/rest/api/2/issue', { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': `Basic ${{ secrets.JIRA_TOKEN }}` + }, + body: JSON.stringify({ + fields: { + description: `Issue created by ${context.actor} at ${context.payload.issue.html_url} \n\n${body}`, + issuetype: { + id: '10001' + }, + parent: { + key: 'DI-1911' + }, + project: { + id: '10118' + }, + summary: `[GH-ISSUE] ${title}` + }, + update: {} + }) + }); + + if (!res.ok) { + throw new Error(`Failed to create ticket: ${res.statusText} (${res.status}) - ${await res.text()}`); + } + + const data = await res.json(); + console.log(`Created ticket: ${data.key}`); diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 6ac3967b..27de2381 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -1,6 +1,10 @@ # CHANGELOG -## [Unreleased](https://github.com/algolia/algoliasearch-rails/compare/2.2.1...master) +## [Unreleased](https://github.com/algolia/algoliasearch-rails/compare/3.0.0...master) + +## [3.0.0](https://github.com/algolia/algoliasearch-rails/compare/2.3.2...3.0.0) +This new major version leverages the latest version of the Ruby Algolia API client. It also drops (official) support for Rails 5.x and Ruby versions older than 2.5 +For a list of known breaking changes, please refer to [the upgrade guide](./UPGRADING_TO_V3.MD) ## [2.3.2](https://github.com/algolia/algoliasearch-rails/compare/2.3.1...2.3.2) ### Added diff --git a/Gemfile b/Gemfile index 33526172..180e072c 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source "http://rubygems.org" gem 'json', '>= 1.5.1' -gem 'algolia', '< 3.0.0' +gem 'algolia', '>= 3.5.2' if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'rbx' gem 'rubysl', '~> 2.0', :platform => :rbx @@ -16,7 +16,7 @@ group :test do else gem 'sqlite3', '< 1.4.0', :platform => [:rbx, :ruby] end - gem 'rspec', '>= 2.5.0', '< 3.0' + gem 'rspec', '~> 3.0' gem 'jdbc-sqlite3', :platform => :jruby gem 'activerecord-jdbc-adapter', :platform => :jruby gem 'activerecord-jdbcsqlite3-adapter', :platform => :jruby @@ -27,7 +27,7 @@ group :test do end group :development do - gem 'rake', '~> 10.1.0' + gem 'rake', '>= 10.1.0' gem 'rdoc' end diff --git a/UPGRADING_TO_V3.MD b/UPGRADING_TO_V3.MD new file mode 100644 index 00000000..f94a479c --- /dev/null +++ b/UPGRADING_TO_V3.MD @@ -0,0 +1,66 @@ +Version 3 of the `algoliasearch-rails` gem replaces the Algolia API client version in use from version 2 to version 3. +These versions of the API client differ significantly, so you likely need to make code changes when updating. +We've tried keeping most of the changes internal, but there are still some breaking changes you need to be aware of when upgrading. + +If you encounter any breaking changes to the Rail integration that are not listed here, please open a Pull Request to add them to this list. + +## Breaking changes + +`algolia_ensure_init` (this method is protected and shouldn't be called manually, but we list it here anyways): the method no longer returns an initialized `index` object as this is not part of the new API client. The method now returns nothing, but it still ensures the index exists and applies settings if needed. + +--- +`Model.search`, `Model.raw_search`: response keys in the new API client are no longer strings, but are *always* symbols. For example: +```ruby +# Before +results = Product.raw_search('shirt') +p results['hits'] + +# After +results = Product.raw_search('shirt') +p results[:hits] +``` +--- +`Model.search_for_facet_values`: this no longer returns an array of hashes, but an array of objects of type `Algolia::Search::FacetHits`: +```ruby +# Before +facets = Color.search_for_facet_values('short_name', 'bl', :query => 'black') +puts facets.first['value'] + +# After +facets = Color.search_for_facet_values('short_name', 'bl', :query => 'black') +facets.first.value +``` + +--- +`Model.index_name` takes an additional, optional parameter. You can use this if you want to get the name of one of your replica indices, which ensures the index naming takes configuration that modifies the index name into account. +For example, if you have the `:per_environment` option set to true, it will automatically add the environment name in the index name. +```ruby +def Product + include AlgoliaSearch + + algoliasearch({ per_environment: true }) do + add_replica 'Suits', per_environment: true do + # replica settings + end + end + +end +main_index_name = Product.index_name +replica_index_name = Product.index_name('Suits') +``` + +--- +`AlgoliaSearch::Configuration.client_opts`, `AlgoliaSearch::Configuration::REQUIRED_CONFIGURATION` and `AlgoliaSearch::SafeIndex` have been removed. +If you need to configure the API client other than the ways that are provided now, it's recommended to set up an instance manually. + +--- +`Model.index` and `Model.algolia_index` have been removed, as there is no notion of an `Index` object in the new version of the API clients. +Instead, you can use `Model.index_name` to get the name of the index to target, and use this on an instance of the API client directly. + +```ruby +# Before +res = Product.index.search('shoe') + +# After +res = AlgoliaSearch.client.search_single_index(Product.index_name, { query: 'shoe' }) +``` diff --git a/algoliasearch-rails.gemspec b/algoliasearch-rails.gemspec index af05a538..64645a19 100644 --- a/algoliasearch-rails.gemspec +++ b/algoliasearch-rails.gemspec @@ -79,7 +79,7 @@ Gem::Specification.new do |s| if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then s.add_runtime_dependency(%q, [">= 1.5.1"]) - s.add_runtime_dependency(%q, ["< 3.0.0"]) + s.add_runtime_dependency(%q, [">= 3.5.2"]) s.add_development_dependency(%q, [">= 2.3.15"]) s.add_development_dependency(%q, [">= 0"]) s.add_development_dependency(%q, [">= 0"]) @@ -87,11 +87,11 @@ Gem::Specification.new do |s| s.add_development_dependency "rdoc" else s.add_dependency(%q, [">= 1.5.1"]) - s.add_dependency(%q, ["< 3.0.0"]) + s.add_dependency(%q, [">= 3.5.2"]) end else s.add_dependency(%q, [">= 1.5.1"]) - s.add_dependency(%q, ["< 3.0.0"]) + s.add_dependency(%q, [">= 3.5.2"]) end end diff --git a/lib/algoliasearch-rails.rb b/lib/algoliasearch-rails.rb index 82754dcc..21ee87f1 100644 --- a/lib/algoliasearch-rails.rb +++ b/lib/algoliasearch-rails.rb @@ -95,7 +95,6 @@ def initialize(options, &block) def use_serializer(serializer) @serializer = serializer - # instance_variable_set("@serializer", serializer) end def attribute(*names, &block) @@ -177,16 +176,7 @@ def get_attributes(object) end attributes.merge!(attributes_to_hash(@additional_attributes, object)) if @additional_attributes - - if @options[:sanitize] - sanitizer = begin - ::HTML::FullSanitizer.new - rescue NameError - # from rails 4.2 - ::Rails::Html::FullSanitizer.new - end - attributes = sanitize_attributes(attributes, sanitizer) - end + attributes = sanitize_attributes(attributes, Rails::Html::FullSanitizer.new) if @options[:sanitize] if @options[:force_utf8_encoding] && Object.const_defined?(:RUBY_VERSION) && RUBY_VERSION.to_f > 1.8 attributes = encode_attributes(attributes) @@ -241,10 +231,20 @@ def get_setting(name) end def to_settings + settings = to_hash + + # Remove the synonyms setting since those need to be set separately + settings.delete(:synonyms) + settings.delete("synonyms") + + Algolia::Search::IndexSettings.new(settings) + end + + def to_hash settings = {} OPTIONS.each do |k| v = get_setting(k) - settings[k] = v if !v.nil? + settings[setting_name(k)] = v if !v.nil? end if !@options[:replica] @@ -256,9 +256,18 @@ def to_settings end settings.delete(:replicas) if settings[:replicas].empty? end + settings end + def setting_name(name) + name.to_s.gsub(/::/, '/'). + gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). + gsub(/([a-z\d])([A-Z])/,'\1_\2'). + tr("-", "_"). + downcase + end + def add_index(index_name, options = {}, &block) raise ArgumentError.new('Cannot specify additional index on a replica index') if @options[:replica] raise ArgumentError.new('No block given') if !block_given? @@ -287,70 +296,6 @@ def additional_indexes autoload :AlgoliaJob, 'algoliasearch/algolia_job' end - # this class wraps an Algolia::Index object ensuring all raised exceptions - # are correctly logged or thrown depending on the `raise_on_failure` option - class SafeIndex - def initialize(name, raise_on_failure) - @index = AlgoliaSearch.client.init_index(name) - @raise_on_failure = raise_on_failure.nil? || raise_on_failure - end - - ::Algolia::Search::Index.instance_methods(false).each do |m| - define_method(m) do |*args, &block| - SafeIndex.log_or_throw(m, @raise_on_failure) do - @index.send(m, *args, &block) - end - end - end - - # special handling of wait_task to handle null task_id - def wait_task(task_id) - return if task_id.nil? && !@raise_on_failure # ok - SafeIndex.log_or_throw(:wait_task, @raise_on_failure) do - @index.wait_task(task_id) - end - end - - # special handling of get_settings to avoid raising errors on 404 - def get_settings(*args) - SafeIndex.log_or_throw(:get_settings, @raise_on_failure) do - begin - @index.get_settings(*args) - rescue Algolia::AlgoliaHttpError => e - return {} if e.code == 404 # not fatal - raise e - end - end - end - - # expose move as well - def self.move_index(old_name, new_name) - SafeIndex.log_or_throw(:move_index, true) do - AlgoliaSearch.client.move_index(old_name, new_name) - end - end - - private - def self.log_or_throw(method, raise_on_failure, &block) - begin - yield - rescue Algolia::AlgoliaError => e - raise e if raise_on_failure - # log the error - (Rails.logger || Logger.new(STDOUT)).error("[algoliasearch-rails] #{e.message}") - # return something - case method.to_s - when 'search' - # some attributes are required - { 'hits' => [], 'hitsPerPage' => 0, 'page' => 0, 'facets' => {}, 'error' => e } - else - # empty answer - { 'error' => e } - end - end - end - end - # these are the class methods added when AlgoliaSearch is included module ClassMethods @@ -367,7 +312,6 @@ class < algolia_object_id_of(o, options) end - last_task = index.save_objects(objects) + last_task = AlgoliaSearch.client.save_objects(index_name, objects).last.task_id end - index.wait_task(last_task.raw_response["taskID"]) if last_task and (synchronous || options[:synchronous]) + AlgoliaSearch.client.wait_for_task(index_name, last_task) if last_task and (synchronous || options[:synchronous]) end nil end @@ -530,28 +475,30 @@ def algolia_reindex(batch_size = AlgoliaSearch::IndexSettings::DEFAULT_BATCH_SIZ next if algolia_indexing_disabled?(options) next if options[:replica] + algolia_ensure_init(options, settings) + index_name = algolia_index_name(options) + # fetch the master settings - master_index = algolia_ensure_init(options, settings) - master_settings = master_index.get_settings rescue {} # if master doesn't exist yet + master_settings = AlgoliaSearch.client.get_settings(index_name).to_hash rescue {} # if master doesn't exist yet master_exists = master_settings != {} - master_settings.merge!(JSON.parse(settings.to_settings.to_json)) # convert symbols to strings + master_settings.merge!(settings.to_hash) # remove the replicas of the temporary index master_settings.delete :replicas master_settings.delete 'replicas' # init temporary index - src_index_name = algolia_index_name(options) - tmp_index_name = "#{src_index_name}.tmp" + tmp_index_name = "#{index_name}.tmp" tmp_options = options.merge({ :index_name => tmp_index_name }) tmp_options.delete(:per_environment) # already included in the temporary index_name tmp_settings = settings.dup if options[:check_settings] == false && master_exists - AlgoliaSearch.client.copy_index!(src_index_name, tmp_index_name, { scope: %w[settings synonyms rules] }) - tmp_index = SafeIndex.new(tmp_index_name, !!options[:raise_on_failure]) - else - tmp_index = algolia_ensure_init(tmp_options, tmp_settings, master_settings) + task_id = AlgoliaSearch.client.operation_index( + index_name, + Algolia::Search::OperationIndexParams.new(operation: Algolia::Search::OperationType::COPY, destination: tmp_index_name, scope: %w[settings synonyms rules]) + ).task_id + AlgoliaSearch.client.wait_for_task(index_name, task_id) end algolia_find_in_batches(batch_size) do |group| @@ -560,11 +507,15 @@ def algolia_reindex(batch_size = AlgoliaSearch::IndexSettings::DEFAULT_BATCH_SIZ group = group.select { |o| algolia_indexable?(o, tmp_options) } end objects = group.map { |o| tmp_settings.get_attributes(o).merge 'objectID' => algolia_object_id_of(o, tmp_options) } - tmp_index.save_objects(objects) + + AlgoliaSearch.client.save_objects(tmp_index_name, objects) end - move_task = SafeIndex.move_index(tmp_index.name, src_index_name) - master_index.wait_task(move_task.raw_response["taskID"]) if synchronous || options[:synchronous] + task_id = AlgoliaSearch.client.operation_index( + tmp_index_name, + Algolia::Search::OperationIndexParams.new(operation: "move", destination: index_name) + ).task_id + AlgoliaSearch.client.wait_for_task(index_name, task_id) if synchronous || options[:synchronous] end nil end @@ -572,27 +523,40 @@ def algolia_reindex(batch_size = AlgoliaSearch::IndexSettings::DEFAULT_BATCH_SIZ def algolia_set_settings(synchronous = false) algolia_configurations.each do |options, settings| if options[:primary_settings] && options[:inherit] - primary = options[:primary_settings].to_settings + primary = options[:primary_settings].to_settings.to_hash primary.delete :replicas primary.delete 'replicas' - final_settings = primary.merge(settings.to_settings) + final_settings = primary.merge(settings.to_settings.to_hash) else - final_settings = settings.to_settings + final_settings = settings.to_settings.to_hash end - index = SafeIndex.new(algolia_index_name(options), true) - task = index.set_settings(final_settings) - index.wait_task(task.raw_response["taskID"]) if synchronous + s = final_settings.map do |k, v| + [settings.setting_name(k), v] + end.to_h + + synonyms = s.delete("synonyms") || s.delete(:synonyms) + unless synonyms.nil? || synonyms.empty? + resp = AlgoliaSearch.client.save_synonyms(index_name,synonyms.map {|s| Algolia::Search::SynonymHit.new({object_id: s.join("-"), synonyms: s, type: "synonym"}) } ) + AlgoliaSearch.client.wait_for_task(index_name, resp.task_id) if synchronous || options[:synchronous] + end + + resp = AlgoliaSearch.client.set_settings(index_name, Algolia::Search::IndexSettings.new(s)) + AlgoliaSearch.client.wait_for_task(index_name, resp.task_id) if synchronous || options[:synchronous] end end def algolia_index_objects(objects, synchronous = false) algolia_configurations.each do |options, settings| next if algolia_indexing_disabled?(options) - index = algolia_ensure_init(options, settings) + algolia_ensure_init(options, settings) + index_name = algolia_index_name(options) + next if options[:replica] - task = index.save_objects(objects.map { |o| settings.get_attributes(o).merge 'objectID' => algolia_object_id_of(o, options) }) - index.wait_task(task.raw_response["taskID"]) if synchronous || options[:synchronous] + tasks = AlgoliaSearch.client.save_objects(index_name, objects.map { |o| settings.get_attributes(o).merge 'objectID' => algolia_object_id_of(o, options) }) + tasks.each do |task| + AlgoliaSearch.client.wait_for_task(index_name, task.task_id) if synchronous || options[:synchronous] + end end end @@ -600,22 +564,23 @@ def algolia_index!(object, synchronous = false) return if algolia_without_auto_index_scope algolia_configurations.each do |options, settings| next if algolia_indexing_disabled?(options) + object_id = algolia_object_id_of(object, options) - index = algolia_ensure_init(options, settings) + index_name = algolia_index_name(options) + algolia_ensure_init(options, settings) next if options[:replica] + if algolia_indexable?(object, options) raise ArgumentError.new("Cannot index a record with a blank objectID") if object_id.blank? + resp = AlgoliaSearch.client.save_object(index_name, settings.get_attributes(object).merge({ 'objectID' => algolia_object_id_of(object, options) })) if synchronous || options[:synchronous] - index.save_object!(settings.get_attributes(object).merge 'objectID' => algolia_object_id_of(object, options)) - else - index.save_object(settings.get_attributes(object).merge 'objectID' => algolia_object_id_of(object, options)) + AlgoliaSearch.client.wait_for_task(index_name, resp.task_id) end elsif algolia_conditional_index?(options) && !object_id.blank? # remove non-indexable objects + resp = AlgoliaSearch.client.delete_object(index_name, object_id) if synchronous || options[:synchronous] - index.delete_object!(object_id) - else - index.delete_object(object_id) + AlgoliaSearch.client.wait_for_task(index_name, resp.task_id) end end end @@ -628,12 +593,14 @@ def algolia_remove_from_index!(object, synchronous = false) raise ArgumentError.new("Cannot index a record with a blank objectID") if object_id.blank? algolia_configurations.each do |options, settings| next if algolia_indexing_disabled?(options) - index = algolia_ensure_init(options, settings) + algolia_ensure_init(options, settings) + index_name = algolia_index_name(options) + next if options[:replica] + + resp = AlgoliaSearch.client.delete_object(index_name, object_id) if synchronous || options[:synchronous] - index.delete_object!(object_id) - else - index.delete_object(object_id) + AlgoliaSearch.client.wait_for_task(index_name, resp.task_id) end end nil @@ -641,22 +608,38 @@ def algolia_remove_from_index!(object, synchronous = false) def algolia_clear_index!(synchronous = false) algolia_configurations.each do |options, settings| - next if algolia_indexing_disabled?(options) - index = algolia_ensure_init(options, settings) - next if options[:replica] - synchronous || options[:synchronous] ? index.clear_objects! : index.clear_objects - @algolia_indexes[settings] = nil + next if algolia_indexing_disabled?(options) || options[:replica] + + algolia_ensure_init(options, settings) + index_name = algolia_index_name(options) + res = AlgoliaSearch.client.clear_objects(index_name) + + if synchronous || options[:synchronous] + AlgoliaSearch.client.wait_for_task(index_name, res.task_id) + end end nil end + def algolia_raw_search(q, params = {}) - index_name = params.delete(:index) || + index_name_base = params.delete(:index) || params.delete('index') || params.delete(:replica) || params.delete('replica') - index = algolia_index(index_name) - index.search(q, Hash[params.map { |k,v| [k.to_s, v.to_s] }]) + + opts = algoliasearch_options + unless index_name_base.nil? + algolia_configurations.each do |o, s| + if o[:index_name].to_s == index_name_base.to_s + opts = o + ensure_algolia_index(index_name_base) + end + end + end + + index_name = algolia_index_name(opts, index_name_base) + AlgoliaSearch.client.search_single_index(index_name,Hash[params.to_h.map { |k,v| [k.to_s, v.to_s] }].merge({query: q})).to_hash end module AdditionalMethods @@ -672,7 +655,7 @@ def algolia_raw_answer end def algolia_facets - @algolia_json['facets'] + @algolia_json[:facets] end private @@ -688,7 +671,7 @@ def algolia_search(q, params = {}) params[:page] -= 1 if params[:page].to_i > 0 end json = algolia_raw_search(q, params) - hit_ids = json['hits'].map { |hit| hit['objectID'] } + hit_ids = json[:hits].map { |hit| hit[:objectID] } if defined?(::Mongoid::Document) && self.include?(::Mongoid::Document) condition_key = algolia_object_id_method.in else @@ -697,18 +680,18 @@ def algolia_search(q, params = {}) results_by_id = algoliasearch_options[:type].where(condition_key => hit_ids).index_by do |hit| algolia_object_id_of(hit) end - results = json['hits'].map do |hit| - o = results_by_id[hit['objectID'].to_s] + results = json[:hits].map do |hit| + o = results_by_id[hit[:objectID].to_s] if o - o.highlight_result = hit['_highlightResult'] - o.snippet_result = hit['_snippetResult'] + o.highlight_result = hit[:_highlightResult] + o.snippet_result = hit[:_snippetResult] o end end.compact # Algolia has a default limit of 1000 retrievable hits - total_hits = json['nbHits'].to_i < json['nbPages'].to_i * json['hitsPerPage'].to_i ? - json['nbHits'].to_i: json['nbPages'].to_i * json['hitsPerPage'].to_i - res = AlgoliaSearch::Pagination.create(results, total_hits, algoliasearch_options.merge({ :page => json['page'].to_i + 1, :per_page => json['hitsPerPage'] })) + total_hits = json[:nbHits].to_i < json[:nbPages].to_i * json[:hitsPerPage].to_i ? + json[:nbHits].to_i: json[:nbPages].to_i * json[:hitsPerPage].to_i + res = AlgoliaSearch::Pagination.create(results, total_hits, algoliasearch_options.merge({ :page => json[:page].to_i + 1, :per_page => json[:hitsPerPage] })) res.extend(AdditionalMethods) res.send(:algolia_init_raw_answer, json) res @@ -719,15 +702,16 @@ def algolia_search_for_facet_values(facet, text, params = {}) params.delete('index') || params.delete(:replica) || params.delete('replicas') - index = algolia_index(index_name) - query = Hash[params.map { |k, v| [k.to_s, v.to_s] }] - index.search_for_facet_values(facet, text, query)['facetHits'] + index_name ||= algolia_index_name(algoliasearch_options) + req = Algolia::Search::SearchForFacetValuesRequest.new({facet_query: text, params: params.to_query}) + + AlgoliaSearch.client.search_for_facet_values(index_name, facet, req).facet_hits end # deprecated (renaming) alias :algolia_search_facet :algolia_search_for_facet_values - def algolia_index(name = nil) + def ensure_algolia_index(name = nil) if name algolia_configurations.each do |o, s| return algolia_ensure_init(o, s) if o[:index_name].to_s == name.to_s @@ -737,9 +721,9 @@ def algolia_index(name = nil) algolia_ensure_init end - def algolia_index_name(options = nil) + def algolia_index_name(options = nil, index_name = nil) options ||= algoliasearch_options - name = options[:index_name] || model_name.to_s.gsub('::', '_') + name = index_name || options[:index_name] || model_name.to_s.gsub('::', '_') name = "#{name}_#{Rails.env.to_s}" if options[:per_environment] name end @@ -774,36 +758,46 @@ def algolia_must_reindex?(object) protected - def algolia_ensure_init(options = nil, settings = nil, index_settings = nil) + def algolia_ensure_init(options = nil, settings = nil, index_settings_hash = nil) raise ArgumentError.new('No `algoliasearch` block found in your model.') if algoliasearch_settings.nil? - @algolia_indexes ||= {} + @algolia_indexes_init ||= {} options ||= algoliasearch_options settings ||= algoliasearch_settings - return @algolia_indexes[settings] if @algolia_indexes[settings] + return if @algolia_indexes_init[settings] - @algolia_indexes[settings] = SafeIndex.new(algolia_index_name(options), algoliasearch_options[:raise_on_failure]) + index_name = algolia_index_name(options) - index_settings ||= settings.to_settings - index_settings = options[:primary_settings].to_settings.merge(index_settings) if options[:inherit] - replicas = index_settings.delete(:replicas) || - index_settings.delete('replicas') - index_settings[:replicas] = replicas unless replicas.nil? || options[:inherit] + + index_settings_hash ||= settings.to_settings.to_hash + index_settings_hash = options[:primary_settings].to_settings.to_hash.merge(index_settings_hash) if options[:inherit] + replicas = index_settings_hash.delete(:replicas) || index_settings_hash.delete('replicas') + index_settings_hash[:replicas] = replicas unless replicas.nil? || options[:inherit] options[:check_settings] = true if options[:check_settings].nil? current_settings = if options[:check_settings] && !algolia_indexing_disabled?(options) - @algolia_indexes[settings].get_settings(:getVersion => 1) rescue nil # if the index doesn't exist + AlgoliaSearch.client.get_settings(index_name, {:getVersion => 1}).to_hash rescue nil # if the index doesn't exist end - if !algolia_indexing_disabled?(options) && options[:check_settings] && algoliasearch_settings_changed?(current_settings, index_settings) - set_settings_method = options[:synchronous] ? :set_settings! : :set_settings - @algolia_indexes[settings].send(set_settings_method, index_settings) + if !algolia_indexing_disabled?(options) && options[:check_settings] && algoliasearch_settings_changed?(current_settings, index_settings_hash) + s = index_settings_hash.map do |k, v| + [settings.setting_name(k), v] + end.to_h + + synonyms = s.delete("synonyms") || s.delete(:synonyms) + unless synonyms.nil? || synonyms.empty? + resp = AlgoliaSearch.client.save_synonyms(index_name,synonyms.map {|s| Algolia::Search::SynonymHit.new({object_id: s.join("-"), synonyms: s, type: "synonym"}) } ) + AlgoliaSearch.client.wait_for_task(index_name, resp.task_id) if options[:synchronous] + end + + resp = AlgoliaSearch.client.set_settings(index_name, Algolia::Search::IndexSettings.new(s)) + AlgoliaSearch.client.wait_for_task(index_name, resp.task_id) if options[:synchronous] end - @algolia_indexes[settings] + return end private @@ -843,7 +837,7 @@ def algolia_object_id_changed?(o, options = nil) def algoliasearch_settings_changed?(prev, current) return true if prev.nil? current.each do |k, v| - prev_v = prev[k.to_s] + prev_v = prev[k.to_sym] || prev[k.to_s] if v.is_a?(Array) and prev_v.is_a?(Array) # compare array of strings, avoiding symbols VS strings comparison return true if v.map { |x| x.to_s } != prev_v.map { |x| x.to_s } diff --git a/lib/algoliasearch/configuration.rb b/lib/algoliasearch/configuration.rb index cd48b49f..83020fbb 100644 --- a/lib/algoliasearch/configuration.rb +++ b/lib/algoliasearch/configuration.rb @@ -1,10 +1,5 @@ module AlgoliaSearch module Configuration - REQUIRED_CONFIGURATION = { - user_agent: "Algolia for Rails (#{AlgoliaSearch::VERSION}); Rails (#{defined?(::Rails::VERSION::STRING) ? ::Rails::VERSION::STRING : 'unknown'})", - symbolize_keys: false - } - def initialize @client = nil end @@ -14,19 +9,8 @@ def configuration end def configuration=(configuration) - user_agent = [REQUIRED_CONFIGURATION[:user_agent], configuration[:append_to_user_agent]].compact.join('; ') @@configuration = default_configuration - .merge(configuration) - .merge(REQUIRED_CONFIGURATION) - .merge({ user_agent: user_agent }) - end - - def client_opts - @@opts ||= {} - end - - def client_opts=(opts) - @@opts = opts + .merge(configuration) end def client @@ -38,7 +22,16 @@ def client end def setup_client - @client = Algolia::Search::Client.new(Algolia::Search::Config.new(@@configuration), client_opts) + @client = Algolia::SearchClient.create( + @@configuration[:application_id], + @@configuration[:api_key], + { + user_agent_segments: [ + "Algolia for Rails (#{AlgoliaSearch::VERSION})", + "Rails (#{defined?(::Rails::VERSION::STRING) ? ::Rails::VERSION::STRING : 'unknown'})", + @@configuration[:append_to_user_agent] + ].compact + }) end def default_configuration diff --git a/lib/algoliasearch/pagination/pagy.rb b/lib/algoliasearch/pagination/pagy.rb index b6847316..520de28a 100644 --- a/lib/algoliasearch/pagination/pagy.rb +++ b/lib/algoliasearch/pagination/pagy.rb @@ -5,6 +5,7 @@ module AlgoliaSearch module Pagination class Pagy + def self.create(results, total_hits, options = {}) vars = { count: total_hits, @@ -12,7 +13,13 @@ def self.create(results, total_hits, options = {}) items: options[:per_page] } - pagy = ::Pagy.new(vars) + pagy_version = Gem::Version.new(::Pagy::VERSION) + pagy = if pagy_version >= Gem::Version.new('9.0') + ::Pagy.new(**vars) + else + ::Pagy.new(vars) + end + [pagy, results] end end diff --git a/lib/algoliasearch/version.rb b/lib/algoliasearch/version.rb index 2748a12b..bbc2c50d 100644 --- a/lib/algoliasearch/version.rb +++ b/lib/algoliasearch/version.rb @@ -1,3 +1,3 @@ module AlgoliaSearch - VERSION = '2.3.2' + VERSION = '3.0.0' end diff --git a/spec/integration_spec.rb b/spec/integration_spec.rb index 18a71a44..88b952e7 100644 --- a/spec/integration_spec.rb +++ b/spec/integration_spec.rb @@ -294,8 +294,9 @@ def nb_children end # create this index before the class actually loads, to ensure the customRanking is updated -index = AlgoliaSearch.client.init_index(safe_index_name('City_replica2')) -index.set_settings!({'customRanking' => ['desc(d)']}) +index_name = safe_index_name('City_replica2') +res = AlgoliaSearch.client.set_settings(index_name, Algolia::Search::IndexSettings.new(custom_ranking: ['desc(d)'])) +AlgoliaSearch.client.wait_for_task(index_name, res.task_id) class City < ActiveRecord::Base include AlgoliaSearch @@ -313,12 +314,12 @@ def geoloc_array add_attribute :a_null_lat, :a_lng customRanking ['desc(b)'] - add_replica safe_index_name('City_replica1'), :per_environment => true do + add_replica safe_index_name('City_replica1'), :per_environment => true, :synchronous => true do searchableAttributes ['country'] customRanking ['asc(a)'] end - add_replica safe_index_name('City_replica2'), :per_environment => true do + add_replica safe_index_name('City_replica2'), :per_environment => true, :synchronous => true do customRanking ['asc(a)', 'desc(c)'] end end @@ -364,7 +365,7 @@ def public? describe 'DisabledIndexing' do it 'should not call get_settings' do - expect_any_instance_of(Algolia::Search::Index).not_to receive(:get_settings) + expect_any_instance_of(Algolia::SearchClient).not_to receive(:get_settings) DisabledIndexing.send(:algolia_ensure_init) end end @@ -373,7 +374,7 @@ def public? before(:each) do # NOTE: # Redefine below class *each* time to avoid the cache in the class. - # If the cahce is ready, algolia_ensure_init call neither set_settings nor set_settings! ever. + # If the cache is ready, algolia_ensure_init call neither set_settings nor set_settings! ever. Object.send(:remove_const, :EnableCheckSettingsSynchronously) if Object.constants.include?(:EnableCheckSettingsSynchronously) class EnableCheckSettingsSynchronously < ActiveRecord::Base include AlgoliaSearch @@ -389,8 +390,8 @@ class EnableCheckSettingsSynchronously < ActiveRecord::Base end it 'should call set_setting with wait_task(sync)' do - expect_any_instance_of(Algolia::Search::Index).to receive(:set_settings).and_call_original # wait_task use this return val - expect_any_instance_of(Algolia::Search::Index).to receive(:wait_task) + expect_any_instance_of(Algolia::SearchClient).to receive(:set_settings).and_call_original # wait_task use this return val + expect_any_instance_of(Algolia::SearchClient).to receive(:wait_for_task) EnableCheckSettingsSynchronously.send(:algolia_ensure_init) end end @@ -401,7 +402,7 @@ class EnableCheckSettingsSynchronously < ActiveRecord::Base end it 'should not call set_setting' do - expect_any_instance_of(Algolia::Search::Index).not_to receive(:set_settings) + expect_any_instance_of(Algolia::SearchClient).not_to receive(:set_settings) EnableCheckSettingsSynchronously.send(:algolia_ensure_init) end end @@ -411,7 +412,7 @@ class EnableCheckSettingsSynchronously < ActiveRecord::Base before(:each) do # NOTE: # Redefine below class *each* time to avoid the cache in the class. - # If the cahce is ready, algolia_ensure_init call neither set_settings nor set_settings! ever. + # If the cache is ready, algolia_ensure_init call neither set_settings nor set_settings! ever. Object.send(:remove_const, :EnableCheckSettingsAsynchronously) if Object.constants.include?(:EnableCheckSettingsAsynchronously) class EnableCheckSettingsAsynchronously < ActiveRecord::Base include AlgoliaSearch @@ -427,8 +428,8 @@ class EnableCheckSettingsAsynchronously < ActiveRecord::Base end it 'should call set_setting without wait_task(sync)' do - expect_any_instance_of(Algolia::Search::Index).to receive(:set_settings) - expect_any_instance_of(Algolia::Search::Index).not_to receive(:wait_task) + expect_any_instance_of(Algolia::SearchClient).to receive(:set_settings) + expect_any_instance_of(Algolia::SearchClient).not_to receive(:wait_for_task) EnableCheckSettingsAsynchronously.send(:algolia_ensure_init) end end @@ -439,7 +440,7 @@ class EnableCheckSettingsAsynchronously < ActiveRecord::Base end it 'should not call set_setting' do - expect_any_instance_of(Algolia::Search::Index).not_to receive(:set_settings) + expect_any_instance_of(Algolia::SearchClient).not_to receive(:set_settings) EnableCheckSettingsAsynchronously.send(:algolia_ensure_init) end end @@ -451,7 +452,7 @@ class EnableCheckSettingsAsynchronously < ActiveRecord::Base end it 'should call get_settings' do - expect_any_instance_of(Algolia::Search::Index).to receive(:get_settings) + expect_any_instance_of(Algolia::SearchClient).to receive(:get_settings) SequelBook.send(:algolia_ensure_init) end @@ -541,11 +542,11 @@ class SubReplicas < ActiveRecord::Base searchableAttributes ['name'] customRanking ["asc(name)"] - add_index safe_index_name("Additional_Index"), :per_environment => true do + add_index safe_index_name("Additional_Index"), :per_environment => true, :synchronous => true do searchableAttributes ['name'] customRanking ["asc(name)"] - add_replica safe_index_name("Replica_Index"), :per_environment => true do + add_replica safe_index_name("Replica_Index"), :per_environment => true, :synchronous => true do searchableAttributes ['name'] customRanking ["desc(name)"] end @@ -658,8 +659,8 @@ class SerializedObject < ActiveRecord::Base it "should convert to utf-8" do EncodedString.create! results = EncodedString.raw_search '' - expect(results['hits'].size).to eq(1) - expect(results['hits'].first['value']).to eq("\xC2\xA0\xE2\x80\xA2\xC2\xA0".force_encoding('utf-8')) + expect(results[:hits].size).to eq(1) + expect(results[:hits].first[:value]).to eq("\xC2\xA0\xE2\x80\xA2\xC2\xA0".force_encoding('utf-8')) end end end @@ -768,7 +769,7 @@ class SerializedObject < ActiveRecord::Base describe 'Namespaced::Model' do before(:all) do - Namespaced::Model.index.clear_objects! + Namespaced::Model.clear_index!(true) end it "should have an index name without :: hierarchy" do @@ -825,11 +826,11 @@ class SerializedObject < ActiveRecord::Base @i1.children << NestedItem.create(:hidden => true) << NestedItem.create(:hidden => true) NestedItem.where(:id => [@i1.id, @i2.id]).reindex!(AlgoliaSearch::IndexSettings::DEFAULT_BATCH_SIZE, true) - result = NestedItem.index.get_object(@i1.id) - result['nb_children'].should == 2 + result = AlgoliaSearch.client.get_object(NestedItem.index_name, @i1.id.to_s) + result[:nb_children].should == 2 result = NestedItem.raw_search('') - result['nbHits'].should == 1 + result[:nbHits].should == 1 if @i2.respond_to? :update_attributes @i2.update_attributes :hidden => false @@ -838,7 +839,7 @@ class SerializedObject < ActiveRecord::Base end result = NestedItem.raw_search('') - result['nbHits'].should == 2 + result[:nbHits].should == 2 end end @@ -870,8 +871,8 @@ class SerializedObject < ActiveRecord::Base it "should be raw searchable" do results = Color.raw_search("blue") - results['hits'].size.should eq(1) - results['nbHits'].should eq(1) + results[:hits].size.should eq(1) + results[:nbHits].should eq(1) end it "should not auto index if scoped" do @@ -940,7 +941,7 @@ class SerializedObject < ActiveRecord::Base it "should index an array of objects" do json = Color.raw_search('') Color.index_objects Color.limit(1), true # reindex last color, `limit` is incompatible with the reindex! method - json['nbHits'].should eq(Color.raw_search('')['nbHits']) + json[:nbHits].should eq(Color.raw_search('')[:nbHits]) end it "should not index non-saved object" do @@ -956,11 +957,11 @@ class SerializedObject < ActiveRecord::Base @blue = Color.create!(:name => "blue", :short_name => "blu", :hex => 0x0000FF) @black = Color.create!(:name => "black", :short_name => "bla", :hex => 0x000000) @green = Color.create!(:name => "green", :short_name => "gre", :hex => 0x00FF00) - facets = Color.search_for_facet_values('short_name', 'bl', :query => 'black') + facets = Color.search_for_facet_values('short_name', 'bl', { :query => 'black' }) expect(facets.size).to eq(1) - expect(facets.first['value']).to eq('bla') - expect(facets.first['highlighted']).to eq('bla') - expect(facets.first['count']).to eq(1) + expect(facets.first.value).to eq('bla') + expect(facets.first.highlighted).to eq('bla') + expect(facets.first.count).to eq(1) end end @@ -1168,18 +1169,18 @@ class SerializedObject < ActiveRecord::Base end it "should be searchable using replica index" do - r = City.index(safe_index_name('City_replica1')).search 'no land' - r['nbHits'].should eq(1) + r = AlgoliaSearch.client.search_single_index(safe_index_name("City_replica1_#{Rails.env.to_s}"), { query: 'no land' }) + r.nb_hits.should eq(1) end it "should be searchable using replica index 2" do r = City.raw_search 'no land', :index => safe_index_name('City_replica1') - r['nbHits'].should eq(1) + r[:nbHits].should eq(1) end it "should be searchable using replica index 3" do r = City.raw_search 'no land', :replica => safe_index_name('City_replica1') - r['nbHits'].should eq(1) + r[:nbHits].should eq(1) end it "should be searchable using replica index 4" do @@ -1194,37 +1195,32 @@ class SerializedObject < ActiveRecord::Base it "should reindex with replicas in place" do City.reindex!(AlgoliaSearch::IndexSettings::DEFAULT_BATCH_SIZE, true) - expect(City.index.get_settings['replicas'].length).to eq(2) + expect(AlgoliaSearch.client.get_settings(City.index_name).replicas.length).to eq(2) end it "should reindex with replicas using a temporary index" do City.reindex(AlgoliaSearch::IndexSettings::DEFAULT_BATCH_SIZE, true) - expect(City.index.get_settings['replicas'].length).to eq(2) + expect(AlgoliaSearch.client.get_settings(City.index_name).replicas.length).to eq(2) end it "should not include the replicas setting on replicas" do City.send(:algolia_configurations).to_a.each do |v| if v[0][:replica] - expect(v[1].to_settings[:replicas]).to be_nil + expect(v[1].to_settings.replicas).to be_nil else - expect(v[1].to_settings[:replicas]).to match_array(["#{safe_index_name('City_replica1')}_#{Rails.env}", "#{safe_index_name('City_replica2')}_#{Rails.env}"]) + expect(v[1].to_settings.replicas).to match_array(["#{safe_index_name('City_replica1')}_#{Rails.env}", "#{safe_index_name('City_replica2')}_#{Rails.env}"]) end end end - it "should browse" do - total = City.index.search('')['nbHits'] - n = 0 - City.index.browse_objects do |hit| - n += 1 - end - expect(n).to eq(total) - end - it "should have set the custom ranking on all indices" do - expect(City.index.get_settings['customRanking']).to eq(['desc(b)']) - expect(City.index(safe_index_name('City_replica1')).get_settings['customRanking']).to eq(['asc(a)']) - expect(City.index(safe_index_name('City_replica2')).get_settings['customRanking']).to eq(['asc(a)', 'desc(c)']) + City.ensure_algolia_index + City.ensure_algolia_index(safe_index_name("City_replica1")) + City.ensure_algolia_index(safe_index_name("City_replica2")) + + expect(AlgoliaSearch.client.get_settings(City.index_name).custom_ranking).to eq(['desc(b)']) + expect(AlgoliaSearch.client.get_settings(City.index_name(nil, safe_index_name("City_replica1"))).custom_ranking).to eq(['asc(a)']) + expect(AlgoliaSearch.client.get_settings(City.index_name(nil, safe_index_name("City_replica2"))).custom_ranking).to eq(['asc(a)', 'desc(c)']) end end @@ -1253,7 +1249,8 @@ class ForwardToReplicas < ActiveRecord::Base end after(:each) do - ForwardToReplicas.index.delete! + res = AlgoliaSearch.client.delete_index(ForwardToReplicas.index_name) + AlgoliaSearch.client.wait_for_task(ForwardToReplicas.index_name, res.task_id) end it 'shouldn\'t have inherited from the primary' do @@ -1263,13 +1260,15 @@ class ForwardToReplicas < ActiveRecord::Base ForwardToReplicas.create(:name => 'val') ForwardToReplicas.reindex! - primary_settings = ForwardToReplicas.index.get_settings - expect(primary_settings['searchableAttributes']).to eq(%w(first_value)) - expect(primary_settings['attributesToHighlight']).to eq(%w(primary_highlight)) - replica_settings = ForwardToReplicas.index(safe_index_name('ForwardToReplicas_replica')).get_settings - expect(replica_settings['searchableAttributes']).to eq(nil) - expect(replica_settings['attributesToHighlight']).to eq(%w(replica_highlight)) + + primary_settings = AlgoliaSearch.client.get_settings(ForwardToReplicas.index_name) + expect(primary_settings.searchable_attributes).to eq(%w(first_value)) + expect(primary_settings.attributes_to_highlight).to eq(%w(primary_highlight)) + + replica_settings = AlgoliaSearch.client.get_settings(ForwardToReplicas.index_name(nil, safe_index_name('ForwardToReplicas_replica'))) + expect(replica_settings.searchable_attributes).to eq(nil) + expect(replica_settings.attributes_to_highlight).to eq(%w(replica_highlight)) end it 'should update the replica settings when changed' do @@ -1301,15 +1300,15 @@ class ForwardToReplicasTwo < ActiveRecord::Base ForwardToReplicasTwo.create(:name => 'val') ForwardToReplicasTwo.reindex! - primary_settings = ForwardToReplicas.index.get_settings - expect(primary_settings['searchableAttributes']).to eq(%w(second_value)) - expect(primary_settings['attributesToHighlight']).to eq(%w(primary_highlight)) + primary_settings = AlgoliaSearch.client.get_settings(ForwardToReplicas.index_name) + expect(primary_settings.searchable_attributes).to eq(%w(second_value)) + expect(primary_settings.attributes_to_highlight).to eq(%w(primary_highlight)) - replica_settings = ForwardToReplicas.index(safe_index_name('ForwardToReplicas_replica')).get_settings - expect(replica_settings['searchableAttributes']).to eq(%w(second_value)) - expect(replica_settings['attributesToHighlight']).to eq(%w(replica_highlight)) + replica_settings = AlgoliaSearch.client.get_settings(ForwardToReplicas.index_name(nil, safe_index_name('ForwardToReplicas_replica'))) + expect(replica_settings.searchable_attributes).to eq(%w(second_value)) + expect(replica_settings.attributes_to_highlight).to eq(%w(replica_highlight)) - expect(ForwardToReplicas.index.name).to eq(ForwardToReplicasTwo.index.name) + expect(ForwardToReplicas.index_name).to eq(ForwardToReplicasTwo.index_name) end it "shouldn't update the replica settings if there is no change" do @@ -1339,7 +1338,7 @@ class ForwardToReplicasTwo < ActiveRecord::Base ForwardToReplicas.create(:name => 'val') ForwardToReplicas.reindex! - expect_any_instance_of(Algolia::Search::Index).not_to receive(:set_settings!) + expect_any_instance_of(Algolia::SearchClient).not_to receive(:set_settings) ForwardToReplicasTwo.send :algolia_ensure_init @@ -1388,10 +1387,10 @@ class ForwardToReplicasTwo < ActiveRecord::Base if v[0][:replica] expect(v[0][:index_name]).to eq(safe_index_name("VirtualReplica_replica")) expect(v[0][:virtual]).to eq(true) - expect(v[1].to_settings[:replicas]).to be_nil + expect(v[1].to_settings.replicas).to be_nil else expect(v[0][:index_name]).to eq(safe_index_name("VirtualReplica_primary")) - expect(v[1].to_settings[:replicas]).to match_array(["virtual(#{safe_index_name("VirtualReplica_replica")})"]) + expect(v[1].to_settings.replicas).to match_array(["virtual(#{safe_index_name("VirtualReplica_replica")})"]) end end end @@ -1407,10 +1406,15 @@ class ForwardToReplicasTwo < ActiveRecord::Base end describe 'Book' do + require 'rails-html-sanitizer' + before(:all) do Book.clear_index!(true) - Book.index(safe_index_name('BookAuthor')).clear_objects - Book.index(safe_index_name('Book')).clear_objects + index_name_author = Book.index_name(nil, safe_index_name('BookAuthor')) + index_name_book = Book.index_name(nil, safe_index_name('Book')) + + AlgoliaSearch.client.wait_for_task(index_name_author, AlgoliaSearch.client.clear_objects(index_name_author).task_id) + AlgoliaSearch.client.wait_for_task(index_name_book, AlgoliaSearch.client.clear_objects(index_name_book).task_id) end it "should index the book in 2 indexes of 3" do @@ -1419,41 +1423,24 @@ class ForwardToReplicasTwo < ActiveRecord::Base expect(results.size).to eq(1) results.should include(@steve_jobs) - index_author = Book.index(safe_index_name('BookAuthor')) - index_author.should_not be_nil - results = index_author.search('steve') - results['hits'].length.should eq(0) - results = index_author.search('walter') - results['hits'].length.should eq(1) + results = Book.search("steve", index: safe_index_name('BookAuthor')) + results.length.should eq(0) + results = Book.search("walter", index: safe_index_name('BookAuthor')) + results.length.should eq(1) # premium -> not part of the public index - index_book = Book.index(safe_index_name('Book')) - index_book.should_not be_nil - results = index_book.search('steve') - results['hits'].length.should eq(0) + results = Book.search("steve", index: safe_index_name('Book')) + results.length.should eq(0) end it "should sanitize attributes" do @hack = Book.create! :name => "\"> hack0r", :author => "", :premium => true, :released => true b = Book.raw_search('hack') - expect(b['hits'].length).to eq(1) - begin - expect(b['hits'][0]['name']).to eq('"> hack0r') - expect(b['hits'][0]['author']).to eq('alert(1)') - expect(b['hits'][0]['_highlightResult']['name']['value']).to eq('"> hack0r') - rescue - # rails 4.2's sanitizer - begin - expect(b['hits'][0]['name']).to eq('"> hack0r') - expect(b['hits'][0]['author']).to eq('') - expect(b['hits'][0]['_highlightResult']['name']['value']).to eq('"> hack0r') - rescue - # jruby - expect(b['hits'][0]['name']).to eq('"> hack0r') - expect(b['hits'][0]['author']).to eq('') - expect(b['hits'][0]['_highlightResult']['name']['value']).to eq('"> hack0r') - end - end + + expect(b[:hits].length).to eq(1) + expect(b[:hits][0][:name]).to eq('"> hack0r') + expect(b[:hits][0][:author]).to eq('') + expect(b[:hits][0][:_highlightResult][:name][:value]).to eq('"> hack0r') end it "should handle removal in an extra index" do @@ -1461,9 +1448,8 @@ class ForwardToReplicasTwo < ActiveRecord::Base book = Book.create! :name => 'Public book', :author => 'me', :premium => false, :released => true # should be searchable in the 'Book' index - index = Book.index(safe_index_name('Book')) - results = index.search('Public book') - expect(results['hits'].size).to eq(1) + results = Book.search("Public book", index: safe_index_name('Book')) + expect(results.size).to eq(1) # update the book and make it non-public anymore (not premium, not released) if book.respond_to? :update_attributes @@ -1473,13 +1459,13 @@ class ForwardToReplicasTwo < ActiveRecord::Base end # should be removed from the index - results = index.search('Public book') - expect(results['hits'].size).to eq(0) + results = Book.search("Public book", index: safe_index_name('Book')) + expect(results.size).to eq(0) end it "should use the per_environment option in the additional index as well" do - index = Book.index(safe_index_name('Book')) - expect(index.name).to eq("#{safe_index_name('Book')}_#{Rails.env}") + index_name = Book.index_name(nil, safe_index_name('Book')) + expect(index_name).to eq("#{safe_index_name('Book')}_#{Rails.env}") end end @@ -1487,21 +1473,28 @@ class ForwardToReplicasTwo < ActiveRecord::Base before(:all) do require 'kaminari' AlgoliaSearch.configuration = { :application_id => ENV['ALGOLIA_APPLICATION_ID'], :api_key => ENV['ALGOLIA_API_KEY'], :pagination_backend => :kaminari } + + City.create :name => 'San Francisco', :country => 'USA', :lat => 37.75, :lng => -122.68 + City.create :name => 'Mountain View', :country => 'No man\'s land', :lat => 37.38, :lng => -122.08 + end + + after(:all) do + City.clear_index!(true) end it "should paginate" do pagination = City.search '' - pagination.total_count.should eq(City.raw_search('')['nbHits']) + pagination.total_count.should eq(City.raw_search('')[:nbHits]) p1 = City.search '', :page => 1, :hitsPerPage => 1 p1.size.should eq(1) p1[0].should eq(pagination[0]) - p1.total_count.should eq(City.raw_search('')['nbHits']) + p1.total_count.should eq(City.raw_search('')[:nbHits]) p2 = City.search '', :page => 2, :hitsPerPage => 1 p2.size.should eq(1) p2[0].should eq(pagination[1]) - p2.total_count.should eq(City.raw_search('')['nbHits']) + p2.total_count.should eq(City.raw_search('')[:nbHits]) end end @@ -1509,13 +1502,19 @@ class ForwardToReplicasTwo < ActiveRecord::Base before(:all) do require 'will_paginate' AlgoliaSearch.configuration = { :application_id => ENV['ALGOLIA_APPLICATION_ID'], :api_key => ENV['ALGOLIA_API_KEY'], :pagination_backend => :will_paginate } + City.create :name => 'San Francisco', :country => 'USA', :lat => 37.75, :lng => -122.68 + City.create :name => 'Mountain View', :country => 'No man\'s land', :lat => 37.38, :lng => -122.08 + end + + after(:all) do + City.clear_index!(true) end it "should paginate" do p1 = City.search '', :hitsPerPage => 2 p1.length.should eq(2) p1.per_page.should eq(2) - p1.total_entries.should eq(City.raw_search('')['nbHits']) + p1.total_entries.should eq(City.raw_search('')[:nbHits]) end end @@ -1530,39 +1529,38 @@ class ForwardToReplicasTwo < ActiveRecord::Base after(:all) do # Reset the configuration to avoid conflicts with other tests AlgoliaSearch.configuration = { :application_id => ENV['ALGOLIA_APPLICATION_ID'], :api_key => ENV['ALGOLIA_API_KEY'] } + City.clear_index!(true) end it "should paginate" do pagy, cities = City.search '', :hitsPerPage => 2 pagy.page.should eq(1) - pagy.items.should eq(2) - pagy.count.should eq(City.raw_search('')['nbHits']) + pagy.count.should eq(City.raw_search('')[:nbHits]) cities.length.should eq(2) cities.should be_an(Array) end end - describe 'Disabled' do before(:all) do - DisabledBoolean.index.clear_objects! - DisabledProc.index.clear_objects! - DisabledSymbol.index.clear_objects! + DisabledBoolean.send(:algolia_ensure_init) + DisabledProc.send(:algolia_ensure_init) + DisabledSymbol.send(:algolia_ensure_init) end it "should disable the indexing using a boolean" do DisabledBoolean.create :name => 'foo' - expect(DisabledBoolean.search('').size).to eq(0) + expect { DisabledBoolean.search('') }.to raise_error Algolia::AlgoliaHttpError # index doesn't exist end it "should disable the indexing using a proc" do DisabledProc.create :name => 'foo' - expect(DisabledProc.search('').size).to eq(0) + expect { DisabledProc.search('') }.to raise_error Algolia::AlgoliaHttpError # index doesn't exist end it "should disable the indexing using a symbol" do DisabledSymbol.create :name => 'foo' - expect(DisabledSymbol.search('').size).to eq(0) + expect { DisabledSymbol.search('') }.to raise_error Algolia::AlgoliaHttpError # index doesn't exist end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f8e6f4a6..4f4175fd 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -32,8 +32,12 @@ # Remove all indexes setup in this run in local or CI c.after(:suite) do safe_index_list.each do |i| - index = AlgoliaSearch.client.init_index(i['name']) - index.delete! + begin + res = AlgoliaSearch.client.delete_index(i.name) + AlgoliaSearch.client.wait_for_task(i.name, res.task_id) + rescue + # fail gracefully + end end end end @@ -48,7 +52,7 @@ def safe_index_name(name) # get a list of safe indexes in local or CI def safe_index_list - list = AlgoliaSearch.client.list_indexes['items'] - list = list.select { |index| index["name"].include?(SAFE_INDEX_PREFIX) } - list.sort_by { |index| index["primary"] || "" } + list = AlgoliaSearch.client.list_indices.items + list = list.select { |index| index.name.include?(SAFE_INDEX_PREFIX) } + list.sort_by { |index| index.primary || "" } end