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