sunspot 1.1.0 → 1.2.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/Gemfile +10 -0
- data/Gemfile.lock +32 -0
- data/History.txt +24 -0
- data/README.rdoc +18 -5
- data/lib/sunspot.rb +40 -0
- data/lib/sunspot/dsl.rb +2 -2
- data/lib/sunspot/dsl/field_query.rb +2 -2
- data/lib/sunspot/dsl/fields.rb +0 -10
- data/lib/sunspot/dsl/restriction.rb +4 -4
- data/lib/sunspot/dsl/restriction_with_near.rb +121 -0
- data/lib/sunspot/dsl/scope.rb +55 -67
- data/lib/sunspot/dsl/standard_query.rb +11 -15
- data/lib/sunspot/field.rb +30 -29
- data/lib/sunspot/field_factory.rb +0 -18
- data/lib/sunspot/installer/solrconfig_updater.rb +0 -30
- data/lib/sunspot/query.rb +4 -3
- data/lib/sunspot/query/common_query.rb +2 -2
- data/lib/sunspot/query/composite_fulltext.rb +7 -2
- data/lib/sunspot/query/connective.rb +21 -6
- data/lib/sunspot/query/dismax.rb +1 -0
- data/lib/sunspot/query/geo.rb +53 -0
- data/lib/sunspot/query/more_like_this.rb +1 -0
- data/lib/sunspot/query/restriction.rb +5 -5
- data/lib/sunspot/query/standard_query.rb +0 -4
- data/lib/sunspot/search/abstract_search.rb +1 -7
- data/lib/sunspot/search/hit.rb +10 -10
- data/lib/sunspot/search/query_facet.rb +8 -3
- data/lib/sunspot/session.rb +10 -2
- data/lib/sunspot/session_proxy.rb +16 -0
- data/lib/sunspot/session_proxy/master_slave_session_proxy.rb +1 -1
- data/lib/sunspot/session_proxy/sharding_session_proxy.rb +7 -0
- data/lib/sunspot/session_proxy/silent_fail_session_proxy.rb +42 -0
- data/lib/sunspot/session_proxy/thread_local_session_proxy.rb +1 -1
- data/lib/sunspot/setup.rb +1 -17
- data/lib/sunspot/type.rb +38 -6
- data/lib/sunspot/util.rb +21 -31
- data/lib/sunspot/version.rb +1 -1
- data/solr/solr/conf/solrconfig.xml +0 -4
- data/spec/api/binding_spec.rb +12 -0
- data/spec/api/indexer/attributes_spec.rb +22 -22
- data/spec/api/query/connectives_examples.rb +14 -1
- data/spec/api/query/fulltext_examples.rb +3 -3
- data/spec/api/query/geo_examples.rb +69 -0
- data/spec/api/query/scope_examples.rb +32 -13
- data/spec/api/query/standard_spec.rb +1 -1
- data/spec/api/search/faceting_spec.rb +5 -1
- data/spec/api/search/hits_spec.rb +14 -12
- data/spec/api/session_proxy/class_sharding_session_proxy_spec.rb +1 -1
- data/spec/api/session_proxy/sharding_session_proxy_spec.rb +1 -1
- data/spec/api/session_proxy/silent_fail_session_proxy_spec.rb +24 -0
- data/spec/api/session_spec.rb +22 -0
- data/spec/integration/local_search_spec.rb +42 -69
- data/spec/integration/scoped_search_spec.rb +30 -0
- data/spec/mocks/connection.rb +6 -2
- data/spec/mocks/photo.rb +0 -1
- data/spec/mocks/post.rb +11 -2
- data/spec/mocks/user.rb +6 -1
- data/spec/spec_helper.rb +2 -12
- metadata +209 -177
- data/lib/sunspot/query/local.rb +0 -26
- data/solr/solr/lib/lucene-spatial-2.9.1.jar +0 -0
- data/solr/solr/lib/solr-spatial-light-0.0.6.jar +0 -0
- data/spec/api/query/local_examples.rb +0 -38
- data/tasks/gemspec.rake +0 -33
- data/tasks/rcov.rake +0 -28
- data/tasks/spec.rake +0 -24
@@ -103,22 +103,18 @@ module Sunspot
|
|
103
103
|
end
|
104
104
|
alias_method :keywords, :fulltext
|
105
105
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
# Whether to sort by distance from these coordinates. If other sorts are
|
115
|
-
# specified, they take precedence over distance sort.
|
116
|
-
#
|
117
|
-
def near(coordinates, options)
|
118
|
-
if options.respond_to?(:to_f)
|
119
|
-
options = { :distance => options }
|
106
|
+
def with(*args)
|
107
|
+
case args.first
|
108
|
+
when String, Symbol
|
109
|
+
field_name = args[0]
|
110
|
+
value = args.length > 1 ? args[1] : Scope::NONE
|
111
|
+
if value == Scope::NONE
|
112
|
+
return DSL::RestrictionWithNear.new(@setup.field(field_name.to_sym), @scope, @query, false)
|
113
|
+
end
|
120
114
|
end
|
121
|
-
|
115
|
+
|
116
|
+
# else
|
117
|
+
super
|
122
118
|
end
|
123
119
|
end
|
124
120
|
end
|
data/lib/sunspot/field.rb
CHANGED
@@ -4,13 +4,15 @@ module Sunspot
|
|
4
4
|
attr_accessor :type # The Type of the field
|
5
5
|
attr_accessor :reference # Model class that the value of this field refers to
|
6
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.
|
7
8
|
|
8
|
-
#
|
9
|
+
#
|
9
10
|
#
|
10
11
|
def initialize(name, type, options = {}) #:nodoc
|
11
12
|
@name, @type = name.to_sym, type
|
12
13
|
@stored = !!options.delete(:stored)
|
13
14
|
@more_like_this = !!options.delete(:more_like_this)
|
15
|
+
set_indexed_name(options)
|
14
16
|
raise ArgumentError, "Field of type #{type} cannot be used for more_like_this" unless type.accepts_more_like_this? or !@more_like_this
|
15
17
|
end
|
16
18
|
|
@@ -56,19 +58,7 @@ module Sunspot
|
|
56
58
|
@type.cast(value)
|
57
59
|
end
|
58
60
|
|
59
|
-
#
|
60
|
-
# Name with which this field is indexed internally. Based on public name and
|
61
|
-
# type.
|
62
|
-
#
|
63
|
-
# ==== Returns
|
64
|
-
#
|
65
|
-
# String:: Internal name of the field
|
66
61
|
#
|
67
|
-
def indexed_name
|
68
|
-
@type.indexed_name(@name)
|
69
|
-
end
|
70
|
-
|
71
|
-
#
|
72
62
|
# Whether this field accepts multiple values.
|
73
63
|
#
|
74
64
|
# ==== Returns
|
@@ -79,7 +69,7 @@ module Sunspot
|
|
79
69
|
!!@multiple
|
80
70
|
end
|
81
71
|
|
82
|
-
#
|
72
|
+
#
|
83
73
|
# Whether this field can be used for more_like_this queries.
|
84
74
|
# If true, the field is configured to store termVectors.
|
85
75
|
#
|
@@ -99,9 +89,30 @@ module Sunspot
|
|
99
89
|
indexed_name == field.indexed_name
|
100
90
|
end
|
101
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
|
+
|
102
113
|
end
|
103
114
|
|
104
|
-
#
|
115
|
+
#
|
105
116
|
# FulltextField instances represent fields that are indexed as fulltext.
|
106
117
|
# These fields are tokenized in the index, and can have boost applied to
|
107
118
|
# them. They also always allow multiple values (since the only downside of
|
@@ -121,11 +132,11 @@ module Sunspot
|
|
121
132
|
end
|
122
133
|
|
123
134
|
def indexed_name
|
124
|
-
"#{super}
|
135
|
+
"#{super}"
|
125
136
|
end
|
126
137
|
end
|
127
138
|
|
128
|
-
#
|
139
|
+
#
|
129
140
|
# AttributeField instances encapsulate non-tokenized attribute data.
|
130
141
|
# AttributeFields can have any type except TextType, and can also have
|
131
142
|
# a reference (for instantiated facets), optionally allow multiple values
|
@@ -134,8 +145,8 @@ module Sunspot
|
|
134
145
|
#
|
135
146
|
class AttributeField < Field #:nodoc:
|
136
147
|
def initialize(name, type, options = {})
|
137
|
-
super(name, type, options)
|
138
148
|
@multiple = !!options.delete(:multiple)
|
149
|
+
super(name, type, options)
|
139
150
|
@reference =
|
140
151
|
if (reference = options.delete(:references)).respond_to?(:name)
|
141
152
|
reference.name
|
@@ -145,17 +156,6 @@ module Sunspot
|
|
145
156
|
raise ArgumentError, "Unknown field option #{options.keys.first.inspect} provided for field #{name.inspect}" unless options.empty?
|
146
157
|
end
|
147
158
|
|
148
|
-
# The name of the field as it is indexed in Solr. The indexed name
|
149
|
-
# contains a suffix that contains information about the type as well as
|
150
|
-
# whether the field allows multiple values for a document.
|
151
|
-
#
|
152
|
-
# ==== Returns
|
153
|
-
#
|
154
|
-
# String:: The field's indexed name
|
155
|
-
#
|
156
|
-
def indexed_name
|
157
|
-
"#{super}#{'m' if @multiple}#{'s' if @stored}#{'v' if more_like_this?}"
|
158
|
-
end
|
159
159
|
end
|
160
160
|
|
161
161
|
class TypeField #:nodoc:
|
@@ -190,3 +190,4 @@ module Sunspot
|
|
190
190
|
end
|
191
191
|
end
|
192
192
|
end
|
193
|
+
|
@@ -125,23 +125,5 @@ module Sunspot
|
|
125
125
|
[@name, @type]
|
126
126
|
end
|
127
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
128
|
end
|
147
129
|
end
|
@@ -44,8 +44,6 @@ module Sunspot
|
|
44
44
|
)
|
45
45
|
end
|
46
46
|
@root = @document.root
|
47
|
-
maybe_create_spatial_component
|
48
|
-
maybe_add_spatial_component_to_standard_handler
|
49
47
|
maybe_add_more_like_this_handler
|
50
48
|
original_path = "#{@solrconfig_path}.orig"
|
51
49
|
FileUtils.cp(@solrconfig_path, original_path)
|
@@ -62,34 +60,6 @@ module Sunspot
|
|
62
60
|
|
63
61
|
private
|
64
62
|
|
65
|
-
def maybe_create_spatial_component
|
66
|
-
if @root.xpath('searchComponent[@name="spatial"]').any?
|
67
|
-
say('Spatial search component already defined')
|
68
|
-
else
|
69
|
-
say('Defining spatial search component')
|
70
|
-
search_component_node =
|
71
|
-
Nokogiri::XML::Node.new('searchComponent', @document)
|
72
|
-
search_component_node['name'] = 'spatial'
|
73
|
-
search_component_node['class'] =
|
74
|
-
'me.outofti.solrspatiallight.SpatialQueryComponent'
|
75
|
-
@root << search_component_node
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
def maybe_add_spatial_component_to_standard_handler
|
80
|
-
standard_handler_node =
|
81
|
-
@root.xpath('requestHandler[@name="standard"]').first
|
82
|
-
last_components_node =
|
83
|
-
standard_handler_node.xpath('arr[@name="last-components"]').first ||
|
84
|
-
add_element(standard_handler_node, 'arr', 'name' => 'last-components')
|
85
|
-
if last_components_node.xpath('str[normalize-space()="spatial"]').any?
|
86
|
-
say('Spatial search component already in standard search handler')
|
87
|
-
else
|
88
|
-
say('Adding spatial search component into standard search handler')
|
89
|
-
add_element(last_components_node, 'str').content = 'spatial'
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
63
|
def maybe_add_more_like_this_handler
|
94
64
|
unless @root.xpath('requestHandler[@name="/mlt"]').first
|
95
65
|
mlt_node = add_element(
|
data/lib/sunspot/query.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
%w(filter abstract_field_facet connective boost_query date_field_facet dismax
|
2
|
-
field_facet highlighting
|
3
|
-
standard_query more_like_this more_like_this_query query_facet scope
|
4
|
-
sort_composite text_field_boost function_query
|
2
|
+
field_facet highlighting pagination restriction common_query
|
3
|
+
standard_query more_like_this more_like_this_query geo query_facet scope
|
4
|
+
sort sort_composite text_field_boost function_query
|
5
|
+
composite_fulltext).each do |file|
|
5
6
|
require(File.join(File.dirname(__FILE__), 'query', file))
|
6
7
|
end
|
7
8
|
module Sunspot
|
@@ -6,9 +6,9 @@ module Sunspot
|
|
6
6
|
@sort = SortComposite.new
|
7
7
|
@components = [@scope, @sort]
|
8
8
|
if types.length == 1
|
9
|
-
@scope.
|
9
|
+
@scope.add_positive_restriction(TypeField.instance, Restriction::EqualTo, types.first)
|
10
10
|
else
|
11
|
-
@scope.
|
11
|
+
@scope.add_positive_restriction(TypeField.instance, Restriction::AnyOf, types)
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
@@ -9,15 +9,20 @@ module Sunspot
|
|
9
9
|
@components << dismax = Dismax.new(keywords)
|
10
10
|
dismax
|
11
11
|
end
|
12
|
+
|
13
|
+
def add_location(field, lat, lng, options)
|
14
|
+
@components << location = Geo.new(field, lat, lng, options)
|
15
|
+
location
|
16
|
+
end
|
12
17
|
|
13
18
|
def to_params
|
14
19
|
case @components.length
|
15
20
|
when 0
|
16
21
|
{}
|
17
22
|
when 1
|
18
|
-
@components.first.to_params
|
23
|
+
@components.first.to_params.merge(:fl => '* score')
|
19
24
|
else
|
20
|
-
to_subqueries
|
25
|
+
to_subqueries.merge(:fl => '* score')
|
21
26
|
end
|
22
27
|
end
|
23
28
|
|
@@ -15,22 +15,37 @@ module Sunspot
|
|
15
15
|
#
|
16
16
|
# Add a restriction to the connective.
|
17
17
|
#
|
18
|
-
def add_restriction(field, restriction_type, value
|
19
|
-
add_component(restriction_type.new(field, value
|
18
|
+
def add_restriction(negated, field, restriction_type, *value)
|
19
|
+
add_component(restriction_type.new(negated, field, *value))
|
20
20
|
end
|
21
21
|
|
22
22
|
#
|
23
23
|
# Add a shorthand restriction; the restriction type is determined by
|
24
24
|
# the value.
|
25
25
|
#
|
26
|
-
def add_shorthand_restriction(field, value
|
26
|
+
def add_shorthand_restriction(negated, field, value)
|
27
27
|
restriction_type =
|
28
28
|
case value
|
29
29
|
when Array then Restriction::AnyOf
|
30
30
|
when Range then Restriction::Between
|
31
31
|
else Restriction::EqualTo
|
32
32
|
end
|
33
|
-
add_restriction(field, restriction_type, value
|
33
|
+
add_restriction(negated, field, restriction_type, value)
|
34
|
+
end
|
35
|
+
|
36
|
+
#
|
37
|
+
# Add a positive restriction. The restriction will match all
|
38
|
+
# documents who match the terms fo the restriction.
|
39
|
+
#
|
40
|
+
def add_positive_restriction(field, restriction_type, value)
|
41
|
+
add_restriction(false, field, restriction_type, value)
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
# Add a positive shorthand restriction (see add_shorthand_restriction)
|
46
|
+
#
|
47
|
+
def add_positive_shorthand_restriction(field, value)
|
48
|
+
add_shorthand_restriction(false, field, value)
|
34
49
|
end
|
35
50
|
|
36
51
|
#
|
@@ -38,14 +53,14 @@ module Sunspot
|
|
38
53
|
# documents who do not match the terms of the restriction.
|
39
54
|
#
|
40
55
|
def add_negated_restriction(field, restriction_type, value)
|
41
|
-
add_restriction(field, restriction_type, value
|
56
|
+
add_restriction(true, field, restriction_type, value)
|
42
57
|
end
|
43
58
|
|
44
59
|
#
|
45
60
|
# Add a negated shorthand restriction (see add_shorthand_restriction)
|
46
61
|
#
|
47
62
|
def add_negated_shorthand_restriction(field, value)
|
48
|
-
add_shorthand_restriction(field, value
|
63
|
+
add_shorthand_restriction(true, field, value)
|
49
64
|
end
|
50
65
|
|
51
66
|
#
|
data/lib/sunspot/query/dismax.rb
CHANGED
@@ -62,6 +62,7 @@ module Sunspot
|
|
62
62
|
def to_subquery
|
63
63
|
params = self.to_params
|
64
64
|
params.delete :defType
|
65
|
+
params.delete :fl
|
65
66
|
keywords = params.delete(:q)
|
66
67
|
options = params.map { |key, value| "#{key}='#{escape_quotes(value)}'"}.join(' ')
|
67
68
|
"_query_:\"{!dismax #{options}}#{escape_quotes(keywords)}\""
|
@@ -0,0 +1,53 @@
|
|
1
|
+
begin
|
2
|
+
require 'geohash'
|
3
|
+
rescue LoadError => e
|
4
|
+
require 'pr_geohash'
|
5
|
+
end
|
6
|
+
|
7
|
+
module Sunspot
|
8
|
+
module Query
|
9
|
+
class Geo
|
10
|
+
MAX_PRECISION = 12
|
11
|
+
DEFAULT_PRECISION = 7
|
12
|
+
DEFAULT_PRECISION_FACTOR = 16.0
|
13
|
+
|
14
|
+
def initialize(field, lat, lng, options)
|
15
|
+
@field, @options = field, options
|
16
|
+
@geohash = GeoHash.encode(lat.to_f, lng.to_f, MAX_PRECISION)
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_params
|
20
|
+
{ :q => to_boolean_query }
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_subquery
|
24
|
+
"(#{to_boolean_query})"
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def to_boolean_query
|
30
|
+
queries = []
|
31
|
+
MAX_PRECISION.downto(precision) do |i|
|
32
|
+
star = i == MAX_PRECISION ? '' : '*'
|
33
|
+
precision_boost = Util.format_float(
|
34
|
+
boost * precision_factor ** (i-MAX_PRECISION).to_f, 3)
|
35
|
+
queries << "#{@field.indexed_name}:#{@geohash[0, i]}#{star}^#{precision_boost}"
|
36
|
+
end
|
37
|
+
queries.join(' OR ')
|
38
|
+
end
|
39
|
+
|
40
|
+
def precision
|
41
|
+
@options[:precision] || DEFAULT_PRECISION
|
42
|
+
end
|
43
|
+
|
44
|
+
def precision_factor
|
45
|
+
@options[:precision_factor] || DEFAULT_PRECISION_FACTOR
|
46
|
+
end
|
47
|
+
|
48
|
+
def boost
|
49
|
+
@options[:boost] || 1.0
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -42,8 +42,9 @@ module Sunspot
|
|
42
42
|
|
43
43
|
RESERVED_WORDS = Set['AND', 'OR', 'NOT']
|
44
44
|
|
45
|
-
def initialize(field, value
|
46
|
-
|
45
|
+
def initialize(negated, field, value)
|
46
|
+
raise ArgumentError.new("RFCTR") unless [true, false].include?(negated)
|
47
|
+
@negated, @field, @value = negated, field, value
|
47
48
|
end
|
48
49
|
|
49
50
|
#
|
@@ -116,7 +117,7 @@ module Sunspot
|
|
116
117
|
# is used by disjunction denormalization.
|
117
118
|
#
|
118
119
|
def negate
|
119
|
-
self.class.new(@field, @value
|
120
|
+
self.class.new(!@negated, @field, @value)
|
120
121
|
end
|
121
122
|
|
122
123
|
protected
|
@@ -218,8 +219,7 @@ module Sunspot
|
|
218
219
|
end
|
219
220
|
|
220
221
|
def to_solr_conditional
|
221
|
-
|
222
|
-
"[#{solr_value(first)} TO #{solr_value(last)}]"
|
222
|
+
"[#{solr_value(@value.first)} TO #{solr_value(@value.last)}]"
|
223
223
|
end
|
224
224
|
end
|
225
225
|
|