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

This commit is contained in:
Daniel 2025-07-31 15:20:11 -05:00 committed by GitHub
parent 1da82b2db0
commit 5041880da0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 78 additions and 25 deletions

View file

@ -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++

View file

@ -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 },
)
}

View file

@ -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
}
}