Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export interface SavedObjectsFindOptions
| [rootSearchFields?](./kibana-plugin-core-public.savedobjectsfindoptions.rootsearchfields.md) | string\[\] | <i>(Optional)</i> The fields to perform the parsed query against. Unlike the <code>searchFields</code> argument, these are expected to be root fields and will not be modified. If used in conjunction with <code>searchFields</code>, both are concatenated together. |
| [search?](./kibana-plugin-core-public.savedobjectsfindoptions.search.md) | string | <i>(Optional)</i> Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String <code>query</code> argument for more information |
| [searchAfter?](./kibana-plugin-core-public.savedobjectsfindoptions.searchafter.md) | estypes.Id\[\] | <i>(Optional)</i> Use the sort values from the previous page to retrieve the next page of results. |
| [searchFields?](./kibana-plugin-core-public.savedobjectsfindoptions.searchfields.md) | string\[\] | <i>(Optional)</i> The fields to perform the parsed query against. See Elasticsearch Simple Query String <code>fields</code> argument for more information |
| [searchFields?](./kibana-plugin-core-public.savedobjectsfindoptions.searchfields.md) | string\[\] \| Record&lt;string, string\[\]&gt; | <i>(Optional)</i> The fields to perform the parsed query against. See Elasticsearch Simple Query String <code>fields</code> argument for more information Can be either an array of string, in which case the fields will be used for all specified types, or a record specifying the search fields per type. |
| [sortField?](./kibana-plugin-core-public.savedobjectsfindoptions.sortfield.md) | string | <i>(Optional)</i> |
| [sortOrder?](./kibana-plugin-core-public.savedobjectsfindoptions.sortorder.md) | estypes.SearchSortOrder | <i>(Optional)</i> |
| [type](./kibana-plugin-core-public.savedobjectsfindoptions.type.md) | string \| string\[\] | |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

## SavedObjectsFindOptions.searchFields property

The fields to perform the parsed query against. See Elasticsearch Simple Query String `fields` argument for more information
The fields to perform the parsed query against. See Elasticsearch Simple Query String `fields` argument for more information Can be either an array of string, in which case the fields will be used for all specified types, or a record specifying the search fields per type.

<b>Signature:</b>

```typescript
searchFields?: string[];
searchFields?: string[] | Record<string, string[]>;
```
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export interface SavedObjectsFindOptions
| [rootSearchFields?](./kibana-plugin-core-server.savedobjectsfindoptions.rootsearchfields.md) | string\[\] | <i>(Optional)</i> The fields to perform the parsed query against. Unlike the <code>searchFields</code> argument, these are expected to be root fields and will not be modified. If used in conjunction with <code>searchFields</code>, both are concatenated together. |
| [search?](./kibana-plugin-core-server.savedobjectsfindoptions.search.md) | string | <i>(Optional)</i> Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String <code>query</code> argument for more information |
| [searchAfter?](./kibana-plugin-core-server.savedobjectsfindoptions.searchafter.md) | estypes.Id\[\] | <i>(Optional)</i> Use the sort values from the previous page to retrieve the next page of results. |
| [searchFields?](./kibana-plugin-core-server.savedobjectsfindoptions.searchfields.md) | string\[\] | <i>(Optional)</i> The fields to perform the parsed query against. See Elasticsearch Simple Query String <code>fields</code> argument for more information |
| [searchFields?](./kibana-plugin-core-server.savedobjectsfindoptions.searchfields.md) | string\[\] \| Record&lt;string, string\[\]&gt; | <i>(Optional)</i> The fields to perform the parsed query against. See Elasticsearch Simple Query String <code>fields</code> argument for more information Can be either an array of string, in which case the fields will be used for all specified types, or a record specifying the search fields per type. |
| [sortField?](./kibana-plugin-core-server.savedobjectsfindoptions.sortfield.md) | string | <i>(Optional)</i> |
| [sortOrder?](./kibana-plugin-core-server.savedobjectsfindoptions.sortorder.md) | estypes.SearchSortOrder | <i>(Optional)</i> |
| [type](./kibana-plugin-core-server.savedobjectsfindoptions.type.md) | string \| string\[\] | |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

## SavedObjectsFindOptions.searchFields property

The fields to perform the parsed query against. See Elasticsearch Simple Query String `fields` argument for more information
The fields to perform the parsed query against. See Elasticsearch Simple Query String `fields` argument for more information Can be either an array of string, in which case the fields will be used for all specified types, or a record specifying the search fields per type.

<b>Signature:</b>

```typescript
searchFields?: string[];
searchFields?: string[] | Record<string, string[]>;
```
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ The default search field to use for this type. Defaults to `id`<!-- -->.
```typescript
defaultSearchField?: string;
```

## Remarks

the field must be mapped as `text` and not `keyword`

2 changes: 1 addition & 1 deletion src/core/public/public.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1429,7 +1429,7 @@ export interface SavedObjectsFindOptions {
rootSearchFields?: string[];
search?: string;
searchAfter?: estypes.Id[];
searchFields?: string[];
searchFields?: string[] | Record<string, string[]>;
// (undocumented)
sortField?: string;
// (undocumented)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3375,14 +3375,6 @@ describe('SavedObjectsRepository', () => {
await test({ type: '', namespaces: ['some-ns'], typeToNamespacesMap: new Map() });
});

it(`throws when searchFields is defined but not an array`, async () => {
await expect(
// @ts-expect-error searchFields is an array
savedObjectsRepository.find({ type, searchFields: 'string' })
).rejects.toThrowError('options.searchFields must be an array');
expect(client.search).not.toHaveBeenCalled();
});

it(`throws when fields is defined but not an array`, async () => {
// @ts-expect-error fields is an array
await expect(savedObjectsRepository.find({ type, fields: 'string' })).rejects.toThrowError(
Expand Down
4 changes: 0 additions & 4 deletions src/core/server/saved_objects/service/lib/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -892,10 +892,6 @@ export class SavedObjectsRepository {
return SavedObjectsUtils.createEmptyFindResponse<T, A>(options);
}

if (searchFields && !Array.isArray(searchFields)) {
throw SavedObjectsErrorHelpers.createBadRequestError('options.searchFields must be an array');
}

if (fields && !Array.isArray(fields)) {
throw SavedObjectsErrorHelpers.createBadRequestError('options.fields must be an array');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import * as esKuery from '@kbn/es-query';

type KueryNode = any;

import { ALL_NAMESPACES_STRING, DEFAULT_NAMESPACE_STRING } from '../utils';
Expand Down Expand Up @@ -432,19 +433,27 @@ describe('#getQueryParams', () => {
});

describe('`searchFields` and `rootSearchFields` parameters', () => {
const getExpectedFields = (searchFields: string[], typeOrTypes: string | string[]) => {
const getExpectedFields = (
searchFields: string[] | Record<string, string[]>,
typeOrTypes: string | string[]
) => {
const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes];
return searchFields.map((x) => types.map((y) => `${y}.${x}`)).flat();
const allSearchFields = Array.isArray(searchFields)
? searchFields
: [...new Set(Object.values(searchFields).flat())];
return allSearchFields.map((x) => types.map((y) => `${y}.${x}`)).flat();
};

const test = ({
searchFields,
rootSearchFields,
typeSubsets = ALL_TYPE_SUBSETS,
}: {
searchFields?: string[];
searchFields?: string[] | Record<string, string[]>;
rootSearchFields?: string[];
typeSubsets?: Array<string | string[]>;
}) => {
for (const typeOrTypes of ALL_TYPE_SUBSETS) {
for (const typeOrTypes of typeSubsets) {
const result = getQueryParams({
registry,
type: typeOrTypes,
Expand Down Expand Up @@ -520,6 +529,10 @@ describe('#getQueryParams', () => {
it('supports search fields and raw search fields', () => {
test({ searchFields: ['title'], rootSearchFields: ['_id'] });
});

it('supports per-type searchFields', () => {
test({ searchFields: { pending: ['title'], saved: ['desc'] }, typeSubsets: [ALL_TYPES] });
});
});

describe('`defaultSearchOperator` parameter', () => {
Expand Down Expand Up @@ -559,7 +572,7 @@ describe('#getQueryParams', () => {
type,
}: {
search?: string;
searchFields?: string[];
searchFields?: string[] | Record<string, string[]>;
type?: string[];
}) =>
getQueryParams({
Expand Down Expand Up @@ -599,7 +612,7 @@ describe('#getQueryParams', () => {
const mppClauses = shouldClauses.slice(1);

expect(mppClauses.map((clause: any) => Object.keys(clause.match_phrase_prefix)[0])).toEqual(
['saved.title', 'pending.title', 'saved.desc', 'pending.desc']
['saved.title', 'saved.desc', 'pending.title', 'pending.desc']
);
});

Expand Down Expand Up @@ -666,6 +679,45 @@ describe('#getQueryParams', () => {
{ 'saved.description': { query: 'foo', boost: 1 } },
]);
});

it('supports specifying per-type searchFields ', () => {
const result = getQueryParamForSearch({
search: searchQuery,
type: ['saved', 'pending'],
searchFields: {
saved: ['title', 'desc'],
pending: ['title'],
},
});
const shouldClauses = result.query.bool.should;

expect(shouldClauses.length).toBe(4);

const mppClauses = shouldClauses.slice(1);

expect(mppClauses.map((clause: any) => Object.keys(clause.match_phrase_prefix)[0])).toEqual(
['saved.title', 'saved.desc', 'pending.title']
);
});

it('fallbacks to `defaultSearchField` when fields are not specified for a type', () => {
const result = getQueryParamForSearch({
search: searchQuery,
type: ['saved', 'pending'],
searchFields: {
saved: ['desc'],
},
});
const shouldClauses = result.query.bool.should;

expect(shouldClauses.length).toBe(3);

const mppClauses = shouldClauses.slice(1);

expect(mppClauses.map((clause: any) => Object.keys(clause.match_phrase_prefix)[0])).toEqual(
['saved.desc', 'pending.title']
);
});
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import * as esKuery from '@kbn/es-query';

type KueryNode = any;

import { ISavedObjectTypeRegistry } from '../../../saved_objects_type_registry';
Expand Down Expand Up @@ -132,7 +133,7 @@ interface QueryParams {
typeToNamespacesMap?: Map<string, string[] | undefined>;
search?: string;
defaultSearchOperator?: SearchOperator;
searchFields?: string[];
searchFields?: string[] | Record<string, string[]>;
rootSearchFields?: string[];
hasReference?: HasReferenceQueryParams | HasReferenceQueryParams[];
hasReferenceOperator?: SearchOperator;
Expand Down Expand Up @@ -212,6 +213,20 @@ export function getQueryParams({
hasReference = [hasReference];
}

const allSearchFields =
!searchFields || Array.isArray(searchFields)
? searchFields
: [...new Set(Object.values(searchFields).flat())];

const searchFieldMap = Array.isArray(searchFields)
? types.reduce((record, t) => {
return {
...record,
[t]: searchFields as string[],
};
}, {} as Record<string, string[]>)
: searchFields;

const bool: any = {
filter: [
...(kueryNode != null ? [esKuery.toElasticsearchQuery(kueryNode)] : []),
Expand All @@ -235,15 +250,15 @@ export function getQueryParams({
const simpleQueryStringClause = getSimpleQueryStringClause({
search,
types,
searchFields,
searchFields: allSearchFields,
rootSearchFields,
defaultSearchOperator,
});

if (useMatchPhrasePrefix) {
bool.should = [
simpleQueryStringClause,
...getMatchPhrasePrefixClauses({ search, searchFields, types, registry }),
...getMatchPhrasePrefixClauses({ search, searchFields: searchFieldMap, types, registry }),
];
bool.minimum_should_match = 1;
} else {
Expand All @@ -267,7 +282,7 @@ const getMatchPhrasePrefixClauses = ({
types,
}: {
search: string;
searchFields?: string[];
searchFields?: Record<string, string[]>;
types: string[];
registry: ISavedObjectTypeRegistry;
}) => {
Expand All @@ -292,34 +307,28 @@ interface FieldWithBoost {
}

const getMatchPhrasePrefixFields = ({
searchFields = [],
searchFields = {},
types,
registry,
}: {
searchFields?: string[];
searchFields?: Record<string, string[]>;
types: string[];
registry: ISavedObjectTypeRegistry;
}): FieldWithBoost[] => {
const output: FieldWithBoost[] = [];

searchFields = searchFields.filter((field) => field !== '*');
let fields: string[];
if (searchFields.length === 0) {
fields = types.reduce((typeFields, type) => {
let allFields: string[] = [];
for (const type of types) {
let typeFields = (searchFields[type] ?? []).filter((field) => field !== '*');
if (typeFields.length === 0) {
const defaultSearchField = registry.getType(type)?.management?.defaultSearchField;
if (defaultSearchField) {
return [...typeFields, `${type}.${defaultSearchField}`];
typeFields = [defaultSearchField];
}
return typeFields;
}, [] as string[]);
} else {
fields = [];
for (const field of searchFields) {
fields = fields.concat(types.map((type) => `${type}.${field}`));
}
allFields = [...allFields, ...typeFields.map((typeField) => `${type}.${typeField}`)];
}

fields.forEach((rawField) => {
const output: FieldWithBoost[] = [];
allFields.forEach((rawField) => {
const [field, rawBoost] = rawField.split('^');
let boost: number = 1;
if (rawBoost) {
Expand All @@ -337,6 +346,7 @@ const getMatchPhrasePrefixFields = ({
boost,
});
});

return output;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ interface GetSearchDslOptions {
type: string | string[];
search?: string;
defaultSearchOperator?: SearchOperator;
searchFields?: string[];
searchFields?: string[] | Record<string, string[]>;
rootSearchFields?: string[];
searchAfter?: estypes.Id[];
sortField?: string;
Expand Down
10 changes: 8 additions & 2 deletions src/core/server/saved_objects/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,12 @@ export interface SavedObjectsFindOptions {
fields?: string[];
/** Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String `query` argument for more information */
search?: string;
/** The fields to perform the parsed query against. See Elasticsearch Simple Query String `fields` argument for more information */
searchFields?: string[];
/**
* The fields to perform the parsed query against. See Elasticsearch Simple Query String `fields` argument for more information
* Can be either an array of string, in which case the fields will be used for all specified types, or a record specifying
* the search fields per type.
*/
searchFields?: string[] | Record<string, string[]>;
/**
* Use the sort values from the previous page to retrieve the next page of results.
*/
Expand Down Expand Up @@ -371,6 +375,8 @@ export interface SavedObjectsTypeManagementDefinition<Attributes = any> {
visibleInManagement?: boolean;
/**
* The default search field to use for this type. Defaults to `id`.
*
* @remarks the field must be mapped as `text` and not `keyword`
*/
defaultSearchField?: string;
/**
Expand Down
2 changes: 1 addition & 1 deletion src/core/server/server.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2352,7 +2352,7 @@ export interface SavedObjectsFindOptions {
rootSearchFields?: string[];
search?: string;
searchAfter?: estypes.Id[];
searchFields?: string[];
searchFields?: string[] | Record<string, string[]>;
// (undocumented)
sortField?: string;
// (undocumented)
Expand Down
10 changes: 4 additions & 6 deletions src/plugins/saved_objects_management/server/routes/find.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,19 +61,17 @@ export const registerFindRoute = (
);

const client = getClient({ includedHiddenTypes });
const searchFields = new Set<string>();

const searchFields: Record<string, string[]> = {};
importAndExportableTypes.forEach((type) => {
const searchField = managementService.getDefaultSearchField(type);
if (searchField) {
searchFields.add(searchField);
}
const searchField = managementService.getDefaultSearchField(type) ?? 'id';
searchFields[type] = [searchField];
});

const findResponse = await client.find<any>({
...query,
fields: undefined,
searchFields: [...searchFields],
searchFields,
});

const enhancedSavedObjects = findResponse.saved_objects
Expand Down
Loading