fix(search): Fix handling of quoted wildcard patterns in field-specific search queries and add tests for location-based wildcard phrase searches

This commit is contained in:
Philipp Kunz 2025-04-22 20:09:21 +00:00
parent e4d787096e
commit 9c6d6d9f2c
4 changed files with 48 additions and 3 deletions

View File

@ -1,5 +1,11 @@
# Changelog
## 2025-04-22 - 5.12.2 - fix(search)
Fix handling of quoted wildcard patterns in field-specific search queries and add tests for location-based wildcard phrase searches
- Strip surrounding quotes from wildcard patterns in field queries to correctly transform them to regex
- Introduce new tests in test/test.search.ts to validate exact quoted and unquoted wildcard searches on a location field
## 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.

View File

@ -9,6 +9,8 @@ import { searchable } from '../ts/classes.doc.js';
// Set up database connection
let smartmongoInstance: smartmongo.SmartMongo;
let testDb: smartdata.SmartdataDb;
// Class for location-based wildcard/phrase tests
let LocationDoc: any;
// Define a test class with searchable fields using the standard SmartDataDbDoc
@smartdata.Collection(() => testDb)
@ -290,6 +292,37 @@ tap.test('should apply validate hook to post-filter results', async () => {
expensive.forEach((p) => expect(p.price).toBeGreaterThan(500));
});
// Tests for quoted and wildcard field-specific phrases
tap.test('setup location test products', async () => {
@smartdata.Collection(() => testDb)
class LD extends smartdata.SmartDataDbDoc<LD, LD> {
@smartdata.unI() public id: string = smartunique.shortId();
@smartdata.svDb() @searchable() public location: string;
constructor(loc: string) { super(); this.location = loc; }
}
// Assign to outer variable for subsequent tests
LocationDoc = LD;
const locations = ['Berlin', 'Frankfurt am Main', 'Frankfurt am Oder', 'London'];
for (const loc of locations) {
await new LocationDoc(loc).save();
}
});
tap.test('should search exact quoted field phrase', async () => {
const results = await (LocationDoc as any).search('location:"Frankfurt am Main"');
expect(results.length).toEqual(1);
expect(results[0].location).toEqual('Frankfurt am Main');
});
tap.test('should search wildcard quoted field phrase', async () => {
const results = await (LocationDoc as any).search('location:"Frankfurt am *"');
const names = results.map((d: any) => d.location).sort();
expect(names).toEqual(['Frankfurt am Main', 'Frankfurt am Oder']);
});
tap.test('should search unquoted wildcard field', async () => {
const results = await (LocationDoc as any).search('location:Frankfurt*');
const names = results.map((d: any) => d.location).sort();
expect(names).toEqual(['Frankfurt am Main', 'Frankfurt am Oder']);
});
// 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*');

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartdata',
version: '5.12.1',
version: '5.12.2',
description: 'An advanced library for NoSQL data organization and manipulation using TypeScript with support for MongoDB, data validation, collections, and custom data types.'
}

View File

@ -397,7 +397,12 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
const wildcardField = q.match(/^(\w+):(.+[*?].*)$/);
if (wildcardField) {
const field = wildcardField[1];
const pattern = wildcardField[2];
// Support quoted wildcard patterns: strip surrounding quotes
let pattern = wildcardField[2];
if ((pattern.startsWith('"') && pattern.endsWith('"')) ||
(pattern.startsWith("'") && pattern.endsWith("'"))) {
pattern = pattern.slice(1, -1);
}
if (!searchableFields.includes(field)) {
throw new Error(`Field '${field}' is not searchable for class ${this.name}`);
}
@ -421,7 +426,8 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
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+):(.+)$/);