Преглед изворни кода

fix: handle Qdrant deletion errors gracefully to prevent indexing interruption (#6296)

Daniel пре 5 месеци
родитељ
комит
5041880da0

+ 8 - 3
src/services/code-index/processors/file-watcher.ts

@@ -204,15 +204,20 @@ export class FileWatcher implements IFileWatcher {
 						currentFile: path,
 					})
 				}
-			} catch (error) {
-				overallBatchError = error as Error
+			} catch (error: any) {
+				const errorStatus = error?.status || error?.response?.status || error?.statusCode
+				const errorMessage = error instanceof Error ? error.message : String(error)
+
 				// Log telemetry for deletion error
 				TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
-					error: sanitizeErrorMessage(overallBatchError.message),
+					error: sanitizeErrorMessage(errorMessage),
 					location: "deletePointsByMultipleFilePaths",
 					errorType: "deletion_error",
+					errorStatus: errorStatus,
 				})
 
+				// Mark all paths as error
+				overallBatchError = error as Error
 				for (const path of pathsToExplicitlyDelete) {
 					batchResults.push({ path, status: "error", error: error as Error })
 					processedCountInBatch++

+ 22 - 9
src/services/code-index/processors/scanner.ts

@@ -281,17 +281,24 @@ export class DirectoryScanner implements IDirectoryScanner {
 					try {
 						await this.qdrantClient.deletePointsByFilePath(cachedFilePath)
 						await this.cacheManager.deleteHash(cachedFilePath)
-					} catch (error) {
+					} catch (error: any) {
+						const errorStatus = error?.status || error?.response?.status || error?.statusCode
+						const errorMessage = error instanceof Error ? error.message : String(error)
+
 						console.error(
 							`[DirectoryScanner] Failed to delete points for ${cachedFilePath} in workspace ${scanWorkspace}:`,
 							error,
 						)
+
 						TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
-							error: sanitizeErrorMessage(error instanceof Error ? error.message : String(error)),
+							error: sanitizeErrorMessage(errorMessage),
 							stack: error instanceof Error ? sanitizeErrorMessage(error.stack || "") : undefined,
 							location: "scanDirectory:deleteRemovedFiles",
+							errorStatus: errorStatus,
 						})
+
 						if (onError) {
+							// Report error to error handler
 							onError(
 								error instanceof Error
 									? new Error(
@@ -304,7 +311,8 @@ export class DirectoryScanner implements IDirectoryScanner {
 										),
 							)
 						}
-						// Decide if we should re-throw or just log
+						// Log error and continue processing instead of re-throwing
+						console.error(`Failed to delete points for removed file: ${cachedFilePath}`, error)
 					}
 				}
 			}
@@ -347,25 +355,30 @@ export class DirectoryScanner implements IDirectoryScanner {
 				if (uniqueFilePaths.length > 0) {
 					try {
 						await this.qdrantClient.deletePointsByMultipleFilePaths(uniqueFilePaths)
-					} catch (deleteError) {
+					} catch (deleteError: any) {
+						const errorStatus =
+							deleteError?.status || deleteError?.response?.status || deleteError?.statusCode
+						const errorMessage = deleteError instanceof Error ? deleteError.message : String(deleteError)
+
 						console.error(
 							`[DirectoryScanner] Failed to delete points for ${uniqueFilePaths.length} files before upsert in workspace ${scanWorkspace}:`,
 							deleteError,
 						)
+
 						TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
-							error: sanitizeErrorMessage(
-								deleteError instanceof Error ? deleteError.message : String(deleteError),
-							),
+							error: sanitizeErrorMessage(errorMessage),
 							stack:
 								deleteError instanceof Error
 									? sanitizeErrorMessage(deleteError.stack || "")
 									: undefined,
 							location: "processBatch:deletePointsByMultipleFilePaths",
 							fileCount: uniqueFilePaths.length,
+							errorStatus: errorStatus,
 						})
-						// Re-throw the error with workspace context
+
+						// Re-throw with workspace context
 						throw new Error(
-							`Failed to delete points for ${uniqueFilePaths.length} files. Workspace: ${scanWorkspace}. ${deleteError instanceof Error ? deleteError.message : String(deleteError)}`,
+							`Failed to delete points for ${uniqueFilePaths.length} files. Workspace: ${scanWorkspace}. ${errorMessage}`,
 							{ cause: deleteError },
 						)
 					}

+ 48 - 13
src/services/code-index/vector-store/qdrant-client.ts

@@ -423,27 +423,62 @@ export class QdrantVectorStore implements IVectorStore {
 		}
 
 		try {
+			// First check if the collection exists
+			const collectionExists = await this.collectionExists()
+			if (!collectionExists) {
+				console.warn(
+					`[QdrantVectorStore] Skipping deletion - collection "${this.collectionName}" does not exist`,
+				)
+				return
+			}
+
 			const workspaceRoot = getWorkspacePath()
-			const normalizedPaths = filePaths.map((filePath) => {
-				const absolutePath = path.resolve(workspaceRoot, filePath)
-				return path.normalize(absolutePath)
+
+			// Build filters using pathSegments to match the indexed fields
+			const filters = filePaths.map((filePath) => {
+				// IMPORTANT: Use the relative path to match what's stored in upsertPoints
+				// upsertPoints stores the relative filePath, not the absolute path
+				const relativePath = path.isAbsolute(filePath) ? path.relative(workspaceRoot, filePath) : filePath
+
+				// Normalize the relative path
+				const normalizedRelativePath = path.normalize(relativePath)
+
+				// Split the path into segments like we do in upsertPoints
+				const segments = normalizedRelativePath.split(path.sep).filter(Boolean)
+
+				// Create a filter that matches all segments of the path
+				// This ensures we only delete points that match the exact file path
+				const mustConditions = segments.map((segment, index) => ({
+					key: `pathSegments.${index}`,
+					match: { value: segment },
+				}))
+
+				return { must: mustConditions }
 			})
 
-			const filter = {
-				should: normalizedPaths.map((normalizedPath) => ({
-					key: "filePath",
-					match: {
-						value: normalizedPath,
-					},
-				})),
-			}
+			// Use 'should' to match any of the file paths (OR condition)
+			const filter = filters.length === 1 ? filters[0] : { should: filters }
 
 			await this.client.delete(this.collectionName, {
 				filter,
 				wait: true,
 			})
-		} catch (error) {
-			console.error("Failed to delete points by file paths:", error)
+		} catch (error: any) {
+			// Extract more detailed error information
+			const errorMessage = error?.message || String(error)
+			const errorStatus = error?.status || error?.response?.status || error?.statusCode
+			const errorDetails = error?.response?.data || error?.data || ""
+
+			console.error(`[QdrantVectorStore] Failed to delete points by file paths:`, {
+				error: errorMessage,
+				status: errorStatus,
+				details: errorDetails,
+				collection: this.collectionName,
+				fileCount: filePaths.length,
+				// Include first few file paths for debugging (avoid logging too many)
+				samplePaths: filePaths.slice(0, 3),
+			})
+
 			throw error
 		}
 	}