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
data/lib/sunspot.rb
CHANGED
@@ -196,23 +196,27 @@ module Sunspot
|
|
196
196
|
session.index!(*objects)
|
197
197
|
end
|
198
198
|
|
199
|
-
# Commits the singleton session
|
199
|
+
# Commits (soft or hard) the singleton session
|
200
200
|
#
|
201
201
|
# When documents are added to or removed from Solr, the changes are
|
202
202
|
# initially stored in memory, and are not reflected in Solr's existing
|
203
|
-
# searcher instance. When a commit message is sent, the changes are written
|
203
|
+
# searcher instance. When a hard commit message is sent, the changes are written
|
204
204
|
# to disk, and a new searcher is spawned. Commits are thus fairly
|
205
205
|
# expensive, so if your application needs to index several documents as part
|
206
206
|
# of a single operation, it is advisable to index them all and then call
|
207
207
|
# commit at the end of the operation.
|
208
|
+
# Solr 4 introduced the concept of a soft commit which is much faster
|
209
|
+
# since it only makes index changes visible while not writing changes to disk.
|
210
|
+
# If Solr crashes or there is a loss of power, changes that occurred after
|
211
|
+
# the last hard commit will be lost.
|
208
212
|
#
|
209
213
|
# Note that Solr can also be configured to automatically perform a commit
|
210
214
|
# after either a specified interval after the last change, or after a
|
211
215
|
# specified number of documents are added. See
|
212
216
|
# http://wiki.apache.org/solr/SolrConfigXml
|
213
217
|
#
|
214
|
-
def commit
|
215
|
-
session.commit
|
218
|
+
def commit(soft_commit = false)
|
219
|
+
session.commit soft_commit
|
216
220
|
end
|
217
221
|
|
218
222
|
# Optimizes the index on the singletion session.
|
@@ -510,10 +514,10 @@ module Sunspot
|
|
510
514
|
end
|
511
515
|
|
512
516
|
#
|
513
|
-
# Sends a commit if the session is dirty (see #dirty?).
|
517
|
+
# Sends a commit (soft or hard) if the session is dirty (see #dirty?).
|
514
518
|
#
|
515
|
-
def commit_if_dirty
|
516
|
-
session.commit_if_dirty
|
519
|
+
def commit_if_dirty(soft_commit = false)
|
520
|
+
session.commit_if_dirty soft_commit
|
517
521
|
end
|
518
522
|
|
519
523
|
#
|
@@ -530,8 +534,8 @@ module Sunspot
|
|
530
534
|
#
|
531
535
|
# Sends a commit if the session has deletes since the last commit (see #delete_dirty?).
|
532
536
|
#
|
533
|
-
def commit_if_delete_dirty
|
534
|
-
session.commit_if_delete_dirty
|
537
|
+
def commit_if_delete_dirty(soft_commit = false)
|
538
|
+
session.commit_if_delete_dirty soft_commit
|
535
539
|
end
|
536
540
|
|
537
541
|
# Returns the configuration associated with the singleton session. See
|
data/lib/sunspot/dsl.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
|
-
%w(fields scope paginatable adjustable field_query
|
2
|
-
functional fulltext restriction
|
3
|
-
more_like_this_query function
|
1
|
+
%w(spellcheckable fields scope paginatable adjustable field_query
|
2
|
+
standard_query query_facet functional fulltext restriction
|
3
|
+
restriction_with_near search more_like_this_query function
|
4
|
+
field_group field_stats).each do |file|
|
4
5
|
require File.join(File.dirname(__FILE__), 'dsl', file)
|
5
6
|
end
|
data/lib/sunspot/dsl/fields.rb
CHANGED
@@ -33,13 +33,10 @@ module Sunspot
|
|
33
33
|
# DSL::Fulltext#boost_fields method.
|
34
34
|
#
|
35
35
|
def text(*names, &block)
|
36
|
-
options = names.
|
36
|
+
options = names.last.is_a?(Hash) ? names.pop : {}
|
37
|
+
|
37
38
|
names.each do |name|
|
38
|
-
@setup.add_text_field_factory(
|
39
|
-
name,
|
40
|
-
options || {},
|
41
|
-
&block
|
42
|
-
)
|
39
|
+
@setup.add_text_field_factory(name, options, &block)
|
43
40
|
end
|
44
41
|
end
|
45
42
|
|
@@ -74,35 +71,33 @@ module Sunspot
|
|
74
71
|
#
|
75
72
|
def method_missing(method, *args, &block)
|
76
73
|
options = Util.extract_options_from(args)
|
77
|
-
|
78
|
-
|
79
|
-
else
|
80
|
-
type_string = method.to_s
|
81
|
-
end
|
74
|
+
join = method.to_s == 'join'
|
75
|
+
type_string = join ? options.delete(:type).to_s : method.to_s
|
82
76
|
type_const_name = "#{Util.camel_case(type_string.sub(/^dynamic_/, ''))}Type"
|
83
77
|
trie = options.delete(:trie)
|
84
78
|
type_const_name = "Trie#{type_const_name}" if trie
|
79
|
+
|
85
80
|
begin
|
86
|
-
|
87
|
-
type_class = options[:type]
|
88
81
|
type_class = Type.const_get(type_const_name)
|
89
|
-
rescue
|
82
|
+
rescue NameError
|
90
83
|
if trie
|
91
84
|
raise ArgumentError, "Trie fields are only valid for numeric and time types"
|
92
85
|
else
|
93
86
|
super(method, *args, &block)
|
94
87
|
end
|
95
88
|
end
|
89
|
+
|
96
90
|
type = type_class.instance
|
97
91
|
name = args.shift
|
92
|
+
|
98
93
|
if method.to_s =~ /^dynamic_/
|
99
94
|
if type.accepts_dynamic?
|
100
95
|
@setup.add_dynamic_field_factory(name, type, options, &block)
|
101
96
|
else
|
102
97
|
super(method, *args, &block)
|
103
98
|
end
|
104
|
-
elsif
|
105
|
-
@setup.add_join_field_factory(name, type, options, &block)
|
99
|
+
elsif join
|
100
|
+
@setup.add_join_field_factory(name, type, options.merge(:clazz => @setup.clazz), &block)
|
106
101
|
else
|
107
102
|
@setup.add_field_factory(name, type, options, &block)
|
108
103
|
end
|
@@ -20,12 +20,15 @@ module Sunspot
|
|
20
20
|
# :offset<Integer,String>::
|
21
21
|
# Applies a shift to paginated records. The default is 0.
|
22
22
|
#
|
23
|
+
# :cursor<String>:: Cursor value for cursor-based pagination. The default is nil.
|
24
|
+
#
|
23
25
|
def paginate(options = {})
|
24
26
|
page = options.delete(:page)
|
25
27
|
per_page = options.delete(:per_page)
|
26
28
|
offset = options.delete(:offset)
|
29
|
+
cursor = options.delete(:cursor)
|
27
30
|
raise ArgumentError, "unknown argument #{options.keys.first.inspect} passed to paginate" unless options.empty?
|
28
|
-
@query.paginate(page, per_page, offset)
|
31
|
+
@query.paginate(page, per_page, offset, cursor)
|
29
32
|
end
|
30
33
|
end
|
31
34
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Sunspot
|
2
|
+
module DSL #:nodoc:
|
3
|
+
module Spellcheckable #:nodoc
|
4
|
+
# Ask Solr to suggest alternative spellings for the query
|
5
|
+
#
|
6
|
+
# ==== Options
|
7
|
+
#
|
8
|
+
# The list of options can be found here: http://wiki.apache.org/solr/SpellCheckComponent
|
9
|
+
def spellcheck(options = {})
|
10
|
+
@query.add_spellcheck(options)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -9,7 +9,7 @@ module Sunspot
|
|
9
9
|
# See Sunspot.search for usage examples
|
10
10
|
#
|
11
11
|
class StandardQuery < FieldQuery
|
12
|
-
include Paginatable, Adjustable
|
12
|
+
include Paginatable, Adjustable, Spellcheckable
|
13
13
|
|
14
14
|
# Specify a phrase that should be searched as fulltext. Only +text+
|
15
15
|
# fields are searched - see DSL::Fields.text
|
@@ -56,67 +56,95 @@ module Sunspot
|
|
56
56
|
# a pizza" will not. Default behavior is a query phrase slop of zero.
|
57
57
|
#
|
58
58
|
def fulltext(keywords, options = {}, &block)
|
59
|
-
if keywords
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
if minimum_match = options.delete(:minimum_match)
|
69
|
-
fulltext_query.minimum_match = minimum_match.to_i
|
70
|
-
end
|
71
|
-
if tie = options.delete(:tie)
|
72
|
-
fulltext_query.tie = tie.to_f
|
73
|
-
end
|
74
|
-
if query_phrase_slop = options.delete(:query_phrase_slop)
|
75
|
-
fulltext_query.query_phrase_slop = query_phrase_slop.to_i
|
76
|
-
end
|
59
|
+
return if not keywords or keywords.to_s =~ /^\s*$/
|
60
|
+
|
61
|
+
field_names = Util.Array(options.delete(:fields)).compact
|
62
|
+
|
63
|
+
add_fulltext(keywords, field_names) do |query, fields|
|
64
|
+
query.minimum_match = options.delete(:minimum_match).to_i if options.key?(:minimum_match)
|
65
|
+
query.tie = options.delete(:tie).to_f if options.key?(:tie)
|
66
|
+
query.query_phrase_slop = options.delete(:query_phrase_slop).to_i if options.key?(:query_phrase_slop)
|
67
|
+
|
77
68
|
if highlight_field_names = options.delete(:highlight)
|
78
69
|
if highlight_field_names == true
|
79
|
-
|
70
|
+
query.add_highlight
|
80
71
|
else
|
81
72
|
highlight_fields = []
|
82
73
|
Util.Array(highlight_field_names).each do |field_name|
|
83
74
|
highlight_fields.concat(@setup.text_fields(field_name))
|
84
75
|
end
|
85
|
-
|
76
|
+
query.add_highlight(highlight_fields)
|
86
77
|
end
|
87
78
|
end
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
79
|
+
|
80
|
+
if block && query
|
81
|
+
fulltext_dsl = Fulltext.new(query, @setup)
|
82
|
+
Util.instance_eval_or_call(fulltext_dsl, &block)
|
83
|
+
else
|
84
|
+
fulltext_dsl = nil
|
94
85
|
end
|
95
|
-
|
86
|
+
|
87
|
+
if fields.empty? && (!fulltext_dsl || !fulltext_dsl.fields_added?)
|
96
88
|
@setup.all_text_fields.each do |field|
|
97
|
-
unless
|
89
|
+
unless query.has_fulltext_field?(field)
|
98
90
|
unless fulltext_dsl && fulltext_dsl.exclude_fields.include?(field.name)
|
99
|
-
|
91
|
+
query.add_fulltext_field(field, field.default_boost)
|
100
92
|
end
|
101
93
|
end
|
102
94
|
end
|
103
95
|
end
|
104
96
|
end
|
105
97
|
end
|
98
|
+
|
106
99
|
alias_method :keywords, :fulltext
|
107
100
|
|
108
101
|
def with(*args)
|
109
102
|
case args.first
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
103
|
+
when String, Symbol
|
104
|
+
if args.length == 1 # NONE
|
105
|
+
field = @setup.field(args[0].to_sym)
|
106
|
+
return DSL::RestrictionWithNear.new(field, @scope, @query, false)
|
107
|
+
end
|
115
108
|
end
|
116
109
|
|
117
110
|
# else
|
118
111
|
super
|
119
112
|
end
|
113
|
+
|
114
|
+
def any(&block)
|
115
|
+
@query.disjunction do
|
116
|
+
Util.instance_eval_or_call(self, &block)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def all(&block)
|
121
|
+
@query.conjunction do
|
122
|
+
Util.instance_eval_or_call(self, &block)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
def add_fulltext(keywords, field_names)
|
129
|
+
return yield(@query.add_fulltext(keywords), []) unless field_names.any?
|
130
|
+
|
131
|
+
all_fields = field_names.map { |name| @setup.text_fields(name) }.flatten
|
132
|
+
all_fields -= join_fields = all_fields.find_all(&:joined?)
|
133
|
+
|
134
|
+
if all_fields.any?
|
135
|
+
fulltext_query = @query.add_fulltext(keywords)
|
136
|
+
all_fields.each { |field| fulltext_query.add_fulltext_field(field, field.default_boost) }
|
137
|
+
yield(fulltext_query, all_fields)
|
138
|
+
end
|
139
|
+
|
140
|
+
if join_fields.any?
|
141
|
+
join_fields.group_by { |field| [field.target, field.from, field.to] }.each_pair do |(target, from, to), fields|
|
142
|
+
join_query = @query.add_join(keywords, target, from, to)
|
143
|
+
fields.each { |field| join_query.add_fulltext_field(field, field.default_boost) }
|
144
|
+
yield(join_query, fields)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
120
148
|
end
|
121
149
|
end
|
122
150
|
end
|
data/lib/sunspot/field.rb
CHANGED
@@ -82,17 +82,35 @@ module Sunspot
|
|
82
82
|
!!@more_like_this
|
83
83
|
end
|
84
84
|
|
85
|
+
#
|
86
|
+
# Whether the field was joined from another model.
|
87
|
+
#
|
88
|
+
# ==== Returns
|
89
|
+
#
|
90
|
+
# Boolean:: True if this field was joined from another model
|
91
|
+
#
|
92
|
+
def joined?
|
93
|
+
!!@joined
|
94
|
+
end
|
95
|
+
|
85
96
|
def hash
|
86
97
|
indexed_name.hash
|
87
98
|
end
|
88
99
|
|
89
100
|
def eql?(field)
|
90
|
-
indexed_name == field.indexed_name
|
101
|
+
field.is_a?(self.class) && indexed_name == field.indexed_name
|
91
102
|
end
|
92
103
|
alias_method :==, :eql?
|
93
104
|
|
94
105
|
private
|
95
106
|
|
107
|
+
#
|
108
|
+
# Raise if an unknown option passed
|
109
|
+
#
|
110
|
+
def check_options(options)
|
111
|
+
raise ArgumentError, "Unknown field option #{options.keys.first.inspect} provided for field #{name.inspect}" unless options.empty?
|
112
|
+
end
|
113
|
+
|
96
114
|
#
|
97
115
|
# Determine the indexed name. If the :as option is given use that, otherwise
|
98
116
|
# create the value based on the indexed_name of the type with additional
|
@@ -107,7 +125,8 @@ module Sunspot
|
|
107
125
|
if options[:as]
|
108
126
|
options.delete(:as).to_s
|
109
127
|
else
|
110
|
-
|
128
|
+
name = options[:prefix] ? @name.to_s.sub(/^#{options[:prefix]}_/, '') : @name
|
129
|
+
"#{@type.indexed_name(name)}#{'m' if multiple? }#{'s' if @stored}#{'v' if more_like_this?}"
|
111
130
|
end
|
112
131
|
end
|
113
132
|
|
@@ -129,7 +148,8 @@ module Sunspot
|
|
129
148
|
@multiple = true
|
130
149
|
@boost = options.delete(:boost)
|
131
150
|
@default_boost = options.delete(:default_boost)
|
132
|
-
|
151
|
+
|
152
|
+
check_options(options)
|
133
153
|
end
|
134
154
|
|
135
155
|
def indexed_name
|
@@ -154,24 +174,50 @@ module Sunspot
|
|
154
174
|
elsif reference.respond_to?(:to_sym)
|
155
175
|
reference.to_sym
|
156
176
|
end
|
157
|
-
raise ArgumentError, "Unknown field option #{options.keys.first.inspect} provided for field #{name.inspect}" unless options.empty?
|
158
|
-
end
|
159
177
|
|
178
|
+
check_options(options)
|
179
|
+
end
|
160
180
|
end
|
161
181
|
|
182
|
+
#
|
183
|
+
# JoinField encapsulates attributes from referenced models.
|
184
|
+
# Could be of any type
|
185
|
+
#
|
162
186
|
class JoinField < Field #:nodoc:
|
187
|
+
attr_reader :default_boost, :target
|
163
188
|
|
164
189
|
def initialize(name, type, options = {})
|
165
190
|
@multiple = !!options.delete(:multiple)
|
191
|
+
|
166
192
|
super(name, type, options)
|
167
|
-
|
168
|
-
|
193
|
+
|
194
|
+
@prefix = options.delete(:prefix)
|
195
|
+
@join = options.delete(:join)
|
196
|
+
@clazz = options.delete(:clazz)
|
197
|
+
@target = options.delete(:target)
|
198
|
+
@default_boost = options.delete(:default_boost)
|
199
|
+
@joined = true
|
200
|
+
|
201
|
+
check_options(options)
|
202
|
+
end
|
203
|
+
|
204
|
+
def from
|
205
|
+
Sunspot::Setup.for(@target).field(@join[:from]).indexed_name
|
206
|
+
end
|
207
|
+
|
208
|
+
def to
|
209
|
+
Sunspot::Setup.for(@clazz).field(@join[:to]).indexed_name
|
169
210
|
end
|
170
211
|
|
171
212
|
def local_params
|
172
|
-
"{!join
|
213
|
+
"{!join from=#{from} to=#{to}}"
|
173
214
|
end
|
174
215
|
|
216
|
+
def eql?(field)
|
217
|
+
super && target == field.target && from == field.from && to == field.to
|
218
|
+
end
|
219
|
+
|
220
|
+
alias_method :==, :eql?
|
175
221
|
end
|
176
222
|
|
177
223
|
class TypeField #:nodoc:
|
@@ -78,12 +78,11 @@ module Sunspot
|
|
78
78
|
|
79
79
|
class Join < Abstract
|
80
80
|
def initialize(name, type, options = {}, &block)
|
81
|
-
super(name, options, &block)
|
81
|
+
super(options[:prefix] ? "#{options[:prefix]}_#{name}" : name, options, &block)
|
82
82
|
unless name.to_s =~ /^\w+$/
|
83
83
|
raise ArgumentError, "Invalid field name #{name}: only letters, numbers, and underscores are allowed."
|
84
84
|
end
|
85
|
-
@field =
|
86
|
-
JoinField.new(name, type, options)
|
85
|
+
@field = JoinField.new(self.name, type, options)
|
87
86
|
end
|
88
87
|
|
89
88
|
#
|
@@ -98,7 +97,6 @@ module Sunspot
|
|
98
97
|
# into the Solr document for indexing. (noop here for joins)
|
99
98
|
#
|
100
99
|
def populate_document(document, model) #:nodoc:
|
101
|
-
|
102
100
|
end
|
103
101
|
|
104
102
|
#
|
data/lib/sunspot/indexer.rb
CHANGED
@@ -8,7 +8,6 @@ module Sunspot
|
|
8
8
|
# subclasses).
|
9
9
|
#
|
10
10
|
class Indexer #:nodoc:
|
11
|
-
include RSolr::Char
|
12
11
|
|
13
12
|
def initialize(connection)
|
14
13
|
@connection = connection
|
@@ -55,7 +54,7 @@ module Sunspot
|
|
55
54
|
#
|
56
55
|
def remove_all(clazz = nil)
|
57
56
|
if clazz
|
58
|
-
@connection.delete_by_query("type:#{escape(clazz.name)}")
|
57
|
+
@connection.delete_by_query("type:#{Util.escape(clazz.name)}")
|
59
58
|
else
|
60
59
|
@connection.delete_by_query("*:*")
|
61
60
|
end
|