added source filtering

This commit is contained in:
Stéphane Campinas 2016-01-12 10:08:35 +00:00
parent 420f1fa092
commit e7204bef63
11 changed files with 317 additions and 11 deletions

View file

@ -0,0 +1,60 @@
var expect = require('expect.js');
define(function (require) {
var isRetrieved = require('src/plugins/kibana/public/settings/sections/indices/retrieved_field');
describe('Settings', function () {
describe('Indices', function () {
describe('isRetrieved(sourceFiltering, name)', function () {
it('should be a function', function () {
expect(isRetrieved).to.be.a(Function);
});
it('should retrieve john', function () {
var sourceFiltering = {
include: 'john'
};
expect(isRetrieved(sourceFiltering, 'john')).to.be(true);
});
it('should not retrieve connor', function () {
var sourceFiltering = {
exclude: 'connor'
};
expect(isRetrieved(sourceFiltering, 'connor')).to.be(false);
});
it('should retrieve connor', function () {
var sourceFiltering = {
exclude: '*.connor'
};
expect(isRetrieved(sourceFiltering, 'connor')).to.be(true);
expect(isRetrieved(sourceFiltering, 'john.connor')).to.be(false);
});
it('should not retrieve neither john nor connor', function () {
var sourceFiltering = {
exclude: [ 'john', 'connor' ]
};
expect(isRetrieved(sourceFiltering, 'connor')).to.be(false);
expect(isRetrieved(sourceFiltering, 'john')).to.be(false);
expect(isRetrieved(sourceFiltering, 'toto')).to.be(true);
});
it('should not retrieve john.*.connor', function () {
var sourceFiltering = {
exclude: 'john.*.connor'
};
expect(isRetrieved(sourceFiltering, 'john.j.connor')).to.be(false);
expect(isRetrieved(sourceFiltering, 'john.t.connor')).to.be(false);
expect(isRetrieved(sourceFiltering, 'john.j.watterson')).to.be(true);
});
});
});
});
});

View file

@ -41,13 +41,14 @@
<li class="kbn-settings-tab" ng-class="{ active: state.tab === fieldType.index }" ng-repeat="fieldType in fieldTypes">
<a ng-click="changeTab(fieldType)">
{{ fieldType.title }}
<small>({{ fieldType.count }})</small>
<small ng-if="fieldType.count !== undefined">({{ fieldType.count }})</small>
</a>
</li>
</ul>
<indexed-fields ng-show="state.tab == 'indexedFields'" class="fields indexed-fields"></indexed-fields>
<scripted-fields ng-show="state.tab == 'scriptedFields'" class="fields scripted-fields"></scripted-fields>
<source-filtering ng-show="state.tab == 'sourceFiltering'" class="fields source-filtering"></source-filtering>
</div>
</kbn-settings-indices>

View file

@ -1,6 +1,7 @@
import _ from 'lodash';
import 'plugins/kibana/settings/sections/indices/_indexed_fields';
import 'plugins/kibana/settings/sections/indices/_scripted_fields';
import 'plugins/kibana/settings/sections/indices/_source_filtering';
import 'plugins/kibana/settings/sections/indices/_index_header';
import PluginsKibanaSettingsSectionsIndicesRefreshKibanaIndexProvider from 'plugins/kibana/settings/sections/indices/_refresh_kibana_index';
import UrlProvider from 'ui/url';

View file

@ -11,14 +11,21 @@ export default function GetFieldTypes() {
scripted: 0
});
return [{
title: 'fields',
index: 'indexedFields',
count: fieldCount.indexed
}, {
title: 'scripted fields',
index: 'scriptedFields',
count: fieldCount.scripted
}];
return [
{
title: 'fields',
index: 'indexedFields',
count: fieldCount.indexed
},
{
title: 'scripted fields',
index: 'scriptedFields',
count: fieldCount.scripted
},
{
title: 'Retrieved Fields',
index: 'sourceFiltering'
}
];
};
};

View file

@ -1,4 +1,5 @@
import _ from 'lodash';
import isRetrieved from 'plugins/kibana/settings/sections/indices/retrieved_field';
import 'ui/paginated_table';
import nameHtml from 'plugins/kibana/settings/sections/indices/_field_name.html';
import typeHtml from 'plugins/kibana/settings/sections/indices/_field_type.html';
@ -25,6 +26,7 @@ uiModules.get('apps/settings')
{ title: 'format' },
{ title: 'analyzed', info: 'Analyzed fields may require extra memory to visualize' },
{ title: 'indexed', info: 'Fields that are not indexed are unavailable for search' },
{ title: 'retrieved', info: 'Fields that are not retrieved as part of the _source object per hit' },
{ title: 'controls', sortable: false }
];
@ -34,6 +36,7 @@ uiModules.get('apps/settings')
// clear and destroy row scopes
_.invoke(rowScopes.splice(0), '$destroy');
const sourceFiltering = $scope.indexPattern.getSourceFiltering();
const fields = filter($scope.indexPattern.getNonScriptedFields(), $scope.fieldFilter);
_.find($scope.fieldTypes, {index: 'indexedFields'}).count = fields.length; // Update the tab count
@ -41,6 +44,7 @@ uiModules.get('apps/settings')
const childScope = _.assign($scope.$new(), { field: field });
rowScopes.push(childScope);
<<<<<<< HEAD
return [
{
markup: nameHtml,
@ -61,6 +65,9 @@ uiModules.get('apps/settings')
markup: field.indexed ? yesTemplate : noTemplate,
value: field.indexed
},
{
markup: isRetrieved(sourceFiltering, field.displayName) ? yesTemplate : noTemplate
},
{
markup: controlsHtml,
scope: childScope

View file

@ -0,0 +1,114 @@
<h3>Retrieved fields
<span class="pull-right text-info hintbox-label" ng-click="showHelp = !showHelp">
<h4><i class="fa fa-info"></i> Retrieved Fields Help</h4>
</span>
</h3>
<div class="hintbox" ng-if="showHelp">
<h4 class="hintbox-heading">
<i class="fa fa-question-circle text-info"></i> Retrieved Fields Help
</h4>
<p>
All fields are by default retrieved and are available inside the "_source" object of each hit. Thanks to the
<a target="_window" href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-source-filtering.html">
source filtering API
<i aria-hidden="true" class="fa-link fa"></i>
</a>
of Elasticsearch, you can choose which fields are actually retrieved. The value needs to be a JSON object.
</p>
<br/>
<strong>Examples</strong>
<p style="color: #b4bcc2;">Exclusion of a single field</p>
<div
readonly
ui-ace="{
advanced: {
highlightActiveLine: false
},
useWrapMode: true,
rendererOptions: {
showPrintMargin: false,
maxLines: 4294967296
},
mode: 'json'
}">{
"exclude": "user"
}</div>
<p style="color: #b4bcc2;">Exclusion with a path pattern</p>
<div
readonly
ui-ace="{
advanced: {
highlightActiveLine: false
},
useWrapMode: true,
rendererOptions: {
showPrintMargin: false,
maxLines: 4294967296
},
mode: 'json'
}">{
"exclude": [
"obj1.*",
"*.value"
]
}</div>
<p style="color: #b4bcc2;">Complete control with exclusions and inclusion</p>
<div
readonly
ui-ace="{
advanced: {
highlightActiveLine: false
},
useWrapMode: true,
rendererOptions: {
showPrintMargin: false,
maxLines: 4294967296
},
mode: 'json'
}">{
"exclude": "obj1.*",
"include": "obj2.*.val"
}</div>
</div>
<p>
By default, all fields are retrieved to populate results table.
Sometimes however some fields can be very large and seriously affect performance. This can be often the case when you have materialized one to many relationships, e.g., an entity having many nested entities. Those fields are still useful for searching or analytics but not in a result table.<br/><br/>
Use this setting to decide what to include or exclude in the data retrieved from the Elasticsearch index.<br/>
If empty, all fields are retrieved.
</p>
<form ng-submit="save()" name="form">
<div
id="json-ace"
ng-model="sourceFiltering"
ui-ace="{
useWrapMode: true,
advanced: {
highlightActiveLine: false
},
rendererOptions: {
showPrintMargin: false,
maxLines: 4294967296
},
mode: 'json'
}">{{ sourceFiltering | json }}</div>
<div class="form-group">
<button
ng-disabled="form.$invalid"
type="submit"
aria-label="Submit"
class="btn btn-success">
Submit
</button>
</div>
</form>

View file

@ -0,0 +1,44 @@
define(function (require) {
require('ui/modules').get('apps/settings')
.directive('sourceFiltering', function (Notifier, $window) {
var notify = new Notifier();
return {
restrict: 'E',
template: require('plugins/kibana/settings/sections/indices/_source_filtering.html'),
link: function ($scope) {
$scope.showHelp = false;
$scope.sourceFiltering = JSON.stringify($scope.indexPattern.getSourceFiltering(), null, ' ');
$scope.save = function () {
try {
var sourceFiltering;
if ($scope.sourceFiltering) {
sourceFiltering = JSON.parse($scope.sourceFiltering);
if (sourceFiltering.constructor !== Object) {
throw 'You must enter a JSON object with exclude/include field(s)';
}
for (var att in sourceFiltering) {
if (sourceFiltering.hasOwnProperty(att) && att !== 'exclude' && att !== 'include') {
throw 'The JSON object should have only either an exclude or an include field';
}
}
$scope.indexPattern.setSourceFiltering(sourceFiltering);
notify.info('Updated the set of retrieved fields');
} else if ($scope.indexPattern.getSourceFiltering()) {
var confirmIfEmpty = 'The following configuration will be deleted:\n\n' +
JSON.stringify($scope.indexPattern.getSourceFiltering(), null, ' ');
if ($window.confirm(confirmIfEmpty)) {
$scope.indexPattern.setSourceFiltering(undefined);
notify.info('All fields are now retrieved');
} else {
$scope.sourceFiltering = JSON.stringify($scope.indexPattern.getSourceFiltering(), null, ' ');
}
}
} catch (e) {
notify.error(e);
}
};
}
};
});
});

View file

@ -0,0 +1,50 @@
define(function (require) {
/**
* Returns true if the path matches the given pattern
*/
function matchPath(pathPattern, path) {
var pattern = pathPattern.split('.');
var pathArr = path.split('.');
if (pattern.length !== pathArr.length) {
return false;
}
for (var i = 0; i < pattern.length; i++) {
if (pattern[i] !== '*' && pattern[i] !== pathArr[i]) {
return false;
}
}
return true;
}
function process(val, name) {
if (val.constructor === Array) {
for (var i = 0; i < val.length; i++) {
if (matchPath(val[i], name)) {
return true;
}
}
return false;
} else {
return matchPath(val, name);
}
}
/**
* Returns true if the field named "name" should be retrieved as part of
* the _source object for each hit.
*/
return function isRetrieved(sourceFiltering, name) {
if (sourceFiltering === undefined) {
return true;
}
if (sourceFiltering.include) {
var inc = sourceFiltering.include;
return process(inc, name);
} else if (sourceFiltering.exclude) {
var exc = sourceFiltering.exclude;
return !process(exc, name);
}
return false;
};
});

View file

@ -72,6 +72,10 @@ describe('docTable', function () {
expect($elem.text()).to.not.be.empty();
});
it('should set the source filtering defintion', function () {
expect($scope.indexPattern.getSourceFiltering.called).to.be(true);
});
it('should set the indexPattern to that of the searchSource', function () {
expect($scope.indexPattern).to.be(searchSource.get('index'));
});

View file

@ -80,6 +80,10 @@ uiModules.get('kibana')
$scope.searchSource.size(config.get('discover:sampleSize'));
$scope.searchSource.sort(getSort($scope.sorting, $scope.indexPattern));
const sourceFiltering = $scope.indexPattern.getSourceFiltering();
if (sourceFiltering) {
$scope.searchSource.source(sourceFiltering);
}
// Set the watcher after initialization
$scope.$watchCollection('sorting', function (newSort, oldSort) {

View file

@ -1,3 +1,4 @@
<<<<<<< HEAD
import _ from 'lodash';
import errors from 'ui/errors';
import angular from 'angular';
@ -35,6 +36,7 @@ export default function IndexPatternFactory(Private, timefilter, Notifier, confi
timeFieldName: 'string',
notExpandable: 'boolean',
intervalName: 'string',
sourceFiltering: 'json',
fields: 'json',
fieldFormatMap: {
type: 'string',
@ -119,6 +121,17 @@ export default function IndexPatternFactory(Private, timefilter, Notifier, confi
});
};
// Set the source filtering configuration for that index
self.setSourceFiltering = function (config) {
self.sourceFiltering = config;
self.save();
};
// Get the source filtering configuration for that index
self.getSourceFiltering = function () {
return self.sourceFiltering;
};
function initFields(fields) {
self.fields = new FieldList(self, fields || self.fields || []);
}
@ -328,7 +341,8 @@ export default function IndexPatternFactory(Private, timefilter, Notifier, confi
edit: '/settings/indices/{{id}}',
addField: '/settings/indices/{{id}}/create-field',
indexedFields: '/settings/indices/{{id}}?_a=(tab:indexedFields)',
scriptedFields: '/settings/indices/{{id}}?_a=(tab:scriptedFields)'
scriptedFields: '/settings/indices/{{id}}?_a=(tab:scriptedFields)',
sourceFiltering: '/settings/indices/{{id}}?_a=(tab:sourceFiltering)'
};
return IndexPattern;