sunspot 0.9.7

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.
Files changed (101) hide show
  1. data/History.txt +83 -0
  2. data/LICENSE +18 -0
  3. data/README.rdoc +154 -0
  4. data/Rakefile +9 -0
  5. data/TODO +9 -0
  6. data/VERSION.yml +4 -0
  7. data/bin/sunspot-configure-solr +46 -0
  8. data/bin/sunspot-solr +62 -0
  9. data/lib/light_config.rb +40 -0
  10. data/lib/sunspot.rb +469 -0
  11. data/lib/sunspot/adapters.rb +265 -0
  12. data/lib/sunspot/composite_setup.rb +186 -0
  13. data/lib/sunspot/configuration.rb +38 -0
  14. data/lib/sunspot/data_extractor.rb +47 -0
  15. data/lib/sunspot/dsl.rb +3 -0
  16. data/lib/sunspot/dsl/field_query.rb +72 -0
  17. data/lib/sunspot/dsl/fields.rb +86 -0
  18. data/lib/sunspot/dsl/query.rb +59 -0
  19. data/lib/sunspot/dsl/query_facet.rb +31 -0
  20. data/lib/sunspot/dsl/restriction.rb +25 -0
  21. data/lib/sunspot/dsl/scope.rb +193 -0
  22. data/lib/sunspot/dsl/search.rb +30 -0
  23. data/lib/sunspot/facet.rb +16 -0
  24. data/lib/sunspot/facet_data.rb +120 -0
  25. data/lib/sunspot/facet_row.rb +10 -0
  26. data/lib/sunspot/field.rb +157 -0
  27. data/lib/sunspot/field_factory.rb +126 -0
  28. data/lib/sunspot/indexer.rb +123 -0
  29. data/lib/sunspot/instantiated_facet.rb +42 -0
  30. data/lib/sunspot/instantiated_facet_row.rb +22 -0
  31. data/lib/sunspot/query.rb +191 -0
  32. data/lib/sunspot/query/base_query.rb +90 -0
  33. data/lib/sunspot/query/connective.rb +126 -0
  34. data/lib/sunspot/query/dynamic_query.rb +69 -0
  35. data/lib/sunspot/query/field_facet.rb +151 -0
  36. data/lib/sunspot/query/field_query.rb +63 -0
  37. data/lib/sunspot/query/pagination.rb +39 -0
  38. data/lib/sunspot/query/query_facet.rb +73 -0
  39. data/lib/sunspot/query/query_facet_row.rb +19 -0
  40. data/lib/sunspot/query/query_field_facet.rb +13 -0
  41. data/lib/sunspot/query/restriction.rb +233 -0
  42. data/lib/sunspot/query/scope.rb +165 -0
  43. data/lib/sunspot/query/sort.rb +36 -0
  44. data/lib/sunspot/query/sort_composite.rb +33 -0
  45. data/lib/sunspot/schema.rb +165 -0
  46. data/lib/sunspot/search.rb +219 -0
  47. data/lib/sunspot/search/hit.rb +66 -0
  48. data/lib/sunspot/session.rb +201 -0
  49. data/lib/sunspot/setup.rb +271 -0
  50. data/lib/sunspot/type.rb +200 -0
  51. data/lib/sunspot/util.rb +164 -0
  52. data/solr/etc/jetty.xml +212 -0
  53. data/solr/etc/webdefault.xml +379 -0
  54. data/solr/lib/jetty-6.1.3.jar +0 -0
  55. data/solr/lib/jetty-util-6.1.3.jar +0 -0
  56. data/solr/lib/jsp-2.1/ant-1.6.5.jar +0 -0
  57. data/solr/lib/jsp-2.1/core-3.1.1.jar +0 -0
  58. data/solr/lib/jsp-2.1/jsp-2.1.jar +0 -0
  59. data/solr/lib/jsp-2.1/jsp-api-2.1.jar +0 -0
  60. data/solr/lib/servlet-api-2.5-6.1.3.jar +0 -0
  61. data/solr/solr/conf/elevate.xml +36 -0
  62. data/solr/solr/conf/protwords.txt +21 -0
  63. data/solr/solr/conf/schema.xml +50 -0
  64. data/solr/solr/conf/solrconfig.xml +696 -0
  65. data/solr/solr/conf/stopwords.txt +57 -0
  66. data/solr/solr/conf/synonyms.txt +31 -0
  67. data/solr/start.jar +0 -0
  68. data/solr/webapps/solr.war +0 -0
  69. data/spec/api/adapters_spec.rb +33 -0
  70. data/spec/api/build_search_spec.rb +1039 -0
  71. data/spec/api/indexer_spec.rb +311 -0
  72. data/spec/api/query_spec.rb +153 -0
  73. data/spec/api/search_retrieval_spec.rb +362 -0
  74. data/spec/api/session_spec.rb +157 -0
  75. data/spec/api/spec_helper.rb +1 -0
  76. data/spec/api/sunspot_spec.rb +18 -0
  77. data/spec/integration/dynamic_fields_spec.rb +55 -0
  78. data/spec/integration/faceting_spec.rb +169 -0
  79. data/spec/integration/keyword_search_spec.rb +83 -0
  80. data/spec/integration/scoped_search_spec.rb +289 -0
  81. data/spec/integration/spec_helper.rb +1 -0
  82. data/spec/integration/stored_fields_spec.rb +10 -0
  83. data/spec/integration/test_pagination.rb +32 -0
  84. data/spec/mocks/adapters.rb +32 -0
  85. data/spec/mocks/blog.rb +3 -0
  86. data/spec/mocks/comment.rb +19 -0
  87. data/spec/mocks/connection.rb +84 -0
  88. data/spec/mocks/mock_adapter.rb +30 -0
  89. data/spec/mocks/mock_record.rb +48 -0
  90. data/spec/mocks/photo.rb +8 -0
  91. data/spec/mocks/post.rb +73 -0
  92. data/spec/mocks/user.rb +8 -0
  93. data/spec/spec_helper.rb +47 -0
  94. data/tasks/gemspec.rake +25 -0
  95. data/tasks/rcov.rake +28 -0
  96. data/tasks/rdoc.rake +22 -0
  97. data/tasks/schema.rake +19 -0
  98. data/tasks/spec.rake +24 -0
  99. data/tasks/todo.rake +4 -0
  100. data/templates/schema.xml.haml +24 -0
  101. metadata +246 -0
@@ -0,0 +1,66 @@
1
+ module Sunspot
2
+ class Search
3
+ class Hit
4
+ SPECIAL_KEYS = Set.new(%w(id type score)) #:nodoc:
5
+
6
+ #
7
+ # Primary key of object associated with this hit, as string.
8
+ #
9
+ attr_reader :primary_key
10
+ #
11
+ # Class name of object associated with this hit, as string.
12
+ #
13
+ attr_reader :class_name
14
+ #
15
+ # Keyword relevance score associated with this result. Nil if this hit
16
+ # is not from a keyword search.
17
+ #
18
+ attr_reader :score
19
+
20
+ attr_writer :instance #:nodoc:
21
+
22
+ def initialize(raw_hit, search) #:nodoc:
23
+ @class_name, @primary_key = *raw_hit['id'].match(/([^ ]+) (.+)/)[1..2]
24
+ @score = raw_hit['score']
25
+ @search = search
26
+ @stored_values = raw_hit
27
+ @stored_cache = {}
28
+ end
29
+
30
+ #
31
+ # Retrieve stored field value. For any attribute field configured with
32
+ # :stored => true, the Hit object will contain the stored value for
33
+ # that field. The value of this field will be typecast according to the
34
+ # type of the field.
35
+ #
36
+ # ==== Parameters
37
+ #
38
+ # field_name<Symbol>::
39
+ # The name of the field for which to retrieve the stored value.
40
+ #
41
+ def stored(field_name)
42
+ @stored_cache[field_name.to_sym] ||=
43
+ begin
44
+ field = Sunspot::Setup.for(@class_name).field(field_name)
45
+ field.cast(@stored_values[field.indexed_name])
46
+ end
47
+ end
48
+
49
+ #
50
+ # Retrieve the instance associated with this hit. This is lazy-loaded, but
51
+ # the first time it is called on any hit, all the hits for the search will
52
+ # load their instances using the adapter's #load_all method.
53
+ #
54
+ def instance
55
+ if @instance.nil?
56
+ @search.populate_hits!
57
+ end
58
+ @instance
59
+ end
60
+
61
+ def inspect
62
+ "#<Sunspot::Search::Hit:#{@class_name} #{@primary_key}>"
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,201 @@
1
+ module Sunspot
2
+ #
3
+ # A Sunspot session encapsulates a connection to Solr and a set of
4
+ # configuration choices. Though users of Sunspot may manually instantiate
5
+ # Session objects, in the general case it's easier to use the singleton
6
+ # stored in the Sunspot module. Since the Sunspot module provides all of
7
+ # the instance methods of Session as class methods, they are not documented
8
+ # again here.
9
+ #
10
+ class Session
11
+ class <<self
12
+ attr_writer :connection_class #:nodoc:
13
+
14
+ #
15
+ # For testing purposes
16
+ #
17
+ def connection_class #:nodoc:
18
+ @connection_class ||= RSolr::Connection
19
+ end
20
+ end
21
+
22
+ #
23
+ # Sunspot::Configuration object for this session
24
+ #
25
+ attr_reader :config
26
+
27
+ #
28
+ # Sessions are initialized with a Sunspot configuration and a Solr
29
+ # connection. Usually you will want to stick with the default arguments
30
+ # when instantiating your own sessions.
31
+ #
32
+ def initialize(config = Configuration.build, connection = nil)
33
+ @config = config
34
+ yield(@config) if block_given?
35
+ @connection = connection
36
+ @updates = 0
37
+ end
38
+
39
+ #
40
+ # See Sunspot.new_search
41
+ #
42
+ def new_search(*types)
43
+ types.flatten!
44
+ setup =
45
+ if types.length == 1
46
+ Setup.for(types.first)
47
+ else
48
+ CompositeSetup.for(types)
49
+ end
50
+ Search.new(connection, setup, Query::Query.new(types, setup, @config))
51
+ end
52
+
53
+ #
54
+ # See Sunspot.search
55
+ #
56
+ def search(*types, &block)
57
+ options = types.last.is_a?(Hash) ? types.pop : {}
58
+ search = new_search(*types)
59
+ search.build(&block) if block
60
+ search.query.options = options
61
+ search.execute!
62
+ end
63
+
64
+ #
65
+ # See Sunspot.index
66
+ #
67
+ def index(*objects)
68
+ objects.flatten!
69
+ @updates += objects.length
70
+ indexer.add(objects)
71
+ end
72
+
73
+ #
74
+ # See Sunspot.index!
75
+ #
76
+ def index!(*objects)
77
+ index(*objects)
78
+ commit
79
+ end
80
+
81
+ #
82
+ # See Sunspot.commit
83
+ #
84
+ def commit
85
+ @updates = 0
86
+ connection.commit
87
+ end
88
+
89
+ #
90
+ # See Sunspot.remove
91
+ #
92
+ def remove(*objects)
93
+ objects.flatten!
94
+ @updates += objects.length
95
+ for object in objects
96
+ indexer.remove(object)
97
+ end
98
+ end
99
+
100
+ #
101
+ # See Sunspot.remove!
102
+ #
103
+ def remove!(*objects)
104
+ remove(*objects)
105
+ commit
106
+ end
107
+
108
+ #
109
+ # See Sunspot.remove_by_id
110
+ #
111
+ def remove_by_id(clazz, id)
112
+ class_name =
113
+ if clazz.is_a?(Class)
114
+ clazz.name
115
+ else
116
+ clazz.to_s
117
+ end
118
+ indexer.remove_by_id(class_name, id)
119
+ end
120
+
121
+ #
122
+ # See Sunspot.remove_by_id!
123
+ #
124
+ def remove_by_id!(clazz, id)
125
+ remove_by_id(clazz, id)
126
+ commit
127
+ end
128
+
129
+ #
130
+ # See Sunspot.remove_all
131
+ #
132
+ def remove_all(*classes)
133
+ classes.flatten!
134
+ if classes.empty?
135
+ @updates += 1
136
+ Indexer.remove_all(connection)
137
+ else
138
+ @updates += classes.length
139
+ for clazz in classes
140
+ indexer.remove_all(clazz)
141
+ end
142
+ end
143
+ end
144
+
145
+ #
146
+ # See Sunspot.remove_all!
147
+ #
148
+ def remove_all!(*classes)
149
+ remove_all(*classes)
150
+ commit
151
+ end
152
+
153
+ #
154
+ # See Sunspot.dirty?
155
+ #
156
+ def dirty?
157
+ @updates > 0
158
+ end
159
+
160
+ #
161
+ # See Sunspot.commit_if_dirty
162
+ #
163
+ def commit_if_dirty
164
+ commit if dirty?
165
+ end
166
+
167
+ #
168
+ # See Sunspot.batch
169
+ #
170
+ def batch
171
+ indexer.start_batch
172
+ yield
173
+ indexer.flush_batch
174
+ end
175
+
176
+ private
177
+
178
+ #
179
+ # Retrieve the Solr connection for this session, creating one if it does not
180
+ # already exist.
181
+ #
182
+ # ==== Returns
183
+ #
184
+ # Solr::Connection:: The connection for this session
185
+ #
186
+ def connection
187
+ @connection ||=
188
+ begin
189
+ connection = self.class.connection_class.new(
190
+ RSolr::Adapter::HTTP.new(:url => config.solr.url)
191
+ )
192
+ connection.adapter.connector.adapter_name = config.http_client
193
+ connection
194
+ end
195
+ end
196
+
197
+ def indexer
198
+ @indexer ||= Indexer.new(connection)
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,271 @@
1
+ module Sunspot
2
+ #
3
+ # This class encapsulates the search/indexing setup for a given class. Its
4
+ # contents are built using the Sunspot.setup method.
5
+ #
6
+ class Setup #:nodoc:
7
+ def initialize(clazz)
8
+ @clazz = clazz
9
+ @class_name = clazz.name
10
+ @field_factories, @text_field_factories, @dynamic_field_factories,
11
+ @field_factories_cache, @text_field_factories_cache,
12
+ @dynamic_field_factories_cache = *Array.new(6) { Hash.new }
13
+ @dsl = DSL::Fields.new(self)
14
+ add_field_factory(:class, Type::ClassType)
15
+ end
16
+
17
+ def type_names
18
+ [@class_name]
19
+ end
20
+
21
+ #
22
+ # Add field_factories for scope/ordering
23
+ #
24
+ # ==== Parameters
25
+ #
26
+ # field_factories<Array>:: Array of Sunspot::Field objects
27
+ #
28
+ def add_field_factory(name, type, options = {}, &block)
29
+ field_factory = FieldFactory::Static.new(name, type, options, &block)
30
+ @field_factories[field_factory.signature] = field_factory
31
+ @field_factories_cache[field_factory.name] = field_factory
32
+ end
33
+
34
+ #
35
+ # Add field_factories for fulltext search
36
+ #
37
+ # ==== Parameters
38
+ #
39
+ # field_factories<Array>:: Array of Sunspot::Field objects
40
+ #
41
+ def add_text_field_factory(name, options = {}, &block)
42
+ field_factory = FieldFactory::Static.new(name, Type::TextType, options, &block)
43
+ @text_field_factories[name] = field_factory
44
+ @text_field_factories_cache[field_factory.name] = field_factory
45
+ end
46
+
47
+ #
48
+ # Add dynamic field_factories
49
+ #
50
+ # ==== Parameters
51
+ #
52
+ # field_factories<Array>:: Array of dynamic field objects
53
+ #
54
+ def add_dynamic_field_factory(name, type, options = {}, &block)
55
+ field_factory = FieldFactory::Dynamic.new(name, type, options, &block)
56
+ @dynamic_field_factories[field_factory.signature] = field_factory
57
+ @dynamic_field_factories_cache[field_factory.name] = field_factory
58
+ end
59
+
60
+ def add_document_boost(attr_name, &block)
61
+ @document_boost_extractor =
62
+ if attr_name
63
+ if attr_name.respond_to?(:to_f)
64
+ DataExtractor::Constant.new(attr_name)
65
+ else
66
+ DataExtractor::AttributeExtractor.new(attr_name)
67
+ end
68
+ else
69
+ DataExtractor::BlockExtractor.new(&block)
70
+ end
71
+ end
72
+
73
+ #
74
+ # Builder method for evaluating the setup DSL
75
+ #
76
+ def setup(&block)
77
+ @dsl.instance_eval(&block)
78
+ end
79
+
80
+ def field(field_name)
81
+ if field_factory = @field_factories_cache[field_name.to_sym]
82
+ field_factory.build
83
+ else
84
+ raise(
85
+ UnrecognizedFieldError,
86
+ "No field configured for #{@clazz.name} with name '#{field_name}'"
87
+ )
88
+ end
89
+ end
90
+
91
+ def text_field(field_name)
92
+ if field_factory = @text_field_factories_cache[field_name.to_sym]
93
+ field_factory.build
94
+ else
95
+ raise(
96
+ UnrecognizedFieldError,
97
+ "No text field configured for #{@clazz.name} with name '#{field_name}'"
98
+ )
99
+ end
100
+ end
101
+
102
+ def dynamic_field_factory(field_name)
103
+ @dynamic_field_factories_cache[field_name.to_sym] || raise(
104
+ UnrecognizedFieldError,
105
+ "No dynamic field configured for #{@clazz.name} with name '#{field_name}'"
106
+ )
107
+ end
108
+
109
+ def fields
110
+ field_factories.map { |field_factory| field_factory.build }
111
+ end
112
+
113
+ def text_fields
114
+ text_field_factories.map { |text_field_factory| text_field_factory.build }
115
+ end
116
+
117
+ #
118
+ # Get the field_factories associated with this setup as well as all inherited field_factories
119
+ #
120
+ # ==== Returns
121
+ #
122
+ # Array:: Collection of all field_factories associated with this setup
123
+ #
124
+ def field_factories
125
+ collection_from_inheritable_hash(:field_factories)
126
+ end
127
+
128
+ #
129
+ # Get the text field_factories associated with this setup as well as all inherited
130
+ # text field_factories
131
+ #
132
+ # ==== Returns
133
+ #
134
+ # Array:: Collection of all text field_factories associated with this setup
135
+ #
136
+ def text_field_factories
137
+ collection_from_inheritable_hash(:text_field_factories)
138
+ end
139
+
140
+ #
141
+ # Get all static, dynamic, and text field_factories associated with this setup as
142
+ # well as all inherited field_factories
143
+ #
144
+ # ==== Returns
145
+ #
146
+ # Array:: Collection of all text and scope field_factories associated with this setup
147
+ #
148
+ def all_field_factories
149
+ all_field_factories = []
150
+ all_field_factories.concat(field_factories).concat(text_field_factories).concat(dynamic_field_factories)
151
+ all_field_factories
152
+ end
153
+
154
+ #
155
+ # Get all dynamic field_factories for this and parent setups
156
+ #
157
+ # ==== Returns
158
+ #
159
+ # Array:: Dynamic field_factories
160
+ #
161
+ def dynamic_field_factories
162
+ collection_from_inheritable_hash(:dynamic_field_factories)
163
+ end
164
+
165
+ #
166
+ # Return the class associated with this setup.
167
+ #
168
+ # ==== Returns
169
+ #
170
+ # clazz<Class>:: Class setup is configured for
171
+ #
172
+ def clazz
173
+ Util.full_const_get(@class_name)
174
+ end
175
+
176
+ def document_boost_for(model)
177
+ if @document_boost_extractor
178
+ @document_boost_extractor.value_for(model)
179
+ end
180
+ end
181
+
182
+ protected
183
+
184
+ #
185
+ # Get the nearest inherited setup, if any
186
+ #
187
+ # ==== Returns
188
+ #
189
+ # Sunspot::Setup:: Setup for the nearest ancestor of this setup's class
190
+ #
191
+ def parent
192
+ Setup.for(clazz.superclass)
193
+ end
194
+
195
+ def get_inheritable_hash(name)
196
+ hash = instance_variable_get(:"@#{name}")
197
+ parent.get_inheritable_hash(name).each_pair do |key, value|
198
+ hash[key] = value unless hash.has_key?(key)
199
+ end if parent
200
+ hash
201
+ end
202
+
203
+ private
204
+
205
+ def collection_from_inheritable_hash(name)
206
+ get_inheritable_hash(name).values
207
+ end
208
+
209
+ class <<self
210
+ #
211
+ # Retrieve or create the Setup instance for the given class, evaluating
212
+ # the given block to add to the setup's configuration
213
+ #
214
+ def setup(clazz, &block) #:nodoc:
215
+ self.for!(clazz).setup(&block)
216
+ end
217
+
218
+ #
219
+ # Retrieve the setup instance for the given class, or for the nearest
220
+ # ancestor that has a setup, if any.
221
+ #
222
+ # ==== Parameters
223
+ #
224
+ # clazz<Class>:: Class for which to retrieve a setup
225
+ #
226
+ # ==== Returns
227
+ #
228
+ # Sunspot::Setup::
229
+ # Setup instance associated with the given class or its nearest ancestor
230
+ #
231
+ def for(clazz) #:nodoc:
232
+ class_name =
233
+ if clazz.respond_to?(:name)
234
+ clazz.name
235
+ else
236
+ clazz
237
+ end
238
+ setups[class_name.to_sym] || self.for(clazz.superclass) if clazz
239
+ end
240
+
241
+ protected
242
+
243
+ #
244
+ # Retrieve or create a Setup instance for this class
245
+ #
246
+ # ==== Parameters
247
+ #
248
+ # clazz<Class>:: Class for which to retrieve a setup
249
+ #
250
+ # ==== Returns
251
+ #
252
+ # Sunspot::Setup:: New or existing setup for this class
253
+ #
254
+ def for!(clazz) #:nodoc:
255
+ setups[clazz.name.to_sym] ||= new(clazz)
256
+ end
257
+
258
+ private
259
+
260
+ # Singleton hash of class names to Setup instances
261
+ #
262
+ # ==== Returns
263
+ #
264
+ # Hash:: Class names keyed to Setup instances
265
+ #
266
+ def setups
267
+ @setups ||= {}
268
+ end
269
+ end
270
+ end
271
+ end