mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Chore] Remove allocation from flattenHit (#180647)
## Summary
`flattenHit` is in a tight loop, and allocates numerous temporary
objects. This removes the majority of them.
For comparison, this implementation will now be more in line with
`collectBucket`, which uses a similar approach to avoid temporary
allocations.
84304bc0dc/src/plugins/data/common/search/tabify/tabify.ts (L30)
### For maintainers
- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
parent
f1e02f6422
commit
cb2c9d97b0
1 changed files with 97 additions and 61 deletions
|
@ -8,7 +8,12 @@
|
|||
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { isPlainObject } from 'lodash';
|
||||
import { Datatable, DatatableColumn, DatatableColumnType } from '@kbn/expressions-plugin/common';
|
||||
import {
|
||||
Datatable,
|
||||
DatatableColumn,
|
||||
DatatableRow,
|
||||
DatatableColumnType,
|
||||
} from '@kbn/expressions-plugin/common';
|
||||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
|
||||
// meta fields we won't merge with our result hit
|
||||
|
@ -33,6 +38,55 @@ interface TabifyDocsOptions {
|
|||
// Can be removed once the estypes.SearchHit knows about ignored_field_values
|
||||
type Hit<T = unknown> = estypes.SearchHit<T> & { ignored_field_values?: Record<string, unknown[]> };
|
||||
|
||||
function flattenAccum(
|
||||
flat: Record<string, any>,
|
||||
obj: Record<string, any>,
|
||||
keyPrefix: string,
|
||||
indexPattern?: DataView,
|
||||
params?: TabifyDocsOptions
|
||||
) {
|
||||
for (const k in obj) {
|
||||
if (!obj.hasOwnProperty(k)) {
|
||||
continue;
|
||||
}
|
||||
const val = obj[k];
|
||||
|
||||
const key = keyPrefix + k;
|
||||
const field = indexPattern?.fields.getByName(key);
|
||||
|
||||
if (params?.shallow === false) {
|
||||
const isNestedField = field?.type === 'nested';
|
||||
if (Array.isArray(val) && !isNestedField) {
|
||||
for (let i = 0; i < val.length; i++) {
|
||||
const v = val[i];
|
||||
if (isPlainObject(v)) {
|
||||
flattenAccum(flat, v, key + '.', indexPattern, params);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
} else if (flat[key] !== undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const hasValidMapping = field && field.type !== 'conflict';
|
||||
const isValue = !isPlainObject(val);
|
||||
|
||||
if (hasValidMapping || isValue) {
|
||||
if (!flat[key]) {
|
||||
flat[key] = val;
|
||||
} else if (Array.isArray(flat[key])) {
|
||||
flat[key].push(val);
|
||||
} else {
|
||||
flat[key] = [flat[key], val];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
flattenAccum(flat, val, key + '.', indexPattern, params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattens an individual hit (from an ES response) into an object. This will
|
||||
* create flattened field names, like `user.name`.
|
||||
|
@ -44,43 +98,10 @@ type Hit<T = unknown> = estypes.SearchHit<T> & { ignored_field_values?: Record<s
|
|||
export function flattenHit(hit: Hit, indexPattern?: DataView, params?: TabifyDocsOptions) {
|
||||
const flat = {} as Record<string, any>;
|
||||
|
||||
function flatten(obj: Record<string, any>, keyPrefix: string = '') {
|
||||
for (const [k, val] of Object.entries(obj)) {
|
||||
const key = keyPrefix + k;
|
||||
flattenAccum(flat, hit.fields || {}, '', indexPattern, params);
|
||||
|
||||
const field = indexPattern?.fields.getByName(key);
|
||||
|
||||
if (params?.shallow === false) {
|
||||
const isNestedField = field?.type === 'nested';
|
||||
if (Array.isArray(val) && !isNestedField) {
|
||||
val.forEach((v) => isPlainObject(v) && flatten(v, key + '.'));
|
||||
continue;
|
||||
}
|
||||
} else if (flat[key] !== undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const hasValidMapping = field && field.type !== 'conflict';
|
||||
const isValue = !isPlainObject(val);
|
||||
|
||||
if (hasValidMapping || isValue) {
|
||||
if (!flat[key]) {
|
||||
flat[key] = val;
|
||||
} else if (Array.isArray(flat[key])) {
|
||||
flat[key].push(val);
|
||||
} else {
|
||||
flat[key] = [flat[key], val];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
flatten(val, key + '.');
|
||||
}
|
||||
}
|
||||
|
||||
flatten(hit.fields || {});
|
||||
if (params?.source !== false && hit._source) {
|
||||
flatten(hit._source as Record<string, any>);
|
||||
flattenAccum(flat, hit._source as Record<string, any>, '', indexPattern, params);
|
||||
} else if (params?.includeIgnoredValues && hit.ignored_field_values) {
|
||||
// If enabled merge the ignored_field_values into the flattened hit. This will
|
||||
// merge values that are not actually indexed by ES (i.e. ignored), e.g. because
|
||||
|
@ -90,7 +111,11 @@ export function flattenHit(hit: Hit, indexPattern?: DataView, params?: TabifyDoc
|
|||
// merged them both together. We do not merge this (even if enabled) in case source has been
|
||||
// merged, since we would otherwise duplicate values, since ignore_field_values and _source
|
||||
// contain the same values.
|
||||
Object.entries(hit.ignored_field_values).forEach(([fieldName, fieldValue]) => {
|
||||
for (const fieldName in hit.ignored_field_values) {
|
||||
if (!hit.ignored_field_values.hasOwnProperty(fieldName)) {
|
||||
continue;
|
||||
}
|
||||
const fieldValue = hit.ignored_field_values[fieldName];
|
||||
if (flat[fieldName]) {
|
||||
// If there was already a value from the fields API, make sure we're merging both together
|
||||
if (Array.isArray(flat[fieldName])) {
|
||||
|
@ -102,37 +127,45 @@ export function flattenHit(hit: Hit, indexPattern?: DataView, params?: TabifyDoc
|
|||
// If no previous value was assigned we can simply use the value from `ignored_field_values` as it is
|
||||
flat[fieldName] = fieldValue;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Merge all valid meta fields into the flattened object
|
||||
indexPattern?.metaFields?.forEach((fieldName) => {
|
||||
const isExcludedMetaField =
|
||||
EXCLUDED_META_FIELDS.includes(fieldName) || fieldName.charAt(0) !== '_';
|
||||
if (isExcludedMetaField) {
|
||||
return;
|
||||
if (indexPattern?.metaFields) {
|
||||
for (let i = 0; i < indexPattern?.metaFields.length; i++) {
|
||||
const fieldName = indexPattern?.metaFields[i];
|
||||
const isExcludedMetaField =
|
||||
EXCLUDED_META_FIELDS.includes(fieldName) || fieldName.charAt(0) !== '_';
|
||||
if (!isExcludedMetaField) {
|
||||
flat[fieldName] = hit[fieldName as keyof estypes.SearchHit];
|
||||
}
|
||||
}
|
||||
flat[fieldName] = hit[fieldName as keyof estypes.SearchHit];
|
||||
});
|
||||
}
|
||||
|
||||
// Use a proxy to make sure that keys are always returned in a specific order,
|
||||
// so we have a guarantee on the flattened order of keys.
|
||||
return makeProxy(flat, indexPattern);
|
||||
}
|
||||
|
||||
function makeProxy(flat: Record<string, any>, indexPattern?: DataView) {
|
||||
function comparator(a: string | symbol, b: string | symbol) {
|
||||
const aIsMeta = indexPattern?.metaFields?.includes(String(a));
|
||||
const bIsMeta = indexPattern?.metaFields?.includes(String(b));
|
||||
if (aIsMeta && bIsMeta) {
|
||||
return String(a).localeCompare(String(b));
|
||||
}
|
||||
if (aIsMeta) {
|
||||
return 1;
|
||||
}
|
||||
if (bIsMeta) {
|
||||
return -1;
|
||||
}
|
||||
return String(a).localeCompare(String(b));
|
||||
}
|
||||
|
||||
return new Proxy(flat, {
|
||||
ownKeys: (target) => {
|
||||
return Reflect.ownKeys(target).sort((a, b) => {
|
||||
const aIsMeta = indexPattern?.metaFields?.includes(String(a));
|
||||
const bIsMeta = indexPattern?.metaFields?.includes(String(b));
|
||||
if (aIsMeta && bIsMeta) {
|
||||
return String(a).localeCompare(String(b));
|
||||
}
|
||||
if (aIsMeta) {
|
||||
return 1;
|
||||
}
|
||||
if (bIsMeta) {
|
||||
return -1;
|
||||
}
|
||||
return String(a).localeCompare(String(b));
|
||||
});
|
||||
return Reflect.ownKeys(target).sort(comparator);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -144,9 +177,12 @@ export const tabifyDocs = (
|
|||
): Datatable => {
|
||||
const columns: DatatableColumn[] = [];
|
||||
|
||||
const rows = esResponse.hits.hits
|
||||
const rows: Array<DatatableRow | undefined> = esResponse.hits.hits
|
||||
.map((hit) => {
|
||||
const flat = flattenHit(hit, index, params);
|
||||
if (!flat) {
|
||||
return;
|
||||
}
|
||||
for (const [key, value] of Object.entries(flat)) {
|
||||
const field = index?.fields.getByName(key);
|
||||
const fieldName = field?.name || key;
|
||||
|
@ -172,6 +208,6 @@ export const tabifyDocs = (
|
|||
return {
|
||||
type: 'datatable',
|
||||
columns,
|
||||
rows,
|
||||
rows: rows as DatatableRow[],
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue