feat(TRUEREF-0020): add embedding profiles, default local embeddings, and version-scoped semantic retrieval

- Add embedding_profiles table with provider registry pattern
- Install @xenova/transformers as runtime dependency
- Update snippet_embeddings with composite PK (snippet_id, profile_id)
- Seed default local profile using Xenova/all-MiniLM-L6-v2
- Add provider registry (local-transformers, openai-compatible)
- Update EmbeddingService to persist and retrieve by profileId
- Add version-scoped VectorSearch with optional versionId filtering
- Add searchMode (auto|keyword|semantic|hybrid) to HybridSearchService
- Update API /context route to load active profile, support searchMode/alpha params
- Extend MCP query-docs tool with searchMode and alpha parameters
- Update settings API to work with embedding_profiles table
- Add comprehensive test coverage for profiles, registry, version scoping

Status: 445/451 tests passing, core feature complete
This commit is contained in:
Giancarmine Salucci
2026-03-25 19:16:37 +01:00
parent fef6f66930
commit 169df4d984
19 changed files with 2668 additions and 246 deletions

View File

@@ -36,6 +36,16 @@ export interface HybridSearchOptions {
* Default: 0.5.
*/
alpha?: number;
/**
* Search mode: 'auto' (default), 'keyword', 'semantic', or 'hybrid'.
* Overrides alpha when set to 'keyword' (forces 0) or 'semantic' (forces 1).
*/
searchMode?: 'auto' | 'keyword' | 'semantic' | 'hybrid';
/**
* Embedding profile ID for vector search.
* Default: 'local-default'.
*/
profileId?: string;
}
/**
@@ -90,7 +100,24 @@ export class HybridSearchService {
options: HybridSearchOptions
): Promise<SnippetSearchResult[]> {
const limit = options.limit ?? 20;
const alpha = options.alpha ?? 0.5;
const mode = options.searchMode ?? 'auto';
// Resolve alpha from searchMode
let alpha: number;
switch (mode) {
case 'keyword':
alpha = 0;
break;
case 'semantic':
alpha = 1;
break;
case 'hybrid':
alpha = options.alpha ?? 0.5;
break;
default:
// 'auto'
alpha = options.alpha ?? 0.5;
}
// Always run FTS5 — it is synchronous and fast.
const ftsResults = this.searchService.searchSnippets(query, {
@@ -115,11 +142,12 @@ export class HybridSearchService {
const queryEmbedding = embeddings[0].values;
const vectorResults = this.vectorSearch.vectorSearch(
queryEmbedding,
options.repositoryId,
limit * 3
);
const vectorResults = this.vectorSearch.vectorSearch(queryEmbedding, {
repositoryId: options.repositoryId,
versionId: options.versionId,
profileId: options.profileId,
limit: limit * 3
});
// Pure vector mode: skip RRF and return vector results directly.
if (alpha === 1) {