sunspot 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|