From a5e27e1387187cb55d9f79744b62c8b8074d3ee4 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 22 Jul 2021 15:53:20 +0100 Subject: [PATCH] Fix lucene filtering of all types by parsing values as expected types, and correctly wrapping non-numeric types while building queries --- .../src/api/controllers/row/internalSearch.js | 79 ++++++++++++++----- packages/standard-components/src/lucene.js | 16 ++-- 2 files changed, 70 insertions(+), 25 deletions(-) diff --git a/packages/server/src/api/controllers/row/internalSearch.js b/packages/server/src/api/controllers/row/internalSearch.js index 72f5e6e5c8..041f030477 100644 --- a/packages/server/src/api/controllers/row/internalSearch.js +++ b/packages/server/src/api/controllers/row/internalSearch.js @@ -3,13 +3,35 @@ const env = require("../../../environment") const fetch = require("node-fetch") /** - * Escapes any characters in a string which lucene searches require to be - * escaped. - * @param value The value to escape - * @returns {string} + * Preprocesses a value before going into a lucene search. + * Transforms strings to lowercase and wraps strings and bools in quotes. + * @param value The value to process + * @param options The preprocess options + * @returns {string|*} */ -const luceneEscape = value => { - return `${value}`.replace(/[ #+\-&|!(){}\]^"~*?:\\]/g, "\\$&") +const preprocess = ( + value, + options = { escape: true, lowercase: true, wrap: true } +) => { + // Determine if type needs wrapped + const originalType = typeof value + + // Convert to lowercase + if (options.lowercase) { + value = value?.toLowerCase ? value.toLowerCase() : value + } + + // Escape characters + if (options.escape) { + value = `${value}`.replace(/[ #+\-&|!(){}\]^"~*?:\\]/g, "\\$&") + } + + // Wrap in quotes + if (options.wrap) { + value = originalType === "number" ? value : `"${value}"` + } + + return value } /** @@ -113,7 +135,10 @@ class QueryBuilder { function build(structure, queryFn) { for (let [key, value] of Object.entries(structure)) { - const expression = queryFn(luceneEscape(key.replace(/ /, "_")), value) + key = preprocess(key.replace(/ /, "_"), { + escape: true, + }) + const expression = queryFn(key, value) if (expression == null) { continue } @@ -124,7 +149,14 @@ class QueryBuilder { // Construct the actual lucene search query string from JSON structure if (this.query.string) { build(this.query.string, (key, value) => { - return value ? `${key}:${luceneEscape(value.toLowerCase())}*` : null + if (!value) { + return null + } + value = preprocess(value, { + escape: true, + lowercase: true, + }) + return `${key}:${value}*` }) } if (this.query.range) { @@ -138,30 +170,37 @@ class QueryBuilder { if (value.high == null || value.high === "") { return null } - return `${key}:[${value.low} TO ${value.high}]` + const low = preprocess(value.low) + const high = preprocess(value.high) + return `${key}:[${low} TO ${high}]` }) } if (this.query.fuzzy) { build(this.query.fuzzy, (key, value) => { - return value ? `${key}:${luceneEscape(value.toLowerCase())}~` : null + if (!value) { + return null + } + value = preprocess(value, { + escape: true, + lowercase: true, + }) + return `${key}:${value}~` }) } if (this.query.equal) { build(this.query.equal, (key, value) => { - const escapedValue = luceneEscape(value.toLowerCase()) - // have to do the or to manage straight values, or strings - return value - ? `(${key}:${escapedValue} OR ${key}:"${escapedValue}")` - : null + if (!value) { + return null + } + return `${key}:${preprocess(value)}` }) } if (this.query.notEqual) { build(this.query.notEqual, (key, value) => { - const escapedValue = luceneEscape(value.toLowerCase()) - // have to do the or to manage straight values, or strings - return value - ? `(!${key}:${escapedValue} OR !${key}:"${escapedValue}")` - : null + if (!value) { + return null + } + return `!${key}:${preprocess(value)}` }) } if (this.query.empty) { diff --git a/packages/standard-components/src/lucene.js b/packages/standard-components/src/lucene.js index 91c69dfda2..317d8c3e74 100644 --- a/packages/standard-components/src/lucene.js +++ b/packages/standard-components/src/lucene.js @@ -15,10 +15,16 @@ export const buildLuceneQuery = filter => { if (Array.isArray(filter)) { filter.forEach(expression => { let { operator, field, type, value } = expression - // Ensure date fields are transformed into ISO strings + // Parse all values into correct types if (type === "datetime" && value) { value = new Date(value).toISOString() } + if (type === "number") { + value = parseFloat(value) + } + if (type === "boolean") { + value = value?.toLowerCase() === "true" + } if (operator.startsWith("range")) { if (!query.range[field]) { query.range[field] = { @@ -42,10 +48,10 @@ export const buildLuceneQuery = filter => { // Transform boolean filters to cope with null. // "equals false" needs to be "not equals true" // "not equals false" needs to be "equals true" - if (operator === "equal" && value === "false") { - query.notEqual[field] = "true" - } else if (operator === "notEqual" && value === "false") { - query.equal[field] = "true" + if (operator === "equal" && value === false) { + query.notEqual[field] = true + } else if (operator === "notEqual" && value === false) { + query.equal[field] = true } else { query[operator][field] = value }