From 29e58672a19bef9dbacc316c44118bfdf1231dbf Mon Sep 17 00:00:00 2001 From: Nicholas Ung Date: Fri, 26 Jan 2024 14:12:56 -0800 Subject: [PATCH] Feature: Add Ability to Select a Search Pipeline in Comparison Tool (#352) * Add search pipeline parameters to index and route files Signed-off-by: Nicholas Ung * Add search pipeline dropdown Signed-off-by: Nicholas Ung * Integrate option to select search pipeline Signed-off-by: Nicholas Ung * Update tests and snapshots Signed-off-by: Nicholas Ung * Update get pipelines endpoint Signed-off-by: Nicholas Ung * Update .babelrc Signed-off-by: Nicholas Ung * Update test snapshots Signed-off-by: Nicholas Ung * Add id attribute to search bar (#338) * Add id to search bar Signed-off-by: Nicholas Ung * Add keys to getDlTmpl component Signed-off-by: Nicholas Ung * Change enclosing tag to span instead of div Signed-off-by: Nicholas Ung --------- Signed-off-by: Nicholas Ung * Update .babelrc Signed-off-by: Nicholas Ung * Add ability to select _none option Signed-off-by: Nicholas Ung * Update test coverages Signed-off-by: Nicholas Ung * Update variables in pipeline selection Signed-off-by: Nicholas Ung --------- Signed-off-by: Nicholas Ung --- .babelrc | 1 + common/index.ts | 3 + public/components/query_compare/home.tsx | 18 +- .../query_compare/search_result/index.tsx | 7 +- .../result_components/result_components.tsx | 4 +- .../__snapshots__/search_config.test.tsx.snap | 531 +++++++-- .../search_configs.test.tsx.snap | 1062 +++++++++++++---- .../__tests__/search_config.test.tsx | 8 + .../search_configs/search_config.tsx | 74 +- .../search_configs/search_configs.tsx | 8 + public/contexts/index.tsx | 15 + server/metrics/index.ts | 1 + server/routes/dsl_route.ts | 87 +- 13 files changed, 1411 insertions(+), 408 deletions(-) diff --git a/.babelrc b/.babelrc index d9073a59..2a3340e9 100644 --- a/.babelrc +++ b/.babelrc @@ -14,3 +14,4 @@ ["@babel/plugin-transform-runtime", { "regenerator": true }] ] } + diff --git a/common/index.ts b/common/index.ts index fcb74f26..a670499d 100644 --- a/common/index.ts +++ b/common/index.ts @@ -8,6 +8,9 @@ export const PLUGIN_NAME = 'Search Relevance'; export enum ServiceEndpoints { GetIndexes = '/api/relevancy/search/indexes', + GetPipelines = '/api/relevancy/search/pipelines', GetSearchResults = '/api/relevancy/search', GetStats = '/api/relevancy/stats', } + +export const SEARCH_API = '/_search'; diff --git a/public/components/query_compare/home.tsx b/public/components/query_compare/home.tsx index 26582fa4..0ef866c0 100644 --- a/public/components/query_compare/home.tsx +++ b/public/components/query_compare/home.tsx @@ -4,7 +4,7 @@ */ import React, { useEffect } from 'react'; -import { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; +import { NavigationPublicPluginStart } from '../../../../../src/plugins/navigation/public'; import { CoreStart, ChromeBreadcrumb } from '../../../../../src/core/public'; import '../../ace-themes/sql_console'; import { CreateIndex } from './create_index'; @@ -25,7 +25,6 @@ interface QueryExplorerProps { setToast: (title: string, color?: string, text?: any, side?: string) => void; chrome: CoreStart['chrome']; } - export const Home = ({ parentBreadCrumbs, notifications, @@ -35,19 +34,28 @@ export const Home = ({ setToast, chrome, }: QueryExplorerProps) => { - const { documentsIndexes, setDocumentsIndexes, showFlyout } = useSearchRelevanceContext(); + const { + documentsIndexes, + setDocumentsIndexes, + pipelines, + setPipelines, + showFlyout, + } = useSearchRelevanceContext(); useEffect(() => { setBreadcrumbs([...parentBreadCrumbs]); }, [setBreadcrumbs, parentBreadCrumbs]); - // Get Indexes + // Get Indexes and Pipelines useEffect(() => { http.get(ServiceEndpoints.GetIndexes).then((res: DocumentsIndex[]) => { setDocumentsIndexes(res); }); - }, [http, setDocumentsIndexes]); + http.get(ServiceEndpoints.GetPipelines).then((res: {}) => { + setPipelines(res); + }); + }, [http, setDocumentsIndexes, setPipelines]); return ( <>
diff --git a/public/components/query_compare/search_result/index.tsx b/public/components/query_compare/search_result/index.tsx index 4d27fb10..7e6de1ad 100644 --- a/public/components/query_compare/search_result/index.tsx +++ b/public/components/query_compare/search_result/index.tsx @@ -41,6 +41,8 @@ export const SearchResult = ({ http }: SearchResultProps) => { updateComparedResult2, selectedIndex1, selectedIndex2, + pipeline1, + pipeline2, } = useSearchRelevanceContext(); const onClickSearch = () => { @@ -86,6 +88,7 @@ export const SearchResult = ({ http }: SearchResultProps) => { const handleQuery = ( queryError: QueryError, selectedIndex: string, + pipeline: string, jsonQuery: any, updateComparedResult: (result: SearchResults) => void, setQueryResult: React.Dispatch>, @@ -97,7 +100,7 @@ export const SearchResult = ({ http }: SearchResultProps) => { updateComparedResult({} as any); } else if (!queryError.queryString.length && !queryError.selectIndex) { setQueryError(initialQueryErrorState); - return { index: selectedIndex, ...jsonQuery }; + return { index: selectedIndex, pipeline, ...jsonQuery }; } }; @@ -106,6 +109,7 @@ export const SearchResult = ({ http }: SearchResultProps) => { query1: handleQuery( queryErrors[0], selectedIndex1, + pipeline1, jsonQueries[0], updateComparedResult1, setQueryResult1, @@ -114,6 +118,7 @@ export const SearchResult = ({ http }: SearchResultProps) => { query2: handleQuery( queryErrors[1], selectedIndex2, + pipeline2, jsonQueries[1], updateComparedResult2, setQueryResult2, diff --git a/public/components/query_compare/search_result/result_components/result_components.tsx b/public/components/query_compare/search_result/result_components/result_components.tsx index 8844caf1..c64886e9 100644 --- a/public/components/query_compare/search_result/result_components/result_components.tsx +++ b/public/components/query_compare/search_result/result_components/result_components.tsx @@ -29,9 +29,7 @@ const InitialState = () => { No results} - body={ -

Add at least one query to display search results.

- } + body={

Add at least one query to display search results.

} /> ); diff --git a/public/components/query_compare/search_result/search_components/__tests__/__snapshots__/search_config.test.tsx.snap b/public/components/query_compare/search_result/search_components/__tests__/__snapshots__/search_config.test.tsx.snap index 36d1e463..5f19165d 100644 --- a/public/components/query_compare/search_result/search_components/__tests__/__snapshots__/search_config.test.tsx.snap +++ b/public/components/query_compare/search_result/search_components/__tests__/__snapshots__/search_config.test.tsx.snap @@ -3,6 +3,7 @@ exports[`Flyout component Renders flyout component 1`] = ` - +
-
- - - -
-
- +
-
- - - - + + + + +
+ + + + + +
+
+
+
+
+ +
+
+ +
+ + +
+ +
+
+ + + +
+
+
- - - - - +
+ +
+ +
+
+ +
+ +
+ + + +
+
+
+
+ + +
+ + +
+ Optional
- +
- - -
+ + + - + - +
-
- - - -
-
- +
-
- - - - + + + + +
+ + + + + +
+
+
+
+
+ +
+
+ +
+ + +
+ +
+
+ + + +
+
+
- - - - - +
+ +
+ +
+
+ +
+ +
+ + + +
+
+
+
+ + +
+ + +
+ Optional
- +
- - - +
+ + -
+ - +
-
- - - -
-
- +
-
- - - - + + + + +
+ + + + + +
+
+
+
+
+ +
+
+ +
+ + +
+ +
+
+ + + +
+
+
- - - - - +
+ +
+ +
+
+ +
+ +
+ + + +
+
+
+
+ + +
+ + +
+ Optional
- +
- - - +
+ + -
+ { it('Renders flyout component', async () => { const setQueryString = jest.fn(); const setSelectedIndex = jest.fn(); + const setPipeline = jest.fn(); const setQueryError = jest.fn(); const wrapper = mount( @@ -27,6 +28,8 @@ describe('Flyout component', () => { setQueryString={setQueryString} selectedIndex={''} setSelectedIndex={setSelectedIndex} + pipeline={''} + setPipeline={setPipeline} queryError={initialQueryErrorState} setQueryError={setQueryError} /> @@ -39,9 +42,14 @@ describe('Flyout component', () => { wrapper.find('EuiCodeEditor').prop('onChange')?.({ target: { value: '' } }); wrapper.find('EuiSelect').prop('onChange')?.({ target: {} }); wrapper.find('EuiSelect').prop('onBlur')?.({ target: {} }); + wrapper.find('EuiComboBox').prop('onChange')?.({ target: { selectedPipelineOptions: [] } }); + wrapper.find('EuiComboBox').prop('onChange')?.({ + target: { selectedPipelineOptions: [{ label: '_none' }] }, + }); }); expect(setQueryString).toHaveBeenCalledTimes(1); expect(setSelectedIndex).toHaveBeenCalledTimes(1); + expect(setPipeline).toHaveBeenCalledTimes(2); expect(setQueryError).toHaveBeenCalledTimes(3); }); }); diff --git a/public/components/query_compare/search_result/search_components/search_configs/search_config.tsx b/public/components/query_compare/search_result/search_components/search_configs/search_config.tsx index 58180d0d..654ad012 100644 --- a/public/components/query_compare/search_result/search_components/search_configs/search_config.tsx +++ b/public/components/query_compare/search_result/search_components/search_configs/search_config.tsx @@ -12,6 +12,9 @@ import { EuiCodeEditor, EuiText, EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiComboBox, } from '@elastic/eui'; import { useSearchRelevanceContext } from '../../../../../contexts'; @@ -25,6 +28,8 @@ interface SearchConfigProps { setSelectedIndex: React.Dispatch>; queryError: QueryError; setQueryError: React.Dispatch>; + pipeline: string; + setPipeline: React.Dispatch>; } export const SearchConfig: FunctionComponent = ({ @@ -35,8 +40,10 @@ export const SearchConfig: FunctionComponent = ({ setSelectedIndex, queryError, setQueryError, + pipeline, + setPipeline, }) => { - const { documentsIndexes, setShowFlyout } = useSearchRelevanceContext(); + const { documentsIndexes, pipelines, setShowFlyout } = useSearchRelevanceContext(); // On select index const onChangeSelectedIndex: React.ChangeEventHandler = (e) => { setSelectedIndex(e.target.value); @@ -47,6 +54,20 @@ export const SearchConfig: FunctionComponent = ({ })); }; + // Sort search pipelines based off of each individual pipeline name. + const sortedPipelines = [...Object.keys(pipelines)] + .sort((a, b) => a.localeCompare(b)) + .map((searchPipeline) => ({ + label: searchPipeline, + })); + // Add the '_none' option to the pipeline dropdown (runs the index without a pipeline). + sortedPipelines.push({ label: '_none' }); + + // On select pipeline for ComboBox + const onChangePipeline = (selectedPipelineOptions: string | any[]) => { + setPipeline(selectedPipelineOptions[0]?.label || ''); + }; + // Select index on blur const selectIndexOnBlur = () => { // If Index Select on blur without selecting an index, show error @@ -88,24 +109,39 @@ export const SearchConfig: FunctionComponent = ({

Query {queryNumber}

- {queryError.selectIndex}} - isInvalid={!!queryError.selectIndex.length} - > - ({ - value: index, - text: index, - }))} - aria-label="Search Index" - onChange={onChangeSelectedIndex} - value={selectedIndex} - onBlur={selectIndexOnBlur} - /> - + + + {queryError.selectIndex}} + isInvalid={!!queryError.selectIndex.length} + > + ({ + value: index, + text: index, + }))} + aria-label="Search Index" + onChange={onChangeSelectedIndex} + value={selectedIndex} + onBlur={selectIndexOnBlur} + /> + + + + + + + + @@ -70,6 +76,8 @@ export const SearchConfigsPanel = ({ setSelectedIndex={setSelectedIndex2} queryError={queryError2} setQueryError={setQueryError2} + pipeline={pipeline2} + setPipeline={setPipeline2} /> diff --git a/public/contexts/index.tsx b/public/contexts/index.tsx index d15f1de6..d0fd077f 100644 --- a/public/contexts/index.tsx +++ b/public/contexts/index.tsx @@ -21,6 +21,12 @@ export interface SearchRelevanceContextProps { setSelectedIndex1: React.Dispatch>; selectedIndex2: string; setSelectedIndex2: React.Dispatch>; + pipelines: {}; + setPipelines: React.Dispatch>; + pipeline1: string; + setPipeline1: React.Dispatch>; + pipeline2: string; + setPipeline2: React.Dispatch>; } export const SearchRelevanceContext = createContext(null); @@ -42,6 +48,9 @@ export const SearchRelevanceContextProvider = ({ children }: { children: React.R const [comparedResult2, setComparedResult2] = useState({}); const [selectedIndex1, setSelectedIndex1] = useState(''); const [selectedIndex2, setSelectedIndex2] = useState(''); + const [pipelines, setPipelines] = useState<{}>({}); + const [pipeline1, setPipeline1] = useState(''); + const [pipeline2, setPipeline2] = useState(''); const updateComparedResult1 = (result: SearchResults) => { setComparedResult1(getDocumentRank(result?.hits?.hits)); @@ -66,6 +75,12 @@ export const SearchRelevanceContextProvider = ({ children }: { children: React.R setSelectedIndex1, selectedIndex2, setSelectedIndex2, + pipelines, + setPipelines, + pipeline1, + setPipeline1, + pipeline2, + setPipeline2, }} > {children} diff --git a/server/metrics/index.ts b/server/metrics/index.ts index 11302658..7b874b27 100644 --- a/server/metrics/index.ts +++ b/server/metrics/index.ts @@ -20,4 +20,5 @@ export enum METRIC_ACTION { COMPARISON_SEARCH = 'comparison_search', SINGLE_SEARCH = 'single_search', FETCH_INDEX = 'fetch_index', + FETCH_PIPELINE = 'fetch_pipeline', } diff --git a/server/routes/dsl_route.ts b/server/routes/dsl_route.ts index bd64a8b6..94671f8c 100644 --- a/server/routes/dsl_route.ts +++ b/server/routes/dsl_route.ts @@ -8,7 +8,7 @@ import { RequestParams } from '@opensearch-project/opensearch'; import { IRouter } from '../../../../src/core/server'; import { METRIC_NAME, METRIC_ACTION } from '../metrics'; -import { ServiceEndpoints } from '../../common'; +import { ServiceEndpoints, SEARCH_API } from '../../common'; interface SearchResultsResponse { result1?: Object; @@ -29,15 +29,23 @@ export function registerDslRoute(router: IRouter) { const { query1, query2 } = request.body; const actionName = query1 && query2 ? METRIC_ACTION.COMPARISON_SEARCH : METRIC_ACTION.SINGLE_SEARCH; - let resBody: SearchResultsResponse = {}; + const resBody: SearchResultsResponse = {}; if (query1) { - const { index, size, ...rest } = query1; - const params: RequestParams.Search = { - index, - size, - body: rest, - }; + const { index, pipeline, size, ...rest } = query1; + const params: RequestParams.Search = + pipeline !== '' + ? { + index, + size, + body: rest, + search_pipeline: pipeline, + } + : { + index, + size, + body: rest, + }; const start = performance.now(); try { @@ -75,12 +83,20 @@ export function registerDslRoute(router: IRouter) { } if (query2) { - const { index, size, ...rest } = query2; - const params: RequestParams.Search = { - index, - size, - body: rest, - }; + const { index, pipeline, size, ...rest } = query2; + const params: RequestParams.Search = + pipeline !== '' + ? { + index, + size, + body: rest, + search_pipeline: pipeline, + } + : { + index, + size, + body: rest, + }; const start = performance.now(); try { @@ -122,6 +138,7 @@ export function registerDslRoute(router: IRouter) { } ); + // Get Indices router.get( { path: ServiceEndpoints.GetIndexes, @@ -163,4 +180,46 @@ export function registerDslRoute(router: IRouter) { } } ); + + // Get Pipelines + router.get( + { + path: ServiceEndpoints.GetPipelines, + validate: {}, + }, + async (context, request, response) => { + const start = performance.now(); + let resBody: any = {}; + try { + const resp = await context.core.opensearch.client.asCurrentUser.transport.request({ + method: 'GET', + path: `${SEARCH_API}/pipeline`, + }); + resBody = resp.body; + const end = performance.now(); + context.searchRelevance.metricsService.addMetric( + METRIC_NAME.SEARCH_RELEVANCE, + METRIC_ACTION.FETCH_PIPELINE, + 200, + end - start + ); + return response.ok({ + body: resBody, + }); + } catch (error) { + const end = performance.now(); + context.searchRelevance.metricsService.addMetric( + METRIC_NAME.SEARCH_RELEVANCE, + METRIC_ACTION.FETCH_PIPELINE, + error.statusCode, + end - start + ); + if (error.statusCode !== 404) console.error(error); + return response.custom({ + statusCode: error.statusCode || 500, + body: error.message, + }); + } + } + ); }