@backstage/plugin-search-backend-node
Advanced tools
Comparing version 0.2.2 to 0.3.0
# @backstage/plugin-search-backend-node | ||
## 0.3.0 | ||
### Minor Changes | ||
- 9f3ecb555: Build search queries using the query builder in `LunrSearchEngine`. This removes | ||
the support for specifying custom queries with the lunr query syntax, but makes | ||
sure that inputs are properly escaped. Supporting the full lunr syntax is still | ||
possible by setting a custom query translator. | ||
The interface of `LunrSearchEngine.setTranslator()` is changed to support | ||
building lunr queries. | ||
### Patch Changes | ||
- 9f3ecb555: Enhance the search results of `LunrSearchEngine` to support a more natural | ||
search experience. This is done by allowing typos (by using fuzzy search) and | ||
supporting typeahead search (using wildcard queries to match incomplete words). | ||
- 4176a60e5: Change search scheduler from starting indexing in a fixed interval (for example | ||
every 60 seconds), to wait a fixed time between index runs. | ||
This makes sure that no second index process for the same document type is | ||
started when the previous one is still running. | ||
## 0.2.2 | ||
@@ -4,0 +25,0 @@ |
@@ -77,5 +77,30 @@ 'use strict'; | ||
function runPeriodically(fn, delayMs) { | ||
let cancel; | ||
let cancelled = false; | ||
const cancellationPromise = new Promise((resolve) => { | ||
cancel = () => { | ||
resolve(); | ||
cancelled = true; | ||
}; | ||
}); | ||
const startRefresh = async () => { | ||
while (!cancelled) { | ||
try { | ||
await fn(); | ||
} catch { | ||
} | ||
await Promise.race([ | ||
new Promise((resolve) => setTimeout(resolve, delayMs)), | ||
cancellationPromise | ||
]); | ||
} | ||
}; | ||
startRefresh(); | ||
return cancel; | ||
} | ||
class Scheduler { | ||
constructor({logger}) { | ||
this.intervalTimeouts = []; | ||
this.runningTasks = []; | ||
this.logger = logger; | ||
@@ -85,3 +110,3 @@ this.schedule = []; | ||
addToSchedule(task, interval) { | ||
if (this.intervalTimeouts.length) { | ||
if (this.runningTasks.length) { | ||
throw new Error("Cannot add task to schedule that has already been started."); | ||
@@ -94,6 +119,3 @@ } | ||
this.schedule.forEach(({task, interval}) => { | ||
task(); | ||
this.intervalTimeouts.push(setInterval(() => { | ||
task(); | ||
}, interval)); | ||
this.runningTasks.push(runPeriodically(() => task(), interval)); | ||
}); | ||
@@ -103,6 +125,6 @@ } | ||
this.logger.info("Stopping all scheduled search tasks."); | ||
this.intervalTimeouts.forEach((timeout) => { | ||
clearInterval(timeout); | ||
this.runningTasks.forEach((cancel) => { | ||
cancel(); | ||
}); | ||
this.intervalTimeouts = []; | ||
this.runningTasks = []; | ||
} | ||
@@ -119,25 +141,42 @@ } | ||
}) => { | ||
let lunrQueryFilters; | ||
const lunrTerm = term ? `+${term}` : ""; | ||
if (filters) { | ||
lunrQueryFilters = Object.entries(filters).map(([field, value]) => { | ||
if (["string", "number", "boolean"].includes(typeof value)) { | ||
if (typeof value === "string") { | ||
return ` +${field}:${value.replace(":", "\\:")}`; | ||
} | ||
return ` +${field}:${value}`; | ||
return { | ||
lunrQueryBuilder: (q) => { | ||
const termToken = lunr__default['default'].tokenizer(term); | ||
q.term(termToken, { | ||
usePipeline: true, | ||
boost: 100 | ||
}); | ||
q.term(termToken, { | ||
usePipeline: false, | ||
boost: 10, | ||
wildcard: lunr__default['default'].Query.wildcard.TRAILING | ||
}); | ||
q.term(termToken, { | ||
usePipeline: false, | ||
editDistance: 2, | ||
boost: 1 | ||
}); | ||
if (filters) { | ||
Object.entries(filters).forEach(([field, value]) => { | ||
if (!q.allFields.includes(field)) { | ||
throw new Error(`unrecognised field ${field}`); | ||
} | ||
if (["string", "number", "boolean"].includes(typeof value)) { | ||
q.term(lunr__default['default'].tokenizer(value == null ? void 0 : value.toString()), { | ||
presence: lunr__default['default'].Query.presence.REQUIRED, | ||
fields: [field] | ||
}); | ||
} else if (Array.isArray(value)) { | ||
this.logger.warn(`Non-scalar filter value used for field ${field}. Consider using a different Search Engine for better results.`); | ||
q.term(lunr__default['default'].tokenizer(value), { | ||
presence: lunr__default['default'].Query.presence.OPTIONAL, | ||
fields: [field] | ||
}); | ||
} else { | ||
this.logger.warn(`Unknown filter type used on field ${field}`); | ||
} | ||
}); | ||
} | ||
if (Array.isArray(value)) { | ||
this.logger.warn(`Non-scalar filter value used for field ${field}. Consider using a different Search Engine for better results.`); | ||
return ` ${value.map((v) => { | ||
return `${field}:${v}`; | ||
})}`; | ||
} | ||
this.logger.warn(`Unknown filter type used on field ${field}`); | ||
return ""; | ||
}).join(""); | ||
} | ||
return { | ||
lunrQueryString: `${lunrTerm}${lunrQueryFilters || ""}`, | ||
documentTypes: types || ["*"] | ||
}, | ||
documentTypes: types | ||
}; | ||
@@ -166,33 +205,19 @@ }; | ||
query(query) { | ||
const {lunrQueryString, documentTypes} = this.translator(query); | ||
const {lunrQueryBuilder, documentTypes} = this.translator(query); | ||
const results = []; | ||
if (documentTypes.length === 1 && documentTypes[0] === "*") { | ||
Object.keys(this.lunrIndices).forEach((type) => { | ||
try { | ||
results.push(...this.lunrIndices[type].search(lunrQueryString).map((result) => { | ||
return { | ||
result, | ||
type | ||
}; | ||
})); | ||
} catch (err) { | ||
if (err instanceof lunr__default['default'].QueryParseError && err.message.startsWith("unrecognised field")) | ||
return; | ||
Object.keys(this.lunrIndices).filter((type) => !documentTypes || documentTypes.includes(type)).forEach((type) => { | ||
try { | ||
results.push(...this.lunrIndices[type].query(lunrQueryBuilder).map((result) => { | ||
return { | ||
result, | ||
type | ||
}; | ||
})); | ||
} catch (err) { | ||
if (err instanceof Error && err.message.startsWith("unrecognised field")) { | ||
return; | ||
} | ||
}); | ||
} else { | ||
Object.keys(this.lunrIndices).filter((type) => documentTypes.includes(type)).forEach((type) => { | ||
try { | ||
results.push(...this.lunrIndices[type].search(lunrQueryString).map((result) => { | ||
return { | ||
result, | ||
type | ||
}; | ||
})); | ||
} catch (err) { | ||
if (err instanceof lunr__default['default'].QueryParseError && err.message.startsWith("unrecognised field")) | ||
return; | ||
} | ||
}); | ||
} | ||
throw err; | ||
} | ||
}); | ||
results.sort((doc1, doc2) => { | ||
@@ -199,0 +224,0 @@ return doc2.result.score - doc1.result.score; |
@@ -89,3 +89,3 @@ import { Logger } from 'winston'; | ||
private schedule; | ||
private intervalTimeouts; | ||
private runningTasks; | ||
constructor({ logger }: { | ||
@@ -95,4 +95,5 @@ logger: Logger; | ||
/** | ||
* Adds each task and interval to the schedule | ||
* | ||
* Adds each task and interval to the schedule. | ||
* When running the tasks, the scheduler waits at least for the time specified | ||
* in the interval once the task was completed, before running it again. | ||
*/ | ||
@@ -111,4 +112,4 @@ addToSchedule(task: Function, interval: number): void; | ||
declare type ConcreteLunrQuery = { | ||
lunrQueryString: string; | ||
documentTypes: string[]; | ||
lunrQueryBuilder: lunr.Index.QueryBuilder; | ||
documentTypes?: string[]; | ||
}; | ||
@@ -115,0 +116,0 @@ declare type LunrQueryTranslator = (query: SearchQuery) => ConcreteLunrQuery; |
{ | ||
"name": "@backstage/plugin-search-backend-node", | ||
"version": "0.2.2", | ||
"version": "0.3.0", | ||
"main": "dist/index.cjs.js", | ||
@@ -28,3 +28,3 @@ "types": "dist/index.d.ts", | ||
"devDependencies": { | ||
"@backstage/backend-common": "^0.8.3", | ||
"@backstage/backend-common": "^0.8.5", | ||
"@backstage/cli": "^0.7.2" | ||
@@ -35,3 +35,3 @@ }, | ||
], | ||
"gitHead": "5c2550a6d4485e86d5ad9ecec82fb5fab74c6ca7" | ||
"gitHead": "6cebb9d587224c055516d1ab2958f34bd3659c43" | ||
} |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
48256
345