sunspot 2.2.8 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 97232f93d9d1c5ce704e6249cd1afcec04f7f231
4
- data.tar.gz: 9eb35de3e65284cdec3eb299d6085cc32e662590
2
+ SHA256:
3
+ metadata.gz: 698b438f344a3e391c87d9f755dc227869fefad88555b261ef4dcd9cc9d9c1d1
4
+ data.tar.gz: 2ba1b782a43a5eafd6f1bc6397ef221fe94bbbb92f1036505530719a50f07882
5
5
  SHA512:
6
- metadata.gz: 5bd0d81960ded04f07fdfa72811ddf959c5c97b0339e58246761469d3f9b4d9a82bf71ec69e8ee222b05ff9c07b5ff406f2702ef1ab8caeacbb1d219415b0044
7
- data.tar.gz: b8adf8fc509ea65e460139687084832f8353e81e6ad1fd50f976db0a721545934761894ca42281962b19ce64b179a69b607dd3bb05b735b5fc53603aff0685a5
6
+ metadata.gz: 8f89e196fd125f6c0b2bd8eda2aecf7c288860ff14ab128d695f61252344fc07b0011ed8c10d8c5578227eae814df368fecf3332d306ae2a79e9b5da593dfc97
7
+ data.tar.gz: 780ca66b8d6e569a994670b2e6f990444333dc03f109bd015c7f26924673e801281f611f5d08b14b48afb35b6177099747113e0bbdc10a07aebe4cee5f93c8ea
@@ -26,6 +26,7 @@ module Sunspot
26
26
  read_timeout nil
27
27
  open_timeout nil
28
28
  proxy nil
29
+ update_format :xml
29
30
  end
30
31
  master_solr do
31
32
  url nil
@@ -333,6 +333,17 @@ module Sunspot
333
333
  end
334
334
  end
335
335
 
336
+ def json_facet(*field_names)
337
+ options = Sunspot::Util.extract_options_from(field_names)
338
+
339
+ field_names.each do |field_name|
340
+ field = @setup.field(field_name)
341
+ facet = Sunspot::Util.parse_json_facet(field_name, options, @setup)
342
+ @search.add_json_facet(field, options)
343
+ @query.add_query_facet(facet)
344
+ end
345
+ end
346
+
336
347
  def stats(*field_names, &block)
337
348
  options = Sunspot::Util.extract_options_from(field_names)
338
349
 
@@ -13,6 +13,13 @@ module Sunspot
13
13
  @search_stats.add_facet(field)
14
14
  end
15
15
  end
16
+
17
+ def json_facet(field_name, options = {})
18
+ field = @setup.field(field_name)
19
+ facet = Sunspot::Util.parse_json_facet(field_name, options, @setup)
20
+ @query_stats.add_json_facet(facet)
21
+ end
22
+
16
23
  end
17
24
  end
18
25
  end
@@ -1,6 +1,6 @@
1
- %w(filter abstract_field_facet connective boost_query date_field_facet
2
- range_facet abstract_fulltext dismax join field_list
3
- field_facet highlighting pagination restriction common_query spellcheck
1
+ %w(filter abstract_field_facet abstract_json_field_facet connective boost_query date_field_facet field_json_facet
2
+ range_facet range_json_facet date_field_json_facet abstract_fulltext dismax join
3
+ field_list field_facet highlighting pagination restriction common_query spellcheck
4
4
  standard_query more_like_this more_like_this_query geo geofilt bbox query_facet
5
5
  scope sort sort_composite text_field_boost function_query field_stats
6
6
  composite_fulltext group group_query).each do |file|
@@ -0,0 +1,70 @@
1
+ module Sunspot
2
+ module Query
3
+ class AbstractJsonFieldFacet
4
+
5
+ attr_accessor :field
6
+
7
+ DISTINCT_STRATEGIES = [:unique, :hll]
8
+
9
+ def initialize(field, options, setup)
10
+ @field, @options, @setup = field, options, setup
11
+ end
12
+
13
+ def init_params
14
+ params = {}
15
+ params[:limit] = @options[:limit] unless @options[:limit].nil?
16
+ params[:mincount] = @options[:minimum_count] unless @options[:minimum_count].nil?
17
+ params[:sort] = { @options[:sort] => @options[:sort_type]||'desc' } unless @options[:sort].nil?
18
+ params[:prefix] = @options[:prefix] unless @options[:prefix].nil?
19
+ params[:offset] = @options[:offset] unless @options[:offset].nil?
20
+
21
+ if !@options[:distinct].nil?
22
+ dist_opts = @options[:distinct]
23
+ raise Exception.new("Need to specify a strategy") if dist_opts[:strategy].nil?
24
+ raise Exception.new("The strategy must be one of #{DISTINCT_STRATEGIES}") unless DISTINCT_STRATEGIES.include?(dist_opts[:strategy])
25
+ @stategy = dist_opts[:strategy]
26
+ @group_by = dist_opts[:group_by].nil? ? @field : @setup.field(dist_opts[:group_by])
27
+ params[:field] = @group_by.indexed_name
28
+ params[:facet] = {}
29
+ params[:facet][:distinct] = "#{@stategy}(#{@field.indexed_name})"
30
+ end
31
+
32
+ params
33
+ end
34
+
35
+ def get_params
36
+ query = field_name_with_local_params
37
+ nested_params = recursive_nested_params(@options)
38
+
39
+ if !nested_params.nil?
40
+ query[@field.name][:facet] ||= {}
41
+ query[@field.name][:facet].merge!(nested_params)
42
+ end
43
+ query
44
+ end
45
+
46
+ def to_params
47
+ { 'json.facet' => self.get_params.to_json }
48
+ end
49
+
50
+ private
51
+
52
+ def recursive_nested_params(options)
53
+ if !options[:nested].nil? && options[:nested].is_a?(Hash)
54
+ opts = options[:nested]
55
+ field_name = opts[:field]
56
+
57
+ options = Sunspot::Util.extract_options_from([opts])
58
+ params = Sunspot::Util.parse_json_facet(field_name, options, @setup).field_name_with_local_params
59
+ if !opts.nil?
60
+ nested_params = recursive_nested_params(opts)
61
+ params[field_name][:facet] = nested_params unless nested_params.nil?
62
+ end
63
+
64
+ params
65
+ end
66
+ end
67
+
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,25 @@
1
+ module Sunspot
2
+ module Query
3
+ class DateFieldJsonFacet < AbstractJsonFieldFacet
4
+
5
+ def initialize(field, options, setup)
6
+ raise Exception.new('Need to specify a time_range') if options[:time_range].nil?
7
+ @start = options[:time_range].first
8
+ @end = options[:time_range].last
9
+ @gap = "+#{options[:gap] || 86400}SECONDS"
10
+ super
11
+ end
12
+
13
+ def field_name_with_local_params
14
+ params = {}
15
+ params[:type] = 'range'
16
+ params[:field] = @field.indexed_name
17
+ params[:start] = @field.to_indexed(@start)
18
+ params[:end] = @field.to_indexed(@end)
19
+ params[:gap] = @gap
20
+ params.merge!(init_params)
21
+ { @field.name => params }
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,19 @@
1
+ module Sunspot
2
+ module Query
3
+ class FieldJsonFacet < AbstractJsonFieldFacet
4
+
5
+ def initialize(field, options, setup)
6
+ super
7
+ end
8
+
9
+ def field_name_with_local_params
10
+ {
11
+ @field.name => {
12
+ type: 'terms',
13
+ field: @field.indexed_name,
14
+ }.merge!(init_params)
15
+ }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -10,9 +10,42 @@ module Sunspot
10
10
  @facets << field
11
11
  end
12
12
 
13
+ def add_json_facet(json_facet)
14
+ @json_facet = json_facet
15
+ end
16
+
13
17
  def to_params
14
- params = { :stats => true, :"stats.field" => [@field.indexed_name]}
15
- params[facet_key] = @facets.map(&:indexed_name) unless @facets.empty?
18
+ params = {}
19
+ if !@json_facet.nil?
20
+ params['json.facet'] = recursive_add_stats(@json_facet.get_params).to_json
21
+ else
22
+ params.merge!(:stats => true, :"stats.field" => [@field.indexed_name])
23
+ params[facet_key] = @facets.map(&:indexed_name) unless @facets.empty?
24
+ end
25
+ params
26
+ end
27
+
28
+ STATS_FUNCTIONS = [:min, :max, :sum, :avg, :sumsq]
29
+
30
+ def recursive_add_stats(query)
31
+ query.keys.each do |k|
32
+ if !query[k][:facet].nil?
33
+ query[k][:facet] = recursive_add_stats(query[k][:facet])
34
+ end
35
+ query[k][:facet] ||= {}
36
+ query[k][:sort] = { @options[:sort] => @options[:sort_type]||'desc' } unless @options[:sort].nil?
37
+ query[k][:facet].merge!(json_stats_params)
38
+ end
39
+ query
40
+ end
41
+
42
+ def json_stats_params
43
+ params = {}
44
+ STATS_FUNCTIONS.each { |s| params[s] = "#{s.to_s}(#{@field.indexed_name})" }
45
+ unless @options[:stats].nil?
46
+ to_remove = STATS_FUNCTIONS - @options[:stats]
47
+ to_remove.map { |s| params.delete(s)}
48
+ end
16
49
  params
17
50
  end
18
51
 
@@ -0,0 +1,28 @@
1
+ module Sunspot
2
+ module Query
3
+ class RangeJsonFacet < AbstractJsonFieldFacet
4
+
5
+ SECONDS_IN_DAY = 86400
6
+
7
+ def initialize(field, options, setup)
8
+ raise Exception.new("Need to specify a range") if options[:range].nil?
9
+ @start = options[:range].first
10
+ @end = options[:range].last
11
+ @gap = options[:gap] || SECONDS_IN_DAY
12
+ super
13
+ end
14
+
15
+ def field_name_with_local_params
16
+ {
17
+ @field.name => {
18
+ type: 'range',
19
+ field: @field.indexed_name,
20
+ start: @field.to_indexed(@start),
21
+ end: @field.to_indexed(@end),
22
+ gap: @gap
23
+ }.merge!(init_params)
24
+ }
25
+ end
26
+ end
27
+ end
28
+ end
@@ -88,8 +88,16 @@ module Sunspot
88
88
  # Return an XML representation of this schema using the ERB template
89
89
  #
90
90
  def to_xml
91
- template = File.join(File.dirname(__FILE__), '..', '..', 'templates', 'schema.xml.erb')
92
- ERB.new(File.read(template), nil, '-').result(binding)
91
+ template_path = File.join(File.dirname(__FILE__), '..', '..', 'templates', 'schema.xml.erb')
92
+ template_text = File.read(template_path)
93
+
94
+ erb = if RUBY_VERSION >= '2.6'
95
+ ERB.new(template_text, trim_mode: '-')
96
+ else
97
+ ERB.new(template_text, nil, '-')
98
+ end
99
+
100
+ erb.result(binding)
93
101
  end
94
102
 
95
103
  private
@@ -1,6 +1,7 @@
1
- %w(abstract_search standard_search more_like_this_search query_facet field_facet
2
- date_facet range_facet facet_row hit highlight field_group group hit_enumerable
3
- stats_row field_stats stats_facet query_group).each do |file|
1
+ %w(abstract_search standard_search more_like_this_search query_facet
2
+ field_facet field_json_facet date_facet range_facet json_facet_stats
3
+ facet_row json_facet_row hit highlight field_group group hit_enumerable
4
+ stats_row stats_json_row field_stats stats_facet query_group).each do |file|
4
5
  require File.join(File.dirname(__FILE__), 'search', file)
5
6
  end
6
7
 
@@ -182,6 +182,10 @@ module Sunspot
182
182
  end
183
183
  end
184
184
 
185
+ def json_facet_stats(name, options = {})
186
+ JsonFacetStats.new(name, self, options)
187
+ end
188
+
185
189
  #
186
190
  # Deprecated in favor of optional second argument to #facet
187
191
  #
@@ -193,6 +197,10 @@ module Sunspot
193
197
  @solr_result['facet_counts']
194
198
  end
195
199
 
200
+ def json_facet_response #:nodoc:
201
+ @solr_result['facets']
202
+ end
203
+
196
204
  def stats_response #:nodoc:
197
205
  @solr_result['stats']['stats_fields']
198
206
  end
@@ -255,6 +263,11 @@ module Sunspot
255
263
  add_stats(field.name, FieldStats.new(field, self))
256
264
  end
257
265
 
266
+ def add_json_facet(field, options = {})
267
+ name = (options[:name] || field.name)
268
+ add_facet(name, FieldJsonFacet.new(field, self, options))
269
+ end
270
+
258
271
  def highlights_for(doc) #:nodoc:
259
272
  if @solr_result['highlighting']
260
273
  @solr_result['highlighting'][doc['id']]
@@ -0,0 +1,33 @@
1
+ module Sunspot
2
+ module Search
3
+ class FieldJsonFacet
4
+
5
+ attr_reader :name
6
+
7
+ def initialize(field, search, options)
8
+ @name, @search, @options = name, search, options
9
+ @field = field
10
+ end
11
+
12
+ def rows
13
+ @rows ||=
14
+ begin
15
+ json_facet_response = @search.json_facet_response[@field.name.to_s]
16
+ data = json_facet_response.nil? ? [] : json_facet_response['buckets']
17
+ rows = []
18
+ data.each do |d|
19
+ rows << JsonFacetRow.new(d, self)
20
+ end
21
+
22
+ if @options[:sort] == :count
23
+ rows.sort! { |lrow, rrow| rrow.count <=> lrow.count }
24
+ else
25
+ rows.sort! { |lrow, rrow| lrow.value <=> rrow.value }
26
+ end
27
+ rows
28
+ end
29
+
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,40 @@
1
+ module Sunspot
2
+ module Search
3
+ class JsonFacetRow
4
+ attr_reader :value, :count, :nested
5
+ attr_writer :instance #:nodoc:
6
+
7
+ def initialize(data, facet) #:nodoc:
8
+ @value = data['val']
9
+ @count = data['distinct'] || data['count']
10
+ @facet = facet
11
+ @nested_key = data.keys.select { |k| data[k].is_a?(Hash) }.first
12
+ @nested = recursive_nested_initialization(data) unless @nested_key.nil?
13
+ end
14
+
15
+ #
16
+ # Return the instance referenced by this facet row. Only valid for field
17
+ # facets whose fields are defined with the :references key.
18
+ #
19
+ def instance
20
+ if !defined?(@instance)
21
+ @facet.populate_instances
22
+ end
23
+ @instance
24
+ end
25
+
26
+ def inspect
27
+ "<Sunspot::Search::FacetRow:#{value.inspect} (#{count}) #{nested.nil? ? '' : " nested_count=#{nested.size}"}>"
28
+ end
29
+
30
+ private
31
+
32
+ def recursive_nested_initialization(data)
33
+ data[@nested_key]['buckets'].map do |d|
34
+ JsonFacetRow.new(d, @facet)
35
+ end
36
+ end
37
+
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,23 @@
1
+ module Sunspot
2
+ module Search
3
+ class JsonFacetStats
4
+ def initialize(field, search, options)
5
+ @field, @search, @options = field, search, options
6
+ end
7
+
8
+ def rows
9
+ @rows ||=
10
+ begin
11
+ json_facet_response = @search.json_facet_response[@field.to_s]
12
+ data = json_facet_response.nil? ? [] : json_facet_response['buckets']
13
+ rows = []
14
+ data.each do |d|
15
+ rows << StatsJsonRow.new(d, nil, d['val'])
16
+ end
17
+ rows
18
+ end
19
+
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,82 @@
1
+ module Sunspot
2
+ module Search
3
+ class StatsJsonRow
4
+ attr_reader :data, :value, :nested
5
+ attr_writer :instance #:nodoc:
6
+
7
+ def initialize(data, facet = nil, value = nil) #:nodoc:
8
+ @data, @facet, @value = data, facet, value
9
+ @facet_fields = []
10
+ @nested_key = data.keys.select { |k| data[k].is_a?(Hash) }.first
11
+ @nested = recursive_nested_initialization(data) unless @nested_key.nil?
12
+ end
13
+
14
+ def min
15
+ data['min']
16
+ end
17
+
18
+ def max
19
+ data['max']
20
+ end
21
+
22
+ def count
23
+ data['count']
24
+ end
25
+
26
+ def sum
27
+ data['sum']
28
+ end
29
+
30
+ def missing
31
+ data['missing']
32
+ end
33
+
34
+ def sum_of_squares
35
+ data['sumsq']
36
+ end
37
+ alias :sumsq :sum_of_squares
38
+
39
+ def mean
40
+ data['avg']
41
+ end
42
+ alias :avg :mean
43
+
44
+ def standard_deviation
45
+ data['stddev']
46
+ end
47
+
48
+ def facet name
49
+ facets.find { |facet| facet.field.name == name.to_sym }
50
+ end
51
+
52
+ def facets
53
+ @facets ||= @facet_fields.map do |field|
54
+ StatsFacet.new(field, data['facets'][field.indexed_name])
55
+ end
56
+ end
57
+
58
+ def instance
59
+ if !defined?(@instance)
60
+ @facet.populate_instances
61
+ end
62
+ @instance
63
+ end
64
+
65
+ def inspect
66
+ "<Sunspot::Search::StatsJsonRow:#{value.inspect} min=#{min} max=#{max}"\
67
+ " count=#{count} sum=#{sum} missing=#{missing} sum_of_squares=#{sum_of_squares}"\
68
+ " mean=#{mean} standard_deviation=#{standard_deviation}"\
69
+ " #{nested.nil? ? '' : "nested_count=#{nested.size}"}>"
70
+ end
71
+
72
+ private
73
+
74
+ def recursive_nested_initialization(data)
75
+ data[@nested_key]['buckets'].map do |d|
76
+ StatsJsonRow.new(d, @facet, d['val'])
77
+ end
78
+ end
79
+
80
+ end
81
+ end
82
+ end
@@ -59,7 +59,9 @@ module Sunspot
59
59
  end
60
60
 
61
61
  def inspect
62
- "<Sunspot::Search::StatsRow:#{value.inspect} min=#{min} max=#{max} count=#{count}>"
62
+ "<Sunspot::Search::StatsRow:#{value.inspect} min=#{min} max=#{max}"\
63
+ " count=#{self.count} sum=#{sum} missing=#{missing} sum_of_squares=#{sum_of_squares}"\
64
+ " mean=#{mean} standard_deviation=#{standard_deviation}>"
63
65
  end
64
66
  end
65
67
  end
@@ -254,11 +254,13 @@ module Sunspot
254
254
  # RSolr::Connection::Base:: The connection for this session
255
255
  #
256
256
  def connection
257
- @connection ||=
258
- self.class.connection_class.connect(:url => config.solr.url,
259
- :read_timeout => config.solr.read_timeout,
260
- :open_timeout => config.solr.open_timeout,
261
- :proxy => config.solr.proxy)
257
+ @connection ||= self.class.connection_class.connect(
258
+ url: config.solr.url,
259
+ read_timeout: config.solr.read_timeout,
260
+ open_timeout: config.solr.open_timeout,
261
+ proxy: config.solr.proxy,
262
+ update_format: config.solr.update_format || :xml
263
+ )
262
264
  end
263
265
 
264
266
  def indexer
@@ -188,6 +188,29 @@ module Sunspot
188
188
  RSolr.solr_escape(value).gsub(/([\s\.])/, '\\\\\1')
189
189
  end
190
190
 
191
+ def parse_json_facet(field_name, options, setup)
192
+ field = setup.field(field_name)
193
+ if options[:time_range]
194
+ unless field.type.is_a?(Sunspot::Type::TimeType)
195
+ raise(
196
+ ArgumentError,
197
+ ':time_range can only be specified for Date or Time fields'
198
+ )
199
+ end
200
+ Sunspot::Query::DateFieldJsonFacet.new(field, options, setup)
201
+ elsif options[:range]
202
+ unless [Sunspot::Type::TimeType, Sunspot::Type::FloatType, Sunspot::Type::IntegerType ].find{|type| field.type.is_a?(type)}
203
+ raise(
204
+ ArgumentError,
205
+ ':range can only be specified for date or numeric fields'
206
+ )
207
+ end
208
+ Sunspot::Query::RangeJsonFacet.new(field, options, setup)
209
+ else
210
+ Sunspot::Query::FieldJsonFacet.new(field, options, setup)
211
+ end
212
+ end
213
+
191
214
  private
192
215
 
193
216
  #
@@ -1,3 +1,3 @@
1
1
  module Sunspot
2
- VERSION = '2.2.8'
2
+ VERSION = '2.3.0'
3
3
  end
@@ -66,7 +66,9 @@ shared_examples_for 'fulltext query' do
66
66
  end
67
67
 
68
68
  it 'puts default dismax parameters in subquery' do
69
- expect(subqueries(:q).last[:qf].split(' ').sort).to eq(%w(backwards_title_text body_textsv tags_textv title_text))
69
+ expect(subqueries(:q).last[:qf].split(' ').sort).to(
70
+ eq(%w(backwards_title_text body_textsv tags_textv text_array_text title_text))
71
+ )
70
72
  end
71
73
 
72
74
  it 'puts field list in main query' do
@@ -78,14 +80,18 @@ shared_examples_for 'fulltext query' do
78
80
  search = search do
79
81
  keywords 'keyword search'
80
82
  end
81
- expect(connection.searches.last[:qf].split(' ').sort).to eq(%w(backwards_title_text body_textsv tags_textv title_text))
83
+ expect(connection.searches.last[:qf].split(' ').sort).to(
84
+ eq(%w(backwards_title_text body_textsv tags_textv text_array_text title_text))
85
+ )
82
86
  end
83
87
 
84
88
  it 'searches both stored and unstored text fields' do
85
89
  search Post, Namespaced::Comment do
86
90
  keywords 'keyword search'
87
91
  end
88
- expect(connection.searches.last[:qf].split(' ').sort).to eq(%w(author_name_text backwards_title_text body_text body_textsv tags_textv title_text))
92
+ expect(connection.searches.last[:qf].split(' ').sort).to(
93
+ eq(%w(author_name_text backwards_title_text body_text body_textsv tags_textv text_array_text title_text))
94
+ )
89
95
  end
90
96
 
91
97
  it 'searches only specified text fields when specified' do
@@ -101,7 +107,7 @@ shared_examples_for 'fulltext query' do
101
107
  exclude_fields :backwards_title, :body_mlt
102
108
  end
103
109
  end
104
- expect(connection.searches.last[:qf].split(' ').sort).to eq(%w(body_textsv tags_textv title_text))
110
+ expect(connection.searches.last[:qf].split(' ').sort).to eq(%w(body_textsv tags_textv text_array_text title_text))
105
111
  end
106
112
 
107
113
  it 'assigns boost to fields when specified' do
@@ -173,7 +179,9 @@ shared_examples_for 'fulltext query' do
173
179
  boost_fields :title => 1.5
174
180
  end
175
181
  end
176
- expect(connection.searches.last[:qf].split(' ').sort).to eq(%w(backwards_title_text body_textsv tags_textv title_text^1.5))
182
+ expect(connection.searches.last[:qf].split(' ').sort).to(
183
+ eq(%w(backwards_title_text body_textsv tags_textv text_array_text title_text^1.5))
184
+ )
177
185
  end
178
186
 
179
187
  it 'ignores boost fields that do not apply' do
@@ -182,7 +190,9 @@ shared_examples_for 'fulltext query' do
182
190
  boost_fields :bogus => 1.2, :title => 1.5
183
191
  end
184
192
  end
185
- expect(connection.searches.last[:qf].split(' ').sort).to eq(%w(backwards_title_text body_textsv tags_textv title_text^1.5))
193
+ expect(connection.searches.last[:qf].split(' ').sort).to(
194
+ eq(%w(backwards_title_text body_textsv tags_textv text_array_text title_text^1.5))
195
+ )
186
196
  end
187
197
 
188
198
  it 'sets default boost with default fields' do
@@ -2,6 +2,7 @@ module IntegrationHelper
2
2
  def self.included(base)
3
3
  base.before(:all) do
4
4
  Sunspot.config.solr.url = ENV['SOLR_URL'] || 'http://localhost:8983/solr/default'
5
+ Sunspot.config.solr.update_format = ENV['UPDATE_FORMAT'].to_sym if ENV['UPDATE_FORMAT']
5
6
  Sunspot.reset!(true)
6
7
  end
7
8
  end
@@ -156,6 +156,136 @@ describe 'search faceting' do
156
156
  end
157
157
  end
158
158
 
159
+ context 'json facet options' do
160
+ before :all do
161
+ Sunspot.remove_all
162
+ facet_values = %w(zero one two three four)
163
+ facet_values.each_with_index do |value, i|
164
+ i.times { Sunspot.index(Post.new(:title => value, :blog_id => 1)) }
165
+ end
166
+ Sunspot.index(Post.new(:blog_id => 1))
167
+ Sunspot.index(Post.new(:title => 'zero', :blog_id => 2))
168
+ Sunspot.commit
169
+ end
170
+
171
+ it 'should return indexed elements' do
172
+ search = Sunspot.search(Post) do
173
+ json_facet(:title)
174
+ end
175
+ expect(search.facet(:title).rows.size).to eq(5)
176
+ end
177
+
178
+ it 'should limit the number of facet rows' do
179
+ search = Sunspot.search(Post) do
180
+ json_facet :title, :limit => 3
181
+ end
182
+ expect(search.facet(:title).rows.size).to eq(3)
183
+ end
184
+
185
+ it 'should not return zeros by default' do
186
+ search = Sunspot.search(Post) do
187
+ with :blog_id, 1
188
+ json_facet :title
189
+ end
190
+ expect(search.facet(:title).rows.map { |row| row.value }).not_to include('zero')
191
+ end
192
+
193
+ it 'should return a specified minimum count' do
194
+ search = Sunspot.search(Post) do
195
+ with :blog_id, 1
196
+ json_facet :title, :minimum_count => 2
197
+ end
198
+ expect(search.facet(:title).rows.map { |row| row.value }).to eq(%w(four three two))
199
+ end
200
+
201
+ it 'should order facets lexically' do
202
+ search = Sunspot.search(Post) do
203
+ with :blog_id, 1
204
+ json_facet :title, :sort => :index
205
+ end
206
+ expect(search.facet(:title).rows.map { |row| row.value }).to eq(%w(four one three two))
207
+ end
208
+
209
+ it 'should order facets by count' do
210
+ search = Sunspot.search(Post) do
211
+ with :blog_id, 1
212
+ json_facet :title, :sort => :count
213
+ end
214
+ expect(search.facet(:title).rows.map { |row| row.value }).to eq(%w(four three two one))
215
+ end
216
+
217
+ it 'should limit facet values by prefix' do
218
+ search = Sunspot.search(Post) do
219
+ with :blog_id, 1
220
+ json_facet :title, :prefix => 't'
221
+ end
222
+ expect(search.facet(:title).rows.map { |row| row.value }.sort).to eq(%w(three two))
223
+ end
224
+
225
+ end
226
+
227
+ context 'nested json facet' do
228
+ before :all do
229
+ Sunspot.remove_all
230
+ facet_values = %w(zero one two three four)
231
+ nested_facet_values = %w(alfa bravo charlie delta)
232
+
233
+ facet_values.each do |value|
234
+ nested_facet_values.each do |v2|
235
+ Sunspot.index(Post.new(:title => value, :author_name => v2, :blog_id => 1))
236
+ end
237
+ end
238
+
239
+ 0.upto(9) { |i| Sunspot.index(Post.new(:title => 'zero', :author_name => "another#{i}", :blog_id => 1)) }
240
+
241
+ Sunspot.commit
242
+ end
243
+
244
+ it 'should get nested' do
245
+ search = Sunspot.search(Post) do
246
+ json_facet(:title, nested: { field: :author_name } )
247
+ end
248
+ expect(search.facet(:title).rows.first.nested.size).to eq(4)
249
+ end
250
+
251
+ it 'without limit take the first 10' do
252
+ search = Sunspot.search(Post) do
253
+ json_facet(:title, nested: { field: :author_name } )
254
+ end
255
+ expect(search.facet(:title).rows.last.nested.size).to eq(10)
256
+ end
257
+
258
+ it 'without limit' do
259
+ search = Sunspot.search(Post) do
260
+ json_facet(:title, nested: { field: :author_name, limit: -1 } )
261
+ end
262
+ expect(search.facet(:title).rows.last.nested.size).to eq(14)
263
+ end
264
+
265
+ it 'works with distinct' do
266
+ search = Sunspot.search(Post) do
267
+ json_facet(:title, nested: { field: :author_name, distinct: { strategy: :unique } } )
268
+ end
269
+ expect(search.facet(:title).rows.first.nested.map(&:count).uniq.size).to eq(1)
270
+ end
271
+
272
+ it 'should limit the nested facet' do
273
+ search = Sunspot.search(Post) do
274
+ json_facet(:title, nested: { field: :author_name, limit: 2 } )
275
+ end
276
+ expect(search.facet(:title).rows.first.nested.size).to eq(2)
277
+ end
278
+
279
+ it 'should work nested of nested' do
280
+ search = Sunspot.search(Post) do
281
+ json_facet(:title, nested: { field: :author_name, nested: { field: :title } } )
282
+ end
283
+ expect(search.facet(:title).rows.first.nested.first.nested.size).to eq(1)
284
+ expect(search.facet(:title).rows.first.nested.first.nested.first.nested).to eq(nil)
285
+ end
286
+
287
+ end
288
+
159
289
  context 'prefix escaping' do
160
290
  before do
161
291
  Sunspot.remove_all
@@ -259,6 +389,57 @@ describe 'search faceting' do
259
389
  end
260
390
  end
261
391
 
392
+ context 'distinct field facets' do
393
+ before :all do
394
+ Sunspot.remove_all
395
+
396
+ Sunspot.index!(
397
+ (0..5).map { |i| Post.new(:blog_id => i, :title => 'title') }
398
+ )
399
+
400
+ 0.upto(3) { |i| Sunspot.index(Post.new(:blog_id => i, :title => 'title')) }
401
+
402
+ Sunspot.index!(Post.new(:blog_id => 4, :title => 'other title'))
403
+ Sunspot.index!(Post.new(:blog_id => 5, :title => 'other title'))
404
+
405
+ Sunspot.index!(Post.new(:blog_id => 40, :title => 'title'))
406
+ Sunspot.index!(Post.new(:blog_id => 40, :title => 'title'))
407
+
408
+ Sunspot.index!(Post.new(:blog_id => 40, :title => 'other title'))
409
+ Sunspot.index!(Post.new(:blog_id => 40, :title => 'other title'))
410
+ end
411
+
412
+ it 'should return unique indexed elements for a field' do
413
+ search = Sunspot.search(Post) do
414
+ json_facet(:blog_id, distinct: { strategy: :unique })
415
+ end
416
+
417
+ expect(search.facet(:blog_id).rows.size).to eq(7)
418
+ expect(search.facet(:blog_id).rows.map(&:count).uniq.size).to eq(1)
419
+ end
420
+
421
+ it 'should return unique indexed elements for a field and facet on a field' do
422
+ search = Sunspot.search(Post) do
423
+ json_facet(:blog_id, distinct: { group_by: :title, strategy: :unique })
424
+ end
425
+
426
+ expect(search.facet(:blog_id).rows.size).to eq(2)
427
+ expect(search.facet(:blog_id).rows[0].count).to eq(3)
428
+ expect(search.facet(:blog_id).rows[1].count).to eq(7)
429
+ end
430
+
431
+ it 'should return unique indexed elements for a field and facet on a field with hll' do
432
+ search = Sunspot.search(Post) do
433
+ json_facet(:blog_id, distinct: { group_by: :title, strategy: :hll })
434
+ end
435
+
436
+ expect(search.facet(:blog_id).rows.size).to eq(2)
437
+ expect(search.facet(:blog_id).rows[0].count).to eq(3)
438
+ expect(search.facet(:blog_id).rows[1].count).to eq(7)
439
+ end
440
+
441
+ end
442
+
262
443
  context 'date facets' do
263
444
  before :all do
264
445
  Sunspot.remove_all
@@ -278,6 +459,38 @@ describe 'search faceting' do
278
459
  expect(search.facet(:published_at).rows.last.value).to eq((time + 60*60*24)..(time + 60*60*24*2))
279
460
  expect(search.facet(:published_at).rows.last.count).to eq(1)
280
461
  end
462
+
463
+ it 'json facet should return time ranges' do
464
+ days_diff = 15
465
+ time_from = Time.utc(2009, 7, 8)
466
+ time_to = Time.utc(2009, 7, 8 + days_diff)
467
+ search = Sunspot.search(Post) do
468
+ json_facet(
469
+ :published_at,
470
+ :time_range => time_from..time_to
471
+ )
472
+ end
473
+
474
+ expect(search.facet(:published_at).rows.size).to eq(days_diff)
475
+ expect(search.facet(:published_at).rows[0].count).to eq(2)
476
+ expect(search.facet(:published_at).rows[1].count).to eq(1)
477
+ end
478
+
479
+ it 'json facet should return time ranges with custom gap' do
480
+ days_diff = 10
481
+ time_from = Time.utc(2009, 7, 8)
482
+ time_to = Time.utc(2009, 7, 8 + days_diff)
483
+ search = Sunspot.search(Post) do
484
+ json_facet(
485
+ :published_at,
486
+ :time_range => time_from..time_to,
487
+ gap: 60*60*24*2
488
+ )
489
+ end
490
+ expect(search.facet(:published_at).rows.size).to eq(days_diff / 2)
491
+ expect(search.facet(:published_at).rows[0].count).to eq(3)
492
+ end
493
+
281
494
  end
282
495
 
283
496
  context 'class facets' do
@@ -332,7 +332,7 @@ describe 'scoped_search' do
332
332
  expect(Sunspot.search(Post) do
333
333
  without(:blog_id, [])
334
334
  end.results).to eq(posts)
335
- end
335
+ end
336
336
 
337
337
  it 'should return results, ignoring any restriction in a conjunction that has been passed an empty array' do
338
338
  posts = (1..3).map { |i| Post.new(:blog_id => i)}
@@ -3,9 +3,9 @@ require File.expand_path('../spec_helper', File.dirname(__FILE__))
3
3
  describe 'search stats' do
4
4
  before :each do
5
5
  Sunspot.remove_all
6
- @posts = Post.new(:ratings_average => 4.0, :blog_id => 2),
7
- Post.new(:ratings_average => 4.0, :blog_id => 1),
8
- Post.new(:ratings_average => 3.0, :blog_id => 2)
6
+ @posts = Post.new(:ratings_average => 4.0, :author_name => 'plinio', :blog_id => 2),
7
+ Post.new(:ratings_average => 4.0, :author_name => 'caio', :blog_id => 1),
8
+ Post.new(:ratings_average => 3.0, :author_name => 'sempronio', :blog_id => 2)
9
9
  Sunspot.index!(@posts)
10
10
  end
11
11
 
@@ -44,4 +44,45 @@ describe 'search stats' do
44
44
  end.stats(:average_rating).facet(:blog_id).rows[1].max).to eq(4.0)
45
45
  end
46
46
  end
47
+
48
+ describe 'json facets' do
49
+ it 'returns minimum on facet row with two blog ids' do
50
+ expect(Sunspot.search(Post) do
51
+ stats :average_rating, sort: :min do
52
+ json_facet :blog_id
53
+ end
54
+ end.json_facet_stats(:blog_id).rows[1].min).to eq(3.0)
55
+ end
56
+
57
+ it 'returns maximum on facet row with two blog ids' do
58
+ expect(Sunspot.search(Post) do
59
+ stats :average_rating, sort: :max do
60
+ json_facet :blog_id
61
+ end
62
+ end.json_facet_stats(:blog_id).rows[1].max).to eq(4.0)
63
+ end
64
+
65
+ it 'returns only sum' do
66
+ search = Sunspot.search(Post) do
67
+ stats :average_rating, stats: [:sum] do
68
+ json_facet :blog_id
69
+ end
70
+ end
71
+ expect(search.json_facet_stats(:blog_id).rows[1].max).to eq(nil)
72
+ expect(search.json_facet_stats(:blog_id).rows[1].min).to eq(nil)
73
+ expect(search.json_facet_stats(:blog_id).rows[1].avg).to eq(nil)
74
+ expect(search.json_facet_stats(:blog_id).rows[1].sumsq).to eq(nil)
75
+ expect(search.json_facet_stats(:blog_id).rows[1].sum).to eq(4.0)
76
+ end
77
+
78
+ it 'works with nested facets' do
79
+ search = Sunspot.search(Post) do
80
+ stats :average_rating, sort: :min do
81
+ json_facet(:blog_id, nested: { field: :author_name, limit: 3, nested: { field: :average_rating } } )
82
+ end
83
+ end
84
+ expect(search.json_facet_stats(:blog_id).rows[1].nested.first.nested.first.min).to eq(4.0)
85
+ end
86
+
87
+ end
47
88
  end
@@ -37,12 +37,16 @@ end
37
37
 
38
38
  Sunspot.setup(Post) do
39
39
  text :title, :boost => 2
40
+ text :text_array, :boost => 3 do
41
+ [title, title]
42
+ end
40
43
  text :body, :stored => true, :more_like_this => true
41
44
  text :backwards_title do
42
45
  title.reverse if title
43
46
  end
44
47
  text :tags, :more_like_this => true
45
48
  string :title, :stored => true
49
+ string :author_name
46
50
  integer :blog_id, :references => Blog
47
51
  integer :category_ids, :multiple => true
48
52
  float :average_rating, :using => :ratings_average, :trie => true
@@ -23,14 +23,10 @@ RSpec.configure do |config|
23
23
  config.order = 'random'
24
24
 
25
25
  # Mock session available to all spec/api tests
26
- config.include MockSessionHelper,
27
- :type => :api,
28
- :file_path => /spec[\\\/]api/
26
+ config.include MockSessionHelper, type: :api, file_path: /spec[\\\/]api/
29
27
 
30
28
  # Real Solr instance is available to integration tests
31
- config.include IntegrationHelper,
32
- :type => :integration,
33
- :file_path => /spec[\\\/]integration/
29
+ config.include IntegrationHelper, type: :integration, file_path: /spec[\\\/]integration/
34
30
 
35
31
  # Nested under spec/api
36
32
  [:indexer, :query, :search].each do |spec_type|
@@ -31,7 +31,7 @@ Gem::Specification.new do |s|
31
31
  s.add_dependency 'pr_geohash', '~>1.0'
32
32
 
33
33
  s.add_development_dependency 'rake', '< 12.3'
34
- s.add_development_dependency 'rspec'
34
+ s.add_development_dependency 'rspec', '~> 3.7'
35
35
  s.add_development_dependency 'appraisal', '2.2.0'
36
36
 
37
37
  s.rdoc_options << '--webcvs=http://github.com/outoftime/sunspot/tree/master/%s' <<
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sunspot
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.8
4
+ version: 2.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mat Brown
@@ -30,7 +30,7 @@ authors:
30
30
  autorequire:
31
31
  bindir: bin
32
32
  cert_chain: []
33
- date: 2018-03-26 00:00:00.000000000 Z
33
+ date: 2018-04-08 00:00:00.000000000 Z
34
34
  dependencies:
35
35
  - !ruby/object:Gem::Dependency
36
36
  name: rsolr
@@ -84,16 +84,16 @@ dependencies:
84
84
  name: rspec
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - ">="
87
+ - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '0'
89
+ version: '3.7'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
- - - ">="
94
+ - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '0'
96
+ version: '3.7'
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: appraisal
99
99
  requirement: !ruby/object:Gem::Requirement
@@ -159,14 +159,17 @@ files:
159
159
  - lib/sunspot/query.rb
160
160
  - lib/sunspot/query/abstract_field_facet.rb
161
161
  - lib/sunspot/query/abstract_fulltext.rb
162
+ - lib/sunspot/query/abstract_json_field_facet.rb
162
163
  - lib/sunspot/query/bbox.rb
163
164
  - lib/sunspot/query/boost_query.rb
164
165
  - lib/sunspot/query/common_query.rb
165
166
  - lib/sunspot/query/composite_fulltext.rb
166
167
  - lib/sunspot/query/connective.rb
167
168
  - lib/sunspot/query/date_field_facet.rb
169
+ - lib/sunspot/query/date_field_json_facet.rb
168
170
  - lib/sunspot/query/dismax.rb
169
171
  - lib/sunspot/query/field_facet.rb
172
+ - lib/sunspot/query/field_json_facet.rb
170
173
  - lib/sunspot/query/field_list.rb
171
174
  - lib/sunspot/query/field_stats.rb
172
175
  - lib/sunspot/query/filter.rb
@@ -182,6 +185,7 @@ files:
182
185
  - lib/sunspot/query/pagination.rb
183
186
  - lib/sunspot/query/query_facet.rb
184
187
  - lib/sunspot/query/range_facet.rb
188
+ - lib/sunspot/query/range_json_facet.rb
185
189
  - lib/sunspot/query/restriction.rb
186
190
  - lib/sunspot/query/scope.rb
187
191
  - lib/sunspot/query/sort.rb
@@ -197,11 +201,14 @@ files:
197
201
  - lib/sunspot/search/facet_row.rb
198
202
  - lib/sunspot/search/field_facet.rb
199
203
  - lib/sunspot/search/field_group.rb
204
+ - lib/sunspot/search/field_json_facet.rb
200
205
  - lib/sunspot/search/field_stats.rb
201
206
  - lib/sunspot/search/group.rb
202
207
  - lib/sunspot/search/highlight.rb
203
208
  - lib/sunspot/search/hit.rb
204
209
  - lib/sunspot/search/hit_enumerable.rb
210
+ - lib/sunspot/search/json_facet_row.rb
211
+ - lib/sunspot/search/json_facet_stats.rb
205
212
  - lib/sunspot/search/more_like_this_search.rb
206
213
  - lib/sunspot/search/paginated_collection.rb
207
214
  - lib/sunspot/search/query_facet.rb
@@ -209,6 +216,7 @@ files:
209
216
  - lib/sunspot/search/range_facet.rb
210
217
  - lib/sunspot/search/standard_search.rb
211
218
  - lib/sunspot/search/stats_facet.rb
219
+ - lib/sunspot/search/stats_json_row.rb
212
220
  - lib/sunspot/search/stats_row.rb
213
221
  - lib/sunspot/session.rb
214
222
  - lib/sunspot/session_proxy.rb
@@ -348,7 +356,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
348
356
  version: '0'
349
357
  requirements: []
350
358
  rubyforge_project: sunspot
351
- rubygems_version: 2.6.13
359
+ rubygems_version: 2.7.6
352
360
  signing_key:
353
361
  specification_version: 4
354
362
  summary: Library for expressive, powerful interaction with the Solr search engine