Skip to content

Commit 4a78f51

Browse files
MuriloFPdaniel-lxs
andauthored
Feat/issue 5149 configurable max search results (#5402)
* feat: add configurable max search results for codebase indexing (#5149) - Add codebaseIndexSearchMaxResults to configuration schema with validation (10-1000) - Update Qdrant client to accept maxResults parameter in search method - Add UI slider in Experimental Settings to configure max search results - Rename constants to DEFAULT_MAX_SEARCH_RESULTS and DEFAULT_SEARCH_MIN_SCORE for clarity - Add translations for new setting across all 17 supported languages - Add comprehensive test coverage for config manager, Qdrant client, and UI components fix: settings persistence for codebase index configuration - Add new updateCodebaseIndexConfig message type to properly merge config updates - Update SettingsView to send entire codebaseIndexConfig object instead of just enabled flag - Add backend handler to merge configuration updates instead of overwriting - Add tests for the new message handler functionality This ensures the max search results setting persists correctly when saved. * fix: correct property name in updateCodebaseIndexConfig message The frontend was sending 'config' but the backend expects 'codebaseIndexConfig'. This mismatch was preventing the max search results setting from persisting. * feat: refactor codebase index constants and update search result defaults * feat(chat): add advanced settings for maximum search results configuration * refactor: remove updateCodebaseIndexConfig and integrate max search results into saveCodeIndexSettingsAtomic - Removed updateCodebaseIndexConfig message type and handler as per PR feedback - Added codebaseIndexSearchMaxResults to codeIndexSettings type in WebviewMessage.ts - Updated saveCodeIndexSettingsAtomic to save codebaseIndexSearchMaxResults - Fixed SettingsView.tsx to use codebaseIndexEnabled message instead of updateCodebaseIndexConfig * Delete webview-ui/src/components/settings/__tests__/ExperimentalSettings.spec.tsx * refactor: remove updateCodebaseIndexConfig tests to streamline codebase indexing logic * revert this --------- Co-authored-by: Daniel Riccio <[email protected]> Co-authored-by: Daniel <[email protected]>
1 parent 9da5166 commit 4a78f51

File tree

31 files changed

+291
-46
lines changed

31 files changed

+291
-46
lines changed

packages/types/src/codebase-index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
import { z } from "zod"
22

3+
/**
4+
* Codebase Index Constants
5+
*/
6+
export const CODEBASE_INDEX_DEFAULTS = {
7+
MIN_SEARCH_RESULTS: 10,
8+
MAX_SEARCH_RESULTS: 200,
9+
DEFAULT_SEARCH_RESULTS: 50,
10+
SEARCH_RESULTS_STEP: 10,
11+
DEFAULT_SEARCH_MIN_SCORE: 0.4,
12+
} as const
13+
314
/**
415
* CodebaseIndexConfig
516
*/
@@ -11,6 +22,11 @@ export const codebaseIndexConfigSchema = z.object({
1122
codebaseIndexEmbedderBaseUrl: z.string().optional(),
1223
codebaseIndexEmbedderModelId: z.string().optional(),
1324
codebaseIndexSearchMinScore: z.number().min(0).max(1).optional(),
25+
codebaseIndexSearchMaxResults: z
26+
.number()
27+
.min(CODEBASE_INDEX_DEFAULTS.MIN_SEARCH_RESULTS)
28+
.max(CODEBASE_INDEX_DEFAULTS.MAX_SEARCH_RESULTS)
29+
.optional(),
1430
})
1531

1632
export type CodebaseIndexConfig = z.infer<typeof codebaseIndexConfigSchema>

src/core/webview/webviewMessageHandler.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1840,6 +1840,7 @@ export const webviewMessageHandler = async (
18401840
codebaseIndexEmbedderModelId: settings.codebaseIndexEmbedderModelId,
18411841
codebaseIndexOpenAiCompatibleBaseUrl: settings.codebaseIndexOpenAiCompatibleBaseUrl,
18421842
codebaseIndexOpenAiCompatibleModelDimension: settings.codebaseIndexOpenAiCompatibleModelDimension,
1843+
codebaseIndexSearchMaxResults: settings.codebaseIndexSearchMaxResults,
18431844
}
18441845

18451846
// Save global state first

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

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -767,7 +767,7 @@ describe("CodeIndexConfigManager", () => {
767767
expect(configManager.currentSearchMinScore).toBe(0.15)
768768
})
769769

770-
it("should fall back to default SEARCH_MIN_SCORE when neither user setting nor model threshold exists", async () => {
770+
it("should fall back to default DEFAULT_SEARCH_MIN_SCORE when neither user setting nor model threshold exists", async () => {
771771
mockContextProxy.getGlobalState.mockReturnValue({
772772
codebaseIndexEnabled: true,
773773
codebaseIndexQdrantUrl: "http://qdrant.local",
@@ -781,7 +781,7 @@ describe("CodeIndexConfigManager", () => {
781781
})
782782

783783
await configManager.loadConfiguration()
784-
// Should fall back to default SEARCH_MIN_SCORE (0.4)
784+
// Should fall back to default DEFAULT_SEARCH_MIN_SCORE (0.4)
785785
expect(configManager.currentSearchMinScore).toBe(0.4)
786786
})
787787

@@ -881,6 +881,61 @@ describe("CodeIndexConfigManager", () => {
881881
expect(anotherManager.currentSearchMinScore).toBe(0.4) // Default
882882
})
883883
})
884+
885+
describe("currentSearchMaxResults", () => {
886+
it("should return user setting when provided, otherwise default", async () => {
887+
// Test 1: User setting takes precedence
888+
mockContextProxy.getGlobalState.mockReturnValue({
889+
codebaseIndexEnabled: true,
890+
codebaseIndexQdrantUrl: "http://qdrant.local",
891+
codebaseIndexEmbedderProvider: "openai",
892+
codebaseIndexEmbedderModelId: "text-embedding-3-small",
893+
codebaseIndexSearchMaxResults: 150, // User setting
894+
})
895+
896+
await configManager.loadConfiguration()
897+
expect(configManager.currentSearchMaxResults).toBe(150) // User setting
898+
899+
// Test 2: Default when no user setting
900+
mockContextProxy.getGlobalState.mockReturnValue({
901+
codebaseIndexEnabled: true,
902+
codebaseIndexQdrantUrl: "http://qdrant.local",
903+
codebaseIndexEmbedderProvider: "openai",
904+
codebaseIndexEmbedderModelId: "text-embedding-3-small",
905+
// No user setting
906+
})
907+
908+
const newManager = new CodeIndexConfigManager(mockContextProxy)
909+
await newManager.loadConfiguration()
910+
expect(newManager.currentSearchMaxResults).toBe(50) // Default (DEFAULT_MAX_SEARCH_RESULTS)
911+
912+
// Test 3: Boundary values
913+
mockContextProxy.getGlobalState.mockReturnValue({
914+
codebaseIndexEnabled: true,
915+
codebaseIndexQdrantUrl: "http://qdrant.local",
916+
codebaseIndexEmbedderProvider: "openai",
917+
codebaseIndexEmbedderModelId: "text-embedding-3-small",
918+
codebaseIndexSearchMaxResults: 10, // Minimum allowed
919+
})
920+
921+
const minManager = new CodeIndexConfigManager(mockContextProxy)
922+
await minManager.loadConfiguration()
923+
expect(minManager.currentSearchMaxResults).toBe(10)
924+
925+
// Test 4: Maximum value
926+
mockContextProxy.getGlobalState.mockReturnValue({
927+
codebaseIndexEnabled: true,
928+
codebaseIndexQdrantUrl: "http://qdrant.local",
929+
codebaseIndexEmbedderProvider: "openai",
930+
codebaseIndexEmbedderModelId: "text-embedding-3-small",
931+
codebaseIndexSearchMaxResults: 200, // Maximum allowed
932+
})
933+
934+
const maxManager = new CodeIndexConfigManager(mockContextProxy)
935+
await maxManager.loadConfiguration()
936+
expect(maxManager.currentSearchMaxResults).toBe(200)
937+
})
938+
})
884939
})
885940

886941
describe("empty/missing API key handling", () => {
@@ -1157,9 +1212,12 @@ describe("CodeIndexConfigManager", () => {
11571212
modelId: "text-embedding-3-large",
11581213
openAiOptions: { openAiNativeApiKey: "test-openai-key" },
11591214
ollamaOptions: { ollamaBaseUrl: undefined },
1215+
geminiOptions: undefined,
1216+
openAiCompatibleOptions: undefined,
11601217
qdrantUrl: "http://qdrant.local",
11611218
qdrantApiKey: "test-qdrant-key",
11621219
searchMinScore: 0.4,
1220+
searchMaxResults: 50,
11631221
})
11641222
})
11651223

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

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { ApiHandlerOptions } from "../../shared/api"
22
import { ContextProxy } from "../../core/config/ContextProxy"
33
import { EmbedderProvider } from "./interfaces/manager"
44
import { CodeIndexConfig, PreviousConfigSnapshot } from "./interfaces/config"
5-
import { SEARCH_MIN_SCORE } from "./constants"
5+
import { DEFAULT_SEARCH_MIN_SCORE, DEFAULT_MAX_SEARCH_RESULTS } from "./constants"
66
import { getDefaultModelId, getModelDimension, getModelScoreThreshold } from "../../shared/embeddingModels"
77

88
/**
@@ -20,6 +20,7 @@ export class CodeIndexConfigManager {
2020
private qdrantUrl?: string = "http://localhost:6333"
2121
private qdrantApiKey?: string
2222
private searchMinScore?: number
23+
private searchMaxResults?: number
2324

2425
constructor(private readonly contextProxy: ContextProxy) {
2526
// Initialize with current configuration to avoid false restart triggers
@@ -46,6 +47,7 @@ export class CodeIndexConfigManager {
4647
codebaseIndexEmbedderBaseUrl: "",
4748
codebaseIndexEmbedderModelId: "",
4849
codebaseIndexSearchMinScore: undefined,
50+
codebaseIndexSearchMaxResults: undefined,
4951
}
5052

5153
const {
@@ -55,6 +57,7 @@ export class CodeIndexConfigManager {
5557
codebaseIndexEmbedderBaseUrl,
5658
codebaseIndexEmbedderModelId,
5759
codebaseIndexSearchMinScore,
60+
codebaseIndexSearchMaxResults,
5861
} = codebaseIndexConfig
5962

6063
const openAiKey = this.contextProxy?.getSecret("codeIndexOpenAiKey") ?? ""
@@ -71,6 +74,7 @@ export class CodeIndexConfigManager {
7174
this.qdrantUrl = codebaseIndexQdrantUrl
7275
this.qdrantApiKey = qdrantApiKey ?? ""
7376
this.searchMinScore = codebaseIndexSearchMinScore
77+
this.searchMaxResults = codebaseIndexSearchMaxResults
7478
this.openAiOptions = { openAiNativeApiKey: openAiKey }
7579

7680
// Set embedder provider with support for openai-compatible
@@ -334,6 +338,7 @@ export class CodeIndexConfigManager {
334338
qdrantUrl: this.qdrantUrl,
335339
qdrantApiKey: this.qdrantApiKey,
336340
searchMinScore: this.currentSearchMinScore,
341+
searchMaxResults: this.currentSearchMaxResults,
337342
}
338343
}
339344

@@ -377,7 +382,7 @@ export class CodeIndexConfigManager {
377382

378383
/**
379384
* Gets the configured minimum search score based on user setting, model-specific threshold, or fallback.
380-
* Priority: 1) User setting, 2) Model-specific threshold, 3) Default SEARCH_MIN_SCORE constant.
385+
* Priority: 1) User setting, 2) Model-specific threshold, 3) Default DEFAULT_SEARCH_MIN_SCORE constant.
381386
*/
382387
public get currentSearchMinScore(): number {
383388
// First check if user has configured a custom score threshold
@@ -388,6 +393,14 @@ export class CodeIndexConfigManager {
388393
// Fall back to model-specific threshold
389394
const currentModelId = this.modelId ?? getDefaultModelId(this.embedderProvider)
390395
const modelSpecificThreshold = getModelScoreThreshold(this.embedderProvider, currentModelId)
391-
return modelSpecificThreshold ?? SEARCH_MIN_SCORE
396+
return modelSpecificThreshold ?? DEFAULT_SEARCH_MIN_SCORE
397+
}
398+
399+
/**
400+
* Gets the configured maximum search results.
401+
* Returns user setting if configured, otherwise returns default.
402+
*/
403+
public get currentSearchMaxResults(): number {
404+
return this.searchMaxResults ?? DEFAULT_MAX_SEARCH_RESULTS
392405
}
393406
}

src/services/code-index/constants/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1+
import { CODEBASE_INDEX_DEFAULTS } from "@roo-code/types"
2+
13
/**Parser */
24
export const MAX_BLOCK_CHARS = 1000
35
export const MIN_BLOCK_CHARS = 50
46
export const MIN_CHUNK_REMAINDER_CHARS = 200 // Minimum characters for the *next* chunk after a split
57
export const MAX_CHARS_TOLERANCE_FACTOR = 1.15 // 15% tolerance for max chars
68

79
/**Search */
8-
export const SEARCH_MIN_SCORE = 0.4
9-
export const MAX_SEARCH_RESULTS = 50 // Maximum number of search results to return
10+
export const DEFAULT_SEARCH_MIN_SCORE = CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_MIN_SCORE
11+
export const DEFAULT_MAX_SEARCH_RESULTS = CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_RESULTS
1012

1113
/**File Watcher */
1214
export const QDRANT_CODE_BLOCK_NAMESPACE = "f47ac10b-58cc-4372-a567-0e02b2c3d479"

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export interface CodeIndexConfig {
1616
qdrantUrl?: string
1717
qdrantApiKey?: string
1818
searchMinScore?: number
19+
searchMaxResults?: number
1920
}
2021

2122
/**

src/services/code-index/interfaces/vector-store.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,17 @@ export interface IVectorStore {
2323
/**
2424
* Searches for similar vectors
2525
* @param queryVector Vector to search for
26-
* @param limit Maximum number of results to return
26+
* @param directoryPrefix Optional directory prefix to filter results
27+
* @param minScore Optional minimum score threshold
28+
* @param maxResults Optional maximum number of results to return
2729
* @returns Promise resolving to search results
2830
*/
29-
search(queryVector: number[], directoryPrefix?: string, minScore?: number): Promise<VectorStoreSearchResult[]>
31+
search(
32+
queryVector: number[],
33+
directoryPrefix?: string,
34+
minScore?: number,
35+
maxResults?: number,
36+
): Promise<VectorStoreSearchResult[]>
3037

3138
/**
3239
* Deletes points by file path

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export class CodeIndexSearchService {
3030
}
3131

3232
const minScore = this.configManager.currentSearchMinScore
33+
const maxResults = this.configManager.currentSearchMaxResults
3334

3435
const currentState = this.stateManager.getCurrentStatus().systemStatus
3536
if (currentState !== "Indexed" && currentState !== "Indexing") {
@@ -52,7 +53,7 @@ export class CodeIndexSearchService {
5253
}
5354

5455
// Perform search
55-
const results = await this.vectorStore.search(vector, normalizedPrefix, minScore)
56+
const results = await this.vectorStore.search(vector, normalizedPrefix, minScore, maxResults)
5657
return results
5758
} catch (error) {
5859
console.error("[CodeIndexSearchService] Error during search:", error)

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

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { createHash } from "crypto"
33

44
import { QdrantVectorStore } from "../qdrant-client"
55
import { getWorkspacePath } from "../../../../utils/path"
6-
import { MAX_SEARCH_RESULTS, SEARCH_MIN_SCORE } from "../../constants"
6+
import { DEFAULT_MAX_SEARCH_RESULTS, DEFAULT_SEARCH_MIN_SCORE } from "../../constants"
77

88
// Mocks
99
vitest.mock("@qdrant/js-client-rest")
@@ -1005,8 +1005,8 @@ describe("QdrantVectorStore", () => {
10051005
expect(mockQdrantClientInstance.query).toHaveBeenCalledWith(expectedCollectionName, {
10061006
query: queryVector,
10071007
filter: undefined,
1008-
score_threshold: SEARCH_MIN_SCORE,
1009-
limit: MAX_SEARCH_RESULTS,
1008+
score_threshold: DEFAULT_SEARCH_MIN_SCORE,
1009+
limit: DEFAULT_MAX_SEARCH_RESULTS,
10101010
params: {
10111011
hnsw_ef: 128,
10121012
exact: false,
@@ -1056,8 +1056,8 @@ describe("QdrantVectorStore", () => {
10561056
},
10571057
],
10581058
},
1059-
score_threshold: SEARCH_MIN_SCORE,
1060-
limit: MAX_SEARCH_RESULTS,
1059+
score_threshold: DEFAULT_SEARCH_MIN_SCORE,
1060+
limit: DEFAULT_MAX_SEARCH_RESULTS,
10611061
params: {
10621062
hnsw_ef: 128,
10631063
exact: false,
@@ -1083,7 +1083,31 @@ describe("QdrantVectorStore", () => {
10831083
query: queryVector,
10841084
filter: undefined,
10851085
score_threshold: customMinScore,
1086-
limit: MAX_SEARCH_RESULTS,
1086+
limit: DEFAULT_MAX_SEARCH_RESULTS,
1087+
params: {
1088+
hnsw_ef: 128,
1089+
exact: false,
1090+
},
1091+
with_payload: {
1092+
include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"],
1093+
},
1094+
})
1095+
})
1096+
1097+
it("should use custom maxResults when provided", async () => {
1098+
const queryVector = [0.1, 0.2, 0.3]
1099+
const customMaxResults = 100
1100+
const mockQdrantResults = { points: [] }
1101+
1102+
mockQdrantClientInstance.query.mockResolvedValue(mockQdrantResults)
1103+
1104+
await vectorStore.search(queryVector, undefined, undefined, customMaxResults)
1105+
1106+
expect(mockQdrantClientInstance.query).toHaveBeenCalledWith(expectedCollectionName, {
1107+
query: queryVector,
1108+
filter: undefined,
1109+
score_threshold: DEFAULT_SEARCH_MIN_SCORE,
1110+
limit: customMaxResults,
10871111
params: {
10881112
hnsw_ef: 128,
10891113
exact: false,
@@ -1229,8 +1253,8 @@ describe("QdrantVectorStore", () => {
12291253
},
12301254
],
12311255
},
1232-
score_threshold: SEARCH_MIN_SCORE,
1233-
limit: MAX_SEARCH_RESULTS,
1256+
score_threshold: DEFAULT_SEARCH_MIN_SCORE,
1257+
limit: DEFAULT_MAX_SEARCH_RESULTS,
12341258
params: {
12351259
hnsw_ef: 128,
12361260
exact: false,
@@ -1254,7 +1278,7 @@ describe("QdrantVectorStore", () => {
12541278
;(console.error as any).mockRestore()
12551279
})
12561280

1257-
it("should use constants MAX_SEARCH_RESULTS and SEARCH_MIN_SCORE correctly", async () => {
1281+
it("should use constants DEFAULT_MAX_SEARCH_RESULTS and DEFAULT_SEARCH_MIN_SCORE correctly", async () => {
12581282
const queryVector = [0.1, 0.2, 0.3]
12591283
const mockQdrantResults = { points: [] }
12601284

@@ -1263,8 +1287,8 @@ describe("QdrantVectorStore", () => {
12631287
await vectorStore.search(queryVector)
12641288

12651289
const callArgs = mockQdrantClientInstance.query.mock.calls[0][1]
1266-
expect(callArgs.limit).toBe(MAX_SEARCH_RESULTS)
1267-
expect(callArgs.score_threshold).toBe(SEARCH_MIN_SCORE)
1290+
expect(callArgs.limit).toBe(DEFAULT_MAX_SEARCH_RESULTS)
1291+
expect(callArgs.score_threshold).toBe(DEFAULT_SEARCH_MIN_SCORE)
12681292
})
12691293
})
12701294
})

0 commit comments

Comments
 (0)