sunspot_rbg 1.3.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.
Files changed (222) hide show
  1. data/.gitignore +12 -0
  2. data/Gemfile +4 -0
  3. data/History.txt +222 -0
  4. data/LICENSE +18 -0
  5. data/Rakefile +17 -0
  6. data/TODO +13 -0
  7. data/VERSION.yml +4 -0
  8. data/bin/sunspot-installer +19 -0
  9. data/bin/sunspot-solr +74 -0
  10. data/installer/config/schema.yml +95 -0
  11. data/lib/light_config.rb +40 -0
  12. data/lib/sunspot/adapters.rb +265 -0
  13. data/lib/sunspot/composite_setup.rb +202 -0
  14. data/lib/sunspot/configuration.rb +46 -0
  15. data/lib/sunspot/data_extractor.rb +50 -0
  16. data/lib/sunspot/dsl/adjustable.rb +47 -0
  17. data/lib/sunspot/dsl/field_query.rb +279 -0
  18. data/lib/sunspot/dsl/fields.rb +103 -0
  19. data/lib/sunspot/dsl/fulltext.rb +243 -0
  20. data/lib/sunspot/dsl/function.rb +14 -0
  21. data/lib/sunspot/dsl/functional.rb +44 -0
  22. data/lib/sunspot/dsl/more_like_this_query.rb +56 -0
  23. data/lib/sunspot/dsl/paginatable.rb +28 -0
  24. data/lib/sunspot/dsl/query_facet.rb +36 -0
  25. data/lib/sunspot/dsl/restriction.rb +25 -0
  26. data/lib/sunspot/dsl/restriction_with_near.rb +121 -0
  27. data/lib/sunspot/dsl/scope.rb +217 -0
  28. data/lib/sunspot/dsl/search.rb +30 -0
  29. data/lib/sunspot/dsl/standard_query.rb +121 -0
  30. data/lib/sunspot/dsl.rb +5 -0
  31. data/lib/sunspot/field.rb +193 -0
  32. data/lib/sunspot/field_factory.rb +129 -0
  33. data/lib/sunspot/indexer.rb +131 -0
  34. data/lib/sunspot/installer/library_installer.rb +45 -0
  35. data/lib/sunspot/installer/schema_builder.rb +219 -0
  36. data/lib/sunspot/installer/solrconfig_updater.rb +76 -0
  37. data/lib/sunspot/installer/task_helper.rb +18 -0
  38. data/lib/sunspot/installer.rb +31 -0
  39. data/lib/sunspot/query/abstract_field_facet.rb +52 -0
  40. data/lib/sunspot/query/boost_query.rb +24 -0
  41. data/lib/sunspot/query/common_query.rb +85 -0
  42. data/lib/sunspot/query/composite_fulltext.rb +36 -0
  43. data/lib/sunspot/query/connective.rb +206 -0
  44. data/lib/sunspot/query/date_field_facet.rb +14 -0
  45. data/lib/sunspot/query/dismax.rb +128 -0
  46. data/lib/sunspot/query/field_facet.rb +41 -0
  47. data/lib/sunspot/query/filter.rb +38 -0
  48. data/lib/sunspot/query/function_query.rb +52 -0
  49. data/lib/sunspot/query/geo.rb +53 -0
  50. data/lib/sunspot/query/highlighting.rb +55 -0
  51. data/lib/sunspot/query/more_like_this.rb +61 -0
  52. data/lib/sunspot/query/more_like_this_query.rb +12 -0
  53. data/lib/sunspot/query/pagination.rb +38 -0
  54. data/lib/sunspot/query/query_facet.rb +16 -0
  55. data/lib/sunspot/query/restriction.rb +262 -0
  56. data/lib/sunspot/query/scope.rb +9 -0
  57. data/lib/sunspot/query/sort.rb +95 -0
  58. data/lib/sunspot/query/sort_composite.rb +33 -0
  59. data/lib/sunspot/query/standard_query.rb +16 -0
  60. data/lib/sunspot/query/text_field_boost.rb +17 -0
  61. data/lib/sunspot/query.rb +11 -0
  62. data/lib/sunspot/schema.rb +151 -0
  63. data/lib/sunspot/search/abstract_search.rb +293 -0
  64. data/lib/sunspot/search/date_facet.rb +35 -0
  65. data/lib/sunspot/search/facet_row.rb +27 -0
  66. data/lib/sunspot/search/field_facet.rb +88 -0
  67. data/lib/sunspot/search/highlight.rb +38 -0
  68. data/lib/sunspot/search/hit.rb +136 -0
  69. data/lib/sunspot/search/more_like_this_search.rb +31 -0
  70. data/lib/sunspot/search/paginated_collection.rb +55 -0
  71. data/lib/sunspot/search/query_facet.rb +67 -0
  72. data/lib/sunspot/search/standard_search.rb +21 -0
  73. data/lib/sunspot/search.rb +9 -0
  74. data/lib/sunspot/server.rb +152 -0
  75. data/lib/sunspot/session.rb +260 -0
  76. data/lib/sunspot/session_proxy/abstract_session_proxy.rb +29 -0
  77. data/lib/sunspot/session_proxy/class_sharding_session_proxy.rb +66 -0
  78. data/lib/sunspot/session_proxy/id_sharding_session_proxy.rb +89 -0
  79. data/lib/sunspot/session_proxy/master_slave_session_proxy.rb +43 -0
  80. data/lib/sunspot/session_proxy/sharding_session_proxy.rb +222 -0
  81. data/lib/sunspot/session_proxy/silent_fail_session_proxy.rb +42 -0
  82. data/lib/sunspot/session_proxy/thread_local_session_proxy.rb +37 -0
  83. data/lib/sunspot/session_proxy.rb +87 -0
  84. data/lib/sunspot/setup.rb +350 -0
  85. data/lib/sunspot/text_field_setup.rb +29 -0
  86. data/lib/sunspot/type.rb +372 -0
  87. data/lib/sunspot/util.rb +243 -0
  88. data/lib/sunspot/version.rb +3 -0
  89. data/lib/sunspot.rb +569 -0
  90. data/lib/sunspot_rbg.rb +7 -0
  91. data/log/.gitignore +1 -0
  92. data/pkg/.gitignore +1 -0
  93. data/script/console +10 -0
  94. data/solr/README.txt +42 -0
  95. data/solr/etc/jetty.xml +218 -0
  96. data/solr/etc/webdefault.xml +379 -0
  97. data/solr/lib/jetty-6.1.3.jar +0 -0
  98. data/solr/lib/jetty-util-6.1.3.jar +0 -0
  99. data/solr/lib/jsp-2.1/ant-1.6.5.jar +0 -0
  100. data/solr/lib/jsp-2.1/core-3.1.1.jar +0 -0
  101. data/solr/lib/jsp-2.1/jsp-2.1.jar +0 -0
  102. data/solr/lib/jsp-2.1/jsp-api-2.1.jar +0 -0
  103. data/solr/lib/servlet-api-2.5-6.1.3.jar +0 -0
  104. data/solr/logs/.gitignore +1 -0
  105. data/solr/solr/.gitignore +1 -0
  106. data/solr/solr/README.txt +54 -0
  107. data/solr/solr/conf/admin-extra.html +31 -0
  108. data/solr/solr/conf/elevate.xml +36 -0
  109. data/solr/solr/conf/mapping-ISOLatin1Accent.txt +246 -0
  110. data/solr/solr/conf/protwords.txt +21 -0
  111. data/solr/solr/conf/schema.xml +238 -0
  112. data/solr/solr/conf/scripts.conf +24 -0
  113. data/solr/solr/conf/solrconfig.xml +934 -0
  114. data/solr/solr/conf/spellings.txt +2 -0
  115. data/solr/solr/conf/stopwords.txt +58 -0
  116. data/solr/solr/conf/synonyms.txt +31 -0
  117. data/solr/solr/conf/xslt/example.xsl +132 -0
  118. data/solr/solr/conf/xslt/example_atom.xsl +67 -0
  119. data/solr/solr/conf/xslt/example_rss.xsl +66 -0
  120. data/solr/solr/conf/xslt/luke.xsl +337 -0
  121. data/solr/start.jar +0 -0
  122. data/solr/webapps/solr.war +0 -0
  123. data/solr-1.3/etc/jetty.xml +212 -0
  124. data/solr-1.3/etc/webdefault.xml +379 -0
  125. data/solr-1.3/lib/jetty-6.1.3.jar +0 -0
  126. data/solr-1.3/lib/jetty-util-6.1.3.jar +0 -0
  127. data/solr-1.3/lib/jsp-2.1/ant-1.6.5.jar +0 -0
  128. data/solr-1.3/lib/jsp-2.1/core-3.1.1.jar +0 -0
  129. data/solr-1.3/lib/jsp-2.1/jsp-2.1.jar +0 -0
  130. data/solr-1.3/lib/jsp-2.1/jsp-api-2.1.jar +0 -0
  131. data/solr-1.3/lib/servlet-api-2.5-6.1.3.jar +0 -0
  132. data/solr-1.3/solr/conf/elevate.xml +36 -0
  133. data/solr-1.3/solr/conf/protwords.txt +21 -0
  134. data/solr-1.3/solr/conf/schema.xml +64 -0
  135. data/solr-1.3/solr/conf/solrconfig.xml +725 -0
  136. data/solr-1.3/solr/conf/stopwords.txt +57 -0
  137. data/solr-1.3/solr/conf/synonyms.txt +31 -0
  138. data/solr-1.3/solr/lib/geoapi-nogenerics-2.1-M2.jar +0 -0
  139. data/solr-1.3/solr/lib/gt2-referencing-2.3.1.jar +0 -0
  140. data/solr-1.3/solr/lib/jsr108-0.01.jar +0 -0
  141. data/solr-1.3/solr/lib/locallucene.jar +0 -0
  142. data/solr-1.3/solr/lib/localsolr.jar +0 -0
  143. data/solr-1.3/start.jar +0 -0
  144. data/solr-1.3/webapps/solr.war +0 -0
  145. data/spec/api/adapters_spec.rb +33 -0
  146. data/spec/api/binding_spec.rb +50 -0
  147. data/spec/api/indexer/attributes_spec.rb +149 -0
  148. data/spec/api/indexer/batch_spec.rb +46 -0
  149. data/spec/api/indexer/dynamic_fields_spec.rb +42 -0
  150. data/spec/api/indexer/fixed_fields_spec.rb +57 -0
  151. data/spec/api/indexer/fulltext_spec.rb +43 -0
  152. data/spec/api/indexer/removal_spec.rb +53 -0
  153. data/spec/api/indexer/spec_helper.rb +1 -0
  154. data/spec/api/indexer_spec.rb +14 -0
  155. data/spec/api/query/advanced_manipulation_examples.rb +35 -0
  156. data/spec/api/query/connectives_examples.rb +189 -0
  157. data/spec/api/query/dsl_spec.rb +18 -0
  158. data/spec/api/query/dynamic_fields_examples.rb +165 -0
  159. data/spec/api/query/faceting_examples.rb +397 -0
  160. data/spec/api/query/fulltext_examples.rb +313 -0
  161. data/spec/api/query/function_spec.rb +70 -0
  162. data/spec/api/query/geo_examples.rb +68 -0
  163. data/spec/api/query/highlighting_examples.rb +223 -0
  164. data/spec/api/query/more_like_this_spec.rb +140 -0
  165. data/spec/api/query/ordering_pagination_examples.rb +95 -0
  166. data/spec/api/query/scope_examples.rb +275 -0
  167. data/spec/api/query/spec_helper.rb +1 -0
  168. data/spec/api/query/standard_spec.rb +28 -0
  169. data/spec/api/query/text_field_scoping_examples.rb +30 -0
  170. data/spec/api/query/types_spec.rb +20 -0
  171. data/spec/api/search/dynamic_fields_spec.rb +33 -0
  172. data/spec/api/search/faceting_spec.rb +360 -0
  173. data/spec/api/search/highlighting_spec.rb +69 -0
  174. data/spec/api/search/hits_spec.rb +120 -0
  175. data/spec/api/search/paginated_collection_spec.rb +26 -0
  176. data/spec/api/search/results_spec.rb +66 -0
  177. data/spec/api/search/search_spec.rb +23 -0
  178. data/spec/api/search/spec_helper.rb +1 -0
  179. data/spec/api/server_spec.rb +91 -0
  180. data/spec/api/session_proxy/class_sharding_session_proxy_spec.rb +85 -0
  181. data/spec/api/session_proxy/id_sharding_session_proxy_spec.rb +30 -0
  182. data/spec/api/session_proxy/master_slave_session_proxy_spec.rb +41 -0
  183. data/spec/api/session_proxy/sharding_session_proxy_spec.rb +77 -0
  184. data/spec/api/session_proxy/silent_fail_session_proxy_spec.rb +24 -0
  185. data/spec/api/session_proxy/spec_helper.rb +9 -0
  186. data/spec/api/session_proxy/thread_local_session_proxy_spec.rb +50 -0
  187. data/spec/api/session_spec.rb +220 -0
  188. data/spec/api/spec_helper.rb +3 -0
  189. data/spec/api/sunspot_spec.rb +18 -0
  190. data/spec/ext.rb +11 -0
  191. data/spec/helpers/indexer_helper.rb +29 -0
  192. data/spec/helpers/query_helper.rb +38 -0
  193. data/spec/helpers/search_helper.rb +80 -0
  194. data/spec/integration/dynamic_fields_spec.rb +55 -0
  195. data/spec/integration/faceting_spec.rb +238 -0
  196. data/spec/integration/highlighting_spec.rb +22 -0
  197. data/spec/integration/indexing_spec.rb +33 -0
  198. data/spec/integration/keyword_search_spec.rb +317 -0
  199. data/spec/integration/local_search_spec.rb +64 -0
  200. data/spec/integration/more_like_this_spec.rb +43 -0
  201. data/spec/integration/scoped_search_spec.rb +354 -0
  202. data/spec/integration/spec_helper.rb +7 -0
  203. data/spec/integration/stored_fields_spec.rb +10 -0
  204. data/spec/integration/test_pagination.rb +32 -0
  205. data/spec/mocks/adapters.rb +32 -0
  206. data/spec/mocks/blog.rb +3 -0
  207. data/spec/mocks/comment.rb +21 -0
  208. data/spec/mocks/connection.rb +126 -0
  209. data/spec/mocks/mock_adapter.rb +30 -0
  210. data/spec/mocks/mock_class_sharding_session_proxy.rb +24 -0
  211. data/spec/mocks/mock_record.rb +52 -0
  212. data/spec/mocks/mock_sharding_session_proxy.rb +15 -0
  213. data/spec/mocks/photo.rb +11 -0
  214. data/spec/mocks/post.rb +85 -0
  215. data/spec/mocks/super_class.rb +2 -0
  216. data/spec/mocks/user.rb +13 -0
  217. data/spec/spec_helper.rb +30 -0
  218. data/sunspot.gemspec +40 -0
  219. data/tasks/rdoc.rake +27 -0
  220. data/tasks/schema.rake +19 -0
  221. data/tasks/todo.rake +4 -0
  222. metadata +457 -0
@@ -0,0 +1,193 @@
1
+ module Sunspot
2
+ class Field #:nodoc:
3
+ attr_accessor :name # The public-facing name of the field
4
+ attr_accessor :type # The Type of the field
5
+ attr_accessor :reference # Model class that the value of this field refers to
6
+ attr_reader :boost
7
+ attr_reader :indexed_name # Name with which this field is indexed internally. Based on public name and type or the +:as+ option.
8
+
9
+ #
10
+ #
11
+ def initialize(name, type, options = {}) #:nodoc
12
+ @name, @type = name.to_sym, type
13
+ @stored = !!options.delete(:stored)
14
+ @more_like_this = !!options.delete(:more_like_this)
15
+ set_indexed_name(options)
16
+ raise ArgumentError, "Field of type #{type} cannot be used for more_like_this" unless type.accepts_more_like_this? or !@more_like_this
17
+ end
18
+
19
+ # Convert a value to its representation for Solr indexing. This delegates
20
+ # to the #to_indexed method on the field's type.
21
+ #
22
+ # ==== Parameters
23
+ #
24
+ # value<Object>:: Value to convert to Solr representation
25
+ #
26
+ # ==== Returns
27
+ #
28
+ # String:: Solr representation of the object
29
+ #
30
+ # ==== Raises
31
+ #
32
+ # ArgumentError::
33
+ # the value is an array, but this field does not allow multiple values
34
+ #
35
+ def to_indexed(value)
36
+ if value.is_a? Array
37
+ if @multiple
38
+ value.map { |val| to_indexed(val) }
39
+ else
40
+ raise ArgumentError, "#{name} is not a multiple-value field, so it cannot index values #{value.inspect}"
41
+ end
42
+ else
43
+ @type.to_indexed(value)
44
+ end
45
+ end
46
+
47
+ # Cast the value into the appropriate Ruby class for the field's type
48
+ #
49
+ # ==== Parameters
50
+ #
51
+ # value<String>:: Solr's representation of the value
52
+ #
53
+ # ==== Returns
54
+ #
55
+ # Object:: The cast value
56
+ #
57
+ def cast(value)
58
+ @type.cast(value)
59
+ end
60
+
61
+ #
62
+ # Whether this field accepts multiple values.
63
+ #
64
+ # ==== Returns
65
+ #
66
+ # Boolean:: True if this field accepts multiple values.
67
+ #
68
+ def multiple?
69
+ !!@multiple
70
+ end
71
+
72
+ #
73
+ # Whether this field can be used for more_like_this queries.
74
+ # If true, the field is configured to store termVectors.
75
+ #
76
+ # ==== Returns
77
+ #
78
+ # Boolean:: True if this field can be used for more_like_this queries.
79
+ #
80
+ def more_like_this?
81
+ !!@more_like_this
82
+ end
83
+
84
+ def hash
85
+ indexed_name.hash
86
+ end
87
+
88
+ def eql?(field)
89
+ indexed_name == field.indexed_name
90
+ end
91
+ alias_method :==, :eql?
92
+
93
+ private
94
+
95
+ #
96
+ # Determine the indexed name. If the :as option is given use that, otherwise
97
+ # create the value based on the indexed_name of the type with additional
98
+ # suffixes for multiple, stored, and more_like_this.
99
+ #
100
+ # ==== Returns
101
+ #
102
+ # String: The field's indexed name
103
+ #
104
+ def set_indexed_name(options)
105
+ @indexed_name =
106
+ if options[:as]
107
+ options.delete(:as)
108
+ else
109
+ "#{@type.indexed_name(@name).to_s}#{'m' if @multiple }#{'s' if @stored}#{'v' if more_like_this?}"
110
+ end
111
+ end
112
+
113
+ end
114
+
115
+ #
116
+ # FulltextField instances represent fields that are indexed as fulltext.
117
+ # These fields are tokenized in the index, and can have boost applied to
118
+ # them. They also always allow multiple values (since the only downside of
119
+ # allowing multiple values is that it prevents the field from being sortable,
120
+ # and sorting on tokenized fields is nonsensical anyway, there is no reason
121
+ # to do otherwise). FulltextField instances always have the type TextType.
122
+ #
123
+ class FulltextField < Field #:nodoc:
124
+ attr_reader :default_boost
125
+
126
+ def initialize(name, options = {})
127
+ super(name, Type::TextType.instance, options)
128
+ @multiple = true
129
+ @boost = options.delete(:boost)
130
+ @default_boost = options.delete(:default_boost)
131
+ raise ArgumentError, "Unknown field option #{options.keys.first.inspect} provided for field #{name.inspect}" unless options.empty?
132
+ end
133
+
134
+ def indexed_name
135
+ "#{super}"
136
+ end
137
+ end
138
+
139
+ #
140
+ # AttributeField instances encapsulate non-tokenized attribute data.
141
+ # AttributeFields can have any type except TextType, and can also have
142
+ # a reference (for instantiated facets), optionally allow multiple values
143
+ # (false by default), and can store their values (false by default). All
144
+ # scoping, sorting, and faceting is done with attribute fields.
145
+ #
146
+ class AttributeField < Field #:nodoc:
147
+ def initialize(name, type, options = {})
148
+ @multiple = !!options.delete(:multiple)
149
+ super(name, type, options)
150
+ @reference =
151
+ if (reference = options.delete(:references)).respond_to?(:name)
152
+ reference.name
153
+ elsif reference.respond_to?(:to_sym)
154
+ reference.to_sym
155
+ end
156
+ raise ArgumentError, "Unknown field option #{options.keys.first.inspect} provided for field #{name.inspect}" unless options.empty?
157
+ end
158
+
159
+ end
160
+
161
+ class TypeField #:nodoc:
162
+ class <<self
163
+ def instance
164
+ @instance ||= new
165
+ end
166
+ end
167
+
168
+ def indexed_name
169
+ 'type'
170
+ end
171
+
172
+ def to_indexed(clazz)
173
+ clazz.name
174
+ end
175
+ end
176
+
177
+ class IdField #:nodoc:
178
+ class <<self
179
+ def instance
180
+ @instance ||= new
181
+ end
182
+ end
183
+
184
+ def indexed_name
185
+ 'id'
186
+ end
187
+
188
+ def to_indexed(id)
189
+ id.to_s
190
+ end
191
+ end
192
+ end
193
+
@@ -0,0 +1,129 @@
1
+ module Sunspot
2
+ #
3
+ # The FieldFactory module contains classes for generating fields. FieldFactory
4
+ # implementation classes should implement a #build method, although the arity
5
+ # of the method depends on the type of factory. They also must implement a
6
+ # #populate_document method, which extracts field data from a given model and
7
+ # adds it into the Solr document for indexing.
8
+ #
9
+ module FieldFactory #:nodoc:all
10
+ #
11
+ # Base class for field factories.
12
+ #
13
+ class Abstract
14
+ attr_reader :name
15
+
16
+ def initialize(name, options = {}, &block)
17
+ @name = name.to_sym
18
+ @data_extractor =
19
+ if block
20
+ DataExtractor::BlockExtractor.new(&block)
21
+ else
22
+ DataExtractor::AttributeExtractor.new(options.delete(:using) || name)
23
+ end
24
+ end
25
+ end
26
+
27
+ #
28
+ # A StaticFieldFactory generates normal static fields. Each factory instance
29
+ # contains an eager-initialized field instance, which is returned by the
30
+ # #build method.
31
+ #
32
+ class Static < Abstract
33
+ def initialize(name, type, options = {}, &block)
34
+ super(name, options, &block)
35
+ unless name.to_s =~ /^\w+$/
36
+ raise ArgumentError, "Invalid field name #{name}: only letters, numbers, and underscores are allowed."
37
+ end
38
+ @field =
39
+ if type.is_a?(Type::TextType)
40
+ FulltextField.new(name, options)
41
+ else
42
+ AttributeField.new(name, type, options)
43
+ end
44
+ end
45
+
46
+ #
47
+ # Return the field instance built by this factory
48
+ #
49
+ def build
50
+ @field
51
+ end
52
+
53
+ #
54
+ # Extract the encapsulated field's data from the given model and add it
55
+ # into the Solr document for indexing.
56
+ #
57
+ def populate_document(document, model) #:nodoc:
58
+ unless (value = @data_extractor.value_for(model)).nil?
59
+ Util.Array(@field.to_indexed(value)).each do |scalar_value|
60
+ options = {}
61
+ options[:boost] = @field.boost if @field.boost
62
+ document.add_field(
63
+ @field.indexed_name.to_sym,
64
+ scalar_value,
65
+ options
66
+ )
67
+ end
68
+ end
69
+ end
70
+
71
+ #
72
+ # A unique signature identifying this field by name and type.
73
+ #
74
+ def signature
75
+ [@field.name, @field.type]
76
+ end
77
+ end
78
+
79
+ #
80
+ # DynamicFieldFactories create dynamic field instances based on dynamic
81
+ # configuration.
82
+ #
83
+ class Dynamic < Abstract
84
+ attr_accessor :name, :type
85
+
86
+ def initialize(name, type, options = {}, &block)
87
+ super(name, options, &block)
88
+ @type, @options = type, options
89
+ end
90
+
91
+ #
92
+ # Build a field based on the dynamic name given.
93
+ #
94
+ def build(dynamic_name)
95
+ AttributeField.new("#{@name}:#{dynamic_name}", @type, @options.dup)
96
+ end
97
+ #
98
+ # This alias allows a DynamicFieldFactory to be used in place of a Setup
99
+ # or CompositeSetup instance by query components.
100
+ #
101
+ alias_method :field, :build
102
+
103
+ #
104
+ # Generate dynamic fields based on hash returned by data accessor and
105
+ # add the field data to the document.
106
+ #
107
+ def populate_document(document, model)
108
+ if values = @data_extractor.value_for(model)
109
+ values.each_pair do |dynamic_name, value|
110
+ field_instance = build(dynamic_name)
111
+ Util.Array(field_instance.to_indexed(value)).each do |scalar_value|
112
+ document.add_field(
113
+ field_instance.indexed_name.to_sym,
114
+ scalar_value
115
+ )
116
+ end
117
+ end
118
+ end
119
+ end
120
+
121
+ #
122
+ # Unique signature identifying this dynamic field based on name and type
123
+ #
124
+ def signature
125
+ [@name, @type]
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,131 @@
1
+ module Sunspot
2
+ #
3
+ # This class presents a service for adding, updating, and removing data
4
+ # from the Solr index. An Indexer instance is associated with a particular
5
+ # setup, and thus is capable of indexing instances of a certain class (and its
6
+ # subclasses).
7
+ #
8
+ class Indexer #:nodoc:
9
+ include RSolr::Char
10
+
11
+ def initialize(connection)
12
+ @connection = connection
13
+ end
14
+
15
+ #
16
+ # Construct a representation of the model for indexing and send it to the
17
+ # connection for indexing
18
+ #
19
+ # ==== Parameters
20
+ #
21
+ # model<Object>:: the model to index
22
+ #
23
+ def add(model)
24
+ documents = Util.Array(model).map { |m| prepare(m) }
25
+ if @batch.nil?
26
+ add_documents(documents)
27
+ else
28
+ @batch.concat(documents)
29
+ end
30
+ end
31
+
32
+ #
33
+ # Remove the given model from the Solr index
34
+ #
35
+ def remove(*models)
36
+ @connection.delete_by_id(
37
+ models.map { |model| Adapters::InstanceAdapter.adapt(model).index_id }
38
+ )
39
+ end
40
+
41
+ #
42
+ # Remove the model from the Solr index by specifying the class and ID
43
+ #
44
+ def remove_by_id(class_name, *ids)
45
+ @connection.delete_by_id(
46
+ ids.map { |id| Adapters::InstanceAdapter.index_id_for(class_name, id) }
47
+ )
48
+ end
49
+
50
+ #
51
+ # Delete all documents of the class indexed by this indexer from Solr.
52
+ #
53
+ def remove_all(clazz = nil)
54
+ if clazz
55
+ @connection.delete_by_query("type:#{escape(clazz.name)}")
56
+ else
57
+ @connection.delete_by_query("*:*")
58
+ end
59
+ end
60
+
61
+ #
62
+ # Remove all documents that match the scope given in the Query
63
+ #
64
+ def remove_by_scope(scope)
65
+ @connection.delete_by_query(scope.to_boolean_phrase)
66
+ end
67
+
68
+ #
69
+ # Start batch processing
70
+ #
71
+ def start_batch
72
+ @batch = []
73
+ end
74
+
75
+ #
76
+ # Write batch out to Solr and clear it
77
+ #
78
+ def flush_batch
79
+ add_documents(@batch)
80
+ @batch = nil
81
+ end
82
+
83
+ private
84
+
85
+ #
86
+ # Convert documents into hash of indexed properties
87
+ #
88
+ def prepare(model)
89
+ document = document_for(model)
90
+ setup = setup_for(model)
91
+ if boost = setup.document_boost_for(model)
92
+ document.attrs[:boost] = boost
93
+ end
94
+ setup.all_field_factories.each do |field_factory|
95
+ field_factory.populate_document(document, model)
96
+ end
97
+ document
98
+ end
99
+
100
+ def add_documents(documents)
101
+ @connection.add(documents)
102
+ end
103
+
104
+ #
105
+ # All indexed documents index and store the +id+ and +type+ fields.
106
+ # This method constructs the document hash containing those key-value
107
+ # pairs.
108
+ #
109
+ def document_for(model)
110
+ RSolr::Message::Document.new(
111
+ :id => Adapters::InstanceAdapter.adapt(model).index_id,
112
+ :type => Util.superclasses_for(model.class).map { |clazz| clazz.name }
113
+ )
114
+ end
115
+
116
+ #
117
+ # Get the Setup object for the given object's class.
118
+ #
119
+ # ==== Parameters
120
+ #
121
+ # object<Object>:: The object whose setup is to be retrieved
122
+ #
123
+ # ==== Returns
124
+ #
125
+ # Sunspot::Setup:: The setup for the object's class
126
+ #
127
+ def setup_for(object)
128
+ Setup.for(object.class) || raise(NoSetupError, "Sunspot is not configured for #{object.class.inspect}")
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,45 @@
1
+ require 'fileutils'
2
+
3
+ module Sunspot
4
+ class Installer
5
+ class LibraryInstaller
6
+ class <<self
7
+ def execute(library_path, options)
8
+ new(library_path, options).execute
9
+ end
10
+ end
11
+
12
+ def initialize(library_path, options)
13
+ @library_path = library_path
14
+ @verbose = !!options[:verbose]
15
+ @force = !!options[:force]
16
+ end
17
+
18
+ def execute
19
+ sunspot_library_path = File.join(File.dirname(__FILE__), '..', '..',
20
+ '..', 'solr', 'solr', 'lib')
21
+ return if File.expand_path(sunspot_library_path) == File.expand_path(@library_path)
22
+ FileUtils.mkdir_p(@library_path)
23
+ Dir.glob(File.join(sunspot_library_path, '*.jar')).each do |jar|
24
+ jar = File.expand_path(jar)
25
+ dest = File.join(@library_path, File.basename(jar))
26
+ if File.exist?(dest)
27
+ if @force
28
+ say("Removing existing library #{dest}")
29
+ else
30
+ next
31
+ end
32
+ end
33
+ say("Copying #{jar} => #{dest}")
34
+ FileUtils.cp(jar, dest)
35
+ end
36
+ end
37
+
38
+ def say(message)
39
+ if @verbose
40
+ STDOUT.puts(message)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,219 @@
1
+ require 'yaml'
2
+ require 'rexml/rexml'
3
+ require 'rexml/document'
4
+ require 'fileutils'
5
+
6
+ module Sunspot
7
+ class Installer
8
+ #
9
+ # This class modifies an existing Solr schema.xml file to work with Sunspot.
10
+ # It makes the minimum necessary changes to the schema, adding fields and
11
+ # types only when they don't already exist. It also comments all fields and
12
+ # types that Sunspot needs, whether or not they were preexisting, so that
13
+ # users can modify the resulting schema without unwittingly breaking it.
14
+ #
15
+ class SchemaBuilder
16
+ include TaskHelper
17
+
18
+ CONFIG_PATH = File.join(
19
+ File.dirname(__FILE__), '..', '..', '..', 'installer', 'config', 'schema.yml'
20
+ )
21
+
22
+ class <<self
23
+ def execute(schema_path, options = {})
24
+ new(schema_path, options).execute
25
+ end
26
+
27
+ private :new
28
+ end
29
+
30
+ def initialize(schema_path, options = {})
31
+ @schema_path = schema_path
32
+ @config = File.open(CONFIG_PATH) { |f| YAML.load(f) }
33
+ @verbose = !!options[:verbose]
34
+ @force = !!options[:force]
35
+ end
36
+
37
+ def execute
38
+ if @force
39
+ FileUtils.mkdir_p(File.dirname(@schema_path))
40
+ source_path = File.join(File.dirname(__FILE__), '..', '..', '..', 'solr', 'solr', 'conf', 'schema.xml')
41
+ FileUtils.cp(source_path, @schema_path)
42
+ say("Copied default schema.xml to #{@schema_path}")
43
+ else
44
+ @document = File.open(@schema_path) do |f|
45
+ Nokogiri::XML(
46
+ f, nil, nil,
47
+ Nokogiri::XML::ParseOptions::DEFAULT_XML |
48
+ Nokogiri::XML::ParseOptions::NOBLANKS
49
+ )
50
+ end
51
+ @root = @document.root
52
+ @added_types = Set[]
53
+ add_fixed_fields
54
+ add_dynamic_fields
55
+ set_misc_settings
56
+ original_path = "#{@schema_path}.orig"
57
+ FileUtils.cp(@schema_path, original_path)
58
+ say("Saved backup of original to #{original_path}")
59
+ File.open(@schema_path, 'w') do |file|
60
+ @document.write_xml_to(file, :indent => 2)
61
+ end
62
+ say("Wrote schema to #{@schema_path}")
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def add_fixed_fields
69
+ @config['fixed'].each do |name, options|
70
+ maybe_add_field(name, options['type'], :indexed, *Array(options['attributes']))
71
+ end
72
+ end
73
+
74
+ def add_dynamic_fields
75
+ @config['types'].each do |type, options|
76
+ if suffix = options['suffix']
77
+ variant_combinations(options).each do |variants|
78
+ variants_suffix = variants.map { |variant| variant[1] || '' }.join
79
+ maybe_add_field("*_#{suffix}#{variants_suffix}", type, *variants.map { |variant| variant[0] })
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ def maybe_add_field(name, type, *flags)
86
+ node_name = name =~ /\*/ ? 'dynamicField' : 'field'
87
+ if field_node = fields_node.xpath(%Q(#{node_name}[@name="#{name}"])).first
88
+ say("Using existing #{node_name} #{name.inspect}")
89
+ add_comment(field_node)
90
+ return
91
+ end
92
+ maybe_add_type(type)
93
+ say("Adding field #{name.inspect}")
94
+ attributes = {
95
+ 'name' => name,
96
+ 'type' => type,
97
+ 'indexed' => 'true',
98
+ 'stored' => 'false',
99
+ 'multiValued' => 'false'
100
+ }
101
+ flags.each do |flag|
102
+ attributes[flag.to_s] = 'true'
103
+ end
104
+ field_node = add_element(fields_node, node_name, attributes)
105
+ add_comment(field_node)
106
+ end
107
+
108
+ def maybe_add_type(type)
109
+ unless @added_types.include?(type)
110
+ @added_types << type
111
+ if type_node = types_node.xpath(%Q(fieldType[@name="#{type}"])).first ||
112
+ types_node.xpath(%Q(fieldtype[@name="#{type}"])).first
113
+ say("Using existing type #{type.inspect}")
114
+ add_comment(type_node)
115
+ return
116
+ end
117
+ say("Adding type #{type.inspect}")
118
+ type_config = @config['types'][type]
119
+ type_node = add_element(
120
+ types_node,
121
+ 'fieldType',
122
+ 'name' => type,
123
+ 'class' => to_solr_class(type_config['class']),
124
+ 'omitNorms' => type_config['omit_norms'].nil? ? 'true' : type_config['omit_norms'].to_s
125
+ )
126
+ if type_config['tokenizer']
127
+ add_analyzer(type_node, type_config)
128
+ end
129
+ add_comment(type_node)
130
+ end
131
+ end
132
+
133
+ def add_analyzer(node, config)
134
+ analyzer_node = add_element(node, 'analyzer')
135
+ add_element(
136
+ analyzer_node,
137
+ 'tokenizer',
138
+ 'class' => to_solr_class(config['tokenizer'])
139
+ )
140
+ Array(config['filters']).each do |filter|
141
+ add_element(analyzer_node, 'filter', 'class' => to_solr_class(filter))
142
+ end
143
+ end
144
+
145
+ def set_misc_settings
146
+ if @root.xpath('uniqueKey[normalize-space()="id"]').any?
147
+ say('Unique key already set')
148
+ else
149
+ say("Creating unique key node")
150
+ add_element(@root, 'uniqueKey').content = 'id'
151
+ end
152
+ say('Setting default operator to AND')
153
+ solr_query_parser_node = @root.xpath('solrQueryParser').first ||
154
+ add_element(@root, 'solrQueryParser')
155
+ solr_query_parser_node['defaultOperator'] = 'AND'
156
+ end
157
+
158
+ def add_comment(node)
159
+ comment_message = " *** This #{node.name} is used by Sunspot! *** "
160
+ unless (comment = previous_non_text_sibling(node)) && comment.comment? && comment.content =~ /Sunspot/
161
+ node.add_previous_sibling(Nokogiri::XML::Comment.new(@document, comment_message))
162
+ end
163
+ end
164
+
165
+ def types_node
166
+ @types_node ||= @root.xpath('/schema/types').first
167
+ end
168
+
169
+ def fields_node
170
+ @fields_node ||= @root.xpath('/schema/fields').first
171
+ end
172
+
173
+ def to_solr_class(class_name)
174
+ if class_name =~ /\./
175
+ class_name
176
+ else
177
+ "solr.#{class_name}"
178
+ end
179
+ end
180
+
181
+ def previous_non_text_sibling(node)
182
+ if previous_sibling = node.previous_sibling
183
+ if previous_sibling.node_type == :text
184
+ previous_non_text_sibling(previous_sibling)
185
+ else
186
+ previous_sibling
187
+ end
188
+ end
189
+ end
190
+
191
+ #
192
+ # All of the possible combinations of variants
193
+ #
194
+ def variant_combinations(type_options)
195
+ invariants = type_options['invariants'] || {}
196
+ combinations = []
197
+ variants = @config['variants'].reject { |name, suffix| invariants.has_key?(name) }
198
+ 0.upto(2 ** variants.length - 1) do |b|
199
+ combinations << combination = []
200
+ variants.each_with_index do |variant, i|
201
+ combination << variant if b & 1<<i > 0
202
+ end
203
+ invariants.each do |name, value|
204
+ if value
205
+ combination << [name]
206
+ end
207
+ end
208
+ end
209
+ combinations
210
+ end
211
+
212
+ def say(message)
213
+ if @verbose
214
+ STDOUT.puts(message)
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end