Skip to content

Commit ef2a3db

Browse files
CW-B-WAlorse
authored andcommitted
fix(qdrant): add URL prefix handling for QdrantClient initialization (RooCodeInc#5033)
1 parent c2b72bb commit ef2a3db

File tree

2 files changed

+161
-0
lines changed

2 files changed

+161
-0
lines changed

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

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ describe("QdrantVectorStore", () => {
113113
host: "qdrant.ashbyfam.com",
114114
https: true,
115115
port: 443,
116+
prefix: undefined, // No prefix for root path
116117
apiKey: undefined,
117118
headers: {
118119
"User-Agent": "Roo-Code",
@@ -127,6 +128,7 @@ describe("QdrantVectorStore", () => {
127128
host: "example.com",
128129
https: true,
129130
port: 9000,
131+
prefix: undefined, // No prefix for root path
130132
apiKey: undefined,
131133
headers: {
132134
"User-Agent": "Roo-Code",
@@ -145,6 +147,7 @@ describe("QdrantVectorStore", () => {
145147
host: "example.com",
146148
https: true,
147149
port: 443,
150+
prefix: "/api/v1", // Should have prefix
148151
apiKey: undefined,
149152
headers: {
150153
"User-Agent": "Roo-Code",
@@ -161,6 +164,7 @@ describe("QdrantVectorStore", () => {
161164
host: "example.com",
162165
https: false,
163166
port: 80,
167+
prefix: undefined, // No prefix for root path
164168
apiKey: undefined,
165169
headers: {
166170
"User-Agent": "Roo-Code",
@@ -175,6 +179,7 @@ describe("QdrantVectorStore", () => {
175179
host: "localhost",
176180
https: false,
177181
port: 8080,
182+
prefix: undefined, // No prefix for root path
178183
apiKey: undefined,
179184
headers: {
180185
"User-Agent": "Roo-Code",
@@ -193,6 +198,7 @@ describe("QdrantVectorStore", () => {
193198
host: "example.com",
194199
https: false,
195200
port: 80,
201+
prefix: "/api/v1", // Should have prefix
196202
apiKey: undefined,
197203
headers: {
198204
"User-Agent": "Roo-Code",
@@ -337,6 +343,159 @@ describe("QdrantVectorStore", () => {
337343
})
338344
})
339345

346+
describe("URL Prefix Handling", () => {
347+
it("should pass the URL pathname as prefix to QdrantClient if not root", () => {
348+
const vectorStoreWithPrefix = new QdrantVectorStore(
349+
mockWorkspacePath,
350+
"http://localhost:6333/some/path",
351+
mockVectorSize,
352+
)
353+
expect(QdrantClient).toHaveBeenLastCalledWith({
354+
host: "localhost",
355+
https: false,
356+
port: 6333,
357+
prefix: "/some/path",
358+
apiKey: undefined,
359+
headers: {
360+
"User-Agent": "Roo-Code",
361+
},
362+
})
363+
expect((vectorStoreWithPrefix as any).qdrantUrl).toBe("http://localhost:6333/some/path")
364+
})
365+
366+
it("should not pass prefix if the URL pathname is root ('/')", () => {
367+
const vectorStoreWithoutPrefix = new QdrantVectorStore(
368+
mockWorkspacePath,
369+
"http://localhost:6333/",
370+
mockVectorSize,
371+
)
372+
expect(QdrantClient).toHaveBeenLastCalledWith({
373+
host: "localhost",
374+
https: false,
375+
port: 6333,
376+
prefix: undefined,
377+
apiKey: undefined,
378+
headers: {
379+
"User-Agent": "Roo-Code",
380+
},
381+
})
382+
expect((vectorStoreWithoutPrefix as any).qdrantUrl).toBe("http://localhost:6333/")
383+
})
384+
385+
it("should handle HTTPS URL with path as prefix", () => {
386+
const vectorStoreWithHttpsPrefix = new QdrantVectorStore(
387+
mockWorkspacePath,
388+
"https://qdrant.ashbyfam.com/api",
389+
mockVectorSize,
390+
)
391+
expect(QdrantClient).toHaveBeenLastCalledWith({
392+
host: "qdrant.ashbyfam.com",
393+
https: true,
394+
port: 443,
395+
prefix: "/api",
396+
apiKey: undefined,
397+
headers: {
398+
"User-Agent": "Roo-Code",
399+
},
400+
})
401+
expect((vectorStoreWithHttpsPrefix as any).qdrantUrl).toBe("https://qdrant.ashbyfam.com/api")
402+
})
403+
404+
it("should normalize URL pathname by removing trailing slash for prefix", () => {
405+
const vectorStoreWithTrailingSlash = new QdrantVectorStore(
406+
mockWorkspacePath,
407+
"http://localhost:6333/api/",
408+
mockVectorSize,
409+
)
410+
expect(QdrantClient).toHaveBeenLastCalledWith({
411+
host: "localhost",
412+
https: false,
413+
port: 6333,
414+
prefix: "/api", // Trailing slash should be removed
415+
apiKey: undefined,
416+
headers: {
417+
"User-Agent": "Roo-Code",
418+
},
419+
})
420+
expect((vectorStoreWithTrailingSlash as any).qdrantUrl).toBe("http://localhost:6333/api/")
421+
})
422+
423+
it("should normalize URL pathname by removing multiple trailing slashes for prefix", () => {
424+
const vectorStoreWithMultipleTrailingSlashes = new QdrantVectorStore(
425+
mockWorkspacePath,
426+
"http://localhost:6333/api///",
427+
mockVectorSize,
428+
)
429+
expect(QdrantClient).toHaveBeenLastCalledWith({
430+
host: "localhost",
431+
https: false,
432+
port: 6333,
433+
prefix: "/api", // All trailing slashes should be removed
434+
apiKey: undefined,
435+
headers: {
436+
"User-Agent": "Roo-Code",
437+
},
438+
})
439+
expect((vectorStoreWithMultipleTrailingSlashes as any).qdrantUrl).toBe("http://localhost:6333/api///")
440+
})
441+
442+
it("should handle multiple path segments correctly for prefix", () => {
443+
const vectorStoreWithMultiSegment = new QdrantVectorStore(
444+
mockWorkspacePath,
445+
"http://localhost:6333/api/v1/qdrant",
446+
mockVectorSize,
447+
)
448+
expect(QdrantClient).toHaveBeenLastCalledWith({
449+
host: "localhost",
450+
https: false,
451+
port: 6333,
452+
prefix: "/api/v1/qdrant",
453+
apiKey: undefined,
454+
headers: {
455+
"User-Agent": "Roo-Code",
456+
},
457+
})
458+
expect((vectorStoreWithMultiSegment as any).qdrantUrl).toBe("http://localhost:6333/api/v1/qdrant")
459+
})
460+
461+
it("should handle complex URL with multiple segments, multiple trailing slashes, query params, and fragment", () => {
462+
const complexUrl = "https://example.com/ollama/api/v1///?key=value#pos"
463+
const vectorStoreComplex = new QdrantVectorStore(mockWorkspacePath, complexUrl, mockVectorSize)
464+
expect(QdrantClient).toHaveBeenLastCalledWith({
465+
host: "example.com",
466+
https: true,
467+
port: 443,
468+
prefix: "/ollama/api/v1", // Trailing slash removed, query/fragment ignored
469+
apiKey: undefined,
470+
headers: {
471+
"User-Agent": "Roo-Code",
472+
},
473+
})
474+
expect((vectorStoreComplex as any).qdrantUrl).toBe(complexUrl)
475+
})
476+
477+
it("should ignore query parameters and fragments when determining prefix", () => {
478+
const vectorStoreWithQueryParams = new QdrantVectorStore(
479+
mockWorkspacePath,
480+
"http://localhost:6333/api/path?key=value#fragment",
481+
mockVectorSize,
482+
)
483+
expect(QdrantClient).toHaveBeenLastCalledWith({
484+
host: "localhost",
485+
https: false,
486+
port: 6333,
487+
prefix: "/api/path", // Query params and fragment should be ignored
488+
apiKey: undefined,
489+
headers: {
490+
"User-Agent": "Roo-Code",
491+
},
492+
})
493+
expect((vectorStoreWithQueryParams as any).qdrantUrl).toBe(
494+
"http://localhost:6333/api/path?key=value#fragment",
495+
)
496+
})
497+
})
498+
340499
describe("initialize", () => {
341500
it("should create a new collection if none exists and return true", async () => {
342501
// Mock getCollection to throw a 404-like error

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,15 @@ export class QdrantVectorStore implements IVectorStore {
5757
host: urlObj.hostname,
5858
https: useHttps,
5959
port: port,
60+
prefix: urlObj.pathname === "/" ? undefined : urlObj.pathname.replace(/\/+$/, ""),
6061
apiKey,
6162
headers: {
6263
"User-Agent": "Roo-Code",
6364
},
6465
})
6566
} catch (urlError) {
6667
// If URL parsing fails, fall back to URL-based config
68+
// Note: This fallback won't correctly handle prefixes, but it's a last resort for malformed URLs.
6769
this.client = new QdrantClient({
6870
url: parsedUrl,
6971
apiKey,

0 commit comments

Comments
 (0)