Compare commits

...

14 Commits

Author SHA1 Message Date
0834ec5c91 5.9.2
Some checks failed
Default (tags) / security (push) Successful in 32s
Default (tags) / test (push) Successful in 3m3s
Default (tags) / release (push) Failing after 51s
Default (tags) / metadata (push) Successful in 57s
2025-04-18 15:10:04 +00:00
6a2a708ea1 fix(documentation): Update search API documentation to replace deprecated searchWithLucene examples with the unified search(query) API and clarify its behavior. 2025-04-18 15:10:03 +00:00
1d977986f1 5.9.1
Some checks failed
Default (tags) / security (push) Successful in 39s
Default (tags) / test (push) Successful in 3m2s
Default (tags) / release (push) Failing after 51s
Default (tags) / metadata (push) Successful in 58s
2025-04-18 14:56:11 +00:00
e325b42906 fix(search): Refactor search tests to use unified search API and update text index type casting 2025-04-18 14:56:11 +00:00
1a359d355a 5.9.0
Some checks failed
Default (tags) / security (push) Successful in 39s
Default (tags) / test (push) Failing after 6m57s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-18 11:25:39 +00:00
b5a9449d5e feat(collections/search): Improve text index creation and search fallback mechanisms in collections and document search methods 2025-04-18 11:25:39 +00:00
558f83a3d9 5.8.4
Some checks failed
Default (tags) / security (push) Successful in 38s
Default (tags) / test (push) Successful in 3m2s
Default (tags) / release (push) Failing after 50s
Default (tags) / metadata (push) Successful in 57s
2025-04-17 11:47:38 +00:00
76ae454221 fix(core): Update commit metadata with no functional code changes 2025-04-17 11:47:38 +00:00
90cfc4644d 5.8.3
Some checks failed
Default (tags) / security (push) Successful in 39s
Default (tags) / test (push) Successful in 3m3s
Default (tags) / release (push) Failing after 54s
Default (tags) / metadata (push) Successful in 1m4s
2025-04-17 11:21:35 +00:00
0be279e5f5 fix(readme): Improve readme documentation on data models and connection management 2025-04-17 11:21:35 +00:00
9755522bba 5.8.2
Some checks failed
Default (tags) / security (push) Successful in 38s
Default (tags) / test (push) Successful in 2m58s
Default (tags) / release (push) Failing after 51s
Default (tags) / metadata (push) Successful in 58s
2025-04-14 18:13:10 +00:00
de8736e99e fix(classes.doc.ts): Ensure collection initialization before creating a cursor in getCursorExtended 2025-04-14 18:13:10 +00:00
c430627a21 5.8.1
Some checks failed
Default (tags) / security (push) Successful in 32s
Default (tags) / test (push) Successful in 3m0s
Default (tags) / release (push) Failing after 50s
Default (tags) / metadata (push) Successful in 1m2s
2025-04-14 18:06:29 +00:00
0bfebaf5b9 fix(cursor, doc): Add explicit return types and casts to SmartdataDbCursor methods and update getCursorExtended signature in SmartDataDbDoc. 2025-04-14 18:06:29 +00:00
8 changed files with 164 additions and 107 deletions

View File

@ -1,5 +1,51 @@
# Changelog # Changelog
## 2025-04-18 - 5.9.2 - fix(documentation)
Update search API documentation to replace deprecated searchWithLucene examples with the unified search(query) API and clarify its behavior.
- Replaced 'searchWithLucene' examples with 'search(query)' in the README.
- Updated explanation to detail field-specific exact match, partial word regex search, multi-word literal matching, and handling of empty queries.
- Clarified guidelines for creating MongoDB text indexes on searchable fields for optimized search performance.
## 2025-04-18 - 5.9.1 - fix(search)
Refactor search tests to use unified search API and update text index type casting
- Replaced all calls from searchWithLucene with search in test/search tests
- Updated text index specification in the collection class to use proper type casting
## 2025-04-18 - 5.9.0 - feat(collections/search)
Improve text index creation and search fallback mechanisms in collections and document search methods
- Auto-create a compound text index on all searchable fields in SmartdataCollection with a one-time flag to prevent duplicate index creation.
- Refine the search method in SmartDataDbDoc to support exact field matches and safe regex fallback for non-Lucene queries.
## 2025-04-17 - 5.8.4 - fix(core)
Update commit metadata with no functional code changes
- Commit info and documentation refreshed
- No code or test changes detected in the diff
## 2025-04-17 - 5.8.3 - fix(readme)
Improve readme documentation on data models and connection management
- Clarify that data models use @Collection, @unI, @svDb, @index, and @searchable decorators
- Document that ObjectId and Buffer fields are stored as BSON types natively without extra decorators
- Update connection management section to use 'db.close()' instead of 'db.disconnect()'
- Revise license section to reference the MIT License without including additional legal details
## 2025-04-14 - 5.8.2 - fix(classes.doc.ts)
Ensure collection initialization before creating a cursor in getCursorExtended
- Added 'await collection.init()' to guarantee that the MongoDB collection is initialized before using the cursor
- Prevents potential runtime errors when accessing collection.mongoDbCollection
## 2025-04-14 - 5.8.1 - fix(cursor, doc)
Add explicit return types and casts to SmartdataDbCursor methods and update getCursorExtended signature in SmartDataDbDoc.
- Specify Promise<T> as return type for next() in SmartdataDbCursor and cast return value to T.
- Specify Promise<T[]> as return type for toArray() in SmartdataDbCursor and cast return value to T[].
- Update getCursorExtended to return Promise<SmartdataDbCursor<T>> for clearer type safety.
## 2025-04-14 - 5.8.0 - feat(cursor) ## 2025-04-14 - 5.8.0 - feat(cursor)
Add toArray method to SmartdataDbCursor to convert raw MongoDB documents into initialized class instances Add toArray method to SmartdataDbCursor to convert raw MongoDB documents into initialized class instances

View File

@ -1,6 +1,6 @@
{ {
"name": "@push.rocks/smartdata", "name": "@push.rocks/smartdata",
"version": "5.8.0", "version": "5.9.2",
"private": false, "private": false,
"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.",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",

View File

@ -18,7 +18,7 @@ A powerful TypeScript-first MongoDB wrapper that provides advanced features for
- **Enhanced Cursors**: Chainable, type-safe cursor API with memory efficient data processing - **Enhanced Cursors**: Chainable, type-safe cursor API with memory efficient data processing
- **Type Conversion**: Automatic handling of MongoDB types like ObjectId and Binary data - **Type Conversion**: Automatic handling of MongoDB types like ObjectId and Binary data
- **Serialization Hooks**: Custom serialization and deserialization of document properties - **Serialization Hooks**: Custom serialization and deserialization of document properties
- **Powerful Search Capabilities**: Lucene-like query syntax with field-specific search, advanced operators, and fallback mechanisms - **Powerful Search Capabilities**: Unified `search(query)` API supporting field:value exact matches, multi-field regex searches, case-insensitive matching, and automatic escaping to prevent regex injection
## Requirements ## Requirements
@ -66,7 +66,7 @@ await db.init();
### Defining Data Models ### Defining Data Models
Data models in `@push.rocks/smartdata` are classes that represent collections and documents in your MongoDB database. Use decorators such as `@Collection`, `@unI`, and `@svDb` to define your data models. Data models in `@push.rocks/smartdata` are classes that represent collections and documents in your MongoDB database. Use decorators such as `@Collection`, `@unI`, `@svDb`, `@index`, and `@searchable` to define your data models. Fields of type `ObjectId` or `Buffer` decorated with `@svDb()` will be stored as BSON ObjectId and Binary, respectively; no separate `@oid()` or `@bin()` decorators are required.
```typescript ```typescript
import { import {
@ -74,8 +74,6 @@ import {
Collection, Collection,
unI, unI,
svDb, svDb,
oid,
bin,
index, index,
searchable, searchable,
} from '@push.rocks/smartdata'; } from '@push.rocks/smartdata';
@ -96,12 +94,10 @@ class User extends SmartDataDbDoc<User, User> {
public email: string; // Mark 'email' to be saved in DB public email: string; // Mark 'email' to be saved in DB
@svDb() @svDb()
@oid() // Automatically handle as ObjectId type public organizationId: ObjectId; // Stored as BSON ObjectId
public organizationId: ObjectId; // Will be automatically converted to/from ObjectId
@svDb() @svDb()
@bin() // Automatically handle as Binary data public profilePicture: Buffer; // Stored as BSON Binary
public profilePicture: Buffer; // Will be automatically converted to/from Binary
@svDb({ @svDb({
serialize: (data) => JSON.stringify(data), // Custom serialization serialize: (data) => JSON.stringify(data), // Custom serialization
@ -222,43 +218,31 @@ class Product extends SmartDataDbDoc<Product, Product> {
const searchableFields = getSearchableFields('Product'); // ['name', 'description', 'category'] const searchableFields = getSearchableFields('Product'); // ['name', 'description', 'category']
// Basic search across all searchable fields // Basic search across all searchable fields
const iPhoneProducts = await Product.searchWithLucene('iPhone'); const iphoneProducts = await Product.search('iPhone');
// Field-specific search // Field-specific exact match
const electronicsProducts = await Product.searchWithLucene('category:Electronics'); const electronicsProducts = await Product.search('category:Electronics');
// Search with wildcards // Partial word search (regex across all fields)
const macProducts = await Product.searchWithLucene('Mac*'); const laptopResults = await Product.search('laptop');
// Search in specific fields with partial words // Multi-word literal search
const laptopResults = await Product.searchWithLucene('description:laptop'); const paperwhite = await Product.search('Kindle Paperwhite');
// Search is case-insensitive // Empty query returns all documents
const results1 = await Product.searchWithLucene('electronics'); const allProducts = await Product.search('');
const results2 = await Product.searchWithLucene('Electronics');
// results1 and results2 will contain the same documents
// Using boolean operators (requires text index in MongoDB)
const wirelessOrLaptop = await Product.searchWithLucene('wireless OR laptop');
// Negative searches
const electronicsNotSamsung = await Product.searchWithLucene('Electronics NOT Samsung');
// Phrase searches
const exactPhrase = await Product.searchWithLucene('"high-speed blender"');
// Grouping with parentheses
const complexQuery = await Product.searchWithLucene('(wireless OR bluetooth) AND Electronics');
``` ```
The search functionality includes: The search functionality includes:
- `@searchable()` decorator for marking fields as searchable - `@searchable()` decorator for marking fields as searchable
- `getSearchableFields()` to retrieve all searchable fields for a class - `getSearchableFields()` to list searchable fields for a model
- `search()` method for basic search (requires MongoDB text index) - `search(query: string)` method supporting:
- `searchWithLucene()` method with robust fallback mechanisms - Field-specific exact matches (`field:value`)
- Support for field-specific searches, wildcards, and boolean operators - Case-insensitive partial matches across all searchable fields
- Automatic fallback to regex-based search if MongoDB text search fails - Multi-word literal matching
- Empty queries returning all documents
- Automatic escaping of special characters to prevent regex injection
### EasyStore ### EasyStore
@ -541,7 +525,7 @@ class Order extends SmartDataDbDoc<Order, Order> {
### Connection Management ### Connection Management
- Always call `db.init()` before using any database features - Always call `db.init()` before using any database features
- Use `db.disconnect()` when shutting down your application - Use `db.close()` when shutting down your application
- Set appropriate connection pool sizes based on your application's needs - Set appropriate connection pool sizes based on your application's needs
### Document Design ### Document Design
@ -553,10 +537,10 @@ class Order extends SmartDataDbDoc<Order, Order> {
### Search Optimization ### Search Optimization
- Create MongoDB text indexes for collections that need advanced search operations - (Optional) Create MongoDB text indexes on searchable fields to speed up full-text search
- Use `searchWithLucene()` for robust searches with fallback mechanisms - Use `search(query)` for all search operations (field:value, partial matches, multi-word)
- Prefer field-specific searches when possible for better performance - Prefer field-specific exact matches when possible for optimal performance
- Use simple term queries instead of boolean operators if you don't have text indexes - Avoid unnecessary complexity in query strings to keep regex searches efficient
### Performance Optimization ### Performance Optimization
@ -590,7 +574,7 @@ Please make sure to update tests as appropriate and follow our coding standards.
## License and Legal Information ## License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. This repository is licensed under the MIT License. For details, see [MIT License](https://opensource.org/licenses/MIT).
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file. **Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

View File

@ -104,21 +104,21 @@ tap.test('should search products by basic search method', async () => {
} }
}); });
tap.test('should search products with searchWithLucene method', async () => { tap.test('should search products with search method', async () => {
// Using the robust searchWithLucene method // Using the robust searchWithLucene method
const wirelessResults = await Product.searchWithLucene('wireless'); const wirelessResults = await Product.search('wireless');
console.log( console.log(
`Found ${wirelessResults.length} products matching 'wireless' using searchWithLucene`, `Found ${wirelessResults.length} products matching 'wireless' using search`,
); );
expect(wirelessResults.length).toEqual(1); expect(wirelessResults.length).toEqual(1);
expect(wirelessResults[0].name).toEqual('AirPods'); expect(wirelessResults[0].name).toEqual('AirPods');
}); });
tap.test('should search products by category with searchWithLucene', async () => { tap.test('should search products by category with search', async () => {
// Using field-specific search with searchWithLucene // Using field-specific search with searchWithLucene
const kitchenResults = await Product.searchWithLucene('category:Kitchen'); const kitchenResults = await Product.search('category:Kitchen');
console.log(`Found ${kitchenResults.length} products in Kitchen category using searchWithLucene`); console.log(`Found ${kitchenResults.length} products in Kitchen category using search`);
expect(kitchenResults.length).toEqual(2); expect(kitchenResults.length).toEqual(2);
expect(kitchenResults[0].category).toEqual('Kitchen'); expect(kitchenResults[0].category).toEqual('Kitchen');
@ -127,7 +127,7 @@ tap.test('should search products by category with searchWithLucene', async () =>
tap.test('should search products with partial word matches', async () => { tap.test('should search products with partial word matches', async () => {
// Testing partial word matches // Testing partial word matches
const proResults = await Product.searchWithLucene('Pro'); const proResults = await Product.search('Pro');
console.log(`Found ${proResults.length} products matching 'Pro'`); console.log(`Found ${proResults.length} products matching 'Pro'`);
// Should match both "MacBook Pro" and "professionals" in description // Should match both "MacBook Pro" and "professionals" in description
@ -136,7 +136,7 @@ tap.test('should search products with partial word matches', async () => {
tap.test('should search across multiple searchable fields', async () => { tap.test('should search across multiple searchable fields', async () => {
// Test searching across all searchable fields // Test searching across all searchable fields
const bookResults = await Product.searchWithLucene('book'); const bookResults = await Product.search('book');
console.log(`Found ${bookResults.length} products matching 'book' across all fields`); console.log(`Found ${bookResults.length} products matching 'book' across all fields`);
// Should match "MacBook" in name and "Books" in category // Should match "MacBook" in name and "Books" in category
@ -145,8 +145,8 @@ tap.test('should search across multiple searchable fields', async () => {
tap.test('should handle case insensitive searches', async () => { tap.test('should handle case insensitive searches', async () => {
// Test case insensitivity // Test case insensitivity
const electronicsResults = await Product.searchWithLucene('electronics'); const electronicsResults = await Product.search('electronics');
const ElectronicsResults = await Product.searchWithLucene('Electronics'); const ElectronicsResults = await Product.search('Electronics');
console.log(`Found ${electronicsResults.length} products matching lowercase 'electronics'`); console.log(`Found ${electronicsResults.length} products matching lowercase 'electronics'`);
console.log(`Found ${ElectronicsResults.length} products matching capitalized 'Electronics'`); console.log(`Found ${ElectronicsResults.length} products matching capitalized 'Electronics'`);
@ -166,14 +166,14 @@ tap.test('should demonstrate search fallback mechanisms', async () => {
// Use a simpler term that should be found in descriptions // Use a simpler term that should be found in descriptions
// Avoid using "OR" operator which requires a text index // Avoid using "OR" operator which requires a text index
const results = await Product.searchWithLucene('high'); const results = await Product.search('high');
console.log(`Found ${results.length} products matching 'high'`); console.log(`Found ${results.length} products matching 'high'`);
// "High-speed blender" contains "high" // "High-speed blender" contains "high"
expect(results.length).toBeGreaterThan(0); expect(results.length).toBeGreaterThan(0);
// Try another fallback example that won't need $text // Try another fallback example that won't need $text
const powerfulResults = await Product.searchWithLucene('powerful'); const powerfulResults = await Product.search('powerful');
console.log(`Found ${powerfulResults.length} products matching 'powerful'`); console.log(`Found ${powerfulResults.length} products matching 'powerful'`);
// "Powerful laptop for professionals" contains "powerful" // "Powerful laptop for professionals" contains "powerful"
@ -192,6 +192,35 @@ tap.test('should explain the advantages of the integrated approach', async () =>
expect(true).toEqual(true); expect(true).toEqual(true);
}); });
// Additional robustness tests
tap.test('should search exact name using field:value', async () => {
const nameResults = await Product.search('name:AirPods');
expect(nameResults.length).toEqual(1);
expect(nameResults[0].name).toEqual('AirPods');
});
tap.test('should throw when searching non-searchable field', async () => {
let error: Error;
try {
await Product.search('price:129');
} catch (err) {
error = err as Error;
}
expect(error).toBeTruthy();
expect(error.message).toMatch(/not searchable/);
});
tap.test('empty query should return all products', async () => {
const allResults = await Product.search('');
expect(allResults.length).toEqual(8);
});
tap.test('should search multi-word term across fields', async () => {
const termResults = await Product.search('iPhone 12');
expect(termResults.length).toEqual(1);
expect(termResults[0].name).toEqual('iPhone 12');
});
tap.test('close database connection', async () => { tap.test('close database connection', async () => {
await testDb.mongoDb.dropDatabase(); await testDb.mongoDb.dropDatabase();
await testDb.close(); await testDb.close();

View File

@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartdata', name: '@push.rocks/smartdata',
version: '5.8.0', version: '5.9.2',
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.'
} }

View File

@ -1,7 +1,7 @@
import * as plugins from './plugins.js'; import * as plugins from './plugins.js';
import { SmartdataDb } from './classes.db.js'; import { SmartdataDb } from './classes.db.js';
import { SmartdataDbCursor } from './classes.cursor.js'; import { SmartdataDbCursor } from './classes.cursor.js';
import { SmartDataDbDoc, type IIndexOptions } from './classes.doc.js'; import { SmartDataDbDoc, type IIndexOptions, getSearchableFields } from './classes.doc.js';
import { SmartdataDbWatcher } from './classes.watcher.js'; import { SmartdataDbWatcher } from './classes.watcher.js';
import { CollectionFactory } from './classes.collectionfactory.js'; import { CollectionFactory } from './classes.collectionfactory.js';
@ -128,6 +128,8 @@ export class SmartdataCollection<T> {
public smartdataDb: SmartdataDb; public smartdataDb: SmartdataDb;
public uniqueIndexes: string[] = []; public uniqueIndexes: string[] = [];
public regularIndexes: Array<{field: string, options: IIndexOptions}> = []; public regularIndexes: Array<{field: string, options: IIndexOptions}> = [];
// flag to ensure text index is created only once
private textIndexCreated: boolean = false;
constructor(classNameArg: string, smartDataDbArg: SmartdataDb) { constructor(classNameArg: string, smartDataDbArg: SmartdataDb) {
// tell the collection where it belongs // tell the collection where it belongs
@ -153,6 +155,16 @@ export class SmartdataCollection<T> {
console.log(`Successfully initiated Collection ${this.collectionName}`); console.log(`Successfully initiated Collection ${this.collectionName}`);
} }
this.mongoDbCollection = this.smartdataDb.mongoDb.collection(this.collectionName); this.mongoDbCollection = this.smartdataDb.mongoDb.collection(this.collectionName);
// Auto-create a compound text index on all searchable fields
const searchableFields = getSearchableFields(this.collectionName);
if (searchableFields.length > 0 && !this.textIndexCreated) {
// Build a compound text index spec
const indexSpec: Record<string, 'text'> = {};
searchableFields.forEach(f => { indexSpec[f] = 'text'; });
// Cast to any to satisfy TypeScript IndexSpecification typing
await this.mongoDbCollection.createIndex(indexSpec as any, { name: 'smartdata_text_index' });
this.textIndexCreated = true;
}
} }
} }

View File

@ -15,14 +15,14 @@ export class SmartdataDbCursor<T = any> {
this.smartdataDbDoc = dbDocArg; this.smartdataDbDoc = dbDocArg;
} }
public async next(closeAtEnd = true) { public async next(closeAtEnd = true): Promise<T> {
const result = this.smartdataDbDoc.createInstanceFromMongoDbNativeDoc( const result = this.smartdataDbDoc.createInstanceFromMongoDbNativeDoc(
await this.mongodbCursor.next(), await this.mongodbCursor.next(),
); );
if (!result && closeAtEnd) { if (!result && closeAtEnd) {
await this.close(); await this.close();
} }
return result; return result as T;
} }
public async forEach(forEachFuncArg: (itemArg: T) => Promise<any>, closeCursorAtEnd = true) { public async forEach(forEachFuncArg: (itemArg: T) => Promise<any>, closeCursorAtEnd = true) {
@ -40,9 +40,9 @@ export class SmartdataDbCursor<T = any> {
} }
} }
public async toArray() { public async toArray(): Promise<T[]> {
const result = await this.mongodbCursor.toArray(); const result = await this.mongodbCursor.toArray();
return result.map((itemArg) => this.smartdataDbDoc.createInstanceFromMongoDbNativeDoc(itemArg)); return result.map((itemArg) => this.smartdataDbDoc.createInstanceFromMongoDbNativeDoc(itemArg)) as T[];
} }
public async close() { public async close() {

View File

@ -61,6 +61,10 @@ export function getSearchableFields(className: string): string[] {
} }
return Array.from(searchableFieldsMap.get(className)); 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 * unique index - decorator to mark a unique index
@ -253,8 +257,9 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
this: plugins.tsclass.typeFest.Class<T>, this: plugins.tsclass.typeFest.Class<T>,
filterArg: plugins.tsclass.typeFest.PartialDeep<T>, filterArg: plugins.tsclass.typeFest.PartialDeep<T>,
modifierFunction = (cursorArg: plugins.mongodb.FindCursor<plugins.mongodb.WithId<plugins.mongodb.BSON.Document>>) => cursorArg, modifierFunction = (cursorArg: plugins.mongodb.FindCursor<plugins.mongodb.WithId<plugins.mongodb.BSON.Document>>) => cursorArg,
) { ): Promise<SmartdataDbCursor<T>> {
const collection: SmartdataCollection<T> = (this as any).collection; const collection: SmartdataCollection<T> = (this as any).collection;
await collection.init();
let cursor: plugins.mongodb.FindCursor<any> = collection.mongoDbCollection.find( let cursor: plugins.mongodb.FindCursor<any> = collection.mongoDbCollection.find(
convertFilterForMongoDb(filterArg), convertFilterForMongoDb(filterArg),
); );
@ -325,57 +330,38 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
} }
/** /**
* Search documents using Lucene query syntax * Search documents by text or field:value syntax, with safe regex fallback
* @param luceneQuery Lucene query string * @param query A search term or field:value expression
* @returns Array of matching documents * @returns Array of matching documents
*/ */
public static async search<T>( public static async search<T>(
this: plugins.tsclass.typeFest.Class<T>, this: plugins.tsclass.typeFest.Class<T>,
luceneQuery: string, query: string,
): Promise<T[]> { ): Promise<T[]> {
const filter = (this as any).createSearchFilter(luceneQuery); const className = (this as any).className || this.name;
return await (this as any).getInstances(filter); const searchableFields = getSearchableFields(className);
} if (searchableFields.length === 0) {
throw new Error(`No searchable fields defined for class ${className}`);
/**
* 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);
} }
// 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) * Search by text across all searchable fields (fallback method)
* @param searchText The text to search for in all searchable fields * @param searchText The text to search for in all searchable fields