fix(search): Improve implicit AND logic for mixed free term and field queries in search and enhance wildcard field handling.
This commit is contained in:
parent
0ca1d452b4
commit
2bf923b4f1
@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-04-22 - 5.12.1 - fix(search)
|
||||||
|
Improve implicit AND logic for mixed free term and field queries in search and enhance wildcard field handling.
|
||||||
|
|
||||||
|
- Updated regex for field:value parsing to capture full value with wildcards.
|
||||||
|
- Added explicit handling for free terms by converting to regex across searchable fields.
|
||||||
|
- Improved error messaging for attempts to search non-searchable fields.
|
||||||
|
- Extended tests to cover combined free term and wildcard field searches, including error cases.
|
||||||
|
|
||||||
## 2025-04-22 - 5.12.0 - feat(doc/search)
|
## 2025-04-22 - 5.12.0 - feat(doc/search)
|
||||||
Enhance search functionality with filter and validate options for advanced query control
|
Enhance search functionality with filter and validate options for advanced query control
|
||||||
|
|
||||||
|
@ -290,6 +290,39 @@ tap.test('should apply validate hook to post-filter results', async () => {
|
|||||||
expensive.forEach((p) => expect(p.price).toBeGreaterThan(500));
|
expensive.forEach((p) => expect(p.price).toBeGreaterThan(500));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Combined free-term and field wildcard tests
|
||||||
|
tap.test('should combine free term and wildcard field search', async () => {
|
||||||
|
const results = await Product.search('book category:Book*');
|
||||||
|
expect(results.length).toEqual(2);
|
||||||
|
results.forEach((p) => expect(p.category).toEqual('Books'));
|
||||||
|
});
|
||||||
|
tap.test('should not match when free term matches but wildcard field does not', async () => {
|
||||||
|
const results = await Product.search('book category:Kitchen*');
|
||||||
|
expect(results.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Non-searchable field should cause an error for combined queries
|
||||||
|
tap.test('should throw when combining term with non-searchable field', async () => {
|
||||||
|
let error: Error;
|
||||||
|
try {
|
||||||
|
await Product.search('book location:Berlin');
|
||||||
|
} catch (e) {
|
||||||
|
error = e as Error;
|
||||||
|
}
|
||||||
|
expect(error).toBeTruthy();
|
||||||
|
expect(error.message).toMatch(/not searchable/);
|
||||||
|
});
|
||||||
|
tap.test('should throw when combining term with non-searchable wildcard field', async () => {
|
||||||
|
let error: Error;
|
||||||
|
try {
|
||||||
|
await Product.search('book location:Berlin*');
|
||||||
|
} catch (e) {
|
||||||
|
error = e as Error;
|
||||||
|
}
|
||||||
|
expect(error).toBeTruthy();
|
||||||
|
expect(error.message).toMatch(/not searchable/);
|
||||||
|
});
|
||||||
|
|
||||||
// Close database connection
|
// Close database connection
|
||||||
tap.test('close database connection', async () => {
|
tap.test('close database connection', async () => {
|
||||||
await testDb.mongoDb.dropDatabase();
|
await testDb.mongoDb.dropDatabase();
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartdata',
|
name: '@push.rocks/smartdata',
|
||||||
version: '5.12.0',
|
version: '5.12.1',
|
||||||
description: 'An advanced library for NoSQL data organization and manipulation using TypeScript with support for MongoDB, data validation, collections, and custom data types.'
|
description: 'An advanced library for NoSQL data organization and manipulation using TypeScript with support for MongoDB, data validation, collections, and custom data types.'
|
||||||
}
|
}
|
||||||
|
@ -414,31 +414,35 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|||||||
const orConds = searchableFields.map((f) => ({ [f]: { $regex: pattern, $options: 'i' } }));
|
const orConds = searchableFields.map((f) => ({ [f]: { $regex: pattern, $options: 'i' } }));
|
||||||
return await (this as any).execQuery({ $or: orConds }, opts);
|
return await (this as any).execQuery({ $or: orConds }, opts);
|
||||||
}
|
}
|
||||||
// implicit AND for mixed simple term + field:value queries (no explicit operators)
|
// implicit AND: combine free terms and field:value terms (with or without wildcards)
|
||||||
const parts = q.split(/\s+/);
|
const parts = q.split(/\s+/);
|
||||||
const hasColon = parts.some((t) => t.includes(':'));
|
const hasColon = parts.some((t) => t.includes(':'));
|
||||||
// implicit AND for mixed simple term + field:value queries (no explicit operators or range syntax)
|
|
||||||
if (
|
if (
|
||||||
parts.length > 1 && hasColon &&
|
parts.length > 1 && hasColon &&
|
||||||
!q.includes(' AND ') && !q.includes(' OR ') && !q.includes(' NOT ') &&
|
!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("'") &&
|
!q.includes('[') && !q.includes(']')
|
||||||
!q.includes('*') && !q.includes('?')
|
|
||||||
) {
|
) {
|
||||||
const andConds = parts.map((term) => {
|
const andConds = parts.map((term) => {
|
||||||
const m = term.match(/^(\\w+):([^"'\\*\\?\\s]+)$/);
|
const m = term.match(/^(\w+):(.+)$/);
|
||||||
if (m) {
|
if (m) {
|
||||||
const field = m[1];
|
const field = m[1];
|
||||||
const value = m[2];
|
const value = m[2];
|
||||||
if (!searchableFields.includes(field)) {
|
if (!searchableFields.includes(field)) {
|
||||||
throw new Error(`Field '${field}' is not searchable for class ${this.name}`);
|
throw new Error(`Field '${field}' is not searchable for class ${this.name}`);
|
||||||
}
|
}
|
||||||
|
if (value.includes('*') || value.includes('?')) {
|
||||||
|
// wildcard field search
|
||||||
|
const escaped = value.replace(/([.+^${}()|[\\]\\])/g, '\\$1');
|
||||||
|
const pattern = escaped.replace(/\*/g, '.*').replace(/\?/g, '.');
|
||||||
|
return { [field]: { $regex: pattern, $options: 'i' } };
|
||||||
|
}
|
||||||
|
// exact field:value
|
||||||
return { [field]: value };
|
return { [field]: value };
|
||||||
} else {
|
|
||||||
const esc = escapeForRegex(term);
|
|
||||||
const ors = searchableFields.map((f) => ({ [f]: { $regex: esc, $options: 'i' } }));
|
|
||||||
return { $or: ors };
|
|
||||||
}
|
}
|
||||||
|
// free term -> regex across all searchable fields
|
||||||
|
const esc = escapeForRegex(term);
|
||||||
|
return { $or: searchableFields.map((f) => ({ [f]: { $regex: esc, $options: 'i' } })) };
|
||||||
});
|
});
|
||||||
return await (this as any).execQuery({ $and: andConds }, opts);
|
return await (this as any).execQuery({ $and: andConds }, opts);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user