outoftime-sunspot 0.0.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,22 +1,146 @@
1
1
  module Sunspot
2
2
  module DSL
3
+ #
4
+ # This class presents a DSL for constructing queries using the
5
+ # Sunspot.search method. Methods of this class are available inside the
6
+ # search block.
7
+ #
8
+ # See Sunspot.search for usage examples
9
+ #
3
10
  class Query
4
- def initialize(query)
5
- @query = query
11
+ NONE = Object.new
12
+
13
+ def initialize(query) #:nodoc:
14
+ @query = query
6
15
  end
7
16
 
17
+ # Specify a phrase that should be searched as fulltext. Only +text+
18
+ # fields are searched - see DSL::Fields.text
19
+ #
20
+ # Note that the keywords are passed directly to Solr unadulterated. The
21
+ # advantage of this is that users can potentially use boolean logic to
22
+ # make advanced searches. The disadvantage is that syntax errors are
23
+ # possible. This may get better in a future version; suggestions are
24
+ # welcome.
25
+ #
26
+ # ==== Parameters
27
+ #
28
+ # keywords<String>:: phrase to perform fulltext search on
29
+ #
8
30
  def keywords(keywords)
9
31
  @query.keywords = keywords
10
32
  end
11
33
 
12
- def with
13
- @conditions_builder ||= ::Sunspot::DSL::Scope.new(@query)
34
+ #
35
+ # Build a positive restriction. With one argument, this method returns
36
+ # another DSL object which presents methods for attaching various
37
+ # restriction types. With two arguments, acts as a shorthand for creating
38
+ # an equality restriction.
39
+ #
40
+ # ==== Parameters
41
+ #
42
+ # field_name<Symbol>:: Name of the field on which to place the restriction
43
+ # value<Symbol>::
44
+ # If passed, creates an equality restriction with this value
45
+ #
46
+ # ==== Returns
47
+ #
48
+ # Sunspot::DSL::Restriction::
49
+ # Restriction DSL object (if only one argument is passed)
50
+ #
51
+ # ==== Examples
52
+ #
53
+ # An equality restriction:
54
+ #
55
+ # Sunspot.search do
56
+ # with(:blog_id, 1)
57
+ # end
58
+ #
59
+ # Other restriction types:
60
+ #
61
+ # Sunspot.search(Post) do
62
+ # with(:average_rating).greater_than(3.0)
63
+ # end
64
+ #
65
+ def with(field_name, value = NONE)
66
+ if value == NONE
67
+ DSL::Restriction.new(field_name.to_sym, @query, false)
68
+ else
69
+ @query.add_restriction(field_name, Sunspot::Restriction::EqualTo, value, false)
70
+ end
14
71
  end
15
72
 
16
- def conditions
17
- @query.conditions
73
+ #
74
+ # Build a negative restriction (exclusion). This method can take three
75
+ # forms: equality exclusion, exclusion by another restriction, or identity
76
+ # exclusion. The first two forms work the same way as the #with method;
77
+ # the third excludes a specific instance from the search results.
78
+ #
79
+ # ==== Parameters (exclusion by field value)
80
+ #
81
+ # field_name<Symbol>:: Name of the field on which to place the exclusion
82
+ # value<Symbol>::
83
+ # If passed, creates an equality exclusion with this value
84
+ #
85
+ # ==== Parameters (exclusion by identity)
86
+ #
87
+ # args<Object>...::
88
+ # One or more instances that should be excluded from the results
89
+ #
90
+ # ==== Examples
91
+ #
92
+ # An equality exclusion:
93
+ #
94
+ # Sunspot.search(Post) do
95
+ # without(:blog_id, 1)
96
+ # end
97
+ #
98
+ # Other restriction types:
99
+ #
100
+ # Sunspot.search(Post) do
101
+ # without(:average_rating).greater_than(3.0)
102
+ # end
103
+ #
104
+ # Exclusion by identity:
105
+ #
106
+ # Sunspot.search(Post) do
107
+ # without(some_post_instance)
108
+ # end
109
+ #
110
+ def without(*args)
111
+ case args.first
112
+ when String, Symbol
113
+ field_name = args[0]
114
+ value = args.length > 1 ? args[1] : NONE
115
+ if value == NONE
116
+ DSL::Restriction.new(field_name.to_sym, @query, true)
117
+ else
118
+ @query.add_restriction(field_name, Sunspot::Restriction::EqualTo, value, true)
119
+ end
120
+ else
121
+ instances = args
122
+ for instance in instances.flatten
123
+ @query.add_component(Sunspot::Restriction::SameAs.new(instance, true))
124
+ end
125
+ end
18
126
  end
19
127
 
128
+ # Paginate your search. This works the same way as WillPaginate's
129
+ # paginate().
130
+ #
131
+ # Note that Solr searches are _always_ paginated. Not calling #paginate is
132
+ # the equivalent of calling:
133
+ #
134
+ # paginate(:page => 1, :per_page => Sunspot.config.pagination.default_per_page)
135
+ #
136
+ # ==== Options (options)
137
+ #
138
+ # :page<Integer>:: The requested page (required)
139
+ #
140
+ # :per_page<Integer>::
141
+ # How many results to return per page. The default is the value in
142
+ # +Sunspot.config.pagination.default_per_page+
143
+ #
20
144
  def paginate(options = {})
21
145
  page = options.delete(:page) || raise(ArgumentError, "paginate requires a :page argument")
22
146
  per_page = options.delete(:per_page)
@@ -24,9 +148,29 @@ module Sunspot
24
148
  @query.paginate(page, per_page)
25
149
  end
26
150
 
151
+ # Specify the order that results should be returned in. This method can
152
+ # be called multiple times; precedence will be in the order given.
153
+ #
154
+ # ==== Parameters
155
+ #
156
+ # field_name<Symbol>:: the field to use for ordering
157
+ # direction<Symbol>:: :asc or :desc (default :asc)
158
+ #
27
159
  def order_by(field_name, direction = nil)
28
160
  @query.order_by(field_name, direction)
29
161
  end
162
+
163
+ # Request facets on the given field names. See Sunspot::Search#facet and
164
+ # Sunspot::Facet for information on what is returned.
165
+ #
166
+ # ==== Parameters
167
+ #
168
+ # field_names...<Symbol>:: fields for which to return field facets
169
+ def facet(*field_names)
170
+ for field_name in field_names
171
+ @query.add_field_facet(field_name)
172
+ end
173
+ end
30
174
  end
31
175
  end
32
176
  end
@@ -1,34 +1,24 @@
1
1
  module Sunspot
2
2
  module DSL
3
- class Scope
4
- def initialize(query)
5
- @query = query
3
+ #
4
+ # This class presents an API for building restrictions in the query DSL. The
5
+ # methods exposed are the snake-cased names of the classes defined in the
6
+ # Restriction module, with the exception of Base and SameAs. All methods
7
+ # take a single argument, which is the value to be applied to the
8
+ # restriction.
9
+ #
10
+ class Restriction
11
+ def initialize(field_name, query, negative)
12
+ @field_name, @query, @negative = field_name, query, negative
6
13
  end
7
14
 
8
- def method_missing(field_name, *args)
9
- if args.length == 0 then RestrictionBuilder.new(field_name, @query)
10
- elsif args.length == 1 then @query.add_scope @query.build_condition(field_name, ::Sunspot::Restriction::EqualTo, args.first)
11
- else super(field_name.to_sym, *args)
12
- end
13
- end
14
-
15
- class RestrictionBuilder
16
- def initialize(field_name, query)
17
- @field_name, @query = field_name, query
18
- end
19
-
20
- def method_missing(condition_name, *args)
21
- clazz = begin
22
- ::Sunspot::Restriction.const_get(condition_name.to_s.camel_case)
23
- rescue(NameError)
24
- super(condition_name.to_sym, *args)
25
- end
26
- if value = args.first
27
- @query.add_scope @query.build_condition(@field_name, clazz, args.first)
28
- else
29
- @query.interpret_condition @field_name, clazz
15
+ Sunspot::Restriction.names.each do |class_name|
16
+ method_name = Util.snake_case(class_name)
17
+ module_eval(<<-RUBY, __FILE__, __LINE__ + 1)
18
+ def #{method_name}(value)
19
+ @query.add_restriction(@field_name, Sunspot::Restriction::#{class_name}, value, @negative)
30
20
  end
31
- end
21
+ RUBY
32
22
  end
33
23
  end
34
24
  end
@@ -0,0 +1,37 @@
1
+ module Sunspot
2
+ #
3
+ # The facet class encapsulates the information returned by Solr for a
4
+ # particular facet request.
5
+ #
6
+ # See http://wiki.apache.org/solr/SolrFacetingOverview for more information
7
+ # on Solr's faceting capabilities.
8
+ #
9
+ class Facet
10
+ def initialize(facet_values, field) #:nodoc:
11
+ @facet_values, @field = facet_values, field
12
+ end
13
+
14
+ # The name of the field that contains this facet's values
15
+ #
16
+ # ==== Returns
17
+ #
18
+ # Symbol:: The field name
19
+ #
20
+ def field_name
21
+ @field.name
22
+ end
23
+
24
+ # The rows returned for this facet.
25
+ #
26
+ # ==== Returns
27
+ #
28
+ # Array:: Collection of FacetRow objects, in the order returned by Solr
29
+ #
30
+ def rows
31
+ @rows ||=
32
+ @facet_values.map do |facet_value|
33
+ FacetRow.new(facet_value, @field)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,34 @@
1
+ module Sunspot
2
+ # This class encapsulates a facet row (value) for a facet.
3
+ class FacetRow
4
+ def initialize(facet_value, field) #:nodoc:
5
+ @facet_value, @field = facet_value, field
6
+ end
7
+
8
+ # The value associated with the facet. This will be cast according to the
9
+ # field's type; so, for an integer field, this method will return an
10
+ # integer, etc.
11
+ #
12
+ # Note that <strong>+Time+ fields will always return facet values in
13
+ # UTC</strong>.
14
+ #
15
+ # ==== Returns
16
+ #
17
+ # Object:: The value associated with the row, cast to the appropriate type
18
+ #
19
+ def value
20
+ @value ||= @field.cast(@facet_value.name)
21
+ end
22
+
23
+ # The number of documents matching the search parameters that have this
24
+ # value in the facet's field.
25
+ #
26
+ # ==== Returns
27
+ #
28
+ # Integer:: Document count for this value
29
+ #
30
+ def count
31
+ @count ||= @facet_value.value
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,21 @@
1
+ module Sunspot
2
+ module Facets
3
+ #
4
+ # Encapsulates a query component representing a field facet. Users create
5
+ # instances using DSL::Query#facet
6
+ #
7
+ class FieldFacet #:nodoc:
8
+ def initialize(field)
9
+ @field = field
10
+ end
11
+
12
+ # ==== Returns
13
+ #
14
+ # Hash:: solr-ruby params for this field facet
15
+ #
16
+ def to_params
17
+ { :facets => { :fields => [@field.indexed_name] }}
18
+ end
19
+ end
20
+ end
21
+ end
data/lib/sunspot/field.rb CHANGED
@@ -1,39 +1,72 @@
1
1
  module Sunspot
2
- module Field
2
+ module Field #:nodoc[all]
3
+ #
4
+ # Field classes encapsulate information about a field that has been configured
5
+ # for search and indexing. They expose methods that are useful for both
6
+ # operations.
7
+ #
8
+ # Subclasses of Field::Base must implement the method #value_for
9
+ #
3
10
  class Base
4
- attr_accessor :name, :type
11
+ attr_accessor :name # The public-facing name of the field
12
+ attr_accessor :type # The Type of the field
5
13
 
6
- def initialize(name, type, options = {})
7
- @name, @type = name, type
14
+ def initialize(name, type, options = {}) #:nodoc
15
+ @name, @type = name.to_sym, type
8
16
  @multiple = options.delete(:multiple)
9
17
  raise ArgumentError, "Unknown field option #{options.keys.first.inspect} provided for field #{name.inspect}" unless options.empty?
10
18
  end
11
19
 
20
+ # A key-value pair where the key is the field's indexed name and the
21
+ # value is the value that should be indexed for the given model. This can
22
+ # be merged directly into the document hash for adding to solr-ruby.
23
+ #
24
+ # ==== Parameters
25
+ #
26
+ # model<Object>:: the model from which to extract the value
27
+ #
28
+ # ==== Returns
29
+ #
30
+ # Hash:: a single key-value pair with the field name and value
31
+ #
12
32
  def pair_for(model)
13
- if value = value_for(model)
33
+ unless (value = value_for(model)).nil?
14
34
  { indexed_name.to_sym => to_indexed(value) }
15
35
  else
16
36
  {}
17
37
  end
18
38
  end
19
39
 
20
- def ==(other)
21
- other.respond_to?(:name) &&
22
- other.respond_to?(:type) &&
23
- self.name == other.name &&
24
- self.type == other.type
25
- end
26
-
27
- def hash
28
- name.hash + 31 * type.hash
29
- end
30
-
40
+ # The name of the field as it is indexed in Solr. The indexed name
41
+ # contains a suffix that contains information about the type as well as
42
+ # whether the field allows multiple values for a document.
43
+ #
44
+ # ==== Returns
45
+ #
46
+ # String:: The field's indexed name
47
+ #
31
48
  def indexed_name
32
49
  "#{type.indexed_name(name)}#{'m' if multiple?}"
33
50
  end
34
51
 
52
+ # Convert a value to its representation for Solr indexing. This delegates
53
+ # to the #to_indexed method on the field's type.
54
+ #
55
+ # ==== Parameters
56
+ #
57
+ # value<Object>:: Value to convert to Solr representation
58
+ #
59
+ # ==== Returns
60
+ #
61
+ # String:: Solr representation of the object
62
+ #
63
+ # ==== Raises
64
+ #
65
+ # ArgumentError::
66
+ # the value is an array, but this field does not allow multiple values
67
+ #
35
68
  def to_indexed(value)
36
- if value.kind_of? Array
69
+ if value.is_a? Array
37
70
  if multiple?
38
71
  value.map { |val| to_indexed(val) }
39
72
  else
@@ -44,65 +77,88 @@ module Sunspot
44
77
  end
45
78
  end
46
79
 
80
+ # Cast the value into the appropriate Ruby class for the field's type
81
+ #
82
+ # ==== Parameters
83
+ #
84
+ # value<String>:: Solr's representation of the value
85
+ #
86
+ # ==== Returns
87
+ #
88
+ # Object:: The cast value
89
+ #
90
+ def cast(value)
91
+ type.cast(value)
92
+ end
93
+
94
+ # ==== Returns
95
+ #
96
+ # Boolean:: true if the field allows multiple values; false if not
47
97
  def multiple?
48
98
  !!@multiple
49
99
  end
50
100
  end
51
101
 
52
- class AttributeField < ::Sunspot::Field::Base
102
+ #
103
+ # AttributeFields call methods directly on indexed objects and index the
104
+ # return value of the method. By default, the field name is also the
105
+ # attribute that provides the value for indexing, but this can be overridden
106
+ # with the :using option.
107
+ #
108
+ class AttributeField < Base
109
+ def initialize(name, type, options = {})
110
+ @attribute_name = options.delete(:using) || name
111
+ super
112
+ end
113
+
53
114
  protected
54
115
 
116
+ #
117
+ # Call the field's attribute name on the given model and return the value.
118
+ #
119
+ # ==== Parameters
120
+ #
121
+ # model<Object>:: The object from which to extract the value
122
+ #
123
+ # ==== Returns
124
+ #
125
+ # Object:: The value to index
126
+ #
55
127
  def value_for(model)
56
- model.send(name)
128
+ model.send(@attribute_name)
57
129
  end
58
130
  end
59
131
 
60
- class VirtualField < ::Sunspot::Field::Base
132
+ #
133
+ # VirtualFields extract data by evaluating the provided block in the context
134
+ # of the model instance.
135
+ #
136
+ class VirtualField < Base
61
137
  def initialize(name, type, options = {}, &block)
62
138
  super(name, type, options)
63
139
  @block = block
64
140
  end
65
141
 
66
142
  protected
67
- attr_accessor :block
68
143
 
144
+ #
145
+ # Evaluate the block in the model's context and return the block's return
146
+ # value.
147
+ #
148
+ # ==== Parameters
149
+ #
150
+ # model<Object>:: The object from which to extract the value
151
+ #
152
+ # ==== Returns
153
+ #
154
+ # Object:: The value to index
69
155
  def value_for(model)
70
- model.instance_eval(&block)
156
+ if @block.arity <= 0
157
+ model.instance_eval(&@block)
158
+ else
159
+ @block.call(model)
160
+ end
71
161
  end
72
162
  end
73
163
  end
74
-
75
- class <<Field
76
- def register(clazz, fields)
77
- fields = [fields] unless fields.kind_of? Enumerable
78
- self.for(clazz).concat fields
79
- end
80
-
81
- def register_text(clazz, fields)
82
- fields = [fields] unless fields.kind_of? Enumerable
83
- self.text_for(clazz).concat fields
84
- end
85
-
86
- def text_for(clazz)
87
- keyword_fields_hash[clazz.object_id] ||= []
88
- end
89
-
90
- def for(clazz)
91
- fields_hash[clazz.object_id] ||= []
92
- end
93
-
94
- def unregister_all!
95
- fields_hash.clear
96
- end
97
-
98
- private
99
-
100
- def fields_hash
101
- @fields_hash ||= {}
102
- end
103
-
104
- def keyword_fields_hash
105
- @keyword_fields_hash ||= {}
106
- end
107
- end
108
164
  end
@@ -1,65 +1,68 @@
1
1
  module Sunspot
2
- class Indexer
3
- def initialize(connection)
4
- @connection = connection
2
+ #
3
+ # This class presents a service for adding, updating, and removing data
4
+ # from the Solr index. An Indexer instance is associated with a particular
5
+ # setup, and thus is capable of indexing instances of a certain class (and its
6
+ # subclasses).
7
+ #
8
+ class Indexer #:nodoc:
9
+ def initialize(connection, setup)
10
+ @connection, @setup = connection, setup
5
11
  end
6
12
 
13
+ #
14
+ # Construct a representation of the model for indexing and send it to the
15
+ # connection for indexing
16
+ #
17
+ # ==== Parameters
18
+ #
19
+ # model<Object>:: the model to index
20
+ #
7
21
  def add(model)
8
- hash = static_hash_for model
9
- for field in fields
10
- hash.merge! field.pair_for(model)
22
+ hash = static_hash_for(model)
23
+ for field in @setup.all_fields
24
+ hash.merge!(field.pair_for(model))
11
25
  end
12
- connection.add hash
13
- end
14
-
15
- def fields
16
- @fields ||= []
17
- end
18
-
19
- def add_fields(fields)
20
- self.fields.concat fields
26
+ @connection.add(hash)
21
27
  end
22
28
 
29
+ #
30
+ # Remove the given model from the Solr index
31
+ #
23
32
  def remove(model)
24
- connection.delete(::Sunspot::Adapters.adapt_instance(model).index_id)
33
+ @connection.delete(Adapters::InstanceAdapter.adapt(model).index_id)
25
34
  end
26
35
 
27
- protected
28
- attr_reader :connection
29
-
30
- def static_hash_for(model)
31
- { :id => ::Sunspot::Adapters.adapt_instance(model).index_id,
32
- :type => Indexer.superclasses_for(model.class).map { |clazz| clazz.name }}
36
+ #
37
+ # Delete all documents of the class indexed by this indexer from Solr.
38
+ #
39
+ def remove_all
40
+ @connection.delete_by_query("type:#{@setup.clazz.name}")
33
41
  end
34
- end
35
42
 
36
- class <<Indexer
37
- def add(connection, model)
38
- self.for(model.class, connection).add(model)
39
- end
40
-
41
- def remove(connection, model)
42
- self.for(model.class, connection).remove(model)
43
- end
43
+ protected
44
44
 
45
- def remove_all(connection, clazz = nil)
46
- connection.delete_by_query("type:#{clazz ? clazz.name : '[* TO *]'}")
45
+ #
46
+ # All indexed documents index and store the +id+ and +type+ fields.
47
+ # This method constructs the document hash containing those key-value
48
+ # pairs.
49
+ #
50
+ def static_hash_for(model)
51
+ { :id => Adapters::InstanceAdapter.adapt(model).index_id,
52
+ :type => Util.superclasses_for(model.class).map { |clazz| clazz.name }}
47
53
  end
48
54
 
49
- def for(clazz, connection)
50
- indexer = self.new(connection)
51
- for superclass in superclasses_for(clazz)
52
- indexer.add_fields ::Sunspot::Field.for(superclass)
53
- indexer.add_fields ::Sunspot::Field.text_for(superclass)
55
+ class <<self
56
+ #
57
+ # Delete all documents from the Solr index
58
+ #
59
+ # ==== Parameters
60
+ #
61
+ # connection<Solr::Connection>::
62
+ # connection to which to send the delete request
63
+ def remove_all(connection)
64
+ connection.delete_by_query("type:[* TO *]")
54
65
  end
55
- raise ArgumentError, "Class #{clazz.name} has not been configured for indexing" if indexer.fields.empty?
56
- indexer
57
- end
58
-
59
- def superclasses_for(clazz)
60
- superclasses_for = [clazz]
61
- superclasses_for << (clazz = clazz.superclass) while clazz.superclass != Object
62
- superclasses_for
63
66
  end
64
67
  end
65
68
  end