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