From dab65f3164ed3661e3bdf667ab0ca52af6f115cb Mon Sep 17 00:00:00 2001 From: Roo Code Date: Sun, 27 Jul 2025 01:10:08 +0000 Subject: [PATCH] fix: Configure Qdrant to use on-disk storage for memory efficiency - Add memory optimization configuration to QdrantVectorStore - Configure vectors and HNSW indexes to use on-disk storage by default - Add memory-mapped file support for segments larger than 50k vectors - Reduce HNSW search parameter (ef) from 128 to 64 for memory efficiency - Add configuration options for memory optimization settings - Update type definitions to include new configuration options - Update all affected tests to handle new constructor parameters Fixes #6262 --- packages/types/src/codebase-index.ts | 4 + .../__tests__/config-manager.spec.ts | 40 ++++++++ .../__tests__/service-factory.spec.ts | 50 ++++++++++ src/services/code-index/config-manager.ts | 37 ++++++++ src/services/code-index/interfaces/config.ts | 8 ++ src/services/code-index/service-factory.ts | 13 ++- .../__tests__/qdrant-client.spec.ts | 92 ++++++++++++++++++- .../code-index/vector-store/qdrant-client.ts | 74 ++++++++++++++- 8 files changed, 309 insertions(+), 9 deletions(-) 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/src/services/code-index/__tests__/config-manager.spec.ts b/src/services/code-index/__tests__/config-manager.spec.ts index 2d6e704d76..3cbeaaa6f3 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: 64, }) 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: 64, }) }) @@ -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: 64, }) }) @@ -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: 64, }) }) @@ -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: 64, }) }) @@ -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: 64, }) }) @@ -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: 64, }) }) diff --git a/src/services/code-index/__tests__/service-factory.spec.ts b/src/services/code-index/__tests__/service-factory.spec.ts index 1d8f7ba478..0df044b928 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: 64, + }, } mockCacheManager = {} @@ -367,6 +372,11 @@ describe("CodeIndexServiceFactory", () => { "http://localhost:6333", 3072, "test-key", + { + useOnDiskStorage: true, + memoryMapThreshold: 50000, + hnswEfSearch: 64, + }, ) }) @@ -392,6 +402,11 @@ describe("CodeIndexServiceFactory", () => { "http://localhost:6333", 768, "test-key", + { + useOnDiskStorage: true, + memoryMapThreshold: 50000, + hnswEfSearch: 64, + }, ) }) @@ -417,6 +432,11 @@ describe("CodeIndexServiceFactory", () => { "http://localhost:6333", 3072, "test-key", + { + useOnDiskStorage: true, + memoryMapThreshold: 50000, + hnswEfSearch: 64, + }, ) }) @@ -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: 64, + }, ) }) @@ -480,6 +505,11 @@ describe("CodeIndexServiceFactory", () => { "http://localhost:6333", manualDimension, // Should use manual dimension as fallback "test-key", + { + useOnDiskStorage: true, + memoryMapThreshold: 50000, + hnswEfSearch: 64, + }, ) }) @@ -509,6 +539,11 @@ describe("CodeIndexServiceFactory", () => { "http://localhost:6333", 768, "test-key", + { + useOnDiskStorage: true, + memoryMapThreshold: 50000, + hnswEfSearch: 64, + }, ) }) @@ -578,6 +613,11 @@ describe("CodeIndexServiceFactory", () => { "http://localhost:6333", 3072, "test-key", + { + useOnDiskStorage: true, + memoryMapThreshold: 50000, + hnswEfSearch: 64, + }, ) }) @@ -603,6 +643,11 @@ describe("CodeIndexServiceFactory", () => { "http://localhost:6333", 3072, "test-key", + { + useOnDiskStorage: true, + memoryMapThreshold: 50000, + hnswEfSearch: 64, + }, ) }) @@ -627,6 +672,11 @@ describe("CodeIndexServiceFactory", () => { "http://localhost:6333", 1536, "test-key", + { + useOnDiskStorage: true, + memoryMapThreshold: 50000, + hnswEfSearch: 64, + }, ) }) diff --git a/src/services/code-index/config-manager.ts b/src/services/code-index/config-manager.ts index 1723f1c2a0..fe9b934516 100644 --- a/src/services/code-index/config-manager.ts +++ b/src/services/code-index/config-manager.ts @@ -23,6 +23,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 +54,9 @@ export class CodeIndexConfigManager { codebaseIndexEmbedderModelId: "", codebaseIndexSearchMinScore: undefined, codebaseIndexSearchMaxResults: undefined, + codebaseIndexUseOnDiskStorage: true, // Default to true for memory efficiency + codebaseIndexMemoryMapThreshold: 50000, // Default 50k vectors + codebaseIndexHnswEfSearch: 64, // Default 64 for balance } const { @@ -60,6 +67,9 @@ export class CodeIndexConfigManager { codebaseIndexEmbedderModelId, codebaseIndexSearchMinScore, codebaseIndexSearchMaxResults, + codebaseIndexUseOnDiskStorage, + codebaseIndexMemoryMapThreshold, + codebaseIndexHnswEfSearch, } = codebaseIndexConfig const openAiKey = this.contextProxy?.getSecret("codeIndexOpenAiKey") ?? "" @@ -76,6 +86,9 @@ export class CodeIndexConfigManager { this.qdrantApiKey = qdrantApiKey ?? "" this.searchMinScore = codebaseIndexSearchMinScore this.searchMaxResults = codebaseIndexSearchMaxResults + this.useOnDiskStorage = codebaseIndexUseOnDiskStorage ?? true + this.memoryMapThreshold = codebaseIndexMemoryMapThreshold ?? 50000 + this.hnswEfSearch = codebaseIndexHnswEfSearch ?? 64 // Validate and set model dimension const rawDimension = codebaseIndexConfig.codebaseIndexEmbedderModelDimension @@ -144,6 +157,9 @@ export class CodeIndexConfigManager { qdrantUrl?: string qdrantApiKey?: string searchMinScore?: number + useOnDiskStorage?: boolean + memoryMapThreshold?: number + hnswEfSearch?: number } requiresRestart: boolean }> { @@ -187,6 +203,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 +398,9 @@ export class CodeIndexConfigManager { qdrantApiKey: this.qdrantApiKey, searchMinScore: this.currentSearchMinScore, searchMaxResults: this.currentSearchMaxResults, + useOnDiskStorage: this.useOnDiskStorage, + memoryMapThreshold: this.memoryMapThreshold, + hnswEfSearch: this.hnswEfSearch, } } @@ -413,6 +435,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..9dac38adcd 100644 --- a/src/services/code-index/interfaces/config.ts +++ b/src/services/code-index/interfaces/config.ts @@ -18,6 +18,10 @@ export interface CodeIndexConfig { qdrantApiKey?: string searchMinScore?: number searchMaxResults?: number + // Memory optimization settings + useOnDiskStorage?: boolean // Enable on-disk storage for vectors and indexes + memoryMapThreshold?: number // Number of vectors before using memory-mapped files + hnswEfSearch?: number // HNSW search parameter (lower = less memory, faster) } /** @@ -37,4 +41,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..9d8a5dd06f 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: 64, // 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: 64, // 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: 64, // 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: 64, // 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: 64, // 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..ddbc4973e4 100644 --- a/src/services/code-index/vector-store/qdrant-client.ts +++ b/src/services/code-index/vector-store/qdrant-client.ts @@ -17,13 +17,31 @@ export class QdrantVectorStore implements IVectorStore { private client: QdrantClient private readonly collectionName: string private readonly qdrantUrl: string = "http://localhost:6333" + private readonly memoryOptimization: { + useOnDiskStorage?: boolean + memoryMapThreshold?: number + hnswEfSearch?: number + } /** * 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?: { + useOnDiskStorage?: boolean + memoryMapThreshold?: number + hnswEfSearch?: number + }, + ) { // Parse the URL to determine the appropriate QdrantClient configuration const parsedUrl = this.parseQdrantUrl(url) @@ -79,6 +97,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: true, + memoryMapThreshold: 50000, + hnswEfSearch: 64, + } } /** @@ -155,6 +178,27 @@ export class QdrantVectorStore implements IVectorStore { vectors: { size: this.vectorSize, distance: this.DISTANCE_METRIC, + on_disk: this.memoryOptimization.useOnDiskStorage ?? true, // Store vectors on disk instead of RAM + }, + // Configure HNSW index for memory efficiency + hnsw_config: { + m: 16, // Number of bi-directional links created for each node + ef_construct: 100, // Size of the dynamic list during construction + full_scan_threshold: 10000, // Use full scan for small collections + max_indexing_threads: 0, // Use all available CPU cores + on_disk: this.memoryOptimization.useOnDiskStorage ?? true, // Store HNSW index on disk + payload_m: null, // Use default payload index + }, + // Enable memory-mapped storage for better memory management + optimizers_config: { + deleted_threshold: 0.2, // Trigger optimization when 20% vectors are deleted + vacuum_min_vector_number: 1000, // Minimum vectors before vacuum + default_segment_number: 2, // Number of segments to create + max_segment_size: null, // No limit on segment size + memmap_threshold: this.memoryOptimization.memoryMapThreshold ?? 50000, // Use mmap for segments larger than this + indexing_threshold: 20000, // Start indexing after 20k vectors + flush_interval_sec: 5, // Flush to disk every 5 seconds + max_optimization_threads: 0, // Use all available CPU cores }, }) created = true @@ -244,6 +288,27 @@ export class QdrantVectorStore implements IVectorStore { vectors: { size: this.vectorSize, distance: this.DISTANCE_METRIC, + on_disk: this.memoryOptimization.useOnDiskStorage ?? true, // Store vectors on disk instead of RAM + }, + // Configure HNSW index for memory efficiency + hnsw_config: { + m: 16, // Number of bi-directional links created for each node + ef_construct: 100, // Size of the dynamic list during construction + full_scan_threshold: 10000, // Use full scan for small collections + max_indexing_threads: 0, // Use all available CPU cores + on_disk: this.memoryOptimization.useOnDiskStorage ?? true, // Store HNSW index on disk + payload_m: null, // Use default payload index + }, + // Enable memory-mapped storage for better memory management + optimizers_config: { + deleted_threshold: 0.2, // Trigger optimization when 20% vectors are deleted + vacuum_min_vector_number: 1000, // Minimum vectors before vacuum + default_segment_number: 2, // Number of segments to create + max_segment_size: null, // No limit on segment size + memmap_threshold: this.memoryOptimization.memoryMapThreshold ?? 50000, // Use mmap for segments larger than this + indexing_threshold: 20000, // Start indexing after 20k vectors + flush_interval_sec: 5, // Flush to disk every 5 seconds + max_optimization_threads: 0, // Use all available CPU cores }, }) console.log(`[QdrantVectorStore] Successfully created new collection ${this.collectionName}`) @@ -391,8 +456,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 ?? 64, // Use configured value or default exact: false, + quantization: { + ignore: false, // Enable quantization for memory efficiency + rescore: true, // Rescore with original vectors for accuracy + oversampling: 2.0, // Oversample to maintain quality + }, }, with_payload: { include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"],