feat: add Qdrant collection name field to codebase indexing settings

- Add UI field in CodeIndexPopover for viewing/editing collection name
- Update configuration interfaces to include qdrantCollectionName
- Modify QdrantVectorStore to accept optional collection name parameter
- Update config manager to handle collection name in settings
- Add translation keys for new UI labels
- Update tests to account for new parameter

Fixes #7728
This commit is contained in:
Roo Code 2025-09-06 02:56:16 +00:00
parent e8deedd91b
commit 43f94412e1
10 changed files with 85 additions and 8 deletions

View file

@ -21,6 +21,7 @@ export const CODEBASE_INDEX_DEFAULTS = {
export const codebaseIndexConfigSchema = z.object({
codebaseIndexEnabled: z.boolean().optional(),
codebaseIndexQdrantUrl: z.string().optional(),
codebaseIndexQdrantCollectionName: z.string().optional(),
codebaseIndexEmbedderProvider: z
.enum(["openai", "ollama", "openai-compatible", "gemini", "mistral", "vercel-ai-gateway"])
.optional(),

View file

@ -2270,6 +2270,7 @@ export const webviewMessageHandler = async (
...currentConfig,
codebaseIndexEnabled: settings.codebaseIndexEnabled,
codebaseIndexQdrantUrl: settings.codebaseIndexQdrantUrl,
codebaseIndexQdrantCollectionName: settings.codebaseIndexQdrantCollectionName,
codebaseIndexEmbedderProvider: settings.codebaseIndexEmbedderProvider,
codebaseIndexEmbedderBaseUrl: settings.codebaseIndexEmbedderBaseUrl,
codebaseIndexEmbedderModelId: settings.codebaseIndexEmbedderModelId,

View file

@ -104,8 +104,14 @@ describe("CodeIndexConfigManager", () => {
modelId: undefined,
openAiOptions: { openAiNativeApiKey: "" },
ollamaOptions: { ollamaBaseUrl: "" },
geminiOptions: undefined,
mistralOptions: undefined,
openAiCompatibleOptions: undefined,
vercelAiGatewayOptions: undefined,
modelDimension: undefined,
qdrantUrl: "http://localhost:6333",
qdrantApiKey: "",
qdrantCollectionName: "",
searchMinScore: 0.4,
})
expect(result.requiresRestart).toBe(false)

View file

@ -367,6 +367,7 @@ describe("CodeIndexServiceFactory", () => {
"http://localhost:6333",
3072,
"test-key",
undefined, // collectionName
)
})
@ -392,6 +393,7 @@ describe("CodeIndexServiceFactory", () => {
"http://localhost:6333",
768,
"test-key",
undefined, // collectionName
)
})
@ -417,6 +419,7 @@ describe("CodeIndexServiceFactory", () => {
"http://localhost:6333",
3072,
"test-key",
undefined, // collectionName
)
})
@ -449,6 +452,7 @@ describe("CodeIndexServiceFactory", () => {
"http://localhost:6333",
modelDimension, // Should use model's built-in dimension, not manual
"test-key",
undefined, // collectionName
)
})
@ -480,6 +484,7 @@ describe("CodeIndexServiceFactory", () => {
"http://localhost:6333",
manualDimension, // Should use manual dimension as fallback
"test-key",
undefined, // collectionName
)
})
@ -509,6 +514,7 @@ describe("CodeIndexServiceFactory", () => {
"http://localhost:6333",
768,
"test-key",
undefined, // collectionName
)
})
@ -578,6 +584,7 @@ describe("CodeIndexServiceFactory", () => {
"http://localhost:6333",
3072,
"test-key",
undefined, // collectionName
)
})
@ -603,6 +610,7 @@ describe("CodeIndexServiceFactory", () => {
"http://localhost:6333",
3072,
"test-key",
undefined, // collectionName
)
})
@ -627,6 +635,7 @@ describe("CodeIndexServiceFactory", () => {
"http://localhost:6333",
1536,
"test-key",
undefined, // collectionName
)
})

View file

@ -21,6 +21,7 @@ export class CodeIndexConfigManager {
private mistralOptions?: { apiKey: string }
private vercelAiGatewayOptions?: { apiKey: string }
private qdrantUrl?: string = "http://localhost:6333"
private qdrantCollectionName?: string
private qdrantApiKey?: string
private searchMinScore?: number
private searchMaxResults?: number
@ -46,6 +47,7 @@ export class CodeIndexConfigManager {
const codebaseIndexConfig = this.contextProxy?.getGlobalState("codebaseIndexConfig") ?? {
codebaseIndexEnabled: true,
codebaseIndexQdrantUrl: "http://localhost:6333",
codebaseIndexQdrantCollectionName: "",
codebaseIndexEmbedderProvider: "openai",
codebaseIndexEmbedderBaseUrl: "",
codebaseIndexEmbedderModelId: "",
@ -56,6 +58,7 @@ export class CodeIndexConfigManager {
const {
codebaseIndexEnabled,
codebaseIndexQdrantUrl,
codebaseIndexQdrantCollectionName,
codebaseIndexEmbedderProvider,
codebaseIndexEmbedderBaseUrl,
codebaseIndexEmbedderModelId,
@ -75,6 +78,7 @@ export class CodeIndexConfigManager {
// Update instance variables with configuration
this.codebaseIndexEnabled = codebaseIndexEnabled ?? true
this.qdrantUrl = codebaseIndexQdrantUrl
this.qdrantCollectionName = codebaseIndexQdrantCollectionName
this.qdrantApiKey = qdrantApiKey ?? ""
this.searchMinScore = codebaseIndexSearchMinScore
this.searchMaxResults = codebaseIndexSearchMaxResults
@ -148,6 +152,7 @@ export class CodeIndexConfigManager {
mistralOptions?: { apiKey: string }
vercelAiGatewayOptions?: { apiKey: string }
qdrantUrl?: string
qdrantCollectionName?: string
qdrantApiKey?: string
searchMinScore?: number
}
@ -168,6 +173,7 @@ export class CodeIndexConfigManager {
mistralApiKey: this.mistralOptions?.apiKey ?? "",
vercelAiGatewayApiKey: this.vercelAiGatewayOptions?.apiKey ?? "",
qdrantUrl: this.qdrantUrl ?? "",
qdrantCollectionName: this.qdrantCollectionName ?? "",
qdrantApiKey: this.qdrantApiKey ?? "",
}
@ -193,6 +199,7 @@ export class CodeIndexConfigManager {
mistralOptions: this.mistralOptions,
vercelAiGatewayOptions: this.vercelAiGatewayOptions,
qdrantUrl: this.qdrantUrl,
qdrantCollectionName: this.qdrantCollectionName,
qdrantApiKey: this.qdrantApiKey,
searchMinScore: this.currentSearchMinScore,
},
@ -270,6 +277,7 @@ export class CodeIndexConfigManager {
const prevMistralApiKey = prev?.mistralApiKey ?? ""
const prevVercelAiGatewayApiKey = prev?.vercelAiGatewayApiKey ?? ""
const prevQdrantUrl = prev?.qdrantUrl ?? ""
const prevQdrantCollectionName = prev?.qdrantCollectionName ?? ""
const prevQdrantApiKey = prev?.qdrantApiKey ?? ""
// 1. Transition from disabled/unconfigured to enabled/configured
@ -308,6 +316,7 @@ export class CodeIndexConfigManager {
const currentMistralApiKey = this.mistralOptions?.apiKey ?? ""
const currentVercelAiGatewayApiKey = this.vercelAiGatewayOptions?.apiKey ?? ""
const currentQdrantUrl = this.qdrantUrl ?? ""
const currentQdrantCollectionName = this.qdrantCollectionName ?? ""
const currentQdrantApiKey = this.qdrantApiKey ?? ""
if (prevOpenAiKey !== currentOpenAiKey) {
@ -342,7 +351,11 @@ export class CodeIndexConfigManager {
return true
}
if (prevQdrantUrl !== currentQdrantUrl || prevQdrantApiKey !== currentQdrantApiKey) {
if (
prevQdrantUrl !== currentQdrantUrl ||
prevQdrantCollectionName !== currentQdrantCollectionName ||
prevQdrantApiKey !== currentQdrantApiKey
) {
return true
}
@ -396,6 +409,7 @@ export class CodeIndexConfigManager {
mistralOptions: this.mistralOptions,
vercelAiGatewayOptions: this.vercelAiGatewayOptions,
qdrantUrl: this.qdrantUrl,
qdrantCollectionName: this.qdrantCollectionName,
qdrantApiKey: this.qdrantApiKey,
searchMinScore: this.currentSearchMinScore,
searchMaxResults: this.currentSearchMaxResults,
@ -426,9 +440,10 @@ export class CodeIndexConfigManager {
/**
* Gets the current Qdrant configuration
*/
public get qdrantConfig(): { url?: string; apiKey?: string } {
public get qdrantConfig(): { url?: string; collectionName?: string; apiKey?: string } {
return {
url: this.qdrantUrl,
collectionName: this.qdrantCollectionName,
apiKey: this.qdrantApiKey,
}
}

View file

@ -16,6 +16,7 @@ export interface CodeIndexConfig {
mistralOptions?: { apiKey: string }
vercelAiGatewayOptions?: { apiKey: string }
qdrantUrl?: string
qdrantCollectionName?: string
qdrantApiKey?: string
searchMinScore?: number
searchMaxResults?: number
@ -38,5 +39,6 @@ export type PreviousConfigSnapshot = {
mistralApiKey?: string
vercelAiGatewayApiKey?: string
qdrantUrl?: string
qdrantCollectionName?: string
qdrantApiKey?: string
}

View file

@ -145,8 +145,14 @@ export class CodeIndexServiceFactory {
throw new Error(t("embeddings:serviceFactory.qdrantUrlMissing"))
}
// Assuming constructor is updated: new QdrantVectorStore(workspacePath, url, vectorSize, apiKey?)
return new QdrantVectorStore(this.workspacePath, config.qdrantUrl, vectorSize, config.qdrantApiKey)
// Pass collection name if configured, otherwise let QdrantVectorStore generate it
return new QdrantVectorStore(
this.workspacePath,
config.qdrantUrl,
vectorSize,
config.qdrantApiKey,
config.qdrantCollectionName,
)
}
/**

View file

@ -24,7 +24,7 @@ export class QdrantVectorStore implements IVectorStore {
* @param workspacePath Path to the workspace
* @param url Optional URL to the Qdrant server
*/
constructor(workspacePath: string, url: string, vectorSize: number, apiKey?: string) {
constructor(workspacePath: string, url: string, vectorSize: number, apiKey?: string, collectionName?: string) {
// Parse the URL to determine the appropriate QdrantClient configuration
const parsedUrl = this.parseQdrantUrl(url)
@ -77,10 +77,17 @@ export class QdrantVectorStore implements IVectorStore {
})
}
// Generate collection name from workspace path
const hash = createHash("sha256").update(workspacePath).digest("hex")
// Use provided collection name or generate from workspace path
if (collectionName && collectionName.trim()) {
// Sanitize the collection name to ensure it's valid for Qdrant
// Qdrant collection names must match: ^[a-zA-Z0-9_-]+$
this.collectionName = collectionName.trim().replace(/[^a-zA-Z0-9_-]/g, "-")
} else {
// Generate collection name from workspace path (default behavior)
const hash = createHash("sha256").update(workspacePath).digest("hex")
this.collectionName = `ws-${hash.substring(0, 16)}`
}
this.vectorSize = vectorSize
this.collectionName = `ws-${hash.substring(0, 16)}`
}
/**

View file

@ -58,6 +58,7 @@ interface LocalCodeIndexSettings {
// Global state settings
codebaseIndexEnabled: boolean
codebaseIndexQdrantUrl: string
codebaseIndexQdrantCollectionName?: string
codebaseIndexEmbedderProvider: EmbedderProvider
codebaseIndexEmbedderBaseUrl?: string
codebaseIndexEmbedderModelId: string
@ -181,6 +182,7 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
const getDefaultSettings = (): LocalCodeIndexSettings => ({
codebaseIndexEnabled: true,
codebaseIndexQdrantUrl: "",
codebaseIndexQdrantCollectionName: "",
codebaseIndexEmbedderProvider: "openai",
codebaseIndexEmbedderBaseUrl: "",
codebaseIndexEmbedderModelId: "",
@ -213,6 +215,7 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
const settings = {
codebaseIndexEnabled: codebaseIndexConfig.codebaseIndexEnabled ?? true,
codebaseIndexQdrantUrl: codebaseIndexConfig.codebaseIndexQdrantUrl || "",
codebaseIndexQdrantCollectionName: codebaseIndexConfig.codebaseIndexQdrantCollectionName || "",
codebaseIndexEmbedderProvider: codebaseIndexConfig.codebaseIndexEmbedderProvider || "openai",
codebaseIndexEmbedderBaseUrl: codebaseIndexConfig.codebaseIndexEmbedderBaseUrl || "",
codebaseIndexEmbedderModelId: codebaseIndexConfig.codebaseIndexEmbedderModelId || "",
@ -1160,6 +1163,30 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
)}
</div>
<div className="space-y-2">
<label className="text-sm font-medium">
{t("settings:codeIndex.qdrantCollectionNameLabel")}
</label>
<VSCodeTextField
value={currentSettings.codebaseIndexQdrantCollectionName || ""}
onInput={(e: any) =>
updateSetting("codebaseIndexQdrantCollectionName", e.target.value)
}
placeholder={t("settings:codeIndex.qdrantCollectionNamePlaceholder")}
className={cn("w-full", {
"border-red-500": formErrors.codebaseIndexQdrantCollectionName,
})}
/>
{formErrors.codebaseIndexQdrantCollectionName && (
<p className="text-xs text-vscode-errorForeground mt-1 mb-0">
{formErrors.codebaseIndexQdrantCollectionName}
</p>
)}
<p className="text-xs text-vscode-descriptionForeground mt-1">
{t("settings:codeIndex.qdrantCollectionNameDescription")}
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">
{t("settings:codeIndex.qdrantApiKeyLabel")}

View file

@ -78,6 +78,9 @@
"qdrantKeyLabel": "Qdrant Key:",
"qdrantApiKeyLabel": "Qdrant API Key",
"qdrantApiKeyPlaceholder": "Enter your Qdrant API key (optional)",
"qdrantCollectionNameLabel": "Qdrant Collection Name",
"qdrantCollectionNamePlaceholder": "Enter collection name (optional)",
"qdrantCollectionNameDescription": "Custom collection name for storing embeddings. If empty, a name will be auto-generated from your workspace path.",
"setupConfigLabel": "Setup",
"advancedConfigLabel": "Advanced Configuration",
"searchMinScoreLabel": "Search Score Threshold",