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.
- data/.gitignore +12 -0
- data/Gemfile +4 -0
- data/History.txt +222 -0
- data/LICENSE +18 -0
- data/Rakefile +17 -0
- data/TODO +13 -0
- data/VERSION.yml +4 -0
- data/bin/sunspot-installer +19 -0
- data/bin/sunspot-solr +74 -0
- data/installer/config/schema.yml +95 -0
- data/lib/light_config.rb +40 -0
- data/lib/sunspot/adapters.rb +265 -0
- data/lib/sunspot/composite_setup.rb +202 -0
- data/lib/sunspot/configuration.rb +46 -0
- data/lib/sunspot/data_extractor.rb +50 -0
- data/lib/sunspot/dsl/adjustable.rb +47 -0
- data/lib/sunspot/dsl/field_query.rb +279 -0
- data/lib/sunspot/dsl/fields.rb +103 -0
- data/lib/sunspot/dsl/fulltext.rb +243 -0
- data/lib/sunspot/dsl/function.rb +14 -0
- data/lib/sunspot/dsl/functional.rb +44 -0
- data/lib/sunspot/dsl/more_like_this_query.rb +56 -0
- data/lib/sunspot/dsl/paginatable.rb +28 -0
- data/lib/sunspot/dsl/query_facet.rb +36 -0
- data/lib/sunspot/dsl/restriction.rb +25 -0
- data/lib/sunspot/dsl/restriction_with_near.rb +121 -0
- data/lib/sunspot/dsl/scope.rb +217 -0
- data/lib/sunspot/dsl/search.rb +30 -0
- data/lib/sunspot/dsl/standard_query.rb +121 -0
- data/lib/sunspot/dsl.rb +5 -0
- data/lib/sunspot/field.rb +193 -0
- data/lib/sunspot/field_factory.rb +129 -0
- data/lib/sunspot/indexer.rb +131 -0
- data/lib/sunspot/installer/library_installer.rb +45 -0
- data/lib/sunspot/installer/schema_builder.rb +219 -0
- data/lib/sunspot/installer/solrconfig_updater.rb +76 -0
- data/lib/sunspot/installer/task_helper.rb +18 -0
- data/lib/sunspot/installer.rb +31 -0
- data/lib/sunspot/query/abstract_field_facet.rb +52 -0
- data/lib/sunspot/query/boost_query.rb +24 -0
- data/lib/sunspot/query/common_query.rb +85 -0
- data/lib/sunspot/query/composite_fulltext.rb +36 -0
- data/lib/sunspot/query/connective.rb +206 -0
- data/lib/sunspot/query/date_field_facet.rb +14 -0
- data/lib/sunspot/query/dismax.rb +128 -0
- data/lib/sunspot/query/field_facet.rb +41 -0
- data/lib/sunspot/query/filter.rb +38 -0
- data/lib/sunspot/query/function_query.rb +52 -0
- data/lib/sunspot/query/geo.rb +53 -0
- data/lib/sunspot/query/highlighting.rb +55 -0
- data/lib/sunspot/query/more_like_this.rb +61 -0
- data/lib/sunspot/query/more_like_this_query.rb +12 -0
- data/lib/sunspot/query/pagination.rb +38 -0
- data/lib/sunspot/query/query_facet.rb +16 -0
- data/lib/sunspot/query/restriction.rb +262 -0
- data/lib/sunspot/query/scope.rb +9 -0
- data/lib/sunspot/query/sort.rb +95 -0
- data/lib/sunspot/query/sort_composite.rb +33 -0
- data/lib/sunspot/query/standard_query.rb +16 -0
- data/lib/sunspot/query/text_field_boost.rb +17 -0
- data/lib/sunspot/query.rb +11 -0
- data/lib/sunspot/schema.rb +151 -0
- data/lib/sunspot/search/abstract_search.rb +293 -0
- data/lib/sunspot/search/date_facet.rb +35 -0
- data/lib/sunspot/search/facet_row.rb +27 -0
- data/lib/sunspot/search/field_facet.rb +88 -0
- data/lib/sunspot/search/highlight.rb +38 -0
- data/lib/sunspot/search/hit.rb +136 -0
- data/lib/sunspot/search/more_like_this_search.rb +31 -0
- data/lib/sunspot/search/paginated_collection.rb +55 -0
- data/lib/sunspot/search/query_facet.rb +67 -0
- data/lib/sunspot/search/standard_search.rb +21 -0
- data/lib/sunspot/search.rb +9 -0
- data/lib/sunspot/server.rb +152 -0
- data/lib/sunspot/session.rb +260 -0
- data/lib/sunspot/session_proxy/abstract_session_proxy.rb +29 -0
- data/lib/sunspot/session_proxy/class_sharding_session_proxy.rb +66 -0
- data/lib/sunspot/session_proxy/id_sharding_session_proxy.rb +89 -0
- data/lib/sunspot/session_proxy/master_slave_session_proxy.rb +43 -0
- data/lib/sunspot/session_proxy/sharding_session_proxy.rb +222 -0
- data/lib/sunspot/session_proxy/silent_fail_session_proxy.rb +42 -0
- data/lib/sunspot/session_proxy/thread_local_session_proxy.rb +37 -0
- data/lib/sunspot/session_proxy.rb +87 -0
- data/lib/sunspot/setup.rb +350 -0
- data/lib/sunspot/text_field_setup.rb +29 -0
- data/lib/sunspot/type.rb +372 -0
- data/lib/sunspot/util.rb +243 -0
- data/lib/sunspot/version.rb +3 -0
- data/lib/sunspot.rb +569 -0
- data/lib/sunspot_rbg.rb +7 -0
- data/log/.gitignore +1 -0
- data/pkg/.gitignore +1 -0
- data/script/console +10 -0
- data/solr/README.txt +42 -0
- data/solr/etc/jetty.xml +218 -0
- data/solr/etc/webdefault.xml +379 -0
- data/solr/lib/jetty-6.1.3.jar +0 -0
- data/solr/lib/jetty-util-6.1.3.jar +0 -0
- data/solr/lib/jsp-2.1/ant-1.6.5.jar +0 -0
- data/solr/lib/jsp-2.1/core-3.1.1.jar +0 -0
- data/solr/lib/jsp-2.1/jsp-2.1.jar +0 -0
- data/solr/lib/jsp-2.1/jsp-api-2.1.jar +0 -0
- data/solr/lib/servlet-api-2.5-6.1.3.jar +0 -0
- data/solr/logs/.gitignore +1 -0
- data/solr/solr/.gitignore +1 -0
- data/solr/solr/README.txt +54 -0
- data/solr/solr/conf/admin-extra.html +31 -0
- data/solr/solr/conf/elevate.xml +36 -0
- data/solr/solr/conf/mapping-ISOLatin1Accent.txt +246 -0
- data/solr/solr/conf/protwords.txt +21 -0
- data/solr/solr/conf/schema.xml +238 -0
- data/solr/solr/conf/scripts.conf +24 -0
- data/solr/solr/conf/solrconfig.xml +934 -0
- data/solr/solr/conf/spellings.txt +2 -0
- data/solr/solr/conf/stopwords.txt +58 -0
- data/solr/solr/conf/synonyms.txt +31 -0
- data/solr/solr/conf/xslt/example.xsl +132 -0
- data/solr/solr/conf/xslt/example_atom.xsl +67 -0
- data/solr/solr/conf/xslt/example_rss.xsl +66 -0
- data/solr/solr/conf/xslt/luke.xsl +337 -0
- data/solr/start.jar +0 -0
- data/solr/webapps/solr.war +0 -0
- data/solr-1.3/etc/jetty.xml +212 -0
- data/solr-1.3/etc/webdefault.xml +379 -0
- data/solr-1.3/lib/jetty-6.1.3.jar +0 -0
- data/solr-1.3/lib/jetty-util-6.1.3.jar +0 -0
- data/solr-1.3/lib/jsp-2.1/ant-1.6.5.jar +0 -0
- data/solr-1.3/lib/jsp-2.1/core-3.1.1.jar +0 -0
- data/solr-1.3/lib/jsp-2.1/jsp-2.1.jar +0 -0
- data/solr-1.3/lib/jsp-2.1/jsp-api-2.1.jar +0 -0
- data/solr-1.3/lib/servlet-api-2.5-6.1.3.jar +0 -0
- data/solr-1.3/solr/conf/elevate.xml +36 -0
- data/solr-1.3/solr/conf/protwords.txt +21 -0
- data/solr-1.3/solr/conf/schema.xml +64 -0
- data/solr-1.3/solr/conf/solrconfig.xml +725 -0
- data/solr-1.3/solr/conf/stopwords.txt +57 -0
- data/solr-1.3/solr/conf/synonyms.txt +31 -0
- data/solr-1.3/solr/lib/geoapi-nogenerics-2.1-M2.jar +0 -0
- data/solr-1.3/solr/lib/gt2-referencing-2.3.1.jar +0 -0
- data/solr-1.3/solr/lib/jsr108-0.01.jar +0 -0
- data/solr-1.3/solr/lib/locallucene.jar +0 -0
- data/solr-1.3/solr/lib/localsolr.jar +0 -0
- data/solr-1.3/start.jar +0 -0
- data/solr-1.3/webapps/solr.war +0 -0
- data/spec/api/adapters_spec.rb +33 -0
- data/spec/api/binding_spec.rb +50 -0
- data/spec/api/indexer/attributes_spec.rb +149 -0
- data/spec/api/indexer/batch_spec.rb +46 -0
- data/spec/api/indexer/dynamic_fields_spec.rb +42 -0
- data/spec/api/indexer/fixed_fields_spec.rb +57 -0
- data/spec/api/indexer/fulltext_spec.rb +43 -0
- data/spec/api/indexer/removal_spec.rb +53 -0
- data/spec/api/indexer/spec_helper.rb +1 -0
- data/spec/api/indexer_spec.rb +14 -0
- data/spec/api/query/advanced_manipulation_examples.rb +35 -0
- data/spec/api/query/connectives_examples.rb +189 -0
- data/spec/api/query/dsl_spec.rb +18 -0
- data/spec/api/query/dynamic_fields_examples.rb +165 -0
- data/spec/api/query/faceting_examples.rb +397 -0
- data/spec/api/query/fulltext_examples.rb +313 -0
- data/spec/api/query/function_spec.rb +70 -0
- data/spec/api/query/geo_examples.rb +68 -0
- data/spec/api/query/highlighting_examples.rb +223 -0
- data/spec/api/query/more_like_this_spec.rb +140 -0
- data/spec/api/query/ordering_pagination_examples.rb +95 -0
- data/spec/api/query/scope_examples.rb +275 -0
- data/spec/api/query/spec_helper.rb +1 -0
- data/spec/api/query/standard_spec.rb +28 -0
- data/spec/api/query/text_field_scoping_examples.rb +30 -0
- data/spec/api/query/types_spec.rb +20 -0
- data/spec/api/search/dynamic_fields_spec.rb +33 -0
- data/spec/api/search/faceting_spec.rb +360 -0
- data/spec/api/search/highlighting_spec.rb +69 -0
- data/spec/api/search/hits_spec.rb +120 -0
- data/spec/api/search/paginated_collection_spec.rb +26 -0
- data/spec/api/search/results_spec.rb +66 -0
- data/spec/api/search/search_spec.rb +23 -0
- data/spec/api/search/spec_helper.rb +1 -0
- data/spec/api/server_spec.rb +91 -0
- data/spec/api/session_proxy/class_sharding_session_proxy_spec.rb +85 -0
- data/spec/api/session_proxy/id_sharding_session_proxy_spec.rb +30 -0
- data/spec/api/session_proxy/master_slave_session_proxy_spec.rb +41 -0
- data/spec/api/session_proxy/sharding_session_proxy_spec.rb +77 -0
- data/spec/api/session_proxy/silent_fail_session_proxy_spec.rb +24 -0
- data/spec/api/session_proxy/spec_helper.rb +9 -0
- data/spec/api/session_proxy/thread_local_session_proxy_spec.rb +50 -0
- data/spec/api/session_spec.rb +220 -0
- data/spec/api/spec_helper.rb +3 -0
- data/spec/api/sunspot_spec.rb +18 -0
- data/spec/ext.rb +11 -0
- data/spec/helpers/indexer_helper.rb +29 -0
- data/spec/helpers/query_helper.rb +38 -0
- data/spec/helpers/search_helper.rb +80 -0
- data/spec/integration/dynamic_fields_spec.rb +55 -0
- data/spec/integration/faceting_spec.rb +238 -0
- data/spec/integration/highlighting_spec.rb +22 -0
- data/spec/integration/indexing_spec.rb +33 -0
- data/spec/integration/keyword_search_spec.rb +317 -0
- data/spec/integration/local_search_spec.rb +64 -0
- data/spec/integration/more_like_this_spec.rb +43 -0
- data/spec/integration/scoped_search_spec.rb +354 -0
- data/spec/integration/spec_helper.rb +7 -0
- data/spec/integration/stored_fields_spec.rb +10 -0
- data/spec/integration/test_pagination.rb +32 -0
- data/spec/mocks/adapters.rb +32 -0
- data/spec/mocks/blog.rb +3 -0
- data/spec/mocks/comment.rb +21 -0
- data/spec/mocks/connection.rb +126 -0
- data/spec/mocks/mock_adapter.rb +30 -0
- data/spec/mocks/mock_class_sharding_session_proxy.rb +24 -0
- data/spec/mocks/mock_record.rb +52 -0
- data/spec/mocks/mock_sharding_session_proxy.rb +15 -0
- data/spec/mocks/photo.rb +11 -0
- data/spec/mocks/post.rb +85 -0
- data/spec/mocks/super_class.rb +2 -0
- data/spec/mocks/user.rb +13 -0
- data/spec/spec_helper.rb +30 -0
- data/sunspot.gemspec +40 -0
- data/tasks/rdoc.rake +27 -0
- data/tasks/schema.rake +19 -0
- data/tasks/todo.rake +4 -0
- 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
|