mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Add multi field info to the IndexPattern (#33681)
Adds two fields to the IndexPattern Field: * parent - the name of the field this field is a child of * subType - The type of child this field is. Currently the only valid value is multi but we could expand this to include aliases, object children, and nested children. The thinking behind implementing these two new properties instead of a simple isMultiField flag is that it should be generic enough to describe other sorts of parent -> child relationships between fields.
This commit is contained in:
parent
0e45676a95
commit
d8916e37c9
15 changed files with 110 additions and 25 deletions
|
@ -144,7 +144,9 @@
|
|||
"scripted": false,
|
||||
"searchable": true,
|
||||
"aggregatable": true,
|
||||
"readFromDocValues": true
|
||||
"readFromDocValues": true,
|
||||
"parent": "machine.os",
|
||||
"subType": "multi"
|
||||
},
|
||||
{
|
||||
"name": "geo.src",
|
||||
|
|
|
@ -25,7 +25,7 @@ function stubbedLogstashFields() {
|
|||
return [
|
||||
// |aggregatable
|
||||
// | |searchable
|
||||
// name esType | | |metadata
|
||||
// name esType | | |metadata | parent | subType
|
||||
['bytes', 'long', true, true, { count: 10 } ],
|
||||
['ssl', 'boolean', true, true, { count: 20 } ],
|
||||
['@timestamp', 'date', true, true, { count: 30 } ],
|
||||
|
@ -41,7 +41,7 @@ function stubbedLogstashFields() {
|
|||
['geo.coordinates', 'geo_point', true, true ],
|
||||
['extension', 'keyword', true, true ],
|
||||
['machine.os', 'text', true, true ],
|
||||
['machine.os.raw', 'keyword', true, true ],
|
||||
['machine.os.raw', 'keyword', true, true, {}, 'machine.os', 'multi' ],
|
||||
['geo.src', 'keyword', true, true ],
|
||||
['_id', '_id', true, true ],
|
||||
['_type', '_type', true, true ],
|
||||
|
@ -59,7 +59,9 @@ function stubbedLogstashFields() {
|
|||
esType,
|
||||
aggregatable,
|
||||
searchable,
|
||||
metadata = {}
|
||||
metadata = {},
|
||||
parent = undefined,
|
||||
subType = undefined,
|
||||
] = row;
|
||||
|
||||
const {
|
||||
|
@ -83,6 +85,8 @@ function stubbedLogstashFields() {
|
|||
script,
|
||||
lang,
|
||||
scripted,
|
||||
parent,
|
||||
subType,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -29,4 +29,6 @@ export interface FieldDescriptor {
|
|||
readFromDocValues: boolean;
|
||||
searchable: boolean;
|
||||
type: string;
|
||||
parent?: string;
|
||||
subType?: string;
|
||||
}
|
||||
|
|
|
@ -193,6 +193,34 @@
|
|||
"index1"
|
||||
]
|
||||
}
|
||||
},
|
||||
"multi_parent": {
|
||||
"text": {
|
||||
"type": "text",
|
||||
"searchable": true,
|
||||
"aggregatable": false
|
||||
}
|
||||
},
|
||||
"multi_parent.child": {
|
||||
"keyword": {
|
||||
"type": "keyword",
|
||||
"searchable": true,
|
||||
"aggregatable": true
|
||||
}
|
||||
},
|
||||
"object_parent": {
|
||||
"object": {
|
||||
"type": "object",
|
||||
"searchable": false,
|
||||
"aggregatable": false
|
||||
}
|
||||
},
|
||||
"object_parent.child": {
|
||||
"keyword": {
|
||||
"type": "keyword",
|
||||
"searchable": true,
|
||||
"aggregatable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,7 +79,7 @@ import { shouldReadFieldFromDocValues } from './should_read_field_from_doc_value
|
|||
*/
|
||||
export function readFieldCapsResponse(fieldCapsResponse) {
|
||||
const capsByNameThenType = fieldCapsResponse.fields;
|
||||
return Object.keys(capsByNameThenType).map(fieldName => {
|
||||
const kibanaFormattedCaps = Object.keys(capsByNameThenType).map(fieldName => {
|
||||
const capsByType = capsByNameThenType[fieldName];
|
||||
const types = Object.keys(capsByType);
|
||||
|
||||
|
@ -120,7 +120,26 @@ export function readFieldCapsResponse(fieldCapsResponse) {
|
|||
aggregatable: isAggregatable,
|
||||
readFromDocValues: shouldReadFieldFromDocValues(isAggregatable, esType),
|
||||
};
|
||||
}).filter(field => {
|
||||
});
|
||||
|
||||
// Get all types of sub fields. These could be multi fields or children of nested/object types
|
||||
const subFields = kibanaFormattedCaps.filter(field => {
|
||||
return field.name.includes('.');
|
||||
});
|
||||
|
||||
// Discern which sub fields are multi fields. If the parent field is not an object or nested field
|
||||
// the child must be a multi field.
|
||||
subFields.forEach(field => {
|
||||
const parentFieldName = field.name.split('.').slice(0, -1).join('.');
|
||||
const parentFieldCaps = kibanaFormattedCaps.find(caps => caps.name === parentFieldName);
|
||||
|
||||
if (parentFieldCaps && !['object', 'nested'].includes(parentFieldCaps.type)) {
|
||||
field.parent = parentFieldName;
|
||||
field.subType = 'multi';
|
||||
}
|
||||
});
|
||||
|
||||
return kibanaFormattedCaps.filter(field => {
|
||||
return !['object', 'nested'].includes(field.type);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
/* eslint import/no-duplicates: 0 */
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { cloneDeep, omit } from 'lodash';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import * as shouldReadFieldFromDocValuesNS from './should_read_field_from_doc_values';
|
||||
|
@ -37,21 +37,20 @@ describe('index_patterns/field_capabilities/field_caps_response', () => {
|
|||
describe('conflicts', () => {
|
||||
it('returns a field for each in response, no filtering', () => {
|
||||
const fields = readFieldCapsResponse(esResponse);
|
||||
expect(fields).toHaveLength(19);
|
||||
expect(fields).toHaveLength(22);
|
||||
});
|
||||
|
||||
it('includes only name, type, searchable, aggregatable, readFromDocValues, and maybe conflictDescriptions of each field', () => {
|
||||
it('includes only name, type, searchable, aggregatable, readFromDocValues, and maybe conflictDescriptions, parent, ' +
|
||||
'and subType of each field', () => {
|
||||
const responseClone = cloneDeep(esResponse);
|
||||
// try to trick it into including an extra field
|
||||
responseClone.fields['@timestamp'].date.extraCapability = true;
|
||||
const fields = readFieldCapsResponse(responseClone);
|
||||
|
||||
fields.forEach(field => {
|
||||
if (field.conflictDescriptions) {
|
||||
delete field.conflictDescriptions;
|
||||
}
|
||||
const fieldWithoutOptionalKeys = omit(field, 'conflictDescriptions', 'parent', 'subType');
|
||||
|
||||
expect(Object.keys(field)).toEqual([
|
||||
expect(Object.keys(fieldWithoutOptionalKeys)).toEqual([
|
||||
'name',
|
||||
'type',
|
||||
'searchable',
|
||||
|
@ -65,7 +64,8 @@ describe('index_patterns/field_capabilities/field_caps_response', () => {
|
|||
sandbox.spy(shouldReadFieldFromDocValuesNS, 'shouldReadFieldFromDocValues');
|
||||
const fields = readFieldCapsResponse(esResponse);
|
||||
const conflictCount = fields.filter(f => f.type === 'conflict').length;
|
||||
sinon.assert.callCount(shouldReadFieldFromDocValues, fields.length - conflictCount);
|
||||
// +1 is for the object field which gets filtered out of the final return value from readFieldCapsResponse
|
||||
sinon.assert.callCount(shouldReadFieldFromDocValues, fields.length - conflictCount + 1);
|
||||
});
|
||||
|
||||
it('converts es types to kibana types', () => {
|
||||
|
@ -121,6 +121,23 @@ describe('index_patterns/field_capabilities/field_caps_response', () => {
|
|||
expect(mixSearchable.searchable).toBe(true);
|
||||
expect(mixSearchableOther.searchable).toBe(true);
|
||||
});
|
||||
|
||||
it('returns multi fields with parent and subType keys describing the relationship', () => {
|
||||
const fields = readFieldCapsResponse(esResponse);
|
||||
const child = fields.find(f => f.name === 'multi_parent.child');
|
||||
expect(child).toHaveProperty('parent', 'multi_parent');
|
||||
expect(child).toHaveProperty('subType', 'multi');
|
||||
});
|
||||
|
||||
it('should not confuse object children for multi field children', () => {
|
||||
// We detect multi fields by finding fields that have a dot in their name and then looking
|
||||
// to see if their parents are *not* object or nested fields. In the future we may want to
|
||||
// add parent and subType info for object and nested fields but for now we don't need it.
|
||||
const fields = readFieldCapsResponse(esResponse);
|
||||
const child = fields.find(f => f.name === 'object_parent.child');
|
||||
expect(child).not.toHaveProperty('parent');
|
||||
expect(child).not.toHaveProperty('subType');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -23,4 +23,6 @@ export interface Field {
|
|||
aggregatable: boolean;
|
||||
filterable: boolean;
|
||||
searchable: boolean;
|
||||
parent?: string;
|
||||
subType?: string;
|
||||
}
|
||||
|
|
|
@ -98,6 +98,10 @@ export function Field(indexPattern, spec) {
|
|||
// conflict info
|
||||
obj.writ('conflictDescriptions');
|
||||
|
||||
// multi info
|
||||
obj.fact('parent');
|
||||
obj.fact('subType');
|
||||
|
||||
return obj.create();
|
||||
}
|
||||
|
||||
|
|
|
@ -21,5 +21,6 @@ export default function ({ loadTestFile }) {
|
|||
describe('index_patterns/_fields_for_wildcard route', () => {
|
||||
loadTestFile(require.resolve('./params'));
|
||||
loadTestFile(require.resolve('./conflicts'));
|
||||
loadTestFile(require.resolve('./response'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -58,7 +58,9 @@ export default function ({ getService }) {
|
|||
searchable: true,
|
||||
aggregatable: true,
|
||||
name: 'baz.keyword',
|
||||
readFromDocValues: true
|
||||
readFromDocValues: true,
|
||||
parent: 'baz',
|
||||
subType: 'multi',
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
|
@ -86,7 +88,7 @@ export default function ({ getService }) {
|
|||
.expect(200, {
|
||||
fields: [
|
||||
{
|
||||
aggregatable: false,
|
||||
aggregatable: true,
|
||||
name: '_id',
|
||||
readFromDocValues: false,
|
||||
searchable: true,
|
||||
|
@ -118,7 +120,9 @@ export default function ({ getService }) {
|
|||
searchable: true,
|
||||
aggregatable: true,
|
||||
name: 'baz.keyword',
|
||||
readFromDocValues: true
|
||||
readFromDocValues: true,
|
||||
parent: 'baz',
|
||||
subType: 'multi',
|
||||
},
|
||||
{
|
||||
aggregatable: false,
|
||||
|
|
|
@ -144,7 +144,9 @@
|
|||
"scripted": false,
|
||||
"searchable": true,
|
||||
"aggregatable": true,
|
||||
"readFromDocValues": true
|
||||
"readFromDocValues": true,
|
||||
"parent": "machine.os",
|
||||
"subType": "multi"
|
||||
},
|
||||
{
|
||||
"name": "geo.src",
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue