Skip to content

Commit d6a72da

Browse files
committed
fix: prevent unnecessary reindexing after system restart
- Add retry logic for Qdrant connection on initialization - Distinguish between connection failures and missing collections - Only clear cache when a new collection is actually created - Add comprehensive logging to help diagnose connection issues This fixes the issue where the vector database index was being recreated after every system restart, even when the collection already existed in the Docker container. Fixes #7408
1 parent 8684877 commit d6a72da

File tree

3 files changed

+172
-21
lines changed

3 files changed

+172
-21
lines changed

src/services/code-index/orchestrator.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,14 @@ export class CodeIndexOrchestrator {
127127
const collectionCreated = await this.vectorStore.initialize()
128128

129129
if (collectionCreated) {
130+
console.log(
131+
"[CodeIndexOrchestrator] New collection was created, clearing cache file to force full reindex",
132+
)
130133
await this.cacheManager.clearCacheFile()
134+
} else {
135+
console.log(
136+
"[CodeIndexOrchestrator] Using existing collection, preserving cache for incremental updates",
137+
)
131138
}
132139

133140
this.stateManager.setSystemState("Indexing", "Services ready. Starting workspace scan...")

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

Lines changed: 62 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ vitest.mock("path", async () => {
3232

3333
const mockQdrantClientInstance = {
3434
getCollection: vitest.fn(),
35+
getCollections: vitest.fn(), // Add this for testConnection method
3536
createCollection: vitest.fn(),
3637
deleteCollection: vitest.fn(),
3738
createPayloadIndex: vitest.fn(),
@@ -514,6 +515,8 @@ describe("QdrantVectorStore", () => {
514515

515516
describe("initialize", () => {
516517
it("should create a new collection if none exists and return true", async () => {
518+
// Mock getCollections for testConnection
519+
mockQdrantClientInstance.getCollections.mockResolvedValue({ collections: [] })
517520
// Mock getCollection to throw a 404-like error
518521
mockQdrantClientInstance.getCollection.mockRejectedValue({
519522
response: { status: 404 },
@@ -546,6 +549,8 @@ describe("QdrantVectorStore", () => {
546549
expect(mockQdrantClientInstance.createPayloadIndex).toHaveBeenCalledTimes(5)
547550
})
548551
it("should not create a new collection if one exists with matching vectorSize and return false", async () => {
552+
// Mock getCollections for testConnection
553+
mockQdrantClientInstance.getCollections.mockResolvedValue({ collections: [] })
549554
// Mock getCollection to return existing collection info with matching vector size
550555
mockQdrantClientInstance.getCollection.mockResolvedValue({
551556
config: {
@@ -577,6 +582,8 @@ describe("QdrantVectorStore", () => {
577582
})
578583
it("should recreate collection if it exists but vectorSize mismatches and return true", async () => {
579584
const differentVectorSize = 768
585+
// Mock getCollections for testConnection
586+
mockQdrantClientInstance.getCollections.mockResolvedValue({ collections: [] })
580587
// Mock getCollection to return existing collection info with different vector size first,
581588
// then return 404 to confirm deletion
582589
mockQdrantClientInstance.getCollection
@@ -597,6 +604,7 @@ describe("QdrantVectorStore", () => {
597604
mockQdrantClientInstance.createCollection.mockResolvedValue(true as any)
598605
mockQdrantClientInstance.createPayloadIndex.mockResolvedValue({} as any)
599606
vitest.spyOn(console, "warn").mockImplementation(() => {}) // Suppress console.warn
607+
vitest.spyOn(console, "log").mockImplementation(() => {}) // Suppress console.log
600608

601609
const result = await vectorStore.initialize()
602610

@@ -622,11 +630,15 @@ describe("QdrantVectorStore", () => {
622630
}
623631
expect(mockQdrantClientInstance.createPayloadIndex).toHaveBeenCalledTimes(5)
624632
;(console.warn as any).mockRestore() // Restore console.warn
633+
;(console.log as any).mockRestore() // Restore console.log
625634
})
626635
it("should log warning for non-404 errors but still create collection", async () => {
636+
// Mock getCollections for testConnection
637+
mockQdrantClientInstance.getCollections.mockResolvedValue({ collections: [] })
627638
const genericError = new Error("Generic Qdrant Error")
628639
mockQdrantClientInstance.getCollection.mockRejectedValue(genericError)
629640
vitest.spyOn(console, "warn").mockImplementation(() => {}) // Suppress console.warn
641+
vitest.spyOn(console, "log").mockImplementation(() => {}) // Suppress console.log
630642

631643
const result = await vectorStore.initialize()
632644

@@ -635,11 +647,12 @@ describe("QdrantVectorStore", () => {
635647
expect(mockQdrantClientInstance.createCollection).toHaveBeenCalledTimes(1)
636648
expect(mockQdrantClientInstance.deleteCollection).not.toHaveBeenCalled()
637649
expect(mockQdrantClientInstance.createPayloadIndex).toHaveBeenCalledTimes(5)
638-
expect(console.warn).toHaveBeenCalledWith(
639-
expect.stringContaining(`Warning during getCollectionInfo for "${expectedCollectionName}"`),
640-
genericError.message,
650+
// Check that the collection creation was logged
651+
expect(console.log).toHaveBeenCalledWith(
652+
expect.stringContaining(`Creating new collection "${expectedCollectionName}"`),
641653
)
642654
;(console.warn as any).mockRestore()
655+
;(console.log as any).mockRestore()
643656
})
644657
it("should re-throw error from createCollection when no collection initially exists", async () => {
645658
mockQdrantClientInstance.getCollection.mockRejectedValue({
@@ -663,6 +676,8 @@ describe("QdrantVectorStore", () => {
663676
;(console.error as any).mockRestore()
664677
})
665678
it("should log but not fail if payload index creation errors occur", async () => {
679+
// Mock getCollections for testConnection
680+
mockQdrantClientInstance.getCollections.mockResolvedValue({ collections: [] })
666681
// Mock successful collection creation
667682
mockQdrantClientInstance.getCollection.mockRejectedValue({
668683
response: { status: 404 },
@@ -674,6 +689,7 @@ describe("QdrantVectorStore", () => {
674689
const indexError = new Error("Index creation failed")
675690
mockQdrantClientInstance.createPayloadIndex.mockRejectedValue(indexError)
676691
vitest.spyOn(console, "warn").mockImplementation(() => {}) // Suppress console.warn
692+
vitest.spyOn(console, "log").mockImplementation(() => {}) // Suppress console.log
677693

678694
const result = await vectorStore.initialize()
679695

@@ -685,18 +701,19 @@ describe("QdrantVectorStore", () => {
685701
expect(mockQdrantClientInstance.createPayloadIndex).toHaveBeenCalledTimes(5)
686702

687703
// Verify warnings were logged for each failed index
688-
expect(console.warn).toHaveBeenCalledTimes(5)
689-
for (let i = 0; i <= 4; i++) {
690-
expect(console.warn).toHaveBeenCalledWith(
691-
expect.stringContaining(`Could not create payload index for pathSegments.${i}`),
692-
indexError.message,
693-
)
694-
}
695-
704+
// Note: There might be additional warnings from the new logging
705+
expect(console.warn).toHaveBeenCalled()
706+
// Check that warnings were logged for failed indexes
707+
const warnCalls = (console.warn as any).mock.calls
708+
const indexWarnings = warnCalls.filter((call: any[]) => call[0]?.includes("Could not create payload index"))
709+
expect(indexWarnings).toHaveLength(5)
696710
;(console.warn as any).mockRestore()
711+
;(console.log as any).mockRestore()
697712
})
698713

699714
it("should throw vectorDimensionMismatch error when deleteCollection fails during recreation", async () => {
715+
// Mock getCollections for testConnection
716+
mockQdrantClientInstance.getCollections.mockResolvedValue({ collections: [] })
700717
const differentVectorSize = 768
701718
mockQdrantClientInstance.getCollection.mockResolvedValue({
702719
config: {
@@ -712,6 +729,7 @@ describe("QdrantVectorStore", () => {
712729
mockQdrantClientInstance.deleteCollection.mockRejectedValue(deleteError)
713730
vitest.spyOn(console, "error").mockImplementation(() => {})
714731
vitest.spyOn(console, "warn").mockImplementation(() => {})
732+
vitest.spyOn(console, "log").mockImplementation(() => {})
715733

716734
// The error should have a cause property set to the original error
717735
let caughtError: any
@@ -734,9 +752,12 @@ describe("QdrantVectorStore", () => {
734752
expect(console.error).toHaveBeenCalledTimes(2) // One for the critical error, one for the outer catch
735753
;(console.error as any).mockRestore()
736754
;(console.warn as any).mockRestore()
755+
;(console.log as any).mockRestore()
737756
})
738757

739758
it("should throw vectorDimensionMismatch error when createCollection fails during recreation", async () => {
759+
// Mock getCollections for testConnection
760+
mockQdrantClientInstance.getCollections.mockResolvedValue({ collections: [] })
740761
const differentVectorSize = 768
741762
mockQdrantClientInstance.getCollection
742763
.mockResolvedValueOnce({
@@ -760,6 +781,7 @@ describe("QdrantVectorStore", () => {
760781
mockQdrantClientInstance.createCollection.mockRejectedValue(createError)
761782
vitest.spyOn(console, "error").mockImplementation(() => {})
762783
vitest.spyOn(console, "warn").mockImplementation(() => {})
784+
vitest.spyOn(console, "log").mockImplementation(() => {})
763785

764786
// Should throw an error with cause property set to the original error
765787
let caughtError: any
@@ -777,14 +799,18 @@ describe("QdrantVectorStore", () => {
777799
expect(mockQdrantClientInstance.deleteCollection).toHaveBeenCalledTimes(1)
778800
expect(mockQdrantClientInstance.createCollection).toHaveBeenCalledTimes(1)
779801
expect(mockQdrantClientInstance.createPayloadIndex).not.toHaveBeenCalled()
780-
// Should log warning, critical error, and outer error
781-
expect(console.warn).toHaveBeenCalledTimes(1)
802+
// Should log warnings and errors
803+
expect(console.warn).toHaveBeenCalled()
804+
expect(console.log).toHaveBeenCalled()
782805
expect(console.error).toHaveBeenCalledTimes(2)
783806
;(console.error as any).mockRestore()
784807
;(console.warn as any).mockRestore()
808+
;(console.log as any).mockRestore()
785809
})
786810

787811
it("should verify collection deletion before proceeding with recreation", async () => {
812+
// Mock getCollections for testConnection
813+
mockQdrantClientInstance.getCollections.mockResolvedValue({ collections: [] })
788814
const differentVectorSize = 768
789815
mockQdrantClientInstance.getCollection
790816
.mockResolvedValueOnce({
@@ -806,6 +832,7 @@ describe("QdrantVectorStore", () => {
806832
mockQdrantClientInstance.createCollection.mockResolvedValue(true as any)
807833
mockQdrantClientInstance.createPayloadIndex.mockResolvedValue({} as any)
808834
vitest.spyOn(console, "warn").mockImplementation(() => {})
835+
vitest.spyOn(console, "log").mockImplementation(() => {})
809836

810837
const result = await vectorStore.initialize()
811838

@@ -816,9 +843,12 @@ describe("QdrantVectorStore", () => {
816843
expect(mockQdrantClientInstance.createCollection).toHaveBeenCalledTimes(1)
817844
expect(mockQdrantClientInstance.createPayloadIndex).toHaveBeenCalledTimes(5)
818845
;(console.warn as any).mockRestore()
846+
;(console.log as any).mockRestore()
819847
})
820848

821849
it("should throw error if collection still exists after deletion attempt", async () => {
850+
// Mock getCollections for testConnection
851+
mockQdrantClientInstance.getCollections.mockResolvedValue({ collections: [] })
822852
const differentVectorSize = 768
823853
mockQdrantClientInstance.getCollection
824854
.mockResolvedValueOnce({
@@ -844,6 +874,7 @@ describe("QdrantVectorStore", () => {
844874
mockQdrantClientInstance.deleteCollection.mockResolvedValue(true as any)
845875
vitest.spyOn(console, "error").mockImplementation(() => {})
846876
vitest.spyOn(console, "warn").mockImplementation(() => {})
877+
vitest.spyOn(console, "log").mockImplementation(() => {})
847878

848879
let caughtError: any
849880
try {
@@ -863,9 +894,12 @@ describe("QdrantVectorStore", () => {
863894
expect(mockQdrantClientInstance.createPayloadIndex).not.toHaveBeenCalled()
864895
;(console.error as any).mockRestore()
865896
;(console.warn as any).mockRestore()
897+
;(console.log as any).mockRestore()
866898
})
867899

868900
it("should handle dimension mismatch scenario from 2048 to 768 dimensions", async () => {
901+
// Mock getCollections for testConnection
902+
mockQdrantClientInstance.getCollections.mockResolvedValue({ collections: [] })
869903
// Simulate the exact scenario from the issue: switching from 2048 to 768 dimensions
870904
const oldVectorSize = 2048
871905
const newVectorSize = 768
@@ -893,6 +927,7 @@ describe("QdrantVectorStore", () => {
893927
mockQdrantClientInstance.createCollection.mockResolvedValue(true as any)
894928
mockQdrantClientInstance.createPayloadIndex.mockResolvedValue({} as any)
895929
vitest.spyOn(console, "warn").mockImplementation(() => {})
930+
vitest.spyOn(console, "log").mockImplementation(() => {})
896931

897932
const result = await newVectorStore.initialize()
898933

@@ -907,9 +942,12 @@ describe("QdrantVectorStore", () => {
907942
})
908943
expect(mockQdrantClientInstance.createPayloadIndex).toHaveBeenCalledTimes(5)
909944
;(console.warn as any).mockRestore()
945+
;(console.log as any).mockRestore()
910946
})
911947

912948
it("should provide detailed error context for different failure scenarios", async () => {
949+
// Mock getCollections for testConnection
950+
mockQdrantClientInstance.getCollections.mockResolvedValue({ collections: [] })
913951
const differentVectorSize = 768
914952
mockQdrantClientInstance.getCollection.mockResolvedValue({
915953
config: {
@@ -926,6 +964,7 @@ describe("QdrantVectorStore", () => {
926964
mockQdrantClientInstance.deleteCollection.mockRejectedValue(deleteError)
927965
vitest.spyOn(console, "error").mockImplementation(() => {})
928966
vitest.spyOn(console, "warn").mockImplementation(() => {})
967+
vitest.spyOn(console, "log").mockImplementation(() => {})
929968

930969
let caughtError: any
931970
try {
@@ -942,6 +981,7 @@ describe("QdrantVectorStore", () => {
942981
expect(caughtError.cause).toBe(deleteError)
943982
;(console.error as any).mockRestore()
944983
;(console.warn as any).mockRestore()
984+
;(console.log as any).mockRestore()
945985
})
946986
})
947987

@@ -976,16 +1016,22 @@ describe("QdrantVectorStore", () => {
9761016
const genericError = new Error("Network error")
9771017
mockQdrantClientInstance.getCollection.mockRejectedValue(genericError)
9781018
vitest.spyOn(console, "warn").mockImplementation(() => {})
1019+
vitest.spyOn(console, "error").mockImplementation(() => {})
9791020

9801021
const result = await vectorStore.collectionExists()
9811022

9821023
expect(result).toBe(false)
983-
expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(1)
984-
expect(console.warn).toHaveBeenCalledWith(
985-
expect.stringContaining(`Warning during getCollectionInfo for "${expectedCollectionName}"`),
1024+
// 4 calls: 1 initial + 3 retries
1025+
expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(4)
1026+
// The warning is now an error after retries fail
1027+
expect(console.error).toHaveBeenCalledWith(
1028+
expect.stringContaining(
1029+
`Failed to connect to Qdrant after 3 retries for collection "${expectedCollectionName}"`,
1030+
),
9861031
genericError.message,
9871032
)
9881033
;(console.warn as any).mockRestore()
1034+
;(console.error as any).mockRestore()
9891035
})
9901036
describe("deleteCollection", () => {
9911037
it("should delete collection when it exists", async () => {

0 commit comments

Comments
 (0)