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/query.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
%w(filter abstract_field_facet connective boost_query date_field_facet range_facet dismax
|
2
|
-
field_facet highlighting pagination restriction common_query
|
1
|
+
%w(filter abstract_field_facet connective boost_query date_field_facet range_facet abstract_fulltext dismax join
|
2
|
+
field_facet highlighting pagination restriction common_query spellcheck
|
3
3
|
standard_query more_like_this more_like_this_query geo geofilt bbox query_facet
|
4
4
|
scope sort sort_composite text_field_boost function_query field_stats
|
5
5
|
composite_fulltext field_group).each do |file|
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Sunspot
|
2
|
+
module Query
|
3
|
+
|
4
|
+
#
|
5
|
+
# Solr query abstraction
|
6
|
+
#
|
7
|
+
class AbstractFulltext
|
8
|
+
attr_reader :fulltext_fields
|
9
|
+
|
10
|
+
#
|
11
|
+
# Assign a new boost query and return it.
|
12
|
+
#
|
13
|
+
def create_boost_query(factor)
|
14
|
+
@boost_queries << boost_query = BoostQuery.new(factor)
|
15
|
+
boost_query
|
16
|
+
end
|
17
|
+
|
18
|
+
#
|
19
|
+
# Add a boost function
|
20
|
+
#
|
21
|
+
def add_boost_function(function_query)
|
22
|
+
@boost_functions << function_query
|
23
|
+
end
|
24
|
+
|
25
|
+
#
|
26
|
+
# Add a fulltext field to be searched, with optional boost.
|
27
|
+
#
|
28
|
+
def add_fulltext_field(field, boost = nil)
|
29
|
+
@fulltext_fields[field.indexed_name] = TextFieldBoost.new(field, boost)
|
30
|
+
end
|
31
|
+
|
32
|
+
#
|
33
|
+
# Add a phrase field for extra boost.
|
34
|
+
#
|
35
|
+
def add_phrase_field(field, boost = nil)
|
36
|
+
@phrase_fields ||= []
|
37
|
+
@phrase_fields << TextFieldBoost.new(field, boost)
|
38
|
+
end
|
39
|
+
|
40
|
+
#
|
41
|
+
# Set highlighting options for the query. If fields is empty, the
|
42
|
+
# Highlighting object won't pass field names at all, which means
|
43
|
+
# the dismax's :qf parameter will be used by Solr.
|
44
|
+
#
|
45
|
+
def add_highlight(fields=[], options={})
|
46
|
+
@highlights << Highlighting.new(fields, options)
|
47
|
+
end
|
48
|
+
|
49
|
+
#
|
50
|
+
# Determine if a given field is being searched. Used by DSL to avoid
|
51
|
+
# overwriting boost parameters when injecting defaults.
|
52
|
+
#
|
53
|
+
def has_fulltext_field?(field)
|
54
|
+
@fulltext_fields.has_key?(field.indexed_name)
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def escape_param(key, value)
|
60
|
+
"#{key}='#{escape_quotes(Array(value).join(" "))}'"
|
61
|
+
end
|
62
|
+
|
63
|
+
def escape_quotes(value)
|
64
|
+
return value unless value.is_a? String
|
65
|
+
value.gsub(/(['"])/, '\\\\\1')
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -53,14 +53,22 @@ module Sunspot
|
|
53
53
|
stats
|
54
54
|
end
|
55
55
|
|
56
|
-
def
|
56
|
+
def add_spellcheck(options = {})
|
57
|
+
@components << Spellcheck.new(options)
|
58
|
+
end
|
59
|
+
|
60
|
+
def paginate(page, per_page, offset = nil, cursor = nil)
|
57
61
|
if @pagination
|
58
62
|
@pagination.offset = offset
|
59
63
|
@pagination.page = page
|
60
64
|
@pagination.per_page = per_page
|
65
|
+
@pagination.cursor = cursor
|
61
66
|
else
|
62
|
-
@components << @pagination = Pagination.new(page, per_page, offset)
|
67
|
+
@components << @pagination = Pagination.new(page, per_page, offset, cursor)
|
63
68
|
end
|
69
|
+
|
70
|
+
# cursor pagination requires a sort containing a uniqueKey field
|
71
|
+
add_sort(Sunspot::Query::Sort.special(:solr_id).new('asc')) if cursor and !@sort.include?('id ')
|
64
72
|
end
|
65
73
|
|
66
74
|
def to_params
|
@@ -85,6 +93,9 @@ module Sunspot
|
|
85
93
|
@pagination.per_page if @pagination
|
86
94
|
end
|
87
95
|
|
96
|
+
def cursor
|
97
|
+
@pagination.cursor if @pagination
|
98
|
+
end
|
88
99
|
|
89
100
|
private
|
90
101
|
|
@@ -5,31 +5,81 @@ module Sunspot
|
|
5
5
|
@components = []
|
6
6
|
end
|
7
7
|
|
8
|
-
def
|
8
|
+
def add_fulltext(keywords)
|
9
9
|
@components << dismax = Dismax.new(keywords)
|
10
10
|
dismax
|
11
11
|
end
|
12
|
+
|
13
|
+
def add_join(keywords, target, from, to)
|
14
|
+
@components << join = Join.new(keywords, target, from, to)
|
15
|
+
join
|
16
|
+
end
|
12
17
|
|
13
18
|
def add_location(field, lat, lng, options)
|
14
19
|
@components << location = Geo.new(field, lat, lng, options)
|
15
20
|
location
|
16
21
|
end
|
17
22
|
|
23
|
+
def add_disjunction
|
24
|
+
@components << disjunction = Disjunction.new
|
25
|
+
disjunction
|
26
|
+
end
|
27
|
+
|
28
|
+
def add_conjunction
|
29
|
+
@components << conjunction = Conjunction.new
|
30
|
+
conjunction
|
31
|
+
end
|
32
|
+
|
18
33
|
def to_params
|
19
|
-
|
20
|
-
when 0
|
34
|
+
if @components.length == 0
|
21
35
|
{}
|
22
|
-
|
23
|
-
|
36
|
+
elsif @components.length > 1 or @components.find { |c| c.is_a?(Join) }
|
37
|
+
to_subquery.merge(:fl => '* score')
|
24
38
|
else
|
25
|
-
|
39
|
+
@components.first.to_params.merge(:fl => '* score')
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_subquery
|
44
|
+
return {} unless @components.any?
|
45
|
+
|
46
|
+
params = @components.map(&:to_subquery).inject({:q => []}) do |res, subquery|
|
47
|
+
res[:q] << subquery.delete(:q) if subquery[:q]
|
48
|
+
res.merge(subquery)
|
26
49
|
end
|
50
|
+
|
51
|
+
params[:q] = params[:q].size > 1 ? "(#{params[:q].join(" #{connector} ")})" : params[:q].join
|
52
|
+
params
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class Disjunction < CompositeFulltext
|
57
|
+
#
|
58
|
+
# No-op - this is already a disjunction
|
59
|
+
#
|
60
|
+
def add_disjunction
|
61
|
+
self
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def connector
|
67
|
+
'OR'
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class Conjunction < CompositeFulltext
|
72
|
+
#
|
73
|
+
# No-op - this is already a conjunction
|
74
|
+
#
|
75
|
+
def add_conjunction
|
76
|
+
self
|
27
77
|
end
|
28
78
|
|
29
79
|
private
|
30
80
|
|
31
|
-
def
|
32
|
-
|
81
|
+
def connector
|
82
|
+
'AND'
|
33
83
|
end
|
34
84
|
end
|
35
85
|
end
|
data/lib/sunspot/query/dismax.rb
CHANGED
@@ -6,7 +6,7 @@ module Sunspot
|
|
6
6
|
# designed to process user-entered phrases, and search for individual
|
7
7
|
# words across a union of several fields.
|
8
8
|
#
|
9
|
-
class Dismax
|
9
|
+
class Dismax < AbstractFulltext
|
10
10
|
attr_writer :minimum_match, :phrase_slop, :query_phrase_slop, :tie
|
11
11
|
|
12
12
|
def initialize(keywords)
|
@@ -31,34 +31,31 @@ module Sunspot
|
|
31
31
|
params[:fl] = '* score'
|
32
32
|
params[:qf] = @fulltext_fields.values.map { |field| field.to_boosted_field }.join(' ')
|
33
33
|
params[:defType] = 'edismax'
|
34
|
+
params[:mm] = @minimum_match if @minimum_match
|
35
|
+
params[:ps] = @phrase_slop if @phrase_slop
|
36
|
+
params[:qs] = @query_phrase_slop if @query_phrase_slop
|
37
|
+
params[:tie] = @tie if @tie
|
38
|
+
|
34
39
|
if @phrase_fields
|
35
40
|
params[:pf] = @phrase_fields.map { |field| field.to_boosted_field }.join(' ')
|
36
41
|
end
|
42
|
+
|
37
43
|
unless @boost_queries.empty?
|
38
44
|
params[:bq] = @boost_queries.map do |boost_query|
|
39
45
|
boost_query.to_boolean_phrase
|
40
46
|
end
|
41
47
|
end
|
48
|
+
|
42
49
|
unless @boost_functions.empty?
|
43
50
|
params[:bf] = @boost_functions.map do |boost_function|
|
44
51
|
boost_function.to_s
|
45
52
|
end
|
46
53
|
end
|
47
|
-
|
48
|
-
params[:mm] = @minimum_match
|
49
|
-
end
|
50
|
-
if @phrase_slop
|
51
|
-
params[:ps] = @phrase_slop
|
52
|
-
end
|
53
|
-
if @query_phrase_slop
|
54
|
-
params[:qs] = @query_phrase_slop
|
55
|
-
end
|
56
|
-
if @tie
|
57
|
-
params[:tie] = @tie
|
58
|
-
end
|
54
|
+
|
59
55
|
@highlights.each do |highlight|
|
60
56
|
Sunspot::Util.deep_merge!(params, highlight.to_params)
|
61
57
|
end
|
58
|
+
|
62
59
|
params
|
63
60
|
end
|
64
61
|
|
@@ -69,68 +66,18 @@ module Sunspot
|
|
69
66
|
params = self.to_params
|
70
67
|
params.delete :defType
|
71
68
|
params.delete :fl
|
72
|
-
keywords = params.delete(:q)
|
73
|
-
options = params.map { |key, value| escape_param(key, value) }.join(' ')
|
74
|
-
"_query_:\"{!edismax #{options}}#{escape_quotes(keywords)}\""
|
75
|
-
end
|
76
69
|
|
77
|
-
|
78
|
-
|
79
|
-
#
|
80
|
-
def create_boost_query(factor)
|
81
|
-
@boost_queries << boost_query = BoostQuery.new(factor)
|
82
|
-
boost_query
|
83
|
-
end
|
70
|
+
keywords = escape_quotes(params.delete(:q))
|
71
|
+
options = params.map { |key, value| escape_param(key, value) }.join(' ')
|
84
72
|
|
85
|
-
|
86
|
-
# Add a boost function
|
87
|
-
#
|
88
|
-
def add_boost_function(function_query)
|
89
|
-
@boost_functions << function_query
|
73
|
+
{ :q => "_query_:\"{!edismax #{options}}#{keywords}\"" }
|
90
74
|
end
|
91
75
|
|
92
76
|
#
|
93
77
|
# Add a fulltext field to be searched, with optional boost.
|
94
78
|
#
|
95
79
|
def add_fulltext_field(field, boost = nil)
|
96
|
-
|
97
|
-
end
|
98
|
-
|
99
|
-
#
|
100
|
-
# Add a phrase field for extra boost.
|
101
|
-
#
|
102
|
-
def add_phrase_field(field, boost = nil)
|
103
|
-
@phrase_fields ||= []
|
104
|
-
@phrase_fields << TextFieldBoost.new(field, boost)
|
105
|
-
end
|
106
|
-
|
107
|
-
#
|
108
|
-
# Set highlighting options for the query. If fields is empty, the
|
109
|
-
# Highlighting object won't pass field names at all, which means
|
110
|
-
# the dismax's :qf parameter will be used by Solr.
|
111
|
-
#
|
112
|
-
def add_highlight(fields=[], options={})
|
113
|
-
@highlights << Highlighting.new(fields, options)
|
114
|
-
end
|
115
|
-
|
116
|
-
#
|
117
|
-
# Determine if a given field is being searched. Used by DSL to avoid
|
118
|
-
# overwriting boost parameters when injecting defaults.
|
119
|
-
#
|
120
|
-
def has_fulltext_field?(field)
|
121
|
-
@fulltext_fields.has_key?(field.indexed_name)
|
122
|
-
end
|
123
|
-
|
124
|
-
|
125
|
-
private
|
126
|
-
|
127
|
-
def escape_param(key, value)
|
128
|
-
"#{key}='#{escape_quotes(Array(value).join(" "))}'"
|
129
|
-
end
|
130
|
-
|
131
|
-
def escape_quotes(value)
|
132
|
-
return value unless value.is_a? String
|
133
|
-
value.gsub(/(['"])/, '\\\\\1')
|
80
|
+
super unless field.is_a?(Sunspot::JoinField)
|
134
81
|
end
|
135
82
|
|
136
83
|
end
|
@@ -4,7 +4,6 @@ module Sunspot
|
|
4
4
|
# Abstract class for function queries.
|
5
5
|
#
|
6
6
|
class FunctionQuery
|
7
|
-
include RSolr::Char
|
8
7
|
|
9
8
|
def ^(y)
|
10
9
|
@boost_amount = y
|
@@ -34,7 +33,7 @@ module Sunspot
|
|
34
33
|
end
|
35
34
|
|
36
35
|
def to_s
|
37
|
-
"#{escape(@field.indexed_name)}" << (@boost_amount ? "^#{@boost_amount}" : "")
|
36
|
+
"#{Util.escape(@field.indexed_name)}" << (@boost_amount ? "^#{@boost_amount}" : "")
|
38
37
|
end
|
39
38
|
end
|
40
39
|
|
data/lib/sunspot/query/geo.rb
CHANGED
@@ -0,0 +1,90 @@
|
|
1
|
+
module Sunspot
|
2
|
+
module Query
|
3
|
+
|
4
|
+
#
|
5
|
+
# Solr full-text queries use Solr's JoinRequestHandler.
|
6
|
+
#
|
7
|
+
class Join < AbstractFulltext
|
8
|
+
attr_writer :minimum_match, :phrase_slop, :query_phrase_slop, :tie
|
9
|
+
|
10
|
+
def initialize(keywords, target, from, to)
|
11
|
+
@keywords = keywords
|
12
|
+
@target = target
|
13
|
+
@from = from
|
14
|
+
@to = to
|
15
|
+
|
16
|
+
@fulltext_fields = {}
|
17
|
+
|
18
|
+
@minimum_match = nil
|
19
|
+
end
|
20
|
+
|
21
|
+
#
|
22
|
+
# The query as Solr parameters
|
23
|
+
#
|
24
|
+
def to_params
|
25
|
+
params = { :q => @keywords }
|
26
|
+
params[:fl] = '* score'
|
27
|
+
params[:qf] = @fulltext_fields.values.map { |field| field.to_boosted_field }.join(' ')
|
28
|
+
params[:defType] = 'join'
|
29
|
+
params[:mm] = @minimum_match if @minimum_match
|
30
|
+
|
31
|
+
params
|
32
|
+
end
|
33
|
+
|
34
|
+
#
|
35
|
+
# Serialize the query as a Solr nested subquery.
|
36
|
+
#
|
37
|
+
def to_subquery
|
38
|
+
params = self.to_params
|
39
|
+
params.delete :defType
|
40
|
+
params.delete :fl
|
41
|
+
|
42
|
+
keywords = escape_quotes(params.delete(:q))
|
43
|
+
options = params.map { |key, value| escape_param(key, value) }.join(' ')
|
44
|
+
q_name = "q#{@target.name}#{self.object_id}"
|
45
|
+
fq_name = "f#{q_name}"
|
46
|
+
|
47
|
+
{
|
48
|
+
:q => "_query_:\"{!join from=#{@from} to=#{@to} v=$#{q_name} fq=$#{fq_name}}\"",
|
49
|
+
q_name => "_query_:\"{!edismax #{options}}#{keywords}\"",
|
50
|
+
fq_name => "type:#{@target.name}"
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# Assign a new boost query and return it.
|
56
|
+
#
|
57
|
+
def create_boost_query(factor)
|
58
|
+
end
|
59
|
+
|
60
|
+
#
|
61
|
+
# Add a boost function
|
62
|
+
#
|
63
|
+
def add_boost_function(function_query)
|
64
|
+
end
|
65
|
+
|
66
|
+
#
|
67
|
+
# Add a fulltext field to be searched, with optional boost.
|
68
|
+
#
|
69
|
+
def add_fulltext_field(field, boost = nil)
|
70
|
+
super if field.is_a?(Sunspot::JoinField) &&
|
71
|
+
field.target == @target && field.from == @from && field.to == @to
|
72
|
+
end
|
73
|
+
|
74
|
+
#
|
75
|
+
# Add a phrase field for extra boost.
|
76
|
+
#
|
77
|
+
def add_phrase_field(field, boost = nil)
|
78
|
+
end
|
79
|
+
|
80
|
+
#
|
81
|
+
# Set highlighting options for the query. If fields is empty, the
|
82
|
+
# Highlighting object won't pass field names at all, which means
|
83
|
+
# the dismax's :qf parameter will be used by Solr.
|
84
|
+
#
|
85
|
+
def add_highlight(fields=[], options={})
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -6,14 +6,18 @@ module Sunspot
|
|
6
6
|
# reference to it and updates it if pagination is changed.
|
7
7
|
#
|
8
8
|
class Pagination #:nodoc:
|
9
|
-
attr_reader :page, :per_page, :offset
|
9
|
+
attr_reader :page, :per_page, :offset, :cursor
|
10
10
|
|
11
|
-
def initialize(page = nil, per_page = nil, offset = nil)
|
12
|
-
self.offset, self.page, self.per_page = offset, page, per_page
|
11
|
+
def initialize(page = nil, per_page = nil, offset = nil, cursor = nil)
|
12
|
+
self.offset, self.page, self.per_page, self.cursor = offset, page, per_page, cursor
|
13
13
|
end
|
14
14
|
|
15
15
|
def to_params
|
16
|
-
|
16
|
+
if @cursor
|
17
|
+
{ :cursorMark => @cursor, :rows => rows }
|
18
|
+
else
|
19
|
+
{ :start => start, :rows => rows }
|
20
|
+
end
|
17
21
|
end
|
18
22
|
|
19
23
|
def page=(page)
|
@@ -28,6 +32,10 @@ module Sunspot
|
|
28
32
|
@offset = offset.to_i
|
29
33
|
end
|
30
34
|
|
35
|
+
def cursor=(cursor)
|
36
|
+
@cursor = cursor if cursor
|
37
|
+
end
|
38
|
+
|
31
39
|
private
|
32
40
|
|
33
41
|
def start
|