gojee-sunspot 2.0.3 → 2.0.4
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 +5 -0
- data/History.txt +252 -0
- data/LICENSE +18 -0
- data/Rakefile +13 -0
- data/TODO +13 -0
- data/lib/light_config.rb +40 -0
- data/lib/sunspot/adapters.rb +265 -0
- data/lib/sunspot/batcher.rb +62 -0
- data/lib/sunspot/class_set.rb +23 -0
- data/lib/sunspot/composite_setup.rb +202 -0
- data/lib/sunspot/configuration.rb +53 -0
- data/lib/sunspot/data_extractor.rb +50 -0
- data/lib/sunspot/dsl/adjustable.rb +47 -0
- data/lib/sunspot/dsl/field_group.rb +57 -0
- data/lib/sunspot/dsl/field_query.rb +327 -0
- data/lib/sunspot/dsl/fields.rb +103 -0
- data/lib/sunspot/dsl/fulltext.rb +243 -0
- data/lib/sunspot/dsl/function.rb +27 -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 +32 -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 +160 -0
- data/lib/sunspot/dsl/scope.rb +217 -0
- data/lib/sunspot/dsl/search.rb +30 -0
- data/lib/sunspot/dsl/standard_query.rb +123 -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 +136 -0
- data/lib/sunspot/query/abstract_field_facet.rb +52 -0
- data/lib/sunspot/query/bbox.rb +15 -0
- data/lib/sunspot/query/boost_query.rb +24 -0
- data/lib/sunspot/query/common_query.rb +96 -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 +132 -0
- data/lib/sunspot/query/field_facet.rb +41 -0
- data/lib/sunspot/query/field_group.rb +36 -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/geofilt.rb +16 -0
- data/lib/sunspot/query/highlighting.rb +62 -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 +42 -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 +109 -0
- data/lib/sunspot/query/sort_composite.rb +34 -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 +281 -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/field_group.rb +32 -0
- data/lib/sunspot/search/group.rb +50 -0
- data/lib/sunspot/search/highlight.rb +38 -0
- data/lib/sunspot/search/hit.rb +150 -0
- data/lib/sunspot/search/hit_enumerable.rb +72 -0
- data/lib/sunspot/search/more_like_this_search.rb +31 -0
- data/lib/sunspot/search/paginated_collection.rb +57 -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/session.rb +262 -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/multicore_session_proxy.rb +67 -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 +95 -0
- data/lib/sunspot/setup.rb +350 -0
- data/lib/sunspot/text_field_setup.rb +29 -0
- data/lib/sunspot/type.rb +393 -0
- data/lib/sunspot/util.rb +252 -0
- data/lib/sunspot/version.rb +3 -0
- data/lib/sunspot.rb +579 -0
- data/log/.gitignore +1 -0
- data/pkg/.gitignore +1 -0
- data/script/console +10 -0
- data/spec/api/adapters_spec.rb +33 -0
- data/spec/api/batcher_spec.rb +112 -0
- data/spec/api/binding_spec.rb +50 -0
- data/spec/api/class_set_spec.rb +24 -0
- data/spec/api/hit_enumerable_spec.rb +47 -0
- data/spec/api/indexer/attributes_spec.rb +149 -0
- data/spec/api/indexer/batch_spec.rb +72 -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 +79 -0
- data/spec/api/query/geo_examples.rb +68 -0
- data/spec/api/query/group_spec.rb +32 -0
- data/spec/api/query/highlighting_examples.rb +245 -0
- data/spec/api/query/more_like_this_spec.rb +140 -0
- data/spec/api/query/ordering_pagination_examples.rb +116 -0
- data/spec/api/query/scope_examples.rb +275 -0
- data/spec/api/query/spatial_examples.rb +27 -0
- data/spec/api/query/spec_helper.rb +1 -0
- data/spec/api/query/standard_spec.rb +29 -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 +131 -0
- data/spec/api/search/paginated_collection_spec.rb +36 -0
- data/spec/api/search/results_spec.rb +72 -0
- data/spec/api/search/search_spec.rb +23 -0
- data/spec/api/search/spec_helper.rb +1 -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 +39 -0
- data/spec/api/session_spec.rb +232 -0
- data/spec/api/spec_helper.rb +3 -0
- data/spec/api/sunspot_spec.rb +29 -0
- data/spec/ext.rb +11 -0
- data/spec/helpers/indexer_helper.rb +17 -0
- data/spec/helpers/integration_helper.rb +8 -0
- data/spec/helpers/mock_session_helper.rb +13 -0
- data/spec/helpers/query_helper.rb +26 -0
- data/spec/helpers/search_helper.rb +68 -0
- data/spec/integration/dynamic_fields_spec.rb +57 -0
- data/spec/integration/faceting_spec.rb +251 -0
- data/spec/integration/field_grouping_spec.rb +66 -0
- data/spec/integration/geospatial_spec.rb +85 -0
- data/spec/integration/highlighting_spec.rb +44 -0
- data/spec/integration/indexing_spec.rb +55 -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/stored_fields_spec.rb +12 -0
- data/spec/integration/test_pagination.rb +43 -0
- data/spec/integration/unicode_spec.rb +15 -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 +86 -0
- data/spec/mocks/super_class.rb +2 -0
- data/spec/mocks/user.rb +13 -0
- data/spec/spec_helper.rb +40 -0
- data/sunspot.gemspec +42 -0
- data/tasks/rdoc.rake +27 -0
- data/tasks/schema.rake +19 -0
- data/tasks/todo.rake +4 -0
- metadata +261 -3
@@ -0,0 +1,123 @@
|
|
1
|
+
module Sunspot
|
2
|
+
module DSL #:nodoc:
|
3
|
+
#
|
4
|
+
# This class presents a DSL for constructing queries using the
|
5
|
+
# Sunspot.search method. Methods of this class are available inside the
|
6
|
+
# search block. Much of the DSL's functionality is implemented by this
|
7
|
+
# class's superclasses, Sunspot::DSL::FieldQuery and Sunspot::DSL::Scope
|
8
|
+
#
|
9
|
+
# See Sunspot.search for usage examples
|
10
|
+
#
|
11
|
+
class StandardQuery < FieldQuery
|
12
|
+
include Paginatable, Adjustable
|
13
|
+
|
14
|
+
# Specify a phrase that should be searched as fulltext. Only +text+
|
15
|
+
# fields are searched - see DSL::Fields.text
|
16
|
+
#
|
17
|
+
# Keyword search is executed using Solr's dismax handler, which strikes
|
18
|
+
# a good balance between powerful and foolproof. In particular,
|
19
|
+
# well-matched quotation marks can be used to group phrases, and the
|
20
|
+
# + and - modifiers work as expected. All other special Solr boolean
|
21
|
+
# syntax is escaped, and mismatched quotes are ignored entirely.
|
22
|
+
#
|
23
|
+
# This method can optionally take a block, which is evaluated by the
|
24
|
+
# Fulltext DSL class, and exposes several powerful dismax features.
|
25
|
+
#
|
26
|
+
# ==== Parameters
|
27
|
+
#
|
28
|
+
# keywords<String>:: phrase to perform fulltext search on
|
29
|
+
#
|
30
|
+
# ==== Options
|
31
|
+
#
|
32
|
+
# :fields<Array>::
|
33
|
+
# List of fields that should be searched for keywords. Defaults to all
|
34
|
+
# fields configured for the types under search.
|
35
|
+
# :highlight<Boolean,Array>::
|
36
|
+
# If true, perform keyword highlighting on all searched fields. If an
|
37
|
+
# array of field names, perform highlighting on the specified fields.
|
38
|
+
# Note that for highlighting to work, the desired fields have to be set
|
39
|
+
# up with :stored => true.
|
40
|
+
# This can also be called from within the fulltext block.
|
41
|
+
# :minimum_match<Integer>::
|
42
|
+
# The minimum number of search terms that a result must match. By
|
43
|
+
# default, all search terms must match; if the number of search terms
|
44
|
+
# is less than this number, the default behavior applies.
|
45
|
+
# :tie<Float>::
|
46
|
+
# A tiebreaker coefficient for scores derived from subqueries that are
|
47
|
+
# lower-scoring than the maximum score subquery. Typically a near-zero
|
48
|
+
# value is useful. See
|
49
|
+
# http://wiki.apache.org/solr/DisMaxRequestHandler#tie_.28Tie_breaker.29
|
50
|
+
# for more information.
|
51
|
+
# :query_phrase_slop<Integer>::
|
52
|
+
# The number of words that can appear between the words in a
|
53
|
+
# user-entered phrase (i.e., keywords in quotes) and still match. For
|
54
|
+
# instance, in a search for "\"great pizza\"" with a phrase slop of 1,
|
55
|
+
# "great pizza" and "great big pizza" will match, but "great monster of
|
56
|
+
# a pizza" will not. Default behavior is a query phrase slop of zero.
|
57
|
+
#
|
58
|
+
def fulltext(keywords, options = {}, &block)
|
59
|
+
if keywords && !(keywords.to_s =~ /^\s*$/)
|
60
|
+
fulltext_query = @query.add_fulltext(keywords)
|
61
|
+
if field_names = options.delete(:fields)
|
62
|
+
Util.Array(field_names).each do |field_name|
|
63
|
+
@setup.text_fields(field_name).each do |field|
|
64
|
+
fulltext_query.add_fulltext_field(field, field.default_boost)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
if minimum_match = options.delete(:minimum_match)
|
69
|
+
fulltext_query.minimum_match = minimum_match.to_i
|
70
|
+
end
|
71
|
+
if tie = options.delete(:tie)
|
72
|
+
fulltext_query.tie = tie.to_f
|
73
|
+
end
|
74
|
+
if query_phrase_slop = options.delete(:query_phrase_slop)
|
75
|
+
fulltext_query.query_phrase_slop = query_phrase_slop.to_i
|
76
|
+
end
|
77
|
+
if highlight_field_names = options.delete(:highlight)
|
78
|
+
if highlight_field_names == true
|
79
|
+
fulltext_query.add_highlight
|
80
|
+
else
|
81
|
+
highlight_fields = []
|
82
|
+
Util.Array(highlight_field_names).each do |field_name|
|
83
|
+
highlight_fields.concat(@setup.text_fields(field_name))
|
84
|
+
end
|
85
|
+
fulltext_query.add_highlight(highlight_fields)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
if block && fulltext_query
|
89
|
+
fulltext_dsl = Fulltext.new(fulltext_query, @setup)
|
90
|
+
Util.instance_eval_or_call(
|
91
|
+
fulltext_dsl,
|
92
|
+
&block
|
93
|
+
)
|
94
|
+
end
|
95
|
+
if !field_names && (!fulltext_dsl || !fulltext_dsl.fields_added?)
|
96
|
+
@setup.all_text_fields.each do |field|
|
97
|
+
unless fulltext_query.has_fulltext_field?(field)
|
98
|
+
unless fulltext_dsl && fulltext_dsl.exclude_fields.include?(field.name)
|
99
|
+
fulltext_query.add_fulltext_field(field, field.default_boost)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
alias_method :keywords, :fulltext
|
107
|
+
|
108
|
+
def with(*args)
|
109
|
+
case args.first
|
110
|
+
when String, Symbol
|
111
|
+
field_name = args[0]
|
112
|
+
value = args.length > 1 ? args[1] : Scope::NONE
|
113
|
+
if value == Scope::NONE
|
114
|
+
return DSL::RestrictionWithNear.new(@setup.field(field_name.to_sym), @scope, @query, false)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# else
|
119
|
+
super
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
data/lib/sunspot/dsl.rb
ADDED
@@ -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,136 @@
|
|
1
|
+
require 'sunspot/batcher'
|
2
|
+
|
3
|
+
module Sunspot
|
4
|
+
#
|
5
|
+
# This class presents a service for adding, updating, and removing data
|
6
|
+
# from the Solr index. An Indexer instance is associated with a particular
|
7
|
+
# setup, and thus is capable of indexing instances of a certain class (and its
|
8
|
+
# subclasses).
|
9
|
+
#
|
10
|
+
class Indexer #:nodoc:
|
11
|
+
include RSolr::Char
|
12
|
+
|
13
|
+
def initialize(connection)
|
14
|
+
@connection = connection
|
15
|
+
end
|
16
|
+
|
17
|
+
#
|
18
|
+
# Construct a representation of the model for indexing and send it to the
|
19
|
+
# connection for indexing
|
20
|
+
#
|
21
|
+
# ==== Parameters
|
22
|
+
#
|
23
|
+
# model<Object>:: the model to index
|
24
|
+
#
|
25
|
+
def add(model)
|
26
|
+
documents = Util.Array(model).map { |m| prepare(m) }
|
27
|
+
if batcher.batching?
|
28
|
+
batcher.concat(documents)
|
29
|
+
else
|
30
|
+
add_documents(documents)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
#
|
35
|
+
# Remove the given model from the Solr index
|
36
|
+
#
|
37
|
+
def remove(*models)
|
38
|
+
@connection.delete_by_id(
|
39
|
+
models.map { |model| Adapters::InstanceAdapter.adapt(model).index_id }
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
# Remove the model from the Solr index by specifying the class and ID
|
45
|
+
#
|
46
|
+
def remove_by_id(class_name, *ids)
|
47
|
+
@connection.delete_by_id(
|
48
|
+
ids.map { |id| Adapters::InstanceAdapter.index_id_for(class_name, id) }
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
#
|
53
|
+
# Delete all documents of the class indexed by this indexer from Solr.
|
54
|
+
#
|
55
|
+
def remove_all(clazz = nil)
|
56
|
+
if clazz
|
57
|
+
@connection.delete_by_query("type:#{escape(clazz.name)}")
|
58
|
+
else
|
59
|
+
@connection.delete_by_query("*:*")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
#
|
64
|
+
# Remove all documents that match the scope given in the Query
|
65
|
+
#
|
66
|
+
def remove_by_scope(scope)
|
67
|
+
@connection.delete_by_query(scope.to_boolean_phrase)
|
68
|
+
end
|
69
|
+
|
70
|
+
#
|
71
|
+
# Start batch processing
|
72
|
+
#
|
73
|
+
def start_batch
|
74
|
+
batcher.start_new
|
75
|
+
end
|
76
|
+
|
77
|
+
#
|
78
|
+
# Write batch out to Solr and clear it
|
79
|
+
#
|
80
|
+
def flush_batch
|
81
|
+
add_documents(batcher.end_current)
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def batcher
|
87
|
+
@batcher ||= Batcher.new
|
88
|
+
end
|
89
|
+
|
90
|
+
#
|
91
|
+
# Convert documents into hash of indexed properties
|
92
|
+
#
|
93
|
+
def prepare(model)
|
94
|
+
document = document_for(model)
|
95
|
+
setup = setup_for(model)
|
96
|
+
if boost = setup.document_boost_for(model)
|
97
|
+
document.attrs[:boost] = boost
|
98
|
+
end
|
99
|
+
setup.all_field_factories.each do |field_factory|
|
100
|
+
field_factory.populate_document(document, model)
|
101
|
+
end
|
102
|
+
document
|
103
|
+
end
|
104
|
+
|
105
|
+
def add_documents(documents)
|
106
|
+
@connection.add(documents)
|
107
|
+
end
|
108
|
+
|
109
|
+
#
|
110
|
+
# All indexed documents index and store the +id+ and +type+ fields.
|
111
|
+
# This method constructs the document hash containing those key-value
|
112
|
+
# pairs.
|
113
|
+
#
|
114
|
+
def document_for(model)
|
115
|
+
RSolr::Xml::Document.new(
|
116
|
+
:id => Adapters::InstanceAdapter.adapt(model).index_id,
|
117
|
+
:type => Util.superclasses_for(model.class).map { |clazz| clazz.name }
|
118
|
+
)
|
119
|
+
end
|
120
|
+
|
121
|
+
#
|
122
|
+
# Get the Setup object for the given object's class.
|
123
|
+
#
|
124
|
+
# ==== Parameters
|
125
|
+
#
|
126
|
+
# object<Object>:: The object whose setup is to be retrieved
|
127
|
+
#
|
128
|
+
# ==== Returns
|
129
|
+
#
|
130
|
+
# Sunspot::Setup:: The setup for the object's class
|
131
|
+
#
|
132
|
+
def setup_for(object)
|
133
|
+
Setup.for(object.class) || raise(NoSetupError, "Sunspot is not configured for #{object.class.inspect}")
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Sunspot
|
2
|
+
module Query
|
3
|
+
class AbstractFieldFacet
|
4
|
+
include RSolr::Char
|
5
|
+
|
6
|
+
def initialize(field, options)
|
7
|
+
@field, @options = field, options
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_params
|
11
|
+
params = {
|
12
|
+
:facet => 'true',
|
13
|
+
}
|
14
|
+
case @options[:sort]
|
15
|
+
when :count
|
16
|
+
params[qualified_param('sort')] = 'true'
|
17
|
+
when :index
|
18
|
+
params[qualified_param('sort')] = 'false'
|
19
|
+
when nil
|
20
|
+
else
|
21
|
+
raise(
|
22
|
+
ArgumentError,
|
23
|
+
"#{@options[:sort].inspect} is not an allowed value for :sort. Allowed options are :count and :index"
|
24
|
+
)
|
25
|
+
end
|
26
|
+
if @options[:limit]
|
27
|
+
params[qualified_param('limit')] = @options[:limit].to_i
|
28
|
+
end
|
29
|
+
if @options[:prefix]
|
30
|
+
params[qualified_param('prefix')] = escape(@options[:prefix].to_s)
|
31
|
+
end
|
32
|
+
params[qualified_param('mincount')] =
|
33
|
+
case
|
34
|
+
when @options[:minimum_count] then @options[:minimum_count].to_i
|
35
|
+
when @options[:zeros] then 0
|
36
|
+
else 1
|
37
|
+
end
|
38
|
+
params
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def qualified_param(param)
|
44
|
+
:"f.#{key}.facet.#{param}"
|
45
|
+
end
|
46
|
+
|
47
|
+
def key
|
48
|
+
@key ||= @options[:name] || @field.indexed_name
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Sunspot
|
2
|
+
module Query
|
3
|
+
class Bbox
|
4
|
+
def initialize(field, first_corner, second_corner)
|
5
|
+
@field, @first_corner, @second_corner = field, first_corner, second_corner
|
6
|
+
end
|
7
|
+
|
8
|
+
def to_params
|
9
|
+
filter = "#{@field.indexed_name}:[#{@first_corner.join(",")} TO #{@second_corner.join(",")}]"
|
10
|
+
|
11
|
+
{:fq => filter}
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Sunspot
|
2
|
+
module Query
|
3
|
+
#
|
4
|
+
# Representation of a BoostQuery, which allows the searcher to specify a
|
5
|
+
# scope for which matching documents should have an extra boost. This is
|
6
|
+
# essentially a conjunction, with an extra instance variable containing
|
7
|
+
# the boost that should be applied.
|
8
|
+
#
|
9
|
+
class BoostQuery < Connective::Conjunction #:nodoc:
|
10
|
+
def initialize(boost)
|
11
|
+
super(false)
|
12
|
+
@boost = boost
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_boolean_phrase
|
16
|
+
if @boost.is_a?(FunctionQuery)
|
17
|
+
"#{@boost}"
|
18
|
+
else
|
19
|
+
"#{super}^#{@boost}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|