419 lines
15 KiB
TypeScript
419 lines
15 KiB
TypeScript
|
|
/**
|
||
|
|
* Comprehensive Query Builder Example
|
||
|
|
*
|
||
|
|
* Demonstrates type-safe query construction with the QueryBuilder
|
||
|
|
*/
|
||
|
|
|
||
|
|
import {
|
||
|
|
createConfig,
|
||
|
|
ElasticsearchConnectionManager,
|
||
|
|
LogLevel,
|
||
|
|
} from '../../core/index.js';
|
||
|
|
import { DocumentManager } from '../../domain/documents/index.js';
|
||
|
|
import { QueryBuilder, createQuery } from '../../domain/query/index.js';
|
||
|
|
|
||
|
|
interface Product {
|
||
|
|
name: string;
|
||
|
|
description: string;
|
||
|
|
category: string;
|
||
|
|
brand: string;
|
||
|
|
price: number;
|
||
|
|
rating: number;
|
||
|
|
stock: number;
|
||
|
|
tags: string[];
|
||
|
|
createdAt: Date;
|
||
|
|
updatedAt: Date;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function main() {
|
||
|
|
console.log('=== Query Builder Example ===\n');
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// Step 1: Configuration
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
console.log('Step 1: Configuring Elasticsearch connection...');
|
||
|
|
const config = createConfig()
|
||
|
|
.fromEnv()
|
||
|
|
.nodes(process.env.ELASTICSEARCH_URL || 'http://localhost:9200')
|
||
|
|
.basicAuth(
|
||
|
|
process.env.ELASTICSEARCH_USERNAME || 'elastic',
|
||
|
|
process.env.ELASTICSEARCH_PASSWORD || 'changeme'
|
||
|
|
)
|
||
|
|
.timeout(30000)
|
||
|
|
.retries(3)
|
||
|
|
.logLevel(LogLevel.INFO)
|
||
|
|
.enableMetrics(true)
|
||
|
|
.enableTracing(true, { serviceName: 'query-example', serviceVersion: '1.0.0' })
|
||
|
|
.build();
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// Step 2: Initialize Connection
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
console.log('Step 2: Initializing connection manager...');
|
||
|
|
const connectionManager = ElasticsearchConnectionManager.getInstance(config);
|
||
|
|
await connectionManager.initialize();
|
||
|
|
console.log('✓ Connection manager initialized\n');
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// Step 3: Setup Sample Data
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
console.log('Step 3: Setting up sample data...');
|
||
|
|
const products = new DocumentManager<Product>({
|
||
|
|
index: 'products-query-example',
|
||
|
|
autoCreateIndex: true,
|
||
|
|
});
|
||
|
|
await products.initialize();
|
||
|
|
|
||
|
|
// Create sample products
|
||
|
|
const sampleProducts: Array<{ id: string; data: Product }> = [
|
||
|
|
{
|
||
|
|
id: 'laptop-1',
|
||
|
|
data: {
|
||
|
|
name: 'Professional Laptop Pro',
|
||
|
|
description: 'High-performance laptop for professionals',
|
||
|
|
category: 'Electronics',
|
||
|
|
brand: 'TechBrand',
|
||
|
|
price: 1299.99,
|
||
|
|
rating: 4.5,
|
||
|
|
stock: 15,
|
||
|
|
tags: ['laptop', 'professional', 'high-end'],
|
||
|
|
createdAt: new Date('2024-01-15'),
|
||
|
|
updatedAt: new Date('2024-01-20'),
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'laptop-2',
|
||
|
|
data: {
|
||
|
|
name: 'Budget Laptop Basic',
|
||
|
|
description: 'Affordable laptop for everyday use',
|
||
|
|
category: 'Electronics',
|
||
|
|
brand: 'ValueBrand',
|
||
|
|
price: 499.99,
|
||
|
|
rating: 3.8,
|
||
|
|
stock: 30,
|
||
|
|
tags: ['laptop', 'budget', 'student'],
|
||
|
|
createdAt: new Date('2024-02-01'),
|
||
|
|
updatedAt: new Date('2024-02-05'),
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'phone-1',
|
||
|
|
data: {
|
||
|
|
name: 'Smartphone X',
|
||
|
|
description: 'Latest flagship smartphone',
|
||
|
|
category: 'Electronics',
|
||
|
|
brand: 'PhoneBrand',
|
||
|
|
price: 899.99,
|
||
|
|
rating: 4.7,
|
||
|
|
stock: 25,
|
||
|
|
tags: ['smartphone', 'flagship', '5g'],
|
||
|
|
createdAt: new Date('2024-01-20'),
|
||
|
|
updatedAt: new Date('2024-01-25'),
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'tablet-1',
|
||
|
|
data: {
|
||
|
|
name: 'Tablet Pro',
|
||
|
|
description: 'Professional tablet for creative work',
|
||
|
|
category: 'Electronics',
|
||
|
|
brand: 'TechBrand',
|
||
|
|
price: 799.99,
|
||
|
|
rating: 4.6,
|
||
|
|
stock: 20,
|
||
|
|
tags: ['tablet', 'creative', 'professional'],
|
||
|
|
createdAt: new Date('2024-02-10'),
|
||
|
|
updatedAt: new Date('2024-02-15'),
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'monitor-1',
|
||
|
|
data: {
|
||
|
|
name: '4K Monitor',
|
||
|
|
description: 'Ultra HD monitor for gaming and design',
|
||
|
|
category: 'Electronics',
|
||
|
|
brand: 'DisplayBrand',
|
||
|
|
price: 599.99,
|
||
|
|
rating: 4.4,
|
||
|
|
stock: 12,
|
||
|
|
tags: ['monitor', '4k', 'gaming'],
|
||
|
|
createdAt: new Date('2024-01-25'),
|
||
|
|
updatedAt: new Date('2024-01-30'),
|
||
|
|
},
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
// Index sample data
|
||
|
|
const session = products.session();
|
||
|
|
session.start();
|
||
|
|
for (const product of sampleProducts) {
|
||
|
|
session.upsert(product.id, product.data);
|
||
|
|
}
|
||
|
|
await session.commit();
|
||
|
|
console.log(`✓ Indexed ${sampleProducts.length} sample products\n`);
|
||
|
|
|
||
|
|
// Wait for indexing to complete
|
||
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// Step 4: Simple Queries
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
console.log('Step 4: Running simple queries...\n');
|
||
|
|
|
||
|
|
// 4.1: Match query - search by name
|
||
|
|
console.log('4.1: Match query - search for "laptop"');
|
||
|
|
const laptopResults = await createQuery<Product>('products-query-example')
|
||
|
|
.match('name', 'laptop')
|
||
|
|
.size(10)
|
||
|
|
.execute();
|
||
|
|
console.log(`Found ${laptopResults.hits.total.value} laptops`);
|
||
|
|
console.log('Laptops:', laptopResults.hits.hits.map((h) => h._source.name));
|
||
|
|
console.log();
|
||
|
|
|
||
|
|
// 4.2: Term query - exact match on category
|
||
|
|
console.log('4.2: Term query - exact category match');
|
||
|
|
const electronicsResults = await createQuery<Product>('products-query-example')
|
||
|
|
.term('category.keyword', 'Electronics')
|
||
|
|
.execute();
|
||
|
|
console.log(`Found ${electronicsResults.hits.total.value} electronics`);
|
||
|
|
console.log();
|
||
|
|
|
||
|
|
// 4.3: Range query - price between 500 and 1000
|
||
|
|
console.log('4.3: Range query - price between $500 and $1000');
|
||
|
|
const midPriceResults = await createQuery<Product>('products-query-example')
|
||
|
|
.range('price', { gte: 500, lte: 1000 })
|
||
|
|
.sort('price', 'asc')
|
||
|
|
.execute();
|
||
|
|
console.log(`Found ${midPriceResults.hits.total.value} products in price range`);
|
||
|
|
midPriceResults.hits.hits.forEach((hit) => {
|
||
|
|
console.log(` - ${hit._source.name}: $${hit._source.price}`);
|
||
|
|
});
|
||
|
|
console.log();
|
||
|
|
|
||
|
|
// 4.4: Multi-match query - search across multiple fields
|
||
|
|
console.log('4.4: Multi-match query - search "professional" in name and description');
|
||
|
|
const professionalResults = await createQuery<Product>('products-query-example')
|
||
|
|
.multiMatch('professional', ['name', 'description'])
|
||
|
|
.execute();
|
||
|
|
console.log(`Found ${professionalResults.hits.total.value} professional products`);
|
||
|
|
console.log();
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// Step 5: Boolean Queries
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
console.log('Step 5: Running boolean queries...\n');
|
||
|
|
|
||
|
|
// 5.1: Must + Filter - combine multiple conditions
|
||
|
|
console.log('5.1: Boolean query - TechBrand products over $700');
|
||
|
|
const techBrandResults = await createQuery<Product>('products-query-example')
|
||
|
|
.term('brand.keyword', 'TechBrand')
|
||
|
|
.range('price', { gte: 700 })
|
||
|
|
.sort('price', 'desc')
|
||
|
|
.execute();
|
||
|
|
console.log(`Found ${techBrandResults.hits.total.value} matching products`);
|
||
|
|
techBrandResults.hits.hits.forEach((hit) => {
|
||
|
|
console.log(` - ${hit._source.name} (${hit._source.brand}): $${hit._source.price}`);
|
||
|
|
});
|
||
|
|
console.log();
|
||
|
|
|
||
|
|
// 5.2: Should clause - match any condition
|
||
|
|
console.log('5.2: Should query - products matching "laptop" OR "tablet"');
|
||
|
|
const laptopOrTabletResults = await new QueryBuilder<Product>('products-query-example')
|
||
|
|
.should({ match: { name: { query: 'laptop' } } })
|
||
|
|
.should({ match: { name: { query: 'tablet' } } })
|
||
|
|
.minimumMatch(1)
|
||
|
|
.execute();
|
||
|
|
console.log(`Found ${laptopOrTabletResults.hits.total.value} laptops or tablets`);
|
||
|
|
console.log();
|
||
|
|
|
||
|
|
// 5.3: Must not - exclude results
|
||
|
|
console.log('5.3: Must not query - electronics excluding laptops');
|
||
|
|
const noLaptopsResults = await createQuery<Product>('products-query-example')
|
||
|
|
.term('category.keyword', 'Electronics')
|
||
|
|
.mustNot({ match: { name: { query: 'laptop' } } })
|
||
|
|
.execute();
|
||
|
|
console.log(`Found ${noLaptopsResults.hits.total.value} non-laptop electronics`);
|
||
|
|
console.log();
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// Step 6: Aggregations
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
console.log('Step 6: Running aggregations...\n');
|
||
|
|
|
||
|
|
// 6.1: Terms aggregation - group by brand
|
||
|
|
console.log('6.1: Terms aggregation - products by brand');
|
||
|
|
const brandAggResults = await createQuery<Product>('products-query-example')
|
||
|
|
.matchAll()
|
||
|
|
.size(0) // We only want aggregations, not documents
|
||
|
|
.aggregations((agg) => {
|
||
|
|
agg.terms('brands', 'brand.keyword', { size: 10 });
|
||
|
|
})
|
||
|
|
.execute();
|
||
|
|
if (brandAggResults.aggregations && 'brands' in brandAggResults.aggregations) {
|
||
|
|
const brandsAgg = brandAggResults.aggregations.brands as { buckets: Array<{ key: string; doc_count: number }> };
|
||
|
|
console.log('Products by brand:');
|
||
|
|
brandsAgg.buckets.forEach((bucket) => {
|
||
|
|
console.log(` - ${bucket.key}: ${bucket.doc_count} products`);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
console.log();
|
||
|
|
|
||
|
|
// 6.2: Metric aggregations - price statistics
|
||
|
|
console.log('6.2: Metric aggregations - price statistics');
|
||
|
|
const priceStatsResults = await createQuery<Product>('products-query-example')
|
||
|
|
.matchAll()
|
||
|
|
.size(0)
|
||
|
|
.aggregations((agg) => {
|
||
|
|
agg.stats('price_stats', 'price');
|
||
|
|
agg.avg('avg_rating', 'rating');
|
||
|
|
agg.sum('total_stock', 'stock');
|
||
|
|
})
|
||
|
|
.execute();
|
||
|
|
if (priceStatsResults.aggregations) {
|
||
|
|
console.log('Price statistics:', priceStatsResults.aggregations.price_stats);
|
||
|
|
console.log('Average rating:', priceStatsResults.aggregations.avg_rating);
|
||
|
|
console.log('Total stock:', priceStatsResults.aggregations.total_stock);
|
||
|
|
}
|
||
|
|
console.log();
|
||
|
|
|
||
|
|
// 6.3: Nested aggregations - brands with average price
|
||
|
|
console.log('6.3: Nested aggregations - average price per brand');
|
||
|
|
const nestedAggResults = await createQuery<Product>('products-query-example')
|
||
|
|
.matchAll()
|
||
|
|
.size(0)
|
||
|
|
.aggregations((agg) => {
|
||
|
|
agg.terms('brands', 'brand.keyword', { size: 10 }).subAggregation('avg_price', (sub) => {
|
||
|
|
sub.avg('avg_price', 'price');
|
||
|
|
});
|
||
|
|
})
|
||
|
|
.execute();
|
||
|
|
if (nestedAggResults.aggregations && 'brands' in nestedAggResults.aggregations) {
|
||
|
|
const brandsAgg = nestedAggResults.aggregations.brands as {
|
||
|
|
buckets: Array<{ key: string; doc_count: number; avg_price: { value: number } }>;
|
||
|
|
};
|
||
|
|
console.log('Average price by brand:');
|
||
|
|
brandsAgg.buckets.forEach((bucket) => {
|
||
|
|
console.log(` - ${bucket.key}: $${bucket.avg_price.value.toFixed(2)} (${bucket.doc_count} products)`);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
console.log();
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// Step 7: Advanced Features
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
console.log('Step 7: Advanced query features...\n');
|
||
|
|
|
||
|
|
// 7.1: Pagination
|
||
|
|
console.log('7.1: Pagination - page 1 of results (2 per page)');
|
||
|
|
const page1Results = await createQuery<Product>('products-query-example')
|
||
|
|
.matchAll()
|
||
|
|
.paginate(1, 2)
|
||
|
|
.sort('price', 'asc')
|
||
|
|
.execute();
|
||
|
|
console.log(`Page 1: ${page1Results.hits.hits.length} results`);
|
||
|
|
page1Results.hits.hits.forEach((hit) => {
|
||
|
|
console.log(` - ${hit._source.name}: $${hit._source.price}`);
|
||
|
|
});
|
||
|
|
console.log();
|
||
|
|
|
||
|
|
// 7.2: Source filtering - only return specific fields
|
||
|
|
console.log('7.2: Source filtering - only name and price');
|
||
|
|
const filteredResults = await createQuery<Product>('products-query-example')
|
||
|
|
.matchAll()
|
||
|
|
.fields(['name', 'price'])
|
||
|
|
.size(3)
|
||
|
|
.execute();
|
||
|
|
console.log('Filtered results:');
|
||
|
|
filteredResults.hits.hits.forEach((hit) => {
|
||
|
|
console.log(` - Name: ${hit._source.name}, Price: ${hit._source.price}`);
|
||
|
|
});
|
||
|
|
console.log();
|
||
|
|
|
||
|
|
// 7.3: Count documents
|
||
|
|
console.log('7.3: Count documents matching query');
|
||
|
|
const count = await createQuery<Product>('products-query-example')
|
||
|
|
.range('price', { gte: 500 })
|
||
|
|
.count();
|
||
|
|
console.log(`Count of products over $500: ${count}`);
|
||
|
|
console.log();
|
||
|
|
|
||
|
|
// 7.4: Get only sources (convenience method)
|
||
|
|
console.log('7.4: Get sources only');
|
||
|
|
const sources = await createQuery<Product>('products-query-example')
|
||
|
|
.term('brand.keyword', 'TechBrand')
|
||
|
|
.executeAndGetSources();
|
||
|
|
console.log(`TechBrand products: ${sources.map((s) => s.name).join(', ')}`);
|
||
|
|
console.log();
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// Step 8: Complex Real-World Query
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
console.log('Step 8: Complex real-world query...\n');
|
||
|
|
|
||
|
|
console.log('Finding high-rated electronics in stock, sorted by best deals:');
|
||
|
|
const complexResults = await createQuery<Product>('products-query-example')
|
||
|
|
.term('category.keyword', 'Electronics')
|
||
|
|
.range('rating', { gte: 4.0 })
|
||
|
|
.range('stock', { gt: 0 })
|
||
|
|
.range('price', { lte: 1000 })
|
||
|
|
.sort('rating', 'desc')
|
||
|
|
.size(5)
|
||
|
|
.aggregations((agg) => {
|
||
|
|
agg.terms('top_brands', 'brand.keyword', { size: 5 });
|
||
|
|
agg.avg('avg_price', 'price');
|
||
|
|
agg.max('max_rating', 'rating');
|
||
|
|
})
|
||
|
|
.execute();
|
||
|
|
|
||
|
|
console.log(`Found ${complexResults.hits.total.value} matching products`);
|
||
|
|
console.log('\nTop results:');
|
||
|
|
complexResults.hits.hits.forEach((hit, index) => {
|
||
|
|
console.log(` ${index + 1}. ${hit._source.name}`);
|
||
|
|
console.log(` Brand: ${hit._source.brand}`);
|
||
|
|
console.log(` Price: $${hit._source.price}`);
|
||
|
|
console.log(` Rating: ${hit._source.rating}⭐`);
|
||
|
|
console.log(` Stock: ${hit._source.stock} units`);
|
||
|
|
});
|
||
|
|
|
||
|
|
if (complexResults.aggregations) {
|
||
|
|
console.log('\nAggregated insights:');
|
||
|
|
console.log(' Average price:', complexResults.aggregations.avg_price);
|
||
|
|
console.log(' Max rating:', complexResults.aggregations.max_rating);
|
||
|
|
if ('top_brands' in complexResults.aggregations) {
|
||
|
|
const topBrands = complexResults.aggregations.top_brands as { buckets: Array<{ key: string; doc_count: number }> };
|
||
|
|
console.log(' Top brands:');
|
||
|
|
topBrands.buckets.forEach((bucket) => {
|
||
|
|
console.log(` - ${bucket.key}: ${bucket.doc_count} products`);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
console.log();
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// Step 9: Cleanup
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
console.log('Step 9: Cleanup...');
|
||
|
|
await products.deleteIndex();
|
||
|
|
console.log('✓ Test index deleted');
|
||
|
|
|
||
|
|
await connectionManager.destroy();
|
||
|
|
console.log('✓ Connection closed\n');
|
||
|
|
|
||
|
|
console.log('=== Query Builder Example Complete ===');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Run the example
|
||
|
|
main().catch((error) => {
|
||
|
|
console.error('Example failed:', error);
|
||
|
|
process.exit(1);
|
||
|
|
});
|