From 841f7e48367f5b76bdbc6e2de79035277462db81 Mon Sep 17 00:00:00 2001 From: Satoshi Nabetani Date: Fri, 27 Dec 2024 19:06:06 +0900 Subject: [PATCH 1/7] Add Ruby 3.4 to CI (#1049) * Fix: Update RSpec test to properly use `include` matcher with expected values The test was updated to explicitly map `value` from the `groups` collection and use the `include` matcher with specific arguments ("Title1" and "Title2"). This resolves an `ArgumentError` caused by passing a block to `include`. ## Error Details ``` Failures: 1) field grouping allows grouping by a field Failure/Error: expect(search.group(:title).groups).to include { |g| g.value == "Title1" } ArgumentError: include() is not supported, please supply an argument # ./spec/integration/field_grouping_spec.rb:22:in 'block (2 levels) in ' Finished in 7.29 seconds (files took 0.29196 seconds to load) 1426 examples, 1 failure, 1 pending Failed examples: rspec ./spec/integration/field_grouping_spec.rb:17 # field grouping allows grouping by a field ``` * Add Ruby 3.4 to CI --- .github/workflows/ci.yml | 1 + sunspot/spec/integration/field_grouping_spec.rb | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b11ec8b09..a5d70ee55 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,7 @@ jobs: - '3.1' - '3.2' - '3.3' + - '3.4' - 'head' gem: ['sunspot', 'sunspot_rails', 'sunspot_solr'] update-format: ['xml', 'json'] diff --git a/sunspot/spec/integration/field_grouping_spec.rb b/sunspot/spec/integration/field_grouping_spec.rb index 0179d0ed5..b2a7bbef7 100644 --- a/sunspot/spec/integration/field_grouping_spec.rb +++ b/sunspot/spec/integration/field_grouping_spec.rb @@ -19,8 +19,7 @@ group :title end - expect(search.group(:title).groups).to include { |g| g.value == "Title1" } - expect(search.group(:title).groups).to include { |g| g.value == "Title2" } + expect(search.group(:title).groups.map(&:value)).to include("Title1", "Title2") end it "returns the number of matches unique groups" do From 03cc7382ad0c7d5d38f3d2f351a576007559c498 Mon Sep 17 00:00:00 2001 From: Nicholas Jakobsen Date: Mon, 16 Jun 2025 22:42:19 -0700 Subject: [PATCH 2/7] Update README.md Included examples of emptying the search index --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 414adcc68..20c05153f 100644 --- a/README.md +++ b/README.md @@ -1505,16 +1505,20 @@ part of the `save` callbacks. There are a number of ways to index manually within Ruby: ```ruby # On a class itself -Person.reindex +Person.reindex # Reindexes every record in the scope Sunspot.commit # or commit(true) for a soft commit (Solr4) +Person.solr_remove_all_from_index # Eliminates Person record from the index, including records that no longer exist # On mixed objects Sunspot.index [post1, item2] Sunspot.index person3 Sunspot.commit # or commit(true) for a soft commit (Solr4) +Sunspot.remove_all!(User, Person) # With autocommit Sunspot.index! [post1, item2, person3] +Person.solr_remove_all_from_index! +Sunspot.remove_all!(User) ``` If you make a change to the object's "schema" (code in the `searchable` block), From 2f44dcca6773b63cdc23d23b673b2a9f0831ff73 Mon Sep 17 00:00:00 2001 From: Satoshi Nabetani Date: Mon, 30 Jun 2025 16:17:13 +0900 Subject: [PATCH 3/7] Add Ruby 3.5+ compatibility and fix CI issues (#1051) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit addresses multiple compatibility issues to ensure Sunspot works seamlessly across Ruby versions 2.5 through 3.5+: ## Changes 1. **Standard Library Dependencies** - Add `ostruct` and `logger` as explicit runtime dependencies - These were extracted from Ruby's stdlib in 3.5+ and now require explicit gems - Preload in Rails spec helper to prevent autoload race conditions 2. **Ruby 2.5 Compatibility** - Constrain `psych` to < 4.0 for Ruby 2.5 environments - Psych 4.0 dropped Ruby 2.5 support, causing LoadError failures 3. **RDoc Task Loading** - Improve rdoc task loading with proper fallback mechanism - Support both modern (`rdoc/task`) and legacy (`rake/rdoctask`) APIs - Gracefully handle missing RDoc without breaking rake tasks 4. **CI Stability** - Fix flaky test in Ruby 2.7 with JSON format - Add targeted sleep to ensure Solr commit completes before search ## Testing All changes have been verified across the full Ruby version matrix: - Ruby 2.5, 2.6, 2.7, 3.0, 3.1, 3.2, 3.3, 3.4, and head - Both JSON and XML update formats - All three gems: sunspot, sunspot_rails, and sunspot_solr 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude --- sunspot/spec/integration/indexing_spec.rb | 2 ++ sunspot/sunspot.gemspec | 8 ++++++++ sunspot/tasks/rdoc.rake | 21 ++++++++++++--------- sunspot_rails/spec/spec_helper.rb | 4 ++++ sunspot_rails/sunspot_rails.gemspec | 7 +++++++ 5 files changed, 33 insertions(+), 9 deletions(-) diff --git a/sunspot/spec/integration/indexing_spec.rb b/sunspot/spec/integration/indexing_spec.rb index 09dc84977..27253b42a 100644 --- a/sunspot/spec/integration/indexing_spec.rb +++ b/sunspot/spec/integration/indexing_spec.rb @@ -11,6 +11,8 @@ post = Post.new(:title => 'test post') Sunspot.index!(post) Sunspot.remove!(post) + # Add a small sleep to ensure commit completes on Ruby 2.7 + sleep 0.1 if RUBY_VERSION.start_with?('2.7') expect(Sunspot.search(Post) { with(:title, 'test post') }.results).to be_empty end diff --git a/sunspot/sunspot.gemspec b/sunspot/sunspot.gemspec index a54e3d9bb..e0362be9e 100644 --- a/sunspot/sunspot.gemspec +++ b/sunspot/sunspot.gemspec @@ -28,9 +28,17 @@ Gem::Specification.new do |s| s.add_dependency 'rsolr', '>= 1.1.1', '< 3' s.add_dependency 'pr_geohash', '~>1.0' s.add_dependency 'bigdecimal' + s.add_dependency 'ostruct' + s.add_dependency 'logger' s.add_development_dependency 'rake', '~> 13.2' s.add_development_dependency 'rspec', '~> 3.7' s.add_development_dependency 'appraisal', '~> 2.5' + s.add_development_dependency 'rdoc' + + # Ruby 2.5 compatibility + if RUBY_VERSION < '2.6' + s.add_development_dependency 'psych', '< 4.0' + end s.rdoc_options << '--webcvs=http://github.com/outoftime/sunspot/tree/master/%s' << '--title' << 'Sunspot - Solr-powered search for Ruby objects - API Documentation' << diff --git a/sunspot/tasks/rdoc.rake b/sunspot/tasks/rdoc.rake index ac2814bf1..1c97e95a7 100644 --- a/sunspot/tasks/rdoc.rake +++ b/sunspot/tasks/rdoc.rake @@ -1,17 +1,20 @@ require 'sunspot/version' -rdoc_task = +# Try to load RDoc task - newer versions use 'rdoc/task', older versions use 'rake/rdoctask' +rdoc_task = nil +begin + require 'rdoc/task' + rdoc_task = RDoc::Task +rescue LoadError + # Fallback to older API for compatibility begin - require 'rdoc/task' - RDoc::Task + require 'rake/rdoctask' + rdoc_task = Rake::RDocTask rescue LoadError - begin - require 'rake/rdoctask' - Rake::RDocTask - rescue - nil - end + # RDoc is not available - skip rdoc tasks + rdoc_task = nil end +end if rdoc_task rdoc_task.new(:doc) do |rdoc| diff --git a/sunspot_rails/spec/spec_helper.rb b/sunspot_rails/spec/spec_helper.rb index 6ee6f4440..be3f77f6a 100644 --- a/sunspot_rails/spec/spec_helper.rb +++ b/sunspot_rails/spec/spec_helper.rb @@ -1,5 +1,9 @@ ENV["RAILS_ENV"] ||= 'test' +# Load these gems before Rails to avoid Ruby 3.0+ compatibility issues +require 'ostruct' +require 'logger' + require File.expand_path('config/environment', File.expand_path('../rails_app', __FILE__)) require File.expand_path('../../lib/sunspot_rails', __FILE__) require 'rspec/rails' diff --git a/sunspot_rails/sunspot_rails.gemspec b/sunspot_rails/sunspot_rails.gemspec index 4d4def536..42ecec647 100644 --- a/sunspot_rails/sunspot_rails.gemspec +++ b/sunspot_rails/sunspot_rails.gemspec @@ -32,11 +32,18 @@ Gem::Specification.new do |s| s.add_dependency 'rails', '>= 5' s.add_dependency 'sunspot', Sunspot::VERSION + s.add_dependency 'ostruct' + s.add_dependency 'logger' s.add_development_dependency 'appraisal', '~> 2.5' s.add_development_dependency 'bundler', '>= 1.3.0', '< 2.0' if RUBY_VERSION <= '2.0.0' s.add_development_dependency 'nokogiri', '< 1.7' if RUBY_VERSION <= '2.0.0' s.add_development_dependency 'rake', '~> 13.2' + + # Ruby 2.5 compatibility + if RUBY_VERSION < '2.6' + s.add_development_dependency 'psych', '< 4.0' + end s.rdoc_options << '--webcvs=http://github.com/outoftime/sunspot/tree/master/%s' << '--title' << 'Sunspot-Rails - Rails integration for the Sunspot Solr search library - API Documentation' << From 2f2880d4e664325d318711c9ad6050c183aef10a Mon Sep 17 00:00:00 2001 From: Nicholas Jakobsen Date: Mon, 17 Aug 2026 22:22:14 -0700 Subject: [PATCH 4/7] Emit half-open Solr ranges for exclusive-end Ruby ranges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Restriction::Between` writes every range as `[a TO b]`, so a Ruby range built with `...` quietly includes the endpoint it was written to exclude. An exclusive-end range now emits Solr's half-open form, `[a TO b}`. This matters wherever the end of one range is the start of the next — consecutive date ranges being the common case — where the shared boundary would otherwise match in both. Co-Authored-By: Claude Fable 5 --- sunspot/lib/sunspot/query/restriction.rb | 6 +++++- sunspot/spec/api/query/scope_examples.rb | 7 +++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/sunspot/lib/sunspot/query/restriction.rb b/sunspot/lib/sunspot/query/restriction.rb index 2074ad9d1..e9e0b7cc5 100644 --- a/sunspot/lib/sunspot/query/restriction.rb +++ b/sunspot/lib/sunspot/query/restriction.rb @@ -294,7 +294,11 @@ def solr_value(value = @value) end def to_solr_conditional - "[#{solr_value(@value.first)} TO #{solr_value(@value.last)}]" + if @value.exclude_end? + "[#{solr_value(@value.first)} TO #{solr_value(@value.last)}}" + else + "[#{solr_value(@value.first)} TO #{solr_value(@value.last)}]" + end end end diff --git a/sunspot/spec/api/query/scope_examples.rb b/sunspot/spec/api/query/scope_examples.rb index 1fe7ed7a6..e2dcf49f1 100644 --- a/sunspot/spec/api/query/scope_examples.rb +++ b/sunspot/spec/api/query/scope_examples.rb @@ -77,6 +77,13 @@ expect(connection).to have_last_search_including(:fq, 'average_rating_ft:[2\.0 TO 4\.0]') end + it 'scopes by between match with excluded end float range' do + search do + with(:average_rating).between 2.0...3.0 + end + expect(connection).to have_last_search_including(:fq, 'average_rating_ft:[2\.0 TO 3\.0}') + end + it 'scopes by any match with integer' do search do with(:category_ids).any_of [2, 7, 12] From a9b51fbc6ea48f1b1e80862e659650ac6bde94f9 Mon Sep 17 00:00:00 2001 From: Nicholas Jakobsen Date: Mon, 17 Aug 2026 23:04:49 -0700 Subject: [PATCH 5/7] Isolate the indexing delete specs from leftover documents `indexing_spec.rb`'s "should correctly remove by model instance" and "should correctly delete by ID" index a `Post` titled 'test post', delete it, and assert that a search for that title returns nothing. Neither clears the index first. `scoped_search_spec.rb`'s "prefix searching" block indexes its own `Post` titled 'test post' and, following the suite's clean-before convention, leaves it there. The specs run in randomized order, so an ordering that puts the prefix example last before the delete examples leaves that document in the index for them to find, and both fail on a document neither of them indexed. Running the prefix example and the two delete examples in that order reproduces the failure every time; the two delete examples pass 25 runs out of 25 in isolation. Instead of sleeping after the delete, the two examples now open with `Sunspot.remove_all!`, as "removes documents by query" three lines below them already does. The sleep could not have helped: `remove!` and `remove_by_id!` call `commit`, which posts a hard commit, and a Solr commit defaults to `waitSearcher=true`, so the new searcher is registered before the request returns. Raising the sleep to a full second does not prevent the failure. Refs https://github.com/sunspot/sunspot/pull/1053 Co-Authored-By: Claude Opus 5 (1M context) --- sunspot/spec/integration/indexing_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sunspot/spec/integration/indexing_spec.rb b/sunspot/spec/integration/indexing_spec.rb index 27253b42a..72f837f75 100644 --- a/sunspot/spec/integration/indexing_spec.rb +++ b/sunspot/spec/integration/indexing_spec.rb @@ -8,15 +8,15 @@ end it 'should correctly remove by model instance' do + Sunspot.remove_all! post = Post.new(:title => 'test post') Sunspot.index!(post) Sunspot.remove!(post) - # Add a small sleep to ensure commit completes on Ruby 2.7 - sleep 0.1 if RUBY_VERSION.start_with?('2.7') expect(Sunspot.search(Post) { with(:title, 'test post') }.results).to be_empty end it 'should correctly delete by ID' do + Sunspot.remove_all! post = Post.new(:title => 'test post') Sunspot.index!(post) Sunspot.remove_by_id!(Post, post.id) From 5684b77d5962098b09b608a00ecf82a8b6d0dced Mon Sep 17 00:00:00 2001 From: Nicholas Jakobsen Date: Mon, 17 Aug 2026 23:13:38 -0700 Subject: [PATCH 6/7] Add a :matches option to filter facet values by regular expression Solr 7.7 added `facet.matches`, which restricts the values a field facet returns to those matching a regular expression. Sunspot had no way to send it, so narrowing a facet to a subset of its values meant faceting on all of them and filtering the rows in Ruby, which pays for the full term enumeration and applies `:limit` and `:sort` to the unnarrowed set. `facet :title, :matches => 'Test.*'` now emits `f.title_ss.facet.matches`, alongside the other per-facet parameters `AbstractFieldFacet` already qualifies with `f..`. Qualifying it is what keeps the pattern on the facet it was given to. Solr's unqualified `facet.matches` applies to every facet in the request, which empties the rows of the other facets when they are string fields, and fails the request outright with `BytesRef term filters (facet.matches, facet.contains, facet.excludeTerms) are not supported on numeric types` when any of them is numeric. The pattern is a Java regular expression matched against the whole indexed value, so `Rings` matches the value `Rings` and not the value `Lord of the Rings`. The specs assert the emitted parameters rather than the returned rows, because `sunspot_solr` bundles Solr 5.3.1 and it ignores `facet.matches`. The behaviour was verified end to end against Solr 8.11.4. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 15 +++++++++++++++ .../lib/sunspot/query/abstract_field_facet.rb | 3 +++ sunspot/spec/api/query/faceting_examples.rb | 17 +++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/README.md b/README.md index 20c05153f..f7ea3e2c8 100644 --- a/README.md +++ b/README.md @@ -498,6 +498,21 @@ search.facet(:author_id).rows.each do |facet| end ``` +Facet values can be restricted to those matching a regular expression. Requires Solr 7.7 or +higher. + +```ruby +# Counts for post titles mentioning 'Lord' before 'Rings', in any case +search = Post.search do + facet :title, matches: '(?i).*Lord.*Rings.*' +end +``` + +The pattern is a Java regular expression matched against the whole indexed value, so `Rings` +matches the value `Rings` and not the value `Lord of the Rings`. Wrap the pattern in `.*` to +match anywhere within a value. Only the facet the option is given to is restricted; other +facets in the same search return all of their values. + #### Query Facets ```ruby diff --git a/sunspot/lib/sunspot/query/abstract_field_facet.rb b/sunspot/lib/sunspot/query/abstract_field_facet.rb index 046964ffb..03c32991e 100644 --- a/sunspot/lib/sunspot/query/abstract_field_facet.rb +++ b/sunspot/lib/sunspot/query/abstract_field_facet.rb @@ -30,6 +30,9 @@ def to_params if @options[:prefix] params[qualified_param('prefix')] = @options[:prefix].to_s end + if @options[:matches] + params[qualified_param('matches')] = @options[:matches].to_s + end params[qualified_param('mincount')] = case when @options[:minimum_count] then @options[:minimum_count].to_i diff --git a/sunspot/spec/api/query/faceting_examples.rb b/sunspot/spec/api/query/faceting_examples.rb index 6773401e4..adc6ed5dd 100644 --- a/sunspot/spec/api/query/faceting_examples.rb +++ b/sunspot/spec/api/query/faceting_examples.rb @@ -89,6 +89,23 @@ end expect(connection).to have_last_search_with(:"f.title_ss.facet.prefix" => 'Test') end + + it 'sets the facet matches pattern' do + search do + facet :title, :matches => 'Test.*' + end + expect(connection).to have_last_search_with(:"f.title_ss.facet.matches" => 'Test.*') + end + + it 'does not restrict other facets in the same search' do + search do + facet :title, :matches => 'Test.*' + facet :category_ids + end + expect(connection).to have_last_search_with(:"f.title_ss.facet.matches" => 'Test.*') + expect(connection).not_to have_last_search_with(:"f.category_ids_im.facet.matches") + expect(connection).not_to have_last_search_with(:"facet.matches") + end it 'sends a query facet for :any extra' do search do From bbe8da496e4185d853773f530ae8e2698e3a2b86 Mon Sep 17 00:00:00 2001 From: Nicholas Jakobsen Date: Tue, 6 Oct 2026 00:26:36 -0700 Subject: [PATCH 7/7] test: Keep json below 3 for the Rails 6.1 and 7.0 appraisals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI now resolves json 3.0.2, and in the `sunspot_rails` jobs on Ruby 3.0–3.3 with JSON updates, which run the Rails 6.1 and 7.0 appraisals, every example failed in `Sunspot.remove_all!` with `ArgumentError: unknown keyword: quirks_mode` from RSolr's `to_json`. The XML-update jobs don't serialize with `to_json` and pass. Upstream's last green run on this tree (2026-08-18) predates json 3. The Rails 6.1 and 7.0 appraisals now require `json < 3`. Co-Authored-By: Claude Opus 5.5 --- sunspot_rails/Appraisals | 1 + 1 file changed, 1 insertion(+) diff --git a/sunspot_rails/Appraisals b/sunspot_rails/Appraisals index ca3d08520..d24067e7a 100644 --- a/sunspot_rails/Appraisals +++ b/sunspot_rails/Appraisals @@ -21,6 +21,7 @@ if Gem::Version.new('3.0.0') <= ruby_version && ruby_version < Gem::Version.new( gem 'sunspot', path: File.expand_path('sunspot', ENV['SUNSPOT_LIB_HOME']) gem 'sunspot_solr', path: File.expand_path('sunspot_solr', ENV['SUNSPOT_LIB_HOME']) gem 'rails', "~> #{rails_version}" + gem 'json', '< 3' # Under json 3, these Rails versions' to_json raises "unknown keyword: quirks_mode" gem 'sprockets', '~> 3.0' gem 'progress_bar', '~> 1.0.5', require: false gem 'sqlite3', '~> 1.4.0'