From 423e3d8e957606438755a7c712e7c8450d4b91ea Mon Sep 17 00:00:00 2001 From: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Date: Fri, 17 Jun 2022 19:51:40 -0500 Subject: [PATCH] Highlight PG search results Signed-off-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> --- .changeset/metal-windows-share.md | 5 ++++ .../search-backend-module-pg/api-report.md | 6 ++++- plugins/search-backend-module-pg/package.json | 3 ++- .../src/PgSearchEngine/PgSearchEngine.test.ts | 15 +++++++++++ .../src/PgSearchEngine/PgSearchEngine.ts | 18 ++++++++++++- .../database/DatabaseDocumentStore.test.ts | 26 +++++++++++++++++-- .../src/database/DatabaseDocumentStore.ts | 10 +++++-- .../src/database/types.ts | 3 +++ 8 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 .changeset/metal-windows-share.md diff --git a/.changeset/metal-windows-share.md b/.changeset/metal-windows-share.md new file mode 100644 index 0000000000..4fc568986a --- /dev/null +++ b/.changeset/metal-windows-share.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-module-pg': patch +--- + +Added support for highlighting matched terms in search result data diff --git a/plugins/search-backend-module-pg/api-report.md b/plugins/search-backend-module-pg/api-report.md index 71ceac996e..6a8b68a042 100644 --- a/plugins/search-backend-module-pg/api-report.md +++ b/plugins/search-backend-module-pg/api-report.md @@ -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; // (undocumented) static supported(knex: Knex): Promise; @@ -134,6 +134,10 @@ export interface PgSearchQuery { // (undocumented) pgTerm?: string; // (undocumented) + postTag: string; + // (undocumented) + preTag: string; + // (undocumented) types?: string[]; } diff --git a/plugins/search-backend-module-pg/package.json b/plugins/search-backend-module-pg/package.json index fb8cd4afc4..7809f9dd06 100644 --- a/plugins/search-backend-module-pg/package.json +++ b/plugins/search-backend-module-pg/package.json @@ -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", diff --git a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.test.ts b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.test.ts index 3ed002567e..5f3bc3c5d8 100644 --- a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.test.ts +++ b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.test.ts @@ -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), ); diff --git a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts index bf98c58de9..77aa003c5d 100644 --- a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts +++ b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts @@ -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: ``, }, 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: '', + } + : {}, + }, }), ); diff --git a/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.test.ts b/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.test.ts index b973c2e14f..e2303d90b2 100644 --- a/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.test.ts +++ b/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.test.ts @@ -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: '', + postTag: '', + }), ); 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: '', + postTag: '', + }), ); expect(rows).toEqual([ @@ -322,6 +334,8 @@ describe('DatabaseDocumentStore', () => { types: ['my-type'], offset: 0, limit: 25, + preTag: '', + postTag: '', }), ); @@ -375,6 +389,8 @@ describe('DatabaseDocumentStore', () => { fields: { myField: 'this' }, offset: 0, limit: 25, + preTag: '', + postTag: '', }), ); @@ -429,6 +445,8 @@ describe('DatabaseDocumentStore', () => { fields: { myField: ['this', 'that'] }, offset: 0, limit: 25, + preTag: '', + postTag: '', }), ); @@ -490,6 +508,8 @@ describe('DatabaseDocumentStore', () => { fields: { myField: 'this', otherField: 'another' }, offset: 0, limit: 25, + preTag: '', + postTag: '', }), ); @@ -539,6 +559,8 @@ describe('DatabaseDocumentStore', () => { fields: { myField: 'this' }, offset: 0, limit: 25, + preTag: '', + postTag: '', }), ); diff --git a/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.ts b/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.ts index 33b2d0c0d6..3a1ec128ef 100644 --- a/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.ts +++ b/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.ts @@ -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 { // 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')); diff --git a/plugins/search-backend-module-pg/src/database/types.ts b/plugins/search-backend-module-pg/src/database/types.ts index 7e87658745..23679d100f 100644 --- a/plugins/search-backend-module-pg/src/database/types.ts +++ b/plugins/search-backend-module-pg/src/database/types.ts @@ -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; }