@backstage/plugin-search-backend-module-pg 0.3.5-next.1 → 0.3.6-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # @backstage/plugin-search-backend-module-pg
2
2
 
3
+ ## 0.3.6-next.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+ - @backstage/backend-common@0.15.0-next.0
9
+ - @backstage/plugin-search-backend-node@1.0.1-next.0
10
+
11
+ ## 0.3.5
12
+
13
+ ### Patch Changes
14
+
15
+ - 423e3d8e95: **DEPRECATED**: `PgSearchEngine` static `from` has been deprecated and will be removed in a future release. Use static `fromConfig` method to instantiate.
16
+
17
+ Added support for highlighting matched terms in search result data
18
+
19
+ - 679b32172e: Updated dependency `knex` to `^2.0.0`.
20
+ - 886e99b8e7: Now imports `SearchEngine` interface from `@backstage/plugin-search-common` instead of `@backstage/plugin-search-backend-node`
21
+ - Updated dependencies
22
+ - @backstage/backend-common@0.14.1
23
+ - @backstage/plugin-search-backend-node@1.0.0
24
+ - @backstage/plugin-search-common@1.0.0
25
+
26
+ ## 0.3.5-next.2
27
+
28
+ ### Patch Changes
29
+
30
+ - 423e3d8e95: **DEPRECATED**: `PgSearchEngine` static `from` has been deprecated and will be removed in a future release. Use static `fromConfig` method to instantiate.
31
+
32
+ Added support for highlighting matched terms in search result data
33
+
34
+ - 679b32172e: Updated dependency `knex` to `^2.0.0`.
35
+ - Updated dependencies
36
+ - @backstage/backend-common@0.14.1-next.2
37
+
3
38
  ## 0.3.5-next.1
4
39
 
5
40
  ### Patch Changes
package/README.md CHANGED
@@ -14,3 +14,31 @@ other plugins.
14
14
 
15
15
  See [Backstage documentation](https://backstage.io/docs/features/search/search-engines#postgres)
16
16
  for details on how to setup Postgres based search for your Backstage instance.
17
+
18
+ ## Optional Configuration
19
+
20
+ The following is an example of the optional configuration that can be applied when using Postgres as the search backend. Currently this is mostly for just the highlight feature:
21
+
22
+ ```yaml
23
+ search:
24
+ pg:
25
+ highlightOptions:
26
+ useHighlight: true # Used to enable to disable the highlight feature. The default value is true
27
+ maxWord: 35 # Used to set the longest headlines to output. The default value is 35.
28
+ minWord: 15 # Used to set the shortest headlines to output. The default value is 15.
29
+ shortWord: 3 # Words of this length or less will be dropped at the start and end of a headline, unless they are query terms. The default value of three (3) eliminates common English articles.
30
+ highlightAll: false # If true the whole document will be used as the headline, ignoring the preceding three parameters. The default is false.
31
+ maxFragments: 0 # Maximum number of text fragments to display. The default value of zero selects a non-fragment-based headline generation method. A value greater than zero selects fragment-based headline generation (see the linked documentation above for more details).
32
+ fragmentDelimiter: ' ... ' # Delimiter string used to concatenate fragments. Defaults to " ... ".
33
+ ```
34
+
35
+ **Note:** the highlight search term feature uses `ts_headline` which has been known to potentially impact performance. You only need this minimal config to disable it should you have issues:
36
+
37
+ ```yaml
38
+ search:
39
+ pg:
40
+ highlightOptions:
41
+ useHighlight: false
42
+ ```
43
+
44
+ The Postgres documentation on [Highlighting Results](https://www.postgresql.org/docs/current/textsearch-controls.html#TEXTSEARCH-HEADLINE) has more details.
package/config.d.ts ADDED
@@ -0,0 +1,62 @@
1
+ /*
2
+ * Copyright 2021 The Backstage Authors
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ export interface Config {
18
+ /** Configuration options for the search plugin */
19
+ search?: {
20
+ /**
21
+ * Options for PG
22
+ */
23
+ pg?: {
24
+ /**
25
+ * Options for configuring highlight settings
26
+ * See https://www.postgresql.org/docs/current/textsearch-controls.html#TEXTSEARCH-HEADLINE
27
+ */
28
+ highlightOptions?: {
29
+ /**
30
+ * Used to enable to disable the highlight feature. The default value is true
31
+ */
32
+ useHighlight?: boolean;
33
+ /**
34
+ * Used to set the longest headlines to output. The default value is 35.
35
+ */
36
+ maxWords?: number;
37
+ /**
38
+ * Used to set the shortest headlines to output. The default value is 15.
39
+ */
40
+ minWords?: number;
41
+ /**
42
+ * Words of this length or less will be dropped at the start and end of a headline, unless they are query terms.
43
+ * The default value of three (3) eliminates common English articles.
44
+ */
45
+ shortWord?: number;
46
+ /**
47
+ * If true the whole document will be used as the headline, ignoring the preceding three parameters. The default is false.
48
+ */
49
+ highlightAll?: boolean;
50
+ /**
51
+ * Maximum number of text fragments to display. The default value of zero selects a non-fragment-based headline generation method.
52
+ * A value greater than zero selects fragment-based headline generation (see the linked documentation above for more details).
53
+ */
54
+ maxFragments?: number;
55
+ /**
56
+ * Delimiter string used to concatenate fragments. Defaults to " ... ".
57
+ */
58
+ fragmentDelimiter?: string;
59
+ };
60
+ };
61
+ };
62
+ }
package/dist/index.cjs.js CHANGED
@@ -4,6 +4,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var backendCommon = require('@backstage/backend-common');
6
6
  var pluginSearchBackendNode = require('@backstage/plugin-search-backend-node');
7
+ var uuid = require('uuid');
7
8
 
8
9
  async function queryPostgresMajorVersion(knex) {
9
10
  if (knex.client.config.client !== "pg") {
@@ -16,7 +17,10 @@ async function queryPostgresMajorVersion(knex) {
16
17
  return majorVersion;
17
18
  }
18
19
 
19
- const migrationsDir = backendCommon.resolvePackagePath("@backstage/plugin-search-backend-module-pg", "migrations");
20
+ const migrationsDir = backendCommon.resolvePackagePath(
21
+ "@backstage/plugin-search-backend-module-pg",
22
+ "migrations"
23
+ );
20
24
  class DatabaseDocumentStore {
21
25
  constructor(db) {
22
26
  this.db = db;
@@ -25,10 +29,14 @@ class DatabaseDocumentStore {
25
29
  try {
26
30
  const majorVersion = await queryPostgresMajorVersion(knex);
27
31
  if (majorVersion < 12) {
28
- throw new Error(`The PgSearchEngine requires at least postgres version 12 (but is running on ${majorVersion})`);
32
+ throw new Error(
33
+ `The PgSearchEngine requires at least postgres version 12 (but is running on ${majorVersion})`
34
+ );
29
35
  }
30
36
  } catch {
31
- throw new Error("The PgSearchEngine is only supported when using a postgres database (>=12.x)");
37
+ throw new Error(
38
+ "The PgSearchEngine is only supported when using a postgres database (>=12.x)"
39
+ );
32
40
  }
33
41
  await knex.migrate.latest({
34
42
  directory: migrationsDir
@@ -50,19 +58,33 @@ class DatabaseDocumentStore {
50
58
  return this.db.transaction();
51
59
  }
52
60
  async prepareInsert(tx) {
53
- await tx.raw("CREATE TEMP TABLE documents_to_insert (type text NOT NULL, document jsonb NOT NULL, hash bytea NOT NULL GENERATED ALWAYS AS (sha256(replace(document::text || type, '\\', '\\\\')::bytea)) STORED) ON COMMIT DROP");
61
+ await tx.raw(
62
+ "CREATE TEMP TABLE documents_to_insert (type text NOT NULL, document jsonb NOT NULL, hash bytea NOT NULL GENERATED ALWAYS AS (sha256(replace(document::text || type, '\\', '\\\\')::bytea)) STORED) ON COMMIT DROP"
63
+ );
54
64
  }
55
65
  async completeInsert(tx, type) {
56
- await tx.insert(tx("documents_to_insert").select("type", "document", "hash")).into(tx.raw("documents (type, document, hash)")).onConflict("hash").ignore();
57
- await tx("documents").where({ type }).whereNotIn("hash", tx("documents_to_insert").select("hash")).delete();
66
+ await tx.insert(
67
+ tx("documents_to_insert").select(
68
+ "type",
69
+ "document",
70
+ "hash"
71
+ )
72
+ ).into(tx.raw("documents (type, document, hash)")).onConflict("hash").ignore();
73
+ await tx("documents").where({ type }).whereNotIn(
74
+ "hash",
75
+ tx("documents_to_insert").select("hash")
76
+ ).delete();
58
77
  }
59
78
  async insertDocuments(tx, type, documents) {
60
- await tx("documents_to_insert").insert(documents.map((document) => ({
61
- type,
62
- document
63
- })));
79
+ await tx("documents_to_insert").insert(
80
+ documents.map((document) => ({
81
+ type,
82
+ document
83
+ }))
84
+ );
64
85
  }
65
- async query(tx, { types, pgTerm, fields, offset, limit }) {
86
+ async query(tx, searchQuery) {
87
+ const { types, pgTerm, fields, offset, limit, options } = searchQuery;
66
88
  const query = tx("documents");
67
89
  if (pgTerm) {
68
90
  query.from(tx.raw("documents, to_tsquery('english', ?) query", pgTerm)).whereRaw("query @@ body");
@@ -77,11 +99,21 @@ class DatabaseDocumentStore {
77
99
  const value = fields[key];
78
100
  const valueArray = Array.isArray(value) ? value : [value];
79
101
  const valueCompare = valueArray.map((v) => ({ [key]: v })).map((v) => JSON.stringify(v));
80
- query.whereRaw(`(${valueCompare.map(() => "document @> ?").join(" OR ")})`, valueCompare);
102
+ query.whereRaw(
103
+ `(${valueCompare.map(() => "document @> ?").join(" OR ")})`,
104
+ valueCompare
105
+ );
81
106
  });
82
107
  }
83
108
  query.select("type", "document");
84
- if (pgTerm) {
109
+ if (pgTerm && options.useHighlight) {
110
+ const headlineOptions = `MaxWords=${options.maxWords}, MinWords=${options.minWords}, ShortWord=${options.shortWord}, HighlightAll=${options.highlightAll}, MaxFragments=${options.maxFragments}, FragmentDelimiter=${options.fragmentDelimiter}, StartSel=${options.preTag}, StopSel=${options.postTag}`;
111
+ query.select(tx.raw('ts_rank_cd(body, query) AS "rank"')).select(
112
+ tx.raw(
113
+ `ts_headline('english', document, query, '${headlineOptions}') as "highlight"`
114
+ )
115
+ ).orderBy("rank", "desc");
116
+ } else if (pgTerm && !options.useHighlight) {
85
117
  query.select(tx.raw('ts_rank_cd(body, query) AS "rank"')).orderBy("rank", "desc");
86
118
  } else {
87
119
  query.select(tx.raw("1 as rank"));
@@ -125,16 +157,42 @@ class PgSearchEngineIndexer extends pluginSearchBackendNode.BatchSearchEngineInd
125
157
  }
126
158
 
127
159
  class PgSearchEngine {
128
- constructor(databaseStore) {
160
+ constructor(databaseStore, config) {
129
161
  this.databaseStore = databaseStore;
162
+ var _a, _b, _c, _d, _e, _f, _g;
163
+ const uuidTag = uuid.v4();
164
+ const highlightConfig = config.getOptionalConfig(
165
+ "search.pg.highlightOptions"
166
+ );
167
+ const highlightOptions = {
168
+ preTag: `<${uuidTag}>`,
169
+ postTag: `</${uuidTag}>`,
170
+ useHighlight: (_a = highlightConfig == null ? void 0 : highlightConfig.getOptionalBoolean("useHighlight")) != null ? _a : true,
171
+ maxWords: (_b = highlightConfig == null ? void 0 : highlightConfig.getOptionalNumber("maxWords")) != null ? _b : 35,
172
+ minWords: (_c = highlightConfig == null ? void 0 : highlightConfig.getOptionalNumber("minWords")) != null ? _c : 15,
173
+ shortWord: (_d = highlightConfig == null ? void 0 : highlightConfig.getOptionalNumber("shortWord")) != null ? _d : 3,
174
+ highlightAll: (_e = highlightConfig == null ? void 0 : highlightConfig.getOptionalBoolean("highlightAll")) != null ? _e : false,
175
+ maxFragments: (_f = highlightConfig == null ? void 0 : highlightConfig.getOptionalNumber("maxFragments")) != null ? _f : 0,
176
+ fragmentDelimiter: (_g = highlightConfig == null ? void 0 : highlightConfig.getOptionalString("fragmentDelimiter")) != null ? _g : " ... "
177
+ };
178
+ this.highlightOptions = highlightOptions;
130
179
  }
131
180
  static async from(options) {
132
- return new PgSearchEngine(await DatabaseDocumentStore.create(await options.database.getClient()));
181
+ return new PgSearchEngine(
182
+ await DatabaseDocumentStore.create(await options.database.getClient()),
183
+ options.config
184
+ );
185
+ }
186
+ static async fromConfig(config, options) {
187
+ return new PgSearchEngine(
188
+ await DatabaseDocumentStore.create(await options.database.getClient()),
189
+ config
190
+ );
133
191
  }
134
192
  static async supported(database) {
135
193
  return await DatabaseDocumentStore.supported(await database.getClient());
136
194
  }
137
- translator(query) {
195
+ translator(query, options) {
138
196
  const pageSize = 25;
139
197
  const { page } = decodePageCursor(query.pageCursor);
140
198
  const offset = page * pageSize;
@@ -145,7 +203,8 @@ class PgSearchEngine {
145
203
  fields: query.filters,
146
204
  types: query.types,
147
205
  offset,
148
- limit
206
+ limit,
207
+ options: options.highlightOptions
149
208
  },
150
209
  pageSize
151
210
  };
@@ -161,19 +220,35 @@ class PgSearchEngine {
161
220
  });
162
221
  }
163
222
  async query(query) {
164
- const { pgQuery, pageSize } = this.translator(query);
165
- const rows = await this.databaseStore.transaction(async (tx) => this.databaseStore.query(tx, pgQuery));
223
+ const { pgQuery, pageSize } = this.translator(query, {
224
+ highlightOptions: this.highlightOptions
225
+ });
226
+ const rows = await this.databaseStore.transaction(
227
+ async (tx) => this.databaseStore.query(tx, pgQuery)
228
+ );
166
229
  const { page } = decodePageCursor(query.pageCursor);
167
230
  const hasNextPage = rows.length > pageSize;
168
231
  const hasPreviousPage = page > 0;
169
232
  const pageRows = rows.slice(0, pageSize);
170
233
  const nextPageCursor = hasNextPage ? encodePageCursor({ page: page + 1 }) : void 0;
171
234
  const previousPageCursor = hasPreviousPage ? encodePageCursor({ page: page - 1 }) : void 0;
172
- const results = pageRows.map(({ type, document }, index) => ({
173
- type,
174
- document,
175
- rank: page * pageSize + index + 1
176
- }));
235
+ const results = pageRows.map(
236
+ ({ type, document, highlight }, index) => ({
237
+ type,
238
+ document,
239
+ rank: page * pageSize + index + 1,
240
+ highlight: {
241
+ preTag: pgQuery.options.preTag,
242
+ postTag: pgQuery.options.postTag,
243
+ fields: highlight ? {
244
+ text: highlight.text,
245
+ title: highlight.title,
246
+ location: highlight.location,
247
+ path: ""
248
+ } : {}
249
+ }
250
+ })
251
+ );
177
252
  return { results, nextPageCursor, previousPageCursor };
178
253
  }
179
254
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs.js","sources":["../src/database/util.ts","../src/database/DatabaseDocumentStore.ts","../src/PgSearchEngine/PgSearchEngineIndexer.ts","../src/PgSearchEngine/PgSearchEngine.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { Knex } from 'knex';\n\nexport async function queryPostgresMajorVersion(knex: Knex): Promise<number> {\n if (knex.client.config.client !== 'pg') {\n throw new Error(\"Can't resolve version, not a postgres database\");\n }\n\n const { rows } = await knex.raw('SHOW server_version_num');\n const [result] = rows;\n const version = +result.server_version_num;\n const majorVersion = Math.floor(version / 10000);\n return majorVersion;\n}\n","/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { resolvePackagePath } from '@backstage/backend-common';\nimport { IndexableDocument } from '@backstage/plugin-search-common';\nimport { Knex } from 'knex';\nimport {\n DatabaseStore,\n DocumentResultRow,\n PgSearchQuery,\n RawDocumentRow,\n} from './types';\nimport { queryPostgresMajorVersion } from './util';\n\nconst migrationsDir = resolvePackagePath(\n '@backstage/plugin-search-backend-module-pg',\n 'migrations',\n);\n\nexport class DatabaseDocumentStore implements DatabaseStore {\n static async create(knex: Knex): Promise<DatabaseDocumentStore> {\n try {\n const majorVersion = await queryPostgresMajorVersion(knex);\n\n if (majorVersion < 12) {\n // We are using some features (like generated columns) that aren't\n // available in older postgres versions.\n throw new Error(\n `The PgSearchEngine requires at least postgres version 12 (but is running on ${majorVersion})`,\n );\n }\n } catch {\n // Actually both mysql and sqlite have a full text search, too. We could\n // implement them separately or add them here.\n throw new Error(\n 'The PgSearchEngine is only supported when using a postgres database (>=12.x)',\n );\n }\n\n await knex.migrate.latest({\n directory: migrationsDir,\n });\n return new DatabaseDocumentStore(knex);\n }\n\n static async supported(knex: Knex): Promise<boolean> {\n try {\n const majorVersion = await queryPostgresMajorVersion(knex);\n\n return majorVersion >= 12;\n } catch {\n return false;\n }\n }\n\n constructor(private readonly db: Knex) {}\n\n async transaction<T>(fn: (tx: Knex.Transaction) => Promise<T>): Promise<T> {\n return await this.db.transaction(fn);\n }\n\n async getTransaction(): Promise<Knex.Transaction> {\n return this.db.transaction();\n }\n\n async prepareInsert(tx: Knex.Transaction): Promise<void> {\n // We create a temporary table to collect the hashes of the documents that\n // we expect to be in the documents table at the end. The table is deleted\n // at the end of the transaction.\n // The hash makes sure that we generate a new row for every change.\n await tx.raw(\n 'CREATE TEMP TABLE documents_to_insert (' +\n 'type text NOT NULL, ' +\n 'document jsonb NOT NULL, ' +\n // Generating the hash requires a trick, as the text to bytea\n // conversation runs into errors in case the text contains a backslash.\n // Therefore we have to escape them.\n \"hash bytea NOT NULL GENERATED ALWAYS AS (sha256(replace(document::text || type, '\\\\', '\\\\\\\\')::bytea)) STORED\" +\n ') ON COMMIT DROP',\n );\n }\n\n async completeInsert(tx: Knex.Transaction, type: string): Promise<void> {\n // Copy all new rows into the documents table\n await tx\n .insert(\n tx<RawDocumentRow>('documents_to_insert').select(\n 'type',\n 'document',\n 'hash',\n ),\n )\n .into(tx.raw('documents (type, document, hash)'))\n .onConflict('hash')\n .ignore();\n\n // Delete all documents that we don't expect (deleted and changed)\n await tx<RawDocumentRow>('documents')\n .where({ type })\n .whereNotIn(\n 'hash',\n tx<RawDocumentRow>('documents_to_insert').select('hash'),\n )\n .delete();\n }\n\n async insertDocuments(\n tx: Knex.Transaction,\n type: string,\n documents: IndexableDocument[],\n ): Promise<void> {\n // Insert all documents into the temporary table to process them later\n await tx<DocumentResultRow>('documents_to_insert').insert(\n documents.map(document => ({\n type,\n document,\n })),\n );\n }\n\n async query(\n tx: Knex.Transaction,\n { types, pgTerm, fields, offset, limit }: PgSearchQuery,\n ): Promise<DocumentResultRow[]> {\n // Builds a query like:\n // SELECT ts_rank_cd(body, query) AS rank, type, document\n // FROM documents, to_tsquery('english', 'consent') query\n // WHERE query @@ body AND (document @> '{\"kind\": \"API\"}')\n // ORDER BY rank DESC\n // LIMIT 10;\n const query = tx<DocumentResultRow>('documents');\n\n if (pgTerm) {\n query\n .from(tx.raw(\"documents, to_tsquery('english', ?) query\", pgTerm))\n .whereRaw('query @@ body');\n } else {\n query.from('documents');\n }\n\n if (types) {\n query.whereIn('type', types);\n }\n\n if (fields) {\n Object.keys(fields).forEach(key => {\n const value = fields[key];\n const valueArray = Array.isArray(value) ? value : [value];\n const valueCompare = valueArray\n .map(v => ({ [key]: v }))\n .map(v => JSON.stringify(v));\n query.whereRaw(\n `(${valueCompare.map(() => 'document @> ?').join(' OR ')})`,\n valueCompare,\n );\n });\n }\n\n query.select('type', 'document');\n\n if (pgTerm) {\n query\n .select(tx.raw('ts_rank_cd(body, query) AS \"rank\"'))\n .orderBy('rank', 'desc');\n } else {\n query.select(tx.raw('1 as rank'));\n }\n\n return await query.offset(offset).limit(limit);\n }\n}\n","/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node';\nimport { IndexableDocument } from '@backstage/plugin-search-common';\nimport { Knex } from 'knex';\nimport { DatabaseStore } from '../database';\n\nexport type PgSearchEngineIndexerOptions = {\n batchSize: number;\n type: string;\n databaseStore: DatabaseStore;\n};\n\nexport class PgSearchEngineIndexer extends BatchSearchEngineIndexer {\n private store: DatabaseStore;\n private type: string;\n private tx: Knex.Transaction | undefined;\n\n constructor(options: PgSearchEngineIndexerOptions) {\n super({ batchSize: options.batchSize });\n this.store = options.databaseStore;\n this.type = options.type;\n }\n\n async initialize(): Promise<void> {\n this.tx = await this.store.getTransaction();\n try {\n await this.store.prepareInsert(this.tx);\n } catch (e) {\n // In case of error, rollback the transaction and re-throw the error so\n // that the stream can be closed and destroyed properly.\n this.tx.rollback(e);\n throw e;\n }\n }\n\n async index(documents: IndexableDocument[]): Promise<void> {\n try {\n await this.store.insertDocuments(this.tx!, this.type, documents);\n } catch (e) {\n // In case of error, rollback the transaction and re-throw the error so\n // that the stream can be closed and destroyed properly.\n this.tx!.rollback(e);\n throw e;\n }\n }\n\n async finalize(): Promise<void> {\n // Attempt to complete and commit the transaction.\n try {\n await this.store.completeInsert(this.tx!, this.type);\n this.tx!.commit();\n } catch (e) {\n // Otherwise, rollback the transaction and re-throw the error so that the\n // stream can be closed and destroyed properly.\n this.tx!.rollback!(e);\n throw e;\n }\n }\n}\n","/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { PluginDatabaseManager } from '@backstage/backend-common';\nimport { SearchEngine } from '@backstage/plugin-search-backend-node';\nimport {\n SearchQuery,\n IndexableResultSet,\n IndexableResult,\n} from '@backstage/plugin-search-common';\nimport { PgSearchEngineIndexer } from './PgSearchEngineIndexer';\nimport {\n DatabaseDocumentStore,\n DatabaseStore,\n PgSearchQuery,\n} from '../database';\n\nexport type ConcretePgSearchQuery = {\n pgQuery: PgSearchQuery;\n pageSize: number;\n};\n\nexport class PgSearchEngine implements SearchEngine {\n constructor(private readonly databaseStore: DatabaseStore) {}\n\n static async from(options: {\n database: PluginDatabaseManager;\n }): Promise<PgSearchEngine> {\n return new PgSearchEngine(\n await DatabaseDocumentStore.create(await options.database.getClient()),\n );\n }\n\n static async supported(database: PluginDatabaseManager): Promise<boolean> {\n return await DatabaseDocumentStore.supported(await database.getClient());\n }\n\n translator(query: SearchQuery): ConcretePgSearchQuery {\n const pageSize = 25;\n const { page } = decodePageCursor(query.pageCursor);\n const offset = page * pageSize;\n // We request more result to know whether there is another page\n const limit = pageSize + 1;\n\n return {\n pgQuery: {\n pgTerm: query.term\n .split(/\\s/)\n .map(p => p.replace(/[\\0()|&:*!]/g, '').trim())\n .filter(p => p !== '')\n .map(p => `(${JSON.stringify(p)} | ${JSON.stringify(p)}:*)`)\n .join('&'),\n fields: query.filters as Record<string, string | string[]>,\n types: query.types,\n offset,\n limit,\n },\n pageSize,\n };\n }\n\n setTranslator(\n translator: (query: SearchQuery) => ConcretePgSearchQuery,\n ): void {\n this.translator = translator;\n }\n\n async getIndexer(type: string) {\n return new PgSearchEngineIndexer({\n batchSize: 1000,\n type,\n databaseStore: this.databaseStore,\n });\n }\n\n async query(query: SearchQuery): Promise<IndexableResultSet> {\n const { pgQuery, pageSize } = this.translator(query);\n\n const rows = await this.databaseStore.transaction(async tx =>\n this.databaseStore.query(tx, pgQuery),\n );\n\n // We requested one result more than the page size to know whether there is\n // another page.\n const { page } = decodePageCursor(query.pageCursor);\n const hasNextPage = rows.length > pageSize;\n const hasPreviousPage = page > 0;\n const pageRows = rows.slice(0, pageSize);\n const nextPageCursor = hasNextPage\n ? encodePageCursor({ page: page + 1 })\n : undefined;\n const previousPageCursor = hasPreviousPage\n ? encodePageCursor({ page: page - 1 })\n : undefined;\n\n const results = pageRows.map(\n ({ type, document }, index): IndexableResult => ({\n type,\n document,\n rank: page * pageSize + index + 1,\n }),\n );\n\n return { results, nextPageCursor, previousPageCursor };\n }\n}\n\nexport function decodePageCursor(pageCursor?: string): { page: number } {\n if (!pageCursor) {\n return { page: 0 };\n }\n\n return {\n page: Number(Buffer.from(pageCursor, 'base64').toString('utf-8')),\n };\n}\n\nexport function encodePageCursor({ page }: { page: number }): string {\n return Buffer.from(`${page}`, 'utf-8').toString('base64');\n}\n"],"names":["resolvePackagePath","BatchSearchEngineIndexer"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs.js","sources":["../src/database/util.ts","../src/database/DatabaseDocumentStore.ts","../src/PgSearchEngine/PgSearchEngineIndexer.ts","../src/PgSearchEngine/PgSearchEngine.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { Knex } from 'knex';\n\nexport async function queryPostgresMajorVersion(knex: Knex): Promise<number> {\n if (knex.client.config.client !== 'pg') {\n throw new Error(\"Can't resolve version, not a postgres database\");\n }\n\n const { rows } = await knex.raw('SHOW server_version_num');\n const [result] = rows;\n const version = +result.server_version_num;\n const majorVersion = Math.floor(version / 10000);\n return majorVersion;\n}\n","/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { resolvePackagePath } from '@backstage/backend-common';\nimport { IndexableDocument } from '@backstage/plugin-search-common';\nimport { Knex } from 'knex';\nimport {\n DatabaseStore,\n DocumentResultRow,\n PgSearchQuery,\n RawDocumentRow,\n} from './types';\nimport { queryPostgresMajorVersion } from './util';\n\nconst migrationsDir = resolvePackagePath(\n '@backstage/plugin-search-backend-module-pg',\n 'migrations',\n);\n\nexport class DatabaseDocumentStore implements DatabaseStore {\n static async create(knex: Knex): Promise<DatabaseDocumentStore> {\n try {\n const majorVersion = await queryPostgresMajorVersion(knex);\n\n if (majorVersion < 12) {\n // We are using some features (like generated columns) that aren't\n // available in older postgres versions.\n throw new Error(\n `The PgSearchEngine requires at least postgres version 12 (but is running on ${majorVersion})`,\n );\n }\n } catch {\n // Actually both mysql and sqlite have a full text search, too. We could\n // implement them separately or add them here.\n throw new Error(\n 'The PgSearchEngine is only supported when using a postgres database (>=12.x)',\n );\n }\n\n await knex.migrate.latest({\n directory: migrationsDir,\n });\n return new DatabaseDocumentStore(knex);\n }\n\n static async supported(knex: Knex): Promise<boolean> {\n try {\n const majorVersion = await queryPostgresMajorVersion(knex);\n\n return majorVersion >= 12;\n } catch {\n return false;\n }\n }\n\n constructor(private readonly db: Knex) {}\n\n async transaction<T>(fn: (tx: Knex.Transaction) => Promise<T>): Promise<T> {\n return await this.db.transaction(fn);\n }\n\n async getTransaction(): Promise<Knex.Transaction> {\n return this.db.transaction();\n }\n\n async prepareInsert(tx: Knex.Transaction): Promise<void> {\n // We create a temporary table to collect the hashes of the documents that\n // we expect to be in the documents table at the end. The table is deleted\n // at the end of the transaction.\n // The hash makes sure that we generate a new row for every change.\n await tx.raw(\n 'CREATE TEMP TABLE documents_to_insert (' +\n 'type text NOT NULL, ' +\n 'document jsonb NOT NULL, ' +\n // Generating the hash requires a trick, as the text to bytea\n // conversation runs into errors in case the text contains a backslash.\n // Therefore we have to escape them.\n \"hash bytea NOT NULL GENERATED ALWAYS AS (sha256(replace(document::text || type, '\\\\', '\\\\\\\\')::bytea)) STORED\" +\n ') ON COMMIT DROP',\n );\n }\n\n async completeInsert(tx: Knex.Transaction, type: string): Promise<void> {\n // Copy all new rows into the documents table\n await tx\n .insert(\n tx<RawDocumentRow>('documents_to_insert').select(\n 'type',\n 'document',\n 'hash',\n ),\n )\n .into(tx.raw('documents (type, document, hash)'))\n .onConflict('hash')\n .ignore();\n\n // Delete all documents that we don't expect (deleted and changed)\n await tx<RawDocumentRow>('documents')\n .where({ type })\n .whereNotIn(\n 'hash',\n tx<RawDocumentRow>('documents_to_insert').select('hash'),\n )\n .delete();\n }\n\n async insertDocuments(\n tx: Knex.Transaction,\n type: string,\n documents: IndexableDocument[],\n ): Promise<void> {\n // Insert all documents into the temporary table to process them later\n await tx<DocumentResultRow>('documents_to_insert').insert(\n documents.map(document => ({\n type,\n document,\n })),\n );\n }\n\n async query(\n tx: Knex.Transaction,\n searchQuery: PgSearchQuery,\n ): Promise<DocumentResultRow[]> {\n const { types, pgTerm, fields, offset, limit, options } = searchQuery;\n // TODO(awanlin): We should make the language a parameter so that we can support more then just english\n // Builds a query like:\n // SELECT ts_rank_cd(body, query) AS rank, type, document,\n // ts_headline('english', document, query) AS highlight\n // FROM documents, to_tsquery('english', 'consent') query\n // WHERE query @@ body AND (document @> '{\"kind\": \"API\"}')\n // ORDER BY rank DESC\n // LIMIT 10;\n const query = tx<DocumentResultRow>('documents');\n\n if (pgTerm) {\n query\n .from(tx.raw(\"documents, to_tsquery('english', ?) query\", pgTerm))\n .whereRaw('query @@ body');\n } else {\n query.from('documents');\n }\n\n if (types) {\n query.whereIn('type', types);\n }\n\n if (fields) {\n Object.keys(fields).forEach(key => {\n const value = fields[key];\n const valueArray = Array.isArray(value) ? value : [value];\n const valueCompare = valueArray\n .map(v => ({ [key]: v }))\n .map(v => JSON.stringify(v));\n query.whereRaw(\n `(${valueCompare.map(() => 'document @> ?').join(' OR ')})`,\n valueCompare,\n );\n });\n }\n\n query.select('type', 'document');\n\n if (pgTerm && options.useHighlight) {\n const headlineOptions = `MaxWords=${options.maxWords}, MinWords=${options.minWords}, ShortWord=${options.shortWord}, HighlightAll=${options.highlightAll}, MaxFragments=${options.maxFragments}, FragmentDelimiter=${options.fragmentDelimiter}, StartSel=${options.preTag}, StopSel=${options.postTag}`;\n query\n .select(tx.raw('ts_rank_cd(body, query) AS \"rank\"'))\n .select(\n tx.raw(\n `ts_headline(\\'english\\', document, query, '${headlineOptions}') as \"highlight\"`,\n ),\n )\n .orderBy('rank', 'desc');\n } else if (pgTerm && !options.useHighlight) {\n query\n .select(tx.raw('ts_rank_cd(body, query) AS \"rank\"'))\n .orderBy('rank', 'desc');\n } else {\n query.select(tx.raw('1 as rank'));\n }\n\n return await query.offset(offset).limit(limit);\n }\n}\n","/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node';\nimport { IndexableDocument } from '@backstage/plugin-search-common';\nimport { Knex } from 'knex';\nimport { DatabaseStore } from '../database';\n\nexport type PgSearchEngineIndexerOptions = {\n batchSize: number;\n type: string;\n databaseStore: DatabaseStore;\n};\n\nexport class PgSearchEngineIndexer extends BatchSearchEngineIndexer {\n private store: DatabaseStore;\n private type: string;\n private tx: Knex.Transaction | undefined;\n\n constructor(options: PgSearchEngineIndexerOptions) {\n super({ batchSize: options.batchSize });\n this.store = options.databaseStore;\n this.type = options.type;\n }\n\n async initialize(): Promise<void> {\n this.tx = await this.store.getTransaction();\n try {\n await this.store.prepareInsert(this.tx);\n } catch (e) {\n // In case of error, rollback the transaction and re-throw the error so\n // that the stream can be closed and destroyed properly.\n this.tx.rollback(e);\n throw e;\n }\n }\n\n async index(documents: IndexableDocument[]): Promise<void> {\n try {\n await this.store.insertDocuments(this.tx!, this.type, documents);\n } catch (e) {\n // In case of error, rollback the transaction and re-throw the error so\n // that the stream can be closed and destroyed properly.\n this.tx!.rollback(e);\n throw e;\n }\n }\n\n async finalize(): Promise<void> {\n // Attempt to complete and commit the transaction.\n try {\n await this.store.completeInsert(this.tx!, this.type);\n this.tx!.commit();\n } catch (e) {\n // Otherwise, rollback the transaction and re-throw the error so that the\n // stream can be closed and destroyed properly.\n this.tx!.rollback!(e);\n throw e;\n }\n }\n}\n","/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { PluginDatabaseManager } from '@backstage/backend-common';\nimport { SearchEngine } from '@backstage/plugin-search-common';\nimport {\n SearchQuery,\n IndexableResultSet,\n IndexableResult,\n} from '@backstage/plugin-search-common';\nimport { PgSearchEngineIndexer } from './PgSearchEngineIndexer';\nimport {\n DatabaseDocumentStore,\n DatabaseStore,\n PgSearchQuery,\n} from '../database';\nimport { v4 as uuid } from 'uuid';\nimport { Config } from '@backstage/config';\n\n/**\n * Search query that the Postgres search engine understands.\n * @public\n */\nexport type ConcretePgSearchQuery = {\n pgQuery: PgSearchQuery;\n pageSize: number;\n};\n\n/**\n * Options available for the Postgres specific query translator.\n * @public\n */\nexport type PgSearchQueryTranslatorOptions = {\n highlightOptions: PgSearchHighlightOptions;\n};\n\n/**\n * Postgres specific query translator.\n * @public\n */\nexport type PgSearchQueryTranslator = (\n query: SearchQuery,\n options: PgSearchQueryTranslatorOptions,\n) => ConcretePgSearchQuery;\n\n/**\n * Options to instantiate PgSearchEngine\n * @public\n */\nexport type PgSearchOptions = {\n database: PluginDatabaseManager;\n};\n\n/**\n * Options for highlighting search terms\n * @public\n */\nexport type PgSearchHighlightOptions = {\n useHighlight?: boolean;\n maxWords?: number;\n minWords?: number;\n shortWord?: number;\n highlightAll?: boolean;\n maxFragments?: number;\n fragmentDelimiter?: string;\n preTag: string;\n postTag: string;\n};\n\nexport class PgSearchEngine implements SearchEngine {\n private readonly highlightOptions: PgSearchHighlightOptions;\n\n /**\n * @deprecated This will be marked as private in a future release, please us fromConfig instead\n */\n constructor(private readonly databaseStore: DatabaseStore, config: Config) {\n const uuidTag = uuid();\n const highlightConfig = config.getOptionalConfig(\n 'search.pg.highlightOptions',\n );\n\n const highlightOptions: PgSearchHighlightOptions = {\n preTag: `<${uuidTag}>`,\n postTag: `</${uuidTag}>`,\n useHighlight: highlightConfig?.getOptionalBoolean('useHighlight') ?? true,\n maxWords: highlightConfig?.getOptionalNumber('maxWords') ?? 35,\n minWords: highlightConfig?.getOptionalNumber('minWords') ?? 15,\n shortWord: highlightConfig?.getOptionalNumber('shortWord') ?? 3,\n highlightAll:\n highlightConfig?.getOptionalBoolean('highlightAll') ?? false,\n maxFragments: highlightConfig?.getOptionalNumber('maxFragments') ?? 0,\n fragmentDelimiter:\n highlightConfig?.getOptionalString('fragmentDelimiter') ?? ' ... ',\n };\n this.highlightOptions = highlightOptions;\n }\n\n /**\n * @deprecated This will be removed in a future release, please us fromConfig instead\n */\n static async from(options: {\n database: PluginDatabaseManager;\n config: Config;\n }): Promise<PgSearchEngine> {\n return new PgSearchEngine(\n await DatabaseDocumentStore.create(await options.database.getClient()),\n options.config,\n );\n }\n\n static async fromConfig(config: Config, options: PgSearchOptions) {\n return new PgSearchEngine(\n await DatabaseDocumentStore.create(await options.database.getClient()),\n config,\n );\n }\n\n static async supported(database: PluginDatabaseManager): Promise<boolean> {\n return await DatabaseDocumentStore.supported(await database.getClient());\n }\n\n translator(\n query: SearchQuery,\n options: PgSearchQueryTranslatorOptions,\n ): ConcretePgSearchQuery {\n const pageSize = 25;\n const { page } = decodePageCursor(query.pageCursor);\n const offset = page * pageSize;\n // We request more result to know whether there is another page\n const limit = pageSize + 1;\n\n return {\n pgQuery: {\n pgTerm: query.term\n .split(/\\s/)\n .map(p => p.replace(/[\\0()|&:*!]/g, '').trim())\n .filter(p => p !== '')\n .map(p => `(${JSON.stringify(p)} | ${JSON.stringify(p)}:*)`)\n .join('&'),\n fields: query.filters as Record<string, string | string[]>,\n types: query.types,\n offset,\n limit,\n options: options.highlightOptions,\n },\n pageSize,\n };\n }\n\n setTranslator(translator: PgSearchQueryTranslator) {\n this.translator = translator;\n }\n\n async getIndexer(type: string) {\n return new PgSearchEngineIndexer({\n batchSize: 1000,\n type,\n databaseStore: this.databaseStore,\n });\n }\n\n async query(query: SearchQuery): Promise<IndexableResultSet> {\n const { pgQuery, pageSize } = this.translator(query, {\n highlightOptions: this.highlightOptions,\n });\n\n const rows = await this.databaseStore.transaction(async tx =>\n this.databaseStore.query(tx, pgQuery),\n );\n\n // We requested one result more than the page size to know whether there is\n // another page.\n const { page } = decodePageCursor(query.pageCursor);\n const hasNextPage = rows.length > pageSize;\n const hasPreviousPage = page > 0;\n const pageRows = rows.slice(0, pageSize);\n const nextPageCursor = hasNextPage\n ? encodePageCursor({ page: page + 1 })\n : undefined;\n const previousPageCursor = hasPreviousPage\n ? encodePageCursor({ page: page - 1 })\n : undefined;\n\n const results = pageRows.map(\n ({ type, document, highlight }, index): IndexableResult => ({\n type,\n document,\n rank: page * pageSize + index + 1,\n highlight: {\n preTag: pgQuery.options.preTag,\n postTag: pgQuery.options.postTag,\n fields: highlight\n ? {\n text: highlight.text,\n title: highlight.title,\n location: highlight.location,\n path: '',\n }\n : {},\n },\n }),\n );\n\n return { results, nextPageCursor, previousPageCursor };\n }\n}\n\nexport function decodePageCursor(pageCursor?: string): { page: number } {\n if (!pageCursor) {\n return { page: 0 };\n }\n\n return {\n page: Number(Buffer.from(pageCursor, 'base64').toString('utf-8')),\n };\n}\n\nexport function encodePageCursor({ page }: { page: number }): string {\n return Buffer.from(`${page}`, 'utf-8').toString('base64');\n}\n"],"names":["resolvePackagePath","BatchSearchEngineIndexer","uuid"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,7 +1,87 @@
1
- import { IndexableDocument, SearchQuery, IndexableResultSet } from '@backstage/plugin-search-common';
1
+ import { IndexableDocument, SearchQuery, SearchEngine, IndexableResultSet } from '@backstage/plugin-search-common';
2
2
  import { Knex } from 'knex';
3
3
  import { PluginDatabaseManager } from '@backstage/backend-common';
4
- import { BatchSearchEngineIndexer, SearchEngine } from '@backstage/plugin-search-backend-node';
4
+ import { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node';
5
+ import { Config } from '@backstage/config';
6
+
7
+ declare type PgSearchEngineIndexerOptions = {
8
+ batchSize: number;
9
+ type: string;
10
+ databaseStore: DatabaseStore;
11
+ };
12
+ declare class PgSearchEngineIndexer extends BatchSearchEngineIndexer {
13
+ private store;
14
+ private type;
15
+ private tx;
16
+ constructor(options: PgSearchEngineIndexerOptions);
17
+ initialize(): Promise<void>;
18
+ index(documents: IndexableDocument[]): Promise<void>;
19
+ finalize(): Promise<void>;
20
+ }
21
+
22
+ /**
23
+ * Search query that the Postgres search engine understands.
24
+ * @public
25
+ */
26
+ declare type ConcretePgSearchQuery = {
27
+ pgQuery: PgSearchQuery;
28
+ pageSize: number;
29
+ };
30
+ /**
31
+ * Options available for the Postgres specific query translator.
32
+ * @public
33
+ */
34
+ declare type PgSearchQueryTranslatorOptions = {
35
+ highlightOptions: PgSearchHighlightOptions;
36
+ };
37
+ /**
38
+ * Postgres specific query translator.
39
+ * @public
40
+ */
41
+ declare type PgSearchQueryTranslator = (query: SearchQuery, options: PgSearchQueryTranslatorOptions) => ConcretePgSearchQuery;
42
+ /**
43
+ * Options to instantiate PgSearchEngine
44
+ * @public
45
+ */
46
+ declare type PgSearchOptions = {
47
+ database: PluginDatabaseManager;
48
+ };
49
+ /**
50
+ * Options for highlighting search terms
51
+ * @public
52
+ */
53
+ declare type PgSearchHighlightOptions = {
54
+ useHighlight?: boolean;
55
+ maxWords?: number;
56
+ minWords?: number;
57
+ shortWord?: number;
58
+ highlightAll?: boolean;
59
+ maxFragments?: number;
60
+ fragmentDelimiter?: string;
61
+ preTag: string;
62
+ postTag: string;
63
+ };
64
+ declare class PgSearchEngine implements SearchEngine {
65
+ private readonly databaseStore;
66
+ private readonly highlightOptions;
67
+ /**
68
+ * @deprecated This will be marked as private in a future release, please us fromConfig instead
69
+ */
70
+ constructor(databaseStore: DatabaseStore, config: Config);
71
+ /**
72
+ * @deprecated This will be removed in a future release, please us fromConfig instead
73
+ */
74
+ static from(options: {
75
+ database: PluginDatabaseManager;
76
+ config: Config;
77
+ }): Promise<PgSearchEngine>;
78
+ static fromConfig(config: Config, options: PgSearchOptions): Promise<PgSearchEngine>;
79
+ static supported(database: PluginDatabaseManager): Promise<boolean>;
80
+ translator(query: SearchQuery, options: PgSearchQueryTranslatorOptions): ConcretePgSearchQuery;
81
+ setTranslator(translator: PgSearchQueryTranslator): void;
82
+ getIndexer(type: string): Promise<PgSearchEngineIndexer>;
83
+ query(query: SearchQuery): Promise<IndexableResultSet>;
84
+ }
5
85
 
6
86
  interface PgSearchQuery {
7
87
  fields?: Record<string, string | string[]>;
@@ -9,6 +89,7 @@ interface PgSearchQuery {
9
89
  pgTerm?: string;
10
90
  offset: number;
11
91
  limit: number;
92
+ options: PgSearchHighlightOptions;
12
93
  }
13
94
  interface DatabaseStore {
14
95
  transaction<T>(fn: (tx: Knex.Transaction) => Promise<T>): Promise<T>;
@@ -26,6 +107,7 @@ interface RawDocumentRow {
26
107
  interface DocumentResultRow {
27
108
  document: IndexableDocument;
28
109
  type: string;
110
+ highlight: IndexableDocument;
29
111
  }
30
112
 
31
113
  declare class DatabaseDocumentStore implements DatabaseStore {
@@ -38,39 +120,7 @@ declare class DatabaseDocumentStore implements DatabaseStore {
38
120
  prepareInsert(tx: Knex.Transaction): Promise<void>;
39
121
  completeInsert(tx: Knex.Transaction, type: string): Promise<void>;
40
122
  insertDocuments(tx: Knex.Transaction, type: string, documents: IndexableDocument[]): Promise<void>;
41
- query(tx: Knex.Transaction, { types, pgTerm, fields, offset, limit }: PgSearchQuery): Promise<DocumentResultRow[]>;
42
- }
43
-
44
- declare type PgSearchEngineIndexerOptions = {
45
- batchSize: number;
46
- type: string;
47
- databaseStore: DatabaseStore;
48
- };
49
- declare class PgSearchEngineIndexer extends BatchSearchEngineIndexer {
50
- private store;
51
- private type;
52
- private tx;
53
- constructor(options: PgSearchEngineIndexerOptions);
54
- initialize(): Promise<void>;
55
- index(documents: IndexableDocument[]): Promise<void>;
56
- finalize(): Promise<void>;
57
- }
58
-
59
- declare type ConcretePgSearchQuery = {
60
- pgQuery: PgSearchQuery;
61
- pageSize: number;
62
- };
63
- declare class PgSearchEngine implements SearchEngine {
64
- private readonly databaseStore;
65
- constructor(databaseStore: DatabaseStore);
66
- static from(options: {
67
- database: PluginDatabaseManager;
68
- }): Promise<PgSearchEngine>;
69
- static supported(database: PluginDatabaseManager): Promise<boolean>;
70
- translator(query: SearchQuery): ConcretePgSearchQuery;
71
- setTranslator(translator: (query: SearchQuery) => ConcretePgSearchQuery): void;
72
- getIndexer(type: string): Promise<PgSearchEngineIndexer>;
73
- query(query: SearchQuery): Promise<IndexableResultSet>;
123
+ query(tx: Knex.Transaction, searchQuery: PgSearchQuery): Promise<DocumentResultRow[]>;
74
124
  }
75
125
 
76
- export { ConcretePgSearchQuery, DatabaseDocumentStore, DatabaseStore, PgSearchEngine, PgSearchEngineIndexer, PgSearchEngineIndexerOptions, PgSearchQuery, RawDocumentRow };
126
+ export { ConcretePgSearchQuery, DatabaseDocumentStore, DatabaseStore, PgSearchEngine, PgSearchEngineIndexer, PgSearchEngineIndexerOptions, PgSearchHighlightOptions, PgSearchOptions, PgSearchQuery, PgSearchQueryTranslator, PgSearchQueryTranslatorOptions, RawDocumentRow };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@backstage/plugin-search-backend-module-pg",
3
3
  "description": "A module for the search backend that implements search using PostgreSQL",
4
- "version": "0.3.5-next.1",
4
+ "version": "0.3.6-next.0",
5
5
  "main": "dist/index.cjs.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "license": "Apache-2.0",
@@ -23,19 +23,23 @@
23
23
  "clean": "backstage-cli package clean"
24
24
  },
25
25
  "dependencies": {
26
- "@backstage/backend-common": "^0.14.1-next.1",
27
- "@backstage/plugin-search-backend-node": "^0.6.3-next.1",
28
- "@backstage/plugin-search-common": "^0.3.6-next.0",
29
- "knex": "^1.0.2",
30
- "lodash": "^4.17.21"
26
+ "@backstage/backend-common": "^0.15.0-next.0",
27
+ "@backstage/config": "^1.0.1",
28
+ "@backstage/plugin-search-backend-node": "^1.0.1-next.0",
29
+ "@backstage/plugin-search-common": "^1.0.0",
30
+ "knex": "^2.0.0",
31
+ "lodash": "^4.17.21",
32
+ "uuid": "^8.3.2"
31
33
  },
32
34
  "devDependencies": {
33
- "@backstage/backend-test-utils": "^0.1.26-next.1",
34
- "@backstage/cli": "^0.18.0-next.1"
35
+ "@backstage/backend-test-utils": "^0.1.27-next.0",
36
+ "@backstage/cli": "^0.18.1-next.0"
35
37
  },
36
38
  "files": [
37
39
  "dist",
38
- "migrations"
40
+ "migrations",
41
+ "config.d.ts"
39
42
  ],
40
- "gitHead": "e0a993834c31487a97a1ae6878eaf3685f03fc1a"
43
+ "configSchema": "config.d.ts",
44
+ "gitHead": "fc3229c49caf6eced02ed9516199015bf4682664"
41
45
  }