@n2flowjs/nbase
Version:
Neural Vector Database for efficient similarity search
263 lines (237 loc) • 10 kB
text/typescript
import config from '../config'; // Keep this for fallback default timeout
import {
BaseSearchOptions,
BatchSearchConfiguration,
// Import optimized types
BatchSearchQuery,
PartitionedVectorDBInterface,
SearchExecutionOptions,
SearchResult,
} from '../types'; // Path to the optimized types file
import { createTimer } from '../utils/profiling';
/**
* BatchEngineSearch processes multiple vector queries in parallel using a PartitionedVectorDBInterface.
* This version uses the optimized type definitions.
*/
/**
* Manages efficient processing of multiple vector search queries in batches.
*
* The BatchEngineSearch class provides functionality to handle concurrent vector
* search operations against a partitioned vector database. It automatically manages
* large batches by splitting them into smaller chunks, processes queries in parallel,
* and handles timeout and error scenarios gracefully.
*
* Key features:
* - Efficient batching of multiple vector search queries
* - Automatic chunking of large query batches
* - Concurrent query execution with configurable timeouts
* - Support for preserving original query order in results
* - Integration with different search methods (HNSW or clustered)
* - Error handling and graceful degradation
*
* @example
* ```typescript
* const searchEngine = new PartitionedVectorDB(...);
* const batchSearch = new BatchEngineSearch(searchEngine, {
* maxBatchSize: 32,
* defaultSearchTimeoutMs: 10000
* });
*
* const queries = [
* { query: vectorA, k: 5, options: { useHNSW: true } },
* { query: vectorB, k: 10, options: { filter: { category: "tech" } } }
* ];
*
* const results = await batchSearch.searchBatch(queries);
* ```
*/
export class BatchEngineSearch {
private searchEngine: PartitionedVectorDBInterface;
private options: Required<BatchSearchConfiguration>; // Use the new configuration type
constructor(
searchEngine: PartitionedVectorDBInterface,
options: BatchSearchConfiguration = {} // Accept the new configuration type
) {
this.searchEngine = searchEngine;
// Define default values using keys from BatchSearchConfiguration
const defaults: Required<BatchSearchConfiguration> = {
maxBatchSize: 64,
prioritizeOrder: true,
groupSimilarQueries: false,
defaultSearchTimeoutMs: config.batchSearch?.defaultSearchTimeoutMs || 15000,
};
// Merge provided options with defaults
this.options = { ...defaults, ...options };
}
/**
* Process multiple search queries in a batch.
* @param queries - Array of search queries using the new BatchSearchQuery interface.
* @returns Results for each query.
*/
async searchBatch(queries: BatchSearchQuery[]): Promise<SearchResult[][]> {
if (!queries || !queries.length) return [];
const timer = createTimer();
timer.start('batch_search_partitioned');
// Split into smaller batches if too large, using the new option key
if (queries.length > this.options.maxBatchSize) {
const results: SearchResult[][] = [];
console.log(`Batch too large (${queries.length}), splitting into chunks of ${this.options.maxBatchSize}`);
for (let i = 0; i < queries.length; i += this.options.maxBatchSize) {
const batchQueries = queries.slice(i, i + this.options.maxBatchSize);
const batchResults = await this.searchBatch(batchQueries); // Recursive call
results.push(...batchResults);
}
timer.stop('batch_search_partitioned');
console.log(`Finished processing large batch (${queries.length}) in ${timer.getElapsed('batch_search_partitioned')}ms`);
return results;
}
// Process the current batch (or sub-batch)
const results = await this._processBatch(queries);
timer.stop('batch_search_partitioned');
console.log(`Processed batch of ${queries.length} queries in ${timer.getElapsed('batch_search_partitioned')}ms`);
return results;
}
/**
* Process batch queries using the PartitionedVectorDB directly.
* Leverages Promise.all for concurrency across queries and relies on the
* PartitionedVectorDB's internal parallelism.
*/
private async _processBatch(queries: BatchSearchQuery[]): Promise<SearchResult[][]> {
const timer = createTimer();
timer.start('process_batch');
let processedQueries = queries;
if (this.options.groupSimilarQueries) {
processedQueries = this._groupSimilarQueries(queries);
}
// Use Promise.all to run searches concurrently
const resultsPromises = processedQueries.map(async (queryData, originalIndex) => {
// Retain originalIndex for reordering if needed
// Destructure from BatchSearchQuery
const { query, k, options = {} } = queryData;
const queryTimer = createTimer();
queryTimer.start(`query_${originalIndex}`); // Use original index for tracking
let methodUsed = 'unknown';
try {
// Build options object to pass into the search engine method
// Combine fields from BaseSearchOptions and SearchExecutionOptions
const engineSearchOptions: BaseSearchOptions & SearchExecutionOptions = {
// BaseSearchOptions
k: k, // k is usually passed separately but can be included if the engine API requires it
filter: options.filter,
includeMetadata: false, // Metadata is usually not needed in raw search
distanceMetric: options.distanceMetric, // Allow overriding the default metric
// SearchExecutionOptions
partitionIds: options.partitionIds,
efSearch: options.efSearch, // For HNSW search
};
let queryResult: SearchResult[];
// Decide the method based on the options *of each query*
if (options.useHNSW && typeof this.searchEngine.findNearestHNSW === 'function') {
methodUsed = 'hnsw';
queryResult = await this.searchEngine.findNearestHNSW(
query,
k,
engineSearchOptions // Pass the merged options
);
} else if (typeof this.searchEngine.findNearest === 'function') {
methodUsed = 'clustered';
// Ensure HNSW-specific parameters are not passed to findNearest
const { efSearch, ...clusteredOptions } = engineSearchOptions;
queryResult = await this.searchEngine.findNearest(query, k, clusteredOptions);
} else {
throw new Error('Search engine provides neither findNearestHNSW nor findNearest.');
}
queryTimer.stop(`query_${originalIndex}`);
console.log(`Query ${originalIndex} (k=${k}, method=${methodUsed}) took ${queryTimer.getElapsed(`query_${originalIndex}`)}ms`);
return {
originalIndex: originalIndex, // Retain original index for reordering
result: queryResult,
error: null,
};
} catch (error) {
queryTimer.stop(`query_${originalIndex}`);
console.error(`Error processing query ${originalIndex} after ${queryTimer.getElapsed(`query_${originalIndex}`)}ms:`, error);
return {
originalIndex: originalIndex,
result: [] as SearchResult[], // Return an empty array in case of error
error: error instanceof Error ? error.message : String(error),
};
}
});
// Add timeout to each promise using the new option key
const timedPromises = resultsPromises.map((p) =>
Promise.race([
p,
new Promise<{
originalIndex: number;
result: SearchResult[];
error: string;
}>((_, reject) =>
setTimeout(
() => reject(new Error(`Query timed out after ${this.options.defaultSearchTimeoutMs}ms`)),
this.options.defaultSearchTimeoutMs // Use the new key
)
),
]).catch((error) => {
console.error('Batch query failed or timed out:', error);
return {
originalIndex: -1,
result: [],
error: (error as Error).message,
};
})
);
// Wait for all queries to complete or timeout
const settledResults = await Promise.all(timedPromises);
timer.stop('process_batch');
// Reconstruct results array, potentially reordering
const finalResults: SearchResult[][] = new Array(queries.length);
if (this.options.prioritizeOrder) {
// Use originalIndex to place results correctly
for (const res of settledResults) {
if (res.originalIndex !== -1 && res.originalIndex < finalResults.length) {
finalResults[res.originalIndex] = res.result;
if (res.error) {
console.warn(`Query at original index ${res.originalIndex} failed: ${res.error}`);
}
} else if (res.originalIndex === -1 && res.error) {
console.error(`A query timed out or failed without recoverable index: ${res.error}`);
}
}
for (let i = 0; i < finalResults.length; i++) {
if (finalResults[i] === undefined) {
console.warn(`Result for original index ${i} is missing (likely due to unrecoverable error/timeout).`);
finalResults[i] = [];
}
}
} else {
settledResults.forEach((res, i) => {
finalResults[i] = res.result;
if (res.error) {
console.warn(`Query at result index ${i} (order not prioritized) failed: ${res.error}`);
}
});
while (finalResults.length < queries.length) {
finalResults.push([]);
}
finalResults.length = queries.length;
}
return finalResults;
}
/**
* Placeholder for grouping similar queries.
* @private
*/
private _groupSimilarQueries(queries: BatchSearchQuery[]): BatchSearchQuery[] {
if (this.options.groupSimilarQueries) {
console.log('Query grouping requested but basic implementation used.');
}
return queries;
}
/**
* Clean up resources (no-op in this version).
*/
shutdown(): void {
console.log('PartitionedBatchSearch shutdown.');
}
}