UNPKG

@n2flowjs/nbase

Version:

Neural Vector Database for efficient similarity search

590 lines (513 loc) 21 kB
import { Router } from 'express'; import type { Request, Response } from 'express'; import { ensureDatabaseReady } from '../middleware/common'; import { UpdateMetadataRequest, ApiContext, VectorData } from '../../types'; // Thêm VectorData /** * Creates and configures Express router with vector-related API endpoints. * * Sets up the following endpoints: * - `POST /api/vectors` - Add a single vector or bulk vectors * - `GET /api/vectors/:id` - Get a vector by ID * - `GET /api/vectors/:id/exists` - Check if a vector exists * - `PATCH /api/vectors/:id/metadata` - Update vector metadata * - `DELETE /api/vectors/:id` - Delete a vector * - `GET /api/vectors/:id/similar` - Find similar vectors to a given vector * * Each endpoint includes proper error handling, database readiness checks, * and timing metrics. The endpoints support both string and numeric IDs, * with automatic type conversion attempts when a lookup fails. * * @param context - The API context containing database and timer instances * @returns An Express router configured with vector-related endpoints */ export function vectorRoutes(context: ApiContext) { const router = Router(); // Database bây giờ là instance của Database class mới (DatabasePartitioned) const { database, timer } = context; // Apply database readiness middleware to all routes router.use(ensureDatabaseReady(database)); /** * Add vectors (single or bulk) * POST /api/vectors * Tương tác với database.addVector hoặc database.bulkAdd (async) */ router.post('/', async (req: Request, res: Response) => { // Thêm async timer.start('add_vectors'); const { vectors } = req.body; // Check if the request is for a single vector or bulk vectors const isBulk = Array.isArray(vectors); try { console.log('[Database][Vector] Received request to add vectors'); // Database readiness check moved to middleware let addedCount = 0; const addedIds: (string | number)[] = []; // Lưu ID thực tế được trả về const dimensionCounts: Record<number, number> = {}; const partitionInfo: Record<string, number> = {}; // Theo dõi partition nào nhận bao nhiêu vector if (isBulk) { // ----- Bulk Add Logic ----- const vectorsToAdd: VectorData[] = []; const rawVectors = vectors; if (!Array.isArray(rawVectors)) { res.status(400).json({ success: false, error: "Invalid request: 'vectors' array is required for bulk add", }); return; } for (const item of rawVectors) { if (!item || !item.vector || !Array.isArray(item.vector)) { console.warn(`Skipping invalid vector item in bulk add (vector must be an array): ${JSON.stringify(item)}`); continue; } const typedVector = item.vector instanceof Float32Array ? item.vector : new Float32Array(item.vector); const vectorDim = typedVector.length; dimensionCounts[vectorDim] = (dimensionCounts[vectorDim] || 0) + 1; // Metadata được truyền trực tiếp vào VectorData const metadata = item.metadata ? { ...item.metadata, dimension: vectorDim, createdAt: Date.now() } : { dimension: vectorDim, createdAt: Date.now() }; vectorsToAdd.push({ id: item.id, // ID có thể là undefined, DB sẽ tự tạo vector: typedVector, metadata: metadata, }); } if (vectorsToAdd.length > 0) { // Gọi phương thức bulkAdd mới của Database const bulkResult = await database.bulkAdd(vectorsToAdd); console.log('[API] Bulk add result:', bulkResult); // bulkAdd trả về { count, partitionIds } addedCount = bulkResult.count; // bulkAdd không trả về ID cụ thể, chỉ số lượng và partition IDs // Nếu cần ID, phải dùng addVector lặp lại hoặc bulkAdd trả về IDs // Hiện tại, chúng ta chỉ báo cáo số lượng thành công. // addedIds sẽ trống trong trường hợp bulkAdd này. bulkResult.partitionIds.forEach((pId: string) => (partitionInfo[pId] = (partitionInfo[pId] || 0) + 1)); // Ước lượng phân phối console.log(`[API] Bulk add added ${addedCount} vectors to partitions: ${bulkResult.partitionIds.join(', ')}`); } } else { // ----- Single Add Logic ----- const { id, vector, metadata } = req.body; if (!vector || !Array.isArray(vector)) { res.status(400).json({ success: false, error: 'Invalid request: vector array is required', }); return; } const typedVector = vector instanceof Float32Array ? vector : new Float32Array(vector); const vectorDim = typedVector.length; dimensionCounts[vectorDim] = 1; // Metadata được truyền trực tiếp vào addVector const enhancedMetadata = metadata ? { ...metadata, dimension: vectorDim, createdAt: Date.now() } : { dimension: vectorDim, createdAt: Date.now() }; // Gọi phương thức addVector mới của Database (async) // addVector bây giờ trả về { partitionId, vectorId } const addResult = await database.addVector(id, typedVector, enhancedMetadata); console.log('[API] Add result:', addResult); addedIds.push(addResult.vectorId); // Lưu ID thực tế trả về addedCount = 1; partitionInfo[addResult.partitionId] = 1; } // Không cần gọi database.save() ở đây nữa, vì đã có auto-save // database.save().catch(err => { // Bỏ dòng này // console.error("Error saving database after adding vectors:", err); // }); const duration = timer.stop('add_vectors').total; res.status(isBulk ? 200 : 201).json({ success: true, count: addedCount, ids: addedIds, // Sẽ trống nếu dùng bulkAdd không trả về ID dimensions: dimensionCounts, partitionsAffected: partitionInfo, // Thêm thông tin partition duration, }); return; // Thêm return để dừng tiếp tục xử lý } catch (error) { const duration = timer.stop('add_vectors').total; // Dừng timer nếu có lỗi console.error('Error adding vectors:', error); res.status(500).json({ // Sử dụng 500 cho lỗi server/DB success: false, error: (error as Error).message, duration, // Thêm duration vào response lỗi }); return; } }); /** * Get a vector by ID * GET /api/vectors/:id * Tương tác với database.getVector và database.getMetadata (async) */ router.get('/:id', async (req: Request, res: Response) => { // Thêm async const idParam = req.params.id; const includeVector = req.query.includeVector === 'true'; const includeMetadata = req.query.includeMetadata !== 'false'; timer.start('get_vector'); try { // Database readiness check moved to middleware let vectorInfo: { partitionId: string; vector: Float32Array } | null = null; let metadataInfo: { partitionId: string; metadata: Record<string, any>; } | null = null; let foundId: string | number = idParam; // ID thực tế được tìm thấy // Thử tìm vector/metadata với ID gốc if (includeVector) { vectorInfo = await database.getVector(idParam); } if (includeMetadata) { metadataInfo = await database.getMetadata(idParam); } // Nếu không tìm thấy và ID có dạng số, thử chuyển đổi và tìm lại if (!vectorInfo && !metadataInfo && /^\d+$/.test(idParam)) { const numericId = parseInt(idParam, 10); foundId = numericId; // Cập nhật ID tìm thấy if (includeVector) { vectorInfo = await database.getVector(numericId); } if (includeMetadata) { metadataInfo = await database.getMetadata(numericId); } } // Nếu vẫn không tìm thấy sau khi thử cả hai dạng if (!vectorInfo && !metadataInfo) { const duration = timer.stop('get_vector').total; res.status(404).json({ success: false, error: `Vector or metadata with id '${idParam}' not found`, duration, }); return; } // Xây dựng response const response: any = { id: foundId, // Trả về ID thực tế đã tìm thấy partitionId: vectorInfo?.partitionId ?? metadataInfo?.partitionId, // Lấy partitionId từ kết quả nào có success: true, }; let dimension: number | null = null; if (vectorInfo?.vector) { dimension = vectorInfo.vector.length; response.dimension = dimension; if (includeVector) { response.vector = Array.from(vectorInfo.vector); // Chuyển Float32Array thành Array thường } } if (metadataInfo?.metadata) { // Lấy dimension từ metadata nếu chưa có từ vector if (dimension === null && metadataInfo.metadata.dimension !== undefined) { response.dimension = metadataInfo.metadata.dimension; } if (includeMetadata) { response.metadata = metadataInfo.metadata; } } const duration = timer.stop('get_vector').total; response.duration = duration; res.json(response); return; } catch (error) { const duration = timer.stop('get_vector').total; console.error(`Error getting vector ${idParam}:`, error); res.status(500).json({ success: false, error: (error as Error).message, duration, }); return; } }); /** * Check if a vector exists * GET /api/vectors/:id/exists * Tương tác với database.hasVector (async) và getVector/getMetadata để lấy dimension */ router.get('/:id/exists', async (req: Request, res: Response) => { // Thêm async const idParam = req.params.id; timer.start('check_vector_exists'); try { // Database readiness check moved to middleware let exists = await database.hasVector(idParam); let foundId: string | number = idParam; let dimension: number | null = null; // Thử dạng số nếu dạng gốc không tồn tại if (!exists && /^\d+$/.test(idParam)) { const numericId = parseInt(idParam, 10); exists = await database.hasVector(numericId); if (exists) { foundId = numericId; } } // Nếu tồn tại, cố gắng lấy dimension if (exists) { // Ưu tiên lấy từ vector const vectorInfo = await database.getVector(foundId); if (vectorInfo?.vector) { dimension = vectorInfo.vector.length; } else { // Nếu không có vector, thử lấy từ metadata const metadataInfo = await database.getMetadata(foundId); if (metadataInfo?.metadata?.dimension !== undefined) { dimension = metadataInfo.metadata.dimension; } } } const duration = timer.stop('check_vector_exists').total; res.json({ exists, id: idParam, // Trả về ID được yêu cầu ban đầu foundId: exists ? foundId : null, // Trả về ID thực tế tìm thấy (nếu có) dimension, success: true, duration, }); } catch (error) { const duration = timer.stop('check_vector_exists').total; console.error(`Error checking vector existence for ${idParam}:`, error); res.status(500).json({ success: false, error: (error as Error).message, duration, }); } }); /** * Update vector metadata * PATCH /api/vectors/:id/metadata * Tương tác với database.updateMetadata (async) */ router.patch('/:id/metadata', async (req: Request, res: Response) => { // Thêm async const idParam = req.params.id; const { metadata, operation = 'merge' } = req.body as UpdateMetadataRequest; if (!metadata || typeof metadata !== 'object') { res.status(400).json({ success: false, error: 'Invalid request: metadata object is required', }); return; } timer.start('update_metadata'); try { // Database readiness check moved to middleware // Xác định ID thực tế (string hoặc number) let foundId: string | number | null = null; let exists = await database.hasVector(idParam); if (exists) { foundId = idParam; } else if (/^\d+$/.test(idParam)) { const numericId = parseInt(idParam, 10); exists = await database.hasVector(numericId); if (exists) { foundId = numericId; } } if (!foundId) { const duration = timer.stop('update_metadata').total; res.status(404).json({ success: false, error: `Vector with id ${idParam} not found`, duration, }); return; } // Lấy dimension hiện có để bảo toàn (không bắt buộc nếu updateMetadata xử lý) // Tuy nhiên, để an toàn, ta vẫn lấy nó. let dimension: number | null = null; const vectorInfo = await database.getVector(foundId); if (vectorInfo?.vector) { dimension = vectorInfo.vector.length; } else { const metadataInfo = await database.getMetadata(foundId); dimension = metadataInfo?.metadata?.dimension ?? null; } let success = false; if (operation === 'replace') { // Đảm bảo dimension được thêm vào metadata mới nếu có const metadataToReplace = dimension !== null ? { ...metadata, dimension: dimension } : metadata; success = await database.updateMetadata(foundId, metadataToReplace); } else { // merge (default) // updateMetadata với hàm callback để merge success = await database.updateMetadata(foundId, (existingMetadata: Record<string, any>) => { const base = existingMetadata || {}; const merged = { ...base, ...metadata }; // Đảm bảo dimension được giữ lại hoặc thêm vào if (dimension !== null) { merged.dimension = dimension; } return merged; }); } const duration = timer.stop('update_metadata').total; if (success) { res.status(200).json({ success: true, id: foundId, operation, dimension, // Trả về dimension đã bảo toàn/tìm thấy duration, }); return; } else { // Nếu updateMetadata trả về false (ví dụ: không tìm thấy trong quá trình cập nhật) res.status(404).json({ success: false, error: `Vector with id ${foundId} potentially lost during update`, duration, }); return; } } catch (error) { const duration = timer.stop('update_metadata').total; console.error(`Error updating metadata for ${idParam}:`, error); res.status(500).json({ success: false, error: (error as Error).message, duration, }); return; } }); /** * Delete vector * DELETE /api/vectors/:id * Tương tác với database.deleteVector (async) */ router.delete('/:id', async (req: Request, res: Response) => { // Thêm async const idParam = req.params.id; timer.start('delete_vector'); try { // Database readiness check moved to middleware // Thử xóa với ID gốc let deleted = await database.deleteVector(idParam); let deletedId: string | number = idParam; // Nếu không thành công và ID là số, thử xóa với dạng số if (!deleted && /^\d+$/.test(idParam)) { const numericId = parseInt(idParam, 10); deleted = await database.deleteVector(numericId); if (deleted) { deletedId = numericId; } } // Logic thử chuyển đổi number thành string đã bị loại bỏ vì ít gặp // và có thể gây nhầm lẫn nếu ID string trùng với ID number. const duration = timer.stop('delete_vector').total; if (deleted) { res.json({ success: true, id: deletedId, // Trả về ID thực tế đã xóa duration, }); return; } else { res.status(404).json({ success: false, error: `Vector with id ${idParam} not found`, duration, }); return; } } catch (error) { const duration = timer.stop('delete_vector').total; console.error(`Error deleting vector ${idParam}:`, error); res.status(500).json({ success: false, error: (error as Error).message, duration, }); return; } }); /** * Find similar vectors * GET /api/vectors/:id/similar * Tương tác với database.getVector và database.search (async) */ router.get('/:id/similar', async (req: Request, res: Response) => { // Thêm async const idParam = req.params.id; const k = parseInt((req.query.k as string) || '10', 10); const includeMetadata = req.query.includeMetadata !== 'false'; // UnifiedSearch sẽ quyết định includeVectors dựa trên tùy chọn, không cần query param riêng? // Hoặc ta có thể thêm nó vào UnifiedSearchOptions const includeVectors = req.query.includeVectors === 'true'; // UnifiedSearch không có tùy chọn 'exactDimensions', nó sẽ tìm kiếm trên các partition phù hợp // const exactDimensions = req.query.exactDimensions === "true"; // Bỏ tùy chọn này timer.start('find_similar'); try { // Database readiness check moved to middleware // Tìm vector gốc let vectorInfo: { partitionId: string; vector: Float32Array } | null = null; let foundId: string | number = idParam; vectorInfo = await database.getVector(idParam); if (!vectorInfo && /^\d+$/.test(idParam)) { const numericId = parseInt(idParam, 10); vectorInfo = await database.getVector(numericId); if (vectorInfo) { foundId = numericId; } } if (!vectorInfo?.vector) { const duration = timer.stop('find_similar').total; res.status(404).json({ success: false, error: `Source vector with id ${idParam} not found`, duration, }); return; } const queryVector = vectorInfo.vector; const queryDimension = queryVector.length; // Gọi database.search (sử dụng UnifiedSearch) // Tăng k lên 1 để có thể loại bỏ chính vector query const searchOptions = { k: k + 1, includeMetadata, includeVectors, // Các tùy chọn khác của UnifiedSearch có thể thêm vào đây nếu cần // filter: ..., useHNSW: ..., rerank: ... }; // database.search bây giờ là alias của findNearest const results = await database.search(queryVector, searchOptions); // Loại bỏ vector query khỏi kết quả (so sánh cả string và number) const filteredResults = results.filter((result: any) => { // So sánh ID dưới dạng chuỗi để xử lý cả number và string IDs return String(result.id) !== String(foundId); }); // Chỉ lấy k kết quả hàng đầu const topResults = filteredResults.slice(0, k); // Thêm dimension vào kết quả nếu chưa có (UnifiedSearch có thể đã làm điều này) for (const result of topResults) { if (result.vector && result.dimension === undefined) { result.dimension = result.vector.length; } else if (!result.vector && result.metadata?.dimension !== undefined && result.dimension === undefined) { result.dimension = result.metadata.dimension; } } const duration = timer.stop('find_similar').total; res.json({ success: true, queryId: foundId, queryDimension: queryDimension, results: topResults, count: topResults.length, duration, }); return; } catch (error) { const duration = timer.stop('find_similar').total; console.error(`Error finding similar vectors for ${idParam}:`, error); res.status(500).json({ success: false, error: (error as Error).message, duration, }); return; } }); return router; }