searchkick 0.5.3 → 0.6.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/Gemfile +2 -0
- data/README.md +17 -3
- data/gemfiles/mongoid4.gemfile +1 -1
- data/lib/searchkick.rb +12 -2
- data/lib/searchkick/index.rb +67 -0
- data/lib/searchkick/model.rb +8 -4
- data/lib/searchkick/query.rb +441 -0
- data/lib/searchkick/reindex.rb +15 -23
- data/lib/searchkick/results.rb +81 -6
- data/lib/searchkick/search.rb +4 -387
- data/lib/searchkick/similar.rb +1 -1
- data/lib/searchkick/version.rb +1 -1
- data/searchkick.gemspec +5 -6
- data/test/index_test.rb +6 -3
- data/test/query_test.rb +14 -0
- data/test/sql_test.rb +16 -6
- data/test/test_helper.rb +6 -8
- metadata +24 -33
- data/lib/searchkick/logger.rb +0 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 633b6ec4127ea2638b8c05db6c1c1c0b536c4193
|
4
|
+
data.tar.gz: 69d65f9ecc57e55a954482e03a0998a1f10023dc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 669dd3262ff71f967b6d0266851eca6f769befc5317289bb058781cf11d6e46af2e47bf9193a60fbc19117b04c5653551b4966bb349d6a9f88f79baa424b0116
|
7
|
+
data.tar.gz: 024fb4884e8a4d08e3a8954e3d2de0fce7bc7968311da0b64d6c9fe3f24e9b9c6488fa37d11c4c5f4674bdd5e60f72ac4da0ab8d5122c8c60fc07ef8542790c6
|
data/CHANGELOG.md
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -147,7 +147,7 @@ Product.search "fresh honey" # fresh AND honey
|
|
147
147
|
To change this, use:
|
148
148
|
|
149
149
|
```ruby
|
150
|
-
Product.search "fresh honey",
|
150
|
+
Product.search "fresh honey", operator: "or" # fresh OR honey
|
151
151
|
```
|
152
152
|
|
153
153
|
By default, results must match the entire word - `back` will not match `backpack`. You can change this behavior with:
|
@@ -496,7 +496,7 @@ And to search, use:
|
|
496
496
|
```ruby
|
497
497
|
Animal.search "*" # all animals
|
498
498
|
Dog.search "*" # just dogs
|
499
|
-
Animal.search "*", type: [Dog, Cat] # just cats and dogs
|
499
|
+
Animal.search "*", type: [Dog, Cat] # just cats and dogs
|
500
500
|
```
|
501
501
|
|
502
502
|
**Note:** The `suggest` option retrieves suggestions from the parent at the moment.
|
@@ -568,7 +568,13 @@ end
|
|
568
568
|
And use the `query` option to search:
|
569
569
|
|
570
570
|
```ruby
|
571
|
-
Product.search query: {match: {name: "milk"}}
|
571
|
+
products = Product.search query: {match: {name: "milk"}}
|
572
|
+
```
|
573
|
+
|
574
|
+
View the response with:
|
575
|
+
|
576
|
+
```ruby
|
577
|
+
products.response
|
572
578
|
```
|
573
579
|
|
574
580
|
To keep the mappings and settings generated by Searchkick, use:
|
@@ -579,6 +585,14 @@ class Product < ActiveRecord::Base
|
|
579
585
|
end
|
580
586
|
```
|
581
587
|
|
588
|
+
To modify the query generated by Searchkick, use:
|
589
|
+
|
590
|
+
```ruby
|
591
|
+
query = Product.search "2% Milk", execute: false
|
592
|
+
query.body[:query] = {match_all: {}}
|
593
|
+
products = query.execute
|
594
|
+
```
|
595
|
+
|
582
596
|
## Reference
|
583
597
|
|
584
598
|
Searchkick requires Elasticsearch `0.90.0` or higher.
|
data/gemfiles/mongoid4.gemfile
CHANGED
data/lib/searchkick.rb
CHANGED
@@ -1,14 +1,24 @@
|
|
1
|
-
require "
|
1
|
+
require "active_model"
|
2
|
+
require "patron"
|
3
|
+
require "elasticsearch"
|
2
4
|
require "searchkick/version"
|
5
|
+
require "searchkick/index"
|
3
6
|
require "searchkick/reindex"
|
4
7
|
require "searchkick/results"
|
8
|
+
require "searchkick/query"
|
5
9
|
require "searchkick/search"
|
6
10
|
require "searchkick/similar"
|
7
11
|
require "searchkick/model"
|
8
12
|
require "searchkick/tasks"
|
9
|
-
|
13
|
+
# TODO add logger
|
14
|
+
# require "searchkick/logger" if defined?(Rails)
|
10
15
|
|
11
16
|
module Searchkick
|
17
|
+
|
18
|
+
def self.client
|
19
|
+
@client ||= Elasticsearch::Client.new(url: ENV["ELASTICSEARCH_URL"])
|
20
|
+
end
|
21
|
+
|
12
22
|
@callbacks = true
|
13
23
|
|
14
24
|
def self.enable_callbacks
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Searchkick
|
2
|
+
class Index
|
3
|
+
attr_reader :name
|
4
|
+
|
5
|
+
def initialize(name)
|
6
|
+
@name = name
|
7
|
+
end
|
8
|
+
|
9
|
+
def create(options = {})
|
10
|
+
client.indices.create index: name, body: options
|
11
|
+
end
|
12
|
+
|
13
|
+
def delete
|
14
|
+
client.indices.delete index: name
|
15
|
+
end
|
16
|
+
|
17
|
+
def exists?
|
18
|
+
client.indices.exists index: name
|
19
|
+
end
|
20
|
+
|
21
|
+
def refresh
|
22
|
+
client.indices.refresh index: name
|
23
|
+
end
|
24
|
+
|
25
|
+
def store(record)
|
26
|
+
client.index(
|
27
|
+
index: name,
|
28
|
+
type: record.document_type,
|
29
|
+
id: record.id,
|
30
|
+
body: record.as_indexed_json
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
def remove(record)
|
35
|
+
client.delete(
|
36
|
+
index: name,
|
37
|
+
type: record.document_type,
|
38
|
+
id: record.id
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
def import(records)
|
43
|
+
if records.any?
|
44
|
+
client.bulk(
|
45
|
+
index: name,
|
46
|
+
type: records.first.document_type,
|
47
|
+
body: records.map{|r| data = r.as_indexed_json; {index: {_id: data["_id"] || data["id"] || r.id, data: data}} }
|
48
|
+
)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def retrieve(document_type, id)
|
53
|
+
client.get_source(
|
54
|
+
index: name,
|
55
|
+
type: document_type,
|
56
|
+
id: id
|
57
|
+
)
|
58
|
+
end
|
59
|
+
|
60
|
+
protected
|
61
|
+
|
62
|
+
def client
|
63
|
+
Searchkick.client
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
data/lib/searchkick/model.rb
CHANGED
@@ -13,7 +13,7 @@ module Searchkick
|
|
13
13
|
# set index name
|
14
14
|
# TODO support proc
|
15
15
|
index_name = options[:index_name] || [options[:index_prefix], model_name.plural, searchkick_env].compact.join("_")
|
16
|
-
class_variable_set :@@searchkick_index,
|
16
|
+
class_variable_set :@@searchkick_index, Searchkick::Index.new(index_name)
|
17
17
|
|
18
18
|
extend Searchkick::Search
|
19
19
|
extend Searchkick::Reindex
|
@@ -45,7 +45,11 @@ module Searchkick
|
|
45
45
|
def reindex
|
46
46
|
index = self.class.searchkick_index
|
47
47
|
if destroyed? or !should_index?
|
48
|
-
|
48
|
+
begin
|
49
|
+
index.remove self
|
50
|
+
rescue Elasticsearch::Transport::Transport::Errors::NotFound
|
51
|
+
# do nothing
|
52
|
+
end
|
49
53
|
else
|
50
54
|
index.store self
|
51
55
|
end
|
@@ -55,7 +59,7 @@ module Searchkick
|
|
55
59
|
respond_to?(:to_hash) ? to_hash : serializable_hash
|
56
60
|
end
|
57
61
|
|
58
|
-
def
|
62
|
+
def as_indexed_json
|
59
63
|
source = search_data
|
60
64
|
|
61
65
|
# stringify fields
|
@@ -115,7 +119,7 @@ module Searchkick
|
|
115
119
|
|
116
120
|
# p search_data
|
117
121
|
|
118
|
-
source.
|
122
|
+
source.as_json
|
119
123
|
end
|
120
124
|
|
121
125
|
# TODO remove
|
@@ -0,0 +1,441 @@
|
|
1
|
+
module Searchkick
|
2
|
+
class Query
|
3
|
+
attr_reader :klass, :term, :options
|
4
|
+
attr_accessor :body
|
5
|
+
|
6
|
+
def initialize(klass, term, options = {})
|
7
|
+
if term.is_a?(Hash)
|
8
|
+
options = term
|
9
|
+
term = nil
|
10
|
+
else
|
11
|
+
term = term.to_s
|
12
|
+
end
|
13
|
+
|
14
|
+
@klass = klass
|
15
|
+
@term = term
|
16
|
+
@options = options
|
17
|
+
|
18
|
+
fields =
|
19
|
+
if options[:fields]
|
20
|
+
if options[:autocomplete]
|
21
|
+
options[:fields].map{|f| "#{f}.autocomplete" }
|
22
|
+
else
|
23
|
+
options[:fields].map do |value|
|
24
|
+
k, v = value.is_a?(Hash) ? value.to_a.first : [value, :word]
|
25
|
+
"#{k}.#{v == :word ? "analyzed" : v}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
else
|
29
|
+
if options[:autocomplete]
|
30
|
+
(searchkick_options[:autocomplete] || []).map{|f| "#{f}.autocomplete" }
|
31
|
+
else
|
32
|
+
["_all"]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
operator = options[:operator] || (options[:partial] ? "or" : "and")
|
37
|
+
|
38
|
+
# pagination
|
39
|
+
page = [options[:page].to_i, 1].max
|
40
|
+
per_page = (options[:limit] || options[:per_page] || 100000).to_i
|
41
|
+
offset = options[:offset] || (page - 1) * per_page
|
42
|
+
index_name = options[:index_name] || searchkick_index.name
|
43
|
+
|
44
|
+
conversions_field = searchkick_options[:conversions]
|
45
|
+
personalize_field = searchkick_options[:personalize]
|
46
|
+
|
47
|
+
all = term == "*"
|
48
|
+
|
49
|
+
if options[:query]
|
50
|
+
payload = options[:query]
|
51
|
+
elsif options[:similar]
|
52
|
+
payload = {
|
53
|
+
more_like_this: {
|
54
|
+
fields: fields,
|
55
|
+
like_text: term,
|
56
|
+
min_doc_freq: 1,
|
57
|
+
min_term_freq: 1,
|
58
|
+
analyzer: "searchkick_search2"
|
59
|
+
}
|
60
|
+
}
|
61
|
+
elsif all
|
62
|
+
payload = {
|
63
|
+
match_all: {}
|
64
|
+
}
|
65
|
+
else
|
66
|
+
if options[:autocomplete]
|
67
|
+
payload = {
|
68
|
+
multi_match: {
|
69
|
+
fields: fields,
|
70
|
+
query: term,
|
71
|
+
analyzer: "searchkick_autocomplete_search"
|
72
|
+
}
|
73
|
+
}
|
74
|
+
else
|
75
|
+
queries = []
|
76
|
+
fields.each do |field|
|
77
|
+
if field == "_all" or field.end_with?(".analyzed")
|
78
|
+
shared_options = {
|
79
|
+
fields: [field],
|
80
|
+
query: term,
|
81
|
+
use_dis_max: false,
|
82
|
+
operator: operator,
|
83
|
+
cutoff_frequency: 0.001
|
84
|
+
}
|
85
|
+
queries.concat [
|
86
|
+
{multi_match: shared_options.merge(boost: 10, analyzer: "searchkick_search")},
|
87
|
+
{multi_match: shared_options.merge(boost: 10, analyzer: "searchkick_search2")}
|
88
|
+
]
|
89
|
+
if options[:misspellings] != false
|
90
|
+
distance = (options[:misspellings].is_a?(Hash) && options[:misspellings][:distance]) || 1
|
91
|
+
queries.concat [
|
92
|
+
{multi_match: shared_options.merge(fuzziness: distance, max_expansions: 3, analyzer: "searchkick_search")},
|
93
|
+
{multi_match: shared_options.merge(fuzziness: distance, max_expansions: 3, analyzer: "searchkick_search2")}
|
94
|
+
]
|
95
|
+
end
|
96
|
+
else
|
97
|
+
analyzer = field.match(/\.word_(start|middle|end)\z/) ? "searchkick_word_search" : "searchkick_autocomplete_search"
|
98
|
+
queries << {
|
99
|
+
multi_match: {
|
100
|
+
fields: [field],
|
101
|
+
query: term,
|
102
|
+
analyzer: analyzer
|
103
|
+
}
|
104
|
+
}
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
payload = {
|
109
|
+
dis_max: {
|
110
|
+
queries: queries
|
111
|
+
}
|
112
|
+
}
|
113
|
+
end
|
114
|
+
|
115
|
+
if conversions_field and options[:conversions] != false
|
116
|
+
# wrap payload in a bool query
|
117
|
+
payload = {
|
118
|
+
bool: {
|
119
|
+
must: payload,
|
120
|
+
should: {
|
121
|
+
nested: {
|
122
|
+
path: conversions_field,
|
123
|
+
score_mode: "total",
|
124
|
+
query: {
|
125
|
+
custom_score: {
|
126
|
+
query: {
|
127
|
+
match: {
|
128
|
+
query: term
|
129
|
+
}
|
130
|
+
},
|
131
|
+
script: "doc['count'].value"
|
132
|
+
}
|
133
|
+
}
|
134
|
+
}
|
135
|
+
}
|
136
|
+
}
|
137
|
+
}
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
custom_filters = []
|
142
|
+
|
143
|
+
if options[:boost]
|
144
|
+
custom_filters << {
|
145
|
+
filter: {
|
146
|
+
exists: {
|
147
|
+
field: options[:boost]
|
148
|
+
}
|
149
|
+
},
|
150
|
+
script: "log(doc['#{options[:boost]}'].value + 2.718281828)"
|
151
|
+
}
|
152
|
+
end
|
153
|
+
|
154
|
+
if options[:user_id] and personalize_field
|
155
|
+
custom_filters << {
|
156
|
+
filter: {
|
157
|
+
term: {
|
158
|
+
personalize_field => options[:user_id]
|
159
|
+
}
|
160
|
+
},
|
161
|
+
boost: 100
|
162
|
+
}
|
163
|
+
end
|
164
|
+
|
165
|
+
if options[:personalize]
|
166
|
+
custom_filters << {
|
167
|
+
filter: {
|
168
|
+
term: options[:personalize]
|
169
|
+
},
|
170
|
+
boost: 100
|
171
|
+
}
|
172
|
+
end
|
173
|
+
|
174
|
+
if custom_filters.any?
|
175
|
+
payload = {
|
176
|
+
custom_filters_score: {
|
177
|
+
query: payload,
|
178
|
+
filters: custom_filters,
|
179
|
+
score_mode: "total"
|
180
|
+
}
|
181
|
+
}
|
182
|
+
end
|
183
|
+
|
184
|
+
payload = {
|
185
|
+
query: payload,
|
186
|
+
size: per_page,
|
187
|
+
from: offset
|
188
|
+
}
|
189
|
+
payload[:explain] = options[:explain] if options[:explain]
|
190
|
+
|
191
|
+
# order
|
192
|
+
if options[:order]
|
193
|
+
order = options[:order].is_a?(Enumerable) ? options[:order] : {options[:order] => :asc}
|
194
|
+
payload[:sort] = Hash[ order.map{|k, v| [k.to_s == "id" ? :_id : k, v] } ]
|
195
|
+
end
|
196
|
+
|
197
|
+
# filters
|
198
|
+
filters = where_filters(options[:where])
|
199
|
+
if filters.any?
|
200
|
+
payload[:filter] = {
|
201
|
+
and: filters
|
202
|
+
}
|
203
|
+
end
|
204
|
+
|
205
|
+
# facets
|
206
|
+
facet_limits = {}
|
207
|
+
if options[:facets]
|
208
|
+
facets = options[:facets] || {}
|
209
|
+
if facets.is_a?(Array) # convert to more advanced syntax
|
210
|
+
facets = Hash[ facets.map{|f| [f, {}] } ]
|
211
|
+
end
|
212
|
+
|
213
|
+
payload[:facets] = {}
|
214
|
+
facets.each do |field, facet_options|
|
215
|
+
# ask for extra facets due to
|
216
|
+
# https://github.com/elasticsearch/elasticsearch/issues/1305
|
217
|
+
|
218
|
+
if facet_options[:ranges]
|
219
|
+
payload[:facets][field] = {
|
220
|
+
range: {
|
221
|
+
field.to_sym => facet_options[:ranges]
|
222
|
+
}
|
223
|
+
}
|
224
|
+
else
|
225
|
+
payload[:facets][field] = {
|
226
|
+
terms: {
|
227
|
+
field: field,
|
228
|
+
size: facet_options[:limit] ? facet_options[:limit] + 150 : 100000
|
229
|
+
}
|
230
|
+
}
|
231
|
+
end
|
232
|
+
|
233
|
+
facet_limits[field] = facet_options[:limit] if facet_options[:limit]
|
234
|
+
|
235
|
+
# offset is not possible
|
236
|
+
# http://elasticsearch-users.115913.n3.nabble.com/Is-pagination-possible-in-termsStatsFacet-td3422943.html
|
237
|
+
|
238
|
+
facet_filters = where_filters(facet_options[:where])
|
239
|
+
if facet_filters.any?
|
240
|
+
payload[:facets][field][:facet_filter] = {
|
241
|
+
and: {
|
242
|
+
filters: facet_filters
|
243
|
+
}
|
244
|
+
}
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
# suggestions
|
250
|
+
if options[:suggest]
|
251
|
+
suggest_fields = (searchkick_options[:suggest] || []).map(&:to_s)
|
252
|
+
# intersection
|
253
|
+
suggest_fields = suggest_fields & options[:fields].map(&:to_s) if options[:fields]
|
254
|
+
if suggest_fields.any?
|
255
|
+
payload[:suggest] = {text: term}
|
256
|
+
suggest_fields.each do |field|
|
257
|
+
payload[:suggest][field] = {
|
258
|
+
phrase: {
|
259
|
+
field: "#{field}.suggest"
|
260
|
+
}
|
261
|
+
}
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
# highlight
|
267
|
+
if options[:highlight]
|
268
|
+
payload[:highlight] = {
|
269
|
+
fields: Hash[ fields.map{|f| [f, {}] } ]
|
270
|
+
}
|
271
|
+
if options[:highlight].is_a?(Hash) and tag = options[:highlight][:tag]
|
272
|
+
payload[:highlight][:pre_tags] = [tag]
|
273
|
+
payload[:highlight][:post_tags] = [tag.to_s.gsub(/\A</, "</")]
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
# model and eagar loading
|
278
|
+
load = options[:load].nil? ? true : options[:load]
|
279
|
+
|
280
|
+
# An empty array will cause only the _id and _type for each hit to be returned
|
281
|
+
# http://www.elasticsearch.org/guide/reference/api/search/fields/
|
282
|
+
payload[:fields] = [] if load
|
283
|
+
|
284
|
+
if options[:type] or klass != searchkick_klass
|
285
|
+
@type = [options[:type] || klass].flatten.map(&:document_type)
|
286
|
+
end
|
287
|
+
|
288
|
+
@body = payload
|
289
|
+
@facet_limits = facet_limits
|
290
|
+
@page = page
|
291
|
+
@per_page = per_page
|
292
|
+
@load = load
|
293
|
+
end
|
294
|
+
|
295
|
+
def searchkick_index
|
296
|
+
klass.searchkick_index
|
297
|
+
end
|
298
|
+
|
299
|
+
def searchkick_options
|
300
|
+
klass.searchkick_options
|
301
|
+
end
|
302
|
+
|
303
|
+
def searchkick_klass
|
304
|
+
klass.searchkick_klass
|
305
|
+
end
|
306
|
+
|
307
|
+
def document_type
|
308
|
+
klass.document_type
|
309
|
+
end
|
310
|
+
|
311
|
+
def execute
|
312
|
+
params = {
|
313
|
+
index: searchkick_index.name,
|
314
|
+
body: body
|
315
|
+
}
|
316
|
+
params.merge!(type: @type) if @type
|
317
|
+
begin
|
318
|
+
response = Searchkick.client.search(params)
|
319
|
+
rescue => e # TODO rescue type
|
320
|
+
status_code = e.message[1..3].to_i
|
321
|
+
if status_code == 404
|
322
|
+
raise "Index missing - run #{searchkick_klass.name}.reindex"
|
323
|
+
elsif status_code == 500 and (e.message.include?("IllegalArgumentException[minimumSimilarity >= 1]") or e.message.include?("No query registered for [multi_match]") or e.message.include?("[match] query does not support [cutoff_frequency]]"))
|
324
|
+
raise "Upgrade Elasticsearch to 0.90.0 or greater"
|
325
|
+
else
|
326
|
+
raise e
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
# apply facet limit in client due to
|
331
|
+
# https://github.com/elasticsearch/elasticsearch/issues/1305
|
332
|
+
@facet_limits.each do |field, limit|
|
333
|
+
field = field.to_s
|
334
|
+
facet = response["facets"][field]
|
335
|
+
response["facets"][field]["terms"] = facet["terms"].first(limit)
|
336
|
+
response["facets"][field]["other"] = facet["total"] - facet["terms"].sum{|term| term["count"] }
|
337
|
+
end
|
338
|
+
|
339
|
+
opts = {
|
340
|
+
page: @page,
|
341
|
+
per_page: @per_page,
|
342
|
+
load: @load,
|
343
|
+
includes: options[:include] || options[:includes]
|
344
|
+
}
|
345
|
+
Searchkick::Results.new(searchkick_klass, response, opts)
|
346
|
+
end
|
347
|
+
|
348
|
+
private
|
349
|
+
|
350
|
+
def where_filters(where)
|
351
|
+
filters = []
|
352
|
+
(where || {}).each do |field, value|
|
353
|
+
field = :_id if field.to_s == "id"
|
354
|
+
|
355
|
+
if field == :or
|
356
|
+
value.each do |or_clause|
|
357
|
+
filters << {or: or_clause.map{|or_statement| {and: where_filters(or_statement)} }}
|
358
|
+
end
|
359
|
+
else
|
360
|
+
# expand ranges
|
361
|
+
if value.is_a?(Range)
|
362
|
+
value = {gte: value.first, (value.exclude_end? ? :lt : :lte) => value.last}
|
363
|
+
end
|
364
|
+
|
365
|
+
if value.is_a?(Array)
|
366
|
+
value = {in: value}
|
367
|
+
end
|
368
|
+
|
369
|
+
if value.is_a?(Hash)
|
370
|
+
value.each do |op, op_value|
|
371
|
+
case op
|
372
|
+
when :within, :bottom_right
|
373
|
+
# do nothing
|
374
|
+
when :near
|
375
|
+
filters << {
|
376
|
+
geo_distance: {
|
377
|
+
field => op_value.map(&:to_f).reverse,
|
378
|
+
distance: value[:within] || "50mi"
|
379
|
+
}
|
380
|
+
}
|
381
|
+
when :top_left
|
382
|
+
filters << {
|
383
|
+
geo_bounding_box: {
|
384
|
+
field => {
|
385
|
+
top_left: op_value.map(&:to_f).reverse,
|
386
|
+
bottom_right: value[:bottom_right].map(&:to_f).reverse
|
387
|
+
}
|
388
|
+
}
|
389
|
+
}
|
390
|
+
when :not # not equal
|
391
|
+
filters << {not: term_filters(field, op_value)}
|
392
|
+
when :all
|
393
|
+
filters << {terms: {field => op_value, execution: "and"}}
|
394
|
+
when :in
|
395
|
+
filters << term_filters(field, op_value)
|
396
|
+
else
|
397
|
+
range_query =
|
398
|
+
case op
|
399
|
+
when :gt
|
400
|
+
{from: op_value, include_lower: false}
|
401
|
+
when :gte
|
402
|
+
{from: op_value, include_lower: true}
|
403
|
+
when :lt
|
404
|
+
{to: op_value, include_upper: false}
|
405
|
+
when :lte
|
406
|
+
{to: op_value, include_upper: true}
|
407
|
+
else
|
408
|
+
raise "Unknown where operator"
|
409
|
+
end
|
410
|
+
# issue 132
|
411
|
+
if existing = filters.find{ |f| f[:range] && f[:range][field] }
|
412
|
+
existing[:range][field].merge!(range_query)
|
413
|
+
else
|
414
|
+
filters << {range: {field => range_query}}
|
415
|
+
end
|
416
|
+
end
|
417
|
+
end
|
418
|
+
else
|
419
|
+
filters << term_filters(field, value)
|
420
|
+
end
|
421
|
+
end
|
422
|
+
end
|
423
|
+
filters
|
424
|
+
end
|
425
|
+
|
426
|
+
def term_filters(field, value)
|
427
|
+
if value.is_a?(Array) # in query
|
428
|
+
if value.any?
|
429
|
+
{or: value.map{|v| term_filters(field, v) }}
|
430
|
+
else
|
431
|
+
{terms: {field => value}} # match nothing
|
432
|
+
end
|
433
|
+
elsif value.nil?
|
434
|
+
{missing: {"field" => field, existence: true, null_value: true}}
|
435
|
+
else
|
436
|
+
{term: {field => value}}
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
440
|
+
end
|
441
|
+
end
|