feat(collections/search): Improve text index creation and search fallback mechanisms in collections and document search methods
This commit is contained in:
@@ -61,6 +61,10 @@ export function getSearchableFields(className: string): string[] {
|
||||
}
|
||||
return Array.from(searchableFieldsMap.get(className));
|
||||
}
|
||||
// Escape user input for safe use in MongoDB regular expressions
|
||||
function escapeForRegex(input: string): string {
|
||||
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
/**
|
||||
* unique index - decorator to mark a unique index
|
||||
@@ -326,57 +330,38 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
||||
}
|
||||
|
||||
/**
|
||||
* Search documents using Lucene query syntax
|
||||
* @param luceneQuery Lucene query string
|
||||
* Search documents by text or field:value syntax, with safe regex fallback
|
||||
* @param query A search term or field:value expression
|
||||
* @returns Array of matching documents
|
||||
*/
|
||||
public static async search<T>(
|
||||
this: plugins.tsclass.typeFest.Class<T>,
|
||||
luceneQuery: string,
|
||||
query: string,
|
||||
): Promise<T[]> {
|
||||
const filter = (this as any).createSearchFilter(luceneQuery);
|
||||
return await (this as any).getInstances(filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search documents using Lucene query syntax with robust error handling
|
||||
* @param luceneQuery The Lucene query string to search with
|
||||
* @returns Array of matching documents
|
||||
*/
|
||||
public static async searchWithLucene<T>(
|
||||
this: plugins.tsclass.typeFest.Class<T>,
|
||||
luceneQuery: string,
|
||||
): Promise<T[]> {
|
||||
try {
|
||||
const className = (this as any).className || this.name;
|
||||
const searchableFields = getSearchableFields(className);
|
||||
|
||||
if (searchableFields.length === 0) {
|
||||
console.warn(
|
||||
`No searchable fields defined for class ${className}, falling back to simple search`,
|
||||
);
|
||||
return (this as any).searchByTextAcrossFields(luceneQuery);
|
||||
}
|
||||
|
||||
// Simple term search optimization
|
||||
if (
|
||||
!luceneQuery.includes(':') &&
|
||||
!luceneQuery.includes(' AND ') &&
|
||||
!luceneQuery.includes(' OR ') &&
|
||||
!luceneQuery.includes(' NOT ')
|
||||
) {
|
||||
return (this as any).searchByTextAcrossFields(luceneQuery);
|
||||
}
|
||||
|
||||
// Try to use the Lucene-to-MongoDB conversion
|
||||
const filter = (this as any).createSearchFilter(luceneQuery);
|
||||
return await (this as any).getInstances(filter);
|
||||
} catch (error) {
|
||||
console.error(`Error in searchWithLucene: ${error.message}`);
|
||||
return (this as any).searchByTextAcrossFields(luceneQuery);
|
||||
const className = (this as any).className || this.name;
|
||||
const searchableFields = getSearchableFields(className);
|
||||
if (searchableFields.length === 0) {
|
||||
throw new Error(`No searchable fields defined for class ${className}`);
|
||||
}
|
||||
// field:value exact match (case-sensitive for non-regex fields)
|
||||
const fv = query.match(/^(\w+):(.+)$/);
|
||||
if (fv) {
|
||||
const field = fv[1];
|
||||
const value = fv[2];
|
||||
if (!searchableFields.includes(field)) {
|
||||
throw new Error(`Field '${field}' is not searchable for class ${className}`);
|
||||
}
|
||||
return await (this as any).getInstances({ [field]: value });
|
||||
}
|
||||
// safe regex across all searchable fields (case-insensitive)
|
||||
const escaped = escapeForRegex(query);
|
||||
const orConditions = searchableFields.map((field) => ({
|
||||
[field]: { $regex: escaped, $options: 'i' },
|
||||
}));
|
||||
return await (this as any).getInstances({ $or: orConditions });
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Search by text across all searchable fields (fallback method)
|
||||
* @param searchText The text to search for in all searchable fields
|
||||
|
Reference in New Issue
Block a user