ruben-sunspot 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (188) hide show
  1. data/Gemfile +9 -0
  2. data/Gemfile.lock +22 -0
  3. data/History.txt +198 -0
  4. data/LICENSE +18 -0
  5. data/README.rdoc +244 -0
  6. data/Rakefile +11 -0
  7. data/TODO +13 -0
  8. data/VERSION.yml +4 -0
  9. data/bin/sunspot-installer +19 -0
  10. data/bin/sunspot-solr +74 -0
  11. data/installer/config/schema.yml +95 -0
  12. data/lib/light_config.rb +40 -0
  13. data/lib/sunspot.rb +529 -0
  14. data/lib/sunspot/adapters.rb +265 -0
  15. data/lib/sunspot/composite_setup.rb +202 -0
  16. data/lib/sunspot/configuration.rb +46 -0
  17. data/lib/sunspot/data_extractor.rb +50 -0
  18. data/lib/sunspot/dsl.rb +5 -0
  19. data/lib/sunspot/dsl/adjustable.rb +47 -0
  20. data/lib/sunspot/dsl/field_query.rb +266 -0
  21. data/lib/sunspot/dsl/fields.rb +113 -0
  22. data/lib/sunspot/dsl/fulltext.rb +243 -0
  23. data/lib/sunspot/dsl/function.rb +14 -0
  24. data/lib/sunspot/dsl/functional.rb +41 -0
  25. data/lib/sunspot/dsl/more_like_this_query.rb +56 -0
  26. data/lib/sunspot/dsl/paginatable.rb +28 -0
  27. data/lib/sunspot/dsl/query_facet.rb +36 -0
  28. data/lib/sunspot/dsl/restriction.rb +25 -0
  29. data/lib/sunspot/dsl/scope.rb +229 -0
  30. data/lib/sunspot/dsl/search.rb +30 -0
  31. data/lib/sunspot/dsl/standard_query.rb +125 -0
  32. data/lib/sunspot/field.rb +192 -0
  33. data/lib/sunspot/field_factory.rb +147 -0
  34. data/lib/sunspot/indexer.rb +131 -0
  35. data/lib/sunspot/installer.rb +31 -0
  36. data/lib/sunspot/installer/library_installer.rb +45 -0
  37. data/lib/sunspot/installer/schema_builder.rb +219 -0
  38. data/lib/sunspot/installer/solrconfig_updater.rb +106 -0
  39. data/lib/sunspot/installer/task_helper.rb +18 -0
  40. data/lib/sunspot/query.rb +10 -0
  41. data/lib/sunspot/query/abstract_field_facet.rb +52 -0
  42. data/lib/sunspot/query/boost_query.rb +24 -0
  43. data/lib/sunspot/query/common_query.rb +85 -0
  44. data/lib/sunspot/query/composite_fulltext.rb +31 -0
  45. data/lib/sunspot/query/connective.rb +195 -0
  46. data/lib/sunspot/query/date_field_facet.rb +14 -0
  47. data/lib/sunspot/query/dismax.rb +126 -0
  48. data/lib/sunspot/query/field_facet.rb +41 -0
  49. data/lib/sunspot/query/filter.rb +38 -0
  50. data/lib/sunspot/query/function_query.rb +52 -0
  51. data/lib/sunspot/query/highlighting.rb +55 -0
  52. data/lib/sunspot/query/local.rb +26 -0
  53. data/lib/sunspot/query/more_like_this.rb +60 -0
  54. data/lib/sunspot/query/more_like_this_query.rb +12 -0
  55. data/lib/sunspot/query/pagination.rb +38 -0
  56. data/lib/sunspot/query/query_facet.rb +16 -0
  57. data/lib/sunspot/query/restriction.rb +265 -0
  58. data/lib/sunspot/query/scope.rb +16 -0
  59. data/lib/sunspot/query/sort.rb +95 -0
  60. data/lib/sunspot/query/sort_composite.rb +33 -0
  61. data/lib/sunspot/query/standard_query.rb +20 -0
  62. data/lib/sunspot/query/text_field_boost.rb +17 -0
  63. data/lib/sunspot/schema.rb +151 -0
  64. data/lib/sunspot/search.rb +9 -0
  65. data/lib/sunspot/search/abstract_search.rb +302 -0
  66. data/lib/sunspot/search/date_facet.rb +35 -0
  67. data/lib/sunspot/search/facet_row.rb +27 -0
  68. data/lib/sunspot/search/field_facet.rb +88 -0
  69. data/lib/sunspot/search/highlight.rb +38 -0
  70. data/lib/sunspot/search/hit.rb +136 -0
  71. data/lib/sunspot/search/more_like_this_search.rb +31 -0
  72. data/lib/sunspot/search/query_facet.rb +62 -0
  73. data/lib/sunspot/search/standard_search.rb +21 -0
  74. data/lib/sunspot/server.rb +152 -0
  75. data/lib/sunspot/session.rb +252 -0
  76. data/lib/sunspot/session_proxy.rb +71 -0
  77. data/lib/sunspot/session_proxy/abstract_session_proxy.rb +29 -0
  78. data/lib/sunspot/session_proxy/class_sharding_session_proxy.rb +66 -0
  79. data/lib/sunspot/session_proxy/id_sharding_session_proxy.rb +89 -0
  80. data/lib/sunspot/session_proxy/master_slave_session_proxy.rb +43 -0
  81. data/lib/sunspot/session_proxy/sharding_session_proxy.rb +215 -0
  82. data/lib/sunspot/session_proxy/thread_local_session_proxy.rb +37 -0
  83. data/lib/sunspot/setup.rb +360 -0
  84. data/lib/sunspot/text_field_setup.rb +29 -0
  85. data/lib/sunspot/type.rb +342 -0
  86. data/lib/sunspot/util.rb +253 -0
  87. data/lib/sunspot/version.rb +3 -0
  88. data/solr/etc/jetty.xml +214 -0
  89. data/solr/etc/webdefault.xml +379 -0
  90. data/solr/lib/jetty-6.1.3.jar +0 -0
  91. data/solr/lib/jetty-util-6.1.3.jar +0 -0
  92. data/solr/lib/jsp-2.1/ant-1.6.5.jar +0 -0
  93. data/solr/lib/jsp-2.1/core-3.1.1.jar +0 -0
  94. data/solr/lib/jsp-2.1/jsp-2.1.jar +0 -0
  95. data/solr/lib/jsp-2.1/jsp-api-2.1.jar +0 -0
  96. data/solr/lib/servlet-api-2.5-6.1.3.jar +0 -0
  97. data/solr/solr/conf/admin-extra.html +31 -0
  98. data/solr/solr/conf/elevate.xml +36 -0
  99. data/solr/solr/conf/mapping-ISOLatin1Accent.txt +246 -0
  100. data/solr/solr/conf/protwords.txt +21 -0
  101. data/solr/solr/conf/schema.xml +238 -0
  102. data/solr/solr/conf/scripts.conf +24 -0
  103. data/solr/solr/conf/solrconfig.xml +938 -0
  104. data/solr/solr/conf/spellings.txt +2 -0
  105. data/solr/solr/conf/stopwords.txt +58 -0
  106. data/solr/solr/conf/synonyms.txt +31 -0
  107. data/solr/solr/lib/lucene-spatial-2.9.1.jar +0 -0
  108. data/solr/solr/lib/solr-spatial-light-0.0.6.jar +0 -0
  109. data/solr/start.jar +0 -0
  110. data/solr/webapps/solr.war +0 -0
  111. data/spec/api/adapters_spec.rb +33 -0
  112. data/spec/api/binding_spec.rb +38 -0
  113. data/spec/api/indexer/attributes_spec.rb +149 -0
  114. data/spec/api/indexer/batch_spec.rb +46 -0
  115. data/spec/api/indexer/dynamic_fields_spec.rb +42 -0
  116. data/spec/api/indexer/fixed_fields_spec.rb +57 -0
  117. data/spec/api/indexer/fulltext_spec.rb +43 -0
  118. data/spec/api/indexer/removal_spec.rb +53 -0
  119. data/spec/api/indexer/spec_helper.rb +1 -0
  120. data/spec/api/indexer_spec.rb +14 -0
  121. data/spec/api/query/advanced_manipulation_examples.rb +35 -0
  122. data/spec/api/query/connectives_examples.rb +186 -0
  123. data/spec/api/query/dsl_spec.rb +18 -0
  124. data/spec/api/query/dynamic_fields_examples.rb +165 -0
  125. data/spec/api/query/faceting_examples.rb +399 -0
  126. data/spec/api/query/fulltext_examples.rb +315 -0
  127. data/spec/api/query/function_spec.rb +70 -0
  128. data/spec/api/query/highlighting_examples.rb +225 -0
  129. data/spec/api/query/local_examples.rb +38 -0
  130. data/spec/api/query/more_like_this_spec.rb +140 -0
  131. data/spec/api/query/ordering_pagination_examples.rb +97 -0
  132. data/spec/api/query/scope_examples.rb +263 -0
  133. data/spec/api/query/spec_helper.rb +1 -0
  134. data/spec/api/query/standard_spec.rb +28 -0
  135. data/spec/api/query/text_field_scoping_examples.rb +30 -0
  136. data/spec/api/query/types_spec.rb +20 -0
  137. data/spec/api/search/dynamic_fields_spec.rb +33 -0
  138. data/spec/api/search/faceting_spec.rb +356 -0
  139. data/spec/api/search/highlighting_spec.rb +69 -0
  140. data/spec/api/search/hits_spec.rb +149 -0
  141. data/spec/api/search/results_spec.rb +79 -0
  142. data/spec/api/search/search_spec.rb +23 -0
  143. data/spec/api/search/spec_helper.rb +1 -0
  144. data/spec/api/server_spec.rb +91 -0
  145. data/spec/api/session_proxy/class_sharding_session_proxy_spec.rb +85 -0
  146. data/spec/api/session_proxy/id_sharding_session_proxy_spec.rb +30 -0
  147. data/spec/api/session_proxy/master_slave_session_proxy_spec.rb +41 -0
  148. data/spec/api/session_proxy/sharding_session_proxy_spec.rb +77 -0
  149. data/spec/api/session_proxy/spec_helper.rb +9 -0
  150. data/spec/api/session_proxy/thread_local_session_proxy_spec.rb +50 -0
  151. data/spec/api/session_spec.rb +198 -0
  152. data/spec/api/spec_helper.rb +3 -0
  153. data/spec/api/sunspot_spec.rb +18 -0
  154. data/spec/ext.rb +11 -0
  155. data/spec/helpers/indexer_helper.rb +29 -0
  156. data/spec/helpers/query_helper.rb +38 -0
  157. data/spec/helpers/search_helper.rb +80 -0
  158. data/spec/integration/dynamic_fields_spec.rb +55 -0
  159. data/spec/integration/faceting_spec.rb +238 -0
  160. data/spec/integration/highlighting_spec.rb +22 -0
  161. data/spec/integration/indexing_spec.rb +33 -0
  162. data/spec/integration/keyword_search_spec.rb +317 -0
  163. data/spec/integration/local_search_spec.rb +91 -0
  164. data/spec/integration/more_like_this_spec.rb +43 -0
  165. data/spec/integration/scoped_search_spec.rb +349 -0
  166. data/spec/integration/spec_helper.rb +7 -0
  167. data/spec/integration/stored_fields_spec.rb +10 -0
  168. data/spec/integration/test_pagination.rb +32 -0
  169. data/spec/mocks/adapters.rb +32 -0
  170. data/spec/mocks/blog.rb +3 -0
  171. data/spec/mocks/comment.rb +21 -0
  172. data/spec/mocks/connection.rb +126 -0
  173. data/spec/mocks/mock_adapter.rb +30 -0
  174. data/spec/mocks/mock_class_sharding_session_proxy.rb +24 -0
  175. data/spec/mocks/mock_record.rb +52 -0
  176. data/spec/mocks/mock_sharding_session_proxy.rb +15 -0
  177. data/spec/mocks/photo.rb +12 -0
  178. data/spec/mocks/post.rb +76 -0
  179. data/spec/mocks/super_class.rb +2 -0
  180. data/spec/mocks/user.rb +8 -0
  181. data/spec/spec_helper.rb +52 -0
  182. data/tasks/gemspec.rake +33 -0
  183. data/tasks/rcov.rake +28 -0
  184. data/tasks/rdoc.rake +27 -0
  185. data/tasks/schema.rake +19 -0
  186. data/tasks/spec.rake +24 -0
  187. data/tasks/todo.rake +4 -0
  188. metadata +356 -0
@@ -0,0 +1,147 @@
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
+
129
+ class Coordinates
130
+ def initialize(name = nil, &block)
131
+ if block
132
+ @data_extractor = DataExtractor::BlockExtractor.new(&block)
133
+ else
134
+ @data_extractor = DataExtractor::AttributeExtractor.new(name)
135
+ end
136
+ end
137
+
138
+ def populate_document(document, model)
139
+ if coordinates = @data_extractor.value_for(model)
140
+ coordinates = Util::Coordinates.new(coordinates)
141
+ document.add_field(:lat, coordinates.lat)
142
+ document.add_field(:lng, coordinates.lng)
143
+ end
144
+ end
145
+ end
146
+ end
147
+ 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,31 @@
1
+ %w(task_helper library_installer schema_builder solrconfig_updater).each do |file|
2
+ require File.join(File.dirname(__FILE__), 'installer', file)
3
+ end
4
+
5
+ module Sunspot
6
+ class Installer
7
+ class <<self
8
+ def execute(solr_home, options = {})
9
+ new(solr_home, options).execute
10
+ end
11
+
12
+ private :new
13
+ end
14
+
15
+ def initialize(solr_home, options)
16
+ @solr_home, @options = solr_home, options
17
+ end
18
+
19
+ def execute
20
+ SchemaBuilder.execute(
21
+ File.join(@solr_home, 'conf', 'schema.xml'),
22
+ @options
23
+ )
24
+ SolrconfigUpdater.execute(
25
+ File.join(@solr_home, 'conf', 'solrconfig.xml'),
26
+ @options
27
+ )
28
+ LibraryInstaller.execute(File.join(@solr_home, 'lib'), @options)
29
+ end
30
+ end
31
+ 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