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