Skip to content

Commit 3ac6d99

Browse files
committed
fix: handle Qdrant deletion errors gracefully to prevent indexing interruption
- Fixed deletePointsByMultipleFilePaths to use pathSegments instead of filePath field (filePath field lacks required index causing HTTP 400 errors) - Added graceful error handling for HTTP 400 errors in deletion operations - Enhanced error logging with status codes and detailed error information - Updated scanner.ts and file-watcher.ts to handle deletion failures without stopping - Ensures indexing process continues even when points don't exist in vector store This resolves the issue where bad requests during point deletion would halt the entire indexing process.
1 parent 342ee70 commit 3ac6d99

File tree

3 files changed

+144
-37
lines changed

3 files changed

+144
-37
lines changed

src/services/code-index/processors/file-watcher.ts

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -204,23 +204,47 @@ export class FileWatcher implements IFileWatcher {
204204
currentFile: path,
205205
})
206206
}
207-
} catch (error) {
208-
overallBatchError = error as Error
207+
} catch (error: any) {
208+
const errorStatus = error?.status || error?.response?.status || error?.statusCode
209+
const errorMessage = error instanceof Error ? error.message : String(error)
210+
209211
// Log telemetry for deletion error
210212
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
211-
error: sanitizeErrorMessage(overallBatchError.message),
213+
error: sanitizeErrorMessage(errorMessage),
212214
location: "deletePointsByMultipleFilePaths",
213215
errorType: "deletion_error",
216+
errorStatus: errorStatus,
214217
})
215218

216-
for (const path of pathsToExplicitlyDelete) {
217-
batchResults.push({ path, status: "error", error: error as Error })
218-
processedCountInBatch++
219-
this._onBatchProgressUpdate.fire({
220-
processedInBatch: processedCountInBatch,
221-
totalInBatch: totalFilesInBatch,
222-
currentFile: path,
223-
})
219+
// Check if this is a bad request error that we should handle gracefully
220+
if (errorStatus === 400 || errorMessage.toLowerCase().includes("bad request")) {
221+
console.warn(
222+
`[FileWatcher] Received bad request error during deletion for ${allPathsToClearFromDB.size} files. Treating as successful deletion...`,
223+
)
224+
// Treat as successful deletion - remove from cache and mark as success
225+
for (const path of pathsToExplicitlyDelete) {
226+
this.cacheManager.deleteHash(path)
227+
batchResults.push({ path, status: "success" })
228+
processedCountInBatch++
229+
this._onBatchProgressUpdate.fire({
230+
processedInBatch: processedCountInBatch,
231+
totalInBatch: totalFilesInBatch,
232+
currentFile: path,
233+
})
234+
}
235+
} else {
236+
// For other errors, mark as error but don't set overallBatchError
237+
// This allows the rest of the batch to continue processing
238+
overallBatchError = error as Error
239+
for (const path of pathsToExplicitlyDelete) {
240+
batchResults.push({ path, status: "error", error: error as Error })
241+
processedCountInBatch++
242+
this._onBatchProgressUpdate.fire({
243+
processedInBatch: processedCountInBatch,
244+
totalInBatch: totalFilesInBatch,
245+
currentFile: path,
246+
})
247+
}
224248
}
225249
}
226250
}

src/services/code-index/processors/scanner.ts

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -281,17 +281,31 @@ export class DirectoryScanner implements IDirectoryScanner {
281281
try {
282282
await this.qdrantClient.deletePointsByFilePath(cachedFilePath)
283283
await this.cacheManager.deleteHash(cachedFilePath)
284-
} catch (error) {
284+
} catch (error: any) {
285+
const errorStatus = error?.status || error?.response?.status || error?.statusCode
286+
const errorMessage = error instanceof Error ? error.message : String(error)
287+
285288
console.error(
286289
`[DirectoryScanner] Failed to delete points for ${cachedFilePath} in workspace ${scanWorkspace}:`,
287290
error,
288291
)
292+
289293
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
290-
error: sanitizeErrorMessage(error instanceof Error ? error.message : String(error)),
294+
error: sanitizeErrorMessage(errorMessage),
291295
stack: error instanceof Error ? sanitizeErrorMessage(error.stack || "") : undefined,
292296
location: "scanDirectory:deleteRemovedFiles",
297+
errorStatus: errorStatus,
293298
})
294-
if (onError) {
299+
300+
// Check if this is a bad request error that we should handle gracefully
301+
if (errorStatus === 400 || errorMessage.toLowerCase().includes("bad request")) {
302+
console.warn(
303+
`[DirectoryScanner] Received bad request error when deleting ${cachedFilePath}. File may not exist in vector store. Removing from cache anyway...`,
304+
)
305+
// Still remove from cache even if vector store deletion failed
306+
await this.cacheManager.deleteHash(cachedFilePath)
307+
} else if (onError) {
308+
// For other errors, report to error handler but don't throw
295309
onError(
296310
error instanceof Error
297311
? new Error(
@@ -304,7 +318,7 @@ export class DirectoryScanner implements IDirectoryScanner {
304318
),
305319
)
306320
}
307-
// Decide if we should re-throw or just log
321+
// Don't re-throw - allow scanning to continue
308322
}
309323
}
310324
}
@@ -347,27 +361,40 @@ export class DirectoryScanner implements IDirectoryScanner {
347361
if (uniqueFilePaths.length > 0) {
348362
try {
349363
await this.qdrantClient.deletePointsByMultipleFilePaths(uniqueFilePaths)
350-
} catch (deleteError) {
364+
} catch (deleteError: any) {
365+
const errorStatus =
366+
deleteError?.status || deleteError?.response?.status || deleteError?.statusCode
367+
const errorMessage = deleteError instanceof Error ? deleteError.message : String(deleteError)
368+
351369
console.error(
352370
`[DirectoryScanner] Failed to delete points for ${uniqueFilePaths.length} files before upsert in workspace ${scanWorkspace}:`,
353371
deleteError,
354372
)
373+
355374
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
356-
error: sanitizeErrorMessage(
357-
deleteError instanceof Error ? deleteError.message : String(deleteError),
358-
),
375+
error: sanitizeErrorMessage(errorMessage),
359376
stack:
360377
deleteError instanceof Error
361378
? sanitizeErrorMessage(deleteError.stack || "")
362379
: undefined,
363380
location: "processBatch:deletePointsByMultipleFilePaths",
364381
fileCount: uniqueFilePaths.length,
382+
errorStatus: errorStatus,
365383
})
366-
// Re-throw the error with workspace context
367-
throw new Error(
368-
`Failed to delete points for ${uniqueFilePaths.length} files. Workspace: ${scanWorkspace}. ${deleteError instanceof Error ? deleteError.message : String(deleteError)}`,
369-
{ cause: deleteError },
370-
)
384+
385+
// Check if this is a bad request error that we should handle gracefully
386+
if (errorStatus === 400 || errorMessage.toLowerCase().includes("bad request")) {
387+
console.warn(
388+
`[DirectoryScanner] Received bad request error during deletion. Continuing with upsert operation...`,
389+
)
390+
// Don't throw - continue with the upsert operation
391+
} else {
392+
// For other errors, re-throw with workspace context
393+
throw new Error(
394+
`Failed to delete points for ${uniqueFilePaths.length} files. Workspace: ${scanWorkspace}. ${errorMessage}`,
395+
{ cause: deleteError },
396+
)
397+
}
371398
}
372399
}
373400
// --- End Deletion Step ---

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

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,15 @@ export class QdrantVectorStore implements IVectorStore {
414414
* @param filePath Path of the file to delete points for
415415
*/
416416
async deletePointsByFilePath(filePath: string): Promise<void> {
417-
return this.deletePointsByMultipleFilePaths([filePath])
417+
try {
418+
return await this.deletePointsByMultipleFilePaths([filePath])
419+
} catch (error) {
420+
// Error is already handled in deletePointsByMultipleFilePaths
421+
// This is just for consistency in case the method is called directly
422+
console.error(`[QdrantVectorStore] Error in deletePointsByFilePath for ${filePath}:`, error)
423+
// Re-throw to maintain the interface contract
424+
throw error
425+
}
418426
}
419427

420428
async deletePointsByMultipleFilePaths(filePaths: string[]): Promise<void> {
@@ -423,27 +431,75 @@ export class QdrantVectorStore implements IVectorStore {
423431
}
424432

425433
try {
434+
// First check if the collection exists
435+
const collectionExists = await this.collectionExists()
436+
if (!collectionExists) {
437+
console.warn(
438+
`[QdrantVectorStore] Skipping deletion - collection "${this.collectionName}" does not exist`,
439+
)
440+
return
441+
}
442+
426443
const workspaceRoot = getWorkspacePath()
427-
const normalizedPaths = filePaths.map((filePath) => {
444+
445+
// Build filters using pathSegments to match the indexed fields
446+
const filters = filePaths.map((filePath) => {
428447
const absolutePath = path.resolve(workspaceRoot, filePath)
429-
return path.normalize(absolutePath)
448+
const normalizedPath = path.normalize(absolutePath)
449+
450+
// Split the path into segments like we do in upsertPoints
451+
const segments = normalizedPath.split(path.sep).filter(Boolean)
452+
453+
// Create a filter that matches all segments of the path
454+
// This ensures we only delete points that match the exact file path
455+
const mustConditions = segments.map((segment, index) => ({
456+
key: `pathSegments.${index}`,
457+
match: { value: segment },
458+
}))
459+
460+
return { must: mustConditions }
430461
})
431462

432-
const filter = {
433-
should: normalizedPaths.map((normalizedPath) => ({
434-
key: "filePath",
435-
match: {
436-
value: normalizedPath,
437-
},
438-
})),
439-
}
463+
// Log the paths being deleted for debugging
464+
console.log(
465+
`[QdrantVectorStore] Attempting to delete points for ${filePaths.length} file(s) from collection "${this.collectionName}"`,
466+
)
467+
468+
// Use 'should' to match any of the file paths (OR condition)
469+
const filter = filters.length === 1 ? filters[0] : { should: filters }
440470

441471
await this.client.delete(this.collectionName, {
442472
filter,
443473
wait: true,
444474
})
445-
} catch (error) {
446-
console.error("Failed to delete points by file paths:", error)
475+
476+
console.log(`[QdrantVectorStore] Successfully deleted points for ${filePaths.length} file(s)`)
477+
} catch (error: any) {
478+
// Extract more detailed error information
479+
const errorMessage = error?.message || String(error)
480+
const errorStatus = error?.status || error?.response?.status || error?.statusCode
481+
const errorDetails = error?.response?.data || error?.data || ""
482+
483+
console.error(`[QdrantVectorStore] Failed to delete points by file paths:`, {
484+
error: errorMessage,
485+
status: errorStatus,
486+
details: errorDetails,
487+
collection: this.collectionName,
488+
fileCount: filePaths.length,
489+
// Include first few file paths for debugging (avoid logging too many)
490+
samplePaths: filePaths.slice(0, 3),
491+
})
492+
493+
// Check if this is a "bad request" error that we can handle gracefully
494+
if (errorStatus === 400 || errorMessage.toLowerCase().includes("bad request")) {
495+
console.warn(
496+
`[QdrantVectorStore] Received bad request error during deletion. This might indicate the points don't exist or the filter is invalid. Continuing without throwing...`,
497+
)
498+
// Don't throw the error - allow the indexing process to continue
499+
return
500+
}
501+
502+
// For other errors, still throw them
447503
throw error
448504
}
449505
}

0 commit comments

Comments
 (0)