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,36 @@
1
+ module Sunspot
2
+ module Query
3
+ #
4
+ # The Sort class is a query component representing a sort by a given field.
5
+ #
6
+ class Sort #:nodoc:
7
+ DIRECTIONS = {
8
+ :asc => 'asc',
9
+ :ascending => 'asc',
10
+ :desc => 'desc',
11
+ :descending => 'desc'
12
+ }
13
+
14
+ def initialize(field, direction = nil)
15
+ if field.multiple?
16
+ raise(ArgumentError, "#{field.name} cannot be used for ordering because it is a multiple-value field")
17
+ end
18
+ @field, @direction = field, (direction || :asc).to_sym
19
+ end
20
+
21
+ def to_param
22
+ "#{@field.indexed_name.to_sym} #{direction_for_solr}"
23
+ end
24
+
25
+ private
26
+
27
+ def direction_for_solr
28
+ DIRECTIONS[@direction] ||
29
+ raise(
30
+ ArgumentError,
31
+ "Unknown sort direction #{@direction}. Acceptable input is: #{DIRECTIONS.keys.map { |input| input.inspect } * ', '}"
32
+ )
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,33 @@
1
+ module Sunspot
2
+ module Query
3
+ #
4
+ # The SortComposite class encapsulates an ordered collection of Sort
5
+ # objects. It's necessary to keep this as a separate class as Solr takes
6
+ # the sort as a single parameter, so adding sorts as regular components
7
+ # would not merge correctly in the #to_params method.
8
+ #
9
+ class SortComposite #:nodoc:
10
+ def initialize
11
+ @sorts = []
12
+ end
13
+
14
+ #
15
+ # Add a sort to the composite
16
+ #
17
+ def <<(sort)
18
+ @sorts << sort
19
+ end
20
+
21
+ #
22
+ # Combine the sorts into a single param by joining them
23
+ #
24
+ def to_params
25
+ unless @sorts.empty?
26
+ { :sort => @sorts.map { |sort| sort.to_param } * ', ' }
27
+ else
28
+ {}
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,165 @@
1
+ using_rubygems = false
2
+ begin
3
+ require 'haml'
4
+ rescue LoadError => e
5
+ if using_rubygems
6
+ raise(e)
7
+ else
8
+ using_rubygems = true
9
+ require 'rubygems'
10
+ retry
11
+ end
12
+ end
13
+
14
+ module Sunspot
15
+ #
16
+ # Object that encapsulates schema information for building a Solr schema.xml
17
+ # file. This class is used by the schema:compile task as well as the
18
+ # sunspot-configure-solr executable.
19
+ #
20
+ class Schema #:nodoc:all
21
+ FieldType = Struct.new(:name, :class_name, :suffix)
22
+ FieldVariant = Struct.new(:attribute, :suffix)
23
+
24
+ DEFAULT_TOKENIZER = 'solr.StandardTokenizerFactory'
25
+ DEFAULT_FILTERS = %w(solr.StandardFilterFactory solr.LowerCaseFilterFactory)
26
+
27
+ FIELD_TYPES = [
28
+ FieldType.new('boolean', 'Bool', 'b'),
29
+ FieldType.new('sfloat', 'SortableFloat', 'f'),
30
+ FieldType.new('date', 'Date', 'd'),
31
+ FieldType.new('sint', 'SortableInt', 'i'),
32
+ FieldType.new('string', 'Str', 's')
33
+ ]
34
+
35
+ FIELD_VARIANTS = [
36
+ FieldVariant.new('multiValued', 'm'),
37
+ FieldVariant.new('stored', 's')
38
+ ]
39
+
40
+ attr_reader :tokenizer, :filters
41
+
42
+ def initialize
43
+ @tokenizer = DEFAULT_TOKENIZER
44
+ @filters = DEFAULT_FILTERS.dup
45
+ end
46
+
47
+ #
48
+ # Attribute field types defined in the schema
49
+ #
50
+ def types
51
+ FIELD_TYPES
52
+ end
53
+
54
+ #
55
+ # DynamicField instances representing all the available types and variants
56
+ #
57
+ def dynamic_fields
58
+ fields = []
59
+ for field_variants in variant_combinations
60
+ for type in FIELD_TYPES
61
+ fields << DynamicField.new(type, field_variants)
62
+ end
63
+ end
64
+ fields
65
+ end
66
+
67
+ #
68
+ # Which tokenizer to use for text fields
69
+ #
70
+ def tokenizer=(tokenizer)
71
+ @tokenizer =
72
+ if tokenizer =~ /\./
73
+ tokenizer
74
+ else
75
+ "solr.#{tokenizer}TokenizerFactory"
76
+ end
77
+ end
78
+
79
+ #
80
+ # Add a filter for text field tokenization
81
+ #
82
+ def add_filter(filter)
83
+ @filters <<
84
+ if filter =~ /\./
85
+ filter
86
+ else
87
+ "solr.#{filter}FilterFactory"
88
+ end
89
+ end
90
+
91
+ #
92
+ # Return an XML representation of this schema using the Haml template
93
+ #
94
+ def to_xml
95
+ template = File.read(
96
+ File.join(
97
+ File.dirname(__FILE__),
98
+ '..',
99
+ '..',
100
+ 'templates',
101
+ 'schema.xml.haml'
102
+ )
103
+ )
104
+ engine = Haml::Engine.new(template)
105
+ engine.render(Object.new, :schema => self)
106
+ end
107
+
108
+ private
109
+
110
+ #
111
+ # All of the possible combinations of variants
112
+ #
113
+ def variant_combinations
114
+ combinations = []
115
+ 0.upto(2 ** FIELD_VARIANTS.length - 1) do |b|
116
+ combinations << combination = []
117
+ FIELD_VARIANTS.each_with_index do |variant, i|
118
+ combination << variant if b & 1<<i > 0
119
+ end
120
+ end
121
+ combinations
122
+ end
123
+
124
+ #
125
+ # Represents a dynamic field (in the Solr schema sense, not the Sunspot
126
+ # sense).
127
+ #
128
+ class DynamicField
129
+ def initialize(type, field_variants)
130
+ @type, @field_variants = type, field_variants
131
+ end
132
+
133
+ #
134
+ # Name of the field in the schema
135
+ #
136
+ def name
137
+ variant_suffixes = @field_variants.map { |variant| variant.suffix }.join
138
+ "*_#{@type.suffix}#{variant_suffixes}"
139
+ end
140
+
141
+ #
142
+ # Name of the type as defined in the schema
143
+ #
144
+ def type
145
+ @type.name
146
+ end
147
+
148
+ #
149
+ # Implement magic methods to ask if a field is of a particular variant.
150
+ # Returns "true" if the field is of that variant and "false" otherwise.
151
+ #
152
+ def method_missing(name, *args, &block)
153
+ if name.to_s =~ /\?$/ && args.empty?
154
+ if @field_variants.any? { |variant| "#{variant.attribute}?" == name.to_s }
155
+ 'true'
156
+ else
157
+ 'false'
158
+ end
159
+ else
160
+ super(name.to_sym, *args, &block)
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,219 @@
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
+ facet_data = query_facet_data(field_name) ||
94
+ begin
95
+ field = field(field_name)
96
+ date_facet_data(field) ||
97
+ FacetData::FieldFacetData.new(@solr_result['facet_counts']['facet_fields'][field.indexed_name], field)
98
+ end
99
+ facet_class = facet_data.reference ? InstantiatedFacet : Facet
100
+ facet_class.new(facet_data)
101
+ end
102
+ end
103
+
104
+ #
105
+ # Get the facet object for a given dynamic field. This dynamic field will
106
+ # need to have been requested as a field facet inside the search block.
107
+ #
108
+ # ==== Parameters
109
+ #
110
+ # base_name<Symbol>::
111
+ # Base name of the dynamic field definiton (as specified in the setup
112
+ # block)
113
+ # dynamic_name<Symbol>::
114
+ # Dynamic field name to facet on
115
+ #
116
+ # ==== Returns
117
+ #
118
+ # Sunspot::Facet:: Facet object for given dynamic field
119
+ #
120
+ # ==== Example
121
+ #
122
+ # search = Sunspot.search(Post) do
123
+ # dynamic :custom do
124
+ # facet :cuisine
125
+ # end
126
+ # end
127
+ # search.dynamic_facet(:custom, :cuisine)
128
+ # #=> Facet for the dynamic field :cuisine in the :custom field definition
129
+ #
130
+ def dynamic_facet(base_name, dynamic_name)
131
+ (@dynamic_facets_cache ||= {})[[base_name.to_sym, dynamic_name.to_sym]] ||=
132
+ begin
133
+ field = @setup.dynamic_field_factory(base_name).build(dynamic_name)
134
+ Facet.new(FacetData::FieldFacetData.new(@solr_result['facet_counts']['facet_fields'][field.indexed_name], field))
135
+ end
136
+ end
137
+
138
+ #
139
+ # Get the data accessor that will be used to load a particular class out of
140
+ # persistent storage. Data accessors can implement any methods that may be
141
+ # useful for refining how data is loaded out of storage. When building a
142
+ # search manually (e.g., using the Sunspot#new_search method), this should
143
+ # be used before calling #execute(). Use the
144
+ # Sunspot::DSL::Search#data_accessor_for method when building searches using
145
+ # the block DSL.
146
+ #
147
+ def data_accessor_for(clazz)
148
+ (@data_accessors ||= {})[clazz.name.to_sym] ||=
149
+ Adapters::DataAccessor.create(clazz)
150
+ end
151
+
152
+ #
153
+ # Build this search using a DSL block.
154
+ #
155
+ def build(&block) #:nodoc:
156
+ Util.instance_eval_or_call(dsl, &block)
157
+ self
158
+ end
159
+
160
+ def populate_hits! #:nodoc:
161
+ id_hit_hash = Hash.new { |h, k| h[k] = {} }
162
+ hits.each do |hit|
163
+ id_hit_hash[hit.class_name][hit.primary_key] = hit
164
+ end
165
+ id_hit_hash.each_pair do |class_name, hits|
166
+ ids = hits.map { |id, hit| hit.primary_key }
167
+ data_accessor_for(Util.full_const_get(class_name)).load_all(ids).each do |instance|
168
+ hit = id_hit_hash[class_name][Adapters::InstanceAdapter.adapt(instance).id.to_s]
169
+ hit.instance = instance
170
+ end
171
+ end
172
+ end
173
+
174
+ private
175
+
176
+ def solr_response
177
+ @solr_response ||= @solr_result['response']
178
+ end
179
+
180
+ def doc_ids
181
+ @doc_ids ||= solr_response['docs'].map { |doc| doc['id'] }
182
+ end
183
+
184
+ def dsl
185
+ DSL::Search.new(self)
186
+ end
187
+
188
+ def raw_facet(field)
189
+ if field.type == Type::TimeType
190
+ @solr_result['facet_counts']['facet_dates'][field.indexed_name]
191
+ end || @solr_result['facet_counts']['facet_fields'][field.indexed_name]
192
+ end
193
+
194
+ def date_facet_data(field)
195
+ if field.type == Type::TimeType
196
+ if @solr_result['facet_counts'].has_key?('facet_dates')
197
+ if facet_result = @solr_result['facet_counts']['facet_dates'][field.indexed_name]
198
+ FacetData::DateFacetData.new(facet_result, field)
199
+ end
200
+ end
201
+ end
202
+ end
203
+
204
+ def query_facet_data(name)
205
+ if query_facet = @query.query_facet(name.to_sym)
206
+ if @solr_result['facet_counts'].has_key?('facet_queries')
207
+ FacetData::QueryFacetData.new(
208
+ query_facet,
209
+ @solr_result['facet_counts']['facet_queries']
210
+ )
211
+ end
212
+ end
213
+ end
214
+
215
+ def field(name)
216
+ @setup.field(name)
217
+ end
218
+ end
219
+ end