Set a pagination-aware rank value for all search engines.

Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
Eric Peterson
2022-06-09 15:17:55 +02:00
parent 484afdf1dc
commit 915700f64f
12 changed files with 102 additions and 32 deletions
+8
View File
@@ -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.
+7
View File
@@ -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.
+2
View File
@@ -114,6 +114,8 @@ export interface CatalogSearchResultListItemProps {
// (undocumented)
icon?: ReactNode;
// (undocumented)
rank?: number;
// (undocumented)
result: IndexableDocument;
}
@@ -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 (
<Link to={result.location}>
<Link noTrack to={result.location} onClick={handleClick}>
<ListItem alignItems="flex-start">
{props.icon && <ListItemIcon>{props.icon}</ListItemIcon>}
<div className={classes.flexContainer}>
@@ -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,
};
@@ -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 };
}
@@ -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,
@@ -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:
+1
View File
@@ -38,6 +38,7 @@ export type DefaultResultListItemProps = {
secondaryAction?: ReactNode;
result: SearchDocument;
highlight?: ResultHighlight;
rank?: number;
lineClamp?: number;
};
@@ -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 (
<Link to={result.location}>
<Link noTrack to={result.location} onClick={handleClick}>
<ListItem alignItems="center">
{icon && <ListItemIcon>{icon}</ListItemIcon>}
<ListItemText
+1
View File
@@ -393,6 +393,7 @@ export type TechDocsSearchResultListItemProps = {
icon?: ReactNode;
result: any;
highlight?: ResultHighlight;
rank?: number;
lineClamp?: number;
asListItem?: boolean;
asLink?: boolean;
@@ -23,6 +23,7 @@ import {
makeStyles,
} from '@material-ui/core';
import { Link } from '@backstage/core-components';
import { useAnalytics } from '@backstage/core-plugin-api';
import { ResultHighlight } from '@backstage/plugin-search-common';
import { HighlightedSearchResultText } from '@backstage/plugin-search-react';
@@ -45,6 +46,7 @@ export type TechDocsSearchResultListItemProps = {
icon?: ReactNode;
result: any;
highlight?: ResultHighlight;
rank?: number;
lineClamp?: number;
asListItem?: boolean;
asLink?: boolean;
@@ -62,6 +64,7 @@ export const TechDocsSearchResultListItem = (
const {
result,
highlight,
rank,
lineClamp = 5,
asListItem = true,
asLink = true,
@@ -69,6 +72,15 @@ export const TechDocsSearchResultListItem = (
icon,
} = props;
const classes = useStyles();
const analytics = useAnalytics();
const handleClick = () => {
analytics.captureEvent('discover', result.title, {
attributes: { to: result.location },
value: rank,
});
};
const TextItem = () => {
const resultTitle = highlight?.fields.title ? (
<HighlightedSearchResultText
@@ -138,7 +150,13 @@ export const TechDocsSearchResultListItem = (
};
const LinkWrapper = ({ children }: PropsWithChildren<{}>) =>
asLink ? <Link to={result.location}>{children}</Link> : <>{children}</>;
asLink ? (
<Link noTrack to={result.location} onClick={handleClick}>
{children}
</Link>
) : (
<>{children}</>
);
const ListItemWrapper = ({ children }: PropsWithChildren<{}>) =>
asListItem ? (