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 +4 -0
- data/README.rdoc +26 -33
- data/TODO +7 -0
- data/VERSION.yml +2 -2
- data/lib/light_config.rb +5 -5
- data/lib/sunspot/adapters.rb +209 -33
- data/lib/sunspot/configuration.rb +25 -10
- data/lib/sunspot/dsl/fields.rb +42 -11
- data/lib/sunspot/dsl/query.rb +150 -6
- data/lib/sunspot/dsl/scope.rb +16 -26
- data/lib/sunspot/facet.rb +37 -0
- data/lib/sunspot/facet_row.rb +34 -0
- data/lib/sunspot/facets.rb +21 -0
- data/lib/sunspot/field.rb +112 -56
- data/lib/sunspot/indexer.rb +49 -46
- data/lib/sunspot/query.rb +217 -49
- data/lib/sunspot/restriction.rb +158 -14
- data/lib/sunspot/search.rb +79 -28
- data/lib/sunspot/session.rb +75 -8
- data/lib/sunspot/setup.rb +171 -0
- data/lib/sunspot/type.rb +116 -32
- data/lib/sunspot/util.rb +141 -7
- data/lib/sunspot.rb +260 -31
- data/spec/api/build_search_spec.rb +139 -33
- data/spec/api/indexer_spec.rb +33 -2
- data/spec/api/search_retrieval_spec.rb +85 -2
- data/spec/api/session_spec.rb +14 -6
- data/spec/integration/faceting_spec.rb +39 -0
- data/spec/integration/keyword_search_spec.rb +1 -1
- data/spec/integration/scoped_search_spec.rb +175 -0
- data/spec/mocks/mock_adapter.rb +7 -10
- data/spec/mocks/post.rb +7 -2
- data/tasks/rdoc.rake +7 -0
- data/tasks/spec.rake +3 -0
- data/tasks/todo.rake +4 -0
- metadata +12 -7
- data/lib/sunspot/builder.rb +0 -78
- data/spec/api/standard_search_builder_spec.rb +0 -76
- data/spec/custom_expectation.rb +0 -53
- data/spec/integration/field_types_spec.rb +0 -62
data/History.txt
CHANGED
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
|
-
*
|
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
|
77
|
-
with
|
78
|
-
with
|
79
|
-
with
|
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
|
-
|
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
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
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
data/VERSION.yml
CHANGED
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 <<
|
37
|
-
|
38
|
-
|
35
|
+
class <<self
|
36
|
+
def build(&block)
|
37
|
+
LightConfig::Configuration.new(&block)
|
38
|
+
end
|
39
39
|
end
|
40
40
|
end
|
data/lib/sunspot/adapters.rb
CHANGED
@@ -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
|
-
|
4
|
-
|
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
|
-
|
9
|
-
|
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
|
-
|
13
|
-
|
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
|
-
|
17
|
-
|
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
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
216
|
+
protected
|
49
217
|
|
50
|
-
|
51
|
-
|
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
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
data/lib/sunspot/dsl/fields.rb
CHANGED
@@ -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(
|
5
|
-
@
|
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
|
-
|
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 =
|
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
|
-
|
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
|
-
|
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
|
-
|
63
|
+
Field::AttributeField.new(name, type, options || {})
|
33
64
|
else
|
34
|
-
|
65
|
+
Field::VirtualField.new(name, type, options || {}, &block)
|
35
66
|
end
|
36
67
|
end
|
37
68
|
end
|