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
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
|