sunspot 2.1.1 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/lib/sunspot.rb +13 -9
  2. data/lib/sunspot/dsl.rb +4 -3
  3. data/lib/sunspot/dsl/fields.rb +11 -16
  4. data/lib/sunspot/dsl/paginatable.rb +4 -1
  5. data/lib/sunspot/dsl/spellcheckable.rb +14 -0
  6. data/lib/sunspot/dsl/standard_query.rb +63 -35
  7. data/lib/sunspot/field.rb +54 -8
  8. data/lib/sunspot/field_factory.rb +2 -4
  9. data/lib/sunspot/indexer.rb +1 -2
  10. data/lib/sunspot/query.rb +2 -2
  11. data/lib/sunspot/query/abstract_fulltext.rb +69 -0
  12. data/lib/sunspot/query/common_query.rb +13 -2
  13. data/lib/sunspot/query/composite_fulltext.rb +58 -8
  14. data/lib/sunspot/query/dismax.rb +14 -67
  15. data/lib/sunspot/query/function_query.rb +1 -2
  16. data/lib/sunspot/query/geo.rb +1 -1
  17. data/lib/sunspot/query/join.rb +90 -0
  18. data/lib/sunspot/query/pagination.rb +12 -4
  19. data/lib/sunspot/query/restriction.rb +3 -4
  20. data/lib/sunspot/query/sort.rb +6 -0
  21. data/lib/sunspot/query/sort_composite.rb +7 -0
  22. data/lib/sunspot/query/spellcheck.rb +19 -0
  23. data/lib/sunspot/query/standard_query.rb +24 -2
  24. data/lib/sunspot/query/text_field_boost.rb +1 -3
  25. data/lib/sunspot/search/abstract_search.rb +10 -1
  26. data/lib/sunspot/search/cursor_paginated_collection.rb +32 -0
  27. data/lib/sunspot/search/paginated_collection.rb +1 -0
  28. data/lib/sunspot/search/standard_search.rb +71 -3
  29. data/lib/sunspot/session.rb +6 -6
  30. data/lib/sunspot/setup.rb +6 -1
  31. data/lib/sunspot/util.rb +46 -13
  32. data/lib/sunspot/version.rb +1 -1
  33. data/spec/api/query/fulltext_examples.rb +150 -1
  34. data/spec/api/query/geo_examples.rb +2 -6
  35. data/spec/api/query/join_spec.rb +3 -3
  36. data/spec/api/query/ordering_pagination_examples.rb +14 -0
  37. data/spec/api/query/spellcheck_examples.rb +20 -0
  38. data/spec/api/query/standard_spec.rb +1 -0
  39. data/spec/api/search/cursor_paginated_collection_spec.rb +35 -0
  40. data/spec/api/search/paginated_collection_spec.rb +1 -0
  41. data/spec/api/session_spec.rb +36 -2
  42. data/spec/integration/spellcheck_spec.rb +74 -0
  43. data/spec/mocks/connection.rb +5 -3
  44. data/spec/mocks/photo.rb +12 -4
  45. data/spec/spec_helper.rb +4 -0
  46. metadata +24 -5
  47. checksums.yaml +0 -7
@@ -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 paginate(page, per_page, offset = nil)
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 add(keywords)
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
- case @components.length
20
- when 0
34
+ if @components.length == 0
21
35
  {}
22
- when 1
23
- @components.first.to_params.merge(:fl => '* score')
36
+ elsif @components.length > 1 or @components.find { |c| c.is_a?(Join) }
37
+ to_subquery.merge(:fl => '* score')
24
38
  else
25
- to_subqueries.merge(:fl => '* score')
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 to_subqueries
32
- { :q => @components.map { |dismax| dismax.to_subquery }.join(' ') }
81
+ def connector
82
+ 'AND'
33
83
  end
34
84
  end
35
85
  end
@@ -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
- if @minimum_match
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
- # Assign a new boost query and return it.
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
- @fulltext_fields[field.indexed_name] = TextFieldBoost.new(field, boost)
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
 
@@ -21,7 +21,7 @@ module Sunspot
21
21
  end
22
22
 
23
23
  def to_subquery
24
- "(#{to_boolean_query})"
24
+ { :q => "(#{to_boolean_query})" }
25
25
  end
26
26
 
27
27
  private
@@ -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
- { :start => start, :rows => rows }
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