Set a pagination-aware rank value for all search engines.
Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
@@ -114,6 +114,8 @@ export interface CatalogSearchResultListItemProps {
|
||||
// (undocumented)
|
||||
icon?: ReactNode;
|
||||
// (undocumented)
|
||||
rank?: number;
|
||||
// (undocumented)
|
||||
result: IndexableDocument;
|
||||
}
|
||||
|
||||
|
||||
+11
-1
@@ -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}>
|
||||
|
||||
+22
-19
@@ -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:
|
||||
|
||||
@@ -38,6 +38,7 @@ export type DefaultResultListItemProps = {
|
||||
secondaryAction?: ReactNode;
|
||||
result: SearchDocument;
|
||||
highlight?: ResultHighlight;
|
||||
rank?: number;
|
||||
lineClamp?: number;
|
||||
};
|
||||
|
||||
|
||||
+12
-2
@@ -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
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
Reference in New Issue
Block a user