Implement new Kibana query language (#12624)

Initial version of an experimental new query language for Kibana.
This commit is contained in:
Matt Bargar 2017-07-21 11:55:15 -04:00 committed by GitHub
parent 1b024200cd
commit d379e9a35b
102 changed files with 3701 additions and 406 deletions

View file

@ -22,6 +22,8 @@ compatible with other configuration settings. Deleting a custom setting removes
.Kibana Settings Reference
[horizontal]
`query:queryString:options`:: Options for the Lucene query string parser.
`search:queryLanguage`:: Default is `lucene`. Query language used by the query bar. Choose between the lucene query syntax and kuery, an experimental new language built specifically for Kibana.
`search:queryLanguage:switcher:enable`:: Show or hide the query language switcher in the query bar.
`sort:options`:: Options for the Elasticsearch {ref}/search-request-sort.html[sort] parameter.
`dateFormat`:: The format to use for displaying pretty-formatted dates.
`dateFormat:tz`:: The timezone that Kibana uses. The default value of `Browser` uses the timezone detected by the browser.

View file

@ -88,9 +88,12 @@ describe('context app', function () {
const setQuerySpy = searchSourceStub.set.withArgs('query');
expect(setQuerySpy.calledOnce).to.be(true);
expect(setQuerySpy.firstCall.args[1]).to.eql({
terms: {
_uid: ['UID'],
query: {
terms: {
_uid: ['UID'],
}
},
language: 'lucene'
});
});
});

View file

@ -15,9 +15,12 @@ function fetchAnchorProvider(courier, Private) {
.set('version', true)
.set('size', 1)
.set('query', {
terms: {
_uid: [uid],
query: {
terms: {
_uid: [uid],
}
},
language: 'lucene'
})
.set('sort', sort);

View file

@ -50,7 +50,10 @@ function fetchContextProvider(courier, Private) {
.set('size', size)
.set('filter', filters)
.set('query', {
match_all: {},
query: {
match_all: {},
},
language: 'lucene'
})
.set('searchAfter', anchorDocument.sort)
.set('sort', sort);

View file

@ -31,54 +31,14 @@
</div>
<!-- Search. -->
<form
data-transclude-slot="bottomRow"
class="fullWidth"
ng-show="chrome.getVisible()"
role="form"
name="queryInput"
ng-submit="filterResults()"
>
<div class="typeahead" kbn-typeahead="dashboard" on-select="filterResults()">
<div class="kuiLocalSearch">
<div class="kuiLocalSearchAssistedInput">
<input
parse-query
input-focus
kbn-typeahead-input
ng-model="model.query"
placeholder="Search... (e.g. status:200 AND extension:PHP)"
aria-label="Enter query"
data-test-subj="dashboardQuery"
type="text"
class="kuiLocalSearchInput kuiLocalSearchInput--lucene"
ng-class="{'kuiLocalSearchInput-isInvalid': queryInput.$invalid}"
>
<div class="kuiLocalSearchAssistedInput__assistance">
<p class="kuiText">
<a
class="kuiLink"
ng-href="{{queryDocLinks.luceneQuerySyntax}}"
target="_blank"
>
Uses lucene query syntax
</a>
</p>
</div>
</div>
<button
type="submit"
aria-label="Submit query"
class="kuiLocalSearchButton"
data-test-subj="dashboardQueryFilterButton"
ng-disabled="queryInput.$invalid"
>
<span class="kuiIcon fa-search" aria-hidden="true"></span>
</button>
</div>
<kbn-typeahead-items></kbn-typeahead-items>
</div>
</form>
<div ng-show="chrome.getVisible()" class="fullWidth" data-transclude-slot="bottomRow">
<query-bar
query="model.query"
app-name="'dashboard'"
on-submit="updateQuery($query)"
>
</query-bar>
</div>
</div>
</kbn-top-nav>
@ -87,6 +47,7 @@
ng-show="showFilterBar()"
state="state"
index-patterns="indexPatterns"
ng-if="model.query.language === 'lucene'"
></filter-bar>
<div
@ -126,6 +87,7 @@
toggle-expand="toggleExpandPanel"
register-panel-index-pattern="registerPanelIndexPattern"
data-shared-items-count="{{panels.length}}"
on-filter="filter"
></dashboard-grid>
<dashboard-panel

View file

@ -6,6 +6,7 @@ import chrome from 'ui/chrome';
import 'plugins/kibana/dashboard/grid';
import 'plugins/kibana/dashboard/panel/panel';
import 'ui/query_bar';
import { SavedObjectNotFound } from 'ui/errors';
import { getDashboardTitle, getUnsavedChangesWarningMessage } from './dashboard_strings';
@ -25,6 +26,8 @@ import { notify } from 'ui/notify';
import './panel/get_object_loaders_for_dashboard';
import { documentationLinks } from 'ui/documentation_links/documentation_links';
import { showCloneModal } from './top_nav/show_clone_modal';
import { migrateLegacyQuery } from 'ui/utils/migrateLegacyQuery';
import { QueryManagerProvider } from 'ui/query_manager';
import { ESC_KEY_CODE } from 'ui_framework/services';
const app = uiModules.get('app/dashboard', [
@ -81,6 +84,7 @@ app.directive('dashboardApp', function ($injector) {
const quickRanges = $injector.get('quickRanges');
const kbnUrl = $injector.get('kbnUrl');
const confirmModal = $injector.get('confirmModal');
const config = $injector.get('config');
const Private = $injector.get('Private');
const brushEvent = Private(UtilsBrushEventProvider);
const filterBarClickHandler = Private(FilterBarClickHandlerProvider);
@ -100,6 +104,7 @@ app.directive('dashboardApp', function ($injector) {
}
const dashboardState = new DashboardState(dash, AppState, dashboardConfig);
const queryManager = Private(QueryManagerProvider)(dashboardState.getAppState());
// The 'previouslyStored' check is so we only update the time filter on dashboard open, not during
// normal cross app navigation.
@ -130,7 +135,10 @@ app.directive('dashboardApp', function ($injector) {
updateState();
});
dashboardState.applyFilters(dashboardState.getQuery(), filterBar.getFilters());
dashboardState.applyFilters(
dashboardState.getQuery() || { query: '', language: config.get('search:queryLanguage') },
filterBar.getFilters()
);
let pendingVisCount = _.size(dashboardState.getPanels());
timefilter.enabled = true;
@ -179,9 +187,14 @@ app.directive('dashboardApp', function ($injector) {
}
};
$scope.filterResults = function () {
dashboardState.applyFilters($scope.model.query, filterBar.getFilters());
$scope.refresh();
$scope.updateQuery = function (query) {
// reset state if language changes
if ($scope.model.query.language && $scope.model.query.language !== query.language) {
filterBar.removeAll();
dashboardState.getAppState().$newFilters = [];
}
$scope.model.query = query;
};
// called by the saved-object-finder when a user clicks a vis
@ -229,6 +242,28 @@ app.directive('dashboardApp', function ($injector) {
$scope.indexPatterns = dashboardState.getPanelIndexPatterns();
};
$scope.filter = function (field, value, operator, index) {
queryManager.add(field, value, operator, index);
updateState();
};
$scope.$watch('model.query', (newQuery) => {
$scope.model.query = migrateLegacyQuery(newQuery);
dashboardState.applyFilters($scope.model.query, filterBar.getFilters());
$scope.refresh();
});
$scope.$watchCollection(() => dashboardState.getAppState().$newFilters, function (filters = []) {
// need to convert filters generated from user interaction with viz into kuery AST
// These are handled by the filter bar directive when lucene is the query language
Promise.all(filters.map(queryManager.addLegacyFilter))
.then(() => dashboardState.getAppState().$newFilters = [])
.then(updateState)
.then(() => dashboardState.applyFilters($scope.model.query, filterBar.getFilters()))
.then($scope.refresh());
});
$scope.$listen(timefilter, 'fetch', $scope.refresh);
function updateViewMode(newMode) {

View file

@ -2,16 +2,19 @@
import _ from 'lodash';
import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter';
import 'ui/state_management/app_state';
import { luceneStringToDsl } from '../../../../ui/public/courier/data_source/build_query/lucene_string_to_dsl';
export function dashboardContextProvider(Private, getAppState) {
return () => {
const queryFilter = Private(FilterBarQueryFilterProvider);
const bool = { must: [], must_not: [] };
const filterBarFilters = queryFilter.getFilters();
const queryBarFilter = getAppState().query;
const queryBarQuery = getAppState().query;
// Add the query bar filter, its handled differently.
bool.must.push(queryBarFilter);
if (queryBarQuery.language === 'lucene') {
// Add the query bar filter, its handled differently.
bool.must.push(luceneStringToDsl(queryBarQuery.query));
}
// Add each of the filter bar filters
_.each(filterBarFilters, function (filter) {

View file

@ -219,7 +219,19 @@ export class DashboardState {
* new dashboard, if the query differs from the default.
*/
getQueryChanged() {
return !_.isEqual(this.appState.query, this.getLastSavedQuery());
const currentQuery = this.appState.query;
const lastSavedQuery = this.getLastSavedQuery();
const isLegacyStringQuery = (
_.isString(lastSavedQuery)
&& _.isPlainObject(currentQuery)
&& _.has(currentQuery, 'query')
);
if (isLegacyStringQuery) {
return lastSavedQuery !== currentQuery.query;
}
return !_.isEqual(currentQuery, lastSavedQuery);
}
/**
@ -404,14 +416,8 @@ export class DashboardState {
*/
applyFilters(query, filters) {
this.appState.query = query;
if (this.appState.query) {
this.savedDashboard.searchSource.set('filter', _.union(filters, [{
query: this.appState.query
}]));
} else {
this.savedDashboard.searchSource.set('filter', filters);
}
this.savedDashboard.searchSource.set('query', query);
this.savedDashboard.searchSource.set('filter', filters);
this.saveState();
}
@ -424,6 +430,8 @@ export class DashboardState {
this.stateMonitor.ignoreProps('viewMode');
// Filters need to be compared manually because they sometimes have a $$hashkey stored on the object.
this.stateMonitor.ignoreProps('filters');
// Query needs to be compared manually because saved legacy queries get migrated in app state automatically
this.stateMonitor.ignoreProps('query');
this.stateMonitor.onChange(status => {
this.isDirty = status.dirty;

View file

@ -33,10 +33,13 @@ export class FilterUtils {
* @returns {QueryFilter}
*/
static getQueryFilterForDashboard(dashboard) {
const defaultQueryFilter = { query_string: { query: '*' } };
if (dashboard.searchSource.getOwn('query')) {
return dashboard.searchSource.getOwn('query');
}
const dashboardFilters = this.getDashboardFilters(dashboard);
const dashboardQueryFilter = _.find(dashboardFilters, this.isQueryFilter);
return dashboardQueryFilter ? dashboardQueryFilter.query : defaultQueryFilter;
return dashboardQueryFilter ? dashboardQueryFilter.query : '';
}
/**

View file

@ -60,6 +60,11 @@ app.directive('dashboardGrid', function ($compile, Notifier) {
* @type {function}
*/
toggleExpand: '=',
/**
* Called when a filter action has been triggered by a panel
* @type {function}
*/
onFilter: '=',
},
link: function ($scope, $el) {
const notify = new Notifier();
@ -223,7 +228,8 @@ app.directive('dashboardGrid', function ($compile, Notifier) {
app-state="appState"
register-panel-index-pattern="registerPanelIndexPattern"
toggle-expand="toggleExpand(${panel.panelIndex})"
create-child-ui-state="createChildUiState">
create-child-ui-state="createChildUiState"
on-filter="onFilter">
</li>`;
const panelElement = $compile(panelHtml)($scope);
panelElementMapping[panel.panelIndex] = panelElement;

View file

@ -5,7 +5,6 @@ import * as columnActions from 'ui/doc_table/actions/columns';
import 'plugins/kibana/dashboard/panel/get_object_loaders_for_dashboard';
import 'plugins/kibana/visualize/saved_visualizations';
import 'plugins/kibana/discover/saved_searches';
import { FilterManagerProvider } from 'ui/filter_manager';
import { uiModules } from 'ui/modules';
import panelTemplate from 'plugins/kibana/dashboard/panel/panel.html';
import { savedObjectManagementRegistry } from 'plugins/kibana/management/saved_object_registry';
@ -16,7 +15,6 @@ import { DashboardViewMode } from '../dashboard_view_mode';
uiModules
.get('app/dashboard')
.directive('dashboardPanel', function (savedVisualizations, savedSearches, Notifier, Private, $injector, getObjectLoadersForDashboard) {
const filterManager = Private(FilterManagerProvider);
const services = savedObjectManagementRegistry.all().map(function (serviceObj) {
const service = $injector.get(serviceObj.service);
@ -86,6 +84,11 @@ uiModules
* @type {function}
*/
saveState: '=',
/**
* Called when a filter action has been triggered
* @type {function}
*/
onFilter: '=',
appState: '=',
},
link: function ($scope, element) {
@ -148,7 +151,7 @@ uiModules
$scope.filter = function (field, value, operator) {
const index = $scope.savedObj.searchSource.get('index').id;
filterManager.add(field, value, operator, index);
$scope.onFilter(field, value, operator, index);
};
}

View file

@ -14,12 +14,12 @@ import 'ui/index_patterns';
import 'ui/state_management/app_state';
import 'ui/timefilter';
import 'ui/share';
import 'ui/query_bar';
import { VisProvider } from 'ui/vis';
import { BasicResponseHandlerProvider } from 'ui/vis/response_handlers/basic';
import { DocTitleProvider } from 'ui/doc_title';
import PluginsKibanaDiscoverHitSortFnProvider from 'plugins/kibana/discover/_hit_sort_fn';
import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter';
import { FilterManagerProvider } from 'ui/filter_manager';
import { AggTypesBucketsIntervalOptionsProvider } from 'ui/agg_types/buckets/_interval_options';
import { stateMonitorFactory } from 'ui/state_management/state_monitor_factory';
import uiRoutes from 'ui/routes';
@ -27,6 +27,8 @@ import { uiModules } from 'ui/modules';
import indexTemplate from 'plugins/kibana/discover/index.html';
import { StateProvider } from 'ui/state_management/state';
import { documentationLinks } from 'ui/documentation_links/documentation_links';
import { migrateLegacyQuery } from 'ui/utils/migrateLegacyQuery';
import { QueryManagerProvider } from 'ui/query_manager';
import { SavedObjectsClientProvider } from 'ui/saved_objects';
const app = uiModules.get('apps/discover', [
@ -115,7 +117,6 @@ function discoverController(
const docTitle = Private(DocTitleProvider);
const HitSortFn = Private(PluginsKibanaDiscoverHitSortFnProvider);
const queryFilter = Private(FilterBarQueryFilterProvider);
const filterManager = Private(FilterManagerProvider);
const responseHandler = Private(BasicResponseHandlerProvider).handler;
const notify = new Notifier({
location: 'Discover'
@ -176,6 +177,7 @@ function discoverController(
};
const $state = $scope.state = new AppState(getStateDefaults());
const queryManager = Private(QueryManagerProvider)($state);
const getFieldCounts = async () => {
// the field counts aren't set until we have the data back,
@ -251,7 +253,7 @@ function discoverController(
function getStateDefaults() {
return {
query: $scope.searchSource.get('query') || '',
query: $scope.searchSource.get('query') || { query: '', language: config.get('search:queryLanguage') },
sort: getSort.array(savedSearch.sort, $scope.indexPattern),
columns: savedSearch.columns.length > 0 ? savedSearch.columns : config.get('defaultColumns').slice(),
index: $scope.indexPattern.id,
@ -331,6 +333,15 @@ function discoverController(
$scope.fetch();
});
// Necessary for handling new time filters when the date histogram is clicked
$scope.$watchCollection('state.$newFilters', function (filters = []) {
// need to convert filters generated from user interaction with viz into kuery AST
// These are handled by the filter bar directive when lucene is the query language
Promise.all(filters.map(queryManager.addLegacyFilter))
.then(() => $scope.state.$newFilters = [])
.then($scope.fetch);
});
$scope.$watch('vis.aggs', function () {
// no timefield, no vis, nothing to update
if (!$scope.opts.timefield) return;
@ -342,6 +353,12 @@ function discoverController(
}
});
$scope.$watch('state.query', (newQuery) => {
$state.query = migrateLegacyQuery(newQuery);
$scope.fetch();
});
$scope.$watchMulti([
'rows',
'fetchStatus'
@ -444,6 +461,15 @@ function discoverController(
.catch(notify.error);
};
$scope.updateQuery = function (query) {
// reset state if language changes
if ($state.query.language && $state.query.language !== query.language) {
$state.filters = [];
}
$state.query = query;
};
$scope.searchSource.onBeginSegmentedFetch(function (segmented) {
function flushResponseData() {
$scope.hits = 0;
@ -584,7 +610,7 @@ function discoverController(
// TODO: On array fields, negating does not negate the combination, rather all terms
$scope.filterQuery = function (field, values, operation) {
$scope.indexPattern.popularizeField(field, 1);
filterManager.add(field, values, operation, $state.index);
queryManager.add(field, values, operation, $scope.indexPattern.id);
};
$scope.addColumn = function addColumn(columnName) {

View file

@ -16,53 +16,14 @@
</div>
<!-- Search. -->
<form
data-transclude-slot="bottomRow"
class="fullWidth"
role="form"
name="discoverSearch"
ng-submit="fetch()"
>
<div class="typeahead" kbn-typeahead="discover" on-select="fetch()" role="search">
<div class="kuiLocalSearch">
<div class="kuiLocalSearchAssistedInput">
<input
parse-query
input-focus
kbn-typeahead-input
ng-model="state.query"
placeholder="Search... (e.g. status:200 AND extension:PHP)"
aria-label="Search input"
aria-describedby="discover-lucene-syntax-hint"
type="text"
class="kuiLocalSearchInput kuiLocalSearchInput--lucene"
ng-class="{'kuiLocalSearchInput-isInvalid': discoverSearch.$invalid}"
>
<div class="kuiLocalSearchAssistedInput__assistance">
<p class="kuiText">
<a
id="discover-lucene-syntax-hint"
class="kuiLink"
ng-href="{{queryDocLinks.luceneQuerySyntax}}"
target="_blank"
>
Uses lucene query syntax
</a>
</p>
</div>
</div>
<button
type="submit"
aria-label="Search"
class="kuiLocalSearchButton"
ng-disabled="discoverSearch.$invalid"
>
<span class="kuiIcon fa-search" aria-hidden="true"></span>
</button>
</div>
<kbn-typeahead-items></kbn-typeahead-items>
</div>
</form>
<div data-transclude-slot="bottomRow" class="fullWidth">
<query-bar
query="state.query"
app-name="'discover'"
on-submit="updateQuery($query)"
>
</query-bar>
</div>
</div>
</kbn-top-nav>
@ -71,6 +32,7 @@
<filter-bar
state="state"
index-patterns="[indexPattern]"
ng-if="state.query.language === 'lucene'"
></filter-bar>
</div>
<div class="row">

View file

@ -34,12 +34,13 @@
</p>
</div>
<h3>Refine your query</h3>
<p>
The search bar at the top uses Elasticsearch's support for Lucene <a class="kuiLink" ng-href="{{queryDocLinks.luceneQuerySyntax}}" target="_blank">Query String syntax</a>. Let's say we're searching web server logs that have been parsed into a few fields.
</p>
<div ng-if="state.query.language === 'lucene'">
<h3>Refine your query</h3>
<p>
The search bar at the top uses Elasticsearch's support for Lucene <a class="kuiLink" ng-href="{{queryDocLinks.luceneQuerySyntax}}" target="_blank">Query String syntax</a>. Let's say we're searching web server logs that have been parsed into a few fields.
</p>
<p>
<p>
<h4>Examples:</h4>
Find requests that contain the number 200, in any field:
<pre>200</pre>
@ -55,7 +56,7 @@
Or HTML
<pre>status:[400 TO 499] AND (extension:php OR extension:html)</pre>
</p>
</div>
</div>
</div>

View file

@ -32,56 +32,21 @@
</div>
<!-- Allow searching if there is no linked Saved Search. -->
<form
ng-if="vis.type.requiresSearch && vis.type.options.showQueryBar && !state.linked"
name="queryInput"
ng-submit="fetch()"
class="fullWidth"
>
<div class="typeahead" kbn-typeahead="visualize" on-select="fetch()">
<div class="kuiLocalSearch">
<div class="kuiLocalSearchAssistedInput">
<input
ng-model="state.query"
parse-query
input-focus
kbn-typeahead-input
placeholder="Search... (e.g. status:200 AND extension:PHP)"
type="text"
class="kuiLocalSearchInput kuiLocalSearchInput--lucene"
ng-class="{'kuiLocalSearchInput-isInvalid': queryInput.$invalid}"
>
<div class="kuiLocalSearchAssistedInput__assistance">
<p class="kuiText">
<a
class="kuiLink"
ng-href="{{queryDocLinks.luceneQuerySyntax}}"
target="_blank"
>
Uses lucene query syntax
</a>
</p>
</div>
</div>
<button
type="submit"
aria-label="Search"
class="kuiLocalSearchButton"
ng-disabled="queryInput.$invalid"
>
<span aria-hidden="true" class="kuiIcon fa-search"></span>
</button>
</div>
<kbn-typeahead-items></kbn-typeahead-items>
</div>
</form>
<div ng-if="vis.type.requiresSearch && !state.linked && vis.type.options.showQueryBar" class="fullWidth">
<query-bar
query="state.query"
app-name="'visualize'"
on-submit="updateQuery($query)"
>
</query-bar>
</div>
</div>
</div>
</kbn-top-nav>
<!-- Filters. -->
<filter-bar
ng-if="vis.type.options.showFilterBar && !state.linked"
ng-if="vis.type.options.showFilterBar && state.query.language === 'lucene' && !state.linked"
state="state"
index-patterns="[indexPattern]"
></filter-bar>

View file

@ -5,6 +5,7 @@ import 'plugins/kibana/visualize/editor/agg_filter';
import 'ui/visualize';
import 'ui/collapsible_sidebar';
import 'ui/share';
import 'ui/query_bar';
import chrome from 'ui/chrome';
import angular from 'angular';
import { Notifier } from 'ui/notify/notifier';
@ -20,6 +21,8 @@ import { VisualizeConstants } from '../visualize_constants';
import { documentationLinks } from 'ui/documentation_links/documentation_links';
import { KibanaParsedUrl } from 'ui/url/kibana_parsed_url';
import { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url';
import { migrateLegacyQuery } from 'ui/utils/migrateLegacyQuery';
import { QueryManagerProvider } from 'ui/query_manager';
uiRoutes
.when(VisualizeConstants.CREATE_PATH, {
@ -69,7 +72,7 @@ uiModules
};
});
function VisEditor($scope, $route, timefilter, AppState, $window, kbnUrl, courier, Private, Promise, kbnBaseUrl) {
function VisEditor($scope, $route, timefilter, AppState, $window, kbnUrl, courier, Private, Promise, config, kbnBaseUrl) {
const docTitle = Private(DocTitleProvider);
const queryFilter = Private(FilterBarQueryFilterProvider);
@ -127,7 +130,7 @@ function VisEditor($scope, $route, timefilter, AppState, $window, kbnUrl, courie
const stateDefaults = {
uiState: savedVis.uiStateJSON ? JSON.parse(savedVis.uiStateJSON) : {},
linked: !!savedVis.savedSearchId,
query: searchSource.getOwn('query') || { query_string: { query: '*' } },
query: searchSource.getOwn('query') || { query: '', language: config.get('search:queryLanguage') },
filters: searchSource.getOwn('filter') || [],
vis: savedVisState
};
@ -151,6 +154,7 @@ function VisEditor($scope, $route, timefilter, AppState, $window, kbnUrl, courie
return appState;
}());
const queryManager = Private(QueryManagerProvider)($state);
function init() {
// export some objects
@ -184,6 +188,20 @@ function VisEditor($scope, $route, timefilter, AppState, $window, kbnUrl, courie
$appStatus.dirty = status.dirty || !savedVis.id;
});
$scope.$watchCollection('state.$newFilters', function (filters = []) {
// need to convert filters generated from user interaction with viz into kuery AST
// These are handled by the filter bar directive when lucene is the query language
Promise.all(filters.map(queryManager.addLegacyFilter))
.then(() => $scope.state.$newFilters = [])
.then($scope.fetch);
});
$scope.$watch('state.query', (newQuery) => {
$state.query = migrateLegacyQuery(newQuery);
$scope.fetch();
});
$state.replace();
$scope.getVisualizationTitle = function getVisualizationTitle() {
@ -218,6 +236,16 @@ function VisEditor($scope, $route, timefilter, AppState, $window, kbnUrl, courie
});
}
$scope.updateQuery = function (query) {
// reset state if language changes
if ($state.query.language && $state.query.language !== query.language) {
$state.filters = [];
$state.$newFilters = [];
}
$state.query = query;
};
/**
* Called when the user clicks "Save" button.
*/

View file

@ -14,6 +14,16 @@ export function getUiSettingDefaults() {
description: '<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html" target="_blank">Options</a> for the lucene query string parser',
type: 'json'
},
'search:queryLanguage': {
value: 'lucene',
description: 'Query language used by the query bar. Kuery is an experimental new language built specifically for Kibana.',
type: 'select',
options: ['lucene', 'kuery']
},
'search:queryLanguage:switcher:enable': {
value: false,
description: 'Show or hide the query language switcher in the query bar'
},
'sort:options': {
value: '{ "unmapped_type": "boolean" }',
description: '<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-sort.html" target="_blank">Options</a> for the Elasticsearch sort parameter',

View file

@ -0,0 +1,10 @@
import { isEqual } from 'lodash';
import expect from 'expect.js';
// expect.js's `eql` method provides nice error messages but sometimes misses things
// since it only tests loose (==) equality. This function uses lodash's `isEqual` as a
// second sanity check since it checks for strict equality.
export function expectDeepEqual(actual, expected) {
expect(actual).to.eql(expected);
expect(isEqual(actual, expected)).to.be(true);
}

View file

@ -1,5 +1,3 @@
import _ from 'lodash';
import expect from 'expect.js';
import ngMock from 'ng_mock';
import { VisProvider } from 'ui/vis';
@ -38,7 +36,7 @@ describe('AggConfig Filters', function () {
const aggConfig = vis.aggs.byTypeName.filters[0];
const filter = createFilter(aggConfig, 'type:nginx');
expect(_.omit(filter, 'meta')).to.eql(aggConfig.params.filters[1].input);
expect(filter.query.query_string.query).to.be('type:nginx');
expect(filter.meta).to.have.property('index', indexPattern.id);
});

View file

@ -1,5 +1,6 @@
import _ from 'lodash';
import angular from 'angular';
import { luceneStringToDsl } from '../../courier/data_source/build_query/lucene_string_to_dsl.js';
import { AggTypesBucketsBucketAggTypeProvider } from 'ui/agg_types/buckets/_bucket_agg_type';
import { AggTypesBucketsCreateFilterFiltersProvider } from 'ui/agg_types/buckets/create_filter/filters';
@ -27,15 +28,16 @@ export function AggTypesBucketsFiltersProvider(Private, Notifier) {
if (!_.size(inFilters)) return;
const outFilters = _.transform(inFilters, function (filters, filter) {
const input = filter.input;
const input = _.cloneDeep(filter.input);
if (!input) return notif.log('malformed filter agg params, missing "input" query');
const query = input.query;
const query = input.query = luceneStringToDsl(input.query);
if (!query) return notif.log('malformed filter agg params, missing "query" on input');
decorateQuery(query);
const label = filter.label || _.get(query, 'query_string.query') || angular.toJson(query);
const matchAllLabel = (filter.input.query === '' && _.has(query, 'match_all')) ? '*' : '';
const label = filter.label || matchAllLabel || _.get(query, 'query_string.query') || angular.toJson(query);
filters[label] = input;
}, {});

View file

@ -6,15 +6,15 @@ import 'ui/promises';
import { RequestQueueProvider } from '../_request_queue';
import { ErrorHandlersProvider } from '../_error_handlers';
import { FetchProvider } from '../fetch';
import { DecorateQueryProvider } from './_decorate_query';
import { FieldWildcardProvider } from '../../field_wildcard';
import { getHighlightRequest } from '../../../../core_plugins/kibana/common/highlight';
import { migrateFilter } from './_migrate_filter';
import { BuildESQueryProvider } from './build_query';
export function AbstractDataSourceProvider(Private, Promise, PromiseEmitter, config) {
const requestQueue = Private(RequestQueueProvider);
const errorHandlers = Private(ErrorHandlersProvider);
const courierFetch = Private(FetchProvider);
const buildESQuery = Private(BuildESQueryProvider);
const { fieldWildcardFilter } = Private(FieldWildcardProvider);
const getConfig = (...args) => config.get(...args);
@ -301,17 +301,8 @@ export function AbstractDataSourceProvider(Private, Promise, PromiseEmitter, con
.then(function () {
if (type === 'search') {
// This is down here to prevent the circular dependency
const decorateQuery = Private(DecorateQueryProvider);
flatState.body = flatState.body || {};
// defaults for the query
if (!flatState.body.query) {
flatState.body.query = {
'match_all': {}
};
}
const computedFields = flatState.index.getComputedFields();
flatState.body.stored_fields = computedFields.storedFields;
flatState.body.script_fields = flatState.body.script_fields || {};
@ -339,27 +330,20 @@ export function AbstractDataSourceProvider(Private, Promise, PromiseEmitter, con
_.set(flatState.body, '_source.includes', remainingFields);
}
decorateQuery(flatState.body.query);
flatState.body.query = buildESQuery(flatState.index, flatState.query, flatState.filters);
if (flatState.highlightAll != null) {
if (flatState.highlightAll && flatState.body.query) {
flatState.body.highlight = getHighlightRequest(flatState.body.query, getConfig);
}
delete flatState.highlightAll;
}
/**
* Create a filter that can be reversed for filters with negate set
* @param {boolean} reverse This will reverse the filter. If true then
* anything where negate is set will come
* through otherwise it will filter out
* @returns {function}
* Translate a filter into a query to support es 3+
* @param {Object} filter - The filter to translate
* @return {Object} the query version of that filter
*/
const filterNegate = function (reverse) {
return function (filter) {
if (_.isUndefined(filter.meta) || _.isUndefined(filter.meta.negate)) return !reverse;
return filter.meta && filter.meta.negate === reverse;
};
};
/**
* Translate a filter into a query to support es 3+
* @param {Object} filter - The filter to translate
* @return {Object} the query version of that filter
*/
const translateToQuery = function (filter) {
if (!filter) return;
@ -370,55 +354,6 @@ export function AbstractDataSourceProvider(Private, Promise, PromiseEmitter, con
return filter;
};
/**
* Clean out any invalid attributes from the filters
* @param {object} filter The filter to clean
* @returns {object}
*/
const cleanFilter = function (filter) {
return _.omit(filter, ['meta']);
};
// switch to filtered query if there are filters
if (flatState.filters) {
if (flatState.filters.length) {
_.each(flatState.filters, function (filter) {
if (filter.query) {
decorateQuery(filter.query);
}
});
flatState.body.query = {
bool: {
must: (
[flatState.body.query].concat(
(flatState.filters || [])
.filter(filterNegate(false))
.map(translateToQuery)
.map(cleanFilter)
.map(migrateFilter)
)
),
must_not: (
(flatState.filters || [])
.filter(filterNegate(true))
.map(translateToQuery)
.map(cleanFilter)
.map(migrateFilter)
)
}
};
}
delete flatState.filters;
}
if (flatState.highlightAll != null) {
if (flatState.highlightAll && flatState.body.query) {
flatState.body.highlight = getHighlightRequest(flatState.body.query, getConfig);
}
delete flatState.highlightAll;
}
// re-write filters within filter aggregations
(function recurse(aggBranch) {
if (!aggBranch) return;

View file

@ -0,0 +1,70 @@
import { BuildESQueryProvider } from '../build_es_query';
import StubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import ngMock from 'ng_mock';
import { expectDeepEqual } from '../../../../../../test_utils/expect_deep_equal.js';
import { fromKueryExpression, toElasticsearchQuery } from '../../../../kuery';
import { luceneStringToDsl } from '../lucene_string_to_dsl';
import { DecorateQueryProvider } from '../../_decorate_query';
let indexPattern;
let buildEsQuery;
let decorateQuery;
describe('build query', function () {
describe('buildESQuery', function () {
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
indexPattern = Private(StubbedLogstashIndexPatternProvider);
buildEsQuery = Private(BuildESQueryProvider);
decorateQuery = Private(DecorateQueryProvider);
}));
it('should return the parameters of an Elasticsearch bool query', function () {
const result = buildEsQuery();
const expected = {
bool: {
must: [],
filter: [],
should: [],
must_not: [],
}
};
expectDeepEqual(result, expected);
});
it('should combine queries and filters from multiple query languages into a single ES bool query', function () {
const queries = [
{ query: 'foo:bar', language: 'kuery' },
{ query: 'bar:baz', language: 'lucene' },
];
const filters = [
{
match_all: {},
meta: { type: 'match_all' },
},
];
const expectedResult = {
bool: {
must: [
decorateQuery(luceneStringToDsl('bar:baz')),
{ match_all: {} },
],
filter: [
toElasticsearchQuery(fromKueryExpression('foo:bar'), indexPattern),
],
should: [],
must_not: [],
}
};
const result = buildEsQuery(indexPattern, queries, filters);
expectDeepEqual(result, expectedResult);
});
});
});

View file

@ -0,0 +1,107 @@
import { buildQueryFromFilters } from '../from_filters';
import { DecorateQueryProvider } from '../../_decorate_query.js';
import ngMock from 'ng_mock';
import { expectDeepEqual } from '../../../../../../test_utils/expect_deep_equal.js';
let decorateQuery;
describe('build query', function () {
describe('buildQueryFromFilters', function () {
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
decorateQuery = Private(DecorateQueryProvider);
}));
it('should return the parameters of an Elasticsearch bool query', function () {
const result = buildQueryFromFilters([]);
const expected = {
must: [],
filter: [],
should: [],
must_not: [],
};
expectDeepEqual(result, expected);
});
it('should transform an array of kibana filters into ES queries combined in the bool clauses', function () {
const filters = [
{
match_all: {},
meta: { type: 'match_all' }
},
{
exists: { field: 'foo' },
meta: { type: 'exists' }
}
];
const expectedESQueries = [
{ match_all: {} },
{ exists: { field: 'foo' } }
];
const result = buildQueryFromFilters(filters, decorateQuery);
expectDeepEqual(result.must, expectedESQueries);
});
it('should place negated filters in the must_not clause', function () {
const filters = [
{
match_all: {},
meta: { type: 'match_all', negate: true }
},
];
const expectedESQueries = [
{ match_all: {} },
];
const result = buildQueryFromFilters(filters, decorateQuery);
expectDeepEqual(result.must_not, expectedESQueries);
});
it('should translate old ES filter syntax into ES 5+ query objects', function () {
const filters = [
{
query: { exists: { field: 'foo' } },
meta: { type: 'exists' }
}
];
const expectedESQueries = [
{
exists: { field: 'foo' }
}
];
const result = buildQueryFromFilters(filters, decorateQuery);
expectDeepEqual(result.must, expectedESQueries);
});
it('should migrate deprecated match syntax', function () {
const filters = [
{
query: { match: { extension: { query: 'foo', type: 'phrase' } } },
meta: { type: 'phrase' }
}
];
const expectedESQueries = [
{
match_phrase: { extension: { query: 'foo' } },
}
];
const result = buildQueryFromFilters(filters, decorateQuery);
expectDeepEqual(result.must, expectedESQueries);
});
});
});

View file

@ -0,0 +1,48 @@
import { buildQueryFromKuery } from '../from_kuery';
import StubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import ngMock from 'ng_mock';
import { expectDeepEqual } from '../../../../../../test_utils/expect_deep_equal.js';
import { fromKueryExpression, toElasticsearchQuery } from '../../../../kuery';
let indexPattern;
describe('build query', function () {
describe('buildQueryFromKuery', function () {
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
indexPattern = Private(StubbedLogstashIndexPatternProvider);
}));
it('should return the parameters of an Elasticsearch bool query', function () {
const result = buildQueryFromKuery();
const expected = {
must: [],
filter: [],
should: [],
must_not: [],
};
expectDeepEqual(result, expected);
});
it('should transform an array of kuery queries into ES queries combined in the bool\'s filter clause', function () {
const queries = [
{ query: 'foo:bar', language: 'kuery' },
{ query: 'bar:baz', language: 'kuery' },
];
const expectedESQueries = queries.map(
(query) => {
return toElasticsearchQuery(fromKueryExpression(query.query), indexPattern);
}
);
const result = buildQueryFromKuery(indexPattern, queries);
expectDeepEqual(result.filter, expectedESQueries);
});
});
});

View file

@ -0,0 +1,58 @@
import { buildQueryFromLucene } from '../from_lucene';
import { DecorateQueryProvider } from '../../_decorate_query.js';
import ngMock from 'ng_mock';
import { expectDeepEqual } from '../../../../../../test_utils/expect_deep_equal.js';
import { luceneStringToDsl } from '../lucene_string_to_dsl';
let decorateQuery;
describe('build query', function () {
describe('buildQueryFromLucene', function () {
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
decorateQuery = Private(DecorateQueryProvider);
}));
it('should return the parameters of an Elasticsearch bool query', function () {
const result = buildQueryFromLucene();
const expected = {
must: [],
filter: [],
should: [],
must_not: [],
};
expectDeepEqual(result, expected);
});
it('should transform an array of lucene queries into ES queries combined in the bool\'s must clause', function () {
const queries = [
{ query: 'foo:bar', language: 'lucene' },
{ query: 'bar:baz', language: 'lucene' },
];
const expectedESQueries = queries.map(
(query) => {
return decorateQuery(luceneStringToDsl(query.query));
}
);
const result = buildQueryFromLucene(queries, decorateQuery);
expectDeepEqual(result.must, expectedESQueries);
});
it('should also accept queries in ES query DSL format, simply passing them through', function () {
const queries = [
{ query: { match_all: {} }, language: 'lucene' },
];
const result = buildQueryFromLucene(queries, decorateQuery);
expectDeepEqual(result.must, [queries[0].query]);
});
});
});

View file

@ -0,0 +1,35 @@
import { luceneStringToDsl } from '../lucene_string_to_dsl';
import { expectDeepEqual } from '../../../../../../test_utils/expect_deep_equal.js';
import expect from 'expect.js';
describe('build query', function () {
describe('luceneStringToDsl', function () {
it('should wrap strings with an ES query_string query', function () {
const result = luceneStringToDsl('foo:bar');
const expectedResult = {
query_string: { query: 'foo:bar' }
};
expectDeepEqual(result, expectedResult);
});
it('should return a match_all query for empty strings and whitespace', function () {
const expectedResult = {
match_all: {}
};
expectDeepEqual(luceneStringToDsl(''), expectedResult);
expectDeepEqual(luceneStringToDsl(' '), expectedResult);
});
it('should return non-string arguments without modification', function () {
const expectedResult = {};
const result = luceneStringToDsl(expectedResult);
expect(result).to.be(expectedResult);
expectDeepEqual(result, expectedResult);
});
});
});

View file

@ -0,0 +1,34 @@
import { groupBy, has } from 'lodash';
import { DecorateQueryProvider } from '../_decorate_query';
import { buildQueryFromKuery } from './from_kuery';
import { buildQueryFromFilters } from './from_filters';
import { buildQueryFromLucene } from './from_lucene';
export function BuildESQueryProvider(Private) {
const decorateQuery = Private(DecorateQueryProvider);
/**
*
* @param queries - an array of query objects. Each query has a language property and a query property.
* @param filters - an array of filter objects
*/
function buildESQuery(indexPattern, queries = [], filters = []) {
const validQueries = queries.filter((query) => has(query, 'query'));
const queriesByLanguage = groupBy(validQueries, 'language');
const kueryQuery = buildQueryFromKuery(indexPattern, queriesByLanguage.kuery);
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),
}
};
}
return buildESQuery;
}

View file

@ -0,0 +1,63 @@
import _ from 'lodash';
import { migrateFilter } from '../_migrate_filter';
/**
* Create a filter that can be reversed for filters with negate set
* @param {boolean} reverse This will reverse the filter. If true then
* anything where negate is set will come
* through otherwise it will filter out
* @returns {function}
*/
const filterNegate = function (reverse) {
return function (filter) {
if (_.isUndefined(filter.meta) || _.isUndefined(filter.meta.negate)) return !reverse;
return filter.meta && filter.meta.negate === reverse;
};
};
/**
* Translate a filter into a query to support es 5+
* @param {Object} filter - The filter to translate
* @return {Object} the query version of that filter
*/
const translateToQuery = function (filter) {
if (!filter) return;
if (filter.query) {
return filter.query;
}
return filter;
};
/**
* Clean out any invalid attributes from the filters
* @param {object} filter The filter to clean
* @returns {object}
*/
const cleanFilter = function (filter) {
return _.omit(filter, ['meta']);
};
export function buildQueryFromFilters(filters, decorateQuery) {
_.each(filters, function (filter) {
if (filter.query) {
decorateQuery(filter.query);
}
});
return {
must: (filters || [])
.filter(filterNegate(false))
.map(translateToQuery)
.map(cleanFilter)
.map(migrateFilter),
filter: [],
should: [],
must_not: (filters || [])
.filter(filterNegate(true))
.map(translateToQuery)
.map(cleanFilter)
.map(migrateFilter)
};
}

View file

@ -0,0 +1,16 @@
import _ from 'lodash';
import { fromKueryExpression, toElasticsearchQuery, nodeTypes } from '../../../kuery';
export function buildQueryFromKuery(indexPattern, queries) {
const queryASTs = _.map(queries, query => fromKueryExpression(query.query));
const compoundQueryAST = nodeTypes.function.buildNode('and', queryASTs);
const kueryQuery = toElasticsearchQuery(compoundQueryAST, indexPattern);
return Object.assign({
must: [],
filter: [],
should: [],
must_not: [],
}, kueryQuery.bool);
}

View file

@ -0,0 +1,16 @@
import _ from 'lodash';
import { luceneStringToDsl } from './lucene_string_to_dsl';
export function buildQueryFromLucene(queries, decorateQuery) {
const combinedQueries = _.map(queries, (query) => {
const queryDsl = luceneStringToDsl(query.query);
return decorateQuery(queryDsl);
});
return {
must: [].concat(combinedQueries),
filter: [],
should: [],
must_not: [],
};
}

View file

@ -0,0 +1 @@
export { BuildESQueryProvider } from './build_es_query';

View file

@ -0,0 +1,13 @@
import _ from 'lodash';
export function luceneStringToDsl(query) {
if (!_.isString(query)) {
return query;
}
if (query.trim() === '') {
return { match_all: {} };
}
return { query_string: { query } };
}

View file

@ -269,6 +269,9 @@ export function SearchSourceProvider(Promise, Private, config) {
val = normalizeSortRequest(val, this.get('index'));
addToBody();
break;
case 'query':
state.query = (state.query || []).concat(val);
break;
case 'fields':
state[key] = _.uniq([...(state[key] || []), ...val]);
break;
@ -283,10 +286,6 @@ export function SearchSourceProvider(Promise, Private, config) {
state.body = state.body || {};
// ignore if we already have a value
if (state.body[key] == null) {
if (key === 'query' && _.isString(val)) {
val = { query_string: { query: val } };
}
state.body[key] = val;
}
}

View file

@ -17,6 +17,7 @@ import MappingSetupProvider from 'ui/utils/mapping_setup';
import { SearchSourceProvider } from '../data_source/search_source';
import { SavedObjectsClientProvider, findObjectByTitle } from 'ui/saved_objects';
import { migrateLegacyQuery } from '../../utils/migrateLegacyQuery.js';
/**
* An error message to be used when the user rejects a confirm overwrite.
@ -105,6 +106,8 @@ export function SavedObjectProvider(Promise, Private, Notifier, confirmModalProm
}, {});
this.searchSource.set(_.defaults(state, fnProps));
this.searchSource.set('query', migrateLegacyQuery(this.searchSource.getOwn('query')));
};
/**

View file

@ -66,29 +66,21 @@ describe('parse-query directive', function () {
expect(fromUser({ foo: 'bar' })).to.eql({ foo: 'bar' });
});
it('unless the object is empty, that implies a *', function () {
expect(fromUser({})).to.eql({ query_string: { query: '*' } });
it('unless the object is empty, then convert it to an empty string', function () {
expect(fromUser({})).to.eql('');
});
it('should treat an empty string as a *', function () {
expect(fromUser('')).to.eql({ query_string: { query: '*' } });
it('should pass through input strings that not start with {', function () {
expect(fromUser('foo')).to.eql('foo');
expect(fromUser('400')).to.eql('400');
expect(fromUser('true')).to.eql('true');
});
it('should merge in the query string options', function () {
config.set('query:queryString:options', { analyze_wildcard: true });
expect(fromUser('foo')).to.eql({ query_string: { query: 'foo', analyze_wildcard: true } });
expect(fromUser('')).to.eql({ query_string: { query: '*', analyze_wildcard: true } });
});
it('should treat input that does not start with { as a query string', function () {
expect(fromUser('foo')).to.eql({ query_string: { query: 'foo' } });
expect(fromUser('400')).to.eql({ query_string: { query: '400' } });
expect(fromUser('true')).to.eql({ query_string: { query: 'true' } });
});
it('should parse valid JSON', function () {
it('should parse valid JSON and return the object instead of a string', function () {
expect(fromUser('{}')).to.eql({});
expect(fromUser('{a:b}')).to.eql({ query_string: { query: '{a:b}' } });
// invalid json remains a string
expect(fromUser('{a:b}')).to.eql('{a:b}');
});
});

View file

@ -25,7 +25,7 @@ describe('Filter Bar pushFilter()', function () {
let filter;
beforeEach(ngMock.inject(function () {
$state = { filters:[] };
$state = { $newFilters:[] };
pushFilter = pushFilterFn($state);
filter = { query: { query_string: { query: '' } } };
}));
@ -33,7 +33,7 @@ describe('Filter Bar pushFilter()', function () {
it('should create the filters property it needed', function () {
const altState = {};
pushFilterFn(altState)(filter);
expect(altState.filters).to.be.an(Array);
expect(altState.$newFilters).to.be.an(Array);
});
it('should replace the filters property instead of modifying it', function () {
@ -41,24 +41,24 @@ describe('Filter Bar pushFilter()', function () {
let oldFilters;
oldFilters = $state.filters;
$state.filters.push(filter);
expect($state.filters).to.equal(oldFilters); // Same object
oldFilters = $state.$newFilters;
$state.$newFilters.push(filter);
expect($state.$newFilters).to.equal(oldFilters); // Same object
oldFilters = $state.filters;
oldFilters = $state.$newFilters;
pushFilter(filter);
expect($state.filters).to.not.equal(oldFilters); // New object!
expect($state.$newFilters).to.not.equal(oldFilters); // New object!
});
it('should add meta data to the filter', function () {
pushFilter(filter, true, 'myIndex');
expect($state.filters[0].meta).to.be.an(Object);
expect($state.$newFilters[0].meta).to.be.an(Object);
expect($state.filters[0].meta.negate).to.be(true);
expect($state.filters[0].meta.index).to.be('myIndex');
expect($state.$newFilters[0].meta.negate).to.be(true);
expect($state.$newFilters[0].meta.index).to.be('myIndex');
pushFilter(filter, false, 'myIndex');
expect($state.filters[1].meta.negate).to.be(false);
expect($state.$newFilters[0].meta.negate).to.be(false);
});

View file

@ -0,0 +1,82 @@
import expect from 'expect.js';
import ngMock from 'ng_mock';
import { FilterBarLibMapGeoPolygonProvider } from 'ui/filter_bar/lib/map_geo_polygon';
describe('Filter Bar Directive', function () {
describe('mapGeoPolygon()', function () {
let mapGeoPolygon;
let $rootScope;
beforeEach(ngMock.module(
'kibana',
'kibana/courier',
function ($provide) {
$provide.service('courier', require('fixtures/mock_courier'));
}
));
beforeEach(ngMock.inject(function (Private, _$rootScope_) {
mapGeoPolygon = Private(FilterBarLibMapGeoPolygonProvider);
$rootScope = _$rootScope_;
}));
it('should return the key and value for matching filters with bounds', function (done) {
const filter = {
meta: {
index: 'logstash-*'
},
geo_polygon: {
point: { // field name
points: [
{ lat: 5, lon: 10 },
{ lat: 15, lon: 20 }
]
}
}
};
mapGeoPolygon(filter).then(function (result) {
expect(result).to.have.property('key', 'point');
expect(result).to.have.property('value');
// remove html entities and non-alphanumerics to get the gist of the value
expect(result.value.replace(/&[a-z]+?;/g, '').replace(/[^a-z0-9]/g, '')).to.be('lat5lon10lat15lon20');
done();
});
$rootScope.$apply();
});
it('should return undefined for none matching', function (done) {
const filter = { meta: { index: 'logstash-*' }, query: { query_string: { query: 'foo:bar' } } };
mapGeoPolygon(filter).catch(function (result) {
expect(result).to.be(filter);
done();
});
$rootScope.$apply();
});
it('should return the key and value even when using ignore_unmapped', function (done) {
const filter = {
meta: {
index: 'logstash-*'
},
geo_polygon: {
ignore_unmapped: true,
point: { // field name
points: [
{ lat: 5, lon: 10 },
{ lat: 15, lon: 20 }
]
}
}
};
mapGeoPolygon(filter).then(function (result) {
expect(result).to.have.property('key', 'point');
expect(result).to.have.property('value');
// remove html entities and non-alphanumerics to get the gist of the value
expect(result.value.replace(/&[a-z]+?;/g, '').replace(/[^a-z0-9]/g, '')).to.be('lat5lon10lat15lon20');
done();
});
$rootScope.$apply();
});
});
});

View file

@ -8,6 +8,7 @@ import { FilterBarLibMapExistsProvider } from './map_exists';
import { FilterBarLibMapMissingProvider } from './map_missing';
import { FilterBarLibMapQueryStringProvider } from './map_query_string';
import { FilterBarLibMapGeoBoundingBoxProvider } from './map_geo_bounding_box';
import { FilterBarLibMapGeoPolygonProvider } from './map_geo_polygon';
import { FilterBarLibMapScriptProvider } from './map_script';
import { FilterBarLibMapDefaultProvider } from './map_default';
@ -40,8 +41,9 @@ export function FilterBarLibMapFilterProvider(Promise, Private) {
Private(FilterBarLibMapMissingProvider),
Private(FilterBarLibMapQueryStringProvider),
Private(FilterBarLibMapGeoBoundingBoxProvider),
Private(FilterBarLibMapGeoPolygonProvider),
Private(FilterBarLibMapScriptProvider),
Private(FilterBarLibMapDefaultProvider)
Private(FilterBarLibMapDefaultProvider),
];
const noop = function () {
@ -68,6 +70,7 @@ export function FilterBarLibMapFilterProvider(Promise, Private) {
filter.meta.type = result.type;
filter.meta.key = result.key;
filter.meta.value = result.value;
filter.meta.params = result.params;
filter.meta.disabled = !!(filter.meta.disabled);
filter.meta.negate = !!(filter.meta.negate);
filter.meta.alias = filter.meta.alias || null;

View file

@ -10,11 +10,11 @@ export function FilterBarLibMapGeoBoundingBoxProvider(Promise, courier) {
const key = _.keys(filter.geo_bounding_box)
.filter(key => key !== 'ignore_unmapped')[0];
const field = indexPattern.fields.byName[key];
const geoBoundingBox = filter.geo_bounding_box[key];
const topLeft = field.format.convert(geoBoundingBox.top_left);
const bottomRight = field.format.convert(geoBoundingBox.bottom_right);
const params = filter.geo_bounding_box[key];
const topLeft = field.format.convert(params.top_left);
const bottomRight = field.format.convert(params.bottom_right);
const value = topLeft + ' to ' + bottomRight;
return { type, key, value };
return { type, key, value, params };
});
}
return Promise.reject(filter);

View file

@ -0,0 +1,21 @@
import _ from 'lodash';
export function FilterBarLibMapGeoPolygonProvider(Promise, courier) {
return function (filter) {
if (filter.geo_polygon) {
return courier
.indexPatterns
.get(filter.meta.index).then(function (indexPattern) {
const type = 'geo_polygon';
const key = _.keys(filter.geo_polygon)
.filter(key => key !== 'ignore_unmapped')[0];
const field = indexPattern.fields.byName[key];
const params = filter.geo_polygon[key];
const points = params.points.map((point) => field.format.convert(point));
const value = points.join(', ');
return { type, key, value, params };
});
}
return Promise.reject(filter);
};
}

View file

@ -13,9 +13,10 @@ export function FilterBarLibMapPhraseProvider(Promise, courier) {
const type = 'phrase';
const key = isScriptedPhraseFilter ? filter.meta.field : Object.keys(filter.query.match)[0];
const field = indexPattern.fields.byName[key];
const query = isScriptedPhraseFilter ? filter.script.script.params.value : filter.query.match[key].query;
const params = isScriptedPhraseFilter ? filter.script.script.params : filter.query.match[key];
const query = isScriptedPhraseFilter ? params.value : params.query;
const value = field.format.convert(query);
return { type, key, value };
return { type, key, value, params };
});
};
}

View file

@ -14,17 +14,17 @@ export function FilterBarLibMapRangeProvider(Promise, courier) {
const type = 'range';
const key = isScriptedRangeFilter ? filter.meta.field : Object.keys(filter.range)[0];
const convert = indexPattern.fields.byName[key].format.getConverterFor('text');
const range = isScriptedRangeFilter ? filter.script.script.params : filter.range[key];
const params = isScriptedRangeFilter ? filter.script.script.params : filter.range[key];
let left = has(range, 'gte') ? range.gte : range.gt;
let left = has(params, 'gte') ? params.gte : params.gt;
if (left == null) left = -Infinity;
let right = has(range, 'lte') ? range.lte : range.lt;
let right = has(params, 'lte') ? params.lte : params.lt;
if (right == null) right = Infinity;
const value = `${convert(left)} to ${convert(right)}`;
return { type, key, value };
return { type, key, value, params };
});
};

View file

@ -7,11 +7,9 @@ export function FilterBarPushFilterProvider() {
// Hierarchical and tabular data set their aggConfigResult parameter
// differently because of how the point is rewritten between the two. So
// we need to check if the point.orig is set, if not use try the point.aggConfigResult
const filters = _.clone($state.filters || []);
const pendingFilter = { meta: { negate: negate, index: index } };
_.extend(pendingFilter, filter);
filters.push(pendingFilter);
$state.filters = filters;
$state.$newFilters = [pendingFilter];
};
};
}

View file

@ -28,41 +28,7 @@ export function buildRangeFilter(field, params, indexPattern, formattedValue) {
filter.match_all = {};
filter.meta.field = field.name;
} else if (field.scripted) {
const operators = {
gt: '>',
gte: '>=',
lte: '<=',
lt: '<',
};
const comparators = {
gt: 'boolean gt(Supplier s, def v) {return s.get() > v}',
gte: 'boolean gte(Supplier s, def v) {return s.get() >= v}',
lte: 'boolean lte(Supplier s, def v) {return s.get() <= v}',
lt: 'boolean lt(Supplier s, def v) {return s.get() < v}',
};
const knownParams = _.pick(params, (val, key) => { return key in operators; });
let script = _.map(knownParams, function (val, key) {
return '(' + field.script + ')' + operators[key] + key;
}).join(' && ');
// We must wrap painless scripts in a lambda in case they're more than a simple expression
if (field.lang === 'painless') {
const currentComparators = _.reduce(knownParams, (acc, val, key) => acc.concat(comparators[key]), []).join(' ');
const comparisons = _.map(knownParams, function (val, key) {
return `${key}(() -> { ${field.script} }, params.${key})`;
}).join(' && ');
script = `${currentComparators}${comparisons}`;
}
const value = _.map(knownParams, function (val, key) {
return operators[key] + field.format.convert(val);
}).join(' ');
_.set(filter, 'script.script', { inline: script, params: knownParams, lang: field.lang });
filter.script.script.params.value = value;
filter.script = getRangeScript(field, params);
filter.meta.field = field.name;
} else {
filter.range = {};
@ -71,3 +37,51 @@ export function buildRangeFilter(field, params, indexPattern, formattedValue) {
return filter;
}
export function getRangeScript(field, params) {
const operators = {
gt: '>',
gte: '>=',
lte: '<=',
lt: '<',
};
const comparators = {
gt: 'boolean gt(Supplier s, def v) {return s.get() > v}',
gte: 'boolean gte(Supplier s, def v) {return s.get() >= v}',
lte: 'boolean lte(Supplier s, def v) {return s.get() <= v}',
lt: 'boolean lt(Supplier s, def v) {return s.get() < v}',
};
const knownParams = _.pick(params, (val, key) => {
return key in operators;
});
let script = _.map(knownParams, function (val, key) {
return '(' + field.script + ')' + operators[key] + key;
}).join(' && ');
// We must wrap painless scripts in a lambda in case they're more than a simple expression
if (field.lang === 'painless') {
const currentComparators = _.reduce(knownParams, (acc, val, key) => acc.concat(comparators[key]), []).join(' ');
const comparisons = _.map(knownParams, function (val, key) {
return `${key}(() -> { ${field.script} }, params.${key})`;
}).join(' && ');
script = `${currentComparators}${comparisons}`;
}
const value = _.map(knownParams, function (val, key) {
return operators[key] + field.format.convert(val);
}).join(' ');
return {
script: {
inline: script,
params: {
...knownParams,
value,
},
lang: field.lang
}
};
}

View file

@ -0,0 +1,253 @@
import * as ast from '../ast';
import expect from 'expect.js';
import { nodeTypes } from '../../node_types/index';
import StubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import ngMock from 'ng_mock';
import { expectDeepEqual } from '../../../../../test_utils/expect_deep_equal.js';
// Helpful utility allowing us to test the PEG parser by simply checking for deep equality between
// the nodes the parser generates and the nodes our constructor functions generate.
function fromKueryExpressionNoMeta(text) {
return ast.fromKueryExpression(text, { includeMetadata: false });
}
let indexPattern;
describe('kuery AST API', function () {
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
indexPattern = Private(StubbedLogstashIndexPatternProvider);
}));
describe('fromKueryExpression', function () {
it('should return location and text metadata for each AST node', function () {
const notNode = ast.fromKueryExpression('!foo:bar');
expect(notNode).to.have.property('text', '!foo:bar');
expect(notNode.location).to.eql({ min: 0, max: 8 });
const isNode = notNode.arguments[0];
expect(isNode).to.have.property('text', 'foo:bar');
expect(isNode.location).to.eql({ min: 1, max: 8 });
const { arguments: [ argNode1, argNode2 ] } = isNode;
expect(argNode1).to.have.property('text', 'foo');
expect(argNode1.location).to.eql({ min: 1, max: 4 });
expect(argNode2).to.have.property('text', 'bar');
expect(argNode2.location).to.eql({ min: 5, max: 8 });
});
it('should return a match all "is" function for whitespace', function () {
const expected = nodeTypes.function.buildNode('is', '*', '*');
const actual = fromKueryExpressionNoMeta(' ');
expectDeepEqual(actual, expected);
});
it('should return an "and" function for single literals', function () {
const expected = nodeTypes.function.buildNode('and', [nodeTypes.literal.buildNode('foo')], 'implicit');
const actual = fromKueryExpressionNoMeta('foo');
expectDeepEqual(actual, expected);
});
it('should ignore extraneous whitespace at the beginning and end of the query', function () {
const expected = nodeTypes.function.buildNode('and', [nodeTypes.literal.buildNode('foo')], 'implicit');
const actual = fromKueryExpressionNoMeta(' foo ');
expectDeepEqual(actual, expected);
});
it('literals and queries separated by whitespace should be joined by an implicit "and"', function () {
const expected = nodeTypes.function.buildNode('and', [
nodeTypes.literal.buildNode('foo'),
nodeTypes.literal.buildNode('bar'),
], 'implicit');
const actual = fromKueryExpressionNoMeta('foo bar');
expectDeepEqual(actual, expected);
});
it('should also support explicit "and"s as a binary operator', function () {
const expected = nodeTypes.function.buildNode('and', [
nodeTypes.literal.buildNode('foo'),
nodeTypes.literal.buildNode('bar'),
], 'operator');
const actual = fromKueryExpressionNoMeta('foo and bar');
expectDeepEqual(actual, expected);
});
it('should also support "and" as a function', function () {
const expected = nodeTypes.function.buildNode('and', [
nodeTypes.literal.buildNode('foo'),
nodeTypes.literal.buildNode('bar'),
], 'function');
const actual = fromKueryExpressionNoMeta('and(foo, bar)');
expectDeepEqual(actual, expected);
});
it('should support "or" as a binary operator', function () {
const expected = nodeTypes.function.buildNode('or', [
nodeTypes.literal.buildNode('foo'),
nodeTypes.literal.buildNode('bar'),
], 'operator');
const actual = fromKueryExpressionNoMeta('foo or bar');
expectDeepEqual(actual, expected);
});
it('should support "or" as a function', function () {
const expected = nodeTypes.function.buildNode('or', [
nodeTypes.literal.buildNode('foo'),
nodeTypes.literal.buildNode('bar'),
], 'function');
const actual = fromKueryExpressionNoMeta('or(foo, bar)');
expectDeepEqual(actual, expected);
});
it('should support negation of queries with a "!" prefix', function () {
const expected = nodeTypes.function.buildNode('not',
nodeTypes.function.buildNode('or', [
nodeTypes.literal.buildNode('foo'),
nodeTypes.literal.buildNode('bar'),
], 'function'), 'operator');
const actual = fromKueryExpressionNoMeta('!or(foo, bar)');
expectDeepEqual(actual, expected);
});
it('"and" should have a higher precedence than "or"', function () {
const expected = nodeTypes.function.buildNode('or', [
nodeTypes.literal.buildNode('foo'),
nodeTypes.function.buildNode('or', [
nodeTypes.function.buildNode('and', [
nodeTypes.literal.buildNode('bar'),
nodeTypes.literal.buildNode('baz'),
], 'operator'),
nodeTypes.literal.buildNode('qux'),
])
], 'operator');
const actual = fromKueryExpressionNoMeta('foo or bar and baz or qux');
expectDeepEqual(actual, expected);
});
it('should support grouping to override default precedence', function () {
const expected = nodeTypes.function.buildNode('and', [
nodeTypes.function.buildNode('or', [
nodeTypes.literal.buildNode('foo'),
nodeTypes.literal.buildNode('bar'),
], 'operator'),
nodeTypes.literal.buildNode('baz'),
], 'operator');
const actual = fromKueryExpressionNoMeta('(foo or bar) and baz');
expectDeepEqual(actual, expected);
});
it('should support a shorthand operator syntax for "is" functions', function () {
const expected = nodeTypes.function.buildNode('is', 'foo', 'bar', 'operator');
const actual = fromKueryExpressionNoMeta('foo:bar');
expectDeepEqual(actual, expected);
});
it('should support a shorthand operator syntax for inclusive "range" functions', function () {
const argumentNodes = [
nodeTypes.literal.buildNode('bytes'),
nodeTypes.literal.buildNode(1000),
nodeTypes.literal.buildNode(8000),
];
const expected = nodeTypes.function.buildNodeWithArgumentNodes('range', argumentNodes, 'operator');
const actual = fromKueryExpressionNoMeta('bytes:[1000 to 8000]');
expectDeepEqual(actual, expected);
});
it('should support functions with named arguments', function () {
const expected = nodeTypes.function.buildNode('range', 'bytes', { gt: 1000, lt: 8000 }, 'function');
const actual = fromKueryExpressionNoMeta('range(bytes, gt=1000, lt=8000)');
expectDeepEqual(actual, expected);
});
it('should throw an error for unknown functions', function () {
expect(ast.fromKueryExpression).withArgs('foo(bar)').to.throwException(/Unknown function "foo"/);
});
});
describe('toKueryExpression', function () {
it('should return the given node type\'s kuery string representation', function () {
const node = nodeTypes.function.buildNode('exists', 'foo');
const expected = nodeTypes.function.toKueryExpression(node);
const result = ast.toKueryExpression(node);
expectDeepEqual(result, expected);
});
it('should return an empty string for undefined nodes and unknown node types', function () {
expect(ast.toKueryExpression()).to.be('');
const noTypeNode = nodeTypes.function.buildNode('exists', 'foo');
delete noTypeNode.type;
expect(ast.toKueryExpression(noTypeNode)).to.be('');
const unknownTypeNode = nodeTypes.function.buildNode('exists', 'foo');
unknownTypeNode.type = 'notValid';
expect(ast.toKueryExpression(unknownTypeNode)).to.be('');
});
});
describe('toElasticsearchQuery', function () {
it('should return the given node type\'s ES query representation', function () {
const node = nodeTypes.function.buildNode('exists', 'response');
const expected = nodeTypes.function.toElasticsearchQuery(node, indexPattern);
const result = ast.toElasticsearchQuery(node, indexPattern);
expectDeepEqual(result, expected);
});
it('should return an empty "and" function for undefined nodes and unknown node types', function () {
const expected = nodeTypes.function.toElasticsearchQuery(nodeTypes.function.buildNode('and', []));
expectDeepEqual(ast.toElasticsearchQuery(), expected);
const noTypeNode = nodeTypes.function.buildNode('exists', 'foo');
delete noTypeNode.type;
expectDeepEqual(ast.toElasticsearchQuery(noTypeNode), expected);
const unknownTypeNode = nodeTypes.function.buildNode('exists', 'foo');
unknownTypeNode.type = 'notValid';
expectDeepEqual(ast.toElasticsearchQuery(unknownTypeNode), expected);
});
});
describe('symmetry of to/fromKueryExpression', function () {
it('toKueryExpression and fromKueryExpression should be inverse operations', function () {
function testExpression(expression) {
expect(ast.toKueryExpression(ast.fromKueryExpression(expression))).to.be(expression);
}
testExpression('');
testExpression(' ');
testExpression('foo');
testExpression('foo bar');
testExpression('foo 200');
testExpression('bytes:[1000 to 8000]');
testExpression('bytes:[1000 TO 8000]');
testExpression('range(bytes, gt=1000, lt=8000)');
testExpression('range(bytes, gt=1000, lte=8000)');
testExpression('range(bytes, gte=1000, lt=8000)');
testExpression('range(bytes, gte=1000, lte=8000)');
testExpression('response:200');
testExpression('"response":200');
testExpression('response:"200"');
testExpression('"response":"200"');
testExpression('is(response, 200)');
testExpression('!is(response, 200)');
testExpression('foo or is(tic, tock) or foo:bar');
testExpression('or(foo, is(tic, tock), foo:bar)');
testExpression('foo is(tic, tock) foo:bar');
testExpression('foo and is(tic, tock) and foo:bar');
testExpression('(foo or is(tic, tock)) and foo:bar');
testExpression('!(foo or is(tic, tock)) and foo:bar');
});
});
});

View file

@ -0,0 +1,32 @@
import grammar from 'raw!./kuery.peg';
import PEG from 'pegjs';
import _ from 'lodash';
import { nodeTypes } from '../node_types/index';
const kueryParser = PEG.buildParser(grammar);
export function fromKueryExpression(expression, parseOptions = {}) {
if (_.isUndefined(expression)) {
throw new Error('expression must be a string, got undefined instead');
}
parseOptions = Object.assign({}, parseOptions, { helpers: { nodeTypes } });
return kueryParser.parse(expression, parseOptions);
}
export function toKueryExpression(node) {
if (!node || !node.type || !nodeTypes[node.type]) {
return '';
}
return nodeTypes[node.type].toKueryExpression(node);
}
export function toElasticsearchQuery(node, indexPattern) {
if (!node || !node.type || !nodeTypes[node.type]) {
return toElasticsearchQuery(nodeTypes.function.buildNode('and', []));
}
return nodeTypes[node.type].toElasticsearchQuery(node, indexPattern);
}

View file

@ -0,0 +1 @@
export { fromKueryExpression, toKueryExpression, toElasticsearchQuery } from './ast';

View file

@ -0,0 +1,150 @@
/*
* 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
= space? query:OrQuery space? {
if (query.type === 'literal') {
return addMeta(nodeTypes.function.buildNode('and', [query], 'implicit'), text(), location());
}
return query;
}
/ whitespace:[\ \t\r\n]* {
return addMeta(nodeTypes.function.buildNode('is', '*', '*'), text(), location());
}
OrQuery
= left:AndQuery space 'or'i space right:OrQuery {
return addMeta(nodeTypes.function.buildNode('or', [left, right], 'operator'), text(), location());
}
/ AndQuery
AndQuery
= left:NegatedClause space 'and'i space right:AndQuery {
return addMeta(nodeTypes.function.buildNode('and', [left, right], 'operator'), text(), location());
}
/ left:NegatedClause space !'or'i right:AndQuery {
return addMeta(nodeTypes.function.buildNode('and', [left, right], 'implicit'), text(), location());
}
/ NegatedClause
NegatedClause
= [!] clause:Clause {
return addMeta(nodeTypes.function.buildNode('not', clause, 'operator'), text(), location());
}
/ Clause
Clause
= '(' subQuery:start ')' {
return subQuery;
}
/ Term
Term
= field:literal_arg_type ':' value:literal_arg_type {
return addMeta(nodeTypes.function.buildNodeWithArgumentNodes('is', [field, value], 'operator'), text(), location());
}
/ field:literal_arg_type ':[' space? gt:literal_arg_type space 'to'i space lt:literal_arg_type space? ']' {
return addMeta(nodeTypes.function.buildNodeWithArgumentNodes('range', [field, gt, lt], 'operator'), text(), location());
}
/ function
/ !Keywords literal:literal_arg_type { return literal; }
function_name
= first:[a-zA-Z]+ rest:[.a-zA-Z0-9_-]* { return first.join('') + rest.join('') }
function "function"
= name:function_name space? '(' space? arg_list:arg_list? space? ')' {
return addMeta(nodeTypes.function.buildNodeWithArgumentNodes(name, arg_list || [], 'function'), text(), location());
}
arg_list
= first:argument rest:(space? ',' space? arg:argument {return arg})* space? ','? {
return [first].concat(rest);
}
argument
= name:function_name space? '=' space? value:arg_type {
return addMeta(nodeTypes.namedArg.buildNode(name, value), text(), location());
}
/ element:arg_type {return element}
arg_type
= OrQuery
/ literal_arg_type
literal_arg_type
= literal:literal {
var result = addMeta(nodeTypes.literal.buildNode(literal), text(), location());
return result;
}
Keywords
= 'and'i / 'or'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 '
integer
= digits:[0-9]+ {return parseInt(digits.join(''))}

View file

@ -0,0 +1,36 @@
import expect from 'expect.js';
import { convertExistsFilter } from '../exists';
describe('filter to kuery migration', function () {
describe('exists filter', function () {
it('should return a kuery node equivalent to the given filter', function () {
const filter = {
meta: {
type: 'exists',
key: 'foo',
}
};
const result = convertExistsFilter(filter);
expect(result).to.have.property('type', 'function');
expect(result).to.have.property('function', 'exists');
expect(result.arguments[0].value).to.be('foo');
});
it('should throw an exception if the given filter is not of type "exists"', function () {
const filter = {
meta: {
type: 'foo'
}
};
expect(convertExistsFilter).withArgs(filter).to.throwException(
/Expected filter of type "exists", got "foo"/
);
});
});
});

View file

@ -0,0 +1,52 @@
import _ from 'lodash';
import expect from 'expect.js';
import { filterToKueryAST } from '../filter_to_kuery';
import { expectDeepEqual } from '../../../../../test_utils/expect_deep_equal.js';
describe('filter to kuery migration', function () {
describe('filterToKueryAST', function () {
it('should hand off conversion of known filter types to the appropriate converter', function () {
const filter = {
meta: {
type: 'exists',
key: 'foo',
}
};
const result = filterToKueryAST(filter);
expect(result).to.have.property('type', 'function');
expect(result).to.have.property('function', 'exists');
});
it('should thrown an error when an unknown filter type is encountered', function () {
const filter = {
meta: {
type: 'foo',
}
};
expect(filterToKueryAST).withArgs(filter).to.throwException(/Couldn't convert that filter to a kuery/);
});
it('should wrap the AST node of negated filters in a "not" function', function () {
const filter = {
meta: {
type: 'exists',
key: 'foo',
}
};
const negatedFilter = _.set(_.cloneDeep(filter), 'meta.negate', true);
const result = filterToKueryAST(filter);
const negatedResult = filterToKueryAST(negatedFilter);
expect(negatedResult).to.have.property('type', 'function');
expect(negatedResult).to.have.property('function', 'not');
expectDeepEqual(negatedResult.arguments[0], result);
});
});
});

View file

@ -0,0 +1,53 @@
import _ from 'lodash';
import expect from 'expect.js';
import { convertGeoBoundingBox } from '../geo_bounding_box';
describe('filter to kuery migration', function () {
describe('geo_bounding_box filter', function () {
it('should return a kuery node equivalent to the given filter', function () {
const filter = {
meta: {
type: 'geo_bounding_box',
key: 'foo',
params: {
topLeft: {
lat: 10,
lon: 20,
},
bottomRight: {
lat: 30,
lon: 40,
},
},
}
};
const result = convertGeoBoundingBox(filter);
expect(result).to.have.property('type', 'function');
expect(result).to.have.property('function', 'geoBoundingBox');
const { arguments: [ { value: fieldName }, ...args ] } = result;
expect(fieldName).to.be('foo');
const argByName = _.mapKeys(args, 'name');
expect(argByName.topLeft.value.value).to.be('10, 20');
expect(argByName.bottomRight.value.value).to.be('30, 40');
});
it('should throw an exception if the given filter is not of type "geo_bounding_box"', function () {
const filter = {
meta: {
type: 'foo'
}
};
expect(convertGeoBoundingBox).withArgs(filter).to.throwException(
/Expected filter of type "geo_bounding_box", got "foo"/
);
});
});
});

View file

@ -0,0 +1,53 @@
import expect from 'expect.js';
import { convertGeoPolygon } from '../geo_polygon';
describe('filter to kuery migration', function () {
describe('geo_polygon filter', function () {
it('should return a kuery node equivalent to the given filter', function () {
const filter = {
meta: {
type: 'geo_polygon',
key: 'foo',
params: {
points: [
{
lat: 10,
lon: 20,
},
{
lat: 30,
lon: 40,
},
]
}
}
};
const result = convertGeoPolygon(filter);
expect(result).to.have.property('type', 'function');
expect(result).to.have.property('function', 'geoPolygon');
const { arguments: [ { value: fieldName }, ...args ] } = result;
expect(fieldName).to.be('foo');
expect(args[0].value).to.be('10, 20');
expect(args[1].value).to.be('30, 40');
});
it('should throw an exception if the given filter is not of type "geo_polygon"', function () {
const filter = {
meta: {
type: 'foo'
}
};
expect(convertGeoPolygon).withArgs(filter).to.throwException(
/Expected filter of type "geo_polygon", got "foo"/
);
});
});
});

View file

@ -0,0 +1,42 @@
import expect from 'expect.js';
import { convertPhraseFilter } from '../phrase';
describe('filter to kuery migration', function () {
describe('phrase filter', function () {
it('should return a kuery node equivalent to the given filter', function () {
const filter = {
meta: {
type: 'phrase',
key: 'foo',
params: {
query: 'bar'
},
}
};
const result = convertPhraseFilter(filter);
expect(result).to.have.property('type', 'function');
expect(result).to.have.property('function', 'is');
const { arguments: [ { value: fieldName }, { value: value } ] } = result;
expect(fieldName).to.be('foo');
expect(value).to.be('bar');
});
it('should throw an exception if the given filter is not of type "phrase"', function () {
const filter = {
meta: {
type: 'foo'
}
};
expect(convertPhraseFilter).withArgs(filter).to.throwException(
/Expected filter of type "phrase", got "foo"/
);
});
});
});

View file

@ -0,0 +1,47 @@
import _ from 'lodash';
import expect from 'expect.js';
import { convertRangeFilter } from '../range';
describe('filter to kuery migration', function () {
describe('range filter', function () {
it('should return a kuery node equivalent to the given filter', function () {
const filter = {
meta: {
type: 'range',
key: 'foo',
params: {
gt: 1000,
lt: 8000,
},
}
};
const result = convertRangeFilter(filter);
expect(result).to.have.property('type', 'function');
expect(result).to.have.property('function', 'range');
const { arguments: [ { value: fieldName }, ...args ] } = result;
expect(fieldName).to.be('foo');
const argByName = _.mapKeys(args, 'name');
expect(argByName.gt.value.value).to.be(1000);
expect(argByName.lt.value.value).to.be(8000);
});
it('should throw an exception if the given filter is not of type "range"', function () {
const filter = {
meta: {
type: 'foo'
}
};
expect(convertRangeFilter).withArgs(filter).to.throwException(
/Expected filter of type "range", got "foo"/
);
});
});
});

View file

@ -0,0 +1,10 @@
import { nodeTypes } from '../node_types';
export function convertExistsFilter(filter) {
if (filter.meta.type !== 'exists') {
throw new Error(`Expected filter of type "exists", got "${filter.meta.type}"`);
}
const { key } = filter.meta;
return nodeTypes.function.buildNode('exists', key);
}

View file

@ -0,0 +1,35 @@
import { nodeTypes } from '../node_types';
import { convertPhraseFilter } from './phrase';
import { convertRangeFilter } from './range';
import { convertExistsFilter } from './exists';
import { convertGeoBoundingBox } from './geo_bounding_box';
import { convertGeoPolygon } from './geo_polygon';
const conversionChain = [
convertPhraseFilter,
convertRangeFilter,
convertExistsFilter,
convertGeoBoundingBox,
convertGeoPolygon,
];
export function filterToKueryAST(filter) {
const { negate } = filter.meta;
const node = conversionChain.reduce((acc, converter) => {
if (acc !== null) return acc;
try {
return converter(filter);
}
catch (ex) {
return null;
}
}, null);
if (!node) {
throw new Error(`Couldn't convert that filter to a kuery`);
}
return negate ? nodeTypes.function.buildNode('not', node) : node;
}

View file

@ -0,0 +1,12 @@
import _ from 'lodash';
import { nodeTypes } from '../node_types';
export function convertGeoBoundingBox(filter) {
if (filter.meta.type !== 'geo_bounding_box') {
throw new Error(`Expected filter of type "geo_bounding_box", got "${filter.meta.type}"`);
}
const { key, params } = filter.meta;
const camelParams = _.mapKeys(params, (value, key) => _.camelCase(key));
return nodeTypes.function.buildNode('geoBoundingBox', key, camelParams);
}

View file

@ -0,0 +1,10 @@
import { nodeTypes } from '../node_types';
export function convertGeoPolygon(filter) {
if (filter.meta.type !== 'geo_polygon') {
throw new Error(`Expected filter of type "geo_polygon", got "${filter.meta.type}"`);
}
const { key, params: { points } } = filter.meta;
return nodeTypes.function.buildNode('geoPolygon', key, points);
}

View file

@ -0,0 +1 @@
export { filterToKueryAST } from './filter_to_kuery';

View file

@ -0,0 +1,10 @@
import { nodeTypes } from '../node_types';
export function convertPhraseFilter(filter) {
if (filter.meta.type !== 'phrase') {
throw new Error(`Expected filter of type "phrase", got "${filter.meta.type}"`);
}
const { key, params } = filter.meta;
return nodeTypes.function.buildNode('is', key, params.query);
}

View file

@ -0,0 +1,10 @@
import { nodeTypes } from '../node_types';
export function convertRangeFilter(filter) {
if (filter.meta.type !== 'range') {
throw new Error(`Expected filter of type "range", got "${filter.meta.type}"`);
}
const { key, params } = filter.meta;
return nodeTypes.function.buildNode('range', key, params);
}

View file

@ -0,0 +1,97 @@
import expect from 'expect.js';
import * as and from '../and';
import { nodeTypes } from '../../node_types';
import * as ast from '../../ast';
import StubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import ngMock from 'ng_mock';
let indexPattern;
const childNode1 = nodeTypes.function.buildNode('is', 'response', 200);
const childNode2 = nodeTypes.function.buildNode('is', 'extension', 'jpg');
describe('kuery functions', function () {
describe('and', function () {
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
indexPattern = Private(StubbedLogstashIndexPatternProvider);
}));
describe('buildNodeParams', function () {
it('should return "arguments" and "serializeStyle" params', function () {
const result = and.buildNodeParams([childNode1, childNode2]);
expect(result).to.only.have.keys('arguments', 'serializeStyle');
});
it('arguments should contain the unmodified child nodes', function () {
const result = and.buildNodeParams([childNode1, childNode2]);
const { arguments: [ actualChildNode1, actualChildNode2 ] } = result;
expect(actualChildNode1).to.be(childNode1);
expect(actualChildNode2).to.be(childNode2);
});
it('serializeStyle should default to "operator"', function () {
const { serializeStyle } = and.buildNodeParams([childNode1, childNode2]);
expect(serializeStyle).to.be('operator');
});
});
describe('toElasticsearchQuery', function () {
it('should wrap subqueries in an ES bool query\'s filter clause', function () {
const node = nodeTypes.function.buildNode('and', [childNode1, childNode2]);
const result = and.toElasticsearchQuery(node, indexPattern);
expect(result).to.only.have.keys('bool');
expect(result.bool).to.only.have.keys('filter');
expect(result.bool.filter).to.eql(
[childNode1, childNode2].map((childNode) => ast.toElasticsearchQuery(childNode, indexPattern))
);
});
it('should wrap a literal argument with an "is" function targeting all fields', function () {
const literalFoo = nodeTypes.literal.buildNode('foo');
const node = nodeTypes.function.buildNode('and', [literalFoo]);
const result = and.toElasticsearchQuery(node, indexPattern);
const resultChild = result.bool.filter[0];
expect(resultChild).to.have.property('simple_query_string');
expect(resultChild.simple_query_string.all_fields).to.be(true);
});
});
describe('toKueryExpression', function () {
it('should serialize "and" nodes with an implicit syntax when requested', function () {
const node = nodeTypes.function.buildNode('and', [childNode1, childNode2], 'implicit');
const result = and.toKueryExpression(node);
expect(result).to.be('"response":200 "extension":"jpg"');
});
it('should serialize "and" nodes with an operator syntax when requested', function () {
const node = nodeTypes.function.buildNode('and', [childNode1, childNode2], 'operator');
const result = and.toKueryExpression(node);
expect(result).to.be('"response":200 and "extension":"jpg"');
});
it('should wrap "or" sub-queries in parenthesis', function () {
const orNode = nodeTypes.function.buildNode('or', [childNode1, childNode2], 'operator');
const fooBarNode = nodeTypes.function.buildNode('is', 'foo', 'bar');
const andNode = nodeTypes.function.buildNode('and', [orNode, fooBarNode], 'implicit');
const result = and.toKueryExpression(andNode);
expect(result).to.be('("response":200 or "extension":"jpg") "foo":"bar"');
});
it('should throw an error for nodes with unknown or undefined serialize styles', function () {
const node = nodeTypes.function.buildNode('and', [childNode1, childNode2], 'notValid');
expect(and.toKueryExpression)
.withArgs(node).to.throwException(/Cannot serialize "and" function as "notValid"/);
});
});
});
});

View file

@ -0,0 +1,54 @@
import expect from 'expect.js';
import * as exists from '../exists';
import { nodeTypes } from '../../node_types';
import _ from 'lodash';
import StubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import ngMock from 'ng_mock';
let indexPattern;
describe('kuery functions', function () {
describe('exists', function () {
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
indexPattern = Private(StubbedLogstashIndexPatternProvider);
}));
describe('buildNodeParams', function () {
it('should return a single "arguments" param', function () {
const result = exists.buildNodeParams('response');
expect(result).to.only.have.key('arguments');
});
it('arguments should contain the provided fieldName as a literal', function () {
const { arguments: [ arg ] } = exists.buildNodeParams('response');
expect(arg).to.have.property('type', 'literal');
expect(arg).to.have.property('value', 'response');
});
});
describe('toElasticsearchQuery', function () {
it('should return an ES exists query', function () {
const expected = {
exists: { field: 'response' }
};
const existsNode = nodeTypes.function.buildNode('exists', 'response');
const result = exists.toElasticsearchQuery(existsNode, indexPattern);
expect(_.isEqual(expected, result)).to.be(true);
});
it('should throw an error for scripted fields', function () {
const existsNode = nodeTypes.function.buildNode('exists', 'script string');
expect(exists.toElasticsearchQuery)
.withArgs(existsNode, indexPattern).to.throwException(/Exists query does not support scripted fields/);
});
});
});
});

View file

@ -0,0 +1,83 @@
import expect from 'expect.js';
import * as geoBoundingBox from '../geo_bounding_box';
import { nodeTypes } from '../../node_types';
import StubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import ngMock from 'ng_mock';
let indexPattern;
const params = {
bottomRight: {
lat: 50.73,
lon: -135.35
},
topLeft: {
lat: 73.12,
lon: -174.37
}
};
describe('kuery functions', function () {
describe('geoBoundingBox', function () {
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
indexPattern = Private(StubbedLogstashIndexPatternProvider);
}));
describe('buildNodeParams', function () {
it('should return an "arguments" param', function () {
const result = geoBoundingBox.buildNodeParams('geo', params);
expect(result).to.only.have.keys('arguments');
});
it('arguments should contain the provided fieldName as a literal', function () {
const result = geoBoundingBox.buildNodeParams('geo', params);
const { arguments: [ fieldName ] } = result;
expect(fieldName).to.have.property('type', 'literal');
expect(fieldName).to.have.property('value', 'geo');
});
it('arguments should contain the provided params as named arguments with "lat, lon" string values', function () {
const result = geoBoundingBox.buildNodeParams('geo', params);
const { arguments: [ , ...args ] } = result;
args.map((param) => {
expect(param).to.have.property('type', 'namedArg');
expect(['bottomRight', 'topLeft'].includes(param.name)).to.be(true);
expect(param.value.type).to.be('literal');
const expectedParam = params[param.name];
const expectedLatLon = `${expectedParam.lat}, ${expectedParam.lon}`;
expect(param.value.value).to.be(expectedLatLon);
});
});
});
describe('toElasticsearchQuery', function () {
it('should return an ES geo_bounding_box query representing the given node', function () {
const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params);
const result = geoBoundingBox.toElasticsearchQuery(node, indexPattern);
expect(result).to.have.property('geo_bounding_box');
expect(result.geo_bounding_box.geo).to.have.property('top_left', '73.12, -174.37');
expect(result.geo_bounding_box.geo).to.have.property('bottom_right', '50.73, -135.35');
});
it('should use the ignore_unmapped parameter', function () {
const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params);
const result = geoBoundingBox.toElasticsearchQuery(node, indexPattern);
expect(result.geo_bounding_box.ignore_unmapped).to.be(true);
});
it('should throw an error for scripted fields', function () {
const node = nodeTypes.function.buildNode('geoBoundingBox', 'script number', params);
expect(geoBoundingBox.toElasticsearchQuery)
.withArgs(node, indexPattern).to.throwException(/Geo bounding box query does not support scripted fields/);
});
});
});
});

View file

@ -0,0 +1,89 @@
import expect from 'expect.js';
import * as geoPolygon from '../geo_polygon';
import { nodeTypes } from '../../node_types';
import StubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import ngMock from 'ng_mock';
let indexPattern;
const points = [
{
lat: 69.77,
lon: -171.56
},
{
lat: 50.06,
lon: -169.10
},
{
lat: 69.16,
lon: -125.85
}
];
describe('kuery functions', function () {
describe('geoPolygon', function () {
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
indexPattern = Private(StubbedLogstashIndexPatternProvider);
}));
describe('buildNodeParams', function () {
it('should return an "arguments" param', function () {
const result = geoPolygon.buildNodeParams('geo', points);
expect(result).to.only.have.keys('arguments');
});
it('arguments should contain the provided fieldName as a literal', function () {
const result = geoPolygon.buildNodeParams('geo', points);
const { arguments: [ fieldName ] } = result;
expect(fieldName).to.have.property('type', 'literal');
expect(fieldName).to.have.property('value', 'geo');
});
it('arguments should contain the provided points literal "lat, lon" string values', function () {
const result = geoPolygon.buildNodeParams('geo', points);
const { arguments: [ , ...args ] } = result;
args.forEach((param, index) => {
expect(param).to.have.property('type', 'literal');
const expectedPoint = points[index];
const expectedLatLon = `${expectedPoint.lat}, ${expectedPoint.lon}`;
expect(param.value).to.be(expectedLatLon);
});
});
});
describe('toElasticsearchQuery', function () {
it('should return an ES geo_polygon query representing the given node', function () {
const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points);
const result = geoPolygon.toElasticsearchQuery(node, indexPattern);
expect(result).to.have.property('geo_polygon');
expect(result.geo_polygon.geo).to.have.property('points');
result.geo_polygon.geo.points.forEach((point, index) => {
const expectedLatLon = `${points[index].lat}, ${points[index].lon}`;
expect(point).to.be(expectedLatLon);
});
});
it('should use the ignore_unmapped parameter', function () {
const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points);
const result = geoPolygon.toElasticsearchQuery(node, indexPattern);
expect(result.geo_polygon.ignore_unmapped).to.be(true);
});
it('should throw an error for scripted fields', function () {
const node = nodeTypes.function.buildNode('geoPolygon', 'script number', points);
expect(geoPolygon.toElasticsearchQuery)
.withArgs(node, indexPattern).to.throwException(/Geo polygon query does not support scripted fields/);
});
});
});
});

View file

@ -0,0 +1,133 @@
import expect from 'expect.js';
import * as is from '../is';
import { nodeTypes } from '../../node_types';
import _ from 'lodash';
import StubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import ngMock from 'ng_mock';
let indexPattern;
describe('kuery functions', function () {
describe('is', function () {
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
indexPattern = Private(StubbedLogstashIndexPatternProvider);
}));
describe('buildNodeParams', function () {
it('fieldName and value should be required arguments', function () {
expect(is.buildNodeParams).to.throwException(/fieldName is a required argument/);
expect(is.buildNodeParams).withArgs('foo').to.throwException(/value is a required argument/);
});
it('should return "arguments" and "serializeStyle" params', function () {
const result = is.buildNodeParams('response', 200);
expect(result).to.only.have.keys('arguments', 'serializeStyle');
});
it('arguments should contain the provided fieldName and value as literals', function () {
const { arguments: [ fieldName, value ] } = is.buildNodeParams('response', 200);
expect(fieldName).to.have.property('type', 'literal');
expect(fieldName).to.have.property('value', 'response');
expect(value).to.have.property('type', 'literal');
expect(value).to.have.property('value', 200);
});
it('serializeStyle should default to "operator"', function () {
const { serializeStyle } = is.buildNodeParams('response', 200);
expect(serializeStyle).to.be('operator');
});
});
describe('toElasticsearchQuery', function () {
it('should return an ES match_all query when fieldName and value are both "*"', function () {
const expected = {
match_all: {}
};
const node = nodeTypes.function.buildNode('is', '*', '*');
const result = is.toElasticsearchQuery(node, indexPattern);
expect(_.isEqual(expected, result)).to.be(true);
});
it('should return an ES simple_query_string query in all fields mode when fieldName is "*"', function () {
const expected = {
simple_query_string: {
query: '"200"',
all_fields: true,
}
};
const node = nodeTypes.function.buildNode('is', '*', 200);
const result = is.toElasticsearchQuery(node, indexPattern);
expect(_.isEqual(expected, result)).to.be(true);
});
// See discussion about kuery escaping for background:
// https://github.com/elastic/kibana/pull/12624#issuecomment-312650307
it('should ensure the simple_query_string query is wrapped in double quotes to force a phrase search', function () {
const node = nodeTypes.function.buildNode('is', '*', '+response');
const result = is.toElasticsearchQuery(node, indexPattern);
expect(result.simple_query_string.query).to.be('"+response"');
});
it('already double quoted phrases should not get wrapped a second time', function () {
const node = nodeTypes.function.buildNode('is', '*', '"+response"');
const result = is.toElasticsearchQuery(node, indexPattern);
expect(result.simple_query_string.query).to.be('"+response"');
});
it('should return an ES exists query when value is "*"', function () {
const expected = {
exists: { field: 'response' }
};
const node = nodeTypes.function.buildNode('is', 'response', '*');
const result = is.toElasticsearchQuery(node, indexPattern);
expect(_.isEqual(expected, result)).to.be(true);
});
it('should return an ES match_phrase query when a concrete fieldName and value are provided', function () {
const expected = {
match_phrase: {
response: 200
}
};
const node = nodeTypes.function.buildNode('is', 'response', 200);
const result = is.toElasticsearchQuery(node, indexPattern);
expect(_.isEqual(expected, result)).to.be(true);
});
it('should support scripted fields', function () {
const node = nodeTypes.function.buildNode('is', 'script string', 'foo');
const result = is.toElasticsearchQuery(node, indexPattern);
expect(result).to.have.key('script');
});
});
describe('toKueryExpression', function () {
it('should serialize "is" nodes with an operator syntax', function () {
const node = nodeTypes.function.buildNode('is', 'response', 200, 'operator');
const result = is.toKueryExpression(node);
expect(result).to.be('"response":200');
});
it('should throw an error for nodes with unknown or undefined serialize styles', function () {
const node = nodeTypes.function.buildNode('is', 'response', 200, 'notValid');
expect(is.toKueryExpression)
.withArgs(node).to.throwException(/Cannot serialize "is" function as "notValid"/);
});
});
});
});

View file

@ -0,0 +1,97 @@
import expect from 'expect.js';
import * as not from '../not';
import { nodeTypes } from '../../node_types';
import * as ast from '../../ast';
import StubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import ngMock from 'ng_mock';
let indexPattern;
const childNode = nodeTypes.function.buildNode('is', 'response', 200);
describe('kuery functions', function () {
describe('not', function () {
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
indexPattern = Private(StubbedLogstashIndexPatternProvider);
}));
describe('buildNodeParams', function () {
it('should return "arguments" and "serializeStyle" params', function () {
const result = not.buildNodeParams(childNode);
expect(result).to.only.have.keys('arguments', 'serializeStyle');
});
it('arguments should contain the unmodified child node', function () {
const { arguments: [ actualChild ] } = not.buildNodeParams(childNode);
expect(actualChild).to.be(childNode);
});
it('serializeStyle should default to "operator"', function () {
const { serializeStyle } = not.buildNodeParams(childNode);
expect(serializeStyle).to.be('operator');
});
});
describe('toElasticsearchQuery', function () {
it('should wrap a subquery in an ES bool query\'s must_not clause', function () {
const node = nodeTypes.function.buildNode('not', childNode);
const result = not.toElasticsearchQuery(node, indexPattern);
expect(result).to.only.have.keys('bool');
expect(result.bool).to.only.have.keys('must_not');
expect(result.bool.must_not).to.eql(ast.toElasticsearchQuery(childNode, indexPattern));
});
it('should wrap a literal argument with an "is" function targeting all fields', function () {
const literalFoo = nodeTypes.literal.buildNode('foo');
const node = nodeTypes.function.buildNode('not', literalFoo);
const result = not.toElasticsearchQuery(node, indexPattern);
const resultChild = result.bool.must_not;
expect(resultChild).to.have.property('simple_query_string');
expect(resultChild.simple_query_string.all_fields).to.be(true);
});
});
describe('toKueryExpression', function () {
it('should serialize "not" nodes with an operator syntax', function () {
const node = nodeTypes.function.buildNode('not', childNode, 'operator');
const result = not.toKueryExpression(node);
expect(result).to.be('!"response":200');
});
it('should wrap "and" and "or" sub-queries in parenthesis', function () {
const andNode = nodeTypes.function.buildNode('and', [childNode, childNode], 'operator');
const notAndNode = nodeTypes.function.buildNode('not', andNode, 'operator');
expect(not.toKueryExpression(notAndNode)).to.be('!("response":200 and "response":200)');
const orNode = nodeTypes.function.buildNode('or', [childNode, childNode], 'operator');
const notOrNode = nodeTypes.function.buildNode('not', orNode, 'operator');
expect(not.toKueryExpression(notOrNode)).to.be('!("response":200 or "response":200)');
});
it('should not wrap "and" and "or" sub-queries that use the function syntax', function () {
const andNode = nodeTypes.function.buildNode('and', [childNode, childNode], 'function');
const notAndNode = nodeTypes.function.buildNode('not', andNode, 'operator');
expect(not.toKueryExpression(notAndNode)).to.be('!and("response":200, "response":200)');
const orNode = nodeTypes.function.buildNode('or', [childNode, childNode], 'function');
const notOrNode = nodeTypes.function.buildNode('not', orNode, 'operator');
expect(not.toKueryExpression(notOrNode)).to.be('!or("response":200, "response":200)');
});
it('should throw an error for nodes with unknown or undefined serialize styles', function () {
const node = nodeTypes.function.buildNode('not', childNode, 'notValid');
expect(not.toKueryExpression)
.withArgs(node).to.throwException(/Cannot serialize "not" function as "notValid"/);
});
});
});
});

View file

@ -0,0 +1,88 @@
import expect from 'expect.js';
import * as or from '../or';
import { nodeTypes } from '../../node_types';
import * as ast from '../../ast';
import StubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import ngMock from 'ng_mock';
let indexPattern;
const childNode1 = nodeTypes.function.buildNode('is', 'response', 200);
const childNode2 = nodeTypes.function.buildNode('is', 'extension', 'jpg');
describe('kuery functions', function () {
describe('or', function () {
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
indexPattern = Private(StubbedLogstashIndexPatternProvider);
}));
describe('buildNodeParams', function () {
it('should return "arguments" and "serializeStyle" params', function () {
const result = or.buildNodeParams([childNode1, childNode2]);
expect(result).to.only.have.keys('arguments', 'serializeStyle');
});
it('arguments should contain the unmodified child nodes', function () {
const result = or.buildNodeParams([childNode1, childNode2]);
const { arguments: [ actualChildNode1, actualChildNode2 ] } = result;
expect(actualChildNode1).to.be(childNode1);
expect(actualChildNode2).to.be(childNode2);
});
it('serializeStyle should default to "operator"', function () {
const { serializeStyle } = or.buildNodeParams([childNode1, childNode2]);
expect(serializeStyle).to.be('operator');
});
});
describe('toElasticsearchQuery', function () {
it('should wrap subqueries in an ES bool query\'s should clause', function () {
const node = nodeTypes.function.buildNode('or', [childNode1, childNode2]);
const result = or.toElasticsearchQuery(node, indexPattern);
expect(result).to.only.have.keys('bool');
expect(result.bool).to.have.keys('should');
expect(result.bool.should).to.eql(
[childNode1, childNode2].map((childNode) => ast.toElasticsearchQuery(childNode, indexPattern))
);
});
it('should wrap a literal argument with an "is" function targeting all fields', function () {
const literalFoo = nodeTypes.literal.buildNode('foo');
const node = nodeTypes.function.buildNode('or', [literalFoo]);
const result = or.toElasticsearchQuery(node, indexPattern);
const resultChild = result.bool.should[0];
expect(resultChild).to.have.property('simple_query_string');
expect(resultChild.simple_query_string.all_fields).to.be(true);
});
it('should require one of the clauses to match', function () {
const node = nodeTypes.function.buildNode('or', [childNode1, childNode2]);
const result = or.toElasticsearchQuery(node, indexPattern);
expect(result.bool).to.have.property('minimum_should_match', 1);
});
});
describe('toKueryExpression', function () {
it('should serialize "or" nodes with an operator syntax', function () {
const node = nodeTypes.function.buildNode('or', [childNode1, childNode2]);
const result = or.toKueryExpression(node);
expect(result).to.be('"response":200 or "extension":"jpg"');
});
it('should throw an error for nodes with unknown or undefined serialize styles', function () {
const node = nodeTypes.function.buildNode('or', [childNode1, childNode2], 'notValid');
expect(or.toKueryExpression)
.withArgs(node).to.throwException(/Cannot serialize "or" function as "notValid"/);
});
});
});
});

View file

@ -0,0 +1,112 @@
import expect from 'expect.js';
import * as range from '../range';
import { nodeTypes } from '../../node_types';
import _ from 'lodash';
import StubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import ngMock from 'ng_mock';
let indexPattern;
describe('kuery functions', function () {
describe('range', function () {
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
indexPattern = Private(StubbedLogstashIndexPatternProvider);
}));
describe('buildNodeParams', function () {
it('should return "arguments" and "serializeStyle" params', function () {
const result = range.buildNodeParams('bytes', { gt: 1000, lt: 8000 });
expect(result).to.only.have.keys('arguments', 'serializeStyle');
});
it('arguments should contain the provided fieldName as a literal', function () {
const result = range.buildNodeParams('bytes', { gt: 1000, lt: 8000 });
const { arguments: [ fieldName ] } = result;
expect(fieldName).to.have.property('type', 'literal');
expect(fieldName).to.have.property('value', 'bytes');
});
it('arguments should contain the provided params as named arguments', function () {
const givenParams = { gt: 1000, lt: 8000, format: 'epoch_millis' };
const result = range.buildNodeParams('bytes', givenParams);
const { arguments: [ , ...params ] } = result;
expect(params).to.be.an('array');
expect(params).to.not.be.empty();
params.map((param) => {
expect(param).to.have.property('type', 'namedArg');
expect(['gt', 'lt', 'format'].includes(param.name)).to.be(true);
expect(param.value.type).to.be('literal');
expect(param.value.value).to.be(givenParams[param.name]);
});
});
it('serializeStyle should default to "operator"', function () {
const result = range.buildNodeParams('bytes', { gte: 1000, lte: 8000 });
const { serializeStyle } = result;
expect(serializeStyle).to.be('operator');
});
it('serializeStyle should be "function" if either end of the range is exclusive', function () {
const result = range.buildNodeParams('bytes', { gt: 1000, lt: 8000 });
const { serializeStyle } = result;
expect(serializeStyle).to.be('function');
});
});
describe('toElasticsearchQuery', function () {
it('should return an ES range query for the node\'s field and params', function () {
const expected = {
range: {
bytes: {
gt: 1000,
lt: 8000
}
}
};
const node = nodeTypes.function.buildNode('range', 'bytes', { gt: 1000, lt: 8000 });
const result = range.toElasticsearchQuery(node, indexPattern);
expect(_.isEqual(expected, result)).to.be(true);
});
it('should support scripted fields', function () {
const node = nodeTypes.function.buildNode('range', 'script number', { gt: 1000, lt: 8000 });
const result = range.toElasticsearchQuery(node, indexPattern);
expect(result).to.have.key('script');
});
});
describe('toKueryExpression', function () {
it('should serialize "range" nodes with an operator syntax', function () {
const node = nodeTypes.function.buildNode('range', 'bytes', { gte: 1000, lte: 8000 }, 'operator');
const result = range.toKueryExpression(node);
expect(result).to.be('"bytes":[1000 to 8000]');
});
it('should throw an error for nodes with unknown or undefined serialize styles', function () {
const node = nodeTypes.function.buildNode('range', 'bytes', { gte: 1000, lte: 8000 }, 'notValid');
expect(range.toKueryExpression)
.withArgs(node).to.throwException(/Cannot serialize "range" function as "notValid"/);
});
it('should not support exclusive ranges in the operator syntax', function () {
const node = nodeTypes.function.buildNode('range', 'bytes', { gt: 1000, lt: 8000 });
node.serializeStyle = 'operator';
expect(range.toKueryExpression)
.withArgs(node).to.throwException(/Operator syntax only supports inclusive ranges/);
});
});
});
});

View file

@ -0,0 +1,46 @@
import * as ast from '../ast';
import { nodeTypes } from '../node_types';
export function buildNodeParams(children, serializeStyle = 'operator') {
return {
arguments: children,
serializeStyle
};
}
export function toElasticsearchQuery(node, indexPattern) {
const children = node.arguments || [];
return {
bool: {
filter: children.map((child) => {
if (child.type === 'literal') {
child = nodeTypes.function.buildNode('is', '*', child.value);
}
return ast.toElasticsearchQuery(child, indexPattern);
})
}
};
}
export function toKueryExpression(node) {
if (!['operator', 'implicit'].includes(node.serializeStyle)) {
throw new Error(`Cannot serialize "and" function as "${node.serializeStyle}"`);
}
const queryStrings = (node.arguments || []).map((arg) => {
const query = ast.toKueryExpression(arg);
if (arg.type === 'function' && arg.function === 'or') {
return `(${query})`;
}
return query;
});
if (node.serializeStyle === 'implicit') {
return queryStrings.join(' ');
}
if (node.serializeStyle === 'operator') {
return queryStrings.join(' and ');
}
}

View file

@ -0,0 +1,20 @@
import * as literal from '../node_types/literal';
export function buildNodeParams(fieldName) {
return {
arguments: [literal.buildNode(fieldName)],
};
}
export function toElasticsearchQuery(node, indexPattern) {
const { arguments: [ fieldNameArg ] } = node;
const fieldName = literal.toElasticsearchQuery(fieldNameArg);
const field = indexPattern.fields.byName[fieldName];
if (field && field.scripted) {
throw new Error(`Exists query does not support scripted fields`);
}
return {
exists: { field: fieldName }
};
}

View file

@ -0,0 +1,41 @@
import _ from 'lodash';
import { nodeTypes } from '../node_types';
import * as ast from '../ast';
export function buildNodeParams(fieldName, params) {
params = _.pick(params, 'topLeft', 'bottomRight');
const fieldNameArg = nodeTypes.literal.buildNode(fieldName);
const args = _.map(params, (value, key) => {
const latLon = `${value.lat}, ${value.lon}`;
return nodeTypes.namedArg.buildNode(key, latLon);
});
return {
arguments: [fieldNameArg, ...args],
};
}
export function toElasticsearchQuery(node, indexPattern) {
const [ fieldNameArg, ...args ] = node.arguments;
const fieldName = nodeTypes.literal.toElasticsearchQuery(fieldNameArg);
const field = indexPattern.fields.byName[fieldName];
const queryParams = args.reduce((acc, arg) => {
const snakeArgName = _.snakeCase(arg.name);
return {
...acc,
[snakeArgName]: ast.toElasticsearchQuery(arg),
};
}, {});
if (field && field.scripted) {
throw new Error(`Geo bounding box query does not support scripted fields`);
}
return {
geo_bounding_box: {
[fieldName]: queryParams,
ignore_unmapped: true,
},
};
}

View file

@ -0,0 +1,34 @@
import { nodeTypes } from '../node_types';
import * as ast from '../ast';
export function buildNodeParams(fieldName, points) {
const fieldNameArg = nodeTypes.literal.buildNode(fieldName);
const args = points.map((point) => {
const latLon = `${point.lat}, ${point.lon}`;
return nodeTypes.literal.buildNode(latLon);
});
return {
arguments: [fieldNameArg, ...args],
};
}
export function toElasticsearchQuery(node, indexPattern) {
const [ fieldNameArg, ...points ] = node.arguments;
const fieldName = nodeTypes.literal.toElasticsearchQuery(fieldNameArg);
const field = indexPattern.fields.byName[fieldName];
const queryParams = {
points: points.map(ast.toElasticsearchQuery)
};
if (field && field.scripted) {
throw new Error(`Geo polygon query does not support scripted fields`);
}
return {
geo_polygon: {
[fieldName]: queryParams,
ignore_unmapped: true,
},
};
}

View file

@ -0,0 +1,19 @@
import * as is from './is';
import * as and from './and';
import * as or from './or';
import * as not from './not';
import * as range from './range';
import * as exists from './exists';
import * as geoBoundingBox from './geo_bounding_box';
import * as geoPolygon from './geo_polygon';
export const functions = {
is,
and,
or,
not,
range,
exists,
geoBoundingBox,
geoPolygon,
};

View file

@ -0,0 +1,75 @@
import _ from 'lodash';
import * as literal from '../node_types/literal';
import { getPhraseScript } from 'ui/filter_manager/lib/phrase';
export function buildNodeParams(fieldName, value, serializeStyle = 'operator') {
if (_.isUndefined(fieldName)) {
throw new Error('fieldName is a required argument');
}
if (_.isUndefined(value)) {
throw new Error('value is a required argument');
}
return {
arguments: [literal.buildNode(fieldName), literal.buildNode(value)],
serializeStyle
};
}
export function toElasticsearchQuery(node, indexPattern) {
const { arguments: [ fieldNameArg, valueArg ] } = node;
const fieldName = literal.toElasticsearchQuery(fieldNameArg);
const field = indexPattern.fields.byName[fieldName];
const value = !_.isUndefined(valueArg) ? literal.toElasticsearchQuery(valueArg) : valueArg;
if (field && field.scripted) {
return {
script: {
...getPhraseScript(field, value)
}
};
}
else if (fieldName === '*' && value === '*') {
return { match_all: {} };
}
else if (fieldName === '*' && value !== '*') {
const userQuery = String(value);
const query = isDoubleQuoted(userQuery) ? userQuery : `"${userQuery}"`;
return {
simple_query_string: {
query,
all_fields: true
}
};
}
else if (fieldName !== '*' && value === '*') {
return {
exists: { field: fieldName }
};
}
else {
return {
match_phrase: {
[fieldName]: value
}
};
}
}
export function toKueryExpression(node) {
if (node.serializeStyle !== 'operator') {
throw new Error(`Cannot serialize "is" function as "${node.serializeStyle}"`);
}
const { arguments: [ fieldNameArg, valueArg ] } = node;
const fieldName = literal.toKueryExpression(fieldNameArg);
const value = !_.isUndefined(valueArg) ? literal.toKueryExpression(valueArg) : valueArg;
return `${fieldName}:${value}`;
}
function isDoubleQuoted(str) {
return str.startsWith('"') && str.endsWith('"');
}

View file

@ -0,0 +1,42 @@
import * as ast from '../ast';
import { nodeTypes } from '../node_types';
export function buildNodeParams(child, serializeStyle = 'operator') {
return {
arguments: [child],
serializeStyle
};
}
export function toElasticsearchQuery(node, indexPattern) {
let [ argument ] = node.arguments;
if (argument.type === 'literal') {
argument = nodeTypes.function.buildNode('is', '*', argument.value);
}
return {
bool: {
must_not: ast.toElasticsearchQuery(argument, indexPattern)
}
};
}
export function toKueryExpression(node) {
if (node.serializeStyle !== 'operator') {
throw new Error(`Cannot serialize "not" function as "${node.serializeStyle}"`);
}
const [ argument ] = node.arguments;
const queryString = ast.toKueryExpression(argument);
if (
argument.function &&
(argument.function === 'and' || argument.function === 'or') &&
argument.serializeStyle !== 'function'
) {
return `!(${queryString})`;
}
else {
return `!${queryString}`;
}
}

View file

@ -0,0 +1,39 @@
import * as ast from '../ast';
import { nodeTypes } from '../node_types';
export function buildNodeParams(children, serializeStyle = 'operator') {
return {
arguments: children,
serializeStyle,
};
}
export function toElasticsearchQuery(node, indexPattern) {
const children = node.arguments || [];
return {
bool: {
should: children.map((child) => {
if (child.type === 'literal') {
child = nodeTypes.function.buildNode('is', '*', child.value);
}
return ast.toElasticsearchQuery(child, indexPattern);
}),
minimum_should_match: 1,
},
};
}
export function toKueryExpression(node) {
if (node.serializeStyle !== 'operator') {
throw new Error(`Cannot serialize "or" function as "${node.serializeStyle}"`);
}
const queryStrings = (node.arguments || []).map((arg) => {
return ast.toKueryExpression(arg);
});
return queryStrings.join(' or ');
}

View file

@ -0,0 +1,78 @@
import _ from 'lodash';
import { nodeTypes } from '../node_types';
import * as ast from '../ast';
import { getRangeScript } from 'ui/filter_manager/lib/range';
export function buildNodeParams(fieldName, params, serializeStyle = 'operator') {
params = _.pick(params, 'gt', 'lt', 'gte', 'lte', 'format');
const fieldNameArg = nodeTypes.literal.buildNode(fieldName);
const args = _.map(params, (value, key) => {
return nodeTypes.namedArg.buildNode(key, value);
});
// we only support inclusive ranges in the operator syntax currently
if (_.has(params, 'gt') || _.has(params, 'lt')) {
serializeStyle = 'function';
}
return {
arguments: [fieldNameArg, ...args],
serializeStyle,
};
}
export function toElasticsearchQuery(node, indexPattern) {
const [ fieldNameArg, ...args ] = node.arguments;
const fieldName = nodeTypes.literal.toElasticsearchQuery(fieldNameArg);
const field = indexPattern.fields.byName[fieldName];
const namedArgs = extractArguments(args);
const queryParams = _.mapValues(namedArgs, ast.toElasticsearchQuery);
if (field && field.scripted) {
return {
script: {
...getRangeScript(field, queryParams)
}
};
}
return {
range: {
[fieldName]: queryParams
}
};
}
export function toKueryExpression(node) {
if (node.serializeStyle !== 'operator') {
throw new Error(`Cannot serialize "range" function as "${node.serializeStyle}"`);
}
const [ fieldNameArg, ...args ] = node.arguments;
const fieldName = ast.toKueryExpression(fieldNameArg);
const { gte, lte } = extractArguments(args);
if (_.isUndefined(gte) || _.isUndefined(lte)) {
throw new Error(`Operator syntax only supports inclusive ranges`);
}
return `${fieldName}:[${ast.toKueryExpression(gte)} to ${ast.toKueryExpression(lte)}]`;
}
function extractArguments(args) {
if ((args.gt && args.gte) || (args.lt && args.lte)) {
throw new Error('range ends cannot be both inclusive and exclusive');
}
const unnamedArgOrder = ['gte', 'lte', 'format'];
return args.reduce((acc, arg, index) => {
if (arg.type === 'namedArg') {
acc[arg.name] = arg.value;
}
else {
acc[unnamedArgOrder[index]] = arg;
}
return acc;
}, {});
}

View file

@ -0,0 +1,3 @@
export * from './ast';
export * from './filter_migration';
export * from './node_types';

View file

@ -0,0 +1,89 @@
import * as functionType from '../function';
import _ from 'lodash';
import expect from 'expect.js';
import { expectDeepEqual } from '../../../../../test_utils/expect_deep_equal.js';
import * as isFunction from '../../functions/is';
import StubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import ngMock from 'ng_mock';
import { nodeTypes } from '../../node_types';
describe('kuery node types', function () {
describe('function', function () {
let indexPattern;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
indexPattern = Private(StubbedLogstashIndexPatternProvider);
}));
describe('buildNode', function () {
it('should return a node representing the given kuery function', function () {
const result = functionType.buildNode('is', 'response', 200);
expect(result).to.have.property('type', 'function');
expect(result).to.have.property('function', 'is');
expect(result).to.have.property('arguments');
});
});
describe('buildNodeWithArgumentNodes', function () {
it('should return a function node with the given argument list untouched', function () {
const fieldNameLiteral = nodeTypes.literal.buildNode('response');
const valueLiteral = nodeTypes.literal.buildNode(200);
const argumentNodes = [fieldNameLiteral, valueLiteral];
const result = functionType.buildNodeWithArgumentNodes('is', argumentNodes);
expect(result).to.have.property('type', 'function');
expect(result).to.have.property('function', 'is');
expect(result).to.have.property('arguments');
expect(result.arguments).to.be(argumentNodes);
expectDeepEqual(result.arguments, argumentNodes);
});
});
describe('toElasticsearchQuery', function () {
it('should return the given function type\'s ES query representation', function () {
const node = functionType.buildNode('is', 'response', 200);
const expected = isFunction.toElasticsearchQuery(node, indexPattern);
const result = functionType.toElasticsearchQuery(node, indexPattern);
expect(_.isEqual(expected, result)).to.be(true);
});
});
describe('toKueryExpression', function () {
it('should return the function syntax representation of the given node by default', function () {
const node = functionType.buildNode('exists', 'foo');
expect(functionType.toKueryExpression(node)).to.be('exists("foo")');
});
it('should return the function syntax representation of the given node if serializeStyle is "function"', function () {
const node = functionType.buildNode('exists', 'foo');
node.serializeStyle = 'function';
expect(functionType.toKueryExpression(node)).to.be('exists("foo")');
});
it('should defer to the function\'s serializer if another serializeStyle is specified', function () {
const node = functionType.buildNode('is', 'response', 200);
expect(node.serializeStyle).to.be('operator');
expect(functionType.toKueryExpression(node)).to.be('"response":200');
});
it('should simply return the node\'s "text" property if one exists', function () {
const node = functionType.buildNode('exists', 'foo');
node.text = 'bar';
expect(functionType.toKueryExpression(node)).to.be('bar');
});
});
});
});

View file

@ -0,0 +1,44 @@
import expect from 'expect.js';
import * as literal from '../literal';
describe('kuery node types', function () {
describe('literal', function () {
describe('buildNode', function () {
it('should return a node representing the given value', function () {
const result = literal.buildNode('foo');
expect(result).to.have.property('type', 'literal');
expect(result).to.have.property('value', 'foo');
});
});
describe('toElasticsearchQuery', function () {
it('should return the literal value represented by the given node', function () {
const node = literal.buildNode('foo');
const result = literal.toElasticsearchQuery(node);
expect(result).to.be('foo');
});
});
describe('toKueryExpression', function () {
it('should return the literal value represented by the given node', function () {
const numberNode = literal.buildNode(200);
expect(literal.toKueryExpression(numberNode)).to.be(200);
});
it('should wrap string values in double quotes', function () {
const stringNode = literal.buildNode('foo');
expect(literal.toKueryExpression(stringNode)).to.be('"foo"');
});
});
});
});

View file

@ -0,0 +1,53 @@
import expect from 'expect.js';
import { expectDeepEqual } from '../../../../../test_utils/expect_deep_equal.js';
import * as namedArg from '../named_arg';
import { nodeTypes } from '../../node_types';
describe('kuery node types', function () {
describe('named arg', function () {
describe('buildNode', function () {
it('should return a node representing a named argument with the given value', function () {
const result = namedArg.buildNode('fieldName', 'foo');
expect(result).to.have.property('type', 'namedArg');
expect(result).to.have.property('name', 'fieldName');
expect(result).to.have.property('value');
const literalValue = result.value;
expect(literalValue).to.have.property('type', 'literal');
expect(literalValue).to.have.property('value', 'foo');
});
it('should support literal nodes as values', function () {
const value = nodeTypes.literal.buildNode('foo');
const result = namedArg.buildNode('fieldName', value);
expect(result.value).to.be(value);
expectDeepEqual(result.value, value);
});
});
describe('toElasticsearchQuery', function () {
it('should return the argument value represented by the given node', function () {
const node = namedArg.buildNode('fieldName', 'foo');
const result = namedArg.toElasticsearchQuery(node);
expect(result).to.be('foo');
});
});
describe('toKueryExpression', function () {
it('should return the argument name and value represented by the given node', function () {
const node = namedArg.buildNode('fieldName', 'foo');
expect(namedArg.toKueryExpression(node)).to.be('fieldName="foo"');
});
});
});
});

View file

@ -0,0 +1,54 @@
import _ from 'lodash';
import { functions } from '../functions';
import { nodeTypes } from '../node_types';
export function buildNode(functionName, ...functionArgs) {
const kueryFunction = functions[functionName];
if (_.isUndefined(kueryFunction)) {
throw new Error(`Unknown function "${functionName}"`);
}
return {
type: 'function',
function: functionName,
...kueryFunction.buildNodeParams(...functionArgs)
};
}
// Mainly only useful in the grammar where we'll already have real argument nodes in hand
export function buildNodeWithArgumentNodes(functionName, argumentNodes, serializeStyle = 'function') {
if (_.isUndefined(functions[functionName])) {
throw new Error(`Unknown function "${functionName}"`);
}
return {
type: 'function',
function: functionName,
arguments: argumentNodes,
serializeStyle
};
}
export function toElasticsearchQuery(node, indexPattern) {
const kueryFunction = functions[node.function];
return kueryFunction.toElasticsearchQuery(node, indexPattern);
}
export function toKueryExpression(node) {
const kueryFunction = functions[node.function];
if (!_.isUndefined(node.text)) {
return node.text;
}
if (node.serializeStyle && node.serializeStyle !== 'function') {
return kueryFunction.toKueryExpression(node);
}
const functionArguments = (node.arguments || []).map((argument) => {
return nodeTypes[argument.type].toKueryExpression(argument);
});
return `${node.function}(${functionArguments.join(', ')})`;
}

View file

@ -0,0 +1,10 @@
import * as functionType from './function';
import * as literal from './literal';
import * as namedArg from './named_arg';
export const nodeTypes = {
function: functionType,
literal,
namedArg,
};

View file

@ -0,0 +1,21 @@
import _ from 'lodash';
export function buildNode(value) {
return {
type: 'literal',
value,
};
}
export function toElasticsearchQuery(node) {
return node.value;
}
export function toKueryExpression(node) {
if (_.isString(node.value)) {
const escapedValue = node.value.replace(/"/g, '\\"');
return `"${escapedValue}"`;
}
return node.value;
}

View file

@ -0,0 +1,20 @@
import _ from 'lodash';
import * as ast from '../ast';
import { nodeTypes } from '../node_types';
export function buildNode(name, value) {
const argumentNode = (_.get(value, 'type') === 'literal') ? value : nodeTypes.literal.buildNode(value);
return {
type: 'namedArg',
name,
value: argumentNode,
};
}
export function toElasticsearchQuery(node) {
return ast.toElasticsearchQuery(node.value);
}
export function toKueryExpression(node) {
return `${node.name}=${ast.toKueryExpression(node.value)}`;
}

View file

@ -1,42 +1,35 @@
import _ from 'lodash';
import { DecorateQueryProvider } from 'ui/courier/data_source/_decorate_query';
export function ParseQueryLibFromUserProvider(es, Private) {
const decorateQuery = Private(DecorateQueryProvider);
export function ParseQueryLibFromUserProvider() {
/**
* Take text from the user and make it into a query object
* @param {text} user's query input
* Take userInput from the user and make it into a query object
* @param {userInput} user's query input
* @returns {object}
*/
return function (text) {
function getQueryStringQuery(text) {
return decorateQuery({ query_string: { query: text } });
}
return function (userInput) {
const matchAll = '';
const matchAll = getQueryStringQuery('*');
// If we get an empty object, treat it as a *
if (_.isObject(text)) {
if (Object.keys(text).length) {
return text;
} else {
if (_.isObject(userInput)) {
// If we get an empty object, treat it as a *
if (!Object.keys(userInput).length) {
return matchAll;
}
return userInput;
}
// Nope, not an object.
text = (text || '').trim();
if (text.length === 0) return matchAll;
userInput = (userInput || '').trim();
if (userInput.length === 0) return matchAll;
if (text[0] === '{') {
if (userInput[0] === '{') {
try {
return JSON.parse(text);
return JSON.parse(userInput);
} catch (e) {
return getQueryStringQuery(text);
return userInput;
}
} else {
return getQueryStringQuery(text);
return userInput;
}
};
}

View file

@ -12,8 +12,5 @@ export function toUser(text) {
if (text.query_string) return toUser(text.query_string.query);
return angular.toJson(text);
}
if (text === '*') {
return '';
}
return '' + text;
}

View file

@ -0,0 +1,154 @@
import angular from 'angular';
import sinon from 'sinon';
import expect from 'expect.js';
import ngMock from 'ng_mock';
import { expectDeepEqual } from '../../../../../test_utils/expect_deep_equal.js';
let $parentScope;
let $elem;
const markup = `<query-bar query="query" app-name="name" on-submit="submitHandler($query)"></query-bar>`;
function init(query, name, isSwitchingEnabled = true) {
ngMock.module('kibana');
ngMock.module('kibana', function ($provide) {
$provide.service('config', function () {
this.get = sinon.stub().withArgs('search:queryLanguage:switcher:enable').returns(isSwitchingEnabled);
});
});
ngMock.inject(function ($injector, $controller, $rootScope, $compile) {
$parentScope = $rootScope;
$parentScope.submitHandler = sinon.stub();
$parentScope.name = name;
$parentScope.query = query;
$elem = angular.element(markup);
$compile($elem)($parentScope);
$elem.scope().$digest();
});
}
describe('queryBar directive', function () {
describe('language selector', function () {
it('should display a language selector if switching is enabled', function () {
init({ query: 'foo', language: 'lucene' }, 'discover', true);
const selectElement = $elem.find('.kuiLocalSearchSelect');
expect(selectElement.length).to.be(1);
});
it('should not display a language selector if switching is disabled', function () {
init({ query: 'foo', language: 'lucene' }, 'discover', false);
const selectElement = $elem.find('.kuiLocalSearchSelect');
expect(selectElement.length).to.be(0);
});
it('should reflect the language of the query in the selector', function () {
init({ query: 'foo', language: 'lucene' }, 'discover', true);
let selectedOption = $elem.find('.kuiLocalSearchSelect :selected');
let displayLang = selectedOption.text();
expect(displayLang).to.be('lucene');
$parentScope.query = { query: 'foo', language: 'kuery' };
$parentScope.$digest();
selectedOption = $elem.find('.kuiLocalSearchSelect :selected');
displayLang = selectedOption.text();
expect(displayLang).to.be('kuery');
});
it('should call the onSubmit callback when a new language is selected', function () {
init({ query: 'foo', language: 'lucene' }, 'discover', true);
const kueryOption = $elem.find('.kuiLocalSearchSelect option[label="kuery"]');
kueryOption.prop('selected', true).trigger('change');
expect($parentScope.submitHandler.calledOnce).to.be(true);
});
it('should reset the query string provided to the callback when a new language is selected', function () {
init({ query: 'foo', language: 'lucene' }, 'discover', true);
const kueryOption = $elem.find('.kuiLocalSearchSelect option[label="kuery"]');
kueryOption.prop('selected', true).trigger('change');
expectDeepEqual($parentScope.submitHandler.getCall(0).args[0], { query: '', language: 'kuery' });
});
it('should not modify the parent scope\'s query when a new language is selected', function () {
init({ query: 'foo', language: 'lucene' }, 'discover', true);
const kueryOption = $elem.find('.kuiLocalSearchSelect option[label="kuery"]');
kueryOption.prop('selected', true).trigger('change');
expectDeepEqual($parentScope.query, { query: 'foo', language: 'lucene' });
});
});
describe('query string input', function () {
it('should reflect the query passed into the directive', function () {
init({ query: 'foo', language: 'lucene' }, 'discover', true);
const queryInput = $elem.find('.kuiLocalSearchInput');
expect(queryInput.val()).to.be('foo');
});
it('changes to the input text should not modify the parent scope\'s query', function () {
init({ query: 'foo', language: 'lucene' }, 'discover', true);
const queryInput = $elem.find('.kuiLocalSearchInput');
queryInput.val('bar').trigger('input');
expect($elem.isolateScope().queryBar.localQuery.query).to.be('bar');
expect($parentScope.query.query).to.be('foo');
});
it('should not call onSubmit until the form is submitted', function () {
init({ query: 'foo', language: 'lucene' }, 'discover', true);
const queryInput = $elem.find('.kuiLocalSearchInput');
queryInput.val('bar').trigger('input');
expect($parentScope.submitHandler.notCalled).to.be(true);
const submitButton = $elem.find('.kuiLocalSearchButton');
submitButton.click();
expect($parentScope.submitHandler.called).to.be(true);
});
it('should call onSubmit with the current input text when the form is submitted', function () {
init({ query: 'foo', language: 'lucene' }, 'discover', true);
const queryInput = $elem.find('.kuiLocalSearchInput');
queryInput.val('bar').trigger('input');
const submitButton = $elem.find('.kuiLocalSearchButton');
submitButton.click();
expectDeepEqual($parentScope.submitHandler.getCall(0).args[0], { query: 'bar', language: 'lucene' });
});
it('should customize the input element for each language', function () {
init({ query: 'foo', language: 'lucene' }, 'discover', true);
const luceneInput = $elem.find('.kuiLocalSearchInput');
expect(luceneInput.attr('placeholder')).to.be('Search... (e.g. status:200 AND extension:PHP)');
const helpLink = $elem.find('.kuiLocalSearchAssistedInput__assistance .kuiLink');
expect(helpLink.text().trim()).to.be('Uses lucene query syntax');
$parentScope.query = { query: 'foo', language: 'kuery' };
$parentScope.$digest();
const kueryInput = $elem.find('.kuiLocalSearchInput');
expect(kueryInput.attr('placeholder')).to.be('Search with kuery...');
});
});
describe('typeahead key', function () {
it('should use a unique typeahead key for each appName/language combo', function () {
init({ query: 'foo', language: 'lucene' }, 'discover', true);
const typeahead = $elem.find('.typeahead');
expect(typeahead.isolateScope().historyKey).to.be('discover-lucene');
$parentScope.query = { query: 'foo', language: 'kuery' };
$parentScope.$digest();
expect(typeahead.isolateScope().historyKey).to.be('discover-kuery');
});
});
});

View file

@ -0,0 +1,73 @@
<form
role="form"
name="queryBarForm"
ng-submit="queryBar.submit()"
>
<div class="typeahead" kbn-typeahead="{{queryBar.typeaheadKey()}}" on-select="queryBar.submit()" role="search">
<div class="kuiLocalSearch">
<!-- Lucene input -->
<div class="kuiLocalSearchAssistedInput" ng-if="queryBar.localQuery.language === 'lucene'">
<input
parse-query
input-focus
kbn-typeahead-input
ng-model="queryBar.localQuery.query"
placeholder="Search... (e.g. status:200 AND extension:PHP)"
aria-label="Search input"
aria-describedby="discover-lucene-syntax-hint"
type="text"
class="kuiLocalSearchInput kuiLocalSearchInput--lucene"
ng-class="{'kuiLocalSearchInput-isInvalid': queryBarForm.$invalid}"
data-test-subj="queryInput"
>
<div class="kuiLocalSearchAssistedInput__assistance">
<p class="kuiText">
<a
id="discover-lucene-syntax-hint"
class="kuiLink"
ng-href="{{queryBar.queryDocLinks.luceneQuerySyntax}}"
target="_blank"
>
Uses lucene query syntax
</a>
</p>
</div>
</div>
<!-- kuery input -->
<input
ng-if="queryBar.localQuery.language === 'kuery'"
ng-model="queryBar.localQuery.query"
input-focus
kbn-typeahead-input
placeholder="Search with kuery..."
aria-label="Search input"
type="text"
class="kuiLocalSearchInput"
ng-class="{'kuiLocalSearchInput-isInvalid': queryBarForm.$invalid}"
data-test-subj="queryInput"
>
<select
class="kuiLocalSearchSelect"
ng-options="option for option in queryBar.availableQueryLanguages"
ng-model="queryBar.localQuery.language"
ng-change="queryBar.selectLanguage()"
ng-if="queryBar.showLanguageSwitcher"
>
</select>
<button
type="submit"
aria-label="Search"
class="kuiLocalSearchButton"
ng-disabled="queryBarForm.$invalid"
data-test-subj="querySubmitButton"
>
<span class="fa fa-search" aria-hidden="true"></span>
</button>
</div>
<kbn-typeahead-items></kbn-typeahead-items>
</div>
</form>

View file

@ -0,0 +1,42 @@
import { uiModules } from 'ui/modules';
import template from './query_bar.html';
import { queryLanguages } from '../lib/queryLanguages';
import { documentationLinks } from '../../documentation_links/documentation_links.js';
const module = uiModules.get('kibana');
module.directive('queryBar', function () {
return {
restrict: 'E',
template: template,
scope: {
query: '=',
appName: '=?',
onSubmit: '&',
},
controllerAs: 'queryBar',
bindToController: true,
controller: function ($scope, config) {
this.queryDocLinks = documentationLinks.query;
this.appName = this.appName || 'global';
this.availableQueryLanguages = queryLanguages;
this.showLanguageSwitcher = config.get('search:queryLanguage:switcher:enable');
this.typeaheadKey = () => `${this.appName}-${this.query.language}`;
this.submit = () => {
this.onSubmit({ $query: this.localQuery });
};
this.selectLanguage = () => {
this.localQuery.query = '';
this.submit();
};
$scope.$watch('queryBar.query', (newQuery) => {
this.localQuery = Object.assign({}, newQuery);
}, true);
}
};
});

View file

@ -0,0 +1 @@
import './directive/query_bar';

View file

@ -0,0 +1,4 @@
export const queryLanguages = [
'lucene',
'kuery',
];

View file

@ -0,0 +1,165 @@
import { QueryManagerProvider } from '../query_manager';
import { FilterManagerProvider } from 'ui/filter_manager';
import StubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import NoDigestPromises from 'test_utils/no_digest_promises';
import expect from 'expect.js';
import ngMock from 'ng_mock';
import sinon from 'sinon';
import moment from 'moment';
describe('QueryManager', function () {
NoDigestPromises.activateForSuite();
let queryManager;
let filterManager;
let indexPattern;
let timefilter;
beforeEach(ngMock.module(
'kibana',
'kibana/courier',
function ($provide) {
$provide.service('courier', require('fixtures/mock_courier'));
}
));
beforeEach(ngMock.inject(function (Private, _timefilter_) {
timefilter = _timefilter_;
indexPattern = Private(StubbedLogstashIndexPatternProvider);
queryManager = Private(QueryManagerProvider);
filterManager = Private(FilterManagerProvider);
sinon.stub(filterManager, 'add');
}));
describe('add', function () {
it('should defer to the FilterManager when dealing with a lucene query', function () {
const state = {
query: { query: 'foo', language: 'lucene' }
};
const args = ['foo', ['bar'], '+', indexPattern];
queryManager = queryManager(state);
queryManager.add(...args);
expect(filterManager.add.calledOnce).to.be(true);
expect(filterManager.add.calledWith(...args)).to.be(true);
});
it('should add an operator style "is" function to kuery queries' , function () {
const state = {
query: { query: '', language: 'kuery' }
};
queryManager = queryManager(state);
queryManager.add('foo', 'bar', '+', indexPattern);
expect(state.query.query).to.be('"foo":"bar"');
});
it('should combine the new clause with any existing query clauses using an implicit "and"' , function () {
const state = {
query: { query: 'foo', language: 'kuery' }
};
queryManager = queryManager(state);
queryManager.add('foo', 'bar', '+', indexPattern);
expect(state.query.query).to.be('foo "foo":"bar"');
});
it('should support creation of negated clauses' , function () {
const state = {
query: { query: 'foo', language: 'kuery' }
};
queryManager = queryManager(state);
queryManager.add('foo', 'bar', '-', indexPattern);
expect(state.query.query).to.be('foo !"foo":"bar"');
});
it('should add an exists query when the provided field name is "_exists_"' , function () {
const state = {
query: { query: 'foo', language: 'kuery' }
};
queryManager = queryManager(state);
queryManager.add('_exists_', 'baz', '+', indexPattern);
expect(state.query.query).to.be('foo exists("baz")');
});
});
describe('addLegacyFilter', function () {
const filter = {
meta: {
index: 'logstash-*',
type: 'phrase',
key: 'machine.os',
params: {
query: 'osx'
},
},
query: {
match: {
'machine.os': {
query: 'osx',
type: 'phrase'
}
}
}
};
it('should return a Promise', function () {
const state = {
query: { query: '', language: 'lucene' }
};
queryManager = queryManager(state);
expect(queryManager.addLegacyFilter(filter)).to.be.a(Promise);
});
// The filter bar directive will handle new filters when lucene is selected
it('should do nothing if the query language is not "kuery"', function () {
const state = {
query: { query: '', language: 'lucene' }
};
queryManager = queryManager(state);
return queryManager.addLegacyFilter(filter)
.then(() => {
expect(state.query.query).to.be('');
});
});
it('should add a query clause equivalent to the given filter', function () {
const state = {
query: { query: '', language: 'kuery' }
};
queryManager = queryManager(state);
return queryManager.addLegacyFilter(filter)
.then(() => {
expect(state.query.query).to.be('"machine.os":"osx"');
});
});
it('time field filters should update the global time filter instead of modifying the query', function () {
const startTime = moment('1995');
const endTime = moment('1996');
const state = {
query: { query: '', language: 'kuery' }
};
const timestampFilter = {
meta: {
index: 'logstash-*',
},
range: {
time: {
gt: startTime.valueOf(),
lt: endTime.valueOf(),
}
}
};
queryManager = queryManager(state);
return queryManager.addLegacyFilter(timestampFilter)
.then(() => {
expect(state.query.query).to.be('');
expect(startTime.isSame(timefilter.time.from)).to.be(true);
expect(endTime.isSame(timefilter.time.to)).to.be(true);
});
});
});
});

View file

@ -0,0 +1 @@
export { QueryManagerProvider } from './query_manager';

View file

@ -0,0 +1,79 @@
import _ from 'lodash';
import { FilterManagerProvider } from 'ui/filter_manager';
import { FilterBarLibMapAndFlattenFiltersProvider } from 'ui/filter_bar/lib/map_and_flatten_filters';
import { FilterBarLibExtractTimeFilterProvider } from 'ui/filter_bar/lib/extract_time_filter';
import { FilterBarLibChangeTimeFilterProvider } from 'ui/filter_bar/lib/change_time_filter';
import { toKueryExpression, fromKueryExpression, nodeTypes, filterToKueryAST } from 'ui/kuery';
export function QueryManagerProvider(Private) {
const filterManager = Private(FilterManagerProvider);
const mapAndFlattenFilters = Private(FilterBarLibMapAndFlattenFiltersProvider);
const extractTimeFilter = Private(FilterBarLibExtractTimeFilterProvider);
const changeTimeFilter = Private(FilterBarLibChangeTimeFilterProvider);
return function (state) {
function add(field, values = [], operation, index) {
const fieldName = _.isObject(field) ? field.name : field;
if (!Array.isArray(values)) {
values = [values];
}
if (state.query.language === 'lucene') {
filterManager.add(field, values, operation, index);
}
if (state.query.language === 'kuery') {
const negate = operation === '-';
const isExistsQuery = fieldName === '_exists_';
const newQueries = values.map((value) => {
const newQuery = isExistsQuery
? nodeTypes.function.buildNode('exists', value)
: nodeTypes.function.buildNode('is', fieldName, value);
return negate ? nodeTypes.function.buildNode('not', newQuery) : newQuery;
});
const allQueries = _.isEmpty(state.query.query)
? newQueries
: [fromKueryExpression(state.query.query), ...newQueries];
state.query = {
query: toKueryExpression(nodeTypes.function.buildNode('and', allQueries, 'implicit')),
language: 'kuery'
};
}
}
async function addLegacyFilter(filter) {
// The filter_bar directive currently handles filter creation when lucene is the selected language,
// so we only handle updating the kuery AST here.
if (state.query.language === 'kuery') {
const timeFilter = await extractTimeFilter([filter]);
if (timeFilter) {
changeTimeFilter(timeFilter);
}
else {
const [ mappedFilter ] = await mapAndFlattenFilters([filter]);
const newQuery = filterToKueryAST(mappedFilter);
const allQueries = _.isEmpty(state.query.query)
? [newQuery]
: [fromKueryExpression(state.query.query), newQuery];
state.query = {
query: toKueryExpression(nodeTypes.function.buildNode('and', allQueries, 'implicit')),
language: 'kuery'
};
}
}
}
return {
add,
addLegacyFilter,
};
};
}

View file

@ -61,24 +61,6 @@ const init = function () {
describe('typeahead directive', function () {
describe('typeahead requirements', function () {
describe('missing input', function () {
const goodMarkup = markup;
before(function () {
markup = `<div class="typeahead" kbn-typeahead="${typeaheadName}" on-select="selectItem()">
<kbn-typeahead-items></kbn-typeahead-items>
</div>`;
});
after(function () {
markup = goodMarkup;
});
it('should throw with message', function () {
expect(init).to.throwException(/kbn-typeahead-input must be defined/);
});
});
describe('missing on-select attribute', function () {
const goodMarkup = markup;

View file

@ -23,15 +23,6 @@ typeahead.directive('kbnTypeahead', function () {
self.focused = false;
self.mousedOver = false;
// instantiate history and add items to the scope
self.history = new PersistedLog('typeahead:' + $scope.historyKey, {
maxLength: config.get('history:limit'),
filterDuplicates: true
});
$scope.items = self.history.get();
$scope.filteredItems = [];
self.setInputModel = function (model) {
$scope.inputModel = model;
@ -194,6 +185,16 @@ typeahead.directive('kbnTypeahead', function () {
return !self.hidden && ($scope.filteredItems.length > 0) && (self.focused || self.mousedOver);
};
$scope.$watch('historyKey', () => {
self.history = new PersistedLog('typeahead:' + $scope.historyKey, {
maxLength: config.get('history:limit'),
filterDuplicates: true
});
$scope.items = self.history.get();
$scope.filteredItems = [];
});
// handle updates to parent scope history
$scope.$watch('items', function () {
if (self.query) {
@ -215,10 +216,6 @@ typeahead.directive('kbnTypeahead', function () {
if (!_.has(attrs, 'onSelect')) {
throw new Error('on-select must be defined');
}
// should be defined via setInput() method
if (!$scope.inputModel) {
throw new Error('kbn-typeahead-input must be defined');
}
$scope.$watch('typeahead.isVisible()', function (vis) {
$el.toggleClass('visible', vis);

View file

@ -0,0 +1,17 @@
import { has } from 'lodash';
/**
* Creates a standardized query object from old queries that were either strings or pure ES query DSL
*
* @param query - a legacy query, what used to be stored in SearchSource's query property
* @return Object
*/
export function migrateLegacyQuery(query) {
// Lucene was the only option before, so language-less queries are all lucene
if (!has(query, 'language')) {
return { query: query, language: 'lucene' };
}
return query;
}

View file

@ -138,7 +138,7 @@ export default function ({ getService, getPageObjects }) {
const currentQuery = await PageObjects.dashboard.getQuery();
expect(currentQuery).to.equal('');
const currentUrl = await remote.getCurrentUrl();
const newUrl = currentUrl.replace('query:%27*%27', 'query:%27hi%27');
const newUrl = currentUrl.replace('query:%27%27', 'query:%27hi%27');
// Don't add the timestamp to the url or it will cause a hard refresh and we want to test a
// soft refresh.
await remote.get(newUrl.toString(), false);

Some files were not shown because too many files have changed in this diff Show more