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.
data/History.txt CHANGED
@@ -1,3 +1,7 @@
1
+ == 0.0.5 TBD
2
+ * Negative scoping using without() method
3
+ * Exclusion by object identity using without(instance)
4
+
1
5
  == 0.0.2 2009-02-14
2
6
  * Run sunspot's built-in Solr instance using
3
7
  sunspot-solr executable
data/README.rdoc CHANGED
@@ -19,18 +19,16 @@ Sunspot is currently under active development and is not feature-complete.
19
19
  * Define fields based on existing attributes or "virtual fields" for custom indexing
20
20
  * Indexes each object's entire superclass hierarchy, for easy searching for all objects inheriting from a parent class
21
21
  * Intuitive DSL for scoping searches, with all the usual boolean operators available
22
- * Ability to pass dynamic conditions in as a hash, and define which operator to use for each condition
22
+ * Intuitive interface for requesting facets on indexed fields
23
+ * Extensible adapter architecture for easy integration of other ORMs or non-model classes
23
24
  * Full compatibility with will_paginate
24
25
  * Ordering
25
26
 
26
27
  === Sunspot will have these features:
27
28
 
28
29
  * Adapters for DataMapper, ActiveRecord, and File objects
29
- * Extensible adapter architecture for easy integration of other ORMs or non-model classes
30
30
  * High-power integration with ORM features for maximum efficiency (e.g., specify AR :include argument for eager loading associations when hydrating search results)
31
- * Intuitive interface for requesting facets on indexed fields
32
31
  * Define association facets, which return model objects as row values
33
- * Access search parameters as hash or object attributes, for easy integration with form helpers or query string builders
34
32
  * Plugins for drop-in integration with Merb and Rails
35
33
 
36
34
  == Installation
@@ -62,58 +60,49 @@ You can change the URL at which Sunspot accesses Solr with:
62
60
  string :author_name
63
61
  integer :blog_id
64
62
  integer :category_ids
65
- float :average_rating
63
+ float :average_rating, :using => :ratings_average
66
64
  time :published_at
67
65
  string :sort_title do
68
66
  title.downcase.sub(/^(an?|the)\W+/, ''/) if title = self.title
69
67
  end
70
68
  end
71
69
 
70
+ See Sunspot.setup for more information.
71
+
72
72
  === Search for objects:
73
73
 
74
74
  search = Sunspot.search Post do
75
75
  keywords 'great pizza'
76
- with.author_name 'Mark Twain'
77
- with.blog_id.any_of [2, 14]
78
- with.category_ids.all_of [4, 10]
79
- with.published_at.less_than Time.now
76
+ with :author_name, 'Mark Twain'
77
+ with(:blog_id).any_of [2, 14]
78
+ with(:category_ids).all_of [4, 10]
79
+ with(:published_at).less_than Time.now
80
+ without :title, 'Bad Title'
81
+ without bad_instance # specifically exclude this instance from results
80
82
 
81
83
  paginate :page => 3, :per_page => 15
82
84
  order_by :average_rating, :desc
85
+
86
+ facet :blog_id
83
87
  end
84
88
 
89
+ See Sunspot.search for more information.
90
+
85
91
  === Get data from search:
86
92
 
87
93
  search.results
88
94
  search.total
89
95
  search.page
90
96
  search.per_page
97
+ search.facet(:blog_id)
91
98
 
92
- === Build search from a hash (e.g., params) and retrieve them later:
93
-
94
- search = Sunspot.search Post, :keywords => 'great pizza',
95
- :conditions => { :author_name => 'Mark Twain',
96
- :blog_id => [4, 10] },
97
- :order => 'published_at desc'
98
- search.builder.keywords
99
- #=> "great pizza"
100
- search.builder.conditions.author_name
101
- #=> "Mark Twain"
102
- search.builder.params[:conditions][:blog_id]
103
- #=> [4, 10]
104
-
105
- This functionality is great for building a search from user input; if you're
106
- building a search using business logic in your application, the block DSL
107
- is the way to go. Note you can mix-and-match the two:
108
-
109
- search = Sunspot.search Post, :keywords => 'great pizza' do
110
- with.blog_id 1
111
- end
99
+ == About the API documentation
112
100
 
113
- search.builder.keywords
114
- #=> "great pizza"
115
- search.builder.blog_id
116
- #=> nil
101
+ All of the methods documented in the RDoc are considered part of Sunspot's
102
+ public API. Methods that are not part of the public API are documented in the
103
+ code, but excluded from the RDoc. If you find yourself needing to access methods
104
+ that are not part of the public API in order to do what you need, please contact
105
+ me so I can rectify the situation!
117
106
 
118
107
  == Requirements
119
108
 
@@ -121,6 +110,10 @@ is the way to go. Note you can mix-and-match the two:
121
110
  2. solr-ruby
122
111
  3. Java
123
112
 
113
+ == Further Reading
114
+
115
+ Posts about Sunspot from my tumblog: http://outofti.me/tagged/sunspot
116
+
124
117
  == LICENSE:
125
118
 
126
119
  (The MIT License)
data/TODO ADDED
@@ -0,0 +1,7 @@
1
+ === 0.8 ===
2
+ * Facet by type
3
+ * Dynamic fields
4
+ * ActiveRecord, DataMapper, File adapters
5
+ === 0.9 ===
6
+ * Switch to RSolr
7
+ * Query-based faceting (?)
data/VERSION.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
+ :patch: 0
2
3
  :major: 0
3
- :minor: 0
4
- :patch: 2
4
+ :minor: 7
data/lib/light_config.rb CHANGED
@@ -9,7 +9,7 @@ module LightConfig
9
9
  define_method property do
10
10
  @properties[property]
11
11
  end
12
-
12
+
13
13
  define_method "#{property}=" do |value|
14
14
  @properties[property] = value
15
15
  end
@@ -31,10 +31,10 @@ module LightConfig
31
31
  @configuration.instance_variable_get(:@properties)[method] = value
32
32
  end
33
33
  end
34
- end
35
34
 
36
- class <<LightConfig
37
- def build(&block)
38
- LightConfig::Configuration.new(&block)
35
+ class <<self
36
+ def build(&block)
37
+ LightConfig::Configuration.new(&block)
38
+ end
39
39
  end
40
40
  end
@@ -1,54 +1,230 @@
1
1
  module Sunspot
2
+ # Sunspot provides an adapter architecture that allows applications or plugin
3
+ # developers to define adapters for any type of object. An adapter is composed
4
+ # of two classes, an InstanceAdapter and a DataAccessor. Note that an adapter
5
+ # does not need to provide both classes - InstanceAdapter is only needed if
6
+ # you wish to index instances of the class, and DataAccessor is only needed if
7
+ # you wish to retrieve instances of this class in search results. Of course,
8
+ # both will be the case most of the time.
9
+ #
10
+ # See Sunspot::Adapters::DataAccessor.register and
11
+ # Sunspot::Adapters::InstanceAdapter.register for information on how to enable
12
+ # an adapter for use by Sunspot.
13
+ #
14
+ # See spec/mocks/mock_adapter.rb for an example of how adapter classes should
15
+ # be implemented.
16
+ #
2
17
  module Adapters
3
- module InstanceAdapter
4
- def initialize(instance)
18
+ # Subclasses of the InstanceAdapter class should implement the #id method,
19
+ # which returns the primary key of the instance stored in the @instance
20
+ # variable. The primary key must be unique within the scope of the
21
+ # instance's class.
22
+ #
23
+ # ==== Example:
24
+ #
25
+ # class FileAdapter < Sunspot::Adapters::InstanceAdapter
26
+ # def id
27
+ # File.expand_path(@instance.path)
28
+ # end
29
+ # end
30
+ #
31
+ # # then in your initializer
32
+ # Sunspot::Adapters::InstanceAdapter.register(MyAdapter, File)
33
+ #
34
+ class InstanceAdapter
35
+ def initialize(instance) #:nodoc:
5
36
  @instance = instance
6
37
  end
7
38
 
8
- def index_id
9
- "#{instance.class.name} #{id}"
39
+ #
40
+ # The universally-unique ID for this instance that will be stored in solr
41
+ #
42
+ # ==== Returns
43
+ #
44
+ # String:: ID for use in Solr
45
+ #
46
+ def index_id #:nodoc:
47
+ "#{@instance.class.name} #{id}"
10
48
  end
11
49
 
12
- protected
13
- attr_accessor :instance
50
+ class <<self
51
+ # Instantiate an InstanceAdapter for the given object, searching for
52
+ # registered adapters for the object's class.
53
+ #
54
+ # ==== Parameters
55
+ #
56
+ # instance<Object>:: The instance to adapt
57
+ #
58
+ # ==== Returns
59
+ #
60
+ # InstanceAdapter::
61
+ # An instance of an InstanceAdapter implementation that
62
+ # wraps the given instance
63
+ #
64
+ def adapt(instance) #:nodoc:
65
+ self.for(instance.class).new(instance)
66
+ end
67
+
68
+ # Register an instance adapter for a set of classes. When searching for
69
+ # an adapter for a given instance, Sunspot starts with the instance's
70
+ # class, and then searches for registered adapters up the class's
71
+ # ancestor chain.
72
+ #
73
+ # ==== Parameters
74
+ #
75
+ # instance_adapter<Class>:: The instance adapter class to register
76
+ # classes...<Class>::
77
+ # One or more classes that this instance adapter adapts
78
+ #
79
+ def register(instance_adapter, *classes)
80
+ for clazz in classes
81
+ instance_adapters[clazz.name.to_sym] = instance_adapter
82
+ end
83
+ end
84
+
85
+ # Find the best InstanceAdapter implementation that adapts the given
86
+ # class. Starting with the class and then moving up the ancestor chain,
87
+ # looks for registered InstanceAdapter implementations.
88
+ #
89
+ # ==== Parameters
90
+ #
91
+ # clazz<Class>:: The class to find an InstanceAdapter for
92
+ #
93
+ # ==== Returns
94
+ #
95
+ # Class:: Subclass of InstanceAdapter, or nil if none found
96
+ #
97
+ def for(clazz) #:nodoc:
98
+ while clazz != Object
99
+ class_name = clazz.name.to_sym
100
+ return instance_adapters[class_name] if instance_adapters[class_name]
101
+ clazz = clazz.superclass
102
+ end
103
+ nil
104
+ end
105
+
106
+ protected
107
+
108
+ # Lazy-initialize the hash of registered instance adapters
109
+ #
110
+ # ==== Returns
111
+ #
112
+ # Hash:: Hash containing class names keyed to instance adapter classes
113
+ #
114
+ def instance_adapters #:nodoc:
115
+ @instance_adapters ||= {}
116
+ end
117
+ end
14
118
  end
15
119
 
16
- module ClassAdapter
17
- def initialize(clazz)
120
+ # Subclasses of the DataAccessor class take care of retreiving instances of
121
+ # the adapted class from (usually persistent) storage. Subclasses must
122
+ # implement the #load method, which takes an id (the value returned by
123
+ # InstanceAdapter#id, as a string), and returns the instance referenced by
124
+ # that ID. Optionally, it can also override the #load_all method, which
125
+ # takes an array of IDs and returns an array of instances in the order
126
+ # given. #load_all need only be implemented if it can be done more
127
+ # efficiently than simply iterating over the IDs and calling #load on each
128
+ # individually.
129
+ #
130
+ # ==== Example
131
+ #
132
+ # class FileAccessor < Sunspot::Adapters::InstanceAdapter
133
+ # def load(id)
134
+ # @clazz.open(id)
135
+ # end
136
+ # end
137
+ #
138
+ # Sunspot::Adapters::DataAccessor.register(FileAccessor, File)
139
+ #
140
+ class DataAccessor
141
+ def initialize(clazz) #:nodoc:
18
142
  @clazz = clazz
19
143
  end
20
144
 
21
- protected
22
- attr_reader :clazz
23
- end
24
- end
25
-
26
- class <<Adapters
27
- def register(adapter, *classes)
28
- for clazz in classes
29
- adapters[clazz.name] = adapter
145
+ # Subclasses can override this class to provide more efficient bulk
146
+ # loading of instances. Instances must be returned in the same order
147
+ # that the IDs were given.
148
+ #
149
+ # ==== Parameters
150
+ #
151
+ # ids<Array>:: collection of IDs
152
+ #
153
+ # ==== Returns
154
+ #
155
+ # Array:: collection of instances, in order of IDs given
156
+ #
157
+ def load_all(ids)
158
+ ids.map { |id| self.load(id) }
30
159
  end
31
- end
32
160
 
33
- def adapt_class(clazz)
34
- self.for(clazz).const_get('ClassAdapter').new(clazz)
35
- end
161
+ class <<self
162
+ # Create a DataAccessor for the given class, searching registered
163
+ # adapters for the best match. See InstanceAdapter#adapt for discussion
164
+ # of inheritence.
165
+ #
166
+ # ==== Parameters
167
+ #
168
+ # clazz<Class>:: Class to create DataAccessor for
169
+ #
170
+ # ==== Returns
171
+ #
172
+ # DataAccessor::
173
+ # DataAccessor implementation which provides access to given class
174
+ #
175
+ def create(clazz)
176
+ self.for(clazz).new(clazz)
177
+ end
36
178
 
37
- def adapt_instance(instance)
38
- self.for(instance.class).const_get('InstanceAdapter').new(instance)
39
- end
179
+ # Register data accessor for a set of classes. When searching for
180
+ # an accessor for a given class, Sunspot starts with the class,
181
+ # and then searches for registered adapters up the class's ancestor
182
+ # chain.
183
+ #
184
+ # ==== Parameters
185
+ #
186
+ # data_accessor<Class>:: The data accessor class to register
187
+ # classes...<Class>::
188
+ # One or more classes that this data accessor providess access to
189
+ #
190
+ def register(data_accessor, *classes)
191
+ for clazz in classes
192
+ data_accessors[clazz.name.to_sym] = data_accessor
193
+ end
194
+ end
40
195
 
41
- def for(clazz)
42
- while clazz != Object
43
- return adapters[clazz.name] if adapters[clazz.name]
44
- clazz = clazz.superclass
45
- end
46
- end
196
+ # Find the best DataAccessor implementation that adapts the given class.
197
+ # Starting with the class and then moving up the ancestor chain, looks
198
+ # for registered DataAccessor implementations.
199
+ #
200
+ # ==== Parameters
201
+ #
202
+ # clazz<Class>:: The class to find a DataAccessor for
203
+ #
204
+ # ==== Returns
205
+ #
206
+ # Class:: Implementation of DataAccessor, or nil if none found
207
+ #
208
+ def for(clazz) #:nodoc:
209
+ while clazz != Object
210
+ class_name = clazz.name.to_sym
211
+ return data_accessors[class_name] if data_accessors[class_name]
212
+ clazz = clazz.superclass
213
+ end
214
+ end
47
215
 
48
- private
216
+ protected
49
217
 
50
- def adapters
51
- @adapters ||= {}
218
+ # Lazy-initialize the hash of registered data accessors
219
+ #
220
+ # ==== Returns
221
+ #
222
+ # Hash:: Hash containing class names keyed to data accessor classes
223
+ #
224
+ def data_accessors #:nodoc:
225
+ @adapters ||= {}
226
+ end
227
+ end
52
228
  end
53
229
  end
54
230
  end
@@ -1,15 +1,30 @@
1
1
  module Sunspot
2
+ # The Sunspot::Configuration module provides a factory method for Sunspot
3
+ # configuration objects. Available properties are:
4
+ #
5
+ # Sunspot.config.solr.url::
6
+ # The URL at which to connect to Solr
7
+ # (default: 'http://localhost:8983/solr')
8
+ # Sunspot.config.pagination.default_per_page::
9
+ # Solr always paginates its results. This sets Sunspot's default result
10
+ # count per page if it is not explicitly specified in the query.
11
+ #
2
12
  module Configuration
3
- end
4
-
5
- class <<Configuration
6
- def build
7
- LightConfig.build do
8
- solr do
9
- url 'http://localhost:8983/solr'
10
- end
11
- pagination do
12
- default_per_page 30
13
+ class <<self
14
+ # Factory method to build configuration instances.
15
+ #
16
+ # ==== Returns
17
+ #
18
+ # LightConfig::Configuration:: new configuration instance with defaults
19
+ #
20
+ def build #:nodoc:
21
+ LightConfig.build do
22
+ solr do
23
+ url 'http://localhost:8983/solr'
24
+ end
25
+ pagination do
26
+ default_per_page 30
27
+ end
13
28
  end
14
29
  end
15
30
  end
@@ -1,37 +1,68 @@
1
1
  module Sunspot
2
2
  module DSL
3
+ # The Fields class provides a DSL for specifying field definitions in the
4
+ # Sunspot.setup block. As well as the #text method, which creates fulltext
5
+ # fields, uses #method_missing to allow definition of typed fields. The
6
+ # available methods are determined by the constants defined in
7
+ # Sunspot::Type - in theory (though this is untested), plugin developers
8
+ # should be able to add support for new types simply by creating new
9
+ # implementations in Sunspot::Type
10
+ #
3
11
  class Fields
4
- def initialize(clazz)
5
- @clazz = clazz
12
+ def initialize(setup) #:nodoc:
13
+ @setup = setup
6
14
  end
7
15
 
16
+ # Add a text field. Text fields are tokenized before indexing and are
17
+ # the only fields searched in fulltext searches. If a block is passed,
18
+ # create a virtual field; otherwise create an attribute field.
19
+ #
20
+ # ==== Parameters
21
+ #
22
+ # names...<Symbol>:: One or more field names
23
+ #
8
24
  def text(*names, &block)
9
25
  for name in names
10
- ::Sunspot::Field.register_text clazz, build_field(name, ::Sunspot::Type::TextType, &block)
26
+ @setup.add_text_fields(build_field(name, Type::TextType, &block))
11
27
  end
12
28
  end
13
29
 
30
+ # method_missing is used to provide access to typed fields, because
31
+ # developers should be able to add new Sunspot::Type implementations
32
+ # dynamically and have them recognized inside the Fields DSL. Like #text,
33
+ # these methods will create a VirtualField if a block is passed, or an
34
+ # AttributeField if not.
35
+ #
36
+ # ==== Example
37
+ #
38
+ # Sunspot.setup(File) do
39
+ # time :mtime
40
+ # end
41
+ #
42
+ # The call to +time+ will create a field of type Sunspot::Types::TimeType
43
+ #
14
44
  def method_missing(method, *args, &block)
15
45
  begin
16
- type = ::Sunspot::Type.const_get "#{method.to_s.camel_case}Type"
46
+ type = Type.const_get("#{Util.camel_case(method.to_s)}Type")
17
47
  rescue(NameError)
18
48
  super(method.to_sym, *args, &block) and return
19
49
  end
20
50
  name = args.shift
21
- ::Sunspot::Field.register clazz, build_field(name, type, *args, &block)
51
+ @setup.add_fields(build_field(name, type, *args, &block))
22
52
  end
23
53
 
24
- protected
25
- attr_reader :clazz
26
-
27
54
  private
28
55
 
29
- def build_field(name, type, *args, &block)
56
+ # Factory method for field instances, used by the public methods in this
57
+ # class. Create a VirtualField if a block is passed, or an AttributeField
58
+ # if not.
59
+ #
60
+ def build_field(name, type, *args, &block) #:nodoc:
30
61
  options = args.shift if args.first.is_a?(Hash)
31
62
  unless block
32
- ::Sunspot::Field::AttributeField.new(name, type, options || {})
63
+ Field::AttributeField.new(name, type, options || {})
33
64
  else
34
- ::Sunspot::Field::VirtualField.new(name, type, options || {}, &block)
65
+ Field::VirtualField.new(name, type, options || {}, &block)
35
66
  end
36
67
  end
37
68
  end