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