Skip to content

Commit 3a0a6d0

Browse files
committed
feat: implement vector database adapter pattern for multiple providers
- Create abstract VectorDBAdapter base class - Implement adapters for LanceDB, ChromaDB, and SQLite+Vector - Update Qdrant implementation to use adapter pattern - Update service factory to support multiple vector databases - Update configuration manager to handle vector DB selection - Add i18n translations for new vector DB options This allows users to choose from multiple vector database options: - Qdrant (existing, requires external service) - LanceDB (embedded, no external service needed) - ChromaDB (can run embedded or as service) - SQLite+Vector (embedded, lightweight option) Addresses #6223
1 parent 490538f commit 3a0a6d0

File tree

11 files changed

+1992
-32
lines changed

11 files changed

+1992
-32
lines changed

src/i18n/locales/en/embeddings.json

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,16 @@
2424
},
2525
"vectorStore": {
2626
"qdrantConnectionFailed": "Failed to connect to Qdrant vector database. Please ensure Qdrant is running and accessible at {{qdrantUrl}}. Error: {{errorMessage}}",
27-
"vectorDimensionMismatch": "Failed to update vector index for new model. Please try clearing the index and starting again. Details: {{errorMessage}}"
27+
"vectorDimensionMismatch": "Failed to update vector index for new model. Please try clearing the index and starting again. Details: {{errorMessage}}",
28+
"lancedbNotInstalled": "LanceDB is not installed. Please install it with: npm install @lancedb/lancedb. Error: {{errorMessage}}",
29+
"lancedbInitFailed": "Failed to initialize LanceDB. Error: {{errorMessage}}",
30+
"lancedbConnectionFailed": "Failed to connect to LanceDB. Error: {{errorMessage}}",
31+
"chromadbNotInstalled": "ChromaDB is not installed. Please install it with: npm install chromadb. Error: {{errorMessage}}",
32+
"chromadbInitFailed": "Failed to initialize ChromaDB at {{chromaUrl}}. Error: {{errorMessage}}",
33+
"chromadbConnectionFailed": "Failed to connect to ChromaDB at {{chromaUrl}}. Please ensure ChromaDB is running. Error: {{errorMessage}}",
34+
"sqliteNotInstalled": "SQLite is not installed. Please install it with: npm install better-sqlite3. Error: {{errorMessage}}",
35+
"sqliteVssNotInstalled": "SQLite VSS extension is not installed. Please install it with: npm install sqlite-vss. Error: {{errorMessage}}",
36+
"sqliteInitFailed": "Failed to initialize SQLite vector database. Error: {{errorMessage}}"
2837
},
2938
"validation": {
3039
"authenticationFailed": "Authentication failed. Please check your API key in the settings.",
@@ -51,6 +60,7 @@
5160
"vectorDimensionNotDeterminedOpenAiCompatible": "Could not determine vector dimension for model '{{modelId}}' with provider '{{provider}}'. Please ensure the 'Embedding Dimension' is correctly set in the OpenAI-Compatible provider settings.",
5261
"vectorDimensionNotDetermined": "Could not determine vector dimension for model '{{modelId}}' with provider '{{provider}}'. Check model profiles or configuration.",
5362
"qdrantUrlMissing": "Qdrant URL missing for vector store creation",
54-
"codeIndexingNotConfigured": "Cannot create services: Code indexing is not properly configured"
63+
"codeIndexingNotConfigured": "Cannot create services: Code indexing is not properly configured",
64+
"invalidVectorDBProvider": "Invalid vector database provider: {{provider}}"
5565
}
5666
}

src/services/code-index/config-manager.ts

Lines changed: 88 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,13 @@ export class CodeIndexConfigManager {
1919
private openAiCompatibleOptions?: { baseUrl: string; apiKey: string }
2020
private geminiOptions?: { apiKey: string }
2121
private mistralOptions?: { apiKey: string }
22+
// Vector database configuration
23+
private vectorDBProvider: "qdrant" | "lancedb" | "chromadb" | "sqlite-vector" = "qdrant"
2224
private qdrantUrl?: string = "http://localhost:6333"
2325
private qdrantApiKey?: string
26+
private chromadbUrl?: string = "http://localhost:8000"
27+
private chromadbApiKey?: string
28+
// Search configuration
2429
private searchMinScore?: number
2530
private searchMaxResults?: number
2631

@@ -44,7 +49,9 @@ export class CodeIndexConfigManager {
4449
// Load configuration from storage
4550
const codebaseIndexConfig = this.contextProxy?.getGlobalState("codebaseIndexConfig") ?? {
4651
codebaseIndexEnabled: true,
52+
codebaseIndexVectorDBProvider: "qdrant",
4753
codebaseIndexQdrantUrl: "http://localhost:6333",
54+
codebaseIndexChromadbUrl: "http://localhost:8000",
4855
codebaseIndexEmbedderProvider: "openai",
4956
codebaseIndexEmbedderBaseUrl: "",
5057
codebaseIndexEmbedderModelId: "",
@@ -62,23 +69,32 @@ export class CodeIndexConfigManager {
6269
codebaseIndexSearchMaxResults,
6370
} = codebaseIndexConfig
6471

72+
// Extract new properties with optional chaining
73+
const codebaseIndexVectorDBProvider = (codebaseIndexConfig as any).codebaseIndexVectorDBProvider
74+
const codebaseIndexChromadbUrl = (codebaseIndexConfig as any).codebaseIndexChromadbUrl
75+
6576
const openAiKey = this.contextProxy?.getSecret("codeIndexOpenAiKey") ?? ""
6677
const qdrantApiKey = this.contextProxy?.getSecret("codeIndexQdrantApiKey") ?? ""
78+
// ChromaDB API key is not in the secret keys type yet, so we'll handle it differently
79+
const chromadbApiKey = ""
6780
// Fix: Read OpenAI Compatible settings from the correct location within codebaseIndexConfig
68-
const openAiCompatibleBaseUrl = codebaseIndexConfig.codebaseIndexOpenAiCompatibleBaseUrl ?? ""
81+
const openAiCompatibleBaseUrl = (codebaseIndexConfig as any).codebaseIndexOpenAiCompatibleBaseUrl ?? ""
6982
const openAiCompatibleApiKey = this.contextProxy?.getSecret("codebaseIndexOpenAiCompatibleApiKey") ?? ""
7083
const geminiApiKey = this.contextProxy?.getSecret("codebaseIndexGeminiApiKey") ?? ""
7184
const mistralApiKey = this.contextProxy?.getSecret("codebaseIndexMistralApiKey") ?? ""
7285

7386
// Update instance variables with configuration
7487
this.codebaseIndexEnabled = codebaseIndexEnabled ?? true
88+
this.vectorDBProvider = codebaseIndexVectorDBProvider ?? "qdrant"
7589
this.qdrantUrl = codebaseIndexQdrantUrl
7690
this.qdrantApiKey = qdrantApiKey ?? ""
91+
this.chromadbUrl = codebaseIndexChromadbUrl ?? "http://localhost:8000"
92+
this.chromadbApiKey = chromadbApiKey ?? ""
7793
this.searchMinScore = codebaseIndexSearchMinScore
7894
this.searchMaxResults = codebaseIndexSearchMaxResults
7995

8096
// Validate and set model dimension
81-
const rawDimension = codebaseIndexConfig.codebaseIndexEmbedderModelDimension
97+
const rawDimension = (codebaseIndexConfig as any).codebaseIndexEmbedderModelDimension
8298
if (rawDimension !== undefined && rawDimension !== null) {
8399
const dimension = Number(rawDimension)
84100
if (!isNaN(dimension) && dimension > 0) {
@@ -141,8 +157,11 @@ export class CodeIndexConfigManager {
141157
openAiCompatibleOptions?: { baseUrl: string; apiKey: string }
142158
geminiOptions?: { apiKey: string }
143159
mistralOptions?: { apiKey: string }
160+
vectorDBProvider?: "qdrant" | "lancedb" | "chromadb" | "sqlite-vector"
144161
qdrantUrl?: string
145162
qdrantApiKey?: string
163+
chromadbUrl?: string
164+
chromadbApiKey?: string
146165
searchMinScore?: number
147166
}
148167
requiresRestart: boolean
@@ -160,8 +179,11 @@ export class CodeIndexConfigManager {
160179
openAiCompatibleApiKey: this.openAiCompatibleOptions?.apiKey ?? "",
161180
geminiApiKey: this.geminiOptions?.apiKey ?? "",
162181
mistralApiKey: this.mistralOptions?.apiKey ?? "",
182+
vectorDBProvider: this.vectorDBProvider,
163183
qdrantUrl: this.qdrantUrl ?? "",
164184
qdrantApiKey: this.qdrantApiKey ?? "",
185+
chromadbUrl: this.chromadbUrl ?? "",
186+
chromadbApiKey: this.chromadbApiKey ?? "",
165187
}
166188

167189
// Refresh secrets from VSCode storage to ensure we have the latest values
@@ -184,45 +206,64 @@ export class CodeIndexConfigManager {
184206
openAiCompatibleOptions: this.openAiCompatibleOptions,
185207
geminiOptions: this.geminiOptions,
186208
mistralOptions: this.mistralOptions,
209+
vectorDBProvider: this.vectorDBProvider,
187210
qdrantUrl: this.qdrantUrl,
188211
qdrantApiKey: this.qdrantApiKey,
212+
chromadbUrl: this.chromadbUrl,
213+
chromadbApiKey: this.chromadbApiKey,
189214
searchMinScore: this.currentSearchMinScore,
190215
},
191216
requiresRestart,
192217
}
193218
}
194219

195220
/**
196-
* Checks if the service is properly configured based on the embedder type.
221+
* Checks if the service is properly configured based on the embedder type and vector DB provider.
197222
*/
198223
public isConfigured(): boolean {
224+
// First check embedder configuration
225+
let embedderConfigured = false
226+
199227
if (this.embedderProvider === "openai") {
200228
const openAiKey = this.openAiOptions?.openAiNativeApiKey
201-
const qdrantUrl = this.qdrantUrl
202-
return !!(openAiKey && qdrantUrl)
229+
embedderConfigured = !!openAiKey
203230
} else if (this.embedderProvider === "ollama") {
204231
// Ollama model ID has a default, so only base URL is strictly required for config
205232
const ollamaBaseUrl = this.ollamaOptions?.ollamaBaseUrl
206-
const qdrantUrl = this.qdrantUrl
207-
return !!(ollamaBaseUrl && qdrantUrl)
233+
embedderConfigured = !!ollamaBaseUrl
208234
} else if (this.embedderProvider === "openai-compatible") {
209235
const baseUrl = this.openAiCompatibleOptions?.baseUrl
210236
const apiKey = this.openAiCompatibleOptions?.apiKey
211-
const qdrantUrl = this.qdrantUrl
212-
const isConfigured = !!(baseUrl && apiKey && qdrantUrl)
213-
return isConfigured
237+
embedderConfigured = !!(baseUrl && apiKey)
214238
} else if (this.embedderProvider === "gemini") {
215239
const apiKey = this.geminiOptions?.apiKey
216-
const qdrantUrl = this.qdrantUrl
217-
const isConfigured = !!(apiKey && qdrantUrl)
218-
return isConfigured
240+
embedderConfigured = !!apiKey
219241
} else if (this.embedderProvider === "mistral") {
220242
const apiKey = this.mistralOptions?.apiKey
221-
const qdrantUrl = this.qdrantUrl
222-
const isConfigured = !!(apiKey && qdrantUrl)
223-
return isConfigured
243+
embedderConfigured = !!apiKey
244+
}
245+
246+
// Then check vector database configuration
247+
let vectorDBConfigured = false
248+
249+
switch (this.vectorDBProvider) {
250+
case "qdrant":
251+
vectorDBConfigured = !!this.qdrantUrl
252+
break
253+
case "chromadb":
254+
vectorDBConfigured = !!this.chromadbUrl
255+
break
256+
case "lancedb":
257+
case "sqlite-vector":
258+
// These are embedded databases, no URL needed
259+
vectorDBConfigured = true
260+
break
261+
default:
262+
// Default to qdrant for backward compatibility
263+
vectorDBConfigured = !!this.qdrantUrl
224264
}
225-
return false // Should not happen if embedderProvider is always set correctly
265+
266+
return embedderConfigured && vectorDBConfigured
226267
}
227268

228269
/**
@@ -255,8 +296,11 @@ export class CodeIndexConfigManager {
255296
const prevModelDimension = prev?.modelDimension
256297
const prevGeminiApiKey = prev?.geminiApiKey ?? ""
257298
const prevMistralApiKey = prev?.mistralApiKey ?? ""
299+
const prevVectorDBProvider = prev?.vectorDBProvider ?? "qdrant"
258300
const prevQdrantUrl = prev?.qdrantUrl ?? ""
259301
const prevQdrantApiKey = prev?.qdrantApiKey ?? ""
302+
const prevChromadbUrl = prev?.chromadbUrl ?? ""
303+
const prevChromadbApiKey = prev?.chromadbApiKey ?? ""
260304

261305
// 1. Transition from disabled/unconfigured to enabled/configured
262306
if ((!prevEnabled || !prevConfigured) && this.codebaseIndexEnabled && nowConfigured) {
@@ -279,21 +323,28 @@ export class CodeIndexConfigManager {
279323
return false
280324
}
281325

282-
// Provider change
283-
if (prevProvider !== this.embedderProvider) {
284-
return true
285-
}
286-
287-
// Authentication changes (API keys)
326+
// Get current values
288327
const currentOpenAiKey = this.openAiOptions?.openAiNativeApiKey ?? ""
289328
const currentOllamaBaseUrl = this.ollamaOptions?.ollamaBaseUrl ?? ""
290329
const currentOpenAiCompatibleBaseUrl = this.openAiCompatibleOptions?.baseUrl ?? ""
291330
const currentOpenAiCompatibleApiKey = this.openAiCompatibleOptions?.apiKey ?? ""
292331
const currentModelDimension = this.modelDimension
293332
const currentGeminiApiKey = this.geminiOptions?.apiKey ?? ""
294333
const currentMistralApiKey = this.mistralOptions?.apiKey ?? ""
334+
const currentVectorDBProvider = this.vectorDBProvider ?? "qdrant"
295335
const currentQdrantUrl = this.qdrantUrl ?? ""
296336
const currentQdrantApiKey = this.qdrantApiKey ?? ""
337+
const currentChromadbUrl = this.chromadbUrl ?? ""
338+
const currentChromadbApiKey = this.chromadbApiKey ?? ""
339+
340+
// Provider change (embedder or vector DB)
341+
if (prevProvider !== this.embedderProvider) {
342+
return true
343+
}
344+
345+
if (prevVectorDBProvider !== currentVectorDBProvider) {
346+
return true
347+
}
297348

298349
if (prevOpenAiKey !== currentOpenAiKey) {
299350
return true
@@ -323,8 +374,17 @@ export class CodeIndexConfigManager {
323374
return true
324375
}
325376

326-
if (prevQdrantUrl !== currentQdrantUrl || prevQdrantApiKey !== currentQdrantApiKey) {
327-
return true
377+
// Vector database connection changes
378+
if (prevVectorDBProvider === "qdrant" && currentVectorDBProvider === "qdrant") {
379+
if (prevQdrantUrl !== currentQdrantUrl || prevQdrantApiKey !== currentQdrantApiKey) {
380+
return true
381+
}
382+
}
383+
384+
if (prevVectorDBProvider === "chromadb" && currentVectorDBProvider === "chromadb") {
385+
if (prevChromadbUrl !== currentChromadbUrl || prevChromadbApiKey !== currentChromadbApiKey) {
386+
return true
387+
}
328388
}
329389

330390
// Vector dimension changes (still important for compatibility)
@@ -375,8 +435,11 @@ export class CodeIndexConfigManager {
375435
openAiCompatibleOptions: this.openAiCompatibleOptions,
376436
geminiOptions: this.geminiOptions,
377437
mistralOptions: this.mistralOptions,
438+
vectorDBProvider: this.vectorDBProvider,
378439
qdrantUrl: this.qdrantUrl,
379440
qdrantApiKey: this.qdrantApiKey,
441+
chromadbUrl: this.chromadbUrl,
442+
chromadbApiKey: this.chromadbApiKey,
380443
searchMinScore: this.currentSearchMinScore,
381444
searchMaxResults: this.currentSearchMaxResults,
382445
}

src/services/code-index/interfaces/config.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,13 @@ export interface CodeIndexConfig {
1414
openAiCompatibleOptions?: { baseUrl: string; apiKey: string }
1515
geminiOptions?: { apiKey: string }
1616
mistralOptions?: { apiKey: string }
17+
// Vector database configuration
18+
vectorDBProvider?: "qdrant" | "lancedb" | "chromadb" | "sqlite-vector"
1719
qdrantUrl?: string
1820
qdrantApiKey?: string
21+
chromadbUrl?: string
22+
chromadbApiKey?: string
23+
// Search configuration
1924
searchMinScore?: number
2025
searchMaxResults?: number
2126
}
@@ -35,6 +40,10 @@ export type PreviousConfigSnapshot = {
3540
openAiCompatibleApiKey?: string
3641
geminiApiKey?: string
3742
mistralApiKey?: string
43+
// Vector database configuration
44+
vectorDBProvider?: "qdrant" | "lancedb" | "chromadb" | "sqlite-vector"
3845
qdrantUrl?: string
3946
qdrantApiKey?: string
47+
chromadbUrl?: string
48+
chromadbApiKey?: string
4049
}

src/services/code-index/service-factory.ts

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { GeminiEmbedder } from "./embedders/gemini"
66
import { MistralEmbedder } from "./embedders/mistral"
77
import { EmbedderProvider, getDefaultModelId, getModelDimension } from "../../shared/embeddingModels"
88
import { QdrantVectorStore } from "./vector-store/qdrant-client"
9+
import { QdrantAdapter, LanceDBAdapter, ChromaDBAdapter, SQLiteVectorAdapter } from "./vector-store/adapters"
910
import { codeParser, DirectoryScanner, FileWatcher } from "./processors"
1011
import { ICodeParser, IEmbedder, IFileWatcher, IVectorStore } from "./interfaces"
1112
import { CodeIndexConfigManager } from "./config-manager"
@@ -15,6 +16,8 @@ import { t } from "../../i18n"
1516
import { TelemetryService } from "@roo-code/telemetry"
1617
import { TelemetryEventName } from "@roo-code/types"
1718

19+
export type VectorDBProvider = "qdrant" | "lancedb" | "chromadb" | "sqlite-vector"
20+
1821
/**
1922
* Factory class responsible for creating and configuring code indexing service dependencies.
2023
*/
@@ -132,12 +135,45 @@ export class CodeIndexServiceFactory {
132135
}
133136
}
134137

135-
if (!config.qdrantUrl) {
136-
throw new Error(t("embeddings:serviceFactory.qdrantUrlMissing"))
138+
// Get vector database provider from config (default to qdrant for backward compatibility)
139+
const vectorDBProvider = (config.vectorDBProvider as VectorDBProvider) || "qdrant"
140+
141+
// Create appropriate vector store based on provider
142+
switch (vectorDBProvider) {
143+
case "qdrant":
144+
if (!config.qdrantUrl) {
145+
throw new Error(t("embeddings:serviceFactory.qdrantUrlMissing"))
146+
}
147+
return new QdrantAdapter({
148+
workspacePath: this.workspacePath,
149+
url: config.qdrantUrl,
150+
vectorSize,
151+
apiKey: config.qdrantApiKey,
152+
})
153+
154+
case "lancedb":
155+
return new LanceDBAdapter({
156+
workspacePath: this.workspacePath,
157+
vectorSize,
158+
})
159+
160+
case "chromadb":
161+
return new ChromaDBAdapter({
162+
workspacePath: this.workspacePath,
163+
url: config.chromadbUrl || "http://localhost:8000",
164+
vectorSize,
165+
apiKey: config.chromadbApiKey,
166+
})
167+
168+
case "sqlite-vector":
169+
return new SQLiteVectorAdapter({
170+
workspacePath: this.workspacePath,
171+
vectorSize,
172+
})
173+
174+
default:
175+
throw new Error(t("embeddings:serviceFactory.invalidVectorDBProvider", { provider: vectorDBProvider }))
137176
}
138-
139-
// Assuming constructor is updated: new QdrantVectorStore(workspacePath, url, vectorSize, apiKey?)
140-
return new QdrantVectorStore(this.workspacePath, config.qdrantUrl, vectorSize, config.qdrantApiKey)
141177
}
142178

143179
/**

0 commit comments

Comments
 (0)