sunspot 0.9.8 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (115) hide show
  1. data/History.txt +32 -0
  2. data/README.rdoc +40 -3
  3. data/TODO +10 -8
  4. data/VERSION.yml +2 -2
  5. data/bin/sunspot-configure-solr +22 -28
  6. data/bin/sunspot-solr +50 -29
  7. data/lib/sunspot/adapters.rb +1 -1
  8. data/lib/sunspot/composite_setup.rb +13 -15
  9. data/lib/sunspot/configuration.rb +14 -0
  10. data/lib/sunspot/data_extractor.rb +3 -0
  11. data/lib/sunspot/dsl/field_query.rb +33 -6
  12. data/lib/sunspot/dsl/fields.rb +14 -1
  13. data/lib/sunspot/dsl/fulltext.rb +168 -0
  14. data/lib/sunspot/dsl/query.rb +82 -5
  15. data/lib/sunspot/dsl/query_facet.rb +3 -3
  16. data/lib/sunspot/dsl/restriction.rb +7 -7
  17. data/lib/sunspot/dsl/scope.rb +17 -10
  18. data/lib/sunspot/dsl/search.rb +2 -2
  19. data/lib/sunspot/dsl.rb +2 -1
  20. data/lib/sunspot/facet.rb +9 -1
  21. data/lib/sunspot/facet_data.rb +56 -7
  22. data/lib/sunspot/facet_row.rb +2 -0
  23. data/lib/sunspot/field.rb +50 -26
  24. data/lib/sunspot/field_factory.rb +15 -0
  25. data/lib/sunspot/indexer.rb +6 -0
  26. data/lib/sunspot/instantiated_facet.rb +6 -9
  27. data/lib/sunspot/instantiated_facet_row.rb +7 -2
  28. data/lib/sunspot/query/boost_query.rb +20 -0
  29. data/lib/sunspot/query/connective.rb +98 -35
  30. data/lib/sunspot/query/dismax.rb +69 -0
  31. data/lib/sunspot/query/field_facet.rb +1 -22
  32. data/lib/sunspot/query/fulltext_base_query.rb +47 -0
  33. data/lib/sunspot/query/highlighting.rb +43 -0
  34. data/lib/sunspot/query/local.rb +24 -0
  35. data/lib/sunspot/query/pagination.rb +3 -4
  36. data/lib/sunspot/query/query.rb +93 -0
  37. data/lib/sunspot/query/query_facet.rb +14 -9
  38. data/lib/sunspot/query/query_facet_row.rb +3 -3
  39. data/lib/sunspot/query/query_field_facet.rb +10 -3
  40. data/lib/sunspot/query/restriction.rb +36 -15
  41. data/lib/sunspot/query/scope.rb +3 -159
  42. data/lib/sunspot/query/sort.rb +84 -15
  43. data/lib/sunspot/query/text_field_boost.rb +15 -0
  44. data/lib/sunspot/query.rb +2 -188
  45. data/lib/sunspot/schema.rb +7 -25
  46. data/lib/sunspot/search/highlight.rb +38 -0
  47. data/lib/sunspot/search/hit.rb +50 -3
  48. data/lib/sunspot/search.rb +51 -32
  49. data/lib/sunspot/session.rb +32 -12
  50. data/lib/sunspot/setup.rb +47 -10
  51. data/lib/sunspot/text_field_setup.rb +29 -0
  52. data/lib/sunspot/type.rb +4 -4
  53. data/lib/sunspot/util.rb +27 -1
  54. data/lib/sunspot.rb +8 -17
  55. data/solr/solr/conf/schema.xml +54 -40
  56. data/solr/solr/conf/solrconfig.xml +30 -0
  57. data/solr/solr/lib/geoapi-nogenerics-2.1-M2.jar +0 -0
  58. data/solr/solr/lib/gt2-referencing-2.3.1.jar +0 -0
  59. data/solr/solr/lib/jsr108-0.01.jar +0 -0
  60. data/solr/solr/lib/locallucene.jar +0 -0
  61. data/solr/solr/lib/localsolr.jar +0 -0
  62. data/spec/api/indexer/attributes_spec.rb +100 -0
  63. data/spec/api/indexer/batch_spec.rb +46 -0
  64. data/spec/api/indexer/dynamic_fields_spec.rb +33 -0
  65. data/spec/api/indexer/fixed_fields_spec.rb +57 -0
  66. data/spec/api/indexer/fulltext_spec.rb +43 -0
  67. data/spec/api/indexer/removal_spec.rb +46 -0
  68. data/spec/api/indexer/spec_helper.rb +1 -0
  69. data/spec/api/indexer_spec.rb +1 -308
  70. data/spec/api/query/connectives_spec.rb +162 -0
  71. data/spec/api/query/dsl_spec.rb +12 -0
  72. data/spec/api/query/dynamic_fields_spec.rb +149 -0
  73. data/spec/api/query/faceting_spec.rb +272 -0
  74. data/spec/api/query/fulltext_spec.rb +193 -0
  75. data/spec/api/query/highlighting_spec.rb +138 -0
  76. data/spec/api/query/local_spec.rb +54 -0
  77. data/spec/api/query/ordering_pagination_spec.rb +95 -0
  78. data/spec/api/query/scope_spec.rb +266 -0
  79. data/spec/api/query/spec_helper.rb +1 -0
  80. data/spec/api/query/text_field_scoping_spec.rb +30 -0
  81. data/spec/api/query/types_spec.rb +20 -0
  82. data/spec/api/search/dynamic_fields_spec.rb +27 -0
  83. data/spec/api/search/faceting_spec.rb +206 -0
  84. data/spec/api/search/highlighting_spec.rb +65 -0
  85. data/spec/api/search/hits_spec.rb +62 -0
  86. data/spec/api/search/results_spec.rb +52 -0
  87. data/spec/api/search/search_spec.rb +23 -0
  88. data/spec/api/search/spec_helper.rb +1 -0
  89. data/spec/api/spec_helper.rb +1 -1
  90. data/spec/helpers/indexer_helper.rb +29 -0
  91. data/spec/helpers/query_helper.rb +13 -0
  92. data/spec/helpers/search_helper.rb +78 -0
  93. data/spec/integration/faceting_spec.rb +1 -1
  94. data/spec/integration/highlighting_spec.rb +22 -0
  95. data/spec/integration/keyword_search_spec.rb +65 -0
  96. data/spec/integration/local_search_spec.rb +56 -0
  97. data/spec/integration/scoped_search_spec.rb +15 -1
  98. data/spec/integration/spec_helper.rb +3 -3
  99. data/spec/mocks/connection.rb +14 -1
  100. data/spec/mocks/photo.rb +1 -1
  101. data/spec/mocks/post.rb +5 -3
  102. data/spec/mocks/super_class.rb +2 -0
  103. data/spec/spec_helper.rb +13 -0
  104. data/tasks/gemspec.rake +18 -7
  105. data/tasks/schema.rake +1 -1
  106. data/tasks/spec.rake +1 -1
  107. data/templates/schema.xml.erb +36 -0
  108. metadata +117 -48
  109. data/lib/sunspot/query/base_query.rb +0 -90
  110. data/lib/sunspot/query/dynamic_query.rb +0 -69
  111. data/lib/sunspot/query/field_query.rb +0 -63
  112. data/spec/api/build_search_spec.rb +0 -1017
  113. data/spec/api/query_spec.rb +0 -153
  114. data/spec/api/search_retrieval_spec.rb +0 -362
  115. data/templates/schema.xml.haml +0 -24
data/lib/sunspot/field.rb CHANGED
@@ -3,12 +3,13 @@ module Sunspot
3
3
  attr_accessor :name # The public-facing name of the field
4
4
  attr_accessor :type # The Type of the field
5
5
  attr_accessor :reference # Model class that the value of this field refers to
6
- attr_accessor :attributes
6
+ attr_reader :attributes
7
7
 
8
8
  #
9
9
  #
10
- def initialize(name, type) #:nodoc
10
+ def initialize(name, type, options = {}) #:nodoc
11
11
  @name, @type = name.to_sym, type
12
+ @stored = !!options.delete(:stored)
12
13
  @attributes = {}
13
14
  end
14
15
 
@@ -76,6 +77,15 @@ module Sunspot
76
77
  def multiple?
77
78
  !!@multiple
78
79
  end
80
+
81
+ def hash
82
+ indexed_name.hash
83
+ end
84
+
85
+ def eql?(field)
86
+ indexed_name == field.indexed_name
87
+ end
88
+ alias_method :==, :eql?
79
89
  end
80
90
 
81
91
  #
@@ -87,14 +97,21 @@ module Sunspot
87
97
  # to do otherwise). FulltextField instances always have the type TextType.
88
98
  #
89
99
  class FulltextField < Field #:nodoc:
100
+ attr_reader :boost, :default_boost
101
+
90
102
  def initialize(name, options = {})
91
- super(name, Type::TextType)
92
- if options.has_key?(:boost)
93
- @attributes[:boost] = options.delete(:boost)
94
- end
103
+ super(name, Type::TextType, options)
95
104
  @multiple = true
105
+ if boost = options.delete(:boost)
106
+ @attributes[:boost] = boost
107
+ end
108
+ @default_boost = options.delete(:default_boost)
96
109
  raise ArgumentError, "Unknown field option #{options.keys.first.inspect} provided for field #{name.inspect}" unless options.empty?
97
110
  end
111
+
112
+ def indexed_name
113
+ "#{super}#{'s' if @stored}"
114
+ end
98
115
  end
99
116
 
100
117
  #
@@ -106,7 +123,7 @@ module Sunspot
106
123
  #
107
124
  class AttributeField < Field #:nodoc:
108
125
  def initialize(name, type, options = {})
109
- super(name, type)
126
+ super(name, type, options)
110
127
  @multiple = !!options.delete(:multiple)
111
128
  @reference =
112
129
  if (reference = options.delete(:references)).respond_to?(:name)
@@ -114,7 +131,6 @@ module Sunspot
114
131
  elsif reference.respond_to?(:to_sym)
115
132
  reference.to_sym
116
133
  end
117
- @stored = !!options.delete(:stored)
118
134
  raise ArgumentError, "Unknown field option #{options.keys.first.inspect} provided for field #{name.inspect}" unless options.empty?
119
135
  end
120
136
 
@@ -131,27 +147,35 @@ module Sunspot
131
147
  end
132
148
  end
133
149
 
134
- #
135
- # RandomField instances are used for random sorting.
136
- #
137
- class RandomField #:nodoc:
138
- #
139
- # Never multiple, but this has to return false so Sunspot doesn't barf
140
- # when you try to order by it.
141
- #
142
- def multiple?
143
- false
150
+ class TypeField #:nodoc:
151
+ class <<self
152
+ def instance
153
+ @instance ||= new
154
+ end
155
+ end
156
+
157
+ def indexed_name
158
+ 'type'
159
+ end
160
+
161
+ def to_indexed(clazz)
162
+ clazz.name
163
+ end
164
+ end
165
+
166
+ class IdField #:nodoc:
167
+ class <<self
168
+ def instance
169
+ @instance ||= new
170
+ end
144
171
  end
145
172
 
146
- #
147
- # Solr uses the dynamic field name as a seed for random, so we randomize the
148
- # field name accordingly.
149
- #
150
- # #XXX I think it's bad to use a random number as a seed. Would it be
151
- # better to pass in the current timestamp or some such thing?
152
- #
153
173
  def indexed_name
154
- "random_#{rand(1<<16)}"
174
+ 'id'
175
+ end
176
+
177
+ def to_indexed(id)
178
+ id.to_s
155
179
  end
156
180
  end
157
181
  end
@@ -122,5 +122,20 @@ module Sunspot
122
122
  [@name, @type]
123
123
  end
124
124
  end
125
+
126
+ #XXX Right now this doubles as a Field and a FieldFactory - good idea?
127
+ class Coordinates
128
+ def initialize(name)
129
+ @data_extractor = DataExtractor::AttributeExtractor.new(name)
130
+ end
131
+
132
+ def populate_document(document, model)
133
+ if coordinates = @data_extractor.value_for(model)
134
+ coordinates = Util::Coordinates.new(coordinates)
135
+ document.add_field(:lat, coordinates.lat)
136
+ document.add_field(:long, coordinates.lng)
137
+ end
138
+ end
139
+ end
125
140
  end
126
141
  end
@@ -49,10 +49,16 @@ module Sunspot
49
49
  @connection.delete_by_query("type:#{escape(clazz.name)}")
50
50
  end
51
51
 
52
+ #
53
+ # Start batch processing
54
+ #
52
55
  def start_batch
53
56
  @batch = []
54
57
  end
55
58
 
59
+ #
60
+ # Write batch out to Solr and clear it
61
+ #
56
62
  def flush_batch
57
63
  add_documents(@batch)
58
64
  @batch = nil
@@ -4,6 +4,9 @@ module Sunspot
4
4
  # primary key stored in facet rows' values. The rows are hydrated lazily, but
5
5
  # all rows are hydrated the first time #instance is called on any of the rows.
6
6
  #
7
+ # Instatiated facets are possible for fields which are defined with a
8
+ # :references option.
9
+ #
7
10
  # The #rows method returns InstantiatedFacetRow objects.
8
11
  #
9
12
  class InstantiatedFacet < Facet
@@ -26,17 +29,11 @@ module Sunspot
26
29
  end
27
30
  end
28
31
 
29
- def rows
30
- @facet_data.rows { |value, count| InstantiatedFacetRow.new(value, count, self) }
31
- end
32
-
33
- private
34
-
35
32
  #
36
- # Override the Facet#new_row method to return an InstantiateFacetRow
33
+ # A collection of InstantiatedFacetRow objects
37
34
  #
38
- def new_row(pair)
39
- InstantiatedFacetRow.new(pair, self)
35
+ def rows
36
+ @facet_data.rows { |value, count| InstantiatedFacetRow.new(value, count, self) }
40
37
  end
41
38
  end
42
39
  end
@@ -1,8 +1,13 @@
1
1
  module Sunspot
2
+ #
3
+ # InstantiatedFacetRow objects represent a single value for an instantiated
4
+ # facet. As well as the usual FacetRow methods, InstantedFacetRow objects
5
+ # provide access to the persistent object referenced by the row's value.
6
+ #
2
7
  class InstantiatedFacetRow < FacetRow
3
- attr_writer :instance
8
+ attr_writer :instance #:nodoc:
4
9
 
5
- def initialize(value, count, facet)
10
+ def initialize(value, count, facet) #:nodoc:
6
11
  super(value, count)
7
12
  @facet = facet
8
13
  end
@@ -0,0 +1,20 @@
1
+ module Sunspot
2
+ module Query
3
+ #
4
+ # Representation of a BoostQuery, which allows the searcher to specify a
5
+ # scope for which matching documents should have an extra boost. This is
6
+ # essentially a conjunction, with an extra instance variable containing
7
+ # the boost that should be applied.
8
+ #
9
+ class BoostQuery < Connective::Conjunction #:nodoc:
10
+ def initialize(boost)
11
+ super(false)
12
+ @boost = boost
13
+ end
14
+
15
+ def to_boolean_phrase
16
+ "#{super}^#{@boost}"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,55 +1,110 @@
1
1
  module Sunspot
2
2
  module Query
3
- module Connective #:nodoc:
3
+ module Connective #:nodoc:all
4
4
  #
5
5
  # Base class for connectives (conjunctions and disjunctions).
6
6
  #
7
- class Abstract < Scope
8
- def initialize(setup, negated = false) #:nodoc:
9
- @setup, @negated = setup, negated
7
+ class Abstract
8
+ def initialize(negated = false) #:nodoc:
9
+ @negated = negated
10
10
  @components = []
11
11
  end
12
12
 
13
13
  #
14
- # Connective as solr params.
14
+ # Add a restriction to the connective.
15
15
  #
16
- def to_params #:nodoc:
17
- { :fq => to_boolean_phrase }
16
+ def add_restriction(field, restriction_type, value, negated = false)
17
+ @components << restriction_type.new(field, value, negated)
18
18
  end
19
19
 
20
20
  #
21
- # Express the connective as a Lucene boolean phrase.
21
+ # Add a shorthand restriction; the restriction type is determined by
22
+ # the value.
22
23
  #
23
- def to_boolean_phrase #:nodoc:
24
- phrase = if @components.length == 1
25
- @components.first.to_boolean_phrase
26
- else
27
- component_phrases = @components.map do |component|
28
- component.to_boolean_phrase
24
+ def add_shorthand_restriction(field, value, negated = false)
25
+ restriction_type =
26
+ case value
27
+ when Array then Restriction::AnyOf
28
+ when Range then Restriction::Between
29
+ else Restriction::EqualTo
29
30
  end
30
- "(#{component_phrases.join(" #{connector} ")})"
31
- end
32
- if negated?
33
- "-#{phrase}"
34
- else
35
- phrase
36
- end
31
+ add_restriction(field, restriction_type, value, negated)
32
+ end
33
+
34
+ #
35
+ # Add a negated restriction. The added restriction will match all
36
+ # documents who do not match the terms of the restriction.
37
+ #
38
+ def add_negated_restriction(field, restriction_type, value)
39
+ add_restriction(field, restriction_type, value, true)
40
+ end
41
+
42
+ #
43
+ # Add a negated shorthand restriction (see add_shorthand_restriction)
44
+ #
45
+ def add_negated_shorthand_restriction(field, value)
46
+ add_shorthand_restriction(field, value, true)
47
+ end
48
+
49
+ #
50
+ # Add a new conjunction and return it.
51
+ #
52
+ def add_conjunction
53
+ add_component(Conjunction.new)
54
+ end
55
+
56
+ #
57
+ # Add a new disjunction and return it.
58
+ #
59
+ def add_disjunction
60
+ add_component(Disjunction.new)
37
61
  end
38
62
 
39
63
  #
40
- # Add a component to the connective. All components must implement the
41
- # #to_boolean_phrase method.
64
+ # Add an arbitrary component to the conjunction, and return it.
65
+ # The component must respond to #to_boolean_phrase
42
66
  #
43
- def add_component(component) #:nodoc:
67
+ def add_component(component)
44
68
  @components << component
69
+ component
45
70
  end
46
71
 
72
+ #
73
+ # Express the connective as a Lucene boolean phrase.
74
+ #
75
+ def to_boolean_phrase #:nodoc:
76
+ unless @components.empty?
77
+ phrase =
78
+ if @components.length == 1
79
+ @components.first.to_boolean_phrase
80
+ else
81
+ component_phrases = @components.map do |component|
82
+ component.to_boolean_phrase
83
+ end
84
+ "(#{component_phrases.join(" #{connector} ")})"
85
+ end
86
+ if negated?
87
+ "-#{phrase}"
88
+ else
89
+ phrase
90
+ end
91
+ end
92
+ end
93
+
94
+ #
95
+ # Connectives can be negated during the process of denormalization that
96
+ # is performed when a disjunction contains a negated component. This
97
+ # method conforms to the duck type for all boolean query components.
98
+ #
47
99
  def negated?
48
100
  @negated
49
101
  end
50
102
 
103
+ #
104
+ # Returns a new connective that's a negated version of this one.
105
+ #
51
106
  def negate
52
- negated = self.class.new(@setup, !negated?)
107
+ negated = self.class.new(!negated?)
53
108
  for component in @components
54
109
  negated.add_component(component)
55
110
  end
@@ -67,6 +122,9 @@ module Sunspot
67
122
  end
68
123
  end
69
124
 
125
+ #
126
+ # Express this disjunction as a Lucene boolean phrase
127
+ #
70
128
  def to_boolean_phrase
71
129
  if @components.any? { |component| component.negated? }
72
130
  denormalize.to_boolean_phrase
@@ -76,16 +134,8 @@ module Sunspot
76
134
  end
77
135
 
78
136
  #
79
- # Add a conjunction to the disjunction. This overrides the method in
80
- # the Scope class since scopes are implicitly conjunctive and thus
81
- # can return themselves as a conjunction. Inside a disjunction, however,
82
- # a conjunction must explicitly be created.
137
+ # No-op - this is already a disjunction
83
138
  #
84
- def add_conjunction
85
- @components << conjunction = Conjunction.new(setup)
86
- conjunction
87
- end
88
-
89
139
  def add_disjunction
90
140
  self
91
141
  end
@@ -96,8 +146,17 @@ module Sunspot
96
146
  'OR'
97
147
  end
98
148
 
149
+ #
150
+ # If a disjunction contains negated components, it must be
151
+ # "denormalized", because the Lucene parser interprets any negated
152
+ # boolean phrase using AND semantics (this isn't a bug, it's just a
153
+ # subtlety of how Lucene parses queries). So, per DeMorgan's law we
154
+ # create a negated conjunction and add to it all of our components,
155
+ # negated themselves, which creates a query whose Lucene semantics are
156
+ # in line with our intentions.
157
+ #
99
158
  def denormalize
100
- denormalized = self.class.inverse.new(@setup, !negated?)
159
+ denormalized = self.class.inverse.new(!negated?)
101
160
  for component in @components
102
161
  denormalized.add_component(component.negate)
103
162
  end
@@ -115,6 +174,10 @@ module Sunspot
115
174
  end
116
175
  end
117
176
 
177
+ def add_conjunction
178
+ self
179
+ end
180
+
118
181
  private
119
182
 
120
183
  def connector
@@ -0,0 +1,69 @@
1
+ module Sunspot
2
+ module Query
3
+ class Dismax
4
+ def initialize(keywords)
5
+ @keywords = keywords
6
+ @fulltext_fields = {}
7
+ end
8
+
9
+ #
10
+ # The query as Solr parameters
11
+ #
12
+ def to_params
13
+ params = { :q => @keywords }
14
+ params[:fl] = '* score'
15
+ params[:qf] = @fulltext_fields.values.map { |field| field.to_boosted_field }.join(' ')
16
+ params[:defType] = 'dismax'
17
+ if @phrase_fields
18
+ params[:pf] = @phrase_fields.map { |field| field.to_boosted_field }.join(' ')
19
+ end
20
+ if @boost_query
21
+ params[:bq] = @boost_query.to_boolean_phrase
22
+ end
23
+ if @highlight
24
+ Sunspot::Util.deep_merge!(params, @highlight.to_params)
25
+ end
26
+ params
27
+ end
28
+
29
+ #
30
+ # Assign a new boost query and return it.
31
+ #
32
+ def create_boost_query(factor)
33
+ @boost_query = BoostQuery.new(factor)
34
+ end
35
+
36
+ #
37
+ # Add a fulltext field to be searched, with optional boost
38
+ #
39
+ def add_fulltext_field(field, boost = nil)
40
+ @fulltext_fields[field.indexed_name] = TextFieldBoost.new(field, boost)
41
+ end
42
+
43
+ #
44
+ # Add a phrase field for extra boost
45
+ #
46
+ def add_phrase_field(field, boost = nil)
47
+ @phrase_fields ||= []
48
+ @phrase_fields << TextFieldBoost.new(field, boost)
49
+ end
50
+
51
+ #
52
+ # Set highlighting options for the query. If fields is empty, the
53
+ # Highlighting object won't pass field names at all, which means
54
+ # the dismax's :qf parameter will be used by Solr.
55
+ #
56
+ def set_highlight(fields=[], options={})
57
+ @highlight = Highlighting.new(fields, options)
58
+ end
59
+
60
+ #
61
+ # Determine if a given field is being searched. Used by DSL to avoid
62
+ # overwriting boost parameters when injecting defaults.
63
+ #
64
+ def has_fulltext_field?(field)
65
+ @fulltext_fields.has_key?(field.indexed_name)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -29,7 +29,7 @@ module Sunspot
29
29
  end
30
30
  DateFieldFacet.new(field, options)
31
31
  elsif options.has_key?(:only)
32
- QueryFieldFacet.new(field, options.delete(:only))
32
+ QueryFieldFacet.new(field, options.delete(:only), options)
33
33
  else
34
34
  FieldFacet.new(field, options)
35
35
  end
@@ -78,8 +78,6 @@ module Sunspot
78
78
  end
79
79
 
80
80
  class DateFieldFacet < FieldFacet #:nodoc:
81
- ALLOWED_OTHER = Set.new(%w(before after between none all))
82
-
83
81
  #
84
82
  # Convert the facet to date params.
85
83
  #
@@ -126,25 +124,6 @@ module Sunspot
126
124
  def interval
127
125
  @options[:time_interval] || 86400
128
126
  end
129
-
130
- #
131
- # Other time ranges to create facet rows for. Allowed values are defined
132
- # in ALLOWED_OTHER constant.
133
- #
134
- def others
135
- if others = @options[:time_other]
136
- Array(others).map do |other|
137
- other = other.to_s
138
- unless ALLOWED_OTHER.include?(other)
139
- raise(
140
- ArgumentError,
141
- "#{other.inspect} is not a valid argument for :time_other"
142
- )
143
- end
144
- other
145
- end
146
- end
147
- end
148
127
  end
149
128
  end
150
129
  end
@@ -0,0 +1,47 @@
1
+ module Sunspot
2
+ module Query
3
+ class FulltextBaseQuery < BaseQuery #:nodoc:
4
+ def initialize(keywords, options, types, setup)
5
+ super(types, setup)
6
+ @keywords = keywords
7
+
8
+ if highlight_options = options.delete(:highlight)
9
+ set_highlight(highlight_options == true ? [] : highlight_options)
10
+ end
11
+
12
+ if fulltext_fields = options.delete(:fields)
13
+ Array(fulltext_fields).each do |field|
14
+ add_fulltext_field(field)
15
+ end
16
+ end
17
+ end
18
+
19
+
20
+ private
21
+
22
+ def text_fields(field_names)
23
+ field_names.inject([]) do |fields, name|
24
+ fields.concat(@setup.text_fields(name))
25
+ end
26
+ end
27
+
28
+ #
29
+ # Returns the names of text fields that should be queried in a keyword
30
+ # search. If specific fields are requested, use those; otherwise use the
31
+ # union of all fields configured for the types under search.
32
+ #
33
+ def query_fields
34
+ @query_fields ||=
35
+ begin
36
+ fulltext_fields =
37
+ @fulltext_fields || @setup.all_text_fields.map do |field|
38
+ TextFieldBoost.new(field)
39
+ end
40
+ fulltext_fields.map do |fulltext_field|
41
+ fulltext_field.to_boosted_field
42
+ end.join(' ')
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,43 @@
1
+ module Sunspot
2
+ module Query
3
+ #
4
+ # A query component that builds parameters for requesting highlights
5
+ #
6
+ class Highlighting #:nodoc:
7
+ def initialize(fields=[], options={})
8
+ @fields = fields
9
+ @options = options
10
+ end
11
+
12
+ #
13
+ # Return Solr highlighting params
14
+ #
15
+ def to_params
16
+ params = {
17
+ :hl => 'on',
18
+ :"hl.simple.pre" => '@@@hl@@@',
19
+ :"hl.simple.post" => '@@@endhl@@@'
20
+ }
21
+ unless @fields.empty?
22
+ params[:"hl.fl"] = @fields.map { |field| field.indexed_name }
23
+ end
24
+ if max_snippets = @options[:max_snippets]
25
+ params[:"hl.snippets"] = max_snippets
26
+ end
27
+ if fragment_size = @options[:fragment_size]
28
+ params[:"hl.fragsize"] = fragment_size
29
+ end
30
+ if @options[:merge_continuous_fragments]
31
+ params[:"hl.mergeContinuous"] = 'true'
32
+ end
33
+ if @options[:phrase_highlighter]
34
+ params[:"hl.usePhraseHighlighter"] = 'true'
35
+ if @options[:require_field_match]
36
+ params[:"hl.requireFieldMatch"] = 'true'
37
+ end
38
+ end
39
+ params
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,24 @@
1
+ module Sunspot
2
+ module Query
3
+ #
4
+ # This query component generates parameters for LocalSolr geo-radial
5
+ # searches. The LocalSolr API is fairly rigid, so the Local component
6
+ # doesn't have any options - it just takes coordinates and a radius, and
7
+ # generates the appropriate parameters.
8
+ #
9
+ class Local #:nodoc:
10
+ def initialize(coordinates, radius)
11
+ @coordinates, @radius = Util::Coordinates.new(coordinates), radius
12
+ end
13
+
14
+ def to_params
15
+ {
16
+ :qt => 'geo',
17
+ :lat => @coordinates.lat,
18
+ :long => @coordinates.lng,
19
+ :radius => @radius
20
+ }
21
+ end
22
+ end
23
+ end
24
+ end
@@ -8,8 +8,7 @@ module Sunspot
8
8
  class Pagination #:nodoc:
9
9
  attr_reader :page, :per_page
10
10
 
11
- def initialize(configuration, page = nil, per_page = nil)
12
- @configuration = configuration
11
+ def initialize(page = nil, per_page = nil)
13
12
  self.page, self.per_page = page, per_page
14
13
  end
15
14
 
@@ -18,11 +17,11 @@ module Sunspot
18
17
  end
19
18
 
20
19
  def page=(page)
21
- @page = page || 1
20
+ @page = page.to_i if page
22
21
  end
23
22
 
24
23
  def per_page=(per_page)
25
- @per_page = per_page || @configuration.pagination.default_per_page
24
+ @per_page = per_page.to_i if per_page
26
25
  end
27
26
 
28
27
  private