Skip to content

add client-side embeddings cached in IndexedDB#2432

Open
Mbeaulne wants to merge 1 commit into
06-18-improve_component_search_relevance_and_ai_rerankingfrom
06-18-add_client-side_embeddings_cached_in_indexeddb
Open

add client-side embeddings cached in IndexedDB#2432
Mbeaulne wants to merge 1 commit into
06-18-improve_component_search_relevance_and_ai_rerankingfrom
06-18-add_client-side_embeddings_cached_in_indexeddb

Conversation

@Mbeaulne

@Mbeaulne Mbeaulne commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Description

Adds embedding-based semantic search as a pre-processing step before LLM reranking. When an AI provider API base is configured, the component search now fetches text embeddings for both the search query and all indexed components, ranks them by cosine similarity, and merges those results with the top lexical matches before passing candidates to the LLM reranker. This improves the quality of candidates surfaced to the reranker, particularly for queries that don't share keywords with component names or descriptions.

A new componentSearchEmbeddings service handles embedding generation, cosine similarity scoring, and persistent caching of embeddings in IndexedDB (keyed by a FNV-1a hash of the text and model name) to avoid redundant API calls across searches. A mergeUniqueMatches helper deduplicates and merges lexical and embedding results by digest before reranking.

The embedding fetch is tracked with an isEmbeddingSearchPending flag, which gates the rerank active state and disables the AI search buttons while in progress, keeping the spinner and disabled states consistent with the existing reranking UX.

Related Issue and Pull requests

Type of Change

  • Bug fix
  • New feature
  • Improvement
  • Cleanup/Refactor
  • Breaking change
  • Documentation update

Checklist

  • I have tested this does not break current pipelines / runs functionality
  • I have tested the changes on staging

Screenshots (if applicable)

Test Instructions

  1. Configure an OpenAI-compatible API base (e.g. https://api.openai.com/v1) in AI provider settings.
  2. Open the component search panel and enter a query that is semantically related to a component but does not share exact keywords.
  3. Click the AI search (Sparkles) button and verify the spinner appears during both the embedding fetch and reranking phases.
  4. Confirm that results are ranked by semantic relevance.
  5. Repeat the same search and verify via network tooling that no additional embedding API calls are made (cache hit).
  6. Verify that searches still work correctly when no API base is configured (embedding step is skipped gracefully).

Additional Comments

The embedding model is hardcoded to text-embedding-3-small. If no apiBase is set, the embedding step is skipped entirely and behaviour is identical to before this change.

@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown

🎩 Preview

A preview build has been created at: 06-18-add_client-side_embeddings_cached_in_indexeddb/92a80e1

@Mbeaulne Mbeaulne marked this pull request as ready for review June 18, 2026 18:20
@Mbeaulne Mbeaulne requested a review from a team as a code owner June 18, 2026 18:20
@Mbeaulne Mbeaulne force-pushed the 06-18-add_client-side_embeddings_cached_in_indexeddb branch from 626a971 to 60a4a12 Compare June 18, 2026 18:31
@Mbeaulne Mbeaulne mentioned this pull request Jun 18, 2026
8 tasks

if (missingTexts.length === 0) return result;

const embeddings = await fetchEmbeddings(missingTexts, options);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 This is an AI-generated code review comment.

[MEDIUM] The full unfiltered index is embedded on every search, and embedTextsWithCache sends all cache-missing texts in a single fetch body with no chunking. On a cold cache for a large library this exceeds OpenAI embeddings limits (2048 inputs / ~300k tokens) → 400, and the whole semantic layer silently returns [] (swallowed by the host catch). Consider batching missing texts (e.g. 256–512/request) and merging, embedding only the candidate set, or capping N.

}

function cacheKey(text: string): string {
return `${CACHE_SCHEMA_VERSION}:${COMPONENT_SEARCH_EMBEDDING_MODEL}:${hashText(text)}`;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 This is an AI-generated code review comment.

[LOW/MEDIUM] The cache key and the textHash validation field both derive from the same 32-bit FNV-1a hash, so a key collision (~50% likelihood near ~77k distinct texts) returns a stale-but-"validated" vector with no detection. Consider storing the full text (or a wider hash, e.g. SHA-256, or including text length) as the validation field so a key collision is caught as a miss.


const embeddings = await fetchEmbeddings(missingTexts, options);
const now = Date.now();
await componentSearchEmbeddingDb.embeddings.bulkPut(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 This is an AI-generated code review comment.

[LOW] Dexie schema is pinned at version(1); cache busting relies on CACHE_SCHEMA_VERSION embedded in the key, so bumping it orphans all prior rows permanently (no prune). Also, there's no QuotaExceededError handling on this bulkPut — the rejection is swallowed by the host catch, silently disabling caching. Consider pruning rows whose key lacks the current schemaVersion:model: prefix on a schema bump, and catching quota errors to trigger a prune.

limit: number,
): LexicalMatch[] {
const merged: LexicalMatch[] = [];
const seen = new Set<string>();

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 This is an AI-generated code review comment.

[LOW] This mergeUniqueMatches (dedupe-by-digest) is duplicated verbatim in useComponentSearchV2State.ts (~line 53). Consider exporting it once (e.g. from componentSearchIndex.ts) and importing it in both hosts.

startAiSearch(aiCandidateMatches);
void startAiSearch(aiCandidateMatches, aiCandidateMatches.length);
};

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 This is an AI-generated code review comment.

maxTypoDistance allows edit distance 1 on 4-char tokens, so short generic IO names collide: data<->date, path<->bath, list<->last. Fuzzy is name/io-only and scored at 0.75x so exact hits still win, but a typo-free data could pull in date-named IO. Consider raising the distance-1 floor to length >= 5.

@Mbeaulne Mbeaulne force-pushed the 06-18-add_client-side_embeddings_cached_in_indexeddb branch from 60a4a12 to f6d7ea7 Compare June 18, 2026 19:46
@Mbeaulne Mbeaulne force-pushed the 06-18-improve_component_search_relevance_and_ai_reranking branch 2 times, most recently from 6e2b2ae to d8565d2 Compare June 18, 2026 20:28
@Mbeaulne Mbeaulne force-pushed the 06-18-add_client-side_embeddings_cached_in_indexeddb branch from f6d7ea7 to 27a3952 Compare June 18, 2026 20:28
@Mbeaulne Mbeaulne force-pushed the 06-18-improve_component_search_relevance_and_ai_reranking branch from d8565d2 to 761f88a Compare June 18, 2026 20:49
@Mbeaulne Mbeaulne force-pushed the 06-18-add_client-side_embeddings_cached_in_indexeddb branch 2 times, most recently from 316495b to 89c4999 Compare June 18, 2026 21:02
@Mbeaulne Mbeaulne force-pushed the 06-18-improve_component_search_relevance_and_ai_reranking branch from 761f88a to 41a7bd9 Compare June 18, 2026 21:02
@Mbeaulne Mbeaulne force-pushed the 06-18-improve_component_search_relevance_and_ai_reranking branch from 41a7bd9 to c379d9b Compare June 18, 2026 21:16
@Mbeaulne Mbeaulne force-pushed the 06-18-add_client-side_embeddings_cached_in_indexeddb branch from 89c4999 to 92a80e1 Compare June 18, 2026 21:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant