fix: use aggregation search instead of suggest query (#13861)

* fix: use searchAggregates instead of suggest query

* fix: aggregation fields
This commit is contained in:
karanh37 2023-11-07 15:22:01 +05:30 committed by GitHub
parent bc4991d72a
commit 51f3d31da6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 31 additions and 384 deletions

View File

@ -12,7 +12,6 @@
*/
import { t } from 'i18next';
import { isUndefined, uniq } from 'lodash';
import {
BasicConfig,
Fields,
@ -24,7 +23,6 @@ import AntdConfig from 'react-awesome-query-builder/lib/config/antd';
import { EntityFields, SuggestionField } from '../enums/AdvancedSearch.enum';
import { SearchIndex } from '../enums/search.enum';
import { getAggregateFieldOptions } from '../rest/miscAPI';
import { suggestQuery } from '../rest/searchAPI';
import { renderAdvanceSearchButtons } from '../utils/AdvancedSearchUtils';
import { getCombinedQueryFilterObject } from '../utils/ExplorePage/ExplorePageUtils';
@ -220,64 +218,28 @@ export const emptyJsonTree: JsonTree = {
*/
export const autocomplete: (args: {
searchIndex: SearchIndex | SearchIndex[];
entitySearchIndex: SearchIndex | SearchIndex[];
entityField: EntityFields;
suggestField?: SuggestionField;
}) => SelectFieldSettings['asyncFetch'] = ({
searchIndex,
suggestField,
entitySearchIndex,
entityField,
}) => {
const isUserAndTeamSearchIndex =
searchIndex.includes(SearchIndex.USER) ||
searchIndex.includes(SearchIndex.TEAM);
}) => SelectFieldSettings['asyncFetch'] = ({ searchIndex, entityField }) => {
return (search) => {
if (search) {
return suggestQuery({
query: search ?? '*',
searchIndex: searchIndex,
field: suggestField,
// fetch source if index is type of user or team and both
fetchSource: isUserAndTeamSearchIndex,
}).then((resp) => {
return {
values: uniq(resp).map(({ text, _source }) => {
// set displayName or name if index is type of user or team and both.
// else set the text
const name =
isUserAndTeamSearchIndex && !isUndefined(_source)
? _source?.displayName || _source.name
: text;
return getAggregateFieldOptions(
searchIndex,
entityField,
search ?? '',
JSON.stringify(getCombinedQueryFilterObject())
).then((response) => {
const buckets =
response.data.aggregations[`sterms#${entityField}`].buckets;
return {
value: name,
title: name,
};
}),
hasMore: false,
};
});
} else {
return getAggregateFieldOptions(
entitySearchIndex,
entityField,
'',
JSON.stringify(getCombinedQueryFilterObject())
).then((response) => {
const buckets =
response.data.aggregations[`sterms#${entityField}`].buckets;
return {
values: buckets.map((bucket) => ({
value: bucket.key,
title: bucket.label ?? bucket.key,
})),
hasMore: false,
};
});
}
return {
values: buckets.map((bucket) => ({
value: bucket.key,
title: bucket.label ?? bucket.key,
})),
hasMore: false,
};
});
// }
};
};
@ -306,8 +268,10 @@ const getCommonQueryBuilderFields = (
fieldSettings: {
asyncFetch: autocomplete({
searchIndex: [SearchIndex.USER, SearchIndex.TEAM],
entitySearchIndex: [SearchIndex.USER, SearchIndex.TEAM],
searchIndex: entitySearchIndex ?? [
SearchIndex.USER,
SearchIndex.TEAM,
],
entityField: EntityFields.OWNER,
}),
useAsyncSearch: true,
@ -320,8 +284,10 @@ const getCommonQueryBuilderFields = (
mainWidgetProps,
fieldSettings: {
asyncFetch: autocomplete({
searchIndex: [SearchIndex.TAG, SearchIndex.GLOSSARY],
entitySearchIndex,
searchIndex: entitySearchIndex ?? [
SearchIndex.TAG,
SearchIndex.GLOSSARY,
],
entityField: EntityFields.TAG,
}),
useAsyncSearch: true,
@ -334,8 +300,7 @@ const getCommonQueryBuilderFields = (
mainWidgetProps,
fieldSettings: {
asyncFetch: autocomplete({
searchIndex: [SearchIndex.TAG, SearchIndex.GLOSSARY],
entitySearchIndex,
searchIndex: entitySearchIndex ?? [SearchIndex.TAG],
entityField: EntityFields.TIER,
}),
useAsyncSearch: true,
@ -364,9 +329,7 @@ const getServiceQueryBuilderFields = (index: SearchIndex) => {
fieldSettings: {
asyncFetch: autocomplete({
searchIndex: index,
entitySearchIndex: index,
entityField: EntityFields.SERVICE,
suggestField: SuggestionField.SERVICE,
}),
useAsyncSearch: true,
},
@ -387,9 +350,7 @@ const tableQueryBuilderFields: Fields = {
fieldSettings: {
asyncFetch: autocomplete({
searchIndex: SearchIndex.TABLE,
entitySearchIndex: SearchIndex.TABLE,
entityField: EntityFields.DATABASE,
suggestField: SuggestionField.DATABASE,
}),
useAsyncSearch: true,
},
@ -402,9 +363,7 @@ const tableQueryBuilderFields: Fields = {
fieldSettings: {
asyncFetch: autocomplete({
searchIndex: SearchIndex.TABLE,
entitySearchIndex: SearchIndex.TABLE,
entityField: EntityFields.DATABASE_SCHEMA,
suggestField: SuggestionField.SCHEMA,
}),
useAsyncSearch: true,
},
@ -417,9 +376,7 @@ const tableQueryBuilderFields: Fields = {
fieldSettings: {
asyncFetch: autocomplete({
searchIndex: SearchIndex.TABLE,
entitySearchIndex: SearchIndex.TABLE,
entityField: EntityFields.COLUMN,
suggestField: SuggestionField.COLUMN,
}),
useAsyncSearch: true,
},

View File

@ -38,10 +38,10 @@ export enum EntityFields {
OWNER = 'displayName.keyword',
TAG = 'tags.tagFQN',
TIER = 'tier.tagFQN',
SERVICE = 'service.name',
DATABASE = 'database.name',
DATABASE_SCHEMA = 'databaseSchema.name',
COLUMN = 'columns.name',
SERVICE = 'service.name.keyword',
DATABASE = 'database.name.keyword',
DATABASE_SCHEMA = 'databaseSchema.name.keyword',
COLUMN = 'columns.name.keyword',
CHART = 'charts.displayName.keyword',
TASK = 'tasks.displayName.keyword',
}

View File

@ -51,7 +51,6 @@ export const DataInsightContext = createContext<DataInsightContextType>(
);
const fetchTeamSuggestions = autocomplete({
searchIndex: SearchIndex.TEAM,
entitySearchIndex: SearchIndex.TEAM,
entityField: EntityFields.OWNER,
});

View File

@ -83,113 +83,9 @@ const mockTableSearchResponse = {
},
};
const mockSuggestUserResponse = {
suggest: {
'metadata-suggest': [
{
text: 'a',
offset: 0,
length: 1,
options: [
{
text: 'Aaron Johnson',
_index: 'user_search_index',
_type: '_doc',
_id: '2cae227c-e2c4-487c-b52c-a96ae242d90d',
_score: 10.0,
_source: {
id: '2cae227c-e2c4-487c-b52c-a96ae242d90d',
name: 'aaron_johnson0',
fullyQualifiedName: 'aaron_johnson0',
displayName: 'Aaron Johnson',
version: 0.1,
updatedAt: 1661336540995,
updatedBy: 'anonymous',
email: 'aaron_johnson0@gmail.com',
href: 'http://localhost:8585/api/v1/users/2cae227c-e2c4-487c-b52c-a96ae242d90d',
isAdmin: false,
deleted: false,
roles: [],
inheritedRoles: [],
entityType: 'user',
teams: null,
some: {
nested: {
nullValue: null,
},
},
},
},
],
},
],
},
};
describe('searchAPI tests', () => {
beforeEach(() => jest.resetModules());
it('suggestQuery should return object and aggregations', async () => {
jest.mock('./index', () => ({
get: jest
.fn()
.mockImplementation(() =>
Promise.resolve({ data: mockTableSearchResponse })
),
}));
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { searchQuery } = require('./searchAPI');
const res = await searchQuery({ searchIndex: SearchIndex.TABLE });
expect(res.hits.total.value).toBe(10_000);
expect(res.hits.hits).toHaveLength(1);
expect(res.hits.hits[0]._index).toEqual(SearchIndex.TABLE);
expect(res.hits.hits[0]._source).toEqual(
expect.objectContaining({
id: '9b30a945-239a-4cb7-93b0-f1b7425aed41',
name: 'raw_product_catalog',
fullyQualifiedName:
'sample_data.ecommerce_db.shopify.raw_product_catalog',
description:
'This is a raw product catalog table contains the product listing, price, seller etc.. represented in our online DB. ',
version: 0.1,
updatedAt: 1661336543968,
updatedBy: 'anonymous',
href: 'http://localhost:8585/api/v1/tables/9b30a945-239a-4cb7-93b0-f1b7425aed41',
tableType: 'Regular',
})
);
expect(res.aggregations).toEqual(
expect.objectContaining({
EntityType: {
buckets: expect.arrayContaining([
{
key: 'table',
doc_count: 10960,
},
]),
},
ServiceName: {
buckets: expect.arrayContaining([
{
key: 'trino',
doc_count: 10924,
},
{
key: 'sample_data',
doc_count: 36,
},
]),
},
Tags: {
buckets: [],
},
})
);
});
it('searchQuery should not return nulls', async () => {
jest.mock('./index', () => ({
get: jest
@ -224,70 +120,4 @@ describe('searchAPI tests', () => {
expect(res.hits.hits[0]._source.type).toBe('table');
});
it('suggestQuery should return object and text', async () => {
jest.mock('./index', () => ({
get: jest
.fn()
.mockImplementation(() =>
Promise.resolve({ data: mockSuggestUserResponse })
),
}));
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { suggestQuery } = require('./searchAPI');
const res = await suggestQuery({ searchIndex: SearchIndex.USER });
expect(res).toEqual([
expect.objectContaining({
_index: SearchIndex.USER,
_source: expect.objectContaining({
id: '2cae227c-e2c4-487c-b52c-a96ae242d90d',
name: 'aaron_johnson0',
fullyQualifiedName: 'aaron_johnson0',
displayName: 'Aaron Johnson',
version: 0.1,
updatedAt: 1661336540995,
updatedBy: 'anonymous',
email: 'aaron_johnson0@gmail.com',
href: 'http://localhost:8585/api/v1/users/2cae227c-e2c4-487c-b52c-a96ae242d90d',
isAdmin: false,
deleted: false,
roles: [],
inheritedRoles: [],
entityType: 'user',
}),
}),
]);
});
it('suggestQuery should not return nulls', async () => {
jest.mock('./index', () => ({
get: jest
.fn()
.mockImplementation(() =>
Promise.resolve({ data: mockSuggestUserResponse })
),
}));
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { suggestQuery } = require('./searchAPI');
const res = await suggestQuery({ searchIndex: SearchIndex.USER });
// Deep checking for null values
expect(flatten(res[0]._source).filter(isNull)).toHaveLength(0);
});
it('suggestQuery should have type field', async () => {
jest.mock('./index', () => ({
get: jest
.fn()
.mockImplementation(() =>
Promise.resolve({ data: mockSuggestUserResponse })
),
}));
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { suggestQuery } = require('./searchAPI');
const res = await suggestQuery({ searchIndex: SearchIndex.USER });
expect(res[0]._source.type).toBe('user');
});
});

View File

@ -19,12 +19,9 @@ import {
Aggregations,
DataInsightSearchResponse,
KeysOfUnion,
RawSuggestResponse,
SearchIndexSearchSourceMapping,
SearchRequest,
SearchResponse,
SuggestRequest,
SuggestResponse,
} from '../interface/search.interface';
import { omitDeep } from '../utils/APIUtils';
import { getQueryWithSlash } from '../utils/SearchUtils';
@ -290,139 +287,3 @@ export const searchQueryDataInsight = async (
res.data
) as unknown as DataInsightSearchResponse;
};
/**
* Formats a response from {@link rawSuggestQuery}
*
* Warning: avoid this pattern unless applying custom transformation to the raw response!
* ```ts
* const response = await rawSuggestQuery(req);
* const data = formatSuggestQueryResponse(response.data);
* ```
*
* Instead use the shorthand {@link suggestQuery}
* ```ts
* const data = suggestQuery(req);
* ```
*
* @param data
*/
export const formatSuggestQueryResponse = <
SI extends SearchIndex | SearchIndex[],
TIncludeFields extends KeysOfUnion<
SearchIndexSearchSourceMapping[SI extends Array<SearchIndex>
? SI[number]
: SI]
>
>(
data: RawSuggestResponse<
SI extends Array<SearchIndex> ? SI[number] : SI,
TIncludeFields
>
): SuggestResponse<
SI extends Array<SearchIndex> ? SI[number] : SI,
TIncludeFields
> => {
let _data;
_data = data;
// Elasticsearch responses use 'null' for missing values, we want undefined
_data = omitDeep<
SuggestResponse<
SI extends Array<SearchIndex> ? SI[number] : SI,
TIncludeFields
>
>(_data.suggest['metadata-suggest'][0].options, isNil);
/* Elasticsearch objects use `entityType` to track their type, but the EntityReference interface uses `type`
This copies `entityType` into `type` (if `entityType` exists) so responses implement EntityReference */
_data = _data.map((datum) =>
'_source' in datum
? 'entityType' in datum._source
? {
...datum,
_source: {
...(datum._source as SearchIndexSearchSourceMapping[SI extends Array<SearchIndex>
? SI[number]
: SI]),
type: (
datum._source as SearchIndexSearchSourceMapping[SI extends Array<SearchIndex>
? SI[number]
: SI]
).entityType,
},
}
: datum
: datum
);
return _data;
};
/**
* Executes a request to /search/suggest, returning the raw response.
* Warning: Only call this function directly in special cases. Otherwise use {@link suggestQuery}
*
* @param req Request object
*/
export const rawSuggestQuery = <
SI extends SearchIndex | SearchIndex[],
TIncludeFields extends KeysOfUnion<
SearchIndexSearchSourceMapping[SI extends Array<SearchIndex>
? SI[number]
: SI]
>
>(
req: SuggestRequest<SI, TIncludeFields>
): Promise<
AxiosResponse<
RawSuggestResponse<
SI extends Array<SearchIndex> ? SI[number] : SI,
TIncludeFields
>
>
> => {
const { query, searchIndex, field, fetchSource } = req;
return APIClient.get<
RawSuggestResponse<
SI extends Array<SearchIndex> ? SI[number] : SI,
TIncludeFields
>
>('/search/suggest', {
params: {
q: query,
field,
index: getSearchIndexParam(searchIndex),
fetch_source: fetchSource,
include_source_fields: req.fetchSource ? req.includeFields : undefined,
},
});
};
/**
* Access point for the Suggestion API.
* Executes a request to /search/suggest, returning a formatted response.
*
* @param req Request object
*/
export const suggestQuery = async <
SI extends SearchIndex | SearchIndex[],
TIncludeFields extends KeysOfUnion<
SearchIndexSearchSourceMapping[SI extends Array<SearchIndex>
? SI[number]
: SI]
>
>(
req: SuggestRequest<SI, TIncludeFields>
): Promise<
SuggestResponse<
SI extends Array<SearchIndex> ? SI[number] : SI,
TIncludeFields
>
> => {
const res = await rawSuggestQuery(req);
return formatSuggestQueryResponse(res.data);
};