@backstage/plugin-search-backend-module-pg 0.5.41 → 0.5.42-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,14 @@
1
1
  # @backstage/plugin-search-backend-module-pg
2
2
 
3
+ ## 0.5.42-next.0
4
+
5
+ ### Patch Changes
6
+
7
+ - 8155b04: Enable normalization in postgres query to change the behavior of the search.
8
+ - Updated dependencies
9
+ - @backstage/plugin-search-backend-node@1.3.9-next.0
10
+ - @backstage/backend-plugin-api@1.2.1-next.0
11
+
3
12
  ## 0.5.41
4
13
 
5
14
  ### Patch Changes
package/README.md CHANGED
@@ -17,7 +17,7 @@ for details on how to setup Postgres based search for your Backstage instance.
17
17
 
18
18
  ## Optional Configuration
19
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:
20
+ The following is an example of the optional configuration that can be applied when using Postgres as the search backend.
21
21
 
22
22
  ```yaml
23
23
  search:
@@ -30,6 +30,20 @@ search:
30
30
  highlightAll: false # If true the whole document will be used as the headline, ignoring the preceding three parameters. The default is false.
31
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
32
  fragmentDelimiter: ' ... ' # Delimiter string used to concatenate fragments. Defaults to " ... ".
33
+ normalization: 0 # Ranking functions use an integer bit mask to control document length impact on rank. The default value is 0
34
+ ```
35
+
36
+ The Normalization option controls several behaviors. It is a bit mask. [Ranking Search Results](https://www.postgresql.org/docs/current/textsearch-controls.html#TEXTSEARCH-RANKING) has more details.
37
+
38
+ Example with bit mask specifying more behaviours:
39
+
40
+ - 2 divides the rank by the document length
41
+ - 4 divides the rank by the mean harmonic distance between extents
42
+
43
+ ```yaml
44
+ search:
45
+ pg:
46
+ normalization: 2 | 4
33
47
  ```
34
48
 
35
49
  **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:
package/config.d.ts CHANGED
@@ -57,6 +57,10 @@ export interface Config {
57
57
  */
58
58
  fragmentDelimiter?: string;
59
59
  };
60
+ /**
61
+ * Ranking functions use an integer bit mask to control document length impact on rank. The default value is 0
62
+ */
63
+ normalization?: number | string;
60
64
  /**
61
65
  * Batch size to use when indexing
62
66
  */
@@ -27,11 +27,39 @@ class PgSearchEngine {
27
27
  };
28
28
  this.highlightOptions = highlightOptions;
29
29
  this.indexerBatchSize = config.getOptionalNumber("search.pg.indexerBatchSize") ?? 1e3;
30
+ this.normalization = this.getNormalizationValue(config);
30
31
  this.logger = logger;
31
32
  }
32
33
  logger;
33
34
  highlightOptions;
34
35
  indexerBatchSize;
36
+ normalization;
37
+ getNormalizationValue(config) {
38
+ const normalizationConfig = config.getOptional("search.pg.normalization") ?? 0;
39
+ if (typeof normalizationConfig === "number") {
40
+ return normalizationConfig;
41
+ } else if (typeof normalizationConfig === "string") {
42
+ return this.evaluateBitwiseOrExpression(normalizationConfig);
43
+ }
44
+ this.logger?.error(
45
+ `Unknown normalization configuration: ${normalizationConfig}`
46
+ );
47
+ return 0;
48
+ }
49
+ evaluateBitwiseOrExpression(expression) {
50
+ const tokens = expression.split("|").map((token) => token.trim());
51
+ const numbers = tokens.map((token) => {
52
+ const num = parseInt(token, 10);
53
+ if (isNaN(num)) {
54
+ this.logger?.error(
55
+ `Unknown expression for normalization: ${expression}`
56
+ );
57
+ return 0;
58
+ }
59
+ return num;
60
+ });
61
+ return numbers.reduce((acc, num) => acc | num, 0);
62
+ }
35
63
  /**
36
64
  * @deprecated This will be removed in a future release, please use fromConfig instead
37
65
  */
@@ -57,6 +85,7 @@ class PgSearchEngine {
57
85
  const { page } = decodePageCursor(query.pageCursor);
58
86
  const offset = page * pageSize;
59
87
  const limit = pageSize + 1;
88
+ const normalization = options.normalization || 0;
60
89
  return {
61
90
  pgQuery: {
62
91
  pgTerm: query.term.split(/\s/).map((p) => p.replace(/[\0()|&:*!]/g, "").trim()).filter((p) => p !== "").map((p) => `(${JSON.stringify(p)} | ${JSON.stringify(p)}:*)`).join("&"),
@@ -64,6 +93,7 @@ class PgSearchEngine {
64
93
  types: query.types,
65
94
  offset,
66
95
  limit,
96
+ normalization,
67
97
  options: options.highlightOptions
68
98
  },
69
99
  pageSize
@@ -82,7 +112,8 @@ class PgSearchEngine {
82
112
  }
83
113
  async query(query) {
84
114
  const { pgQuery, pageSize } = this.translator(query, {
85
- highlightOptions: this.highlightOptions
115
+ highlightOptions: this.highlightOptions,
116
+ normalization: this.normalization
86
117
  });
87
118
  const rows = await this.databaseStore.transaction(
88
119
  async (tx) => this.databaseStore.query(tx, pgQuery)
@@ -1 +1 @@
1
- {"version":3,"file":"PgSearchEngine.cjs.js","sources":["../../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 */\n\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';\nimport { v4 as uuid } from 'uuid';\nimport { Config } from '@backstage/config';\nimport { DatabaseService, LoggerService } from '@backstage/backend-plugin-api';\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: DatabaseService;\n logger?: LoggerService;\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\n/** @public */\nexport class PgSearchEngine implements SearchEngine {\n private readonly logger?: LoggerService;\n private readonly highlightOptions: PgSearchHighlightOptions;\n private readonly indexerBatchSize: number;\n\n /**\n * @deprecated This will be marked as private in a future release, please us fromConfig instead\n */\n constructor(\n private readonly databaseStore: DatabaseStore,\n config: Config,\n logger?: LoggerService,\n ) {\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 this.indexerBatchSize =\n config.getOptionalNumber('search.pg.indexerBatchSize') ?? 1000;\n this.logger = logger;\n }\n\n /**\n * @deprecated This will be removed in a future release, please use fromConfig instead\n */\n static async from(options: {\n database: DatabaseService;\n config: Config;\n logger?: LoggerService;\n }): Promise<PgSearchEngine> {\n return new PgSearchEngine(\n await DatabaseDocumentStore.create(options.database),\n options.config,\n options.logger,\n );\n }\n\n static async fromConfig(config: Config, options: PgSearchOptions) {\n return new PgSearchEngine(\n await DatabaseDocumentStore.create(options.database),\n config,\n options.logger,\n );\n }\n\n static async supported(database: DatabaseService): 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 = query.pageLimit || 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: this.indexerBatchSize,\n type,\n databaseStore: this.databaseStore,\n logger: this.logger?.child({ documentType: type }),\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":["uuid","DatabaseDocumentStore","PgSearchEngineIndexer"],"mappings":"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"}
1
+ {"version":3,"file":"PgSearchEngine.cjs.js","sources":["../../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 */\n\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';\nimport { v4 as uuid } from 'uuid';\nimport { Config } from '@backstage/config';\nimport { DatabaseService, LoggerService } from '@backstage/backend-plugin-api';\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 normalization?: number;\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: DatabaseService;\n logger?: LoggerService;\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\n/** @public */\nexport class PgSearchEngine implements SearchEngine {\n private readonly logger?: LoggerService;\n private readonly highlightOptions: PgSearchHighlightOptions;\n private readonly indexerBatchSize: number;\n private readonly normalization: number;\n\n /**\n * @deprecated This will be marked as private in a future release, please us fromConfig instead\n */\n constructor(\n private readonly databaseStore: DatabaseStore,\n config: Config,\n logger?: LoggerService,\n ) {\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 this.indexerBatchSize =\n config.getOptionalNumber('search.pg.indexerBatchSize') ?? 1000;\n\n this.normalization = this.getNormalizationValue(config);\n this.logger = logger;\n }\n\n private getNormalizationValue(config: Config) {\n const normalizationConfig =\n config.getOptional('search.pg.normalization') ?? 0;\n if (typeof normalizationConfig === 'number') {\n return normalizationConfig;\n } else if (typeof normalizationConfig === 'string') {\n return this.evaluateBitwiseOrExpression(normalizationConfig);\n }\n this.logger?.error(\n `Unknown normalization configuration: ${normalizationConfig}`,\n );\n\n return 0;\n }\n\n private evaluateBitwiseOrExpression(expression: string) {\n const tokens = expression.split('|').map(token => token.trim());\n\n const numbers = tokens.map(token => {\n const num = parseInt(token, 10);\n if (isNaN(num)) {\n this.logger?.error(\n `Unknown expression for normalization: ${expression}`,\n );\n return 0;\n }\n return num;\n });\n return numbers.reduce((acc, num) => acc | num, 0);\n }\n\n /**\n * @deprecated This will be removed in a future release, please use fromConfig instead\n */\n static async from(options: {\n database: DatabaseService;\n config: Config;\n logger?: LoggerService;\n }): Promise<PgSearchEngine> {\n return new PgSearchEngine(\n await DatabaseDocumentStore.create(options.database),\n options.config,\n options.logger,\n );\n }\n\n static async fromConfig(config: Config, options: PgSearchOptions) {\n return new PgSearchEngine(\n await DatabaseDocumentStore.create(options.database),\n config,\n options.logger,\n );\n }\n\n static async supported(database: DatabaseService): 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 = query.pageLimit || 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 const normalization = options.normalization || 0;\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 normalization,\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: this.indexerBatchSize,\n type,\n databaseStore: this.databaseStore,\n logger: this.logger?.child({ documentType: type }),\n });\n }\n\n async query(query: SearchQuery): Promise<IndexableResultSet> {\n const { pgQuery, pageSize } = this.translator(query, {\n highlightOptions: this.highlightOptions,\n normalization: this.normalization,\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":["uuid","DatabaseDocumentStore","PgSearchEngineIndexer"],"mappings":"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"}
@@ -73,7 +73,15 @@ class DatabaseDocumentStore {
73
73
  );
74
74
  }
75
75
  async query(tx, searchQuery) {
76
- const { types, pgTerm, fields, offset, limit, options } = searchQuery;
76
+ const {
77
+ types,
78
+ pgTerm,
79
+ fields,
80
+ offset,
81
+ limit,
82
+ normalization = 0,
83
+ options
84
+ } = searchQuery;
77
85
  const query = tx("documents");
78
86
  if (pgTerm) {
79
87
  query.from(tx.raw("documents, to_tsquery('english', ?) query", pgTerm)).whereRaw("query @@ body");
@@ -99,13 +107,13 @@ class DatabaseDocumentStore {
99
107
  query.select("type", "document");
100
108
  if (pgTerm && options.useHighlight) {
101
109
  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}`;
102
- query.select(tx.raw('ts_rank_cd(body, query) AS "rank"')).select(
110
+ query.select(tx.raw(`ts_rank_cd(body, query, ${normalization}) AS "rank"`)).select(
103
111
  tx.raw(
104
112
  `ts_headline('english', document, query, '${headlineOptions}') as "highlight"`
105
113
  )
106
114
  ).orderBy("rank", "desc");
107
115
  } else if (pgTerm && !options.useHighlight) {
108
- query.select(tx.raw('ts_rank_cd(body, query) AS "rank"')).orderBy("rank", "desc");
116
+ query.select(tx.raw(`ts_rank_cd(body, query, ${normalization}) AS "rank"`)).orderBy("rank", "desc");
109
117
  } else {
110
118
  query.select(tx.raw("1 as rank"));
111
119
  }
@@ -1 +1 @@
1
- {"version":3,"file":"DatabaseDocumentStore.cjs.js","sources":["../../src/database/DatabaseDocumentStore.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 {\n DatabaseService,\n resolvePackagePath,\n} from '@backstage/backend-plugin-api';\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\n/** @public */\nexport class DatabaseDocumentStore implements DatabaseStore {\n static async create(\n database: DatabaseService,\n ): Promise<DatabaseDocumentStore> {\n const knex = await database.getClient();\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 if (!database.migrations?.skip) {\n await knex.migrate.latest({\n directory: migrationsDir,\n });\n }\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 const rowsToDelete = tx<RawDocumentRow>('documents')\n .select('documents.hash')\n .leftJoin<RawDocumentRow>('documents_to_insert', {\n 'documents.hash': 'documents_to_insert.hash',\n })\n .whereNull('documents_to_insert.hash');\n\n await tx<RawDocumentRow>('documents')\n .where({ type })\n .whereIn('hash', rowsToDelete)\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 fieldValueCompare = valueArray\n .map(v => ({ [key]: v }))\n .map(v => JSON.stringify(v));\n const arrayValueCompare = valueArray\n .map(v => ({ [key]: [v] }))\n .map(v => JSON.stringify(v));\n const valueCompare = [...fieldValueCompare, ...arrayValueCompare];\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"],"names":["resolvePackagePath","queryPostgresMajorVersion"],"mappings":"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"}
1
+ {"version":3,"file":"DatabaseDocumentStore.cjs.js","sources":["../../src/database/DatabaseDocumentStore.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 {\n DatabaseService,\n resolvePackagePath,\n} from '@backstage/backend-plugin-api';\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\n/** @public */\nexport class DatabaseDocumentStore implements DatabaseStore {\n static async create(\n database: DatabaseService,\n ): Promise<DatabaseDocumentStore> {\n const knex = await database.getClient();\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 if (!database.migrations?.skip) {\n await knex.migrate.latest({\n directory: migrationsDir,\n });\n }\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 const rowsToDelete = tx<RawDocumentRow>('documents')\n .select('documents.hash')\n .leftJoin<RawDocumentRow>('documents_to_insert', {\n 'documents.hash': 'documents_to_insert.hash',\n })\n .whereNull('documents_to_insert.hash');\n\n await tx<RawDocumentRow>('documents')\n .where({ type })\n .whereIn('hash', rowsToDelete)\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 {\n types,\n pgTerm,\n fields,\n offset,\n limit,\n normalization = 0,\n options,\n } = 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, 0) 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 fieldValueCompare = valueArray\n .map(v => ({ [key]: v }))\n .map(v => JSON.stringify(v));\n const arrayValueCompare = valueArray\n .map(v => ({ [key]: [v] }))\n .map(v => JSON.stringify(v));\n const valueCompare = [...fieldValueCompare, ...arrayValueCompare];\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, ${normalization}) 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, ${normalization}) 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"],"names":["resolvePackagePath","queryPostgresMajorVersion"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -45,6 +45,7 @@ type ConcretePgSearchQuery = {
45
45
  */
46
46
  type PgSearchQueryTranslatorOptions = {
47
47
  highlightOptions: PgSearchHighlightOptions;
48
+ normalization?: number;
48
49
  };
49
50
  /**
50
51
  * Postgres specific query translator.
@@ -80,10 +81,13 @@ declare class PgSearchEngine implements SearchEngine {
80
81
  private readonly logger?;
81
82
  private readonly highlightOptions;
82
83
  private readonly indexerBatchSize;
84
+ private readonly normalization;
83
85
  /**
84
86
  * @deprecated This will be marked as private in a future release, please us fromConfig instead
85
87
  */
86
88
  constructor(databaseStore: DatabaseStore, config: Config, logger?: LoggerService);
89
+ private getNormalizationValue;
90
+ private evaluateBitwiseOrExpression;
87
91
  /**
88
92
  * @deprecated This will be removed in a future release, please use fromConfig instead
89
93
  */
@@ -107,6 +111,7 @@ interface PgSearchQuery {
107
111
  pgTerm?: string;
108
112
  offset: number;
109
113
  limit: number;
114
+ normalization?: number;
110
115
  options: PgSearchHighlightOptions;
111
116
  }
112
117
  /** @public */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/plugin-search-backend-module-pg",
3
- "version": "0.5.41",
3
+ "version": "0.5.42-next.0",
4
4
  "description": "A module for the search backend that implements search using PostgreSQL",
5
5
  "backstage": {
6
6
  "role": "backend-plugin-module",
@@ -63,17 +63,17 @@
63
63
  "test": "backstage-cli package test"
64
64
  },
65
65
  "dependencies": {
66
- "@backstage/backend-plugin-api": "^1.2.0",
67
- "@backstage/config": "^1.3.2",
68
- "@backstage/plugin-search-backend-node": "^1.3.8",
69
- "@backstage/plugin-search-common": "^1.2.17",
66
+ "@backstage/backend-plugin-api": "1.2.1-next.0",
67
+ "@backstage/config": "1.3.2",
68
+ "@backstage/plugin-search-backend-node": "1.3.9-next.0",
69
+ "@backstage/plugin-search-common": "1.2.17",
70
70
  "knex": "^3.0.0",
71
71
  "lodash": "^4.17.21",
72
72
  "uuid": "^11.0.0"
73
73
  },
74
74
  "devDependencies": {
75
- "@backstage/backend-test-utils": "^1.3.0",
76
- "@backstage/cli": "^0.30.0"
75
+ "@backstage/backend-test-utils": "1.3.1-next.0",
76
+ "@backstage/cli": "0.30.0"
77
77
  },
78
78
  "configSchema": "config.d.ts"
79
79
  }