Skip to content

Commit

Permalink
Introduce simple kibana query language (#15646)
Browse files Browse the repository at this point in the history
* Introduce simple kuery language

* Rename to kql and add modules
  • Loading branch information
lukasolson committed Jan 5, 2018
1 parent 303f98a commit 13246b3
Show file tree
Hide file tree
Showing 13 changed files with 176 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
ng-show="showFilterBar()"
state="state"
index-patterns="indexPatterns"
ng-if="model.query.language === 'lucene'"
ng-if="['lucene', 'kql'].includes(model.query.language)"
></filter-bar>

<div
Expand Down
2 changes: 1 addition & 1 deletion src/core_plugins/kibana/public/discover/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ <h1 tabindex="0" id="kui_local_breadcrumb" class="kuiLocalBreadcrumb">
<filter-bar
state="state"
index-patterns="[indexPattern]"
ng-if="state.query.language === 'lucene'"
ng-if="['lucene', 'kql'].includes(state.query.language)"
></filter-bar>
</div>
<div class="row">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@

<!-- Filters. -->
<filter-bar
ng-if="vis.type.options.showFilterBar && state.query.language === 'lucene'"
ng-if="vis.type.options.showFilterBar && ['lucene', 'kql'].includes(state.query.language)"
state="state"
index-patterns="[indexPattern]"
></filter-bar>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export function CoordinateMapsVisualizationProvider(Notifier, Private) {
const query = this.vis.API.queryManager.getQuery();
const language = query.language;

if (language === 'lucene') {
if (['lucene', 'kql'].includes(language)) {
const filter = { meta: { negate: false, index: indexPatternName } };
filter[filterName] = { ignore_unmapped: true };
filter[filterName][field] = filterData;
Expand Down Expand Up @@ -198,4 +198,3 @@ export function CoordinateMapsVisualizationProvider(Notifier, Private) {

return CoordinateMapsVisualization;
}

11 changes: 6 additions & 5 deletions src/ui/public/courier/data_source/build_query/build_es_query.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { groupBy, has } from 'lodash';
import { DecorateQueryProvider } from '../_decorate_query';
import { buildQueryFromKuery } from './from_kuery';
import { buildQueryFromKuery, buildQueryFromKql } from './from_kuery';
import { buildQueryFromFilters } from './from_filters';
import { buildQueryFromLucene } from './from_lucene';

Expand All @@ -17,15 +17,16 @@ export function BuildESQueryProvider(Private) {
const queriesByLanguage = groupBy(validQueries, 'language');

const kueryQuery = buildQueryFromKuery(indexPattern, queriesByLanguage.kuery);
const kqlQuery = buildQueryFromKql(indexPattern, queriesByLanguage.kql);
const luceneQuery = buildQueryFromLucene(queriesByLanguage.lucene, decorateQuery);
const filterQuery = buildQueryFromFilters(filters, decorateQuery);

return {
bool: {
must: [].concat(kueryQuery.must, luceneQuery.must, filterQuery.must),
filter: [].concat(kueryQuery.filter, luceneQuery.filter, filterQuery.filter),
should: [].concat(kueryQuery.should, luceneQuery.should, filterQuery.should),
must_not: [].concat(kueryQuery.must_not, luceneQuery.must_not, filterQuery.must_not),
must: [].concat(kueryQuery.must, kqlQuery.must, luceneQuery.must, filterQuery.must),
filter: [].concat(kueryQuery.filter, kqlQuery.filter, luceneQuery.filter, filterQuery.filter),
should: [].concat(kueryQuery.should, kqlQuery.should, luceneQuery.should, filterQuery.should),
must_not: [].concat(kueryQuery.must_not, kqlQuery.must_not, luceneQuery.must_not, filterQuery.must_not),
}
};
}
Expand Down
13 changes: 10 additions & 3 deletions src/ui/public/courier/data_source/build_query/from_kuery.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import _ from 'lodash';
import { fromKueryExpression, toElasticsearchQuery, nodeTypes } from '../../../kuery';
import { fromKueryExpression, fromKqlExpression, toElasticsearchQuery, nodeTypes } from '../../../kuery';

export function buildQueryFromKuery(indexPattern, queries) {
const queryASTs = _.map(queries, query => fromKueryExpression(query.query));
return buildQuery(indexPattern, queryASTs);
}

export function buildQueryFromKql(indexPattern, queries) {
const queryASTs = _.map(queries, query => fromKqlExpression(query.query));
return buildQuery(indexPattern, queryASTs);
}

function buildQuery(indexPattern, queryASTs) {
const compoundQueryAST = nodeTypes.function.buildNode('and', queryASTs);
const kueryQuery = toElasticsearchQuery(compoundQueryAST, indexPattern);
return {
Expand All @@ -13,5 +22,3 @@ export function buildQueryFromKuery(indexPattern, queries) {
...kueryQuery.bool
};
}


2 changes: 1 addition & 1 deletion src/ui/public/doc_table/actions/filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function addFilter(field, values = [], operation, index, state, filterMan
values = [values];
}

if (state.query.language === 'lucene') {
if (['lucene', 'kql'].includes(state.query.language)) {
filterManager.add(field, values, operation, index);
}

Expand Down
3 changes: 1 addition & 2 deletions src/ui/public/filter_bar/filter_bar_click_handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export function FilterBarClickHandlerProvider(Notifier, Private) {
filters = dedupFilters($state.filters, uniqFilters(filters), { negate: true });

if (!simulate) {
if ($state.query.language === 'lucene') {
if (['lucene', 'kql'].includes($state.query.language)) {
$state.$newFilters = filters;
}
else if ($state.query.language === 'kuery') {
Expand All @@ -81,4 +81,3 @@ export function FilterBarClickHandlerProvider(Notifier, Private) {
};
};
}

14 changes: 12 additions & 2 deletions src/ui/public/kuery/ast/ast.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import grammar from 'raw-loader!./kuery.peg';
import kqlGrammar from 'raw-loader!./kql.peg';
import PEG from 'pegjs';
import _ from 'lodash';
import { nodeTypes } from '../node_types/index';

const kueryParser = PEG.buildParser(grammar);
const kqlParser = PEG.buildParser(kqlGrammar);

export function fromKueryExpression(expression, parseOptions = {}) {
export function fromKueryExpression(expression, parseOptions) {
return fromExpression(expression, parseOptions, kueryParser);
}

export function fromKqlExpression(expression, parseOptions) {
return fromExpression(expression, parseOptions, kqlParser);
}

function fromExpression(expression, parseOptions = {}, parser = kqlParser) {
if (_.isUndefined(expression)) {
throw new Error('expression must be a string, got undefined instead');
}
Expand All @@ -15,7 +25,7 @@ export function fromKueryExpression(expression, parseOptions = {}) {
helpers: { nodeTypes }
};

return kueryParser.parse(expression, parseOptions);
return parser.parse(expression, parseOptions);
}

export function toKueryExpression(node) {
Expand Down
2 changes: 1 addition & 1 deletion src/ui/public/kuery/ast/index.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { fromKueryExpression, toKueryExpression, toElasticsearchQuery } from './ast';
export { fromKueryExpression, fromKqlExpression, toKueryExpression, toElasticsearchQuery } from './ast';
123 changes: 123 additions & 0 deletions src/ui/public/kuery/ast/kql.peg
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Kuery parser
*/

/*
* Initialization block
*/
{
var nodeTypes = options.helpers.nodeTypes;

if (options.includeMetadata === undefined) {
options.includeMetadata = true;
}

function addMeta(source, text, location) {
if (options.includeMetadata) {
return Object.assign(
{},
source,
{
text: text,
location: simpleLocation(location),
}
);
}

return source;
}

function simpleLocation(location) {
// Returns an object representing the position of the function within the expression,
// demarcated by the position of its first character and last character. We calculate these values
// using the offset because the expression could span multiple lines, and we don't want to deal
// with column and line values.
return {
min: location.start.offset,
max: location.end.offset
}
}
}

start
= Query
/ space* {
return addMeta(nodeTypes.function.buildNode('and', []), text(), location());
}

Query
= space? query:OrQuery space? {
if (query.type === 'literal') {
return addMeta(nodeTypes.function.buildNode('and', [query]), text(), location());
}
return query;
}

OrQuery
= left:AndQuery space 'or'i space right:OrQuery {
return addMeta(nodeTypes.function.buildNode('or', [left, right]), text(), location());
}
/ AndQuery

AndQuery
= left:NotQuery space 'and'i space right:AndQuery {
return addMeta(nodeTypes.function.buildNode('and', [left, right]), text(), location());
}
/ NotQuery

NotQuery
= 'not'i space clause:Clause {
return addMeta(nodeTypes.function.buildNode('not', clause), text(), location());
}
/ Clause

Clause
= '(' subQuery:Query ')' {
return subQuery;
}
/ Term

Term
= field:literal_arg_type space? ':' space? value:literal_arg_type {
return addMeta(nodeTypes.function.buildNodeWithArgumentNodes('is', [field, value]), text(), location());
}
/ field:literal_arg_type space? ':' space? '[' space? gt:literal_arg_type space 'to'i space lt:literal_arg_type space? ']' {
return addMeta(nodeTypes.function.buildNodeWithArgumentNodes('range', [field, gt, lt]), text(), location());
}
/ !Keywords literal:literal_arg_type { return literal; }

literal_arg_type
= literal:literal {
var result = addMeta(nodeTypes.literal.buildNode(literal), text(), location());
return result;
}

Keywords
= 'or'i / 'and'i / 'not'i

/* ----- Core types ----- */

literal "literal"
= '"' chars:dq_char* '"' { return chars.join(''); } // double quoted string
/ "'" chars:sq_char* "'" { return chars.join(''); } // single quoted string
/ 'true' { return true; } // unquoted literals from here down
/ 'false' { return false; }
/ 'null' { return null; }
/ string:[^\[\]()"',:=\ \t]+ { // this also matches numbers via Number()
var result = string.join('');
// Sort of hacky, but PEG doesn't have backtracking so
// a number rule is hard to read, and performs worse
if (isNaN(Number(result))) return result;
return Number(result)
}

space
= [\ \t\r\n]+

dq_char
= "\\" sequence:('"' / "\\") { return sequence; }
/ [^"] // everything except "

sq_char
= "\\" sequence:("'" / "\\") { return sequence; }
/ [^'] // everything except '
17 changes: 17 additions & 0 deletions src/ui/public/query_bar/directive/query_bar.html
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,23 @@
</div>
</div>

<!-- kql input -->
<div class="kuiLocalSearchAssistedInput" ng-if="queryBar.localQuery.language === 'kql'">
<input
ng-model="queryBar.localQuery.query"
input-focus
disable-input-focus="queryBar.disableAutoFocus"
kbn-typeahead-input
placeholder="Search... (e.g. status:200 AND extension:PHP)"
aria-label="Search input"
aria-describedby="discoverKuerySyntaxHint"
type="text"
class="kuiLocalSearchInput"
ng-class="{'kuiLocalSearchInput-isInvalid': queryBarForm.$invalid}"
data-test-subj="queryInput"
/>
</div>

<select
class="kuiLocalSearchSelect"
ng-options="option for option in queryBar.availableQueryLanguages"
Expand Down
1 change: 1 addition & 0 deletions src/ui/public/query_bar/lib/queryLanguages.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const queryLanguages = [
'lucene',
'kuery',
// 'kql'
];

0 comments on commit 13246b3

Please sign in to comment.