command_search 0.2.0 → 0.3.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/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
|