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