From 915700f64fcf600ac0079571372944e8cb130f09 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 9 Jun 2022 15:17:55 +0200 Subject: [PATCH] Set a pagination-aware rank value for all search engines. Signed-off-by: Eric Peterson --- .changeset/search-rank-choice-voting.md | 8 ++++ .changeset/search-rankly-my-dear.md | 7 ++++ plugins/catalog/api-report.md | 2 + .../CatalogSearchResultListItem.tsx | 12 +++++- .../src/engines/ElasticSearchSearchEngine.ts | 41 ++++++++++--------- .../src/PgSearchEngine/PgSearchEngine.ts | 12 ++++-- .../src/engines/LunrSearchEngine.ts | 3 +- .../src/service/AuthorizedSearchEngine.ts | 13 ++++-- plugins/search-react/api-report.md | 1 + .../DefaultResultListItem.tsx | 14 ++++++- plugins/techdocs/api-report.md | 1 + .../TechDocsSearchResultListItem.tsx | 20 ++++++++- 12 files changed, 102 insertions(+), 32 deletions(-) create mode 100644 .changeset/search-rank-choice-voting.md create mode 100644 .changeset/search-rankly-my-dear.md diff --git a/.changeset/search-rank-choice-voting.md b/.changeset/search-rank-choice-voting.md new file mode 100644 index 0000000000..2754d149e7 --- /dev/null +++ b/.changeset/search-rank-choice-voting.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-search-backend': patch +'@backstage/plugin-search-backend-module-elasticsearch': patch +'@backstage/plugin-search-backend-module-pg': patch +'@backstage/plugin-search-backend-node': patch +--- + +The provided search engine now adds a pagination-aware `rank` value to all results. diff --git a/.changeset/search-rankly-my-dear.md b/.changeset/search-rankly-my-dear.md new file mode 100644 index 0000000000..c528c567b1 --- /dev/null +++ b/.changeset/search-rankly-my-dear.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-catalog': patch +'@backstage/plugin-search': patch +'@backstage/plugin-techdocs': patch +--- + +In order to simplify analytics on top of the search experience in Backstage, the provided `<*ResultListItem />` component now captures a `discover` analytics event instead of a `click` event. This event includes the result rank as its `value` and, like a click, the URL/path clicked to as its `to` attribute. diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index 5b7e07701c..34bb5dfcec 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -114,6 +114,8 @@ export interface CatalogSearchResultListItemProps { // (undocumented) icon?: ReactNode; // (undocumented) + rank?: number; + // (undocumented) result: IndexableDocument; } diff --git a/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx b/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx index 993e34fbf6..a7307055c2 100644 --- a/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx +++ b/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx @@ -25,6 +25,7 @@ import { makeStyles, } from '@material-ui/core'; import { Link } from '@backstage/core-components'; +import { useAnalytics } from '@backstage/core-plugin-api'; import { IndexableDocument, ResultHighlight, @@ -51,6 +52,7 @@ export interface CatalogSearchResultListItemProps { icon?: ReactNode; result: IndexableDocument; highlight?: ResultHighlight; + rank?: number; } /** @public */ @@ -60,8 +62,16 @@ export function CatalogSearchResultListItem( const result = props.result as any; const classes = useStyles(); + const analytics = useAnalytics(); + const handleClick = () => { + analytics.captureEvent('discover', result.title, { + attributes: { to: result.location }, + value: props.rank, + }); + }; + return ( - + {props.icon && {props.icon}}
diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts index d11d8a1f90..31112e3446 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts @@ -334,27 +334,30 @@ export class ElasticSearchSearchEngine implements SearchEngine { : undefined; return { - results: result.body.hits.hits.map((d: ElasticSearchResult) => { - const resultItem: IndexableResult = { - type: this.getTypeFromIndex(d._index), - document: d._source, - }; - - if (d.highlight) { - resultItem.highlight = { - preTag: this.highlightOptions.preTag as string, - postTag: this.highlightOptions.postTag as string, - fields: Object.fromEntries( - Object.entries(d.highlight).map(([field, fragments]) => [ - field, - fragments.join(this.highlightOptions.fragmentDelimiter), - ]), - ), + results: result.body.hits.hits.map( + (d: ElasticSearchResult, index: number) => { + const resultItem: IndexableResult = { + type: this.getTypeFromIndex(d._index), + document: d._source, + rank: pageSize * page + index + 1, }; - } - return resultItem; - }), + if (d.highlight) { + resultItem.highlight = { + preTag: this.highlightOptions.preTag as string, + postTag: this.highlightOptions.postTag as string, + fields: Object.fromEntries( + Object.entries(d.highlight).map(([field, fragments]) => [ + field, + fragments.join(this.highlightOptions.fragmentDelimiter), + ]), + ), + }; + } + + return resultItem; + }, + ), nextPageCursor, previousPageCursor, }; diff --git a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts index 445d1bb5c1..bf98c58de9 100644 --- a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts +++ b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts @@ -18,6 +18,7 @@ import { SearchEngine } from '@backstage/plugin-search-backend-node'; import { SearchQuery, IndexableResultSet, + IndexableResult, } from '@backstage/plugin-search-common'; import { PgSearchEngineIndexer } from './PgSearchEngineIndexer'; import { @@ -104,10 +105,13 @@ export class PgSearchEngine implements SearchEngine { ? encodePageCursor({ page: page - 1 }) : undefined; - const results = pageRows.map(({ type, document }) => ({ - type, - document, - })); + const results = pageRows.map( + ({ type, document }, index): IndexableResult => ({ + type, + document, + rank: page * pageSize + index + 1, + }), + ); return { results, nextPageCursor, previousPageCursor }; } diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts index 4a5f4cdd18..03cb421653 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts @@ -207,9 +207,10 @@ export class LunrSearchEngine implements SearchEngine { // Translate results into IndexableResultSet const realResultSet: IndexableResultSet = { - results: results.slice(offset, offset + pageSize).map(d => ({ + results: results.slice(offset, offset + pageSize).map((d, index) => ({ type: d.type, document: this.docStore[d.result.ref], + rank: page * pageSize + index + 1, highlight: { preTag: this.highlightPreTag, postTag: this.highlightPostTag, diff --git a/plugins/search-backend/src/service/AuthorizedSearchEngine.ts b/plugins/search-backend/src/service/AuthorizedSearchEngine.ts index a74cdf8eef..ade43280be 100644 --- a/plugins/search-backend/src/service/AuthorizedSearchEngine.ts +++ b/plugins/search-backend/src/service/AuthorizedSearchEngine.ts @@ -189,10 +189,15 @@ export class AuthorizedSearchEngine implements SearchEngine { ); return { - results: filteredResults.slice( - page * this.pageSize, - (page + 1) * this.pageSize, - ), + results: filteredResults + .slice(page * this.pageSize, (page + 1) * this.pageSize) + .map((result, index) => { + // Overwrite any/all rank entries to avoid leaking knowledge of filtered results. + return { + ...result, + rank: page * this.pageSize + index + 1, + }; + }), previousPageCursor: page === 0 ? undefined : encodePageCursor({ page: page - 1 }), nextPageCursor: diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index f9bb993fd6..e4afee45ca 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -38,6 +38,7 @@ export type DefaultResultListItemProps = { secondaryAction?: ReactNode; result: SearchDocument; highlight?: ResultHighlight; + rank?: number; lineClamp?: number; }; diff --git a/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx b/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx index ff6f3d7fe1..f4260525d6 100644 --- a/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx +++ b/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx @@ -15,7 +15,7 @@ */ import React, { ReactNode } from 'react'; -import { AnalyticsContext } from '@backstage/core-plugin-api'; +import { AnalyticsContext, useAnalytics } from '@backstage/core-plugin-api'; import { ResultHighlight, SearchDocument, @@ -40,6 +40,7 @@ export type DefaultResultListItemProps = { secondaryAction?: ReactNode; result: SearchDocument; highlight?: ResultHighlight; + rank?: number; lineClamp?: number; }; @@ -51,12 +52,21 @@ export type DefaultResultListItemProps = { export const DefaultResultListItemComponent = ({ result, highlight, + rank, icon, secondaryAction, lineClamp = 5, }: DefaultResultListItemProps) => { + const analytics = useAnalytics(); + const handleClick = () => { + analytics.captureEvent('discover', result.title, { + attributes: { to: result.location }, + value: rank, + }); + }; + return ( - + {icon && {icon}} { + analytics.captureEvent('discover', result.title, { + attributes: { to: result.location }, + value: rank, + }); + }; + const TextItem = () => { const resultTitle = highlight?.fields.title ? ( ) => - asLink ? {children} : <>{children}; + asLink ? ( + + {children} + + ) : ( + <>{children} + ); const ListItemWrapper = ({ children }: PropsWithChildren<{}>) => asListItem ? (