Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Discuss] Enhancement/saved object search #15175

Closed
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { getRootProperties } from '../../../../server/mappings';

export const patchMissingProperties = {
id: 'missing_properties',

async getUpdatedPatchMappings(context) {
const {
kibanaIndexMappingsDsl,
currentMappingsDsl
} = context;

const expectedProps = getRootProperties(kibanaIndexMappingsDsl);
const existingProps = getRootProperties(currentMappingsDsl);

return Object.keys(expectedProps)
.reduce((acc, prop) => {
if (existingProps[prop]) {
return acc;
} else {
console.log('missing');
return { ...acc || {}, [prop]: expectedProps[prop] };
}
}, null);
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {
getRootProperties,
getProperty
} from '../../../../server/mappings';

import { get } from 'lodash';


export const patchMissingTitleKeywordFields = {
id: 'missing_title_keyword_fields',

async getUpdatedPatchMappings(context) {
const {
currentMappingsDsl,
kibanaIndexMappingsDsl,
} = context;

const properties = getRootProperties(currentMappingsDsl);
const propertiesWithTitleKeyword = Object.keys(getRootProperties(kibanaIndexMappingsDsl))
.filter(property => Boolean(getProperty(kibanaIndexMappingsDsl, [property, 'title', 'keyword'])));
const mappings = {};

for (const property of propertiesWithTitleKeyword) {
const hasKeyword = !!get(properties, `${property}.properties.title.fields.keyword`);
if (hasKeyword) {
continue;
}

const titleMapping = get(properties, `${property}.properties.title`);
mappings[property] = {
properties: {
title: {
...titleMapping,
fields: {
keyword: {
type: 'keyword',
}
}
}
}
};
}

// Make sure we return a falsy value
if (!Object.keys(mappings).length) {
return;
}

return mappings;
},

async applyChanges(context) {
const {
patchMappings,
callCluster,
indexName,
rootEsType,
log,
} = context;

const properties = Object.keys(patchMappings);
const types = properties.map(type => ({ match: { type } }));

log(['info', 'elasticsearch'], {
tmpl: `Updating by query for Saved Object types "<%= names.join('", "') %>"`,
names: properties,
});

await callCluster('updateByQuery', {
conflicts: 'proceed',
index: indexName,
type: rootEsType,
body: {
query: {
bool: {
should: types,
},
},
},
});
}
};
103 changes: 57 additions & 46 deletions src/core_plugins/elasticsearch/lib/patch_kibana_index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import {
getTypes,
getRootType,
getRootProperties
} from '../../../server/mappings';

import { patchMissingProperties } from './kibana_index_patches/patch_missing_properties';
import { patchMissingTitleKeywordFields } from './kibana_index_patches/patch_missing_title_keyword_fields';

/**
* Checks that the root type in the kibana index has all of the
* root properties specified by the kibanaIndexMappings.
Expand All @@ -17,10 +19,10 @@ import {
*/
export async function patchKibanaIndex(options) {
const {
log,
indexName,
callCluster,
kibanaIndexMappingsDsl
kibanaIndexMappingsDsl,
log,
} = options;

const rootEsType = getRootType(kibanaIndexMappingsDsl);
Expand All @@ -31,29 +33,61 @@ export async function patchKibanaIndex(options) {
return;
}

const missingProperties = await getMissingRootProperties(currentMappingsDsl, kibanaIndexMappingsDsl);
const context = {
...options,
currentMappingsDsl,
rootEsType,
};

const missingPropertyNames = Object.keys(missingProperties);
if (!missingPropertyNames.length) {
// all expected properties are in current mapping
return;
}
const patchesToApply = [
patchMissingProperties,
patchMissingTitleKeywordFields,
];

// log about new properties
log(['info', 'elasticsearch'], {
tmpl: `Adding mappings to kibana index for SavedObject types "<%= names.join('", "') %>"`,
names: missingPropertyNames
});
for (const patch of patchesToApply) {
const patchMappings = await patch.getUpdatedPatchMappings(context);
if (!patchMappings) {
continue;
}

// add the new properties to the index mapping
await callCluster('indices.putMapping', {
index: indexName,
type: rootEsType,
body: {
properties: missingProperties
},
update_all_types: true
});
try {
// log about new properties
log(['info', 'elasticsearch'], {
tmpl: `Adding mappings to kibana index for SavedObject types "<%= names.join('", "') %>"`,
names: Object.keys(patchMappings),
});

// add the new properties to the index mapping
await callCluster('indices.putMapping', {
index: indexName,
type: rootEsType,
body: {
properties: patchMappings,
},
update_all_types: true
});
}
catch (e) {
log(['error', 'elasticsearch'], {
tmpl: `Unable to patch mappings for "<%= cls %>"`,
cls: patch.id,
});
continue;
}

try {
await patch.applyChanges({
...context,
patchMappings,
});
}
catch (e) {
log(['error', 'elasticsearch'], {
tmpl: `Unable to apply patch changes for "<%= cls %>"`,
cls: patch.id,
});
}
}
}

/**
Expand Down Expand Up @@ -88,26 +122,3 @@ async function getCurrentMappings(callCluster, indexName, rootEsType) {

return currentMappingsDsl;
}

/**
* Get the properties that are in the expectedMappingsDsl but not the
* currentMappingsDsl. Properties will be an object of properties normally
* found at `[index]mappings[typeName].properties` is es mapping responses
*
* @param {EsMappingsDsl} currentMappingsDsl
* @param {EsMappingsDsl} expectedMappingsDsl
* @return {PropertyMappings}
*/
async function getMissingRootProperties(currentMappingsDsl, expectedMappingsDsl) {
const expectedProps = getRootProperties(expectedMappingsDsl);
const existingProps = getRootProperties(currentMappingsDsl);

return Object.keys(expectedProps)
.reduce((acc, prop) => {
if (existingProps[prop]) {
return acc;
} else {
return { ...acc, [prop]: expectedProps[prop] };
}
}, {});
}
28 changes: 24 additions & 4 deletions src/core_plugins/kibana/mappings.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@
"type": "keyword"
},
"title": {
"type": "text"
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
}
}
},
Expand All @@ -40,7 +45,12 @@
"type": "keyword"
},
"title": {
"type": "text"
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"uiStateJSON": {
"type": "text"
Expand Down Expand Up @@ -75,7 +85,12 @@
"type": "keyword"
},
"title": {
"type": "text"
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"version": {
"type": "integer"
Expand Down Expand Up @@ -129,7 +144,12 @@
"type": "keyword"
},
"title": {
"type": "text"
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"uiStateJSON": {
"type": "text"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ export function getQueryParams(mappings, type, search, searchFields) {
if (search) {
bool.must = [
{
simple_query_string: {
query_string: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe explain the reason for this a bit? @elastic/kibana-discovery any idea how we can prevent this from being necessary?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would need to know requirements

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this so we can support letting the user prefix the query with a wildcard, (request).

So, if we had the following four visualizations stored: My Vis, My_Vis, My-Vis, MyVis, the user could query against *Vis and return all four results.

If we use simple_query_string, it seems to filter out the wildcard and only returns two results (My Vis and My-Vis).

This mirrors the search behavior you'd see in a text editor which felt natural and right to me, but it's fair to suggest we do not want to support it.

Copy link
Contributor

@spalger spalger Dec 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, leading wildcard is generally regarded as a problem and simple_query_string probably doesn't allow it. I think that for Kibana's use case it would make sense to use a tokenizer that converts My Vis, My_Vis, My-Vis, and MyVis all into my and vis... Is that an option given the fact that we're going to update_by_query anyway?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ngrams could be helpful https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-ngram-tokenizer.html

But I also wonder whether a leading wildcard will really be a problem over the set of vis documents. .kibana is generally going to be really small. We could add a config to turn wildcards off if we're really paranoid.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not concerned about the performance of leading wildcards in saved objects, but I am concerned about switching away from simple_query_string, didn't know you could configure simple_query_string to allow leading wildcards

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we can support leading wildcards with just simple_query_string:
"reason": "[simple_query_string] unsupported field [allow_leading_wildcard]"

@spalger Can you help me understand the dangers of switching from simple_query_string to query_string?

The docs say:

Unlike the regular query_string query, the simple_query_string query will never throw an exception, and discards invalid parts of the query.

But it feels we easily mitigate the exception by catching it within Kibana server and returning an empty result set?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't fully understand the ramifications of moving from simple_query_string to query_string, but I'm almost certain it's more than just about ignoring errors... Maybe @dakrone could help us understand the impact switching might have on Kibana?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coming into this with no history, so I'm not really sure exactly where this is going to be used but...

  • SQS allows you to turn features on and off using flags (not sure if this is useful to you)
  • SQS tries its hardest not to throw exceptions when using invalid query syntax, QS does no such thing and will flip out at the slightest syntax error (again, if you're programatically generating things it might be fine)
  • SQS doesn't support every feature that QS does (like the leading wildcard, as you've been discussing)
  • SQS and QS have different syntax, something like (foo | bar) + baz being different than (foo OR bar) AND baz

To weigh in my unsolicited opinion, If you are comfortable with ngrams as @Bargs recommended, that would be a much easier way to support sub-string search without resorting to fancy querying. You could even have multiple fields and search both, just boosting the non-ngram version.

Hopefully that helps, happy to clarify more if I can.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the information @dakrone!!

I'll continue to investigate ngrams, but it feels like, for this use case, switching from SQS to QS is a viable option still.

query: search,
default_operator: 'AND',
...getFieldsForTypes(
searchFields,
type ? [type] : Object.keys(getRootProperties(mappings))
Expand Down
2 changes: 1 addition & 1 deletion src/ui/public/courier/saved_object/saved_object_loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export class SavedObjectLoader {
search: search ? `${search}*` : undefined,
perPage: size,
page: 1,
searchFields: ['title^3', 'description']
searchFields: ['title^3', 'title.keyword^2', 'description']
}).then((resp) => {
return {
total: resp.total,
Expand Down