outoftime-sunspot 0.0.2 → 0.7.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.
@@ -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