feat(search): Improve search functionality: update documentation, refine Lucene query transformation, and add advanced search tests

This commit is contained in:
2025-04-21 15:27:55 +00:00
parent 0834ec5c91
commit 23b499b3a8
7 changed files with 373 additions and 76 deletions

View File

@ -0,0 +1,187 @@
import { tap, expect } from '@push.rocks/tapbundle';
import * as smartmongo from '@push.rocks/smartmongo';
import * as smartdata from '../ts/index.js';
import { searchable, getSearchableFields } from '../ts/classes.doc.js';
import { smartunique } from '../ts/plugins.js';
// Set up database connection
let smartmongoInstance: smartmongo.SmartMongo;
let testDb: smartdata.SmartdataDb;
// Define a test class for advanced search scenarios
@smartdata.Collection(() => testDb)
class Product extends smartdata.SmartDataDbDoc<Product, Product> {
@smartdata.unI()
public id: string = smartunique.shortId();
@smartdata.svDb()
@searchable()
public name: string;
@smartdata.svDb()
@searchable()
public description: string;
@smartdata.svDb()
@searchable()
public category: string;
@smartdata.svDb()
public price: number;
constructor(
nameArg: string,
descriptionArg: string,
categoryArg: string,
priceArg: number,
) {
super();
this.name = nameArg;
this.description = descriptionArg;
this.category = categoryArg;
this.price = priceArg;
}
}
// Initialize DB and insert sample products
tap.test('setup advanced search database', async () => {
smartmongoInstance = await smartmongo.SmartMongo.createAndStart();
testDb = new smartdata.SmartdataDb(
await smartmongoInstance.getMongoDescriptor(),
);
await testDb.init();
});
tap.test('insert products for advanced search', async () => {
const products = [
new Product(
'Night Owl Lamp',
'Bright lamp for night reading',
'Lighting',
29,
),
new Product(
'Day Light Lamp',
'Daytime lamp with adjustable brightness',
'Lighting',
39,
),
new Product(
'Office Chair',
'Ergonomic chair for office',
'Furniture',
199,
),
new Product(
'Gaming Chair',
'Comfortable for long gaming sessions',
'Furniture',
299,
),
new Product(
'iPhone 12',
'Latest iPhone with A14 Bionic chip',
'Electronics',
999,
),
new Product(
'AirPods',
'Wireless earbuds with noise cancellation',
'Electronics',
249,
),
];
for (const p of products) {
await p.save();
}
const all = await Product.getInstances({});
expect(all.length).toEqual(products.length);
});
// Simple exact field:value matching
tap.test('simpleExact: category:Furniture returns chairs', async () => {
const res = await Product.search('category:Furniture');
expect(res.length).toEqual(2);
const names = res.map((r) => r.name).sort();
expect(names).toEqual(['Gaming Chair', 'Office Chair']);
});
// simpleExact invalid field should throw
tap.test('simpleExact invalid field errors', async () => {
let error: Error;
try {
await Product.search('price:29');
} catch (e) {
error = e as Error;
}
expect(error).toBeTruthy();
expect(error.message).toMatch(/not searchable/);
});
// Quoted phrase search
tap.test('quoted phrase "Bright lamp" matches Night Owl Lamp', async () => {
const res = await Product.search('"Bright lamp"');
expect(res.length).toEqual(1);
expect(res[0].name).toEqual('Night Owl Lamp');
});
tap.test("quoted phrase 'night reading' matches Night Owl Lamp", async () => {
const res = await Product.search("'night reading'");
expect(res.length).toEqual(1);
expect(res[0].name).toEqual('Night Owl Lamp');
});
tap.test('wildcard description:*gaming* matches Gaming Chair', async () => {
const res = await Product.search('description:*gaming*');
expect(res.length).toEqual(1);
expect(res[0].name).toEqual('Gaming Chair');
});
// Boolean AND and OR
tap.test('boolean AND: category:Lighting AND lamp', async () => {
const res = await Product.search('category:Lighting AND lamp');
expect(res.length).toEqual(2);
});
tap.test('boolean OR: Furniture OR Electronics', async () => {
const res = await Product.search('Furniture OR Electronics');
expect(res.length).toEqual(4);
});
// Multi-term unquoted -> AND across terms
tap.test('multi-term unquoted adjustable brightness', async () => {
const res = await Product.search('adjustable brightness');
expect(res.length).toEqual(1);
expect(res[0].name).toEqual('Day Light Lamp');
});
tap.test('multi-term unquoted Night Lamp', async () => {
const res = await Product.search('Night Lamp');
expect(res.length).toEqual(1);
expect(res[0].name).toEqual('Night Owl Lamp');
});
// Grouping with parentheses
tap.test('grouping: (Furniture OR Electronics) AND Chair', async () => {
const res = await Product.search(
'(Furniture OR Electronics) AND Chair',
);
expect(res.length).toEqual(2);
const names = res.map((r) => r.name).sort();
expect(names).toEqual(['Gaming Chair', 'Office Chair']);
});
// Teardown
tap.test('cleanup advanced search database', async () => {
await testDb.mongoDb.dropDatabase();
await testDb.close();
if (smartmongoInstance) {
await smartmongoInstance.stopAndDumpToDir(
`.nogit/dbdump/test.search.advanced.ts`,
);
}
setTimeout(() => process.exit(), 2000);
});
tap.start({ throwOnError: true });

View File

@ -221,6 +221,42 @@ tap.test('should search multi-word term across fields', async () => {
expect(termResults[0].name).toEqual('iPhone 12');
});
// Additional search scenarios
tap.test('should return zero results for non-existent terms', async () => {
const noResults = await Product.search('NonexistentTerm');
expect(noResults.length).toEqual(0);
});
tap.test('should search products by description term "noise"', async () => {
const noiseResults = await Product.search('noise');
expect(noiseResults.length).toEqual(1);
expect(noiseResults[0].name).toEqual('AirPods');
});
tap.test('should search products by description term "flagship"', async () => {
const flagshipResults = await Product.search('flagship');
expect(flagshipResults.length).toEqual(1);
expect(flagshipResults[0].name).toEqual('Galaxy S21');
});
tap.test('should search numeric strings "12"', async () => {
const twelveResults = await Product.search('12');
expect(twelveResults.length).toEqual(1);
expect(twelveResults[0].name).toEqual('iPhone 12');
});
tap.test('should search hyphenated terms "high-speed"', async () => {
const hyphenResults = await Product.search('high-speed');
expect(hyphenResults.length).toEqual(1);
expect(hyphenResults[0].name).toEqual('Blender');
});
tap.test('should search hyphenated terms "E-reader"', async () => {
const ereaderResults = await Product.search('E-reader');
expect(ereaderResults.length).toEqual(1);
expect(ereaderResults[0].name).toEqual('Kindle Paperwhite');
});
tap.test('close database connection', async () => {
await testDb.mongoDb.dropDatabase();
await testDb.close();