UNPKG

kibana-123

Version:

Kibana is an open source (Apache Licensed), browser based analytics and search dashboard for Elasticsearch. Kibana is a snap to setup and start using. Kibana strives to be easy to get started with, while also being flexible and powerful, just like Elastic

340 lines (271 loc) 9.5 kB
import _ from 'lodash'; import { isNumber } from 'lodash'; import Notifier from 'ui/notify/notifier'; import SearchRequestProvider from './search'; import SegmentedHandleProvider from './segmented_handle'; export default function SegmentedReqProvider(es, Private, Promise, timefilter, config) { const SearchReq = Private(SearchRequestProvider); const SegmentedHandle = Private(SegmentedHandleProvider); const notify = new Notifier({ location: 'Segmented Fetch' }); class SegmentedReq extends SearchReq { constructor(source, defer, initFn) { super(source, defer); 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() { super.start(); 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 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 = _.pluck(indices, 'index'); 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 SegmentedReq(this.source, this.defer, this._initFn); } complete() { this._reportStatus(); this._handle.emit('complete'); return super.complete(); } /********* ** SegmentedReq 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.get('index'); this._queueCreated = false; return indexPattern.toDetailedIndexList(timeBounds.min, timeBounds.max, this._direction) .then(queue => { if (!_.isArray(queue)) queue = [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); if (gotHits) this._handle.emit('segment', 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) { notify.event('resort rows', function () { 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.get('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; } } SegmentedReq.prototype.mergedSegment = notify.timed('merge response segment', SegmentedReq.prototype.mergedSegment); return SegmentedReq; };