plunk 0.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 355fabb9e6a73101c70a9990992abe5974626a76
4
+ data.tar.gz: d61e5388b448da8412899038e8b0e3a72f3a81ac
5
+ SHA512:
6
+ metadata.gz: 94c4b45f0018ac30b45b9922625d7ca25f7c28e8ff7506f831ad70e246179812e55b66b794611dc2f86a22e41ba0f6f7f53087f0bc8e0ddfc7d12ce81f108593
7
+ data.tar.gz: e957db51d157857e99a2b69138d678d0375ad27e98c594c30fff19e355a628d5a16d36f09d5b88b646c3c9af8d4cba687c04ab7f652ff87e827a6743f70d6ade
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ coverage
6
+ InstalledFiles
7
+ lib/bundler/man
8
+ pkg
9
+ rdoc
10
+ spec/reports
11
+ test/tmp
12
+ test/version_tmp
13
+ tmp
14
+
15
+ # YARD artifacts
16
+ .yardoc
17
+ _yardoc
18
+ doc/
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,34 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ plunk (0.0.0)
5
+ json
6
+ parslet
7
+ rest-client
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ blankslate (2.1.2.4)
13
+ diff-lcs (1.2.5)
14
+ json (1.8.1)
15
+ mime-types (2.0)
16
+ parslet (1.5.0)
17
+ blankslate (~> 2.0)
18
+ rest-client (1.6.7)
19
+ mime-types (>= 1.16)
20
+ rspec (2.14.1)
21
+ rspec-core (~> 2.14.0)
22
+ rspec-expectations (~> 2.14.0)
23
+ rspec-mocks (~> 2.14.0)
24
+ rspec-core (2.14.7)
25
+ rspec-expectations (2.14.4)
26
+ diff-lcs (>= 1.1.3, < 2.0)
27
+ rspec-mocks (2.14.4)
28
+
29
+ PLATFORMS
30
+ ruby
31
+
32
+ DEPENDENCIES
33
+ plunk!
34
+ rspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2013 Elbii
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,4 @@
1
+ plunk
2
+ =====
3
+
4
+ Human-friendly query language for Elasticsearch
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ RSpec::Core::RakeTask.new(:spec)
4
+
5
+ task :default => :spec
data/lib/plunk.rb ADDED
@@ -0,0 +1,3 @@
1
+ class Plunk
2
+
3
+ end
@@ -0,0 +1,121 @@
1
+ require 'json'
2
+ require 'rest-client'
3
+
4
+ # Wrapper for Elasticsearch API
5
+ class Plunk::Elasticsearch
6
+ attr_accessor :host, :port, :fields
7
+
8
+ def initialize(opts={})
9
+ @scheme = opts[:scheme] || "http"
10
+ @host = opts[:host] || "localhost"
11
+ @port = opts[:port] || "9200"
12
+ @size = opts[:size] || "10000"
13
+ @endpoint = "#{@scheme}://#{@host}:#{@port}"
14
+ end
15
+
16
+ # Get list of all field mappings stored in ES
17
+ # TODO: cache response from ES
18
+ def available_fields
19
+ uri = URI.escape "#{@endpoint}/_mapping"
20
+ result = JSON.parse(RestClient.get(uri))
21
+
22
+ @fields = {}
23
+ Plunk::Elasticsearch.nested_values(result, 'properties')
24
+
25
+ @fields
26
+ end
27
+
28
+ def active_record_errors_for(query)
29
+ return " can't be blank." if query.blank?
30
+
31
+ uri = URI.escape "#{@endpoint}/_validate/query?explain=true"
32
+ response = RestClient.post(uri, build_ES_validator(query))
33
+
34
+ json = JSON.parse(response)
35
+
36
+ json['valid'] ? nil : json['explanations'].collect { |exp| exp['error'] }
37
+ end
38
+
39
+ #
40
+ # UTILITY METHODS
41
+ #
42
+ def self.search(query)
43
+ uri = URI.escape "#{@endpoint}/_search?size=#{@size}"
44
+
45
+ RestClient.post uri, build_ES_query(query)
46
+ end
47
+
48
+ # returns all values for all occurences of the given nested key
49
+ def self.nested_values(hash, key)
50
+ hash.each do |k, v|
51
+ if k == key
52
+ @fields.merge! v
53
+ else
54
+ nested_values(v, key) if v.is_a? Hash
55
+ end
56
+ end
57
+ end
58
+
59
+ # nested field matcher
60
+ def self.extract_values(hash, keys)
61
+ @vals ||= []
62
+
63
+ hash.each_pair do |k, v|
64
+ if v.is_a? Hash
65
+ extract_values(v, keys)
66
+ elsif v.is_a? Array
67
+ v.flatten!
68
+ if v.first.is_a? Hash
69
+ v.each { |el| extract_values(el, keys) }
70
+ elsif keys.include? k
71
+ @vals += v
72
+ end
73
+ elsif keys.include? k
74
+ @vals << v
75
+ end
76
+ end
77
+
78
+ return @vals
79
+ end
80
+
81
+ def self.build_ES_validator(query)
82
+ if valid_json? query
83
+ # Strip the top-level "query" paramter since ES doesn't expect it
84
+ JSON.parse(query)['query'].to_json
85
+ else
86
+ <<-END
87
+ {
88
+ "query_string": {
89
+ "query": "#{query}"
90
+ }
91
+ }
92
+ END
93
+ end
94
+ end
95
+
96
+ def self.build_ES_query(query)
97
+ if valid_json? query
98
+ query
99
+ else
100
+ <<-END
101
+ {
102
+ "query": {
103
+ "query_string": {
104
+ "query": "#{query}"
105
+ }
106
+ }
107
+ }
108
+ END
109
+ end
110
+ end
111
+
112
+ def self.valid_json?(json)
113
+ begin
114
+ JSON.parse json
115
+ true
116
+ rescue JSON::ParserError
117
+ false
118
+ end
119
+ end
120
+ end
121
+
@@ -0,0 +1,84 @@
1
+ require 'parslet'
2
+
3
+ class Plunk::Parser < Parslet::Parser
4
+ # Single character rules
5
+ rule(:lparen) { str('(') >> space? }
6
+ rule(:rparen) { str(')') >> space? }
7
+ rule(:comma) { str(',') >> space? }
8
+ rule(:digit) { match('[0-9]') }
9
+ rule(:space) { match('\s').repeat(1) }
10
+ rule(:space?) { space.maybe }
11
+
12
+ # Numbers
13
+ rule(:integer) { str('-').maybe >> digit.repeat(1) >> space? }
14
+ rule(:float) {
15
+ str('-').maybe >> digit.repeat(1) >> str('.') >> digit.repeat(1) >> space?
16
+ }
17
+ rule(:number) { integer | float }
18
+
19
+ # Field / value
20
+ rule(:identifier) { match['_@a-zA-Z.'].repeat(1) }
21
+ rule(:wildcard) { match('[a-zA-Z0-9.*]').repeat(1) }
22
+ rule(:searchop) { match('[=]').as(:op) >> space? }
23
+
24
+ # boolean operators search
25
+ rule(:concatop) { (str('OR') | str('AND')) >> space? }
26
+ rule(:operator) { match('[|]').as(:op) >> space? }
27
+ rule(:timerange) {
28
+ integer.as(:quantity) >> match('s|m|h|d|w').as(:quantifier)
29
+ }
30
+
31
+ # Grammar parts
32
+ rule(:rhs) {
33
+ (regex | wildcard | integer | subsearch |
34
+ (lparen >> (space? >> (wildcard | integer) >>
35
+ (space >> concatop).maybe).repeat(1) >> rparen))
36
+ }
37
+
38
+ rule(:regex) {
39
+ str('/') >> match('[^/]').repeat >> str('/')
40
+ }
41
+
42
+ rule(:search) {
43
+ identifier.as(:field) >> space? >> searchop >> space? >>
44
+ rhs.as(:value) | rhs.as(:match)
45
+ }
46
+
47
+ rule(:last) {
48
+ str("last") >> space >> timerange.as(:timerange) >> space >>
49
+ search.as(:search)
50
+ }
51
+
52
+ rule(:binaryop) {
53
+ (paren | search).as(:left) >> space? >> operator >> job.as(:right)
54
+ }
55
+
56
+ rule(:subsearch) {
57
+ str('`') >> space? >> nested_search >> str('`')
58
+ }
59
+
60
+ rule(:nested_search) {
61
+ match('[^|]').repeat.as(:term) >> str('|') >> space? >>
62
+ match('[^`]').repeat.as(:extractors)
63
+ }
64
+
65
+ rule(:joined_search) {
66
+
67
+ }
68
+
69
+ rule(:paren) {
70
+ lparen >> space? >> job >> space? >> rparen
71
+ }
72
+
73
+ rule(:job) {
74
+ binaryop | paren | last | search
75
+ }
76
+
77
+ # root :job
78
+ rule(:plunk_query) {
79
+ job >> (space >> job).repeat
80
+ }
81
+
82
+ root :plunk_query
83
+ end
84
+
@@ -0,0 +1,38 @@
1
+ require_relative 'elasticsearch'
2
+
3
+ class Plunk::ResultSet
4
+ attr_accessor :query
5
+
6
+ def initialize(opts=nil)
7
+ if opts
8
+ @query = { query: { }}
9
+
10
+ if opts[:query_string]
11
+ @query[:query][:query_string] = { query: opts[:query_string] }
12
+ end
13
+
14
+ if opts[:start_time] and opts[:end_time]
15
+ @query[:query][:range] = {
16
+ '@timestamp' => {
17
+ gte: opts[:start_time],
18
+ lte: opts[:end_time]
19
+ }
20
+ }
21
+ end
22
+ end
23
+ end
24
+
25
+ def *(rs)
26
+ self.join(rs)
27
+ end
28
+
29
+ def join(rs)
30
+ ResultSet.new("[ #{@query} * #{rs.query} ]")
31
+ end
32
+
33
+ def eval
34
+ return unless @query
35
+ Elasticsearch.search(@query.to_json)
36
+ end
37
+ end
38
+
@@ -0,0 +1,64 @@
1
+ require 'parslet'
2
+
3
+ class Plunk::Transformer < Parslet::Transform
4
+
5
+ rule(match: simple(:value)) do
6
+ ResultSet.new(query_string: "#{value}")
7
+ end
8
+
9
+ rule(
10
+ field: simple(:field),
11
+ value: {
12
+ term: simple(:term),
13
+ extractors: simple(:extractors)
14
+ },
15
+ op: '=') do
16
+
17
+ rs = ResultSet.new(query_string: "#{field}:#{term}")
18
+
19
+ json = JSON.parse rs.eval
20
+ values = Elasticsearch.extract_values json, extractors.to_s.split(',')
21
+
22
+ if values.empty?
23
+ ResultSet.new
24
+ else
25
+ ResultSet.new(query_string: "(#{values.uniq.join(' OR ')})")
26
+ end
27
+ end
28
+
29
+ rule(field: simple(:field), value: simple(:value), op: '=') do
30
+ ResultSet.new(query_string: "#{field}:#{value}")
31
+ end
32
+
33
+ rule(
34
+ search: simple(:query),
35
+ timerange: {
36
+ quantity: simple(:quantity),
37
+ quantifier: simple(:quantifier)
38
+ }) do
39
+
40
+ int_quantity = quantity.to_s.to_i
41
+
42
+ start_time =
43
+ case quantifier
44
+ when 's'
45
+ int_quantity.seconds.ago
46
+ when 'm'
47
+ int_quantity.minutes.ago
48
+ when 'h'
49
+ int_quantity.hours.ago
50
+ when 'd'
51
+ int_quantity.days.ago
52
+ when 'w'
53
+ int_quantity.weeks.ago
54
+ end
55
+
56
+ end_time = Time.now.utc.to_datetime
57
+
58
+ ResultSet.new(
59
+ query_string: query,
60
+ start_time: start_time,
61
+ end_time: end_time)
62
+ end
63
+ end
64
+
data/plunk.gemspec ADDED
@@ -0,0 +1,16 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "plunk"
3
+ s.version = "0.0.0"
4
+ s.date = "2013-11-26"
5
+ s.add_runtime_dependency "json"
6
+ s.add_runtime_dependency "parslet"
7
+ s.add_runtime_dependency "rest-client"
8
+ s.add_development_dependency "rspec"
9
+ s.summary = "Elasticsearch query language"
10
+ s.description = "Human-friendly query language for Elasticsearch"
11
+ s.authors = ["Ram Mehta", "Jamil Bou Kheir"]
12
+ s.email = ["jamil@elbii.com", "ram.mehta@gmail.com"]
13
+ s.files = `git ls-files`.split("\n")
14
+ s.homepage = "https://github.com/elbii/plunk"
15
+ s.license = "MIT"
16
+ end
@@ -0,0 +1,15 @@
1
+ require 'plunk'
2
+ require 'plunk/elasticsearch'
3
+
4
+ describe Plunk::Elasticsearch do
5
+ before :all do
6
+ @elasticsearch = Plunk::Elasticsearch.new
7
+ end
8
+
9
+ context 'test field mapping' do
10
+ it 'should successfully list all fields' do
11
+ fields = @elasticsearch.available_fields
12
+ expect(fields).to be_a Hash
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,101 @@
1
+ require 'plunk'
2
+ require 'plunk/parser'
3
+
4
+ # Print ascii_tree when exception occurs
5
+ class Plunk::ParserWrapper < Plunk::Parser
6
+ def parse(query)
7
+ begin
8
+ super(query)
9
+ rescue Parslet::ParseFailed => failure
10
+ puts failure.cause.ascii_tree
11
+ end
12
+ end
13
+ end
14
+
15
+ describe Plunk::Parser do
16
+ before :all do
17
+ @parser = Plunk::ParserWrapper.new
18
+ end
19
+
20
+ context 'single-query searches' do
21
+ it 'should parse a single keyword' do
22
+ @parsed = @parser.parse 'bar'
23
+ expect(@parsed[:match].to_s).to eq 'bar'
24
+ end
25
+
26
+ it 'should parse a single field/value combo' do
27
+ @parsed = @parser.parse 'tshark.http.@src_ip=bar'
28
+ expect(@parsed[:field].to_s).to eq 'tshark.http.@src_ip'
29
+ expect(@parsed[:value].to_s).to eq 'bar'
30
+ expect(@parsed[:op].to_s).to eq '='
31
+ end
32
+
33
+ it 'should parse a single boolean expression' do
34
+ @parsed = @parser.parse '(bar OR car)'
35
+ expect(@parsed[:match].to_s).to eq '(bar OR car)'
36
+ end
37
+
38
+ it 'should parse a single field / value complex boolean expression' do
39
+ @parsed = @parser.parse 'ids.attackers=(bar OR car)'
40
+ expect(@parsed[:field].to_s).to eq 'ids.attackers'
41
+ expect(@parsed[:value].to_s).to eq '(bar OR car)'
42
+ expect(@parsed[:op].to_s).to eq '='
43
+ end
44
+
45
+ it 'should parse a single field / parenthesized value' do
46
+ @parsed = @parser.parse 'ids.attacker=(10.150.44.195)'
47
+ expect(@parsed[:field].to_s).to eq 'ids.attacker'
48
+ expect(@parsed[:value].to_s).to eq '(10.150.44.195)'
49
+ expect(@parsed[:op].to_s).to eq '='
50
+ end
51
+
52
+ it 'should parse a basic regex search' do
53
+ @parsed = @parser.parse 'foo=/blah foo/'
54
+ expect(@parsed[:field].to_s).to eq 'foo'
55
+ expect(@parsed[:value].to_s).to eq '/blah foo/'
56
+ end
57
+
58
+ context 'the last command' do
59
+ it 'should parse last command with a search' do
60
+ @parsed = @parser.parse 'last 24w tshark.@src_ip = bar'
61
+ expect(@parsed[:timerange][:quantity].to_s).to eq '24'
62
+ expect(@parsed[:timerange][:quantifier].to_s).to eq 'w'
63
+ expect(@parsed[:search][:field].to_s).to eq 'tshark.@src_ip'
64
+ expect(@parsed[:search][:value].to_s).to eq 'bar'
65
+ end
66
+ end
67
+
68
+ context 'chained searches' do
69
+ it 'should parse last command with a regex' do
70
+ @parsed = @parser.parse 'last 24w foo=/blah/'
71
+ expect(@parsed[:timerange][:quantity].to_s).to eq '24'
72
+ expect(@parsed[:timerange][:quantifier].to_s).to eq 'w'
73
+ expect(@parsed[:search][:field].to_s).to eq 'foo'
74
+ expect(@parsed[:search][:value].to_s).to eq '/blah/'
75
+ end
76
+ end
77
+
78
+ it 'should parse last command with boolean' do
79
+ @parsed = @parser.parse 'last 1h (foo OR bar)'
80
+ expect(@parsed[:search][:match].to_s).to eq '(foo OR bar)'
81
+ end
82
+
83
+ it 'should parse key/value with regex' do
84
+ @parsed = @parser.parse 'foo=bar fe.ip=/whodunnit/'
85
+ expect(@parsed[0][:field].to_s).to eq 'foo'
86
+ expect(@parsed[0][:value].to_s).to eq 'bar'
87
+ expect(@parsed[1][:field].to_s).to eq 'fe.ip'
88
+ expect(@parsed[1][:value].to_s).to eq '/whodunnit/'
89
+ end
90
+ end
91
+
92
+ context 'nested search' do
93
+ it 'should parse the nested search' do
94
+ @parsed = @parser.parse 'tshark.len = ` 226 | tshark.frame.time_epoch,tshark.ip.src`'
95
+ expect(@parsed[:field].to_s).to eq 'tshark.len'
96
+ expect(@parsed[:op].to_s).to eq '='
97
+ expect(@parsed[:value][:term].to_s).to eq '226 '
98
+ expect(@parsed[:value][:extractors].to_s).to eq 'tshark.frame.time_epoch,tshark.ip.src'
99
+ end
100
+ end
101
+ end
File without changes
@@ -0,0 +1,10 @@
1
+
2
+ # after :each do
3
+ # transformed = @transformer.apply(@parsed)
4
+ # if transformed.is_a? Array
5
+ # transformed.map(&:class).uniq.should =~ Array(ResultSet)
6
+ # else
7
+ # transformed.should be_a ResultSet
8
+ # end
9
+ # end
10
+
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: plunk
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Ram Mehta
8
+ - Jamil Bou Kheir
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-11-26 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: json
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - '>='
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - '>='
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: parslet
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - '>='
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - '>='
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rest-client
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - '>='
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - '>='
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: rspec
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ description: Human-friendly query language for Elasticsearch
71
+ email:
72
+ - jamil@elbii.com
73
+ - ram.mehta@gmail.com
74
+ executables: []
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - .gitignore
79
+ - Gemfile
80
+ - Gemfile.lock
81
+ - LICENSE
82
+ - README.md
83
+ - Rakefile
84
+ - lib/plunk.rb
85
+ - lib/plunk/elasticsearch.rb
86
+ - lib/plunk/parser.rb
87
+ - lib/plunk/result_set.rb
88
+ - lib/plunk/transformer.rb
89
+ - plunk.gemspec
90
+ - spec/elasticsearch_spec.rb
91
+ - spec/parser_spec.rb
92
+ - spec/result_set_spec.rb
93
+ - spec/transformer_spec.rb
94
+ homepage: https://github.com/elbii/plunk
95
+ licenses:
96
+ - MIT
97
+ metadata: {}
98
+ post_install_message:
99
+ rdoc_options: []
100
+ require_paths:
101
+ - lib
102
+ required_ruby_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - '>='
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ required_rubygems_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - '>='
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ requirements: []
113
+ rubyforge_project:
114
+ rubygems_version: 2.1.11
115
+ signing_key:
116
+ specification_version: 4
117
+ summary: Elasticsearch query language
118
+ test_files: []