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,96 @@
|
|
1
|
+
module Sunspot
|
2
|
+
module Query #:nodoc:
|
3
|
+
class CommonQuery
|
4
|
+
def initialize(types)
|
5
|
+
@scope = Scope.new
|
6
|
+
@sort = SortComposite.new
|
7
|
+
@components = [@scope, @sort]
|
8
|
+
if types.length == 1
|
9
|
+
@scope.add_positive_restriction(TypeField.instance, Restriction::EqualTo, types.first)
|
10
|
+
else
|
11
|
+
@scope.add_positive_restriction(TypeField.instance, Restriction::AnyOf, types)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def solr_parameter_adjustment=(block)
|
16
|
+
@parameter_adjustment = block
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_sort(sort)
|
20
|
+
@sort << sort
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_group(group)
|
24
|
+
@components << group
|
25
|
+
group
|
26
|
+
end
|
27
|
+
|
28
|
+
def add_field_facet(facet)
|
29
|
+
@components << facet
|
30
|
+
facet
|
31
|
+
end
|
32
|
+
|
33
|
+
def add_query_facet(facet)
|
34
|
+
@components << facet
|
35
|
+
facet
|
36
|
+
end
|
37
|
+
|
38
|
+
def add_function(function)
|
39
|
+
@components << function
|
40
|
+
function
|
41
|
+
end
|
42
|
+
|
43
|
+
def add_geo(geo)
|
44
|
+
@components << geo
|
45
|
+
geo
|
46
|
+
end
|
47
|
+
|
48
|
+
def paginate(page, per_page, offset = nil)
|
49
|
+
if @pagination
|
50
|
+
@pagination.offset = offset
|
51
|
+
@pagination.page = page
|
52
|
+
@pagination.per_page = per_page
|
53
|
+
else
|
54
|
+
@components << @pagination = Pagination.new(page, per_page, offset)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_params
|
59
|
+
params = {}
|
60
|
+
@components.each do |component|
|
61
|
+
Sunspot::Util.deep_merge!(params, component.to_params)
|
62
|
+
end
|
63
|
+
@parameter_adjustment.call(params) if @parameter_adjustment
|
64
|
+
params[:q] ||= '*:*'
|
65
|
+
params
|
66
|
+
end
|
67
|
+
|
68
|
+
def [](key)
|
69
|
+
to_params[key.to_sym]
|
70
|
+
end
|
71
|
+
|
72
|
+
def page
|
73
|
+
@pagination.page if @pagination
|
74
|
+
end
|
75
|
+
|
76
|
+
def per_page
|
77
|
+
@pagination.per_page if @pagination
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
#
|
84
|
+
# If we have a single fulltext query, merge is normally. If there are
|
85
|
+
# multiple nested queries, serialize them as `_query_` subqueries.
|
86
|
+
#
|
87
|
+
def merge_fulltext(params)
|
88
|
+
return nil if @fulltexts.empty?
|
89
|
+
return Sunspot::Util.deep_merge!(params, @fulltexts.first.to_params) if @fulltexts.length == 1
|
90
|
+
subqueries = @fulltexts.map {|fulltext| fulltext.to_subquery }.join(' ')
|
91
|
+
Sunspot::Util.deep_merge!(params, {:q => subqueries})
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Sunspot
|
2
|
+
module Query
|
3
|
+
class CompositeFulltext
|
4
|
+
def initialize
|
5
|
+
@components = []
|
6
|
+
end
|
7
|
+
|
8
|
+
def add(keywords)
|
9
|
+
@components << dismax = Dismax.new(keywords)
|
10
|
+
dismax
|
11
|
+
end
|
12
|
+
|
13
|
+
def add_location(field, lat, lng, options)
|
14
|
+
@components << location = Geo.new(field, lat, lng, options)
|
15
|
+
location
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_params
|
19
|
+
case @components.length
|
20
|
+
when 0
|
21
|
+
{}
|
22
|
+
when 1
|
23
|
+
@components.first.to_params.merge(:fl => '* score')
|
24
|
+
else
|
25
|
+
to_subqueries.merge(:fl => '* score')
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def to_subqueries
|
32
|
+
{ :q => @components.map { |dismax| dismax.to_subquery }.join(' ') }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,206 @@
|
|
1
|
+
module Sunspot
|
2
|
+
module Query
|
3
|
+
module Connective #:nodoc:all
|
4
|
+
#
|
5
|
+
# Base class for connectives (conjunctions and disjunctions).
|
6
|
+
#
|
7
|
+
class Abstract
|
8
|
+
include Filter
|
9
|
+
|
10
|
+
def initialize(negated = false) #:nodoc:
|
11
|
+
@negated = negated
|
12
|
+
@components = []
|
13
|
+
end
|
14
|
+
|
15
|
+
#
|
16
|
+
# Add a restriction to the connective.
|
17
|
+
#
|
18
|
+
def add_restriction(negated, field, restriction_type, *value)
|
19
|
+
add_component(restriction_type.new(negated, field, *value))
|
20
|
+
end
|
21
|
+
|
22
|
+
#
|
23
|
+
# Add a shorthand restriction; the restriction type is determined by
|
24
|
+
# the value.
|
25
|
+
#
|
26
|
+
def add_shorthand_restriction(negated, field, value)
|
27
|
+
restriction_type =
|
28
|
+
case value
|
29
|
+
when Array then Restriction::AnyOf
|
30
|
+
when Range then Restriction::Between
|
31
|
+
else Restriction::EqualTo
|
32
|
+
end
|
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)
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# Add a negated restriction. The added restriction will match all
|
53
|
+
# documents who do not match the terms of the restriction.
|
54
|
+
#
|
55
|
+
def add_negated_restriction(field, restriction_type, value)
|
56
|
+
add_restriction(true, field, restriction_type, value)
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# Add a negated shorthand restriction (see add_shorthand_restriction)
|
61
|
+
#
|
62
|
+
def add_negated_shorthand_restriction(field, value)
|
63
|
+
add_shorthand_restriction(true, field, value)
|
64
|
+
end
|
65
|
+
|
66
|
+
#
|
67
|
+
# Add a new conjunction and return it.
|
68
|
+
#
|
69
|
+
def add_conjunction
|
70
|
+
add_component(Conjunction.new)
|
71
|
+
end
|
72
|
+
|
73
|
+
#
|
74
|
+
# Add a new disjunction and return it.
|
75
|
+
#
|
76
|
+
def add_disjunction
|
77
|
+
add_component(Disjunction.new)
|
78
|
+
end
|
79
|
+
|
80
|
+
#
|
81
|
+
# Add an arbitrary component to the conjunction, and return it.
|
82
|
+
# The component must respond to #to_boolean_phrase
|
83
|
+
#
|
84
|
+
def add_component(component)
|
85
|
+
@components << component
|
86
|
+
component
|
87
|
+
end
|
88
|
+
|
89
|
+
#
|
90
|
+
# Express the connective as a Lucene boolean phrase.
|
91
|
+
#
|
92
|
+
def to_boolean_phrase #:nodoc:
|
93
|
+
unless @components.empty?
|
94
|
+
phrase =
|
95
|
+
if @components.length == 1
|
96
|
+
@components.first.to_boolean_phrase
|
97
|
+
else
|
98
|
+
component_phrases = @components.map do |component|
|
99
|
+
component.to_boolean_phrase
|
100
|
+
end
|
101
|
+
"(#{component_phrases.join(" #{connector} ")})"
|
102
|
+
end
|
103
|
+
if negated?
|
104
|
+
"-#{phrase}"
|
105
|
+
else
|
106
|
+
phrase
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
#
|
112
|
+
# Connectives can be negated during the process of denormalization that
|
113
|
+
# is performed when a disjunction contains a negated component. This
|
114
|
+
# method conforms to the duck type for all boolean query components.
|
115
|
+
#
|
116
|
+
def negated?
|
117
|
+
@negated
|
118
|
+
end
|
119
|
+
|
120
|
+
#
|
121
|
+
# Returns a new connective that's a negated version of this one.
|
122
|
+
#
|
123
|
+
def negate
|
124
|
+
negated = self.class.new(!negated?)
|
125
|
+
@components.each do |component|
|
126
|
+
negated.add_component(component)
|
127
|
+
end
|
128
|
+
negated
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
#
|
133
|
+
# Disjunctions combine their components with an OR operator.
|
134
|
+
#
|
135
|
+
class Disjunction < Abstract
|
136
|
+
class <<self
|
137
|
+
def inverse
|
138
|
+
Conjunction
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
#
|
143
|
+
# Express this disjunction as a Lucene boolean phrase
|
144
|
+
#
|
145
|
+
def to_boolean_phrase
|
146
|
+
if @components.any? { |component| component.negated? }
|
147
|
+
denormalize.to_boolean_phrase
|
148
|
+
else
|
149
|
+
super
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
#
|
154
|
+
# No-op - this is already a disjunction
|
155
|
+
#
|
156
|
+
def add_disjunction
|
157
|
+
self
|
158
|
+
end
|
159
|
+
|
160
|
+
private
|
161
|
+
|
162
|
+
def connector
|
163
|
+
'OR'
|
164
|
+
end
|
165
|
+
|
166
|
+
#
|
167
|
+
# If a disjunction contains negated components, it must be
|
168
|
+
# "denormalized", because the Lucene parser interprets any negated
|
169
|
+
# boolean phrase using AND semantics (this isn't a bug, it's just a
|
170
|
+
# subtlety of how Lucene parses queries). So, per DeMorgan's law we
|
171
|
+
# create a negated conjunction and add to it all of our components,
|
172
|
+
# negated themselves, which creates a query whose Lucene semantics are
|
173
|
+
# in line with our intentions.
|
174
|
+
#
|
175
|
+
def denormalize
|
176
|
+
denormalized = self.class.inverse.new(!negated?)
|
177
|
+
@components.each do |component|
|
178
|
+
denormalized.add_component(component.negate)
|
179
|
+
end
|
180
|
+
denormalized
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
#
|
185
|
+
# Conjunctions combine their components with an AND operator.
|
186
|
+
#
|
187
|
+
class Conjunction < Abstract
|
188
|
+
class <<self
|
189
|
+
def inverse
|
190
|
+
Disjunction
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def add_conjunction
|
195
|
+
self
|
196
|
+
end
|
197
|
+
|
198
|
+
private
|
199
|
+
|
200
|
+
def connector
|
201
|
+
'AND'
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Sunspot
|
2
|
+
module Query
|
3
|
+
class DateFieldFacet < AbstractFieldFacet
|
4
|
+
def to_params
|
5
|
+
params = super
|
6
|
+
params[:"facet.date"] = [@field.indexed_name]
|
7
|
+
params[qualified_param('date.start')] = @field.to_indexed(@options[:time_range].first)
|
8
|
+
params[qualified_param('date.end')] = @field.to_indexed(@options[:time_range].last)
|
9
|
+
params[qualified_param('date.gap')] = "+#{@options[:time_interval] || 86400}SECONDS"
|
10
|
+
params
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
module Sunspot
|
2
|
+
module Query
|
3
|
+
|
4
|
+
#
|
5
|
+
# Solr full-text queries use Solr's DisMaxRequestHandler, a search handler
|
6
|
+
# designed to process user-entered phrases, and search for individual
|
7
|
+
# words across a union of several fields.
|
8
|
+
#
|
9
|
+
class Dismax
|
10
|
+
attr_writer :minimum_match, :phrase_slop, :query_phrase_slop, :tie
|
11
|
+
|
12
|
+
def initialize(keywords)
|
13
|
+
@keywords = keywords
|
14
|
+
@fulltext_fields = {}
|
15
|
+
@boost_queries = []
|
16
|
+
@boost_functions = []
|
17
|
+
@highlights = []
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
# The query as Solr parameters
|
22
|
+
#
|
23
|
+
def to_params
|
24
|
+
params = { :q => @keywords }
|
25
|
+
params[:fl] = '* score'
|
26
|
+
params[:qf] = @fulltext_fields.values.map { |field| field.to_boosted_field }.join(' ')
|
27
|
+
params[:defType] = 'dismax'
|
28
|
+
if @phrase_fields
|
29
|
+
params[:pf] = @phrase_fields.map { |field| field.to_boosted_field }.join(' ')
|
30
|
+
end
|
31
|
+
unless @boost_queries.empty?
|
32
|
+
params[:bq] = @boost_queries.map do |boost_query|
|
33
|
+
boost_query.to_boolean_phrase
|
34
|
+
end
|
35
|
+
end
|
36
|
+
unless @boost_functions.empty?
|
37
|
+
params[:bf] = @boost_functions.map do |boost_function|
|
38
|
+
boost_function.to_s
|
39
|
+
end
|
40
|
+
end
|
41
|
+
if @minimum_match
|
42
|
+
params[:mm] = @minimum_match
|
43
|
+
end
|
44
|
+
if @phrase_slop
|
45
|
+
params[:ps] = @phrase_slop
|
46
|
+
end
|
47
|
+
if @query_phrase_slop
|
48
|
+
params[:qs] = @query_phrase_slop
|
49
|
+
end
|
50
|
+
if @tie
|
51
|
+
params[:tie] = @tie
|
52
|
+
end
|
53
|
+
@highlights.each do |highlight|
|
54
|
+
Sunspot::Util.deep_merge!(params, highlight.to_params)
|
55
|
+
end
|
56
|
+
params
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# Serialize the query as a Solr nested subquery.
|
61
|
+
#
|
62
|
+
def to_subquery
|
63
|
+
params = self.to_params
|
64
|
+
params.delete :defType
|
65
|
+
params.delete :fl
|
66
|
+
keywords = params.delete(:q)
|
67
|
+
options = params.map { |key, value| escape_param(key, value) }.join(' ')
|
68
|
+
"_query_:\"{!dismax #{options}}#{escape_quotes(keywords)}\""
|
69
|
+
end
|
70
|
+
|
71
|
+
#
|
72
|
+
# Assign a new boost query and return it.
|
73
|
+
#
|
74
|
+
def create_boost_query(factor)
|
75
|
+
@boost_queries << boost_query = BoostQuery.new(factor)
|
76
|
+
boost_query
|
77
|
+
end
|
78
|
+
|
79
|
+
#
|
80
|
+
# Add a boost function
|
81
|
+
#
|
82
|
+
def add_boost_function(function_query)
|
83
|
+
@boost_functions << function_query
|
84
|
+
end
|
85
|
+
|
86
|
+
#
|
87
|
+
# Add a fulltext field to be searched, with optional boost.
|
88
|
+
#
|
89
|
+
def add_fulltext_field(field, boost = nil)
|
90
|
+
@fulltext_fields[field.indexed_name] = TextFieldBoost.new(field, boost)
|
91
|
+
end
|
92
|
+
|
93
|
+
#
|
94
|
+
# Add a phrase field for extra boost.
|
95
|
+
#
|
96
|
+
def add_phrase_field(field, boost = nil)
|
97
|
+
@phrase_fields ||= []
|
98
|
+
@phrase_fields << TextFieldBoost.new(field, boost)
|
99
|
+
end
|
100
|
+
|
101
|
+
#
|
102
|
+
# Set highlighting options for the query. If fields is empty, the
|
103
|
+
# Highlighting object won't pass field names at all, which means
|
104
|
+
# the dismax's :qf parameter will be used by Solr.
|
105
|
+
#
|
106
|
+
def add_highlight(fields=[], options={})
|
107
|
+
@highlights << Highlighting.new(fields, options)
|
108
|
+
end
|
109
|
+
|
110
|
+
#
|
111
|
+
# Determine if a given field is being searched. Used by DSL to avoid
|
112
|
+
# overwriting boost parameters when injecting defaults.
|
113
|
+
#
|
114
|
+
def has_fulltext_field?(field)
|
115
|
+
@fulltext_fields.has_key?(field.indexed_name)
|
116
|
+
end
|
117
|
+
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def escape_param(key, value)
|
122
|
+
"#{key}='#{escape_quotes(Array(value).join(" "))}'"
|
123
|
+
end
|
124
|
+
|
125
|
+
def escape_quotes(value)
|
126
|
+
return value unless value.is_a? String
|
127
|
+
value.gsub(/(['"])/, '\\\\\1')
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Sunspot
|
2
|
+
module Query
|
3
|
+
class FieldFacet < AbstractFieldFacet
|
4
|
+
def initialize(field, options)
|
5
|
+
if exclude_filters = options[:exclude]
|
6
|
+
@exclude_tag = Util.Array(exclude_filters).map do |filter|
|
7
|
+
filter.tag
|
8
|
+
end.join(',')
|
9
|
+
end
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_params
|
14
|
+
super.merge(:"facet.field" => [field_name_with_local_params])
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def local_params
|
20
|
+
@local_params ||=
|
21
|
+
begin
|
22
|
+
local_params = {}
|
23
|
+
local_params[:ex] = @exclude_tag if @exclude_tag
|
24
|
+
local_params[:key] = @options[:name] if @options[:name]
|
25
|
+
local_params
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def field_name_with_local_params
|
30
|
+
if local_params.empty?
|
31
|
+
@field.indexed_name
|
32
|
+
else
|
33
|
+
pairs = local_params.map do |key, value|
|
34
|
+
"#{key}=#{value}"
|
35
|
+
end
|
36
|
+
"{!#{pairs.join(' ')}}#{@field.indexed_name}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Sunspot
|
2
|
+
module Query
|
3
|
+
#
|
4
|
+
# A FieldGroup groups by the unique values of a given field.
|
5
|
+
#
|
6
|
+
class FieldGroup
|
7
|
+
attr_accessor :limit, :truncate
|
8
|
+
|
9
|
+
def initialize(field)
|
10
|
+
if field.multiple?
|
11
|
+
raise(ArgumentError, "#{field.name} cannot be used for grouping because it is a multiple-value field")
|
12
|
+
end
|
13
|
+
@field = field
|
14
|
+
|
15
|
+
@sort = SortComposite.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_sort(sort)
|
19
|
+
@sort << sort
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_params
|
23
|
+
params = {
|
24
|
+
:group => "true",
|
25
|
+
:"group.field" => @field.indexed_name,
|
26
|
+
}
|
27
|
+
|
28
|
+
params.merge!(@sort.to_params("group."))
|
29
|
+
params[:"group.limit"] = @limit if @limit
|
30
|
+
params[:"group.truncate"] = @truncate if @truncate
|
31
|
+
|
32
|
+
params
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Sunspot
|
2
|
+
module Query
|
3
|
+
module Filter
|
4
|
+
|
5
|
+
#
|
6
|
+
# Express this filter as an :fq parameter; i.e., the boolean phrase,
|
7
|
+
# maybe prefixed by local params.
|
8
|
+
#
|
9
|
+
def to_filter_query
|
10
|
+
if tagged? then "{!tag=#{tag}}#{to_boolean_phrase}"
|
11
|
+
else to_boolean_phrase
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
#
|
16
|
+
# Generate and return a tag that can be attached to this restriction,
|
17
|
+
# for use with multiselect faceting. This needs to be unique, but doesn't
|
18
|
+
# really need to be human-readable, so just generate a string based on the
|
19
|
+
# hash of the boolean phrase.
|
20
|
+
#
|
21
|
+
def tag
|
22
|
+
@tag ||= to_boolean_phrase.hash.abs.to_s(36)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
#
|
28
|
+
# True if a tag has been generated for this filter (e.g., if it's been
|
29
|
+
# excluded from a given facet). If a tag has not been generated at the
|
30
|
+
# time that the filter query param is requested, then it is not necessary
|
31
|
+
# to include a tag in the local params.
|
32
|
+
#
|
33
|
+
def tagged?
|
34
|
+
!!@tag
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Sunspot
|
2
|
+
module Query
|
3
|
+
#
|
4
|
+
# Abstract class for function queries.
|
5
|
+
#
|
6
|
+
class FunctionQuery
|
7
|
+
include RSolr::Char
|
8
|
+
end
|
9
|
+
|
10
|
+
#
|
11
|
+
# Function query which represents a constant.
|
12
|
+
#
|
13
|
+
class ConstantFunctionQuery < FunctionQuery
|
14
|
+
def initialize(constant)
|
15
|
+
@constant = constant
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
Type.to_literal(@constant)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
#
|
24
|
+
# Function query which represents a field.
|
25
|
+
#
|
26
|
+
class FieldFunctionQuery < FunctionQuery
|
27
|
+
def initialize(field)
|
28
|
+
@field = field
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_s
|
32
|
+
"#{escape(@field.indexed_name)}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
#
|
37
|
+
# Function query which represents an actual function invocation.
|
38
|
+
# Takes a function name and arguments as parameters.
|
39
|
+
# Arguments are in turn FunctionQuery objects.
|
40
|
+
#
|
41
|
+
class FunctionalFunctionQuery < FunctionQuery
|
42
|
+
def initialize(function_name, function_args)
|
43
|
+
@function_name, @function_args = function_name, function_args
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_s
|
47
|
+
params = @function_args.map { |arg| arg.to_s }.join(",")
|
48
|
+
"#{@function_name}(#{params})"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -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
|