sunspot 2.0.0 → 2.5.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 +7 -0
- data/.gitignore +1 -0
- data/.rspec +2 -0
- data/Appraisals +7 -0
- data/Gemfile +0 -2
- data/History.txt +10 -0
- data/lib/sunspot.rb +55 -17
- data/lib/sunspot/adapters.rb +68 -18
- data/lib/sunspot/batcher.rb +1 -1
- data/lib/sunspot/configuration.rb +4 -2
- data/lib/sunspot/data_extractor.rb +36 -6
- data/lib/sunspot/dsl.rb +4 -3
- data/lib/sunspot/dsl/adjustable.rb +2 -2
- data/lib/sunspot/dsl/field_query.rb +69 -16
- data/lib/sunspot/dsl/field_stats.rb +25 -0
- data/lib/sunspot/dsl/fields.rb +28 -8
- data/lib/sunspot/dsl/fulltext.rb +9 -1
- data/lib/sunspot/dsl/group.rb +118 -0
- data/lib/sunspot/dsl/paginatable.rb +4 -1
- data/lib/sunspot/dsl/scope.rb +19 -10
- data/lib/sunspot/dsl/search.rb +1 -1
- data/lib/sunspot/dsl/spellcheckable.rb +14 -0
- data/lib/sunspot/dsl/standard_query.rb +63 -35
- data/lib/sunspot/field.rb +76 -4
- data/lib/sunspot/field_factory.rb +60 -11
- data/lib/sunspot/indexer.rb +70 -18
- data/lib/sunspot/query.rb +5 -4
- data/lib/sunspot/query/abstract_field_facet.rb +0 -2
- data/lib/sunspot/query/abstract_fulltext.rb +76 -0
- data/lib/sunspot/query/abstract_json_field_facet.rb +70 -0
- data/lib/sunspot/query/bbox.rb +5 -1
- data/lib/sunspot/query/common_query.rb +31 -6
- data/lib/sunspot/query/composite_fulltext.rb +58 -8
- data/lib/sunspot/query/date_field_json_facet.rb +25 -0
- data/lib/sunspot/query/dismax.rb +25 -71
- data/lib/sunspot/query/field_json_facet.rb +19 -0
- data/lib/sunspot/query/field_list.rb +15 -0
- data/lib/sunspot/query/field_stats.rb +61 -0
- data/lib/sunspot/query/function_query.rb +1 -2
- data/lib/sunspot/query/geo.rb +1 -1
- data/lib/sunspot/query/geofilt.rb +8 -3
- data/lib/sunspot/query/group.rb +46 -0
- data/lib/sunspot/query/group_query.rb +17 -0
- data/lib/sunspot/query/join.rb +88 -0
- data/lib/sunspot/query/more_like_this.rb +1 -1
- data/lib/sunspot/query/pagination.rb +12 -4
- data/lib/sunspot/query/range_json_facet.rb +28 -0
- data/lib/sunspot/query/restriction.rb +99 -13
- data/lib/sunspot/query/sort.rb +41 -0
- data/lib/sunspot/query/sort_composite.rb +7 -0
- data/lib/sunspot/query/spellcheck.rb +19 -0
- data/lib/sunspot/query/standard_query.rb +24 -2
- data/lib/sunspot/query/text_field_boost.rb +1 -3
- data/lib/sunspot/schema.rb +12 -3
- data/lib/sunspot/search.rb +4 -2
- data/lib/sunspot/search/abstract_search.rb +93 -43
- data/lib/sunspot/search/cursor_paginated_collection.rb +32 -0
- data/lib/sunspot/search/field_facet.rb +4 -4
- data/lib/sunspot/search/field_json_facet.rb +33 -0
- data/lib/sunspot/search/field_stats.rb +21 -0
- data/lib/sunspot/search/hit.rb +6 -1
- data/lib/sunspot/search/hit_enumerable.rb +4 -1
- data/lib/sunspot/search/json_facet_row.rb +40 -0
- data/lib/sunspot/search/json_facet_stats.rb +23 -0
- data/lib/sunspot/search/paginated_collection.rb +1 -0
- data/lib/sunspot/search/query_group.rb +74 -0
- data/lib/sunspot/search/standard_search.rb +70 -3
- data/lib/sunspot/search/stats_facet.rb +25 -0
- data/lib/sunspot/search/stats_json_row.rb +82 -0
- data/lib/sunspot/search/stats_row.rb +68 -0
- data/lib/sunspot/session.rb +62 -37
- data/lib/sunspot/session_proxy/class_sharding_session_proxy.rb +6 -4
- data/lib/sunspot/session_proxy/id_sharding_session_proxy.rb +16 -8
- data/lib/sunspot/session_proxy/master_slave_session_proxy.rb +2 -2
- data/lib/sunspot/session_proxy/retry_5xx_session_proxy.rb +1 -1
- data/lib/sunspot/session_proxy/sharding_session_proxy.rb +4 -2
- data/lib/sunspot/session_proxy/silent_fail_session_proxy.rb +1 -1
- data/lib/sunspot/session_proxy/thread_local_session_proxy.rb +6 -4
- data/lib/sunspot/setup.rb +42 -0
- data/lib/sunspot/type.rb +20 -0
- data/lib/sunspot/util.rb +78 -14
- data/lib/sunspot/version.rb +1 -1
- data/spec/api/adapters_spec.rb +40 -15
- data/spec/api/batcher_spec.rb +15 -15
- data/spec/api/binding_spec.rb +3 -3
- data/spec/api/class_set_spec.rb +6 -6
- data/spec/api/data_extractor_spec.rb +39 -0
- data/spec/api/hit_enumerable_spec.rb +32 -9
- data/spec/api/indexer/attributes_spec.rb +35 -30
- data/spec/api/indexer/batch_spec.rb +8 -7
- data/spec/api/indexer/dynamic_fields_spec.rb +8 -8
- data/spec/api/indexer/fixed_fields_spec.rb +16 -11
- data/spec/api/indexer/fulltext_spec.rb +8 -8
- data/spec/api/indexer/removal_spec.rb +24 -14
- data/spec/api/indexer_spec.rb +2 -2
- data/spec/api/query/advanced_manipulation_examples.rb +3 -3
- data/spec/api/query/connectives_examples.rb +26 -14
- data/spec/api/query/dsl_spec.rb +24 -6
- data/spec/api/query/dynamic_fields_examples.rb +18 -18
- data/spec/api/query/faceting_examples.rb +80 -61
- data/spec/api/query/fulltext_examples.rb +194 -40
- data/spec/api/query/function_spec.rb +116 -13
- data/spec/api/query/geo_examples.rb +8 -12
- data/spec/api/query/group_spec.rb +27 -5
- data/spec/api/query/highlighting_examples.rb +26 -26
- data/spec/api/query/join_spec.rb +19 -0
- data/spec/api/query/more_like_this_spec.rb +40 -27
- data/spec/api/query/ordering_pagination_examples.rb +37 -23
- data/spec/api/query/scope_examples.rb +39 -39
- data/spec/api/query/spatial_examples.rb +3 -3
- data/spec/api/query/spellcheck_examples.rb +20 -0
- data/spec/api/query/standard_spec.rb +3 -1
- data/spec/api/query/stats_examples.rb +66 -0
- data/spec/api/query/text_field_scoping_examples.rb +5 -5
- data/spec/api/query/types_spec.rb +4 -4
- data/spec/api/search/cursor_paginated_collection_spec.rb +35 -0
- data/spec/api/search/dynamic_fields_spec.rb +4 -4
- data/spec/api/search/faceting_spec.rb +55 -52
- data/spec/api/search/highlighting_spec.rb +7 -7
- data/spec/api/search/hits_spec.rb +43 -29
- data/spec/api/search/paginated_collection_spec.rb +19 -18
- data/spec/api/search/results_spec.rb +13 -13
- data/spec/api/search/search_spec.rb +3 -3
- data/spec/api/search/stats_spec.rb +94 -0
- data/spec/api/session_proxy/class_sharding_session_proxy_spec.rb +23 -16
- data/spec/api/session_proxy/id_sharding_session_proxy_spec.rb +16 -4
- data/spec/api/session_proxy/master_slave_session_proxy_spec.rb +10 -6
- data/spec/api/session_proxy/retry_5xx_session_proxy_spec.rb +11 -11
- data/spec/api/session_proxy/sharding_session_proxy_spec.rb +15 -14
- data/spec/api/session_proxy/silent_fail_session_proxy_spec.rb +3 -3
- data/spec/api/session_proxy/spec_helper.rb +1 -1
- data/spec/api/session_proxy/thread_local_session_proxy_spec.rb +40 -26
- data/spec/api/session_spec.rb +78 -38
- data/spec/api/sunspot_spec.rb +7 -4
- data/spec/helpers/integration_helper.rb +11 -1
- data/spec/helpers/query_helper.rb +1 -1
- data/spec/helpers/search_helper.rb +30 -0
- data/spec/integration/atomic_updates_spec.rb +58 -0
- data/spec/integration/dynamic_fields_spec.rb +31 -20
- data/spec/integration/faceting_spec.rb +252 -39
- data/spec/integration/field_grouping_spec.rb +47 -15
- data/spec/integration/field_lists_spec.rb +57 -0
- data/spec/integration/geospatial_spec.rb +34 -8
- data/spec/integration/highlighting_spec.rb +8 -8
- data/spec/integration/indexing_spec.rb +7 -6
- data/spec/integration/join_spec.rb +45 -0
- data/spec/integration/keyword_search_spec.rb +68 -38
- data/spec/integration/local_search_spec.rb +4 -4
- data/spec/integration/more_like_this_spec.rb +7 -7
- data/spec/integration/scoped_search_spec.rb +193 -74
- data/spec/integration/spellcheck_spec.rb +119 -0
- data/spec/integration/stats_spec.rb +88 -0
- data/spec/integration/stored_fields_spec.rb +1 -1
- data/spec/integration/test_pagination.rb +4 -4
- data/spec/integration/unicode_spec.rb +1 -1
- data/spec/mocks/adapters.rb +36 -0
- data/spec/mocks/connection.rb +5 -3
- data/spec/mocks/photo.rb +32 -1
- data/spec/mocks/post.rb +18 -3
- data/spec/spec_helper.rb +13 -8
- data/sunspot.gemspec +6 -4
- data/tasks/rdoc.rake +22 -14
- metadata +101 -44
- data/lib/sunspot/dsl/field_group.rb +0 -57
- data/lib/sunspot/query/field_group.rb +0 -37
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module Sunspot
|
|
2
|
+
module Search
|
|
3
|
+
|
|
4
|
+
class CursorPaginatedCollection < Array
|
|
5
|
+
attr_reader :per_page, :total_count, :current_cursor, :next_page_cursor
|
|
6
|
+
alias :total_entries :total_count
|
|
7
|
+
alias :limit_value :per_page
|
|
8
|
+
|
|
9
|
+
def initialize(collection, per_page, total, current_cursor, next_page_cursor)
|
|
10
|
+
@per_page = per_page
|
|
11
|
+
@total_count = total
|
|
12
|
+
@current_cursor = current_cursor
|
|
13
|
+
@next_page_cursor = next_page_cursor
|
|
14
|
+
|
|
15
|
+
replace collection
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def total_pages
|
|
19
|
+
(total_count.to_f / per_page).ceil
|
|
20
|
+
end
|
|
21
|
+
alias :num_pages :total_pages
|
|
22
|
+
|
|
23
|
+
def first_page?
|
|
24
|
+
current_cursor == '*'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def last_page?
|
|
28
|
+
count < per_page
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module Sunspot
|
|
2
2
|
module Search
|
|
3
|
-
#
|
|
3
|
+
#
|
|
4
4
|
# A FieldFacet is a facet whose rows are all values for a certain field, in
|
|
5
5
|
# contrast to a QueryFacet, whose rows represent arbitrary queries.
|
|
6
6
|
#
|
|
@@ -14,7 +14,7 @@ module Sunspot
|
|
|
14
14
|
@field.name
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
#
|
|
17
|
+
#
|
|
18
18
|
# Get the rows returned for this facet.
|
|
19
19
|
#
|
|
20
20
|
# ==== Options (options)
|
|
@@ -50,7 +50,7 @@ module Sunspot
|
|
|
50
50
|
end
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
-
#
|
|
53
|
+
#
|
|
54
54
|
# If this facet references a model class, populate the rows with instances
|
|
55
55
|
# of the model class by loading them out of the appropriate adapter.
|
|
56
56
|
#
|
|
@@ -79,7 +79,7 @@ module Sunspot
|
|
|
79
79
|
rows
|
|
80
80
|
end
|
|
81
81
|
end
|
|
82
|
-
|
|
82
|
+
|
|
83
83
|
def key
|
|
84
84
|
@key ||= (@options[:name] || @field.indexed_name).to_s
|
|
85
85
|
end
|
|
@@ -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,21 @@
|
|
|
1
|
+
module Sunspot
|
|
2
|
+
module Search
|
|
3
|
+
class FieldStats < StatsRow
|
|
4
|
+
def initialize(field, search) #:nodoc:
|
|
5
|
+
@field, @search, @facet_fields = field, search, []
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def add_facet field
|
|
9
|
+
@facet_fields << field
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def field_name
|
|
13
|
+
@field.name
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def data
|
|
17
|
+
@search.stats_response[@field.indexed_name]
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/sunspot/search/hit.rb
CHANGED
|
@@ -17,6 +17,10 @@ module Sunspot
|
|
|
17
17
|
# Class name of object associated with this hit, as string.
|
|
18
18
|
#
|
|
19
19
|
attr_reader :class_name
|
|
20
|
+
#
|
|
21
|
+
# ID prefix used for compositeId shard router
|
|
22
|
+
#
|
|
23
|
+
attr_reader :id_prefix
|
|
20
24
|
#
|
|
21
25
|
# Keyword relevance score associated with this result. Nil if this hit
|
|
22
26
|
# is not from a keyword search.
|
|
@@ -27,7 +31,8 @@ module Sunspot
|
|
|
27
31
|
attr_writer :result #:nodoc:
|
|
28
32
|
|
|
29
33
|
def initialize(raw_hit, highlights, search) #:nodoc:
|
|
30
|
-
@class_name, @primary_key =
|
|
34
|
+
@id_prefix, @class_name, @primary_key =
|
|
35
|
+
*raw_hit['id'].match(/((?:[^!]+!)+)*([^\s]+)\s(.+)/)[1..3]
|
|
31
36
|
@score = raw_hit['score']
|
|
32
37
|
@search = search
|
|
33
38
|
@stored_values = raw_hit
|
|
@@ -11,6 +11,9 @@ module Sunspot
|
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
#
|
|
15
|
+
# Returns all of the hits that have a result
|
|
16
|
+
#
|
|
14
17
|
def verified_hits
|
|
15
18
|
hits.select { |h| h.result }
|
|
16
19
|
end
|
|
@@ -31,7 +34,7 @@ module Sunspot
|
|
|
31
34
|
hits_for_class = id_hit_hash[class_name]
|
|
32
35
|
data_accessor.load_all(ids).each do |result|
|
|
33
36
|
hit = hits_for_class.delete(Adapters::InstanceAdapter.adapt(result).id.to_s)
|
|
34
|
-
hit.result = result
|
|
37
|
+
hit.result = result if hit
|
|
35
38
|
end
|
|
36
39
|
hits_for_class.values.each { |hit| hit.result = nil }
|
|
37
40
|
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,74 @@
|
|
|
1
|
+
require 'sunspot/search/paginated_collection'
|
|
2
|
+
|
|
3
|
+
module Sunspot
|
|
4
|
+
module Search
|
|
5
|
+
class QueryGroup
|
|
6
|
+
def initialize(queries, search) #:nodoc:
|
|
7
|
+
@queries, @search = queries, search
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def groups
|
|
11
|
+
@groups ||=
|
|
12
|
+
if solr_response_for_first
|
|
13
|
+
groups = @queries.map { |query| Group.new(query.label, doclist_for(query), @search) }
|
|
14
|
+
|
|
15
|
+
paginate_collection(groups)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def matches
|
|
20
|
+
if solr_response_for_first
|
|
21
|
+
solr_response_for_first['matches'].to_i
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def total
|
|
26
|
+
@queries.count
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
#
|
|
30
|
+
# It populates all grouped hits at once.
|
|
31
|
+
# Useful for eager loading fall grouped results at once.
|
|
32
|
+
#
|
|
33
|
+
def populate_all_hits
|
|
34
|
+
# Init a 2 dimension Hash that contains an array per key
|
|
35
|
+
id_hit_hash = Hash.new { |hash, key| hash[key] = Hash.new{ |h, k| h[k] = [] } }
|
|
36
|
+
groups.each do |g|
|
|
37
|
+
# Take all hits to being populated later on
|
|
38
|
+
g.hits.each do |hit|
|
|
39
|
+
id_hit_hash[hit.class_name][hit.primary_key] |= [hit]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
# Go for each class and load the results' objects into each of the hits
|
|
43
|
+
id_hit_hash.each_pair do |class_name, many_hits|
|
|
44
|
+
ids = many_hits.keys
|
|
45
|
+
data_accessor = @search.data_accessor_for(Util.full_const_get(class_name))
|
|
46
|
+
hits_for_class = id_hit_hash[class_name]
|
|
47
|
+
data_accessor.load_all(ids).each do |result|
|
|
48
|
+
hits = hits_for_class.delete(Adapters::InstanceAdapter.adapt(result).id.to_s)
|
|
49
|
+
hits.each{ |hit| hit.result = result }
|
|
50
|
+
end
|
|
51
|
+
hits_for_class.values.each { |hits| hits.each{|hit| hit.result = nil } }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def doclist_for(query)
|
|
58
|
+
solr_response_for(query.to_boolean_phrase)['doclist']
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def solr_response_for(query_string)
|
|
62
|
+
@search.group_response[query_string]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def solr_response_for_first
|
|
66
|
+
solr_response_for(@queries[0].to_boolean_phrase)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def paginate_collection(collection)
|
|
70
|
+
PaginatedCollection.new(collection, @search.query.page, @search.query.per_page, total)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module Sunspot
|
|
2
2
|
module Search
|
|
3
|
-
#
|
|
3
|
+
#
|
|
4
4
|
# This class encapsulates the results of a Solr search. It provides access
|
|
5
5
|
# to search results, total result count, facets, and pagination information.
|
|
6
6
|
# Instances of Search are returned by the Sunspot.search and
|
|
@@ -10,9 +10,76 @@ module Sunspot
|
|
|
10
10
|
def request_handler
|
|
11
11
|
super || :select
|
|
12
12
|
end
|
|
13
|
-
|
|
13
|
+
|
|
14
|
+
# Return the raw spellcheck block from the Solr response
|
|
15
|
+
def solr_spellcheck
|
|
16
|
+
@solr_spellcheck ||= @solr_result['spellcheck'] || {}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Reformat the oddly-formatted spellcheck suggestion array into a
|
|
20
|
+
# more useful hash.
|
|
21
|
+
#
|
|
22
|
+
# Original: [term, suggestion, term, suggestion, ..., "correctlySpelled", bool, "collation", str]
|
|
23
|
+
# "collation" is only included if spellcheck.collation was set to true
|
|
24
|
+
# Returns: { term => suggestion, term => suggestion }
|
|
25
|
+
def spellcheck_suggestions
|
|
26
|
+
unless defined?(@spellcheck_suggestions)
|
|
27
|
+
@spellcheck_suggestions = {}
|
|
28
|
+
count = ((solr_spellcheck['suggestions'] || []).length) / 2
|
|
29
|
+
(0..(count - 1)).each do |i|
|
|
30
|
+
break if ["correctlySpelled", "collation"].include? solr_spellcheck[i]
|
|
31
|
+
term = solr_spellcheck['suggestions'][i * 2]
|
|
32
|
+
suggestion = solr_spellcheck['suggestions'][(i * 2) + 1]
|
|
33
|
+
@spellcheck_suggestions[term] = suggestion
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
@spellcheck_suggestions
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Return the suggestion with the single highest frequency.
|
|
40
|
+
# Requires the extended results format.
|
|
41
|
+
def spellcheck_suggestion_for(term)
|
|
42
|
+
spellcheck_suggestions[term]['suggestion'].sort_by do |suggestion|
|
|
43
|
+
suggestion['freq']
|
|
44
|
+
end.last['word']
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Provide a collated query. If the user provides a query string,
|
|
48
|
+
# tokenize it on whitespace and replace terms strictly not present in
|
|
49
|
+
# the index. Otherwise return Solr's suggested collation.
|
|
50
|
+
#
|
|
51
|
+
# Solr's suggested collation is more liberal, replacing even terms that
|
|
52
|
+
# are present in the index.
|
|
53
|
+
#
|
|
54
|
+
# Mix and match in your views for a blend of strict and liberal collations.
|
|
55
|
+
def spellcheck_collation(*terms)
|
|
56
|
+
if solr_spellcheck['suggestions'] && solr_spellcheck['suggestions'].length > 0
|
|
57
|
+
collation = terms.join(" ").dup if terms
|
|
58
|
+
|
|
59
|
+
# If we are given a query string, tokenize it and strictly replace
|
|
60
|
+
# the terms that aren't present in the index
|
|
61
|
+
if terms.length > 0
|
|
62
|
+
terms.each do |term|
|
|
63
|
+
if (spellcheck_suggestions[term]||{})['origFreq'] == 0
|
|
64
|
+
collation[term] = spellcheck_suggestion_for(term)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# If no query was given, or all terms are present in the index,
|
|
70
|
+
# return Solr's suggested collation.
|
|
71
|
+
if terms.length == 0
|
|
72
|
+
collation = solr_spellcheck['collations'][-1]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
collation
|
|
76
|
+
else
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
14
81
|
private
|
|
15
|
-
|
|
82
|
+
|
|
16
83
|
def dsl
|
|
17
84
|
DSL::Search.new(self, @setup)
|
|
18
85
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Sunspot
|
|
2
|
+
module Search
|
|
3
|
+
class StatsFacet < FieldFacet
|
|
4
|
+
attr_reader :field
|
|
5
|
+
|
|
6
|
+
def initialize(field, data) #:nodoc:
|
|
7
|
+
@field, @data = field, data
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def rows(options = {})
|
|
11
|
+
if options[:verified]
|
|
12
|
+
verified_rows
|
|
13
|
+
else
|
|
14
|
+
@rows ||= @data.map do |value, data|
|
|
15
|
+
StatsRow.new(data, self, @field.type.cast(value))
|
|
16
|
+
end.sort_by { |row| row.value.to_s }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def inspect
|
|
21
|
+
"<Sunspot::Search::StatsFacet:#{@field}>"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
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
|