Skip to content

Commit 5da7606

Browse files
committed
fix: resolve vector dimension mismatch error when switching embedding models (#5616)
- Enhanced QdrantVectorStore.initialize() with robust dimension mismatch handling - Added atomic collection recreation with step-by-step verification - Improved error reporting with detailed context for better user experience - Added comprehensive test coverage for all dimension mismatch scenarios - Fixes issue where switching from 2048-dim to 768-dim models would fail
1 parent 50e45a2 commit 5da7606

File tree

2 files changed

+314
-65
lines changed

2 files changed

+314
-65
lines changed

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

Lines changed: 201 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,16 @@ vitest.mock("@qdrant/js-client-rest")
1010
vitest.mock("crypto")
1111
vitest.mock("../../../../utils/path")
1212
vitest.mock("../../../../i18n", () => ({
13-
t: (key: string) => key, // Just return the key for testing
13+
t: (key: string, params?: any) => {
14+
// Mock translation function that includes parameters for testing
15+
if (key === "embeddings:vectorStore.vectorDimensionMismatch" && params?.errorMessage) {
16+
return `Failed to update vector index for new model. Please try clearing the index and starting again. Details: ${params.errorMessage}`
17+
}
18+
if (key === "embeddings:vectorStore.qdrantConnectionFailed" && params?.qdrantUrl && params?.errorMessage) {
19+
return `Failed to connect to Qdrant vector database. Please ensure Qdrant is running and accessible at ${params.qdrantUrl}. Error: ${params.errorMessage}`
20+
}
21+
return key // Just return the key for other cases
22+
},
1423
}))
1524
vitest.mock("path", () => ({
1625
...vitest.importActual("path"),
@@ -564,16 +573,22 @@ describe("QdrantVectorStore", () => {
564573
})
565574
it("should recreate collection if it exists but vectorSize mismatches and return true", async () => {
566575
const differentVectorSize = 768
567-
// Mock getCollection to return existing collection info with different vector size
568-
mockQdrantClientInstance.getCollection.mockResolvedValue({
569-
config: {
570-
params: {
571-
vectors: {
572-
size: differentVectorSize, // Mismatching vector size
576+
// Mock getCollection to return existing collection info with different vector size first,
577+
// then return 404 to confirm deletion
578+
mockQdrantClientInstance.getCollection
579+
.mockResolvedValueOnce({
580+
config: {
581+
params: {
582+
vectors: {
583+
size: differentVectorSize, // Mismatching vector size
584+
},
573585
},
574586
},
575-
},
576-
} as any)
587+
} as any)
588+
.mockRejectedValueOnce({
589+
response: { status: 404 },
590+
message: "Not found",
591+
})
577592
mockQdrantClientInstance.deleteCollection.mockResolvedValue(true as any)
578593
mockQdrantClientInstance.createCollection.mockResolvedValue(true as any)
579594
mockQdrantClientInstance.createPayloadIndex.mockResolvedValue({} as any)
@@ -582,7 +597,7 @@ describe("QdrantVectorStore", () => {
582597
const result = await vectorStore.initialize()
583598

584599
expect(result).toBe(true)
585-
expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(1)
600+
expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(2) // Once to check, once to verify deletion
586601
expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledWith(expectedCollectionName)
587602
expect(mockQdrantClientInstance.deleteCollection).toHaveBeenCalledTimes(1)
588603
expect(mockQdrantClientInstance.deleteCollection).toHaveBeenCalledWith(expectedCollectionName)
@@ -703,7 +718,7 @@ describe("QdrantVectorStore", () => {
703718
}
704719

705720
expect(caughtError).toBeDefined()
706-
expect(caughtError.message).toContain("embeddings:vectorStore.vectorDimensionMismatch")
721+
expect(caughtError.message).toContain("Failed to update vector index for new model")
707722
expect(caughtError.cause).toBe(deleteError)
708723

709724
expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(1)
@@ -719,15 +734,21 @@ describe("QdrantVectorStore", () => {
719734

720735
it("should throw vectorDimensionMismatch error when createCollection fails during recreation", async () => {
721736
const differentVectorSize = 768
722-
mockQdrantClientInstance.getCollection.mockResolvedValue({
723-
config: {
724-
params: {
725-
vectors: {
726-
size: differentVectorSize,
737+
mockQdrantClientInstance.getCollection
738+
.mockResolvedValueOnce({
739+
config: {
740+
params: {
741+
vectors: {
742+
size: differentVectorSize,
743+
},
727744
},
728745
},
729-
},
730-
} as any)
746+
} as any)
747+
// Second call should return 404 to confirm deletion
748+
.mockRejectedValueOnce({
749+
response: { status: 404 },
750+
message: "Not found",
751+
})
731752

732753
// Delete succeeds but create fails
733754
mockQdrantClientInstance.deleteCollection.mockResolvedValue(true as any)
@@ -745,10 +766,10 @@ describe("QdrantVectorStore", () => {
745766
}
746767

747768
expect(caughtError).toBeDefined()
748-
expect(caughtError.message).toContain("embeddings:vectorStore.vectorDimensionMismatch")
769+
expect(caughtError.message).toContain("Failed to update vector index for new model")
749770
expect(caughtError.cause).toBe(createError)
750771

751-
expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(1)
772+
expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(2)
752773
expect(mockQdrantClientInstance.deleteCollection).toHaveBeenCalledTimes(1)
753774
expect(mockQdrantClientInstance.createCollection).toHaveBeenCalledTimes(1)
754775
expect(mockQdrantClientInstance.createPayloadIndex).not.toHaveBeenCalled()
@@ -758,6 +779,166 @@ describe("QdrantVectorStore", () => {
758779
;(console.error as any).mockRestore()
759780
;(console.warn as any).mockRestore()
760781
})
782+
783+
it("should verify collection deletion before proceeding with recreation", async () => {
784+
const differentVectorSize = 768
785+
mockQdrantClientInstance.getCollection
786+
.mockResolvedValueOnce({
787+
config: {
788+
params: {
789+
vectors: {
790+
size: differentVectorSize,
791+
},
792+
},
793+
},
794+
} as any)
795+
// Second call should return 404 to confirm deletion
796+
.mockRejectedValueOnce({
797+
response: { status: 404 },
798+
message: "Not found",
799+
})
800+
801+
mockQdrantClientInstance.deleteCollection.mockResolvedValue(true as any)
802+
mockQdrantClientInstance.createCollection.mockResolvedValue(true as any)
803+
mockQdrantClientInstance.createPayloadIndex.mockResolvedValue({} as any)
804+
vitest.spyOn(console, "warn").mockImplementation(() => {})
805+
806+
const result = await vectorStore.initialize()
807+
808+
expect(result).toBe(true)
809+
// Should call getCollection twice: once to check existing, once to verify deletion
810+
expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(2)
811+
expect(mockQdrantClientInstance.deleteCollection).toHaveBeenCalledTimes(1)
812+
expect(mockQdrantClientInstance.createCollection).toHaveBeenCalledTimes(1)
813+
expect(mockQdrantClientInstance.createPayloadIndex).toHaveBeenCalledTimes(5)
814+
;(console.warn as any).mockRestore()
815+
})
816+
817+
it("should throw error if collection still exists after deletion attempt", async () => {
818+
const differentVectorSize = 768
819+
mockQdrantClientInstance.getCollection
820+
.mockResolvedValueOnce({
821+
config: {
822+
params: {
823+
vectors: {
824+
size: differentVectorSize,
825+
},
826+
},
827+
},
828+
} as any)
829+
// Second call should still return the collection (deletion failed)
830+
.mockResolvedValueOnce({
831+
config: {
832+
params: {
833+
vectors: {
834+
size: differentVectorSize,
835+
},
836+
},
837+
},
838+
} as any)
839+
840+
mockQdrantClientInstance.deleteCollection.mockResolvedValue(true as any)
841+
vitest.spyOn(console, "error").mockImplementation(() => {})
842+
vitest.spyOn(console, "warn").mockImplementation(() => {})
843+
844+
let caughtError: any
845+
try {
846+
await vectorStore.initialize()
847+
} catch (error: any) {
848+
caughtError = error
849+
}
850+
851+
expect(caughtError).toBeDefined()
852+
expect(caughtError.message).toContain("Failed to update vector index for new model")
853+
// The error message should contain the contextual error details
854+
expect(caughtError.message).toContain("Deleted existing collection but failed verification step")
855+
856+
expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(2)
857+
expect(mockQdrantClientInstance.deleteCollection).toHaveBeenCalledTimes(1)
858+
expect(mockQdrantClientInstance.createCollection).not.toHaveBeenCalled()
859+
expect(mockQdrantClientInstance.createPayloadIndex).not.toHaveBeenCalled()
860+
;(console.error as any).mockRestore()
861+
;(console.warn as any).mockRestore()
862+
})
863+
864+
it("should handle dimension mismatch scenario from 2048 to 768 dimensions", async () => {
865+
// Simulate the exact scenario from the issue: switching from 2048 to 768 dimensions
866+
const oldVectorSize = 2048
867+
const newVectorSize = 768
868+
869+
// Create a new vector store with the new dimension
870+
const newVectorStore = new QdrantVectorStore(mockWorkspacePath, mockQdrantUrl, newVectorSize, mockApiKey)
871+
872+
mockQdrantClientInstance.getCollection
873+
.mockResolvedValueOnce({
874+
config: {
875+
params: {
876+
vectors: {
877+
size: oldVectorSize, // Existing collection has 2048 dimensions
878+
},
879+
},
880+
},
881+
} as any)
882+
// Second call should return 404 to confirm deletion
883+
.mockRejectedValueOnce({
884+
response: { status: 404 },
885+
message: "Not found",
886+
})
887+
888+
mockQdrantClientInstance.deleteCollection.mockResolvedValue(true as any)
889+
mockQdrantClientInstance.createCollection.mockResolvedValue(true as any)
890+
mockQdrantClientInstance.createPayloadIndex.mockResolvedValue({} as any)
891+
vitest.spyOn(console, "warn").mockImplementation(() => {})
892+
893+
const result = await newVectorStore.initialize()
894+
895+
expect(result).toBe(true)
896+
expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(2)
897+
expect(mockQdrantClientInstance.deleteCollection).toHaveBeenCalledTimes(1)
898+
expect(mockQdrantClientInstance.createCollection).toHaveBeenCalledWith(expectedCollectionName, {
899+
vectors: {
900+
size: newVectorSize, // Should create with new 768 dimensions
901+
distance: "Cosine",
902+
},
903+
})
904+
expect(mockQdrantClientInstance.createPayloadIndex).toHaveBeenCalledTimes(5)
905+
;(console.warn as any).mockRestore()
906+
})
907+
908+
it("should provide detailed error context for different failure scenarios", async () => {
909+
const differentVectorSize = 768
910+
mockQdrantClientInstance.getCollection.mockResolvedValue({
911+
config: {
912+
params: {
913+
vectors: {
914+
size: differentVectorSize,
915+
},
916+
},
917+
},
918+
} as any)
919+
920+
// Test deletion failure with specific error message
921+
const deleteError = new Error("Qdrant server unavailable")
922+
mockQdrantClientInstance.deleteCollection.mockRejectedValue(deleteError)
923+
vitest.spyOn(console, "error").mockImplementation(() => {})
924+
vitest.spyOn(console, "warn").mockImplementation(() => {})
925+
926+
let caughtError: any
927+
try {
928+
await vectorStore.initialize()
929+
} catch (error: any) {
930+
caughtError = error
931+
}
932+
933+
expect(caughtError).toBeDefined()
934+
expect(caughtError.message).toContain("Failed to update vector index for new model")
935+
// The error message should contain the contextual error details
936+
expect(caughtError.message).toContain("Failed to delete existing collection with vector size")
937+
expect(caughtError.message).toContain("Qdrant server unavailable")
938+
expect(caughtError.cause).toBe(deleteError)
939+
;(console.error as any).mockRestore()
940+
;(console.warn as any).mockRestore()
941+
})
761942
})
762943

763944
it("should return true when collection exists", async () => {

0 commit comments

Comments
 (0)