UnderpantsGnome-sunspot 0.9.1.1

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 (103) hide show
  1. data/History.txt +39 -0
  2. data/LICENSE +18 -0
  3. data/README.rdoc +154 -0
  4. data/Rakefile +9 -0
  5. data/TODO +4 -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 +470 -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/date_facet.rb +36 -0
  16. data/lib/sunspot/date_facet_row.rb +17 -0
  17. data/lib/sunspot/dsl.rb +3 -0
  18. data/lib/sunspot/dsl/field_query.rb +72 -0
  19. data/lib/sunspot/dsl/fields.rb +86 -0
  20. data/lib/sunspot/dsl/query.rb +59 -0
  21. data/lib/sunspot/dsl/query_facet.rb +31 -0
  22. data/lib/sunspot/dsl/restriction.rb +25 -0
  23. data/lib/sunspot/dsl/scope.rb +193 -0
  24. data/lib/sunspot/dsl/search.rb +30 -0
  25. data/lib/sunspot/facet.rb +51 -0
  26. data/lib/sunspot/facet_row.rb +34 -0
  27. data/lib/sunspot/field.rb +157 -0
  28. data/lib/sunspot/field_factory.rb +126 -0
  29. data/lib/sunspot/indexer.rb +127 -0
  30. data/lib/sunspot/instantiated_facet.rb +38 -0
  31. data/lib/sunspot/instantiated_facet_row.rb +12 -0
  32. data/lib/sunspot/query.rb +190 -0
  33. data/lib/sunspot/query/base_query.rb +90 -0
  34. data/lib/sunspot/query/connective.rb +77 -0
  35. data/lib/sunspot/query/dynamic_query.rb +69 -0
  36. data/lib/sunspot/query/field_facet.rb +149 -0
  37. data/lib/sunspot/query/field_query.rb +57 -0
  38. data/lib/sunspot/query/pagination.rb +39 -0
  39. data/lib/sunspot/query/query_facet.rb +72 -0
  40. data/lib/sunspot/query/query_facet_row.rb +19 -0
  41. data/lib/sunspot/query/restriction.rb +225 -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/query_facet.rb +33 -0
  46. data/lib/sunspot/query_facet_row.rb +21 -0
  47. data/lib/sunspot/schema.rb +165 -0
  48. data/lib/sunspot/search.rb +222 -0
  49. data/lib/sunspot/search/hit.rb +62 -0
  50. data/lib/sunspot/session.rb +201 -0
  51. data/lib/sunspot/setup.rb +271 -0
  52. data/lib/sunspot/type.rb +200 -0
  53. data/lib/sunspot/util.rb +164 -0
  54. data/solr/etc/jetty.xml +212 -0
  55. data/solr/etc/webdefault.xml +379 -0
  56. data/solr/lib/jetty-6.1.3.jar +0 -0
  57. data/solr/lib/jetty-util-6.1.3.jar +0 -0
  58. data/solr/lib/jsp-2.1/ant-1.6.5.jar +0 -0
  59. data/solr/lib/jsp-2.1/core-3.1.1.jar +0 -0
  60. data/solr/lib/jsp-2.1/jsp-2.1.jar +0 -0
  61. data/solr/lib/jsp-2.1/jsp-api-2.1.jar +0 -0
  62. data/solr/lib/servlet-api-2.5-6.1.3.jar +0 -0
  63. data/solr/solr/conf/elevate.xml +36 -0
  64. data/solr/solr/conf/protwords.txt +21 -0
  65. data/solr/solr/conf/schema.xml +50 -0
  66. data/solr/solr/conf/solrconfig.xml +696 -0
  67. data/solr/solr/conf/stopwords.txt +57 -0
  68. data/solr/solr/conf/synonyms.txt +31 -0
  69. data/solr/start.jar +0 -0
  70. data/solr/webapps/solr.war +0 -0
  71. data/spec/api/adapters_spec.rb +33 -0
  72. data/spec/api/build_search_spec.rb +918 -0
  73. data/spec/api/indexer_spec.rb +311 -0
  74. data/spec/api/query_spec.rb +153 -0
  75. data/spec/api/search_retrieval_spec.rb +325 -0
  76. data/spec/api/session_spec.rb +157 -0
  77. data/spec/api/spec_helper.rb +1 -0
  78. data/spec/api/sunspot_spec.rb +18 -0
  79. data/spec/integration/dynamic_fields_spec.rb +55 -0
  80. data/spec/integration/faceting_spec.rb +169 -0
  81. data/spec/integration/keyword_search_spec.rb +83 -0
  82. data/spec/integration/scoped_search_spec.rb +188 -0
  83. data/spec/integration/spec_helper.rb +1 -0
  84. data/spec/integration/stored_fields_spec.rb +10 -0
  85. data/spec/integration/test_pagination.rb +32 -0
  86. data/spec/mocks/adapters.rb +32 -0
  87. data/spec/mocks/blog.rb +3 -0
  88. data/spec/mocks/comment.rb +19 -0
  89. data/spec/mocks/connection.rb +84 -0
  90. data/spec/mocks/mock_adapter.rb +30 -0
  91. data/spec/mocks/mock_record.rb +41 -0
  92. data/spec/mocks/photo.rb +8 -0
  93. data/spec/mocks/post.rb +70 -0
  94. data/spec/mocks/user.rb +8 -0
  95. data/spec/spec_helper.rb +47 -0
  96. data/tasks/gemspec.rake +25 -0
  97. data/tasks/rcov.rake +28 -0
  98. data/tasks/rdoc.rake +21 -0
  99. data/tasks/schema.rake +19 -0
  100. data/tasks/spec.rake +24 -0
  101. data/tasks/todo.rake +4 -0
  102. data/templates/schema.xml.haml +24 -0
  103. metadata +245 -0
@@ -0,0 +1,222 @@
1
+ require File.join(File.dirname(__FILE__), 'search', 'hit')
2
+
3
+ module Sunspot
4
+ #
5
+ # This class encapsulates the results of a Solr search. It provides access
6
+ # to search results, total result count, facets, and pagination information.
7
+ # Instances of Search are returned by the Sunspot.search and
8
+ # Sunspot.new_search methods.
9
+ #
10
+ class Search
11
+ # Query information for this search. If you wish to build the query without
12
+ # using the search DSL, this method allows you to access the query API
13
+ # directly. See Sunspot#new_search for how to construct the search object
14
+ # in this case.
15
+ attr_reader :query
16
+
17
+ def initialize(connection, setup, query) #:nodoc:
18
+ @connection, @setup, @query = connection, setup, query
19
+ end
20
+
21
+ #
22
+ # Execute the search on the Solr instance and store the results. If you
23
+ # use Sunspot#search() to construct your searches, there is no need to call
24
+ # this method as it has already been called. If you use
25
+ # Sunspot#new_search(), you will need to call this method after building the
26
+ # query.
27
+ #
28
+ def execute!
29
+ params = @query.to_params
30
+ @solr_result = @connection.select(params)
31
+ self
32
+ end
33
+
34
+ #
35
+ # Get the collection of results as instantiated objects. If WillPaginate is
36
+ # available, the results will be a WillPaginate::Collection instance; if
37
+ # not, it will be a vanilla Array.
38
+ #
39
+ # ==== Returns
40
+ #
41
+ # WillPaginate::Collection or Array:: Instantiated result objects
42
+ #
43
+ def results
44
+ @results ||= if @query.page && defined?(WillPaginate::Collection)
45
+ WillPaginate::Collection.create(@query.page, @query.per_page, total) do |pager|
46
+ pager.replace(hits.map { |hit| hit.instance })
47
+ end
48
+ else
49
+ hits.map { |hit| hit.instance }
50
+ end
51
+ end
52
+
53
+ #
54
+ # Access raw Solr result information. Returns a collection of Hit objects
55
+ # that contain the class name, primary key, keyword relevance score (if
56
+ # applicable), and any stored fields.
57
+ #
58
+ # ==== Returns
59
+ #
60
+ # Array:: Ordered collection of Hit objects
61
+ #
62
+ def hits
63
+ @hits ||= solr_response['docs'].map { |doc| Hit.new(doc, self) }
64
+ end
65
+ alias_method :raw_results, :hits
66
+
67
+ #
68
+ # The total number of documents matching the query parameters
69
+ #
70
+ # ==== Returns
71
+ #
72
+ # Integer:: Total matching documents
73
+ #
74
+ def total
75
+ @total ||= solr_response['numFound']
76
+ end
77
+
78
+ #
79
+ # Get the facet object for the given field. This field will need to have
80
+ # been requested as a field facet inside the search block.
81
+ #
82
+ # ==== Parameters
83
+ #
84
+ # field_name<Symbol>:: field name for which to get the facet
85
+ #
86
+ # ==== Returns
87
+ #
88
+ # Sunspot::Facet:: Facet object for the given field
89
+ #
90
+ def facet(field_name)
91
+ (@facets_cache ||= {})[field_name.to_sym] ||=
92
+ begin
93
+ query_facet(field_name) ||
94
+ begin
95
+ field = field(field_name)
96
+ date_facet(field) ||
97
+ begin
98
+ facet_class = field.reference ? InstantiatedFacet : Facet
99
+ facet_class.new(@solr_result['facet_counts']['facet_fields'][field.indexed_name], field)
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ #
106
+ # Get the facet object for a given dynamic field. This dynamic field will
107
+ # need to have been requested as a field facet inside the search block.
108
+ #
109
+ # ==== Parameters
110
+ #
111
+ # base_name<Symbol>::
112
+ # Base name of the dynamic field definiton (as specified in the setup
113
+ # block)
114
+ # dynamic_name<Symbol>::
115
+ # Dynamic field name to facet on
116
+ #
117
+ # ==== Returns
118
+ #
119
+ # Sunspot::Facet:: Facet object for given dynamic field
120
+ #
121
+ # ==== Example
122
+ #
123
+ # search = Sunspot.search(Post) do
124
+ # dynamic :custom do
125
+ # facet :cuisine
126
+ # end
127
+ # end
128
+ # search.dynamic_facet(:custom, :cuisine)
129
+ # #=> Facet for the dynamic field :cuisine in the :custom field definition
130
+ #
131
+ def dynamic_facet(base_name, dynamic_name)
132
+ (@dynamic_facets_cache ||= {})[[base_name.to_sym, dynamic_name.to_sym]] ||=
133
+ begin
134
+ field = @setup.dynamic_field_factory(base_name).build(dynamic_name)
135
+ Facet.new(@solr_result['facet_counts']['facet_fields'][field.indexed_name], field)
136
+ end
137
+ end
138
+
139
+ #
140
+ # Get the data accessor that will be used to load a particular class out of
141
+ # persistent storage. Data accessors can implement any methods that may be
142
+ # useful for refining how data is loaded out of storage. When building a
143
+ # search manually (e.g., using the Sunspot#new_search method), this should
144
+ # be used before calling #execute(). Use the
145
+ # Sunspot::DSL::Search#data_accessor_for method when building searches using
146
+ # the block DSL.
147
+ #
148
+ def data_accessor_for(clazz)
149
+ (@data_accessors ||= {})[clazz.name.to_sym] ||=
150
+ Adapters::DataAccessor.create(clazz)
151
+ end
152
+
153
+ #
154
+ # Build this search using a DSL block.
155
+ #
156
+ def build(&block) #:nodoc:
157
+ Util.instance_eval_or_call(dsl, &block)
158
+ self
159
+ end
160
+
161
+ def populate_hits! #:nodoc:
162
+ type_hit_hash = Hash.new { |h, k| h[k] = [] }
163
+ id_hit_hash = {}
164
+ for hit in hits
165
+ type_hit_hash[hit.class_name] << hit
166
+ id_hit_hash[hit.primary_key] = hit
167
+ end
168
+ type_hit_hash.each_pair do |class_name, hits|
169
+ ids = hits.map { |hit| hit.primary_key }
170
+ for instance in data_accessor_for(Util.full_const_get(class_name)).load_all(ids)
171
+ hit = id_hit_hash[Adapters::InstanceAdapter.adapt(instance).id.to_s]
172
+ hit.instance = instance
173
+ end
174
+ end
175
+ end
176
+
177
+ private
178
+
179
+ def solr_response
180
+ @solr_response ||= @solr_result['response']
181
+ end
182
+
183
+ def doc_ids
184
+ @doc_ids ||= solr_response['docs'].map { |doc| doc['id'] }
185
+ end
186
+
187
+ def dsl
188
+ DSL::Search.new(self)
189
+ end
190
+
191
+ def raw_facet(field)
192
+ if field.type == Type::TimeType
193
+ @solr_result['facet_counts']['facet_dates'][field.indexed_name]
194
+ end || @solr_result['facet_counts']['facet_fields'][field.indexed_name]
195
+ end
196
+
197
+ def date_facet(field)
198
+ if field.type == Type::TimeType
199
+ if @solr_result['facet_counts'].has_key?('facet_dates')
200
+ if facet_result = @solr_result['facet_counts']['facet_dates'][field.indexed_name]
201
+ DateFacet.new(facet_result, field)
202
+ end
203
+ end
204
+ end
205
+ end
206
+
207
+ def query_facet(name)
208
+ if query_facet = @query.query_facet(name.to_sym)
209
+ if @solr_result['facet_counts'].has_key?('facet_queries')
210
+ QueryFacet.new(
211
+ query_facet,
212
+ @solr_result['facet_counts']['facet_queries']
213
+ )
214
+ end
215
+ end
216
+ end
217
+
218
+ def field(name)
219
+ @setup.field(name)
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,62 @@
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
+ end
61
+ end
62
+ 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(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