diff --git a/packages/types/src/codebase-index.ts b/packages/types/src/codebase-index.ts index 89d5b168d7..90d49221e9 100644 --- a/packages/types/src/codebase-index.ts +++ b/packages/types/src/codebase-index.ts @@ -34,6 +34,10 @@ export const codebaseIndexConfigSchema = z.object({ // OpenAI Compatible specific fields codebaseIndexOpenAiCompatibleBaseUrl: z.string().optional(), codebaseIndexOpenAiCompatibleModelDimension: z.number().optional(), + // Memory optimization settings + codebaseIndexUseOnDiskStorage: z.boolean().optional(), + codebaseIndexMemoryMapThreshold: z.number().optional(), + codebaseIndexHnswEfSearch: z.number().optional(), }) export type CodebaseIndexConfig = z.infer diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 44937da235..af61e2bfb7 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -21,3 +21,4 @@ export * from "./tool.js" export * from "./type-fu.js" export * from "./vscode.js" export * from "./todo.js" +export * from "./qdrant.js" diff --git a/packages/types/src/qdrant.ts b/packages/types/src/qdrant.ts new file mode 100644 index 0000000000..f2b7b65629 --- /dev/null +++ b/packages/types/src/qdrant.ts @@ -0,0 +1,141 @@ +/** + * Qdrant Vector Store Configuration Constants + * + * These constants define default values for Qdrant memory optimization settings + * to reduce RAM usage by storing vectors and indexes on disk instead of in memory. + */ + +/** + * Default memory optimization settings for Qdrant + */ +export const QDRANT_MEMORY_OPTIMIZATION_DEFAULTS = { + /** + * Enable on-disk storage for vectors and HNSW indexes by default + * This significantly reduces memory usage at the cost of slightly slower access + */ + USE_ON_DISK_STORAGE: true, + + /** + * Number of vectors before using memory-mapped files + * Segments larger than this threshold will use memory-mapped files for better memory management + */ + MEMORY_MAP_THRESHOLD: 50000, + + /** + * HNSW search parameter (ef) - controls search quality vs memory usage + * Higher values = better search quality but more memory usage + * Lower values = less memory usage but potentially lower search quality + * Default: 128 (original value, not reduced for testing purposes) + */ + HNSW_EF_SEARCH: 128, +} as const + +/** + * HNSW (Hierarchical Navigable Small World) index configuration constants + */ +export const QDRANT_HNSW_CONFIG_DEFAULTS = { + /** + * Number of bi-directional links created for each node during construction + */ + M: 16, + + /** + * Size of the dynamic list during index construction + */ + EF_CONSTRUCT: 100, + + /** + * Use full scan for collections smaller than this threshold + */ + FULL_SCAN_THRESHOLD: 10000, + + /** + * Maximum number of threads for indexing (0 = use all available CPU cores) + */ + MAX_INDEXING_THREADS: 0, + + /** + * Payload index configuration (null = use default) + */ + PAYLOAD_M: null, +} as const + +/** + * Optimizer configuration constants for memory-mapped storage + */ +export const QDRANT_OPTIMIZER_CONFIG_DEFAULTS = { + /** + * Trigger optimization when this percentage of vectors are deleted + */ + DELETED_THRESHOLD: 0.2, + + /** + * Minimum number of vectors before vacuum operation + */ + VACUUM_MIN_VECTOR_NUMBER: 1000, + + /** + * Default number of segments to create + */ + DEFAULT_SEGMENT_NUMBER: 2, + + /** + * Maximum segment size (null = no limit) + */ + MAX_SEGMENT_SIZE: null, + + /** + * Start indexing after this many vectors + */ + INDEXING_THRESHOLD: 20000, + + /** + * Flush to disk interval in seconds + */ + FLUSH_INTERVAL_SEC: 5, + + /** + * Maximum optimization threads (0 = use all available CPU cores) + */ + MAX_OPTIMIZATION_THREADS: 0, +} as const + +/** + * Quantization configuration for additional memory efficiency + */ +export const QDRANT_QUANTIZATION_CONFIG_DEFAULTS = { + /** + * Enable quantization for memory efficiency + */ + IGNORE: false, + + /** + * Rescore with original vectors for accuracy + */ + RESCORE: true, + + /** + * Oversample to maintain quality + */ + OVERSAMPLING: 2.0, +} as const + +/** + * Memory optimization configuration interface + */ +export interface QdrantMemoryOptimizationConfig { + /** + * Enable on-disk storage for vectors and indexes + */ + useOnDiskStorage?: boolean + + /** + * Number of vectors before using memory-mapped files + */ + memoryMapThreshold?: number + + /** + * HNSW search parameter (ef) - controls search quality vs memory usage + */ + hnswEfSearch?: number +} diff --git a/src/services/code-index/__tests__/config-manager.spec.ts b/src/services/code-index/__tests__/config-manager.spec.ts index 2d6e704d76..e62923c63f 100644 --- a/src/services/code-index/__tests__/config-manager.spec.ts +++ b/src/services/code-index/__tests__/config-manager.spec.ts @@ -104,11 +104,18 @@ describe("CodeIndexConfigManager", () => { isConfigured: false, embedderProvider: "openai", modelId: undefined, + modelDimension: undefined, openAiOptions: { openAiNativeApiKey: "" }, ollamaOptions: { ollamaBaseUrl: "" }, + openAiCompatibleOptions: undefined, + geminiOptions: undefined, + mistralOptions: undefined, qdrantUrl: "http://localhost:6333", qdrantApiKey: "", searchMinScore: 0.4, + useOnDiskStorage: true, + memoryMapThreshold: 50000, + hnswEfSearch: 128, }) expect(result.requiresRestart).toBe(false) }) @@ -135,11 +142,18 @@ describe("CodeIndexConfigManager", () => { isConfigured: true, embedderProvider: "openai", modelId: "text-embedding-3-large", + modelDimension: undefined, openAiOptions: { openAiNativeApiKey: "test-openai-key" }, ollamaOptions: { ollamaBaseUrl: "" }, + openAiCompatibleOptions: undefined, + geminiOptions: undefined, + mistralOptions: undefined, qdrantUrl: "http://qdrant.local", qdrantApiKey: "test-qdrant-key", searchMinScore: 0.4, + useOnDiskStorage: true, + memoryMapThreshold: 50000, + hnswEfSearch: 128, }) }) @@ -168,15 +182,21 @@ describe("CodeIndexConfigManager", () => { isConfigured: true, embedderProvider: "openai-compatible", modelId: "text-embedding-3-large", + modelDimension: undefined, openAiOptions: { openAiNativeApiKey: "" }, ollamaOptions: { ollamaBaseUrl: "" }, openAiCompatibleOptions: { baseUrl: "https://api.example.com/v1", apiKey: "test-openai-compatible-key", }, + geminiOptions: undefined, + mistralOptions: undefined, qdrantUrl: "http://qdrant.local", qdrantApiKey: "test-qdrant-key", searchMinScore: 0.4, + useOnDiskStorage: true, + memoryMapThreshold: 50000, + hnswEfSearch: 128, }) }) @@ -212,9 +232,14 @@ describe("CodeIndexConfigManager", () => { baseUrl: "https://api.example.com/v1", apiKey: "test-openai-compatible-key", }, + geminiOptions: undefined, + mistralOptions: undefined, qdrantUrl: "http://qdrant.local", qdrantApiKey: "test-qdrant-key", searchMinScore: 0.4, + useOnDiskStorage: true, + memoryMapThreshold: 50000, + hnswEfSearch: 128, }) }) @@ -243,6 +268,7 @@ describe("CodeIndexConfigManager", () => { isConfigured: true, embedderProvider: "openai-compatible", modelId: "custom-model", + modelDimension: undefined, openAiOptions: { openAiNativeApiKey: "" }, ollamaOptions: { ollamaBaseUrl: "" }, openAiCompatibleOptions: { @@ -250,9 +276,14 @@ describe("CodeIndexConfigManager", () => { apiKey: "test-openai-compatible-key", // modelDimension is undefined when not set }, + geminiOptions: undefined, + mistralOptions: undefined, qdrantUrl: "http://qdrant.local", qdrantApiKey: "test-qdrant-key", searchMinScore: 0.4, + useOnDiskStorage: true, + memoryMapThreshold: 50000, + hnswEfSearch: 128, }) }) @@ -289,9 +320,13 @@ describe("CodeIndexConfigManager", () => { apiKey: "test-openai-compatible-key", }, geminiOptions: undefined, + mistralOptions: undefined, qdrantUrl: "http://qdrant.local", qdrantApiKey: "test-qdrant-key", searchMinScore: 0.4, + useOnDiskStorage: true, + memoryMapThreshold: 50000, + hnswEfSearch: 128, }) }) @@ -1292,14 +1327,19 @@ describe("CodeIndexConfigManager", () => { isConfigured: true, embedderProvider: "openai", modelId: "text-embedding-3-large", + modelDimension: undefined, openAiOptions: { openAiNativeApiKey: "test-openai-key" }, ollamaOptions: { ollamaBaseUrl: undefined }, geminiOptions: undefined, openAiCompatibleOptions: undefined, + mistralOptions: undefined, qdrantUrl: "http://qdrant.local", qdrantApiKey: "test-qdrant-key", searchMinScore: 0.4, searchMaxResults: 50, + useOnDiskStorage: true, + memoryMapThreshold: 50000, + hnswEfSearch: 128, }) }) diff --git a/src/services/code-index/__tests__/service-factory.spec.ts b/src/services/code-index/__tests__/service-factory.spec.ts index 1d8f7ba478..3fa3aaa2a1 100644 --- a/src/services/code-index/__tests__/service-factory.spec.ts +++ b/src/services/code-index/__tests__/service-factory.spec.ts @@ -49,6 +49,11 @@ describe("CodeIndexServiceFactory", () => { mockConfigManager = { getConfig: vitest.fn(), + memoryOptimizationConfig: { + useOnDiskStorage: true, + memoryMapThreshold: 50000, + hnswEfSearch: 128, + }, } mockCacheManager = {} @@ -367,6 +372,11 @@ describe("CodeIndexServiceFactory", () => { "http://localhost:6333", 3072, "test-key", + { + useOnDiskStorage: true, + memoryMapThreshold: 50000, + hnswEfSearch: 128, + }, ) }) @@ -392,6 +402,11 @@ describe("CodeIndexServiceFactory", () => { "http://localhost:6333", 768, "test-key", + { + useOnDiskStorage: true, + memoryMapThreshold: 50000, + hnswEfSearch: 128, + }, ) }) @@ -417,6 +432,11 @@ describe("CodeIndexServiceFactory", () => { "http://localhost:6333", 3072, "test-key", + { + useOnDiskStorage: true, + memoryMapThreshold: 50000, + hnswEfSearch: 128, + }, ) }) @@ -449,6 +469,11 @@ describe("CodeIndexServiceFactory", () => { "http://localhost:6333", modelDimension, // Should use model's built-in dimension, not manual "test-key", + { + useOnDiskStorage: true, + memoryMapThreshold: 50000, + hnswEfSearch: 128, + }, ) }) @@ -480,6 +505,11 @@ describe("CodeIndexServiceFactory", () => { "http://localhost:6333", manualDimension, // Should use manual dimension as fallback "test-key", + { + useOnDiskStorage: true, + memoryMapThreshold: 50000, + hnswEfSearch: 128, + }, ) }) @@ -509,6 +539,11 @@ describe("CodeIndexServiceFactory", () => { "http://localhost:6333", 768, "test-key", + { + useOnDiskStorage: true, + memoryMapThreshold: 50000, + hnswEfSearch: 128, + }, ) }) @@ -578,6 +613,11 @@ describe("CodeIndexServiceFactory", () => { "http://localhost:6333", 3072, "test-key", + { + useOnDiskStorage: true, + memoryMapThreshold: 50000, + hnswEfSearch: 128, + }, ) }) @@ -603,6 +643,11 @@ describe("CodeIndexServiceFactory", () => { "http://localhost:6333", 3072, "test-key", + { + useOnDiskStorage: true, + memoryMapThreshold: 50000, + hnswEfSearch: 128, + }, ) }) @@ -627,6 +672,11 @@ describe("CodeIndexServiceFactory", () => { "http://localhost:6333", 1536, "test-key", + { + useOnDiskStorage: true, + memoryMapThreshold: 50000, + hnswEfSearch: 128, + }, ) }) diff --git a/src/services/code-index/config-manager.ts b/src/services/code-index/config-manager.ts index 1723f1c2a0..35c72ff1f4 100644 --- a/src/services/code-index/config-manager.ts +++ b/src/services/code-index/config-manager.ts @@ -4,6 +4,7 @@ import { EmbedderProvider } from "./interfaces/manager" import { CodeIndexConfig, PreviousConfigSnapshot } from "./interfaces/config" import { DEFAULT_SEARCH_MIN_SCORE, DEFAULT_MAX_SEARCH_RESULTS } from "./constants" import { getDefaultModelId, getModelDimension, getModelScoreThreshold } from "../../shared/embeddingModels" +import { QDRANT_MEMORY_OPTIMIZATION_DEFAULTS } from "@roo-code/types" /** * Manages configuration state and validation for the code indexing feature. @@ -23,6 +24,10 @@ export class CodeIndexConfigManager { private qdrantApiKey?: string private searchMinScore?: number private searchMaxResults?: number + // Memory optimization settings + private useOnDiskStorage?: boolean + private memoryMapThreshold?: number + private hnswEfSearch?: number constructor(private readonly contextProxy: ContextProxy) { // Initialize with current configuration to avoid false restart triggers @@ -50,6 +55,9 @@ export class CodeIndexConfigManager { codebaseIndexEmbedderModelId: "", codebaseIndexSearchMinScore: undefined, codebaseIndexSearchMaxResults: undefined, + codebaseIndexUseOnDiskStorage: QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.USE_ON_DISK_STORAGE, + codebaseIndexMemoryMapThreshold: QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.MEMORY_MAP_THRESHOLD, + codebaseIndexHnswEfSearch: QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.HNSW_EF_SEARCH, } const { @@ -60,6 +68,9 @@ export class CodeIndexConfigManager { codebaseIndexEmbedderModelId, codebaseIndexSearchMinScore, codebaseIndexSearchMaxResults, + codebaseIndexUseOnDiskStorage, + codebaseIndexMemoryMapThreshold, + codebaseIndexHnswEfSearch, } = codebaseIndexConfig const openAiKey = this.contextProxy?.getSecret("codeIndexOpenAiKey") ?? "" @@ -76,6 +87,10 @@ export class CodeIndexConfigManager { this.qdrantApiKey = qdrantApiKey ?? "" this.searchMinScore = codebaseIndexSearchMinScore this.searchMaxResults = codebaseIndexSearchMaxResults + this.useOnDiskStorage = codebaseIndexUseOnDiskStorage ?? QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.USE_ON_DISK_STORAGE + this.memoryMapThreshold = + codebaseIndexMemoryMapThreshold ?? QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.MEMORY_MAP_THRESHOLD + this.hnswEfSearch = codebaseIndexHnswEfSearch ?? QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.HNSW_EF_SEARCH // Validate and set model dimension const rawDimension = codebaseIndexConfig.codebaseIndexEmbedderModelDimension @@ -144,6 +159,9 @@ export class CodeIndexConfigManager { qdrantUrl?: string qdrantApiKey?: string searchMinScore?: number + useOnDiskStorage?: boolean + memoryMapThreshold?: number + hnswEfSearch?: number } requiresRestart: boolean }> { @@ -187,6 +205,9 @@ export class CodeIndexConfigManager { qdrantUrl: this.qdrantUrl, qdrantApiKey: this.qdrantApiKey, searchMinScore: this.currentSearchMinScore, + useOnDiskStorage: this.useOnDiskStorage, + memoryMapThreshold: this.memoryMapThreshold, + hnswEfSearch: this.hnswEfSearch, }, requiresRestart, } @@ -379,6 +400,9 @@ export class CodeIndexConfigManager { qdrantApiKey: this.qdrantApiKey, searchMinScore: this.currentSearchMinScore, searchMaxResults: this.currentSearchMaxResults, + useOnDiskStorage: this.useOnDiskStorage, + memoryMapThreshold: this.memoryMapThreshold, + hnswEfSearch: this.hnswEfSearch, } } @@ -413,6 +437,21 @@ export class CodeIndexConfigManager { } } + /** + * Gets the memory optimization settings + */ + public get memoryOptimizationConfig(): { + useOnDiskStorage?: boolean + memoryMapThreshold?: number + hnswEfSearch?: number + } { + return { + useOnDiskStorage: this.useOnDiskStorage, + memoryMapThreshold: this.memoryMapThreshold, + hnswEfSearch: this.hnswEfSearch, + } + } + /** * Gets the current model ID being used for embeddings. */ diff --git a/src/services/code-index/interfaces/config.ts b/src/services/code-index/interfaces/config.ts index 9098a60091..790ca215db 100644 --- a/src/services/code-index/interfaces/config.ts +++ b/src/services/code-index/interfaces/config.ts @@ -1,5 +1,6 @@ import { ApiHandlerOptions } from "../../../shared/api" // Adjust path if needed import { EmbedderProvider } from "./manager" +import { QdrantMemoryOptimizationConfig } from "@roo-code/types" /** * Configuration state for the code indexing feature @@ -18,6 +19,10 @@ export interface CodeIndexConfig { qdrantApiKey?: string searchMinScore?: number searchMaxResults?: number + // Memory optimization settings + useOnDiskStorage?: boolean + memoryMapThreshold?: number + hnswEfSearch?: number } /** @@ -37,4 +42,8 @@ export type PreviousConfigSnapshot = { mistralApiKey?: string qdrantUrl?: string qdrantApiKey?: string + // Memory optimization settings + useOnDiskStorage?: boolean + memoryMapThreshold?: number + hnswEfSearch?: number } diff --git a/src/services/code-index/service-factory.ts b/src/services/code-index/service-factory.ts index 68b0f5c0bc..f6fca593e2 100644 --- a/src/services/code-index/service-factory.ts +++ b/src/services/code-index/service-factory.ts @@ -136,8 +136,17 @@ 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) + // Get memory optimization config from config manager + const memoryOptimization = this.configManager.memoryOptimizationConfig + + // Create QdrantVectorStore with memory optimization settings + return new QdrantVectorStore( + this.workspacePath, + config.qdrantUrl, + vectorSize, + config.qdrantApiKey, + memoryOptimization, + ) } /** diff --git a/src/services/code-index/vector-store/__tests__/qdrant-client.spec.ts b/src/services/code-index/vector-store/__tests__/qdrant-client.spec.ts index e539c2edde..4c03d901a9 100644 --- a/src/services/code-index/vector-store/__tests__/qdrant-client.spec.ts +++ b/src/services/code-index/vector-store/__tests__/qdrant-client.spec.ts @@ -528,6 +528,25 @@ describe("QdrantVectorStore", () => { vectors: { size: mockVectorSize, distance: "Cosine", // Assuming 'Cosine' is the DISTANCE_METRIC + on_disk: true, // Default memory optimization + }, + hnsw_config: { + m: 16, + ef_construct: 100, + full_scan_threshold: 10000, + max_indexing_threads: 0, + on_disk: true, + payload_m: null, + }, + optimizers_config: { + deleted_threshold: 0.2, + vacuum_min_vector_number: 1000, + default_segment_number: 2, + max_segment_size: null, + memmap_threshold: 50000, + indexing_threshold: 20000, + flush_interval_sec: 5, + max_optimization_threads: 0, }, }) expect(mockQdrantClientInstance.deleteCollection).not.toHaveBeenCalled() @@ -606,6 +625,25 @@ describe("QdrantVectorStore", () => { vectors: { size: mockVectorSize, // Should use the new, correct vector size distance: "Cosine", + on_disk: true, // Default memory optimization + }, + hnsw_config: { + m: 16, + ef_construct: 100, + full_scan_threshold: 10000, + max_indexing_threads: 0, + on_disk: true, + payload_m: null, + }, + optimizers_config: { + deleted_threshold: 0.2, + vacuum_min_vector_number: 1000, + default_segment_number: 2, + max_segment_size: null, + memmap_threshold: 50000, + indexing_threshold: 20000, + flush_interval_sec: 5, + max_optimization_threads: 0, }, }) @@ -899,6 +937,25 @@ describe("QdrantVectorStore", () => { vectors: { size: newVectorSize, // Should create with new 768 dimensions distance: "Cosine", + on_disk: true, // Default memory optimization + }, + hnsw_config: { + m: 16, + ef_construct: 100, + full_scan_threshold: 10000, + max_indexing_threads: 0, + on_disk: true, + payload_m: null, + }, + optimizers_config: { + deleted_threshold: 0.2, + vacuum_min_vector_number: 1000, + default_segment_number: 2, + max_segment_size: null, + memmap_threshold: 50000, + indexing_threshold: 20000, + flush_interval_sec: 5, + max_optimization_threads: 0, }, }) expect(mockQdrantClientInstance.createPayloadIndex).toHaveBeenCalledTimes(5) @@ -1244,8 +1301,13 @@ describe("QdrantVectorStore", () => { score_threshold: DEFAULT_SEARCH_MIN_SCORE, limit: DEFAULT_MAX_SEARCH_RESULTS, params: { - hnsw_ef: 128, + hnsw_ef: 128, // Default memory optimized value exact: false, + quantization: { + ignore: false, + rescore: true, + oversampling: 2.0, + }, }, with_payload: { include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"], @@ -1295,8 +1357,13 @@ describe("QdrantVectorStore", () => { score_threshold: DEFAULT_SEARCH_MIN_SCORE, limit: DEFAULT_MAX_SEARCH_RESULTS, params: { - hnsw_ef: 128, + hnsw_ef: 128, // Default memory optimized value exact: false, + quantization: { + ignore: false, + rescore: true, + oversampling: 2.0, + }, }, with_payload: { include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"], @@ -1321,8 +1388,13 @@ describe("QdrantVectorStore", () => { score_threshold: customMinScore, limit: DEFAULT_MAX_SEARCH_RESULTS, params: { - hnsw_ef: 128, + hnsw_ef: 128, // Default memory optimized value exact: false, + quantization: { + ignore: false, + rescore: true, + oversampling: 2.0, + }, }, with_payload: { include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"], @@ -1345,8 +1417,13 @@ describe("QdrantVectorStore", () => { score_threshold: DEFAULT_SEARCH_MIN_SCORE, limit: customMaxResults, params: { - hnsw_ef: 128, + hnsw_ef: 128, // Default memory optimized value exact: false, + quantization: { + ignore: false, + rescore: true, + oversampling: 2.0, + }, }, with_payload: { include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"], @@ -1492,8 +1569,13 @@ describe("QdrantVectorStore", () => { score_threshold: DEFAULT_SEARCH_MIN_SCORE, limit: DEFAULT_MAX_SEARCH_RESULTS, params: { - hnsw_ef: 128, + hnsw_ef: 128, // Default memory optimized value exact: false, + quantization: { + ignore: false, + rescore: true, + oversampling: 2.0, + }, }, with_payload: { include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"], diff --git a/src/services/code-index/vector-store/qdrant-client.ts b/src/services/code-index/vector-store/qdrant-client.ts index 5121d65b97..e04fca26ca 100644 --- a/src/services/code-index/vector-store/qdrant-client.ts +++ b/src/services/code-index/vector-store/qdrant-client.ts @@ -6,6 +6,13 @@ import { IVectorStore } from "../interfaces/vector-store" import { Payload, VectorStoreSearchResult } from "../interfaces" import { DEFAULT_MAX_SEARCH_RESULTS, DEFAULT_SEARCH_MIN_SCORE } from "../constants" import { t } from "../../../i18n" +import { + QdrantMemoryOptimizationConfig, + QDRANT_MEMORY_OPTIMIZATION_DEFAULTS, + QDRANT_HNSW_CONFIG_DEFAULTS, + QDRANT_OPTIMIZER_CONFIG_DEFAULTS, + QDRANT_QUANTIZATION_CONFIG_DEFAULTS, +} from "@roo-code/types" /** * Qdrant implementation of the vector store interface @@ -17,13 +24,23 @@ export class QdrantVectorStore implements IVectorStore { private client: QdrantClient private readonly collectionName: string private readonly qdrantUrl: string = "http://localhost:6333" + private readonly memoryOptimization: QdrantMemoryOptimizationConfig /** * Creates a new Qdrant vector store * @param workspacePath Path to the workspace * @param url Optional URL to the Qdrant server + * @param vectorSize Size of the vectors + * @param apiKey Optional API key for authentication + * @param memoryOptimization Optional memory optimization settings */ - constructor(workspacePath: string, url: string, vectorSize: number, apiKey?: string) { + constructor( + workspacePath: string, + url: string, + vectorSize: number, + apiKey?: string, + memoryOptimization?: QdrantMemoryOptimizationConfig, + ) { // Parse the URL to determine the appropriate QdrantClient configuration const parsedUrl = this.parseQdrantUrl(url) @@ -79,6 +96,11 @@ export class QdrantVectorStore implements IVectorStore { const hash = createHash("sha256").update(workspacePath).digest("hex") this.vectorSize = vectorSize this.collectionName = `ws-${hash.substring(0, 16)}` + this.memoryOptimization = memoryOptimization || { + useOnDiskStorage: QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.USE_ON_DISK_STORAGE, + memoryMapThreshold: QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.MEMORY_MAP_THRESHOLD, + hnswEfSearch: QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.HNSW_EF_SEARCH, + } } /** @@ -155,6 +177,33 @@ export class QdrantVectorStore implements IVectorStore { vectors: { size: this.vectorSize, distance: this.DISTANCE_METRIC, + on_disk: + this.memoryOptimization.useOnDiskStorage ?? + QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.USE_ON_DISK_STORAGE, + }, + // Configure HNSW index for memory efficiency + hnsw_config: { + m: QDRANT_HNSW_CONFIG_DEFAULTS.M, + ef_construct: QDRANT_HNSW_CONFIG_DEFAULTS.EF_CONSTRUCT, + full_scan_threshold: QDRANT_HNSW_CONFIG_DEFAULTS.FULL_SCAN_THRESHOLD, + max_indexing_threads: QDRANT_HNSW_CONFIG_DEFAULTS.MAX_INDEXING_THREADS, + on_disk: + this.memoryOptimization.useOnDiskStorage ?? + QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.USE_ON_DISK_STORAGE, + payload_m: QDRANT_HNSW_CONFIG_DEFAULTS.PAYLOAD_M, + }, + // Enable memory-mapped storage for better memory management + optimizers_config: { + deleted_threshold: QDRANT_OPTIMIZER_CONFIG_DEFAULTS.DELETED_THRESHOLD, + vacuum_min_vector_number: QDRANT_OPTIMIZER_CONFIG_DEFAULTS.VACUUM_MIN_VECTOR_NUMBER, + default_segment_number: QDRANT_OPTIMIZER_CONFIG_DEFAULTS.DEFAULT_SEGMENT_NUMBER, + max_segment_size: QDRANT_OPTIMIZER_CONFIG_DEFAULTS.MAX_SEGMENT_SIZE, + memmap_threshold: + this.memoryOptimization.memoryMapThreshold ?? + QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.MEMORY_MAP_THRESHOLD, + indexing_threshold: QDRANT_OPTIMIZER_CONFIG_DEFAULTS.INDEXING_THRESHOLD, + flush_interval_sec: QDRANT_OPTIMIZER_CONFIG_DEFAULTS.FLUSH_INTERVAL_SEC, + max_optimization_threads: QDRANT_OPTIMIZER_CONFIG_DEFAULTS.MAX_OPTIMIZATION_THREADS, }, }) created = true @@ -244,6 +293,33 @@ export class QdrantVectorStore implements IVectorStore { vectors: { size: this.vectorSize, distance: this.DISTANCE_METRIC, + on_disk: + this.memoryOptimization.useOnDiskStorage ?? + QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.USE_ON_DISK_STORAGE, + }, + // Configure HNSW index for memory efficiency + hnsw_config: { + m: QDRANT_HNSW_CONFIG_DEFAULTS.M, + ef_construct: QDRANT_HNSW_CONFIG_DEFAULTS.EF_CONSTRUCT, + full_scan_threshold: QDRANT_HNSW_CONFIG_DEFAULTS.FULL_SCAN_THRESHOLD, + max_indexing_threads: QDRANT_HNSW_CONFIG_DEFAULTS.MAX_INDEXING_THREADS, + on_disk: + this.memoryOptimization.useOnDiskStorage ?? + QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.USE_ON_DISK_STORAGE, + payload_m: QDRANT_HNSW_CONFIG_DEFAULTS.PAYLOAD_M, + }, + // Enable memory-mapped storage for better memory management + optimizers_config: { + deleted_threshold: QDRANT_OPTIMIZER_CONFIG_DEFAULTS.DELETED_THRESHOLD, + vacuum_min_vector_number: QDRANT_OPTIMIZER_CONFIG_DEFAULTS.VACUUM_MIN_VECTOR_NUMBER, + default_segment_number: QDRANT_OPTIMIZER_CONFIG_DEFAULTS.DEFAULT_SEGMENT_NUMBER, + max_segment_size: QDRANT_OPTIMIZER_CONFIG_DEFAULTS.MAX_SEGMENT_SIZE, + memmap_threshold: + this.memoryOptimization.memoryMapThreshold ?? + QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.MEMORY_MAP_THRESHOLD, + indexing_threshold: QDRANT_OPTIMIZER_CONFIG_DEFAULTS.INDEXING_THRESHOLD, + flush_interval_sec: QDRANT_OPTIMIZER_CONFIG_DEFAULTS.FLUSH_INTERVAL_SEC, + max_optimization_threads: QDRANT_OPTIMIZER_CONFIG_DEFAULTS.MAX_OPTIMIZATION_THREADS, }, }) console.log(`[QdrantVectorStore] Successfully created new collection ${this.collectionName}`) @@ -391,8 +467,13 @@ export class QdrantVectorStore implements IVectorStore { score_threshold: minScore ?? DEFAULT_SEARCH_MIN_SCORE, limit: maxResults ?? DEFAULT_MAX_SEARCH_RESULTS, params: { - hnsw_ef: 128, + hnsw_ef: this.memoryOptimization.hnswEfSearch ?? QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.HNSW_EF_SEARCH, exact: false, + quantization: { + ignore: QDRANT_QUANTIZATION_CONFIG_DEFAULTS.IGNORE, + rescore: QDRANT_QUANTIZATION_CONFIG_DEFAULTS.RESCORE, + oversampling: QDRANT_QUANTIZATION_CONFIG_DEFAULTS.OVERSAMPLING, + }, }, with_payload: { include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"],