sunspot 2.1.1 → 2.2.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.
- data/lib/sunspot.rb +13 -9
- data/lib/sunspot/dsl.rb +4 -3
- data/lib/sunspot/dsl/fields.rb +11 -16
- data/lib/sunspot/dsl/paginatable.rb +4 -1
- data/lib/sunspot/dsl/spellcheckable.rb +14 -0
- data/lib/sunspot/dsl/standard_query.rb +63 -35
- data/lib/sunspot/field.rb +54 -8
- data/lib/sunspot/field_factory.rb +2 -4
- data/lib/sunspot/indexer.rb +1 -2
- data/lib/sunspot/query.rb +2 -2
- data/lib/sunspot/query/abstract_fulltext.rb +69 -0
- data/lib/sunspot/query/common_query.rb +13 -2
- data/lib/sunspot/query/composite_fulltext.rb +58 -8
- data/lib/sunspot/query/dismax.rb +14 -67
- data/lib/sunspot/query/function_query.rb +1 -2
- data/lib/sunspot/query/geo.rb +1 -1
- data/lib/sunspot/query/join.rb +90 -0
- data/lib/sunspot/query/pagination.rb +12 -4
- data/lib/sunspot/query/restriction.rb +3 -4
- data/lib/sunspot/query/sort.rb +6 -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/search/abstract_search.rb +10 -1
- data/lib/sunspot/search/cursor_paginated_collection.rb +32 -0
- data/lib/sunspot/search/paginated_collection.rb +1 -0
- data/lib/sunspot/search/standard_search.rb +71 -3
- data/lib/sunspot/session.rb +6 -6
- data/lib/sunspot/setup.rb +6 -1
- data/lib/sunspot/util.rb +46 -13
- data/lib/sunspot/version.rb +1 -1
- data/spec/api/query/fulltext_examples.rb +150 -1
- data/spec/api/query/geo_examples.rb +2 -6
- data/spec/api/query/join_spec.rb +3 -3
- data/spec/api/query/ordering_pagination_examples.rb +14 -0
- data/spec/api/query/spellcheck_examples.rb +20 -0
- data/spec/api/query/standard_spec.rb +1 -0
- data/spec/api/search/cursor_paginated_collection_spec.rb +35 -0
- data/spec/api/search/paginated_collection_spec.rb +1 -0
- data/spec/api/session_spec.rb +36 -2
- data/spec/integration/spellcheck_spec.rb +74 -0
- data/spec/mocks/connection.rb +5 -3
- data/spec/mocks/photo.rb +12 -4
- data/spec/spec_helper.rb +4 -0
- metadata +24 -5
- checksums.yaml +0 -7
@@ -38,7 +38,6 @@ module Sunspot
|
|
38
38
|
#
|
39
39
|
class Base #:nodoc:
|
40
40
|
include Filter
|
41
|
-
include RSolr::Char
|
42
41
|
|
43
42
|
RESERVED_WORDS = Set['AND', 'OR', 'NOT']
|
44
43
|
|
@@ -92,7 +91,7 @@ module Sunspot
|
|
92
91
|
# String:: Boolean phrase for restriction in the positive
|
93
92
|
#
|
94
93
|
def to_positive_boolean_phrase
|
95
|
-
"#{escape(@field.indexed_name)}:#{to_solr_conditional}"
|
94
|
+
"#{Util.escape(@field.indexed_name)}:#{to_solr_conditional}"
|
96
95
|
end
|
97
96
|
|
98
97
|
#
|
@@ -138,7 +137,7 @@ module Sunspot
|
|
138
137
|
# String:: Solr API representation of given value
|
139
138
|
#
|
140
139
|
def solr_value(value = @value)
|
141
|
-
solr_value = escape(@field.to_indexed(value))
|
140
|
+
solr_value = Util.escape(@field.to_indexed(value))
|
142
141
|
if RESERVED_WORDS.include?(solr_value)
|
143
142
|
%Q("#{solr_value}")
|
144
143
|
else
|
@@ -168,7 +167,7 @@ module Sunspot
|
|
168
167
|
unless @value.nil?
|
169
168
|
super
|
170
169
|
else
|
171
|
-
"#{escape(@field.indexed_name)}:[* TO *]"
|
170
|
+
"#{Util.escape(@field.indexed_name)}:[* TO *]"
|
172
171
|
end
|
173
172
|
end
|
174
173
|
|
data/lib/sunspot/query/sort.rb
CHANGED
@@ -115,6 +115,12 @@ module Sunspot
|
|
115
115
|
end
|
116
116
|
end
|
117
117
|
|
118
|
+
class SolrIdSort < Abstract
|
119
|
+
def to_param
|
120
|
+
"id #{direction_for_solr}"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
118
124
|
#
|
119
125
|
# A FunctionSort sorts by solr function.
|
120
126
|
# FunctionComp recursively parses arguments for nesting
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Sunspot
|
2
|
+
module Query
|
3
|
+
class Spellcheck < Connective::Conjunction
|
4
|
+
attr_accessor :options
|
5
|
+
|
6
|
+
def initialize(options = {})
|
7
|
+
@options = options
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_params
|
11
|
+
options = {}
|
12
|
+
@options.each do |key, val|
|
13
|
+
options["spellcheck." + Sunspot::Util.method_case(key.to_s)] = val
|
14
|
+
end
|
15
|
+
{ :spellcheck => true }.merge(options)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -5,11 +5,33 @@ module Sunspot
|
|
5
5
|
|
6
6
|
def initialize(types)
|
7
7
|
super
|
8
|
-
@components << @fulltext =
|
8
|
+
@components << @fulltext = Conjunction.new
|
9
9
|
end
|
10
10
|
|
11
11
|
def add_fulltext(keywords)
|
12
|
-
@fulltext.
|
12
|
+
@fulltext.add_fulltext(keywords)
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_join(keywords, target, from, to)
|
16
|
+
@fulltext.add_join(keywords, target, from, to)
|
17
|
+
end
|
18
|
+
|
19
|
+
def disjunction
|
20
|
+
parent_fulltext = @fulltext
|
21
|
+
@fulltext = @fulltext.add_disjunction
|
22
|
+
|
23
|
+
yield
|
24
|
+
ensure
|
25
|
+
@fulltext = parent_fulltext
|
26
|
+
end
|
27
|
+
|
28
|
+
def conjunction
|
29
|
+
parent_fulltext = @fulltext
|
30
|
+
@fulltext = @fulltext.add_conjunction
|
31
|
+
|
32
|
+
yield
|
33
|
+
ensure
|
34
|
+
@fulltext = parent_fulltext
|
13
35
|
end
|
14
36
|
end
|
15
37
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'sunspot/search/paginated_collection'
|
2
|
+
require 'sunspot/search/cursor_paginated_collection'
|
2
3
|
require 'sunspot/search/hit_enumerable'
|
3
4
|
|
4
5
|
module Sunspot
|
@@ -276,12 +277,20 @@ module Sunspot
|
|
276
277
|
solr_response['docs']
|
277
278
|
end
|
278
279
|
|
280
|
+
def next_cursor
|
281
|
+
@solr_result['nextCursorMark'] if @query.cursor
|
282
|
+
end
|
283
|
+
|
279
284
|
def verified_hits
|
280
285
|
@verified_hits ||= paginate_collection(super)
|
281
286
|
end
|
282
287
|
|
283
288
|
def paginate_collection(collection)
|
284
|
-
|
289
|
+
if @query.cursor
|
290
|
+
CursorPaginatedCollection.new(collection, @query.per_page, total, @query.cursor, next_cursor)
|
291
|
+
else
|
292
|
+
PaginatedCollection.new(collection, @query.page, @query.per_page, total)
|
293
|
+
end
|
285
294
|
end
|
286
295
|
|
287
296
|
def add_facet(name, facet)
|
@@ -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
|
# 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,77 @@ 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. This may not be useful if only one term is
|
53
|
+
# misspelled and preventing useful results.
|
54
|
+
#
|
55
|
+
# Mix and match in your views for a blend of strict and liberal collations.
|
56
|
+
def spellcheck_collation(*terms)
|
57
|
+
if solr_spellcheck['suggestions'] && solr_spellcheck['suggestions'].length > 2
|
58
|
+
collation = terms.join(" ").dup if terms
|
59
|
+
|
60
|
+
# If we are given a query string, tokenize it and strictly replace
|
61
|
+
# the terms that aren't present in the index
|
62
|
+
if terms.length > 0
|
63
|
+
terms.each do |term|
|
64
|
+
if (spellcheck_suggestions[term]||{})['origFreq'] == 0
|
65
|
+
collation[term] = spellcheck_suggestion_for(term)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# If no query was given, or all terms are present in the index,
|
71
|
+
# return Solr's suggested collation.
|
72
|
+
if terms.length == 0
|
73
|
+
collation = solr_spellcheck['suggestions'][-1]
|
74
|
+
end
|
75
|
+
|
76
|
+
collation
|
77
|
+
else
|
78
|
+
nil
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
14
82
|
private
|
15
|
-
|
83
|
+
|
16
84
|
def dsl
|
17
85
|
DSL::Search.new(self, @setup)
|
18
86
|
end
|
data/lib/sunspot/session.rb
CHANGED
@@ -102,9 +102,9 @@ module Sunspot
|
|
102
102
|
#
|
103
103
|
# See Sunspot.commit
|
104
104
|
#
|
105
|
-
def commit
|
105
|
+
def commit(soft_commit = false)
|
106
106
|
@adds = @deletes = 0
|
107
|
-
connection.commit
|
107
|
+
connection.commit :commit_attributes => {:softCommit => soft_commit}
|
108
108
|
end
|
109
109
|
|
110
110
|
#
|
@@ -200,8 +200,8 @@ module Sunspot
|
|
200
200
|
#
|
201
201
|
# See Sunspot.commit_if_dirty
|
202
202
|
#
|
203
|
-
def commit_if_dirty
|
204
|
-
commit if dirty?
|
203
|
+
def commit_if_dirty(soft_commit = false)
|
204
|
+
commit soft_commit if dirty?
|
205
205
|
end
|
206
206
|
|
207
207
|
#
|
@@ -214,8 +214,8 @@ module Sunspot
|
|
214
214
|
#
|
215
215
|
# See Sunspot.commit_if_delete_dirty
|
216
216
|
#
|
217
|
-
def commit_if_delete_dirty
|
218
|
-
commit if delete_dirty?
|
217
|
+
def commit_if_delete_dirty(soft_commit = false)
|
218
|
+
commit soft_commit if delete_dirty?
|
219
219
|
end
|
220
220
|
|
221
221
|
#
|
data/lib/sunspot/setup.rb
CHANGED
@@ -41,7 +41,12 @@ module Sunspot
|
|
41
41
|
def add_join_field_factory(name, type, options = {}, &block)
|
42
42
|
field_factory = FieldFactory::Join.new(name, type, options, &block)
|
43
43
|
@field_factories[field_factory.signature] = field_factory
|
44
|
-
|
44
|
+
|
45
|
+
if type.is_a?(Type::TextType)
|
46
|
+
@text_field_factories_cache[field_factory.name] = field_factory
|
47
|
+
else
|
48
|
+
@field_factories_cache[field_factory.name] = field_factory
|
49
|
+
end
|
45
50
|
end
|
46
51
|
|
47
52
|
#
|
data/lib/sunspot/util.rb
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
module Sunspot
|
2
|
-
#
|
2
|
+
#
|
3
3
|
# The Sunspot::Util module provides utility methods used elsewhere in the
|
4
4
|
# library.
|
5
5
|
#
|
6
6
|
module Util #:nodoc:
|
7
7
|
class <<self
|
8
|
-
#
|
8
|
+
#
|
9
9
|
# Get all of the superclasses for a given class, including the class
|
10
10
|
# itself.
|
11
|
-
#
|
11
|
+
#
|
12
12
|
# ==== Parameters
|
13
13
|
#
|
14
14
|
# clazz<Class>:: class for which to get superclasses
|
@@ -23,7 +23,7 @@ module Sunspot
|
|
23
23
|
superclasses
|
24
24
|
end
|
25
25
|
|
26
|
-
#
|
26
|
+
#
|
27
27
|
# Convert a string to snake case
|
28
28
|
#
|
29
29
|
# ==== Parameters
|
@@ -38,7 +38,7 @@ module Sunspot
|
|
38
38
|
string.scan(/(^|[A-Z])([^A-Z]+)/).map! { |word| word.join.downcase }.join('_')
|
39
39
|
end
|
40
40
|
|
41
|
-
#
|
41
|
+
#
|
42
42
|
# Convert a string to camel case
|
43
43
|
#
|
44
44
|
# ==== Parameters
|
@@ -53,7 +53,23 @@ module Sunspot
|
|
53
53
|
string.split('_').map! { |word| word.capitalize }.join
|
54
54
|
end
|
55
55
|
|
56
|
-
#
|
56
|
+
#
|
57
|
+
# Convert snake case string to method case (Java style)
|
58
|
+
#
|
59
|
+
# ==== Parameters
|
60
|
+
#
|
61
|
+
# string<String>:: String to convert to method case
|
62
|
+
#
|
63
|
+
# ==== Returns
|
64
|
+
#
|
65
|
+
# String:: String in method case
|
66
|
+
#
|
67
|
+
def method_case(string)
|
68
|
+
first = true
|
69
|
+
string.split('_').map! { |word| word = first ? word : word.capitalize; first = false; word }.join
|
70
|
+
end
|
71
|
+
|
72
|
+
#
|
57
73
|
# Get a constant from a fully qualified name
|
58
74
|
#
|
59
75
|
# ==== Parameters
|
@@ -70,13 +86,13 @@ module Sunspot
|
|
70
86
|
end
|
71
87
|
end
|
72
88
|
|
73
|
-
#
|
89
|
+
#
|
74
90
|
# Evaluate the given proc in the context of the given object if the
|
75
91
|
# block's arity is non-positive, or by passing the given object as an
|
76
92
|
# argument if it is negative.
|
77
|
-
#
|
93
|
+
#
|
78
94
|
# ==== Parameters
|
79
|
-
#
|
95
|
+
#
|
80
96
|
# object<Object>:: Object to pass to the proc
|
81
97
|
#
|
82
98
|
def instance_eval_or_call(object, &block)
|
@@ -108,7 +124,7 @@ module Sunspot
|
|
108
124
|
end
|
109
125
|
end
|
110
126
|
|
111
|
-
#
|
127
|
+
#
|
112
128
|
# When generating boosts, Solr requires that the values be in standard
|
113
129
|
# (not scientific) notation. We would like to ensure a minimum number of
|
114
130
|
# significant digits (i.e., digits that are not prefix zeros) for small
|
@@ -122,7 +138,7 @@ module Sunspot
|
|
122
138
|
end
|
123
139
|
end
|
124
140
|
|
125
|
-
#
|
141
|
+
#
|
126
142
|
# Perform a deep merge of hashes, returning the result as a new hash.
|
127
143
|
# See #deep_merge_into for rules used to merge the hashes
|
128
144
|
#
|
@@ -139,7 +155,7 @@ module Sunspot
|
|
139
155
|
deep_merge_into({}, left, right)
|
140
156
|
end
|
141
157
|
|
142
|
-
#
|
158
|
+
#
|
143
159
|
# Perform a deep merge of the right hash into the left hash
|
144
160
|
#
|
145
161
|
# ==== Parameters
|
@@ -155,9 +171,26 @@ module Sunspot
|
|
155
171
|
deep_merge_into(left, left, right)
|
156
172
|
end
|
157
173
|
|
174
|
+
#
|
175
|
+
# Escapes characters for the Solr query parser
|
176
|
+
#
|
177
|
+
# ==== Parameters
|
178
|
+
#
|
179
|
+
# string<String>:: String to escape
|
180
|
+
#
|
181
|
+
# ==== Returns
|
182
|
+
#
|
183
|
+
# String:: escaped string
|
184
|
+
#
|
185
|
+
def escape(value)
|
186
|
+
# RSolr.solr_escape doesn't handle spaces or period chars,
|
187
|
+
# which do need to be escaped
|
188
|
+
RSolr.solr_escape(value).gsub(/([\s\.])/, '\\\\\1')
|
189
|
+
end
|
190
|
+
|
158
191
|
private
|
159
192
|
|
160
|
-
#
|
193
|
+
#
|
161
194
|
# Deep merge two hashes into a third hash, using rules that produce nice
|
162
195
|
# merged parameter hashes. The rules are as follows, for a given key:
|
163
196
|
#
|