command_search 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/command_search.rb +1 -1
- data/lib/command_search/aliaser.rb +10 -8
- data/lib/command_search/command_dealiaser.rb +4 -4
- data/lib/command_search/lexer.rb +11 -11
- data/lib/command_search/memory.rb +0 -3
- data/lib/command_search/mongoer.rb +26 -30
- data/lib/command_search/optimizer.rb +18 -42
- data/lib/command_search/parser.rb +27 -50
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 93226578c66261e1923eeb6f56647b3eed274b3fe0dad3e8f016780c34e46831
|
4
|
+
data.tar.gz: 5d73e80d4b7f8fbca8febe5dc548332d48cd25f46b7611fae6c075268fdda5b0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a25f5ac8e3b4ba433df88abc38551162532ba7e04e47744e44a0ba7dd107a95471490d0506e98746f1344a259f94a8b2ff229f3468a5078ad4ef97158901b948
|
7
|
+
data.tar.gz: b14a8926f623e7b1ca05e51e5c0fbf1a754012a4e84284166e1fbc29b8894bf47b2fd58841d397575c1859dab46f722a21e0317afff482ec2da40302faddcf19
|
data/lib/command_search.rb
CHANGED
@@ -18,7 +18,7 @@ module CommandSearch
|
|
18
18
|
|
19
19
|
aliased_query = Aliaser.alias(query, aliases)
|
20
20
|
tokens = Lexer.lex(aliased_query)
|
21
|
-
parsed = Parser.parse(tokens)
|
21
|
+
parsed = Parser.parse!(tokens)
|
22
22
|
dealiased = CommandDealiaser.dealias(parsed, command_fields)
|
23
23
|
cleaned = CommandDealiaser.decompose_unaliasable(dealiased, command_fields)
|
24
24
|
opted = Optimizer.optimize(cleaned)
|
@@ -8,32 +8,34 @@ module CommandSearch
|
|
8
8
|
Regexp.new(head_border + Regexp.escape(str) + tail_border, 'i')
|
9
9
|
end
|
10
10
|
|
11
|
-
def
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
end
|
16
|
-
str[/"/] || str[/\B'/]
|
11
|
+
def quotes?(head, tail)
|
12
|
+
return true if head.count("'").odd? && tail.count("'").odd?
|
13
|
+
return true if head.count('"').odd? && tail.count('"').odd?
|
14
|
+
false
|
17
15
|
end
|
18
16
|
|
19
17
|
def alias_item(query, alias_key, alias_value)
|
20
18
|
if alias_key.is_a?(Regexp)
|
21
19
|
pattern = alias_key
|
20
|
+
elsif alias_key.is_a?(String)
|
21
|
+
pattern = build_regex(alias_key)
|
22
22
|
else
|
23
|
-
|
23
|
+
return query
|
24
24
|
end
|
25
25
|
current_match = query[pattern]
|
26
26
|
return query unless current_match
|
27
27
|
offset = Regexp.last_match.offset(0)
|
28
28
|
head = query[0...offset.first]
|
29
29
|
tail = alias_item(query[offset.last..-1], alias_key, alias_value)
|
30
|
-
if
|
30
|
+
if quotes?(head, tail)
|
31
31
|
replacement = current_match
|
32
32
|
else
|
33
33
|
if alias_value.is_a?(String)
|
34
34
|
replacement = alias_value
|
35
35
|
elsif alias_value.is_a?(Proc)
|
36
36
|
replacement = alias_value.call(current_match).to_s
|
37
|
+
else
|
38
|
+
return query
|
37
39
|
end
|
38
40
|
end
|
39
41
|
head + replacement + tail
|
@@ -23,9 +23,9 @@ module CommandSearch
|
|
23
23
|
end
|
24
24
|
|
25
25
|
def dealias(ast, aliases)
|
26
|
-
ast.
|
26
|
+
ast.map! do |x|
|
27
27
|
next x unless x[:nest_type]
|
28
|
-
|
28
|
+
dealias(x[:value], aliases)
|
29
29
|
next x unless [:colon, :compare].include?(x[:nest_type])
|
30
30
|
x[:value] = dealias_values(x[:value], aliases)
|
31
31
|
x
|
@@ -33,9 +33,9 @@ module CommandSearch
|
|
33
33
|
end
|
34
34
|
|
35
35
|
def decompose_unaliasable(ast, aliases)
|
36
|
-
ast.
|
36
|
+
ast.map! do |x|
|
37
37
|
next x unless x[:nest_type]
|
38
|
-
|
38
|
+
decompose_unaliasable(x[:value], aliases)
|
39
39
|
next x unless [:colon, :compare].include?(x[:nest_type])
|
40
40
|
unnest_unaliased(x, aliases)
|
41
41
|
end
|
data/lib/command_search/lexer.rb
CHANGED
@@ -8,29 +8,29 @@ module CommandSearch
|
|
8
8
|
while i < input.length
|
9
9
|
match = nil
|
10
10
|
case input[i..-1]
|
11
|
-
when
|
11
|
+
when /\A\s+/
|
12
12
|
type = :space
|
13
|
-
when
|
13
|
+
when /\A"(.*?)"/
|
14
14
|
match = Regexp.last_match[1]
|
15
15
|
type = :quoted_str
|
16
|
-
when
|
16
|
+
when /\A'(.*?)'/
|
17
17
|
match = Regexp.last_match[1]
|
18
18
|
type = :quoted_str
|
19
|
-
when
|
19
|
+
when /\A\-?\d+(\.\d+)?(?=$|[\s"':|<>()])/
|
20
20
|
type = :number
|
21
|
-
when
|
21
|
+
when /\A-/
|
22
22
|
type = :minus
|
23
|
-
when
|
23
|
+
when /\A[^\s:"|<>()]+/
|
24
24
|
type = :str
|
25
|
-
when
|
25
|
+
when /\A\|+/
|
26
26
|
type = :pipe
|
27
|
-
when
|
27
|
+
when /\A[()]/
|
28
28
|
type = :paren
|
29
|
-
when
|
29
|
+
when /\A:/
|
30
30
|
type = :colon
|
31
|
-
when
|
31
|
+
when /\A[<>]=?/
|
32
32
|
type = :compare
|
33
|
-
when
|
33
|
+
when /\A./
|
34
34
|
type = :str
|
35
35
|
end
|
36
36
|
match = match || Regexp.last_match[0]
|
@@ -10,7 +10,6 @@ module CommandSearch
|
|
10
10
|
raw_cmd_type = [command_types[cmd]].flatten
|
11
11
|
allow_existence_boolean = raw_cmd_type.include?(:allow_existence_boolean)
|
12
12
|
cmd_type = (raw_cmd_type - [:allow_existence_boolean]).first
|
13
|
-
return unless cmd_type
|
14
13
|
if cmd_type == Boolean
|
15
14
|
if cmd_search[/true/i]
|
16
15
|
item[cmd]
|
@@ -25,8 +24,6 @@ module CommandSearch
|
|
25
24
|
end
|
26
25
|
elsif !item.key?(cmd)
|
27
26
|
return false
|
28
|
-
elsif val[1][:type] == :str
|
29
|
-
item[cmd][/#{Regexp.escape(cmd_search)}/i]
|
30
27
|
elsif val[1][:type] == :quoted_str
|
31
28
|
regex = /\b#{Regexp.escape(cmd_search)}\b/
|
32
29
|
if cmd_search[/(^\W)|(\W$)/]
|
@@ -5,7 +5,7 @@ module CommandSearch
|
|
5
5
|
module_function
|
6
6
|
|
7
7
|
def build_search(ast_node, fields)
|
8
|
-
str = ast_node[:value]
|
8
|
+
str = ast_node[:value] || ''
|
9
9
|
fields = [fields] unless fields.is_a?(Array)
|
10
10
|
if ast_node[:type] == :quoted_str
|
11
11
|
regex = /\b#{Regexp.escape(str)}\b/
|
@@ -15,9 +15,7 @@ module CommandSearch
|
|
15
15
|
regex = Regexp.new(head_border + Regexp.escape(str) + tail_border)
|
16
16
|
end
|
17
17
|
else
|
18
|
-
|
19
|
-
# and is only needed for outside use or benchmarking.
|
20
|
-
regex = /#{Regexp.escape(str || '')}/i
|
18
|
+
regex = /#{Regexp.escape(str)}/i
|
21
19
|
end
|
22
20
|
if ast_node[:negate]
|
23
21
|
forms = fields.map { |f| { f => { '$not' => regex } } }
|
@@ -33,17 +31,16 @@ module CommandSearch
|
|
33
31
|
end
|
34
32
|
|
35
33
|
def is_bool_str?(str)
|
36
|
-
return true if str[
|
34
|
+
return true if str[/\Atrue\Z|\Afalse\Z/i]
|
37
35
|
false
|
38
36
|
end
|
39
37
|
|
40
38
|
def make_boolean(str)
|
41
|
-
return true if str[
|
39
|
+
return true if str[/\Atrue\Z/i]
|
42
40
|
false
|
43
41
|
end
|
44
42
|
|
45
43
|
def build_command(ast_node, command_types)
|
46
|
-
# aliasing will is done before ast gets to mongoer.rb
|
47
44
|
(field_node, search_node) = ast_node[:value]
|
48
45
|
key = field_node[:value]
|
49
46
|
raw_type = command_types[key.to_sym]
|
@@ -60,8 +57,7 @@ module CommandSearch
|
|
60
57
|
type = raw_type
|
61
58
|
end
|
62
59
|
|
63
|
-
if
|
64
|
-
# val = make_boolean(raw_val)
|
60
|
+
if type == Boolean
|
65
61
|
bool = make_boolean(raw_val)
|
66
62
|
bool = !bool if field_node[:negate]
|
67
63
|
val = [
|
@@ -100,7 +96,7 @@ module CommandSearch
|
|
100
96
|
elsif [Numeric, Integer].include?(type)
|
101
97
|
if raw_val == raw_val.to_i.to_s
|
102
98
|
val = raw_val.to_i
|
103
|
-
elsif raw_val.to_f != 0 || raw_val[
|
99
|
+
elsif raw_val.to_f != 0 || raw_val[/\A[\.0]*0\Z/]
|
104
100
|
val = raw_val.to_f
|
105
101
|
else
|
106
102
|
val = raw_val
|
@@ -205,40 +201,41 @@ module CommandSearch
|
|
205
201
|
{ key => { mongo_op => val } }
|
206
202
|
end
|
207
203
|
|
208
|
-
def build_searches(ast, fields, command_types)
|
209
|
-
ast.
|
204
|
+
def build_searches!(ast, fields, command_types)
|
205
|
+
ast.map! do |x|
|
210
206
|
type = x[:nest_type]
|
211
207
|
if type == :colon
|
212
208
|
build_command(x, command_types)
|
213
209
|
elsif type == :compare
|
214
210
|
build_compare(x, command_types)
|
215
211
|
elsif [:paren, :pipe, :minus].include?(type)
|
216
|
-
|
212
|
+
build_searches!(x[:value], fields, command_types)
|
217
213
|
x
|
218
214
|
else
|
219
215
|
build_search(x, fields)
|
220
216
|
end
|
221
217
|
end
|
218
|
+
ast.flatten!
|
222
219
|
end
|
223
220
|
|
224
|
-
def build_tree(ast)
|
225
|
-
|
221
|
+
def build_tree!(ast)
|
222
|
+
mongo_types = { paren: '$and', pipe: '$or', minus: '$not' }
|
223
|
+
ast.each do |x|
|
226
224
|
next x unless x[:nest_type]
|
227
|
-
|
225
|
+
build_tree!(x[:value])
|
228
226
|
key = mongo_types[x[:nest_type]]
|
229
|
-
|
227
|
+
x[key] = x[:value]
|
228
|
+
x.delete(:nest_type)
|
229
|
+
x.delete(:nest_op)
|
230
|
+
x.delete(:value)
|
231
|
+
x.delete(:type)
|
230
232
|
end
|
231
233
|
end
|
232
234
|
|
233
|
-
def collapse_ors(ast)
|
234
|
-
ast.
|
235
|
-
['$
|
236
|
-
|
237
|
-
x[key] = collapse_ors(x[key])
|
238
|
-
end
|
239
|
-
next x unless x['$or']
|
240
|
-
val = x['$or'].flat_map { |kid| kid['$or'] || kid }
|
241
|
-
{ '$or' => val }
|
235
|
+
def collapse_ors!(ast)
|
236
|
+
ast.each do |x|
|
237
|
+
next unless x['$or']
|
238
|
+
x['$or'].map! { |kid| kid['$or'] || kid }.flatten!
|
242
239
|
end
|
243
240
|
end
|
244
241
|
|
@@ -257,12 +254,11 @@ module CommandSearch
|
|
257
254
|
end
|
258
255
|
|
259
256
|
def build_query(ast, fields, command_types = {})
|
260
|
-
# Numbers are searched as strings unless part of a compare/command
|
261
257
|
out = ast
|
262
258
|
out = decompose_nots(out)
|
263
|
-
|
264
|
-
|
265
|
-
|
259
|
+
build_searches!(out, fields, command_types)
|
260
|
+
build_tree!(out)
|
261
|
+
collapse_ors!(out)
|
266
262
|
out = {} if out == []
|
267
263
|
out = out.first if out.count == 1
|
268
264
|
out = { '$and' => out } if out.count > 1
|
@@ -2,18 +2,16 @@ module CommandSearch
|
|
2
2
|
module Optimizer
|
3
3
|
module_function
|
4
4
|
|
5
|
-
def ands_and_ors(ast)
|
6
|
-
ast.
|
7
|
-
next node unless node[:nest_type]
|
8
|
-
|
9
|
-
node[:value]
|
5
|
+
def ands_and_ors!(ast)
|
6
|
+
ast.map! do |node|
|
7
|
+
next node unless node[:nest_type] == :paren || node[:nest_type] == :pipe
|
8
|
+
ands_and_ors!(node[:value])
|
9
|
+
next node[:value].first if node[:value].length < 2
|
10
10
|
node[:value] = node[:value].flat_map do |kid|
|
11
11
|
next kid[:value] if kid[:nest_type] == :pipe
|
12
12
|
kid
|
13
13
|
end
|
14
|
-
|
15
|
-
next node[:value].first
|
16
|
-
end
|
14
|
+
node[:value].uniq!
|
17
15
|
node
|
18
16
|
end
|
19
17
|
end
|
@@ -23,7 +21,6 @@ module CommandSearch
|
|
23
21
|
next node unless node[:nest_type]
|
24
22
|
node[:value] = negate_negate(node[:value])
|
25
23
|
next [] if node[:value] == []
|
26
|
-
next node if node[:value].count > 1
|
27
24
|
type = node[:nest_type]
|
28
25
|
child_type = node[:value].first[:nest_type]
|
29
26
|
next node unless type == :minus && child_type == :minus
|
@@ -34,51 +31,30 @@ module CommandSearch
|
|
34
31
|
def denest_parens(ast, parent_type = :root)
|
35
32
|
ast.flat_map do |node|
|
36
33
|
next node unless node[:nest_type]
|
37
|
-
|
38
34
|
node[:value] = denest_parens(node[:value], node[:nest_type])
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
next node[:value] if valid_self && valid_parent
|
45
|
-
next node[:value] if valid_self && valid_child
|
35
|
+
# valid_self && (valid_parent || valid_child)
|
36
|
+
if node[:nest_type] == :paren && (parent_type != :pipe || node[:value].count < 2)
|
37
|
+
next node[:value]
|
38
|
+
end
|
46
39
|
node
|
47
40
|
end
|
48
41
|
end
|
49
42
|
|
50
|
-
def remove_empty_strings(ast)
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
node[:value] = remove_empty_strings(node[:value])
|
55
|
-
node
|
43
|
+
def remove_empty_strings!(ast)
|
44
|
+
ast.reject! do |node|
|
45
|
+
remove_empty_strings!(node[:value]) if node[:nest_type]
|
46
|
+
node[:type] == :quoted_str && node[:value] == ''
|
56
47
|
end
|
57
|
-
out.compact
|
58
48
|
end
|
59
49
|
|
60
|
-
def
|
61
|
-
# '(a b)|(c d)' is the only current
|
62
|
-
# situation where parens are needed.
|
63
|
-
# 'a|(b|(c|d))' can be flattened by
|
64
|
-
# repeated application of "ands_and_or"
|
65
|
-
# and "denest_parens".
|
50
|
+
def optimize(ast)
|
66
51
|
out = ast
|
67
52
|
out = denest_parens(out)
|
53
|
+
remove_empty_strings!(out)
|
68
54
|
out = negate_negate(out)
|
69
|
-
|
70
|
-
out
|
55
|
+
ands_and_ors!(out)
|
56
|
+
out.uniq!
|
71
57
|
out
|
72
58
|
end
|
73
|
-
|
74
|
-
def optimize(ast)
|
75
|
-
out_a = optimization_pass(ast)
|
76
|
-
out_b = optimization_pass(out_a)
|
77
|
-
until out_a == out_b
|
78
|
-
out_a = out_b
|
79
|
-
out_b = optimization_pass(out_b)
|
80
|
-
end
|
81
|
-
out_b
|
82
|
-
end
|
83
59
|
end
|
84
60
|
end
|
@@ -3,7 +3,6 @@ module CommandSearch
|
|
3
3
|
module_function
|
4
4
|
|
5
5
|
def parens_rindex(input)
|
6
|
-
val_list = input.map { |x| x[:value] }
|
7
6
|
open_i = input.rindex { |x| x[:value] == '(' && x[:type] == :paren}
|
8
7
|
return unless open_i
|
9
8
|
close_offset = input.drop(open_i).index { |x| x[:value] == ')' && x[:type] == :paren}
|
@@ -11,48 +10,41 @@ module CommandSearch
|
|
11
10
|
[open_i, close_offset + open_i]
|
12
11
|
end
|
13
12
|
|
14
|
-
def group_parens(input)
|
15
|
-
|
16
|
-
|
17
|
-
(a
|
18
|
-
|
19
|
-
out[a..b] = { type: :nest, nest_type: :paren, value: val }
|
13
|
+
def group_parens!(input)
|
14
|
+
while parens_rindex(input)
|
15
|
+
(a, b) = parens_rindex(input)
|
16
|
+
val = input[(a + 1)..(b - 1)]
|
17
|
+
input[a..b] = { type: :nest, nest_type: :paren, value: val }
|
20
18
|
end
|
21
|
-
out
|
22
19
|
end
|
23
20
|
|
24
21
|
def cluster!(type, input, cluster_type = :binary)
|
25
22
|
binary = (cluster_type == :binary)
|
26
|
-
|
27
|
-
out = out[:value] while out.is_a?(Hash)
|
28
|
-
out.compact!
|
23
|
+
input.compact!
|
29
24
|
# rindex (vs index) important for nested prefixes
|
30
|
-
while (i =
|
31
|
-
val = [
|
32
|
-
val.unshift(
|
25
|
+
while (i = input.rindex { |x| x[:type] == type })
|
26
|
+
val = [input[i + 1]]
|
27
|
+
val.unshift(input[i - 1]) if binary && i > 0
|
33
28
|
front_offset = 0
|
34
29
|
front_offset = 1 if binary && i > 0
|
35
|
-
|
30
|
+
input[(i - front_offset)..(i + 1)] = {
|
36
31
|
type: :nest,
|
37
32
|
nest_type: type,
|
38
|
-
nest_op:
|
33
|
+
nest_op: input[i][:value],
|
39
34
|
value: val
|
40
35
|
}
|
41
36
|
end
|
42
|
-
|
43
|
-
|
44
|
-
x[:value] = cluster!(type, x[:value], cluster_type)
|
45
|
-
x
|
37
|
+
input.each do |x|
|
38
|
+
cluster!(type, x[:value], cluster_type) if x[:type] == :nest
|
46
39
|
end
|
47
40
|
end
|
48
41
|
|
49
42
|
def unchain!(types, input)
|
50
43
|
i = 0
|
51
44
|
while i < input.length - 2
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
if types.include?(front) && !types.include?(mid) && types.include?(back)
|
45
|
+
left = input[i][:type]
|
46
|
+
right = input[i + 2][:type]
|
47
|
+
if types.include?(left) && types.include?(right)
|
56
48
|
input.insert(i + 1, input[i + 1])
|
57
49
|
end
|
58
50
|
i += 1
|
@@ -60,7 +52,6 @@ module CommandSearch
|
|
60
52
|
end
|
61
53
|
|
62
54
|
def merge_strs(input, (x, y))
|
63
|
-
return input if input.empty?
|
64
55
|
if input[y] && input[y][:type] == :str
|
65
56
|
values = input.map { |x| x[:value] }
|
66
57
|
{ type: :str, value: values.join() }
|
@@ -71,8 +62,6 @@ module CommandSearch
|
|
71
62
|
end
|
72
63
|
|
73
64
|
def clean_ununusable!(input)
|
74
|
-
return unless input.any?
|
75
|
-
|
76
65
|
i = 0
|
77
66
|
while i < input.length
|
78
67
|
next i += 1 unless input[i][:type] == :minus
|
@@ -97,31 +86,19 @@ module CommandSearch
|
|
97
86
|
end
|
98
87
|
|
99
88
|
def clean_ununused!(input)
|
100
|
-
input.
|
101
|
-
next if x[:type] == :paren && x[:value].is_a?(String)
|
102
|
-
next if x[:nest_type] == :colon && x[:value].empty?
|
103
|
-
if x[:nest_type] == :compare && x[:value].length < 2
|
104
|
-
x = clean_ununused!(x[:value]).first
|
105
|
-
end
|
106
|
-
next x unless x && x[:type] == :nest
|
107
|
-
x[:value] = clean_ununused!(x[:value])
|
108
|
-
x
|
109
|
-
end
|
110
|
-
input.compact!
|
111
|
-
input
|
89
|
+
input.reject! { |x| x[:type] == :paren && x[:value].is_a?(String) }
|
112
90
|
end
|
113
91
|
|
114
|
-
def parse(input)
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
cluster!(:
|
120
|
-
cluster!(:
|
121
|
-
cluster!(:
|
122
|
-
|
123
|
-
|
124
|
-
out
|
92
|
+
def parse!(input)
|
93
|
+
clean_ununusable!(input)
|
94
|
+
unchain!([:colon, :compare], input)
|
95
|
+
group_parens!(input)
|
96
|
+
cluster!(:colon, input)
|
97
|
+
cluster!(:compare, input)
|
98
|
+
cluster!(:minus, input, :prefix)
|
99
|
+
cluster!(:pipe, input)
|
100
|
+
clean_ununused!(input)
|
101
|
+
input
|
125
102
|
end
|
126
103
|
end
|
127
104
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: command_search
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- zumbalogy
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-11-
|
11
|
+
date: 2018-11-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: chronic
|