This repository has been archived by the owner on Nov 7, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 136
/
Copy pathquery_builder.rb
158 lines (139 loc) · 5.42 KB
/
query_builder.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
module DataMagic
module QueryBuilder
class << self
# Creates query from parameters passed into endpoint
def from_params(params, options, config)
per_page = (options[:per_page] || config.page_size || DataMagic::DEFAULT_PAGE_SIZE).to_i
page = options[:page].to_i || 0
per_page = DataMagic::MAX_PAGE_SIZE if per_page > DataMagic::MAX_PAGE_SIZE
query_hash = {
from: page * per_page,
size: per_page,
}
query_hash[:query] = generate_squery(params, options, config).to_search
if options[:command] == 'stats'
query_hash.merge! add_aggregations(params, options, config)
end
if options[:fields] && !options[:fields].empty?
query_hash[:fields] = get_restrict_fields(options)
query_hash[:_source] = false
else
query_hash[:_source] = {
exclude: ["_*"]
}
end
query_hash[:sort] = get_sort_order(options[:sort], config) if options[:sort] && !options[:sort].empty?
query_hash
end
private
def generate_squery(params, options, config)
squery = Stretchy.query(type: 'document')
squery = search_location(squery, options)
search_fields_and_ranges(squery, params, config)
end
# Wrapper for Stretchy aggregation clause builder (which wraps ElasticSearch (ES) :aggs parameter)
# Extracts all extended_stats aggregations from ES, to be filtered later
# Is a no-op if no fields are specified, or none of them are numeric
def add_aggregations(params, options, config)
agg_hash = options[:fields].inject({}) do |memo, f|
if config.column_field_types[f.to_s] && ["integer", "float"].include?(config.column_field_types[f.to_s])
memo[f.to_s] = { extended_stats: { "field" => f.to_s } }
end
memo
end
agg_hash.empty? ? {} : { aggs: agg_hash }
end
def get_restrict_fields(options)
options[:fields].map(&:to_s)
end
# @description turns a string like "state,population:desc" into [{'state' => {order: 'asc'}},{ "population" => {order: "desc"} }]
# @param [String] sort_param
# @return [Array]
def get_sort_order(sort_param, config)
sort_param.to_s.scan(/(\w+[\.\w]*):?(\w*)/).map do |field_name, direction|
direction = 'asc' if direction.empty?
type = config.field_type(field_name)
# for 'autocomplete' search on lowercase not analyzed indexed in _name
field_name = "_#{field_name}" if type == 'autocomplete'
{ field_name => { order: direction } }
end
end
def to_number(value)
value =~ /\./ ? value.to_f : value.to_i
end
def search_fields_and_ranges(squery, params, config)
params.each do |param, value|
field_type = config.field_type(param)
if field_type == "name"
squery = include_name_query(squery, param, value)
elsif field_type == "autocomplete"
squery = autocomplete_query(squery, param, value)
elsif match = /(.+)__(range|ne|not)\z/.match(param)
field, operator = match.captures.map(&:to_sym)
squery = range_query(squery, operator, field, value)
elsif field_type == "integer" && value.is_a?(String) && /,/.match(value) # list of integers
squery = integer_list_query(squery, param, value)
else # field equality
squery = squery.where(param => value)
end
end
squery
end
def include_name_query(squery, field, value)
value = value.split(' ').map { |word| "#{word}*"}.join(' ')
squery.match.query(
# we store lowercase name in field with prefix _
"wildcard": { "_#{field}" => { "value": value.downcase } }
)
end
def range_query(squery, operator, field, value)
if operator == :ne or operator == :not # field negation
squery.where.not(field => value)
else # field range
squery.filter(
or: build_ranges(field, value.split(','))
)
end
end
def autocomplete_query(squery, field, value)
squery.match.query(
common: {
field => {
query: value,
cutoff_frequency: 0.001,
low_freq_operator: "and"
}
})
end
def integer_list_query(squery, field, value)
squery.filter(
terms: {
field => value.split(',').map(&:to_i) }
)
end
def build_ranges(field, range_strings)
range_strings.map do |range|
min, max = range.split('..')
values = {}
values[:gte] = to_number(min) unless min.empty?
values[:lte] = to_number(max) if max
{
range: { field => values }
}
end
end
# Handles location (currently only uses SFO location)
def search_location(squery, options)
distance = options[:distance]
location = Zipcode.latlon(options[:zip])
if distance && !distance.empty?
# default to miles if no distance given
unit = distance[-2..-1]
distance = "#{distance}mi" if unit != "km" and unit != "mi"
squery = squery.geo('location', distance: distance, lat: location[:lat], lng: location[:lon])
end
squery
end
end
end
end