diff --git a/changelog.md b/changelog.md index cb8d130..8d6cd23 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 2025-04-22 - 5.11.4 - fix(search) +Implement implicit AND logic for mixed simple term and field:value queries in search + +- Added a new branch to detect and handle search queries that mix field:value pairs with plain terms without explicit operators +- Builds an implicit $and filter when query parts contain colon(s) but lack explicit boolean operators or quotes +- Ensures proper parsing and improved robustness of search filters + ## 2025-04-22 - 5.11.3 - fix(lucene adapter and search tests) Improve range query parsing in Lucene adapter and expand search test coverage diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 4bdda03..1431345 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartdata', - version: '5.11.3', + version: '5.11.4', description: 'An advanced library for NoSQL data organization and manipulation using TypeScript with support for MongoDB, data validation, collections, and custom data types.' } diff --git a/ts/classes.doc.ts b/ts/classes.doc.ts index 7bae955..178ade6 100644 --- a/ts/classes.doc.ts +++ b/ts/classes.doc.ts @@ -379,6 +379,34 @@ export class SmartDataDbDoc ({ [f]: { $regex: pattern, $options: 'i' } })); return await (this as any).getInstances({ $or: orConds }); } + // implicit AND for mixed simple term + field:value queries (no explicit operators) + const parts = q.split(/\s+/); + const hasColon = parts.some((t) => t.includes(':')); + // implicit AND for mixed simple term + field:value queries (no explicit operators or range syntax) + if ( + parts.length > 1 && hasColon && + !q.includes(' AND ') && !q.includes(' OR ') && !q.includes(' NOT ') && + !q.includes('(') && !q.includes(')') && !q.includes('[') && !q.includes(']') && + !q.includes('"') && !q.includes("'") && + !q.includes('*') && !q.includes('?') + ) { + const andConds = parts.map((term) => { + const m = term.match(/^(\\w+):([^"'\\*\\?\\s]+)$/); + if (m) { + const field = m[1]; + const value = m[2]; + if (!searchableFields.includes(field)) { + throw new Error(`Field '${field}' is not searchable for class ${this.name}`); + } + return { [field]: value }; + } else { + const esc = escapeForRegex(term); + const ors = searchableFields.map((f) => ({ [f]: { $regex: esc, $options: 'i' } })); + return { $or: ors }; + } + }); + return await (this as any).getInstances({ $and: andConds }); + } // detect advanced Lucene syntax: field:value, wildcards, boolean, grouping const luceneSyntax = /(\w+:[^\s]+)|\*|\?|\bAND\b|\bOR\b|\bNOT\b|\(|\)/; if (luceneSyntax.test(q)) {