diff --git a/docs/api/saved-objects/find.asciidoc b/docs/api/saved-objects/find.asciidoc index a95cf61ffd749..8a866c9de71d7 100644 --- a/docs/api/saved-objects/find.asciidoc +++ b/docs/api/saved-objects/find.asciidoc @@ -21,6 +21,8 @@ Note: You cannot access this endpoint via the Console in Kibana. (number) The page of objects to return `search` (optional):: (string) A {ref}/query-dsl-simple-query-string-query.html[simple_query_string] Elasticsearch query to filter the objects in the response +`default_search_operator` (optional):: + (string) The default operator to use for the `simple_query_string` `search_fields` (optional):: (array|string) The fields to perform the `simple_query_string` parsed query against `fields` (optional):: diff --git a/src/server/saved_objects/routes/find.js b/src/server/saved_objects/routes/find.js index c5890120e1441..27a30f0aa6427 100644 --- a/src/server/saved_objects/routes/find.js +++ b/src/server/saved_objects/routes/find.js @@ -31,6 +31,7 @@ export const createFindRoute = (prereqs) => ({ page: Joi.number().min(0).default(1), type: Joi.array().items(Joi.string()).single().required(), search: Joi.string().allow('').optional(), + default_search_operator: Joi.string().valid('OR', 'AND').default('OR'), search_fields: Joi.array().items(Joi.string()).single(), sort_field: Joi.array().items(Joi.string()).single(), fields: Joi.array().items(Joi.string()).single() diff --git a/src/server/saved_objects/routes/find.test.js b/src/server/saved_objects/routes/find.test.js index 3b1de7490367e..5c865c22f426b 100644 --- a/src/server/saved_objects/routes/find.test.js +++ b/src/server/saved_objects/routes/find.test.js @@ -105,7 +105,7 @@ describe('GET /api/saved_objects/_find', () => { expect(savedObjectsClient.find.calledOnce).toBe(true); const options = savedObjectsClient.find.getCall(0).args[0]; - expect(options).toEqual({ perPage: 20, page: 1, type: ['foo', 'bar'] }); + expect(options).toEqual({ perPage: 20, page: 1, type: ['foo', 'bar'], defaultSearchOperator: 'OR' }); }); it('accepts the query parameter page/per_page', async () => { @@ -119,7 +119,7 @@ describe('GET /api/saved_objects/_find', () => { expect(savedObjectsClient.find.calledOnce).toBe(true); const options = savedObjectsClient.find.getCall(0).args[0]; - expect(options).toEqual({ perPage: 10, page: 50, type: ['foo'] }); + expect(options).toEqual({ perPage: 10, page: 50, type: ['foo'], defaultSearchOperator: 'OR' }); }); it('accepts the query parameter search_fields', async () => { @@ -133,7 +133,7 @@ describe('GET /api/saved_objects/_find', () => { expect(savedObjectsClient.find.calledOnce).toBe(true); const options = savedObjectsClient.find.getCall(0).args[0]; - expect(options).toEqual({ perPage: 20, page: 1, searchFields: ['title'], type: ['foo'] }); + expect(options).toEqual({ perPage: 20, page: 1, searchFields: ['title'], type: ['foo'], defaultSearchOperator: 'OR' }); }); it('accepts the query parameter fields as a string', async () => { @@ -147,7 +147,7 @@ describe('GET /api/saved_objects/_find', () => { expect(savedObjectsClient.find.calledOnce).toBe(true); const options = savedObjectsClient.find.getCall(0).args[0]; - expect(options).toEqual({ perPage: 20, page: 1, fields: ['title'], type: ['foo'] }); + expect(options).toEqual({ perPage: 20, page: 1, fields: ['title'], type: ['foo'], defaultSearchOperator: 'OR' }); }); it('accepts the query parameter fields as an array', async () => { @@ -162,7 +162,7 @@ describe('GET /api/saved_objects/_find', () => { const options = savedObjectsClient.find.getCall(0).args[0]; expect(options).toEqual({ - perPage: 20, page: 1, fields: ['title', 'description'], type: ['foo'] + perPage: 20, page: 1, fields: ['title', 'description'], type: ['foo'], defaultSearchOperator: 'OR' }); }); @@ -177,7 +177,7 @@ describe('GET /api/saved_objects/_find', () => { expect(savedObjectsClient.find.calledOnce).toBe(true); const options = savedObjectsClient.find.getCall(0).args[0]; - expect(options).toEqual({ perPage: 20, page: 1, type: ['index-pattern'] }); + expect(options).toEqual({ perPage: 20, page: 1, type: ['index-pattern'], defaultSearchOperator: 'OR' }); }); it('accepts the query parameter type as an array', async () => { @@ -191,6 +191,6 @@ describe('GET /api/saved_objects/_find', () => { expect(savedObjectsClient.find.calledOnce).toBe(true); const options = savedObjectsClient.find.getCall(0).args[0]; - expect(options).toEqual({ perPage: 20, page: 1, type: ['index-pattern', 'visualization'] }); + expect(options).toEqual({ perPage: 20, page: 1, type: ['index-pattern', 'visualization'], defaultSearchOperator: 'OR' }); }); }); diff --git a/src/server/saved_objects/service/lib/repository.js b/src/server/saved_objects/service/lib/repository.js index 2f28081e96606..0508fd854b07c 100644 --- a/src/server/saved_objects/service/lib/repository.js +++ b/src/server/saved_objects/service/lib/repository.js @@ -281,6 +281,7 @@ export class SavedObjectsRepository { * @param {object} [options={}] * @property {(string|Array)} [options.type] * @property {string} [options.search] + * @property {string} [options.defaultSearchOperator] * @property {Array} [options.searchFields] - see Elasticsearch Simple Query String * Query field argument for more information * @property {integer} [options.page=1] @@ -295,6 +296,7 @@ export class SavedObjectsRepository { const { type, search, + defaultSearchOperator = 'OR', searchFields, page = 1, perPage = 20, @@ -327,6 +329,7 @@ export class SavedObjectsRepository { version: true, ...getSearchDsl(this._mappings, this._schema, { search, + defaultSearchOperator, searchFields, type, sortField, diff --git a/src/server/saved_objects/service/lib/repository.test.js b/src/server/saved_objects/service/lib/repository.test.js index 3771cdee44f7a..74c70fe1691f9 100644 --- a/src/server/saved_objects/service/lib/repository.test.js +++ b/src/server/saved_objects/service/lib/repository.test.js @@ -814,7 +814,7 @@ describe('SavedObjectsRepository', () => { } }); - it('passes mappings, schema, search, searchFields, type, sortField, and sortOrder to getSearchDsl', async () => { + it('passes mappings, schema, search, defaultSearchOperator, searchFields, type, sortField, and sortOrder to getSearchDsl', async () => { callAdminCluster.returns(namespacedSearchResults); const relevantOpts = { namespace: 'foo-namespace', @@ -823,6 +823,7 @@ describe('SavedObjectsRepository', () => { type: 'bar', sortField: 'name', sortOrder: 'desc', + defaultSearchOperator: 'AND', }; await savedObjectsRepository.find(relevantOpts); diff --git a/src/server/saved_objects/service/lib/search_dsl/query_params.js b/src/server/saved_objects/service/lib/search_dsl/query_params.js index 5bcff743c1082..47e5812e5eb2e 100644 --- a/src/server/saved_objects/service/lib/search_dsl/query_params.js +++ b/src/server/saved_objects/service/lib/search_dsl/query_params.js @@ -98,9 +98,10 @@ function getClauseForType(schema, namespace, type) { * @param {(string|Array)} type * @param {String} search * @param {Array} searchFields + * @param {String} defaultSearchOperator * @return {Object} */ -export function getQueryParams(mappings, schema, namespace, type, search, searchFields) { +export function getQueryParams(mappings, schema, namespace, type, search, searchFields, defaultSearchOperator) { const types = getTypes(mappings, type); const bool = { filter: [{ @@ -119,7 +120,8 @@ export function getQueryParams(mappings, schema, namespace, type, search, search ...getFieldsForTypes( searchFields, types - ) + ), + ...(defaultSearchOperator ? { default_operator: defaultSearchOperator } : {}), } } ]; diff --git a/src/server/saved_objects/service/lib/search_dsl/query_params.test.js b/src/server/saved_objects/service/lib/search_dsl/query_params.test.js index 45f241ba41883..7d9e26ecde924 100644 --- a/src/server/saved_objects/service/lib/search_dsl/query_params.test.js +++ b/src/server/saved_objects/service/lib/search_dsl/query_params.test.js @@ -716,4 +716,68 @@ describe('searchDsl/queryParams', () => { }); }); }); + + describe('type (plural, namespaced and global), search, defaultSearchOperator', () => { + it('supports defaultSearchOperator', () => { + expect(getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', ['saved', 'global'], 'foo', null, 'AND')) + .toEqual({ + query: { + bool: { + filter: [ + { + bool: { + minimum_should_match: 1, + should: [ + { + bool: { + must: [ + { + term: { + type: 'saved', + }, + }, + { + term: { + namespace: 'foo-namespace', + }, + }, + ], + }, + }, + { + bool: { + must: [ + { + term: { + type: 'global', + }, + }, + ], + must_not: [ + { + exists: { + field: 'namespace', + }, + }, + ], + }, + }, + ], + }, + }, + ], + must: [ + { + simple_query_string: { + all_fields: true, + default_operator: 'AND', + query: 'foo', + }, + }, + ], + }, + }, + }); + }); + }); }); diff --git a/src/server/saved_objects/service/lib/search_dsl/search_dsl.js b/src/server/saved_objects/service/lib/search_dsl/search_dsl.js index a0434aa2f9f8b..d6a224f4c3857 100644 --- a/src/server/saved_objects/service/lib/search_dsl/search_dsl.js +++ b/src/server/saved_objects/service/lib/search_dsl/search_dsl.js @@ -26,6 +26,7 @@ export function getSearchDsl(mappings, schema, options = {}) { const { type, search, + defaultSearchOperator, searchFields, sortField, sortOrder, @@ -41,7 +42,7 @@ export function getSearchDsl(mappings, schema, options = {}) { } return { - ...getQueryParams(mappings, schema, namespace, type, search, searchFields), + ...getQueryParams(mappings, schema, namespace, type, search, searchFields, defaultSearchOperator), ...getSortingParams(mappings, type, sortField, sortOrder), }; } diff --git a/src/server/saved_objects/service/lib/search_dsl/search_dsl.test.js b/src/server/saved_objects/service/lib/search_dsl/search_dsl.test.js index 18c1aa4e9a4fc..1ba780fc79ed0 100644 --- a/src/server/saved_objects/service/lib/search_dsl/search_dsl.test.js +++ b/src/server/saved_objects/service/lib/search_dsl/search_dsl.test.js @@ -55,6 +55,7 @@ describe('getSearchDsl', () => { type: 'foo', search: 'bar', searchFields: ['baz'], + defaultSearchOperator: 'AND', }; getSearchDsl(mappings, schema, opts); @@ -67,6 +68,7 @@ describe('getSearchDsl', () => { opts.type, opts.search, opts.searchFields, + opts.defaultSearchOperator, ); }); diff --git a/src/server/saved_objects/service/saved_objects_client.js b/src/server/saved_objects/service/saved_objects_client.js index f1dc4dfbbc190..a354067e6f702 100644 --- a/src/server/saved_objects/service/saved_objects_client.js +++ b/src/server/saved_objects/service/saved_objects_client.js @@ -139,6 +139,7 @@ export class SavedObjectsClient { * @param {object} [options={}] * @property {(string|Array)} [options.type] * @property {string} [options.search] + * @property {string} [options.defaultSearchOperator] * @property {Array} [options.searchFields] - see Elasticsearch Simple Query String * Query field argument for more information * @property {integer} [options.page=1] diff --git a/src/ui/public/courier/saved_object/saved_object_loader.js b/src/ui/public/courier/saved_object/saved_object_loader.js index c860fc0eaf336..511edd30ec549 100644 --- a/src/ui/public/courier/saved_object/saved_object_loader.js +++ b/src/ui/public/courier/saved_object/saved_object_loader.js @@ -118,6 +118,7 @@ export class SavedObjectLoader { perPage: size, page: 1, searchFields: ['title^3', 'description'], + defaultSearchOperator: 'AND', fields, }).then((resp) => { return { diff --git a/src/ui/public/saved_objects/components/saved_object_finder.js b/src/ui/public/saved_objects/components/saved_object_finder.js index 876b26d08a05e..395a24a3b5520 100644 --- a/src/ui/public/saved_objects/components/saved_object_finder.js +++ b/src/ui/public/saved_objects/components/saved_object_finder.js @@ -113,7 +113,8 @@ class SavedObjectFinderUI extends React.Component { search: filter ? `${filter}*` : undefined, page: 1, perPage: chrome.getUiSettingsClient().get('savedObjects:listingLimit'), - searchFields: ['title^3', 'description'] + searchFields: ['title^3', 'description'], + defaultSearchOperator: 'AND', }); if (this.props.savedObjectType === 'visualization' diff --git a/src/ui/public/saved_objects/saved_objects_client.js b/src/ui/public/saved_objects/saved_objects_client.js index c18a7276ab804..eba1acebabe4b 100644 --- a/src/ui/public/saved_objects/saved_objects_client.js +++ b/src/ui/public/saved_objects/saved_objects_client.js @@ -111,6 +111,7 @@ export class SavedObjectsClient { * @param {object} [options={}] * @property {string} options.type * @property {string} options.search + * @property {string} options.defaultSearchOperator * @property {string} options.searchFields - see Elasticsearch Simple Query String * Query field argument for more information * @property {integer} [options.page=1] diff --git a/test/functional/apps/dashboard/_dashboard_listing.js b/test/functional/apps/dashboard/_dashboard_listing.js index 58c2ac379fa3c..2545ef1b4dae9 100644 --- a/test/functional/apps/dashboard/_dashboard_listing.js +++ b/test/functional/apps/dashboard/_dashboard_listing.js @@ -122,6 +122,12 @@ export default function ({ getService, getPageObjects }) { const countOfDashboards = await PageObjects.dashboard.getCountOfDashboardsInListingTable(); expect(countOfDashboards).to.equal(1); }); + + it('is using AND operator', async function () { + await PageObjects.dashboard.searchForDashboardWithName('three words'); + const countOfDashboards = await PageObjects.dashboard.getCountOfDashboardsInListingTable(); + expect(countOfDashboards).to.equal(0); + }); }); describe('search by title', function () { diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.ts b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.ts index ccb93b6d326fb..d995fd89d9ebf 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.ts +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.ts @@ -136,6 +136,7 @@ export class SpacesSavedObjectsClient implements SavedObjectsClient { * @param {object} [options={}] * @property {(string|Array)} [options.type] * @property {string} [options.search] + * @property {string} [options.defaultSearchOperator] * @property {Array} [options.searchFields] - see Elasticsearch Simple Query String * Query field argument for more information * @property {integer} [options.page=1]