Skip to content

Commit 9e4da86

Browse files
committed
feat: add user-configurable search score threshold slider for semantic search (#5027)
- Add optional codebaseIndexSearchMinScore field to codebase index config schema - Update config manager to prioritize user setting over model-specific thresholds - Replace text input with intuitive slider interface (0.0-1.0 range, 0.05 steps) - Add real-time value display and visual progress indication - Update tests to support new slider component and functionality - Maintain backward compatibility with existing configurations This allows users to directly control semantic search sensitivity instead of relying on hardcoded model-specific thresholds, enabling better nomic-embed-code support in future PR.
1 parent e03c03e commit 9e4da86

File tree

5 files changed

+111
-4
lines changed

5 files changed

+111
-4
lines changed

packages/types/src/codebase-index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const codebaseIndexConfigSchema = z.object({
1010
codebaseIndexEmbedderProvider: z.enum(["openai", "ollama", "openai-compatible"]).optional(),
1111
codebaseIndexEmbedderBaseUrl: z.string().optional(),
1212
codebaseIndexEmbedderModelId: z.string().optional(),
13+
codebaseIndexSearchMinScore: z.number().min(0).max(1).optional(),
1314
})
1415

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

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export class CodeIndexConfigManager {
1818
private openAiCompatibleOptions?: { baseUrl: string; apiKey: string; modelDimension?: number }
1919
private qdrantUrl?: string = "http://localhost:6333"
2020
private qdrantApiKey?: string
21+
private searchMinScore?: number
2122

2223
constructor(private readonly contextProxy: ContextProxy) {
2324
// Initialize with current configuration to avoid false restart triggers
@@ -33,7 +34,6 @@ export class CodeIndexConfigManager {
3334
const codebaseIndexConfig = this.contextProxy?.getGlobalState("codebaseIndexConfig") ?? {
3435
codebaseIndexEnabled: false,
3536
codebaseIndexQdrantUrl: "http://localhost:6333",
36-
codebaseIndexSearchMinScore: 0.4,
3737
codebaseIndexEmbedderProvider: "openai",
3838
codebaseIndexEmbedderBaseUrl: "",
3939
codebaseIndexEmbedderModelId: "",
@@ -45,6 +45,7 @@ export class CodeIndexConfigManager {
4545
codebaseIndexEmbedderProvider,
4646
codebaseIndexEmbedderBaseUrl,
4747
codebaseIndexEmbedderModelId,
48+
codebaseIndexSearchMinScore,
4849
} = codebaseIndexConfig
4950

5051
const openAiKey = this.contextProxy?.getSecret("codeIndexOpenAiKey") ?? ""
@@ -59,6 +60,7 @@ export class CodeIndexConfigManager {
5960
this.isEnabled = codebaseIndexEnabled || false
6061
this.qdrantUrl = codebaseIndexQdrantUrl
6162
this.qdrantApiKey = qdrantApiKey ?? ""
63+
this.searchMinScore = codebaseIndexSearchMinScore
6264
this.openAiOptions = { openAiNativeApiKey: openAiKey }
6365

6466
// Set embedder provider with support for openai-compatible
@@ -335,10 +337,16 @@ export class CodeIndexConfigManager {
335337
}
336338

337339
/**
338-
* Gets the configured minimum search score based on the current model.
339-
* Falls back to the constant SEARCH_MIN_SCORE if no model-specific threshold is found.
340+
* Gets the configured minimum search score based on user setting, model-specific threshold, or fallback.
341+
* Priority: 1) User setting, 2) Model-specific threshold, 3) Default SEARCH_MIN_SCORE constant.
340342
*/
341343
public get currentSearchMinScore(): number {
344+
// First check if user has configured a custom score threshold
345+
if (this.searchMinScore !== undefined) {
346+
return this.searchMinScore
347+
}
348+
349+
// Fall back to model-specific threshold
342350
const currentModelId = this.modelId ?? getDefaultModelId(this.embedderProvider)
343351
const modelSpecificThreshold = getModelScoreThreshold(this.embedderProvider, currentModelId)
344352
return modelSpecificThreshold ?? SEARCH_MIN_SCORE

webview-ui/src/components/settings/CodeIndexSettings.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,45 @@ export const CodeIndexSettings: React.FC<CodeIndexSettingsProps> = ({
449449
</div>
450450
</div>
451451

452+
<div className="flex flex-col gap-3">
453+
<div className="flex items-center gap-4 font-bold">
454+
<div>{t("settings:codeIndex.searchMinScoreLabel")}</div>
455+
</div>
456+
<div className="flex flex-col gap-3">
457+
<div className="flex items-center gap-3">
458+
<span className="text-xs text-vscode-descriptionForeground min-w-[30px]">0.0</span>
459+
<input
460+
type="range"
461+
min="0"
462+
max="1"
463+
step="0.05"
464+
value={codebaseIndexConfig.codebaseIndexSearchMinScore || 0.4}
465+
onChange={(e) => {
466+
const value = parseFloat(e.target.value)
467+
setCachedStateField("codebaseIndexConfig", {
468+
...codebaseIndexConfig,
469+
codebaseIndexSearchMinScore: value,
470+
})
471+
}}
472+
className="flex-1 h-2 bg-vscode-input-background rounded-lg appearance-none cursor-pointer slider"
473+
data-testid="search-min-score-slider"
474+
style={{
475+
background: `linear-gradient(to right, var(--vscode-progressBar-background) 0%, var(--vscode-progressBar-background) ${(codebaseIndexConfig.codebaseIndexSearchMinScore || 0.4) * 100}%, var(--vscode-input-background) ${(codebaseIndexConfig.codebaseIndexSearchMinScore || 0.4) * 100}%, var(--vscode-input-background) 100%)`,
476+
}}
477+
/>
478+
<span className="text-xs text-vscode-descriptionForeground min-w-[30px]">1.0</span>
479+
</div>
480+
<div className="text-center">
481+
<span className="text-sm text-vscode-foreground font-medium">
482+
{(codebaseIndexConfig.codebaseIndexSearchMinScore || 0.4).toFixed(2)}
483+
</span>
484+
</div>
485+
<p className="text-vscode-descriptionForeground text-sm mt-1">
486+
{t("settings:codeIndex.searchMinScoreDescription")}
487+
</p>
488+
</div>
489+
</div>
490+
452491
{(!areSettingsCommitted || !validateIndexingConfig(codebaseIndexConfig, apiConfiguration)) && (
453492
<p className="text-sm text-vscode-descriptionForeground mb-2">
454493
{t("settings:codeIndex.unsavedSettingsMessage")}

webview-ui/src/components/settings/__tests__/CodeIndexSettings.spec.tsx

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ vi.mock("@src/i18n/TranslationContext", () => ({
4343
"settings:codeIndex.clearDataDialog.description": "This will remove all indexed data",
4444
"settings:codeIndex.clearDataDialog.cancelButton": "Cancel",
4545
"settings:codeIndex.clearDataDialog.confirmButton": "Confirm",
46+
"settings:codeIndex.searchMinScoreLabel": "Search Score Threshold",
47+
"settings:codeIndex.searchMinScoreDescription":
48+
"Minimum similarity score (0.0-1.0) required for search results. Lower values return more results but may be less relevant. Higher values return fewer but more relevant results.",
4649
}
4750
return translations[key] || key
4851
},
@@ -158,6 +161,7 @@ describe("CodeIndexSettings", () => {
158161
codebaseIndexEmbedderProvider: "openai" as const,
159162
codebaseIndexEmbedderModelId: "text-embedding-3-small",
160163
codebaseIndexQdrantUrl: "http://localhost:6333",
164+
codebaseIndexSearchMinScore: 0.4,
161165
},
162166
apiConfiguration: {
163167
codeIndexOpenAiKey: "",
@@ -204,7 +208,7 @@ describe("CodeIndexSettings", () => {
204208

205209
expect(screen.getByText("Base URL")).toBeInTheDocument()
206210
expect(screen.getByText("API Key")).toBeInTheDocument()
207-
expect(screen.getAllByTestId("vscode-textfield")).toHaveLength(6) // Base URL, API Key, Embedding Dimension, Model ID, Qdrant URL, Qdrant Key
211+
expect(screen.getAllByTestId("vscode-textfield")).toHaveLength(6) // Base URL, API Key, Embedding Dimension, Model ID, Qdrant URL, Qdrant Key (Search Min Score is now a slider)
208212
})
209213

210214
it("should hide OpenAI Compatible fields when different provider is selected", () => {
@@ -817,6 +821,59 @@ describe("CodeIndexSettings", () => {
817821
})
818822
})
819823

824+
describe("Search Minimum Score Slider", () => {
825+
it("should render search minimum score slider", () => {
826+
render(<CodeIndexSettings {...defaultProps} />)
827+
828+
expect(screen.getByTestId("search-min-score-slider")).toBeInTheDocument()
829+
expect(screen.getByText("Search Score Threshold")).toBeInTheDocument()
830+
})
831+
832+
it("should display current search minimum score value", () => {
833+
const propsWithScore = {
834+
...defaultProps,
835+
codebaseIndexConfig: {
836+
...defaultProps.codebaseIndexConfig,
837+
codebaseIndexSearchMinScore: 0.65,
838+
},
839+
}
840+
841+
render(<CodeIndexSettings {...propsWithScore} />)
842+
843+
const slider = screen.getByTestId("search-min-score-slider")
844+
expect(slider).toHaveValue("0.65")
845+
expect(screen.getByText("0.65")).toBeInTheDocument()
846+
})
847+
848+
it("should call setCachedStateField when slider value changes", () => {
849+
render(<CodeIndexSettings {...defaultProps} />)
850+
851+
const slider = screen.getByTestId("search-min-score-slider")
852+
fireEvent.change(slider, { target: { value: "0.8" } })
853+
854+
expect(mockSetCachedStateField).toHaveBeenCalledWith("codebaseIndexConfig", {
855+
...defaultProps.codebaseIndexConfig,
856+
codebaseIndexSearchMinScore: 0.8,
857+
})
858+
})
859+
860+
it("should use default value when no score is set", () => {
861+
const propsWithoutScore = {
862+
...defaultProps,
863+
codebaseIndexConfig: {
864+
...defaultProps.codebaseIndexConfig,
865+
codebaseIndexSearchMinScore: undefined,
866+
},
867+
}
868+
869+
render(<CodeIndexSettings {...propsWithoutScore} />)
870+
871+
const slider = screen.getByTestId("search-min-score-slider")
872+
expect(slider).toHaveValue("0.4")
873+
expect(screen.getByText("0.40")).toBeInTheDocument()
874+
})
875+
})
876+
820877
describe("Error Handling", () => {
821878
it("should handle invalid provider gracefully", () => {
822879
const propsWithInvalidProvider = {

webview-ui/src/i18n/locales/en/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@
5656
"ollamaUrlLabel": "Ollama URL:",
5757
"qdrantUrlLabel": "Qdrant URL",
5858
"qdrantKeyLabel": "Qdrant Key:",
59+
"searchMinScoreLabel": "Search Score Threshold",
60+
"searchMinScoreDescription": "Minimum similarity score (0.0-1.0) required for search results. Lower values return more results but may be less relevant. Higher values return fewer but more relevant results.",
5961
"startIndexingButton": "Start Indexing",
6062
"clearIndexDataButton": "Clear Index Data",
6163
"unsavedSettingsMessage": "Please save your settings before starting the indexing process.",

0 commit comments

Comments
 (0)