Remove segmented fetch (#33453)
* Remove segmented fetch * Fix namespace * Remove unused translations
This commit is contained in:
parent
3acebd9d57
commit
a848840501
|
@ -41,9 +41,6 @@ working on big documents. Set this property to `false` to disable highlighting.
|
||||||
`doc_table:hideTimeColumn`:: Hide the 'Time' column in Discover and in all Saved Searches on Dashboards.
|
`doc_table:hideTimeColumn`:: Hide the 'Time' column in Discover and in all Saved Searches on Dashboards.
|
||||||
`search:includeFrozen`:: Will include {ref}/frozen-indices.html[frozen indices] in results if enabled. Searching through frozen indices
|
`search:includeFrozen`:: Will include {ref}/frozen-indices.html[frozen indices] in results if enabled. Searching through frozen indices
|
||||||
might increase the search time.
|
might increase the search time.
|
||||||
`courier:maxSegmentCount`:: Kibana splits requests in the Discover app into segments to limit the size of requests sent to
|
|
||||||
the Elasticsearch cluster. This setting constrains the length of the segment list. Long segment lists can significantly
|
|
||||||
increase request processing time.
|
|
||||||
`courier:ignoreFilterIfFieldNotInIndex`:: Set this property to `true` to skip filters that apply to fields that don't exist in a visualization's index. Useful when dashboards consist of visualizations from multiple index patterns.
|
`courier:ignoreFilterIfFieldNotInIndex`:: Set this property to `true` to skip filters that apply to fields that don't exist in a visualization's index. Useful when dashboards consist of visualizations from multiple index patterns.
|
||||||
`courier:maxConcurrentShardRequests`:: Controls the {ref}/search-multi-search.html[max_concurrent_shard_requests] setting used for _msearch requests sent by Kibana. Set to 0 to disable this config and use the Elasticsearch default.
|
`courier:maxConcurrentShardRequests`:: Controls the {ref}/search-multi-search.html[max_concurrent_shard_requests] setting used for _msearch requests sent by Kibana. Set to 0 to disable this config and use the Elasticsearch default.
|
||||||
`fields:popularLimit`:: This setting governs how many of the top most popular fields are shown.
|
`fields:popularLimit`:: This setting governs how many of the top most popular fields are shown.
|
||||||
|
|
|
@ -1,93 +0,0 @@
|
||||||
/*
|
|
||||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
|
||||||
* license agreements. See the NOTICE file distributed with
|
|
||||||
* this work for additional information regarding copyright
|
|
||||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
|
||||||
* the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
* not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import _ from 'lodash';
|
|
||||||
import ngMock from 'ng_mock';
|
|
||||||
import expect from '@kbn/expect';
|
|
||||||
import PluginsKibanaDiscoverHitSortFnProvider from '../_hit_sort_fn';
|
|
||||||
|
|
||||||
describe('hit sort function', function () {
|
|
||||||
let createHitSortFn;
|
|
||||||
|
|
||||||
beforeEach(ngMock.module('kibana'));
|
|
||||||
beforeEach(ngMock.inject(function (Private) {
|
|
||||||
createHitSortFn = Private(PluginsKibanaDiscoverHitSortFnProvider);
|
|
||||||
}));
|
|
||||||
|
|
||||||
|
|
||||||
const runSortTest = function (dir, sortOpts) {
|
|
||||||
const groupSize = _.random(10, 30);
|
|
||||||
const total = sortOpts.length * groupSize;
|
|
||||||
|
|
||||||
sortOpts = sortOpts.map(function (opt) {
|
|
||||||
if (Array.isArray(opt)) return opt;
|
|
||||||
else return [opt];
|
|
||||||
});
|
|
||||||
const sortOptLength = sortOpts.length;
|
|
||||||
|
|
||||||
const hits = _.times(total, function (i) {
|
|
||||||
return {
|
|
||||||
_source: {},
|
|
||||||
sort: sortOpts[i % sortOptLength]
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
hits.sort(createHitSortFn(dir))
|
|
||||||
.forEach(function (hit, i) {
|
|
||||||
const group = Math.floor(i / groupSize);
|
|
||||||
expect(hit.sort).to.eql(sortOpts[group]);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
it('sorts a list of hits in ascending order', function () {
|
|
||||||
runSortTest('asc', [200, 404, 500]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sorts a list of hits in descending order', function () {
|
|
||||||
runSortTest('desc', [10, 3, 1]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('breaks ties in ascending order', function () {
|
|
||||||
runSortTest('asc', [
|
|
||||||
[ 'apache', 200, 'facebook.com' ],
|
|
||||||
[ 'apache', 200, 'twitter.com' ],
|
|
||||||
[ 'apache', 301, 'facebook.com' ],
|
|
||||||
[ 'apache', 301, 'twitter.com' ],
|
|
||||||
[ 'nginx', 200, 'facebook.com' ],
|
|
||||||
[ 'nginx', 200, 'twitter.com' ],
|
|
||||||
[ 'nginx', 301, 'facebook.com' ],
|
|
||||||
[ 'nginx', 301, 'twitter.com' ]
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('breaks ties in descending order', function () {
|
|
||||||
runSortTest('desc', [
|
|
||||||
[ 'nginx', 301, 'twitter.com' ],
|
|
||||||
[ 'nginx', 301, 'facebook.com' ],
|
|
||||||
[ 'nginx', 200, 'twitter.com' ],
|
|
||||||
[ 'nginx', 200, 'facebook.com' ],
|
|
||||||
[ 'apache', 301, 'twitter.com' ],
|
|
||||||
[ 'apache', 301, 'facebook.com' ],
|
|
||||||
[ 'apache', 200, 'twitter.com' ],
|
|
||||||
[ 'apache', 200, 'facebook.com' ]
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,82 +0,0 @@
|
||||||
/*
|
|
||||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
|
||||||
* license agreements. See the NOTICE file distributed with
|
|
||||||
* this work for additional information regarding copyright
|
|
||||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
|
||||||
* the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
* not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// eslint-disable-next-line @kbn/eslint/no-default-export
|
|
||||||
export default function HitSortFnFactory() {
|
|
||||||
/**
|
|
||||||
* Creates a sort function that will resort hits based on the value
|
|
||||||
* es used to sort them.
|
|
||||||
*
|
|
||||||
* background:
|
|
||||||
* When a hit is sorted by elasticsearch, es will write the values that it used
|
|
||||||
* to sort them into an array at the top level of the hit like so
|
|
||||||
*
|
|
||||||
* ```
|
|
||||||
* hits: {
|
|
||||||
* total: x,
|
|
||||||
* hits: [
|
|
||||||
* {
|
|
||||||
* _id: i,
|
|
||||||
* _source: {},
|
|
||||||
* sort: [
|
|
||||||
* // all values used to sort, in the order of precedence
|
|
||||||
* ]
|
|
||||||
* }
|
|
||||||
* ]
|
|
||||||
* };
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @param {[type]} field [description]
|
|
||||||
* @param {[type]} direction [description]
|
|
||||||
* @return {[type]} [description]
|
|
||||||
*/
|
|
||||||
return function createHitSortFn(direction) {
|
|
||||||
const descending = (direction === 'desc');
|
|
||||||
|
|
||||||
return function sortHits(hitA, hitB) {
|
|
||||||
let bBelowa = null;
|
|
||||||
|
|
||||||
const aSorts = hitA.sort || [];
|
|
||||||
const bSorts = hitB.sort || [];
|
|
||||||
|
|
||||||
// walk each sort value, and compare until one is different
|
|
||||||
for (let i = 0; i < bSorts.length; i++) {
|
|
||||||
const a = aSorts[i];
|
|
||||||
const b = bSorts[i];
|
|
||||||
|
|
||||||
if (a == null || b > a) {
|
|
||||||
bBelowa = !descending;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (b < a) {
|
|
||||||
bBelowa = descending;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bBelowa !== null) {
|
|
||||||
return bBelowa ? -1 : 1;
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -39,7 +39,6 @@ import { toastNotifications } from 'ui/notify';
|
||||||
import { VisProvider } from 'ui/vis';
|
import { VisProvider } from 'ui/vis';
|
||||||
import { VislibSeriesResponseHandlerProvider } from 'ui/vis/response_handlers/vislib';
|
import { VislibSeriesResponseHandlerProvider } from 'ui/vis/response_handlers/vislib';
|
||||||
import { DocTitleProvider } from 'ui/doc_title';
|
import { DocTitleProvider } from 'ui/doc_title';
|
||||||
import PluginsKibanaDiscoverHitSortFnProvider from '../_hit_sort_fn';
|
|
||||||
import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter';
|
import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter';
|
||||||
import { intervalOptions } from 'ui/agg_types/buckets/_interval_options';
|
import { intervalOptions } from 'ui/agg_types/buckets/_interval_options';
|
||||||
import { stateMonitorFactory } from 'ui/state_management/state_monitor_factory';
|
import { stateMonitorFactory } from 'ui/state_management/state_monitor_factory';
|
||||||
|
@ -67,6 +66,12 @@ import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_s
|
||||||
import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../breadcrumbs';
|
import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../breadcrumbs';
|
||||||
import { buildVislibDimensions } from 'ui/visualize/loader/pipeline_helpers/build_pipeline';
|
import { buildVislibDimensions } from 'ui/visualize/loader/pipeline_helpers/build_pipeline';
|
||||||
|
|
||||||
|
const fetchStatuses = {
|
||||||
|
UNINITIALIZED: 'uninitialized',
|
||||||
|
LOADING: 'loading',
|
||||||
|
COMPLETE: 'complete',
|
||||||
|
};
|
||||||
|
|
||||||
const app = uiModules.get('apps/discover', [
|
const app = uiModules.get('apps/discover', [
|
||||||
'kibana/notify',
|
'kibana/notify',
|
||||||
'kibana/courier',
|
'kibana/courier',
|
||||||
|
@ -170,7 +175,6 @@ function discoverController(
|
||||||
let visualizeHandler;
|
let visualizeHandler;
|
||||||
const Vis = Private(VisProvider);
|
const Vis = Private(VisProvider);
|
||||||
const docTitle = Private(DocTitleProvider);
|
const docTitle = Private(DocTitleProvider);
|
||||||
const HitSortFn = Private(PluginsKibanaDiscoverHitSortFnProvider);
|
|
||||||
const queryFilter = Private(FilterBarQueryFilterProvider);
|
const queryFilter = Private(FilterBarQueryFilterProvider);
|
||||||
const responseHandler = Private(VislibSeriesResponseHandlerProvider).handler;
|
const responseHandler = Private(VislibSeriesResponseHandlerProvider).handler;
|
||||||
const filterManager = Private(FilterManagerProvider);
|
const filterManager = Private(FilterManagerProvider);
|
||||||
|
@ -190,6 +194,7 @@ function discoverController(
|
||||||
$scope.intervalOptions = intervalOptions;
|
$scope.intervalOptions = intervalOptions;
|
||||||
$scope.showInterval = false;
|
$scope.showInterval = false;
|
||||||
$scope.minimumVisibleRows = 50;
|
$scope.minimumVisibleRows = 50;
|
||||||
|
$scope.fetchStatus = fetchStatuses.UNINITIALIZED;
|
||||||
|
|
||||||
$scope.intervalEnabled = function (interval) {
|
$scope.intervalEnabled = function (interval) {
|
||||||
return interval.val !== 'custom';
|
return interval.val !== 'custom';
|
||||||
|
@ -373,18 +378,16 @@ function discoverController(
|
||||||
const getFieldCounts = async () => {
|
const getFieldCounts = async () => {
|
||||||
// the field counts aren't set until we have the data back,
|
// the field counts aren't set until we have the data back,
|
||||||
// so we wait for the fetch to be done before proceeding
|
// so we wait for the fetch to be done before proceeding
|
||||||
if (!$scope.fetchStatus) {
|
if ($scope.fetchStatus === fetchStatuses.COMPLETE) {
|
||||||
return $scope.fieldCounts;
|
return $scope.fieldCounts;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await new Promise(resolve => {
|
return await new Promise(resolve => {
|
||||||
const unwatch = $scope.$watch('fetchStatus', (newValue) => {
|
const unwatch = $scope.$watch('fetchStatus', (newValue) => {
|
||||||
if (newValue) {
|
if (newValue === fetchStatuses.COMPLETE) {
|
||||||
return;
|
unwatch();
|
||||||
|
resolve($scope.fieldCounts);
|
||||||
}
|
}
|
||||||
|
|
||||||
unwatch();
|
|
||||||
resolve($scope.fieldCounts);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -567,13 +570,9 @@ function discoverController(
|
||||||
if (rows == null && oldRows == null) return status.LOADING;
|
if (rows == null && oldRows == null) return status.LOADING;
|
||||||
|
|
||||||
const rowsEmpty = _.isEmpty(rows);
|
const rowsEmpty = _.isEmpty(rows);
|
||||||
// An undefined fetchStatus means the requests are still being
|
const preparingForFetch = fetchStatus === fetchStatuses.UNINITIALIZED;
|
||||||
// prepared to be sent. When all requests are completed,
|
|
||||||
// fetchStatus is set to null, so it's important that we
|
|
||||||
// specifically check for undefined to determine a loading status.
|
|
||||||
const preparingForFetch = _.isUndefined(fetchStatus);
|
|
||||||
if (preparingForFetch) return status.LOADING;
|
if (preparingForFetch) return status.LOADING;
|
||||||
else if (rowsEmpty && fetchStatus) return status.LOADING;
|
else if (rowsEmpty && fetchStatus === fetchStatuses.LOADING) return status.LOADING;
|
||||||
else if (!rowsEmpty) return status.READY;
|
else if (!rowsEmpty) return status.READY;
|
||||||
else return status.NO_RESULTS;
|
else return status.NO_RESULTS;
|
||||||
}
|
}
|
||||||
|
@ -662,6 +661,8 @@ function discoverController(
|
||||||
.then(setupVisualization)
|
.then(setupVisualization)
|
||||||
.then(function () {
|
.then(function () {
|
||||||
$state.save();
|
$state.save();
|
||||||
|
$scope.fetchStatus = fetchStatuses.LOADING;
|
||||||
|
logInspectorRequest();
|
||||||
return courier.fetch();
|
return courier.fetch();
|
||||||
})
|
})
|
||||||
.catch(notify.error);
|
.catch(notify.error);
|
||||||
|
@ -673,176 +674,72 @@ function discoverController(
|
||||||
$scope.fetch();
|
$scope.fetch();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function onResults(resp) {
|
||||||
|
logInspectorResponse(resp);
|
||||||
|
|
||||||
function handleSegmentedFetch(segmented) {
|
if ($scope.opts.timefield) {
|
||||||
function flushResponseData() {
|
const tabifiedData = tabifyAggResponse($scope.vis.aggs, resp);
|
||||||
$scope.fetchError = undefined;
|
$scope.searchSource.rawResponse = resp;
|
||||||
$scope.hits = 0;
|
Promise
|
||||||
$scope.failures = [];
|
.resolve(buildVislibDimensions($scope.vis, { timeRange: $scope.timeRange, searchSource: $scope.searchSource }))
|
||||||
$scope.rows = [];
|
.then(resp => responseHandler(tabifiedData, resp))
|
||||||
$scope.fieldCounts = {};
|
.then(resp => {
|
||||||
}
|
visualizeHandler.render({
|
||||||
|
as: 'visualization',
|
||||||
if (!$scope.rows) flushResponseData();
|
value: {
|
||||||
|
visType: $scope.vis.type.name,
|
||||||
const sort = $state.sort;
|
visData: resp,
|
||||||
const timeField = $scope.indexPattern.timeFieldName;
|
visConfig: $scope.vis.params,
|
||||||
|
params: {},
|
||||||
/**
|
|
||||||
* Basically an emum.
|
|
||||||
*
|
|
||||||
* opts:
|
|
||||||
* "time" - sorted by the timefield
|
|
||||||
* "non-time" - explicitly sorted by a non-time field, NOT THE SAME AS `sortBy !== "time"`
|
|
||||||
* "implicit" - no sorting set, NOT THE SAME AS "non-time"
|
|
||||||
*
|
|
||||||
* @type {String}
|
|
||||||
*/
|
|
||||||
const sortBy = (function () {
|
|
||||||
if (!Array.isArray(sort)) return 'implicit';
|
|
||||||
else if (sort[0] === '_score') return 'implicit';
|
|
||||||
else if (sort[0] === timeField) return 'time';
|
|
||||||
else return 'non-time';
|
|
||||||
}());
|
|
||||||
|
|
||||||
let sortFn = null;
|
|
||||||
if (sortBy !== 'implicit') {
|
|
||||||
sortFn = new HitSortFn(sort[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.updateTime();
|
|
||||||
|
|
||||||
if (sort[0] === '_score') {
|
|
||||||
segmented.setMaxSegments(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
segmented.setDirection(sortBy === 'time' ? (sort[1] || 'desc') : 'desc');
|
|
||||||
segmented.setSortFn(sortFn);
|
|
||||||
segmented.setSize($scope.opts.sampleSize);
|
|
||||||
|
|
||||||
let inspectorRequests = [];
|
|
||||||
function logResponseInInspector(resp) {
|
|
||||||
if (inspectorRequests.length > 0) {
|
|
||||||
const inspectorRequest = inspectorRequests.shift();
|
|
||||||
inspectorRequest
|
|
||||||
.stats(getResponseInspectorStats($scope.searchSource, resp))
|
|
||||||
.ok({ json: resp });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// triggered when the status updated
|
|
||||||
segmented.on('status', function (status) {
|
|
||||||
$scope.fetchStatus = status;
|
|
||||||
if (status.complete === 0) {
|
|
||||||
// starting new segmented search request
|
|
||||||
inspectorAdapters.requests.reset();
|
|
||||||
inspectorRequests = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status.remaining > 0) {
|
|
||||||
const inspectorRequest = inspectorAdapters.requests.start(
|
|
||||||
i18n('kbn.discover.inspectorRequest.segmentFetchCompleteStatusTitle', {
|
|
||||||
defaultMessage: 'Segment {fetchCompleteStatus}',
|
|
||||||
values: {
|
|
||||||
fetchCompleteStatus: $scope.fetchStatus.complete,
|
|
||||||
}
|
}
|
||||||
}),
|
|
||||||
{
|
|
||||||
description: i18n('kbn.discover.inspectorRequest.segmentFetchCompleteStatusDescription', {
|
|
||||||
defaultMessage: 'This request queries Elasticsearch to fetch the data for the search.',
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
inspectorRequest.stats(getRequestInspectorStats($scope.searchSource));
|
|
||||||
$scope.searchSource.getSearchRequestBody().then(body => {
|
|
||||||
inspectorRequest.json(body);
|
|
||||||
});
|
});
|
||||||
inspectorRequests.push(inspectorRequest);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
});
|
$scope.hits = resp.hits.total;
|
||||||
|
$scope.rows = resp.hits.hits;
|
||||||
|
|
||||||
segmented.on('first', function () {
|
// if we haven't counted yet, reset the counts
|
||||||
flushResponseData();
|
const counts = $scope.fieldCounts = $scope.fieldCounts || {};
|
||||||
});
|
|
||||||
|
|
||||||
segmented.on('segment', (resp) => {
|
$scope.rows.forEach(hit => {
|
||||||
logResponseInInspector(resp);
|
const fields = Object.keys($scope.indexPattern.flattenHit(hit));
|
||||||
if (resp._shards.failed > 0) {
|
fields.forEach(fieldName => {
|
||||||
$scope.failures = _.union($scope.failures, resp._shards.failures);
|
counts[fieldName] = (counts[fieldName] || 0) + 1;
|
||||||
$scope.failures = _.uniq($scope.failures, false, function (failure) {
|
|
||||||
return failure.index + failure.shard + failure.reason;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
segmented.on('emptySegment', function (resp) {
|
|
||||||
logResponseInInspector(resp);
|
|
||||||
});
|
|
||||||
|
|
||||||
segmented.on('mergedSegment', function (merged) {
|
|
||||||
$scope.mergedEsResp = merged;
|
|
||||||
|
|
||||||
if ($scope.opts.timefield) {
|
|
||||||
const tabifiedData = tabifyAggResponse($scope.vis.aggs, merged);
|
|
||||||
$scope.searchSource.rawResponse = merged;
|
|
||||||
Promise
|
|
||||||
.resolve(buildVislibDimensions($scope.vis, { timeRange: $scope.timeRange, searchSource: $scope.searchSource }))
|
|
||||||
.then(resp => responseHandler(tabifiedData, resp))
|
|
||||||
.then(resp => {
|
|
||||||
visualizeHandler.render({
|
|
||||||
as: 'visualization',
|
|
||||||
value: {
|
|
||||||
visType: $scope.vis.type.name,
|
|
||||||
visData: resp,
|
|
||||||
visConfig: $scope.vis.params,
|
|
||||||
params: {},
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.hits = merged.hits.total;
|
|
||||||
|
|
||||||
const indexPattern = $scope.searchSource.getField('index');
|
|
||||||
|
|
||||||
// the merge rows, use a new array to help watchers
|
|
||||||
$scope.rows = merged.hits.hits.slice();
|
|
||||||
|
|
||||||
let counts = $scope.fieldCounts;
|
|
||||||
|
|
||||||
// if we haven't counted yet, or need a fresh count because we are sorting, reset the counts
|
|
||||||
if (!counts || sortFn) counts = $scope.fieldCounts = {};
|
|
||||||
|
|
||||||
$scope.rows.forEach(function (hit) {
|
|
||||||
// skip this work if we have already done it
|
|
||||||
if (hit.$$_counted) return;
|
|
||||||
|
|
||||||
// when we are sorting results, we need to redo the counts each time because the
|
|
||||||
// "top 500" may change with each response, so don't mark this as counted
|
|
||||||
if (!sortFn) hit.$$_counted = true;
|
|
||||||
|
|
||||||
const fields = _.keys(indexPattern.flattenHit(hit));
|
|
||||||
let n = fields.length;
|
|
||||||
let field;
|
|
||||||
while (field = fields[--n]) {
|
|
||||||
if (counts[field]) counts[field] += 1;
|
|
||||||
else counts[field] = 1;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
segmented.on('complete', function () {
|
$scope.fetchStatus = fetchStatuses.COMPLETE;
|
||||||
if ($scope.fetchStatus.hitCount === 0) {
|
|
||||||
flushResponseData();
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.fetchStatus = null;
|
return $scope.searchSource.onResults().then(onResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
let inspectorRequest;
|
||||||
|
|
||||||
|
function logInspectorRequest() {
|
||||||
|
inspectorAdapters.requests.reset();
|
||||||
|
const title = i18n('kbn.discover.inspectorRequestDataTitle', {
|
||||||
|
defaultMessage: 'Data',
|
||||||
|
});
|
||||||
|
const description = i18n('kbn.discover.inspectorRequestDescription', {
|
||||||
|
defaultMessage: 'This request queries Elasticsearch to fetch the data for the search.',
|
||||||
|
});
|
||||||
|
inspectorRequest = inspectorAdapters.requests.start(title, { description });
|
||||||
|
inspectorRequest.stats(getRequestInspectorStats($scope.searchSource));
|
||||||
|
$scope.searchSource.getSearchRequestBody().then(body => {
|
||||||
|
inspectorRequest.json(body);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function logInspectorResponse(resp) {
|
||||||
|
inspectorRequest
|
||||||
|
.stats(getResponseInspectorStats($scope.searchSource, resp))
|
||||||
|
.ok({ json: resp });
|
||||||
|
}
|
||||||
|
|
||||||
function beginSegmentedFetch() {
|
function startSearching() {
|
||||||
$scope.searchSource.onBeginSegmentedFetch(handleSegmentedFetch)
|
return $scope.searchSource.onResults()
|
||||||
|
.then(onResults)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
const fetchError = getPainlessError(error);
|
const fetchError = getPainlessError(error);
|
||||||
|
|
||||||
|
@ -853,10 +750,11 @@ function discoverController(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restart. This enables auto-refresh functionality.
|
// Restart. This enables auto-refresh functionality.
|
||||||
beginSegmentedFetch();
|
startSearching();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
beginSegmentedFetch();
|
|
||||||
|
startSearching();
|
||||||
|
|
||||||
$scope.updateTime = function () {
|
$scope.updateTime = function () {
|
||||||
$scope.timeRange = {
|
$scope.timeRange = {
|
||||||
|
|
|
@ -101,8 +101,6 @@
|
||||||
></h2>
|
></h2>
|
||||||
<div class="euiSpacer euiSpacer--m"></div>
|
<div class="euiSpacer euiSpacer--m"></div>
|
||||||
<div class="euiLoadingSpinner euiLoadingSpinner--large"></div>
|
<div class="euiLoadingSpinner euiLoadingSpinner--large"></div>
|
||||||
<div class="euiSpacer euiSpacer--m"></div>
|
|
||||||
<div ng-show="fetchStatus">{{fetchStatus.complete}} / {{fetchStatus.total}}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -302,19 +302,6 @@ export function getUiSettingDefaults() {
|
||||||
}),
|
}),
|
||||||
category: ['discover'],
|
category: ['discover'],
|
||||||
},
|
},
|
||||||
'courier:maxSegmentCount': {
|
|
||||||
name: i18n.translate('kbn.advancedSettings.courier.maxSegmentCountTitle', {
|
|
||||||
defaultMessage: 'Maximum segment count',
|
|
||||||
}),
|
|
||||||
value: 30,
|
|
||||||
description: i18n.translate('kbn.advancedSettings.courier.maxSegmentCountText', {
|
|
||||||
defaultMessage:
|
|
||||||
'Requests in discover are split into segments to prevent massive requests from being sent to elasticsearch. ' +
|
|
||||||
'This setting attempts to prevent the list of segments from getting too long, ' +
|
|
||||||
'which might cause requests to take much longer to process.',
|
|
||||||
}),
|
|
||||||
category: ['search'],
|
|
||||||
},
|
|
||||||
'courier:ignoreFilterIfFieldNotInIndex': {
|
'courier:ignoreFilterIfFieldNotInIndex': {
|
||||||
name: i18n.translate('kbn.advancedSettings.courier.ignoreFilterTitle', {
|
name: i18n.translate('kbn.advancedSettings.courier.ignoreFilterTitle', {
|
||||||
defaultMessage: 'Ignore filter(s)',
|
defaultMessage: 'Ignore filter(s)',
|
||||||
|
|
|
@ -1,82 +0,0 @@
|
||||||
/*
|
|
||||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
|
||||||
* license agreements. See the NOTICE file distributed with
|
|
||||||
* this work for additional information regarding copyright
|
|
||||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
|
||||||
* the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
* not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import sinon from 'sinon';
|
|
||||||
import expect from '@kbn/expect';
|
|
||||||
import ngMock from 'ng_mock';
|
|
||||||
|
|
||||||
import StubbedSearchSourceProvider from 'fixtures/stubbed_search_source';
|
|
||||||
|
|
||||||
import { SegmentedSearchRequestProvider } from '../segmented_search_request';
|
|
||||||
|
|
||||||
describe('SegmentedSearchRequest _createQueue', () => {
|
|
||||||
let Promise;
|
|
||||||
let SegmentedSearchRequest;
|
|
||||||
let MockSource;
|
|
||||||
|
|
||||||
require('test_utils/no_digest_promises').activateForSuite();
|
|
||||||
|
|
||||||
beforeEach(ngMock.module('kibana'));
|
|
||||||
beforeEach(ngMock.inject((Private, $injector) => {
|
|
||||||
Promise = $injector.get('Promise');
|
|
||||||
SegmentedSearchRequest = Private(SegmentedSearchRequestProvider);
|
|
||||||
|
|
||||||
MockSource = class {
|
|
||||||
constructor() {
|
|
||||||
return $injector.invoke(StubbedSearchSourceProvider);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('manages the req._queueCreated flag', async function () {
|
|
||||||
const req = new SegmentedSearchRequest({ source: new MockSource(), errorHandler: () => {} });
|
|
||||||
req._queueCreated = null;
|
|
||||||
|
|
||||||
const promise = req._createQueue();
|
|
||||||
expect(req._queueCreated).to.be(false);
|
|
||||||
await promise;
|
|
||||||
expect(req._queueCreated).to.be(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('relies on indexPattern.toDetailedIndexList to generate queue', async function () {
|
|
||||||
const searchSource = new MockSource();
|
|
||||||
const indexPattern = searchSource.getField('index');
|
|
||||||
const indices = [1, 2, 3];
|
|
||||||
sinon.stub(indexPattern, 'toDetailedIndexList').returns(Promise.resolve(indices));
|
|
||||||
|
|
||||||
const req = new SegmentedSearchRequest({ source: searchSource, errorHandler: () => {} });
|
|
||||||
const output = await req._createQueue();
|
|
||||||
expect(output).to.equal(indices);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('tells the index pattern its direction', async function () {
|
|
||||||
const searchSource = new MockSource();
|
|
||||||
const indexPattern = searchSource.getField('index');
|
|
||||||
const req = new SegmentedSearchRequest({ source: searchSource, errorHandler: () => {} });
|
|
||||||
sinon.stub(indexPattern, 'toDetailedIndexList').returns(Promise.resolve([1, 2, 3]));
|
|
||||||
|
|
||||||
req.setDirection('asc');
|
|
||||||
await req._createQueue();
|
|
||||||
expect(indexPattern.toDetailedIndexList.lastCall.args[2]).to.be('asc');
|
|
||||||
|
|
||||||
req.setDirection('desc');
|
|
||||||
await req._createQueue();
|
|
||||||
expect(indexPattern.toDetailedIndexList.lastCall.args[2]).to.be('desc');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,148 +0,0 @@
|
||||||
/*
|
|
||||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
|
||||||
* license agreements. See the NOTICE file distributed with
|
|
||||||
* this work for additional information regarding copyright
|
|
||||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
|
||||||
* the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
* not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import ngMock from 'ng_mock';
|
|
||||||
import expect from '@kbn/expect';
|
|
||||||
import { times } from 'lodash';
|
|
||||||
import sinon from 'sinon';
|
|
||||||
|
|
||||||
import HitSortFnProv from 'plugins/kibana/discover/_hit_sort_fn';
|
|
||||||
import NoDigestPromises from 'test_utils/no_digest_promises';
|
|
||||||
import StubbedSearchSourceProvider from 'fixtures/stubbed_search_source';
|
|
||||||
|
|
||||||
import { SegmentedSearchRequestProvider } from '../segmented_search_request';
|
|
||||||
|
|
||||||
describe('SegmentedSearchRequest index selection', function () {
|
|
||||||
let Promise;
|
|
||||||
let SegmentedSearchRequest;
|
|
||||||
let MockSource;
|
|
||||||
let HitSortFn;
|
|
||||||
|
|
||||||
NoDigestPromises.activateForSuite();
|
|
||||||
|
|
||||||
beforeEach(ngMock.module('kibana'));
|
|
||||||
beforeEach(ngMock.inject((Private, $injector) => {
|
|
||||||
Promise = $injector.get('Promise');
|
|
||||||
HitSortFn = Private(HitSortFnProv);
|
|
||||||
SegmentedSearchRequest = Private(SegmentedSearchRequestProvider);
|
|
||||||
|
|
||||||
MockSource = class {
|
|
||||||
constructor() {
|
|
||||||
return $injector.invoke(StubbedSearchSourceProvider);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('queries with size until all 500 docs returned', async function () {
|
|
||||||
const searchSource = new MockSource();
|
|
||||||
const indexPattern = searchSource.getField('index');
|
|
||||||
sinon.stub(indexPattern, 'toDetailedIndexList').returns(Promise.resolve([
|
|
||||||
{ index: 'one', min: 0, max: 1 },
|
|
||||||
{ index: 'two', min: 0, max: 1 },
|
|
||||||
{ index: 'three', min: 0, max: 1 },
|
|
||||||
{ index: 'four', min: 0, max: 1 },
|
|
||||||
{ index: 'five', min: 0, max: 1 },
|
|
||||||
]));
|
|
||||||
|
|
||||||
const req = new SegmentedSearchRequest({ source: searchSource, errorHandler: () => {} });
|
|
||||||
req._handle.setDirection('desc');
|
|
||||||
req._handle.setSortFn(new HitSortFn('desc'));
|
|
||||||
req._handle.setSize(500);
|
|
||||||
await req.start();
|
|
||||||
|
|
||||||
// first 200
|
|
||||||
expect((await req.getFetchParams()).body.size).to.be(500);
|
|
||||||
await req.handleResponse({
|
|
||||||
hits: { total: 1000, hits: times(200, (i) => ({ i })) }
|
|
||||||
});
|
|
||||||
|
|
||||||
// total = 400
|
|
||||||
expect((await req.getFetchParams()).body.size).to.be(500);
|
|
||||||
await req.handleResponse({
|
|
||||||
hits: { total: 1000, hits: times(200, (i) => ({ i })) }
|
|
||||||
});
|
|
||||||
|
|
||||||
// total = 600
|
|
||||||
expect((await req.getFetchParams()).body.size).to.be(500);
|
|
||||||
await req.handleResponse({
|
|
||||||
hits: { total: 1000, hits: times(200, (i) => ({ i })) }
|
|
||||||
});
|
|
||||||
|
|
||||||
expect((await req.getFetchParams()).body.size).to.be(0);
|
|
||||||
await req.handleResponse({
|
|
||||||
hits: { total: 1000, hits: times(200, (i) => ({ i })) }
|
|
||||||
});
|
|
||||||
|
|
||||||
expect((await req.getFetchParams()).body.size).to.be(0);
|
|
||||||
await req.handleResponse({
|
|
||||||
hits: { total: 1000, hits: times(200, (i) => ({ i })) }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`sets size 0 for indices that couldn't preclude hits`, async function () {
|
|
||||||
const searchSource = new MockSource();
|
|
||||||
const indexPattern = searchSource.getField('index');
|
|
||||||
|
|
||||||
// the segreq is looking for 10 documents, and we will give it ten docs with time:5 in the first response.
|
|
||||||
// on the second index it should still request 10 documents because it could produce documents with time:5.
|
|
||||||
// the next two indexes will get size 0, since they couldn't produce documents with the time:5
|
|
||||||
// the final index will get size:10, because it too can produce docs with time:5
|
|
||||||
sinon.stub(indexPattern, 'toDetailedIndexList').returns(Promise.resolve([
|
|
||||||
{ index: 'one', min: 0, max: 10 },
|
|
||||||
{ index: 'two', min: 0, max: 10 },
|
|
||||||
{ index: 'three', min: 12, max: 20 },
|
|
||||||
{ index: 'four', min: 15, max: 20 },
|
|
||||||
{ index: 'five', min: 5, max: 50 },
|
|
||||||
]));
|
|
||||||
|
|
||||||
const req = new SegmentedSearchRequest({ source: searchSource, errorHandler: () => {} });
|
|
||||||
req._handle.setDirection('desc');
|
|
||||||
req._handle.setSortFn(new HitSortFn('desc'));
|
|
||||||
req._handle.setSize(10);
|
|
||||||
await req.start();
|
|
||||||
|
|
||||||
// first 10
|
|
||||||
expect((await req.getFetchParams()).body.size).to.be(10);
|
|
||||||
await req.handleResponse({
|
|
||||||
hits: { total: 1000, hits: times(10, () => ({ _source: { time: 5 } })) }
|
|
||||||
});
|
|
||||||
|
|
||||||
// total = 400
|
|
||||||
expect((await req.getFetchParams()).body.size).to.be(10);
|
|
||||||
await req.handleResponse({
|
|
||||||
hits: { total: 1000, hits: times(10, () => ({ _source: { time: 5 } })) }
|
|
||||||
});
|
|
||||||
|
|
||||||
// total = 600
|
|
||||||
expect((await req.getFetchParams()).body.size).to.be(0);
|
|
||||||
await req.handleResponse({
|
|
||||||
hits: { total: 1000, hits: [] }
|
|
||||||
});
|
|
||||||
|
|
||||||
expect((await req.getFetchParams()).body.size).to.be(0);
|
|
||||||
await req.handleResponse({
|
|
||||||
hits: { total: 1000, hits: [] }
|
|
||||||
});
|
|
||||||
|
|
||||||
expect((await req.getFetchParams()).body.size).to.be(10);
|
|
||||||
await req.handleResponse({
|
|
||||||
hits: { total: 1000, hits: times(10, () => ({ _source: { time: 5 } })) }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,88 +0,0 @@
|
||||||
/*
|
|
||||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
|
||||||
* license agreements. See the NOTICE file distributed with
|
|
||||||
* this work for additional information regarding copyright
|
|
||||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
|
||||||
* the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
* not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import sinon from 'sinon';
|
|
||||||
import expect from '@kbn/expect';
|
|
||||||
import ngMock from 'ng_mock';
|
|
||||||
|
|
||||||
import { SegmentedSearchRequestProvider } from '../segmented_search_request';
|
|
||||||
import { SearchRequestProvider } from '../../search_request';
|
|
||||||
|
|
||||||
describe('SegmentedSearchRequest', () => {
|
|
||||||
let Promise;
|
|
||||||
let SegmentedSearchRequest;
|
|
||||||
let segmentedReq;
|
|
||||||
let abstractReqStart;
|
|
||||||
|
|
||||||
beforeEach(ngMock.module('kibana'));
|
|
||||||
|
|
||||||
beforeEach(ngMock.inject((Private, $injector) => {
|
|
||||||
Promise = $injector.get('Promise');
|
|
||||||
SegmentedSearchRequest = Private(SegmentedSearchRequestProvider);
|
|
||||||
|
|
||||||
const SearchRequest = Private(SearchRequestProvider);
|
|
||||||
abstractReqStart = sinon.stub(SearchRequest.prototype, 'start').callsFake(() => {
|
|
||||||
const promise = Promise.resolve();
|
|
||||||
sinon.spy(promise, 'then');
|
|
||||||
return promise;
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('#start()', () => {
|
|
||||||
let returned;
|
|
||||||
beforeEach(() => {
|
|
||||||
init();
|
|
||||||
returned = segmentedReq.start();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns promise', () => {
|
|
||||||
expect(returned.then).to.be.Function;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls AbstractReq#start()', () => {
|
|
||||||
sinon.assert.calledOnce(abstractReqStart);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('listens to promise from super.start()', () => {
|
|
||||||
sinon.assert.calledOnce(abstractReqStart);
|
|
||||||
const promise = abstractReqStart.firstCall.returnValue;
|
|
||||||
sinon.assert.calledOnce(promise.then);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
segmentedReq = new SegmentedSearchRequest({ source: mockSource(), errorHandler: () => {} });
|
|
||||||
}
|
|
||||||
|
|
||||||
function mockSource() {
|
|
||||||
return {
|
|
||||||
get: sinon.stub().returns(mockIndexPattern()),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function mockIndexPattern() {
|
|
||||||
return {
|
|
||||||
toDetailedIndexList: sinon.stub().returns(Promise.resolve([
|
|
||||||
{ index: 1, min: 0, max: 1 },
|
|
||||||
{ index: 2, min: 0, max: 1 },
|
|
||||||
{ index: 3, min: 0, max: 1 },
|
|
||||||
]))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,70 +0,0 @@
|
||||||
/*
|
|
||||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
|
||||||
* license agreements. See the NOTICE file distributed with
|
|
||||||
* this work for additional information regarding copyright
|
|
||||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
|
||||||
* the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
* not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import ngMock from 'ng_mock';
|
|
||||||
import expect from '@kbn/expect';
|
|
||||||
|
|
||||||
import HitSortFnProv from 'plugins/kibana/discover/_hit_sort_fn';
|
|
||||||
import NoDigestPromises from 'test_utils/no_digest_promises';
|
|
||||||
import StubbedSearchSourceProvider from 'fixtures/stubbed_search_source';
|
|
||||||
|
|
||||||
import { SegmentedSearchRequestProvider } from '../segmented_search_request';
|
|
||||||
|
|
||||||
describe('SegmentedSearchRequest size picking', function () {
|
|
||||||
let SegmentedSearchRequest;
|
|
||||||
let MockSource;
|
|
||||||
let HitSortFn;
|
|
||||||
|
|
||||||
NoDigestPromises.activateForSuite();
|
|
||||||
|
|
||||||
beforeEach(ngMock.module('kibana'));
|
|
||||||
beforeEach(ngMock.inject((Private, $injector) => {
|
|
||||||
HitSortFn = Private(HitSortFnProv);
|
|
||||||
SegmentedSearchRequest = Private(SegmentedSearchRequestProvider);
|
|
||||||
|
|
||||||
MockSource = class {
|
|
||||||
constructor() {
|
|
||||||
return $injector.invoke(StubbedSearchSourceProvider);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('without a size', function () {
|
|
||||||
it('does not set the request size', async function () {
|
|
||||||
const req = new SegmentedSearchRequest({ source: new MockSource(), errorHandler: () => {} });
|
|
||||||
req._handle.setDirection('desc');
|
|
||||||
req._handle.setSortFn(new HitSortFn('desc'));
|
|
||||||
await req.start();
|
|
||||||
|
|
||||||
expect((await req.getFetchParams()).body).to.not.have.property('size');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('with a size', function () {
|
|
||||||
it('sets the request size to the entire desired size', async function () {
|
|
||||||
const req = new SegmentedSearchRequest({ source: new MockSource(), errorHandler: () => {} });
|
|
||||||
req._handle.setDirection('desc');
|
|
||||||
req._handle.setSize(555);
|
|
||||||
req._handle.setSortFn(new HitSortFn('desc'));
|
|
||||||
await req.start();
|
|
||||||
|
|
||||||
expect((await req.getFetchParams()).body).to.have.property('size', 555);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,20 +0,0 @@
|
||||||
/*
|
|
||||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
|
||||||
* license agreements. See the NOTICE file distributed with
|
|
||||||
* this work for additional information regarding copyright
|
|
||||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
|
||||||
* the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
* not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export { SegmentedSearchRequestProvider } from './segmented_search_request';
|
|
|
@ -1,59 +0,0 @@
|
||||||
/*
|
|
||||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
|
||||||
* license agreements. See the NOTICE file distributed with
|
|
||||||
* this work for additional information regarding copyright
|
|
||||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
|
||||||
* the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
* not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { EventsProvider } from '../../../../events';
|
|
||||||
|
|
||||||
export function SegmentedHandleProvider(Private) {
|
|
||||||
const Events = Private(EventsProvider);
|
|
||||||
|
|
||||||
const segmentedRequest = Symbol('Actual Segmented Request');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple class for creating an object to send to the
|
|
||||||
* requester of a SegmentedSearchRequest. Since the SegmentedSearchRequest
|
|
||||||
* extends AbstractRequest, it wasn't able to be the event
|
|
||||||
* emitter it was born to be. This provides a channel for
|
|
||||||
* setting values on the segmented request, and an event
|
|
||||||
* emitter for the request to speak outwardly
|
|
||||||
*
|
|
||||||
* @param {SegmentedSearchRequest} - req - the request this handle relates to
|
|
||||||
*/
|
|
||||||
return class SegmentedHandle extends Events {
|
|
||||||
constructor(req) {
|
|
||||||
super();
|
|
||||||
this[segmentedRequest] = req;
|
|
||||||
}
|
|
||||||
|
|
||||||
setDirection(...args) {
|
|
||||||
this[segmentedRequest].setDirection(...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
setSize(...args) {
|
|
||||||
this[segmentedRequest].setSize(...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
setMaxSegments(...args) {
|
|
||||||
this[segmentedRequest].setMaxSegments(...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
setSortFn(...args) {
|
|
||||||
this[segmentedRequest].setSortFn(...args);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,346 +0,0 @@
|
||||||
/*
|
|
||||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
|
||||||
* license agreements. See the NOTICE file distributed with
|
|
||||||
* this work for additional information regarding copyright
|
|
||||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
|
||||||
* the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
* not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import _ from 'lodash';
|
|
||||||
import { timefilter } from 'ui/timefilter';
|
|
||||||
import { SearchRequestProvider } from '../search_request';
|
|
||||||
import { SegmentedHandleProvider } from './segmented_handle';
|
|
||||||
import { pushAll } from '../../../../utils/collection';
|
|
||||||
|
|
||||||
export function SegmentedSearchRequestProvider(Private, config) {
|
|
||||||
const SearchRequest = Private(SearchRequestProvider);
|
|
||||||
const SegmentedHandle = Private(SegmentedHandleProvider);
|
|
||||||
|
|
||||||
class SegmentedSearchRequest extends SearchRequest {
|
|
||||||
constructor({ source, defer, errorHandler, initFn }) {
|
|
||||||
super({ source, defer, errorHandler });
|
|
||||||
|
|
||||||
this.type = 'segmented';
|
|
||||||
|
|
||||||
// segmented request specific state
|
|
||||||
this._initFn = initFn;
|
|
||||||
|
|
||||||
this._desiredSize = null;
|
|
||||||
this._maxSegments = config.get('courier:maxSegmentCount');
|
|
||||||
this._direction = 'desc';
|
|
||||||
this._sortFn = null;
|
|
||||||
this._queueCreated = false;
|
|
||||||
this._handle = new SegmentedHandle(this);
|
|
||||||
|
|
||||||
this._hitWindow = null;
|
|
||||||
|
|
||||||
// prevent the source from changing between requests,
|
|
||||||
// all calls will return the same promise
|
|
||||||
this._getFlattenedSource = _.once(this._getFlattenedSource);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*********
|
|
||||||
** SearchReq overrides
|
|
||||||
*********/
|
|
||||||
|
|
||||||
start() {
|
|
||||||
return super.start().then(() => {
|
|
||||||
this._complete = [];
|
|
||||||
this._active = null;
|
|
||||||
this._segments = [];
|
|
||||||
this._all = [];
|
|
||||||
this._queue = [];
|
|
||||||
|
|
||||||
this._mergedResp = {
|
|
||||||
took: 0,
|
|
||||||
hits: {
|
|
||||||
hits: [],
|
|
||||||
total: 0,
|
|
||||||
max_score: 0
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// give the request consumer a chance to receive each segment and set
|
|
||||||
// parameters via the handle
|
|
||||||
if (_.isFunction(this._initFn)) this._initFn(this._handle);
|
|
||||||
return this._createQueue();
|
|
||||||
})
|
|
||||||
.then((queue) => {
|
|
||||||
if (this.stopped) return;
|
|
||||||
|
|
||||||
this._all = queue.slice(0);
|
|
||||||
|
|
||||||
// Send the initial fetch status
|
|
||||||
return this._reportStatus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
continue() {
|
|
||||||
return this._reportStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
getFetchParams() {
|
|
||||||
return this._getFlattenedSource().then(flatSource => {
|
|
||||||
const params = _.cloneDeep(flatSource);
|
|
||||||
|
|
||||||
// calculate the number of indices to fetch in this request in order to prevent
|
|
||||||
// more than this._maxSegments requests. We use Math.max(1, n) to ensure that each request
|
|
||||||
// has at least one index pattern, and Math.floor() to make sure that if the
|
|
||||||
// number of indices does not round out evenly the extra index is tacked onto the last
|
|
||||||
// request, making sure the first request returns faster.
|
|
||||||
const remainingSegments = this._maxSegments - this._segments.length;
|
|
||||||
const indexCount = Math.max(1, Math.floor(this._queue.length / remainingSegments));
|
|
||||||
|
|
||||||
const indices = this._active = this._queue.splice(0, indexCount);
|
|
||||||
params.index = indices.map(({ index }) => index).join(',');
|
|
||||||
|
|
||||||
if (_.isNumber(this._desiredSize)) {
|
|
||||||
params.body.size = this._pickSizeForIndices(indices);
|
|
||||||
}
|
|
||||||
|
|
||||||
return params;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleResponse(resp) {
|
|
||||||
return this._consumeSegment(resp);
|
|
||||||
}
|
|
||||||
|
|
||||||
filterError(resp) {
|
|
||||||
if (/ClusterBlockException.*index\sclosed/.test(resp.error)) {
|
|
||||||
this._consumeSegment(false);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isIncomplete() {
|
|
||||||
const queueNotCreated = !this._queueCreated;
|
|
||||||
const queueNotEmpty = this._queue.length > 0;
|
|
||||||
return queueNotCreated || queueNotEmpty;
|
|
||||||
}
|
|
||||||
|
|
||||||
clone() {
|
|
||||||
return new SegmentedSearchRequest(this.source, this.defer, this._initFn);
|
|
||||||
}
|
|
||||||
|
|
||||||
complete() {
|
|
||||||
this._reportStatus();
|
|
||||||
this._handle.emit('complete');
|
|
||||||
return super.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*********
|
|
||||||
** SegmentedSearchRequest specific methods
|
|
||||||
*********/
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the sort total number of segments to emit
|
|
||||||
*
|
|
||||||
* @param {number}
|
|
||||||
*/
|
|
||||||
setMaxSegments(maxSegments) {
|
|
||||||
this._maxSegments = Math.max(_.parseInt(maxSegments), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the sort direction for the request.
|
|
||||||
*
|
|
||||||
* @param {string} dir - one of 'asc' or 'desc'
|
|
||||||
*/
|
|
||||||
setDirection(dir) {
|
|
||||||
switch (dir) {
|
|
||||||
case 'asc':
|
|
||||||
case 'desc':
|
|
||||||
return (this._direction = dir);
|
|
||||||
default:
|
|
||||||
throw new TypeError('unknown sort direction "' + dir + '"');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the function that will be used to sort the rows
|
|
||||||
*
|
|
||||||
* @param {fn}
|
|
||||||
*/
|
|
||||||
setSortFn(sortFn) {
|
|
||||||
this._sortFn = sortFn;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the sort total number of documents to
|
|
||||||
* emit
|
|
||||||
*
|
|
||||||
* Setting to false will not limit the documents,
|
|
||||||
* if a number is set the size of the request to es
|
|
||||||
* will be updated on each new request
|
|
||||||
*
|
|
||||||
* @param {number|false}
|
|
||||||
*/
|
|
||||||
setSize(totalSize) {
|
|
||||||
this._desiredSize = _.parseInt(totalSize);
|
|
||||||
if (isNaN(this._desiredSize)) this._desiredSize = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_createQueue() {
|
|
||||||
const timeBounds = timefilter.getBounds();
|
|
||||||
const indexPattern = this.source.getField('index');
|
|
||||||
this._queueCreated = false;
|
|
||||||
|
|
||||||
return indexPattern.toDetailedIndexList(timeBounds.min, timeBounds.max, this._direction)
|
|
||||||
.then(queue => {
|
|
||||||
this._queue = queue;
|
|
||||||
this._queueCreated = true;
|
|
||||||
return queue;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_reportStatus() {
|
|
||||||
return this._handle.emit('status', {
|
|
||||||
total: this._queueCreated ? this._all.length : NaN,
|
|
||||||
complete: this._queueCreated ? this._complete.length : NaN,
|
|
||||||
remaining: this._queueCreated ? this._queue.length : NaN,
|
|
||||||
hitCount: this._queueCreated ? this._mergedResp.hits.hits.length : NaN
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_getFlattenedSource() {
|
|
||||||
return this.source._flatten();
|
|
||||||
}
|
|
||||||
|
|
||||||
_consumeSegment(seg) {
|
|
||||||
const index = this._active;
|
|
||||||
this._complete.push(index);
|
|
||||||
if (!seg) return; // segment was ignored/filtered, don't store it
|
|
||||||
|
|
||||||
const hadHits = _.get(this._mergedResp, 'hits.hits.length') > 0;
|
|
||||||
const gotHits = _.get(seg, 'hits.hits.length') > 0;
|
|
||||||
const firstHits = !hadHits && gotHits;
|
|
||||||
const haveHits = hadHits || gotHits;
|
|
||||||
|
|
||||||
this._mergeSegment(seg);
|
|
||||||
this.resp = _.omit(this._mergedResp, '_bucketIndex');
|
|
||||||
|
|
||||||
if (firstHits) this._handle.emit('first', seg);
|
|
||||||
gotHits ? this._handle.emit('segment', seg) : this._handle.emit('emptySegment', seg);
|
|
||||||
if (haveHits) this._handle.emit('mergedSegment', this.resp);
|
|
||||||
}
|
|
||||||
|
|
||||||
_mergeHits(hits) {
|
|
||||||
const mergedHits = this._mergedResp.hits.hits;
|
|
||||||
const desiredSize = this._desiredSize;
|
|
||||||
const sortFn = this._sortFn;
|
|
||||||
|
|
||||||
pushAll(hits, mergedHits);
|
|
||||||
|
|
||||||
if (sortFn) {
|
|
||||||
mergedHits.sort(sortFn);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_.isNumber(desiredSize)) {
|
|
||||||
this._mergedResp.hits.hits = mergedHits.slice(0, desiredSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_mergeSegment(seg) {
|
|
||||||
const merged = this._mergedResp;
|
|
||||||
|
|
||||||
this._segments.push(seg);
|
|
||||||
|
|
||||||
merged.took += seg.took;
|
|
||||||
merged.hits.total += seg.hits.total;
|
|
||||||
merged.hits.max_score = Math.max(merged.hits.max_score, seg.hits.max_score);
|
|
||||||
|
|
||||||
if (_.size(seg.hits.hits)) {
|
|
||||||
this._mergeHits(seg.hits.hits);
|
|
||||||
this._detectHitsWindow(merged.hits.hits);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!seg.aggregations) return;
|
|
||||||
|
|
||||||
Object.keys(seg.aggregations).forEach(function (aggKey) {
|
|
||||||
|
|
||||||
if (!merged.aggregations) {
|
|
||||||
// start merging aggregations
|
|
||||||
merged.aggregations = {};
|
|
||||||
merged._bucketIndex = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!merged.aggregations[aggKey]) {
|
|
||||||
merged.aggregations[aggKey] = {
|
|
||||||
buckets: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
seg.aggregations[aggKey].buckets.forEach(function (bucket) {
|
|
||||||
let mbucket = merged._bucketIndex[bucket.key];
|
|
||||||
if (mbucket) {
|
|
||||||
mbucket.doc_count += bucket.doc_count;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mbucket = merged._bucketIndex[bucket.key] = bucket;
|
|
||||||
merged.aggregations[aggKey].buckets.push(mbucket);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_detectHitsWindow(hits) {
|
|
||||||
hits = hits || [];
|
|
||||||
const indexPattern = this.source.getField('index');
|
|
||||||
const desiredSize = this._desiredSize;
|
|
||||||
|
|
||||||
const size = _.size(hits);
|
|
||||||
if (!_.isNumber(desiredSize) || size < desiredSize) {
|
|
||||||
this._hitWindow = {
|
|
||||||
size: size,
|
|
||||||
min: -Infinity,
|
|
||||||
max: Infinity
|
|
||||||
};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let min;
|
|
||||||
let max;
|
|
||||||
|
|
||||||
hits.forEach(function (deepHit) {
|
|
||||||
const hit = indexPattern.flattenHit(deepHit);
|
|
||||||
const time = hit[indexPattern.timeFieldName];
|
|
||||||
if (min == null || time < min) min = time;
|
|
||||||
if (max == null || time > max) max = time;
|
|
||||||
});
|
|
||||||
|
|
||||||
this._hitWindow = { size, min, max };
|
|
||||||
}
|
|
||||||
|
|
||||||
_pickSizeForIndices(indices) {
|
|
||||||
const hitWindow = this._hitWindow;
|
|
||||||
const desiredSize = this._desiredSize;
|
|
||||||
|
|
||||||
if (!_.isNumber(desiredSize)) return null;
|
|
||||||
// we don't have any hits yet, get us more info!
|
|
||||||
if (!hitWindow) return desiredSize;
|
|
||||||
// the order of documents isn't important, just get us more
|
|
||||||
if (!this._sortFn) return Math.max(desiredSize - hitWindow.size, 0);
|
|
||||||
// if all of the documents in every index fall outside of our current doc set, we can ignore them.
|
|
||||||
const someOverlap = indices.some(function (index) {
|
|
||||||
return index.min <= hitWindow.max && hitWindow.min <= index.max;
|
|
||||||
});
|
|
||||||
|
|
||||||
return someOverlap ? desiredSize : 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return SegmentedSearchRequest;
|
|
||||||
}
|
|
|
@ -76,7 +76,6 @@ import { buildEsQuery, getEsQueryConfig, filterMatchesIndex } from '@kbn/es-quer
|
||||||
import '../../promises';
|
import '../../promises';
|
||||||
import { NormalizeSortRequestProvider } from './_normalize_sort_request';
|
import { NormalizeSortRequestProvider } from './_normalize_sort_request';
|
||||||
import { SearchRequestProvider } from '../fetch/request';
|
import { SearchRequestProvider } from '../fetch/request';
|
||||||
import { SegmentedSearchRequestProvider } from '../fetch/request/segmented_search_request';
|
|
||||||
|
|
||||||
import { searchRequestQueue } from '../search_request_queue';
|
import { searchRequestQueue } from '../search_request_queue';
|
||||||
import { FetchSoonProvider } from '../fetch';
|
import { FetchSoonProvider } from '../fetch';
|
||||||
|
@ -117,7 +116,6 @@ function isIndexPattern(val) {
|
||||||
|
|
||||||
export function SearchSourceProvider(Promise, Private, config) {
|
export function SearchSourceProvider(Promise, Private, config) {
|
||||||
const SearchRequest = Private(SearchRequestProvider);
|
const SearchRequest = Private(SearchRequestProvider);
|
||||||
const SegmentedSearchRequest = Private(SegmentedSearchRequestProvider);
|
|
||||||
const normalizeSortRequest = Private(NormalizeSortRequestProvider);
|
const normalizeSortRequest = Private(NormalizeSortRequestProvider);
|
||||||
const fetchSoon = Private(FetchSoonProvider);
|
const fetchSoon = Private(FetchSoonProvider);
|
||||||
const { fieldWildcardFilter } = Private(FieldWildcardProvider);
|
const { fieldWildcardFilter } = Private(FieldWildcardProvider);
|
||||||
|
@ -390,26 +388,6 @@ export function SearchSourceProvider(Promise, Private, config) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeginSegmentedFetch(initFunction) {
|
|
||||||
const self = this;
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
function addRequest() {
|
|
||||||
const defer = Promise.defer();
|
|
||||||
const errorHandler = (request, error) => {
|
|
||||||
reject(error);
|
|
||||||
request.abort();
|
|
||||||
};
|
|
||||||
const req = new SegmentedSearchRequest({ source: self, defer, errorHandler, initFn: initFunction });
|
|
||||||
|
|
||||||
// Return promises created by the completion handler so that
|
|
||||||
// errors will bubble properly
|
|
||||||
return req.getCompletePromise().then(addRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
addRequest();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getSearchRequestBody() {
|
async getSearchRequestBody() {
|
||||||
const searchRequest = await this._flatten();
|
const searchRequest = await this._flatten();
|
||||||
return searchRequest.body;
|
return searchRequest.body;
|
||||||
|
|
|
@ -815,8 +815,6 @@
|
||||||
"kbn.advancedSettings.courier.ignoreFilterTitle": "忽略筛选",
|
"kbn.advancedSettings.courier.ignoreFilterTitle": "忽略筛选",
|
||||||
"kbn.advancedSettings.courier.maxRequestsText": "控制用于 Kibana 发送的 _msearch 请求的 “{maxRequestsLink}” 设置。设置为 0 可禁用此配置并使用 Elasticsearch 默认值。",
|
"kbn.advancedSettings.courier.maxRequestsText": "控制用于 Kibana 发送的 _msearch 请求的 “{maxRequestsLink}” 设置。设置为 0 可禁用此配置并使用 Elasticsearch 默认值。",
|
||||||
"kbn.advancedSettings.courier.maxRequestsTitle": "最大并发分片请求数",
|
"kbn.advancedSettings.courier.maxRequestsTitle": "最大并发分片请求数",
|
||||||
"kbn.advancedSettings.courier.maxSegmentCountText": "Discover 中的请求将拆分成段,以防止大型请求发送给 Elasticsearch。此设置会尝试阻止段列表过长,否则会导致系统花费很长时间处理请求。",
|
|
||||||
"kbn.advancedSettings.courier.maxSegmentCountTitle": "最大段计数",
|
|
||||||
"kbn.advancedSettings.courier.requestPreferenceText": "允许您设置用于处理搜索请求的分片。<ul>\n <li><strong>{sessionId}:</strong> 限制在相同分片上执行所有搜索请求的操作。\n 这有利于在各个请求之间复用分片缓存。</li>\n <li><strong>{custom}:</strong> 允许您定义自己的首选项。\n 使用 <strong>courier:customRequestPreference</strong> 定制首选项值。</li>\n <li><strong>{none}:</strong> 表示不设置首选项。\n 这可能会提供更佳的性能,因此请求可以在所有分片副本上进行分配。\n 不过,结果可能会不一致,因为不同的分片可能处于不同的刷新状态。</li>\n </ul>",
|
"kbn.advancedSettings.courier.requestPreferenceText": "允许您设置用于处理搜索请求的分片。<ul>\n <li><strong>{sessionId}:</strong> 限制在相同分片上执行所有搜索请求的操作。\n 这有利于在各个请求之间复用分片缓存。</li>\n <li><strong>{custom}:</strong> 允许您定义自己的首选项。\n 使用 <strong>courier:customRequestPreference</strong> 定制首选项值。</li>\n <li><strong>{none}:</strong> 表示不设置首选项。\n 这可能会提供更佳的性能,因此请求可以在所有分片副本上进行分配。\n 不过,结果可能会不一致,因为不同的分片可能处于不同的刷新状态。</li>\n </ul>",
|
||||||
"kbn.advancedSettings.courier.requestPreferenceTitle": "请求首选项",
|
"kbn.advancedSettings.courier.requestPreferenceTitle": "请求首选项",
|
||||||
"kbn.advancedSettings.csv.quoteValuesText": "在 csv 导出中是否应使用引号引起值?",
|
"kbn.advancedSettings.csv.quoteValuesText": "在 csv 导出中是否应使用引号引起值?",
|
||||||
|
@ -1280,8 +1278,6 @@
|
||||||
"kbn.discover.hitsPluralTitle": "{hits, plural, one {次命中} other {次命中}}",
|
"kbn.discover.hitsPluralTitle": "{hits, plural, one {次命中} other {次命中}}",
|
||||||
"kbn.discover.howToChangeTheTimeTooltip": "要更改时间,请单击导航栏中的时钟图标",
|
"kbn.discover.howToChangeTheTimeTooltip": "要更改时间,请单击导航栏中的时钟图标",
|
||||||
"kbn.discover.howToSeeOtherMatchingDocumentsDescription": "以下是匹配您的搜索的前 {sampleSize} 个文档,请优化您的搜索以查看其他文档。",
|
"kbn.discover.howToSeeOtherMatchingDocumentsDescription": "以下是匹配您的搜索的前 {sampleSize} 个文档,请优化您的搜索以查看其他文档。",
|
||||||
"kbn.discover.inspectorRequest.segmentFetchCompleteStatusDescription": "此请求将查询 Elasticsearch 以获取搜索的数据。",
|
|
||||||
"kbn.discover.inspectorRequest.segmentFetchCompleteStatusTitle": "段 {fetchCompleteStatus}",
|
|
||||||
"kbn.discover.localMenu.inspectTitle": "检查",
|
"kbn.discover.localMenu.inspectTitle": "检查",
|
||||||
"kbn.discover.localMenu.localMenu.newSearchTitle": "新建",
|
"kbn.discover.localMenu.localMenu.newSearchTitle": "新建",
|
||||||
"kbn.discover.localMenu.newSearchDescription": "新搜索",
|
"kbn.discover.localMenu.newSearchDescription": "新搜索",
|
||||||
|
@ -8174,4 +8170,4 @@
|
||||||
"xpack.watcher.watchActionsTitle": "满足后将执行 {watchActionsCount, plural, one{# 个操作} other {# 个操作}}",
|
"xpack.watcher.watchActionsTitle": "满足后将执行 {watchActionsCount, plural, one{# 个操作} other {# 个操作}}",
|
||||||
"xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。"
|
"xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue