Highlight PG search results
Signed-off-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com>
This commit is contained in:
committed by
Fredrik Adelöw
parent
b5f7763eb4
commit
423e3d8e95
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user