sunspot 2.2.8 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
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