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