Highlight PG search results

Signed-off-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com>
This commit is contained in:
Andre Wanlin
2022-06-17 19:51:40 -05:00
committed by Fredrik Adelöw
parent b5f7763eb4
commit 423e3d8e95
8 changed files with 79 additions and 7 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-search-backend-module-pg': patch
---
Added support for highlighting matched terms in search result data
@@ -43,7 +43,7 @@ export class DatabaseDocumentStore implements DatabaseStore {
// (undocumented)
query(
tx: Knex.Transaction,
{ types, pgTerm, fields, offset, limit }: PgSearchQuery,
{ types, pgTerm, fields, offset, limit, preTag, postTag }: PgSearchQuery,
): Promise<DocumentResultRow[]>;
// (undocumented)
static supported(knex: Knex): Promise<boolean>;
@@ -134,6 +134,10 @@ export interface PgSearchQuery {
// (undocumented)
pgTerm?: string;
// (undocumented)
postTag: string;
// (undocumented)
preTag: string;
// (undocumented)
types?: string[];
}
@@ -27,7 +27,8 @@
"@backstage/plugin-search-backend-node": "^0.6.3-next.1",
"@backstage/plugin-search-common": "^0.3.6-next.0",
"lodash": "^4.17.21",
"knex": "^2.0.0"
"knex": "^2.0.0",
"uuid": "^8.3.2"
},
"devDependencies": {
"@backstage/backend-test-utils": "^0.1.26-next.1",
@@ -158,6 +158,11 @@ describe('PgSearchEngine', () => {
location: 'location-1',
},
type: 'my-type',
highlight: {
title: 'Hello World',
text: 'Lorem Ipsum',
location: 'location-1',
},
},
]);
@@ -199,6 +204,11 @@ describe('PgSearchEngine', () => {
location: `location-${i}`,
},
type: 'my-type',
highlight: {
title: 'Hello World',
text: 'Lorem Ipsum',
location: 'location-1',
},
})),
);
@@ -240,6 +250,11 @@ describe('PgSearchEngine', () => {
location: `location-${i}`,
},
type: 'my-type',
highlight: {
title: 'Hello World',
text: 'Lorem Ipsum',
location: 'location-1',
},
}))
.slice(25),
);
@@ -26,6 +26,7 @@ import {
DatabaseStore,
PgSearchQuery,
} from '../database';
import { v4 as uuid } from 'uuid';
export type ConcretePgSearchQuery = {
pgQuery: PgSearchQuery;
@@ -53,6 +54,7 @@ export class PgSearchEngine implements SearchEngine {
const offset = page * pageSize;
// We request more result to know whether there is another page
const limit = pageSize + 1;
const uuidTag = uuid();
return {
pgQuery: {
@@ -66,6 +68,8 @@ export class PgSearchEngine implements SearchEngine {
types: query.types,
offset,
limit,
preTag: `<${uuidTag}>`,
postTag: `</${uuidTag}>`,
},
pageSize,
};
@@ -106,10 +110,22 @@ export class PgSearchEngine implements SearchEngine {
: undefined;
const results = pageRows.map(
({ type, document }, index): IndexableResult => ({
({ type, document, highlight }, index): IndexableResult => ({
type,
document,
rank: page * pageSize + index + 1,
highlight: {
preTag: pgQuery.preTag,
postTag: pgQuery.postTag,
fields: highlight
? {
text: highlight.text,
title: highlight.title,
location: highlight.location,
path: '',
}
: {},
},
}),
);
@@ -220,7 +220,13 @@ describe('DatabaseDocumentStore', () => {
});
const rows = await store.transaction(tx =>
store.query(tx, { pgTerm: 'Hello & World', offset: 1, limit: 1 }),
store.query(tx, {
pgTerm: 'Hello & World',
offset: 1,
limit: 1,
preTag: '<tag>',
postTag: '</tag>',
}),
);
expect(rows).toEqual([
@@ -261,7 +267,13 @@ describe('DatabaseDocumentStore', () => {
});
const rows = await store.transaction(tx =>
store.query(tx, { pgTerm: 'Hello & World', offset: 0, limit: 25 }),
store.query(tx, {
pgTerm: 'Hello & World',
offset: 0,
limit: 25,
preTag: '<tag>',
postTag: '</tag>',
}),
);
expect(rows).toEqual([
@@ -322,6 +334,8 @@ describe('DatabaseDocumentStore', () => {
types: ['my-type'],
offset: 0,
limit: 25,
preTag: '<tag>',
postTag: '</tag>',
}),
);
@@ -375,6 +389,8 @@ describe('DatabaseDocumentStore', () => {
fields: { myField: 'this' },
offset: 0,
limit: 25,
preTag: '<tag>',
postTag: '</tag>',
}),
);
@@ -429,6 +445,8 @@ describe('DatabaseDocumentStore', () => {
fields: { myField: ['this', 'that'] },
offset: 0,
limit: 25,
preTag: '<tag>',
postTag: '</tag>',
}),
);
@@ -490,6 +508,8 @@ describe('DatabaseDocumentStore', () => {
fields: { myField: 'this', otherField: 'another' },
offset: 0,
limit: 25,
preTag: '<tag>',
postTag: '</tag>',
}),
);
@@ -539,6 +559,8 @@ describe('DatabaseDocumentStore', () => {
fields: { myField: 'this' },
offset: 0,
limit: 25,
preTag: '<tag>',
postTag: '</tag>',
}),
);
@@ -132,10 +132,11 @@ export class DatabaseDocumentStore implements DatabaseStore {
async query(
tx: Knex.Transaction,
{ types, pgTerm, fields, offset, limit }: PgSearchQuery,
{ types, pgTerm, fields, offset, limit, preTag, postTag }: PgSearchQuery,
): Promise<DocumentResultRow[]> {
// Builds a query like:
// SELECT ts_rank_cd(body, query) AS rank, type, document
// SELECT ts_rank_cd(body, query) AS rank, type, document,
// ts_headline('english', document, query) AS highlight
// FROM documents, to_tsquery('english', 'consent') query
// WHERE query @@ body AND (document @> '{"kind": "API"}')
// ORDER BY rank DESC
@@ -173,6 +174,11 @@ export class DatabaseDocumentStore implements DatabaseStore {
if (pgTerm) {
query
.select(tx.raw('ts_rank_cd(body, query) AS "rank"'))
.select(
tx.raw(
`ts_headline(\'english\', document, query, 'StartSel=${preTag}, StopSel=${postTag}') as "highlight"`,
),
)
.orderBy('rank', 'desc');
} else {
query.select(tx.raw('1 as rank'));
@@ -22,6 +22,8 @@ export interface PgSearchQuery {
pgTerm?: string;
offset: number;
limit: number;
preTag: string;
postTag: string;
}
export interface DatabaseStore {
@@ -49,4 +51,5 @@ export interface RawDocumentRow {
export interface DocumentResultRow {
document: IndexableDocument;
type: string;
highlight: IndexableDocument;
}