From 3b7eb13f8a4e0e9bf54804926a39d6c1a9567a99 Mon Sep 17 00:00:00 2001 From: Olivier Lamothe Date: Tue, 6 Jun 2023 10:25:29 -0400 Subject: [PATCH 1/3] feat: add search analysis module KIT-2526 --- package-lock.json | 7 + package.json | 5 +- .../SearchAnalysis/SearchAnalysis.ts | 15 + .../SearchAnalysis/SearchAnalysisInterface.ts | 415 ++++++++++++++++++ src/resources/SearchAnalysis/index.ts | 2 + .../test/SearchAnalysis.spec.ts | 37 ++ src/resources/index.ts | 1 + 7 files changed, 480 insertions(+), 2 deletions(-) create mode 100644 src/resources/SearchAnalysis/SearchAnalysis.ts create mode 100644 src/resources/SearchAnalysis/SearchAnalysisInterface.ts create mode 100644 src/resources/SearchAnalysis/index.ts create mode 100644 src/resources/SearchAnalysis/test/SearchAnalysis.spec.ts diff --git a/package-lock.json b/package-lock.json index 03798b506..95f2a8899 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0-development", "license": "Apache-2.0", "dependencies": { + "dayjs": "^1.11.8", "exponential-backoff": "^3.1.0", "query-string-cjs": "npm:query-string@^7.0.0", "query-string-esm": "npm:query-string@^8.0.0" @@ -4419,6 +4420,11 @@ "node": "*" } }, + "node_modules/dayjs": { + "version": "1.11.8", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.8.tgz", + "integrity": "sha512-LcgxzFoWMEPO7ggRv1Y2N31hUf2R0Vj7fuy/m+Bg1K8rr+KAs1AEy4y9jd5DXe8pbHgX+srkHNS7TH6Q6ZhYeQ==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -5911,6 +5917,7 @@ "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", "dev": true, + "peer": true, "engines": { "node": ">= 4.9.1" } diff --git a/package.json b/package.json index 19ed4dfde..aadc7f8a7 100644 --- a/package.json +++ b/package.json @@ -88,9 +88,10 @@ } }, "dependencies": { + "dayjs": "^1.11.8", "exponential-backoff": "^3.1.0", - "query-string-esm": "npm:query-string@^8.0.0", - "query-string-cjs": "npm:query-string@^7.0.0" + "query-string-cjs": "npm:query-string@^7.0.0", + "query-string-esm": "npm:query-string@^8.0.0" }, "publishConfig": { "access": "public" diff --git a/src/resources/SearchAnalysis/SearchAnalysis.ts b/src/resources/SearchAnalysis/SearchAnalysis.ts new file mode 100644 index 000000000..49aa08a96 --- /dev/null +++ b/src/resources/SearchAnalysis/SearchAnalysis.ts @@ -0,0 +1,15 @@ +import dayjs from 'dayjs'; +import API from '../../APICore.js'; +import Resource from '../Resource.js'; +import {ReplayAnalysis} from './SearchAnalysisInterface.js'; + +export default class SearchAnalysis extends Resource { + static baseUrl = '/rest/search/v3/analysis'; + + replay(id: string, from = dayjs().subtract(1, 'week').format('YYYY-MM-DD')) { + return this.api.post( + `${SearchAnalysis.baseUrl}/inspect/replay?organizationId=${API.orgPlaceholder}`, + {id, dateRange: {from}} + ); + } +} diff --git a/src/resources/SearchAnalysis/SearchAnalysisInterface.ts b/src/resources/SearchAnalysis/SearchAnalysisInterface.ts new file mode 100644 index 000000000..48b7c08c0 --- /dev/null +++ b/src/resources/SearchAnalysis/SearchAnalysisInterface.ts @@ -0,0 +1,415 @@ +import {PostSearchBodyQueryParams} from '../Search/SearchInterfaces.js'; + +/** + * Define the steps available during a query pipeline execution replay + */ +export type AvailableExecutionStep = + | 'pipelineSelection' + | 'paramOverrides' + | 'thesaurus' + | 'stopWords' + | 'filters' + | 'rankingExpressions' + | 'featuredResults' + | 'rankingWeights' + | 'contentRecommendation' + | 'productRecommendation' + | 'art' + | 'dne' + | 'triggers' + | 'indexQuery'; + +/** + * Define the mechanism available to select a pipeline when a replay is executed + */ +export type AvailablePipelineSelection = + | 'AUTHENTICATION_PIPELINE' + | 'AUTHENTICATION_SEARCH_HUB' + | 'ML_ROUTING' + | 'PARAMETER_PIPELINE' + | 'CONDITION_ROUTING' + | 'DEFAULT'; + +/** + * Define a step during the execution of a query pipeline + */ +export interface ExecutionStep { + /** + * The type of the execution step + */ + type: T; + /** + * The human readable name of the execution step + */ + name: string; +} + +/** + * Define a query pipeline condition + */ +export interface Condition { + /** + * The identifier of a condition + */ + id: string; +} + +/** + * Define which query parameters were affected by a query pipeline execution step + */ +export interface AffectedRequestParameters { + affectedRequestParameters: Array<{ + name: string; + value: unknown; + }>; +} +/** + * Define which query results were affected by a query pipeline execution step + */ +export interface AffectedResultsPosition { + affectedResultsPosition: number[]; +} + +/** + * Define a statement applied during a query pipeline execution step + */ +export type Applied = { + /** + * The ID of the statement applied + */ + id: string; + /** + * The condition associated with a statement + */ + condition?: Condition; +}; + +/** + * Define which rules were applied during a query pipeline execution step + */ +export interface AppliedRules, OtherPropertiesApplied = Record> { + /** + * The number of rule applied for a given execution step + */ + appliedCount: number; + /** + * The statements, query parameters and any other properties that affected the result set during the execution of a query pipeline step + */ + applied: Array; +} + +/** + * Define which query pipeline was selected while replaying a query + */ +export interface SelectedPipelineDefinition { + /** + * The ID of the selected pipeline + */ + id: string; + /** + * The human readable name of the selected query pipeline + */ + name: string; + /** + * The condition associated with the selected query pipeline + */ + condition?: Condition; +} + +/** + * Define the A/B test selection mechanism while replaying a query + */ +export interface PipelineSelectionCauseABTest { + /** + * The original selected pipeline + */ + originalPipeline: SelectedPipelineDefinition; + /** + * The targeted pipeline as part of the A/B test + */ + targetPipeline: SelectedPipelineDefinition; +} + +/** + * Define the cause of a pipeline selection + */ +export interface PipelineSelectionCause { + /** + * The pipeline selection cause + */ + type: AvailablePipelineSelection; + /** + * The pipeline A/B test + */ + abTest?: PipelineSelectionCauseABTest; +} + +/** + * Define the execution step associated with query pipeline selection + */ +export interface QueryPipelineSelection extends ExecutionStep<'pipelineSelection'> { + /** + * The selected pipeline + */ + selectedPipeline: SelectedPipelineDefinition; + /** + * The selection cause + */ + selectionCause: PipelineSelectionCause; +} + +/** + * Define the execution step associated with applying query parameter overrides + */ +export type QueryParamOverrides = ExecutionStep<'paramOverrides'> & AppliedRules; + +/** + * Define the execution step associated with applying thesaurus rules + */ +export type Thesaurus = ExecutionStep<'thesaurus'> & AppliedRules; + +/** + * Define the execution step associated with applying stop words + */ +export type StopWords = ExecutionStep<'stopWords'> & AppliedRules; + +/** + * Define the execution step associated with applying query filters + */ +export type Filters = ExecutionStep<'filters'> & AppliedRules; + +/** + * Define the execution step associated with applying query ranking expressions + */ +export type RankingExpressions = ExecutionStep<'rankingExpressions'> & + AppliedRules< + AffectedResultsPosition, + { + /** + * The boost applied by a ranking expression + */ + boost: number; + } + >; + +/** + * Define the execution step associated with applying featured results + */ +export type FeaturedResults = ExecutionStep<'featuredResults'> & AppliedRules; + +/** + * Define the execution step associated with applying ranking weights modifiers + */ +export type RankingWeights = ExecutionStep<'rankingWeights'> & AppliedRules; + +/** + * Define the execution step associated with retrieving content recommendation from an ML model + */ +export type ContentRecommendation = ExecutionStep<'contentRecommendation'> & AppliedRules; + +/** + * Define the execution step associated with retrieving product recommendation from an ML model + */ +export type ProductRecommendation = ExecutionStep<'productRecommendation'> & AppliedRules; + +/** + * Define the execution step associated with applying an Automatic Relevance Tuning ML model + */ +export type AutomaticRelevanceTuning = ExecutionStep<'art'> & AppliedRules; + +/** + * Define the execution step associated with applying an Dynamic Navigation Experience ML Model + */ +export type DynamicNavigationExperience = ExecutionStep<'dne'> & AppliedRules; + +/** + * Define the execution step associated with applying query pipeline triggers + */ +export type Triggers = ExecutionStep<'triggers'> & AppliedRules; + +/** + * Define the execution step associated with the execution of the query in the index. + */ +export interface IndexQuery extends ExecutionStep<'indexQuery'> { + request: Record; +} + +/** + * Define a well known map of available pipeline step with their associated content + */ +export type MappedExecutionSteps = { + [T in AvailableExecutionStep]: T extends 'pipelineSelection' + ? QueryPipelineSelection + : T extends 'paramOverrides' + ? QueryParamOverrides + : T extends 'thesaurus' + ? Thesaurus + : T extends 'stopWords' + ? StopWords + : T extends 'filters' + ? Filters + : T extends 'rankingExpressions' + ? RankingExpressions + : T extends 'featuredResults' + ? FeaturedResults + : T extends 'rankingWeights' + ? RankingWeights + : T extends 'contentRecommendation' + ? ContentRecommendation + : T extends 'productRecommendation' + ? ProductRecommendation + : T extends 'art' + ? AutomaticRelevanceTuning + : T extends 'dne' + ? DynamicNavigationExperience + : T extends 'triggers' + ? Triggers + : T extends 'indexQuery' + ? IndexQuery + : unknown; +}; + +/** + * Define the full query pipeline execution steps flow + */ +export type ExecutionSteps = Array; + +/** + * Define the output of a replay analysis + */ +export interface ReplayAnalysis { + /** + * Define the query parameters that were received by the Search API + */ + requestParameters: PostSearchBodyQueryParams; + /** + * Define the full query pipeline execution steps flow + */ + execution: ExecutionSteps; + /** + * Define the number of results returned as part of a replayed query analysis + */ + totalResultsCount: number; + /** + * Define the array of results returned as part of a replayed query analysis + */ + results: Result[]; +} + +/** + * Defines a result coming back from a replayed query analysis + */ +export interface Result { + /** + * Title of the result + */ + title: string; + /** + * Ranking information associated with the result + */ + rankingInfo?: RankingInformation; + /** + * Fields associated with the result + */ + fields: Record; +} + +/** + * Define the ranking information associated with a result + */ +export interface RankingInformation { + /** + * The total weight applied for that result + */ + totalWeight: number; + /** + * The weights related to the document + */ + documentWeights: Record; + /** + * The weights composed of query terms, ranking function and query ranking expression + */ + weightComposition?: WeightComposition; +} + +/** + * Define the weights composed of query terms, ranking function and query ranking expression + */ +export interface WeightComposition { + /** + * Query terms that affected the document position in the result set + */ + termsWeights?: TermWeight[]; + /** + * Ranking function that affected the document position in the result set + */ + rankingFunctions?: RankingExpression[]; + /** + * Query ranking expressions that affected the document position in the result set + */ + queryRankingExpressions?: RankingExpression[]; +} + +/** + * Define a query term ranking information + */ +export interface TermWeight { + /** + * The dictionnary of term associated with ranking information + */ + term: {[keyword: string]: Term}; + /** + * The weight associated with the term + */ + weightInfo: Record; +} + +/** + * Define a term ranking information + */ +export interface Term { + /** + * The correlation score + */ + correlation: number; + /** + * The TFID score + */ + idfScore: number; +} + +/** + * Define a ranking expression + */ +export interface RankingExpression { + /** + * The query expression + */ + expression: string; + /** + * The origin of the expression + */ + origin: string; + /** + * The rule of the expression + */ + rule?: RankingExpressionRule; + /** + * The score of the expression + */ + score: number; +} + +/** + * Deifne a ranking expression rule + */ +export interface RankingExpressionRule { + /** + * The identifier + */ + id: string; + /** + * The type of rule + */ + type: string; +} diff --git a/src/resources/SearchAnalysis/index.ts b/src/resources/SearchAnalysis/index.ts new file mode 100644 index 000000000..207a477a6 --- /dev/null +++ b/src/resources/SearchAnalysis/index.ts @@ -0,0 +1,2 @@ +export * from './SearchAnalysisInterface.js'; +export * from './SearchAnalysis.js'; diff --git a/src/resources/SearchAnalysis/test/SearchAnalysis.spec.ts b/src/resources/SearchAnalysis/test/SearchAnalysis.spec.ts new file mode 100644 index 000000000..e1bd9cc8f --- /dev/null +++ b/src/resources/SearchAnalysis/test/SearchAnalysis.spec.ts @@ -0,0 +1,37 @@ +import API from '../../../APICore.js'; +import SearchAnalysis from '../SearchAnalysis.js'; + +jest.mock('../../../APICore.js'); + +const APIMock: jest.Mock = API as any; + +describe('SearchAnalysis', () => { + let searchAnalysis: SearchAnalysis; + const api = new APIMock() as jest.Mocked; + const serverlessApi = new APIMock() as jest.Mocked; + + beforeEach(() => { + jest.clearAllMocks(); + searchAnalysis = new SearchAnalysis(api, serverlessApi); + }); + + describe('replay', () => { + it('should make a replay call to the searchAPI with a default date range', () => { + searchAnalysis.replay('some-search-id'); + expect(api.post).toHaveBeenCalledTimes(1); + expect(api.post).toHaveBeenCalledWith( + `${SearchAnalysis.baseUrl}/inspect/replay?organizationId=${API.orgPlaceholder}`, + {id: 'some-search-id', dateRange: {from: expect.any(String)}} + ); + }); + + it('should make a replay call to the searchAPI with a defined date range', () => { + searchAnalysis.replay('some-search-id', '2023-01-01'); + expect(api.post).toHaveBeenCalledTimes(1); + expect(api.post).toHaveBeenCalledWith( + `${SearchAnalysis.baseUrl}/inspect/replay?organizationId=${API.orgPlaceholder}`, + {id: 'some-search-id', dateRange: {from: '2023-01-01'}} + ); + }); + }); +}); diff --git a/src/resources/index.ts b/src/resources/index.ts index f950cf019..6ea0f386a 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -48,3 +48,4 @@ export * from './TableauService/index.js'; export * from './UsageAnalytics/index.js'; export * from './Vaults/index.js'; export * from './HostedPages/index.js'; +export * from './SearchAnalysis/index.js'; From 0d4d8048df2f7cb338bec5afb1b4de9e1e10fd75 Mon Sep 17 00:00:00 2001 From: Olivier Lamothe Date: Tue, 6 Jun 2023 10:33:56 -0400 Subject: [PATCH 2/3] feat: add search analysis KIT-2526 --- src/resources/PlatformResources.ts | 2 ++ src/resources/SearchAnalysis/SearchAnalysisInterface.ts | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/resources/PlatformResources.ts b/src/resources/PlatformResources.ts index c4cdda774..0e6c0fac1 100644 --- a/src/resources/PlatformResources.ts +++ b/src/resources/PlatformResources.ts @@ -48,6 +48,7 @@ import User from './Users/User.js'; import Vaults from './Vaults/Vaults.js'; import TableauService from './TableauService/TableauService.js'; import HostedPages from './HostedPages/HostedPages.js'; +import SearchAnalysis from './SearchAnalysis/SearchAnalysis.js'; const resourcesMap: Array<{key: string; resource: typeof Resource}> = [ {key: 'activity', resource: Activity}, @@ -100,6 +101,7 @@ const resourcesMap: Array<{key: string; resource: typeof Resource}> = [ {key: 'notification', resource: Notifications}, {key: 'privilegeEvaluator', resource: PrivilegeEvaluator}, {key: 'tableauService', resource: TableauService}, + {key: 'searchAnalysis', resource: SearchAnalysis}, ]; class PlatformResources { diff --git a/src/resources/SearchAnalysis/SearchAnalysisInterface.ts b/src/resources/SearchAnalysis/SearchAnalysisInterface.ts index 48b7c08c0..97c6e3025 100644 --- a/src/resources/SearchAnalysis/SearchAnalysisInterface.ts +++ b/src/resources/SearchAnalysis/SearchAnalysisInterface.ts @@ -355,7 +355,7 @@ export interface WeightComposition { */ export interface TermWeight { /** - * The dictionnary of term associated with ranking information + * The dictionary of term associated with ranking information */ term: {[keyword: string]: Term}; /** @@ -373,7 +373,7 @@ export interface Term { */ correlation: number; /** - * The TFID score + * The TFIDF score */ idfScore: number; } @@ -401,7 +401,7 @@ export interface RankingExpression { } /** - * Deifne a ranking expression rule + * Define a ranking expression rule */ export interface RankingExpressionRule { /** From cfc30a6874668fd877d92f224ec2a22688efe3c2 Mon Sep 17 00:00:00 2001 From: Olivier Lamothe Date: Mon, 12 Jun 2023 13:28:34 -0400 Subject: [PATCH 3/3] feat: add search analysis module --- package-lock.json | 6 ------ package.json | 1 - src/resources/SearchAnalysis/SearchAnalysis.ts | 13 ++++++++++--- .../SearchAnalysis/test/SearchAnalysis.spec.ts | 12 ++++++------ 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 95f2a8899..17d77a3f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "0.0.0-development", "license": "Apache-2.0", "dependencies": { - "dayjs": "^1.11.8", "exponential-backoff": "^3.1.0", "query-string-cjs": "npm:query-string@^7.0.0", "query-string-esm": "npm:query-string@^8.0.0" @@ -4420,11 +4419,6 @@ "node": "*" } }, - "node_modules/dayjs": { - "version": "1.11.8", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.8.tgz", - "integrity": "sha512-LcgxzFoWMEPO7ggRv1Y2N31hUf2R0Vj7fuy/m+Bg1K8rr+KAs1AEy4y9jd5DXe8pbHgX+srkHNS7TH6Q6ZhYeQ==" - }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", diff --git a/package.json b/package.json index aadc7f8a7..4633c7b8a 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,6 @@ } }, "dependencies": { - "dayjs": "^1.11.8", "exponential-backoff": "^3.1.0", "query-string-cjs": "npm:query-string@^7.0.0", "query-string-esm": "npm:query-string@^8.0.0" diff --git a/src/resources/SearchAnalysis/SearchAnalysis.ts b/src/resources/SearchAnalysis/SearchAnalysis.ts index 49aa08a96..a371b7e45 100644 --- a/src/resources/SearchAnalysis/SearchAnalysis.ts +++ b/src/resources/SearchAnalysis/SearchAnalysis.ts @@ -1,4 +1,3 @@ -import dayjs from 'dayjs'; import API from '../../APICore.js'; import Resource from '../Resource.js'; import {ReplayAnalysis} from './SearchAnalysisInterface.js'; @@ -6,10 +5,18 @@ import {ReplayAnalysis} from './SearchAnalysisInterface.js'; export default class SearchAnalysis extends Resource { static baseUrl = '/rest/search/v3/analysis'; - replay(id: string, from = dayjs().subtract(1, 'week').format('YYYY-MM-DD')) { + /** + * Replay a query that was already done and get inspection details. + * + * @param id The SearchUID of the request to replay. + * @param from The inclusive date at which to start looking for the request. Example: 2019-08-24T14:15:22Z + * @param to The inclusive date at which to stop looking for the SearchUID. When omitted searches up until the most recent requests. Example:2019-08-24T14:15:22Z + * @returns + */ + replay(id: string, from: string, to?: string) { return this.api.post( `${SearchAnalysis.baseUrl}/inspect/replay?organizationId=${API.orgPlaceholder}`, - {id, dateRange: {from}} + {id, dateRange: {from, to}} ); } } diff --git a/src/resources/SearchAnalysis/test/SearchAnalysis.spec.ts b/src/resources/SearchAnalysis/test/SearchAnalysis.spec.ts index e1bd9cc8f..c5ef229db 100644 --- a/src/resources/SearchAnalysis/test/SearchAnalysis.spec.ts +++ b/src/resources/SearchAnalysis/test/SearchAnalysis.spec.ts @@ -16,21 +16,21 @@ describe('SearchAnalysis', () => { }); describe('replay', () => { - it('should make a replay call to the searchAPI with a default date range', () => { - searchAnalysis.replay('some-search-id'); + it('should make a replay call to the searchAPI with a partially defined date range', () => { + searchAnalysis.replay('some-search-id', '2023-01-01'); expect(api.post).toHaveBeenCalledTimes(1); expect(api.post).toHaveBeenCalledWith( `${SearchAnalysis.baseUrl}/inspect/replay?organizationId=${API.orgPlaceholder}`, - {id: 'some-search-id', dateRange: {from: expect.any(String)}} + {id: 'some-search-id', dateRange: {from: '2023-01-01'}} ); }); - it('should make a replay call to the searchAPI with a defined date range', () => { - searchAnalysis.replay('some-search-id', '2023-01-01'); + it('should make a replay call to the searchAPI with a complete defined date range', () => { + searchAnalysis.replay('some-search-id', '2023-01-01', '2023-02-01'); expect(api.post).toHaveBeenCalledTimes(1); expect(api.post).toHaveBeenCalledWith( `${SearchAnalysis.baseUrl}/inspect/replay?organizationId=${API.orgPlaceholder}`, - {id: 'some-search-id', dateRange: {from: '2023-01-01'}} + {id: 'some-search-id', dateRange: {from: '2023-01-01', to: '2023-02-01'}} ); }); });