fix: dynamic embedding dimensions for HTTP backend

- schema.ts: FLOAT[${EMBEDDING_DIMS}] reads from GITNEXUS_EMBEDDING_DIMS env
- embedding-pipeline.ts: vector search CAST uses actual query vector length
- mcp/core/embedder.ts: HTTP embedding support for MCP query-time search

Without this, using a 1024d model (e.g. bge-large) fails with
'Expected: 384, Actual: 1024' on LadybugDB vector insert.
This commit is contained in:
zm2231 2026-03-20 00:31:39 -04:00
parent 19dc236ace
commit a67863563a
3 changed files with 28 additions and 4 deletions

View file

@ -326,7 +326,7 @@ export const semanticSearch = async (
// Query the vector index on CodeEmbedding to get nodeIds and distances
const vectorQuery = `
CALL QUERY_VECTOR_INDEX('CodeEmbedding', 'code_embedding_idx',
CAST(${queryVecStr} AS FLOAT[384]), ${k})
CAST(${queryVecStr} AS FLOAT[${queryVec.length}]), ${k})
YIELD node AS emb, distance
WITH emb, distance
WHERE distance < ${maxDistance}

View file

@ -398,10 +398,12 @@ CREATE REL TABLE ${REL_TABLE_NAME} (
// Separate table for vector storage to avoid copy-on-write overhead
// ============================================================================
export const EMBEDDING_DIMS = parseInt(process.env.GITNEXUS_EMBEDDING_DIMS ?? '384', 10);
export const EMBEDDING_SCHEMA = `
CREATE NODE TABLE ${EMBEDDING_TABLE_NAME} (
nodeId STRING,
embedding FLOAT[384],
embedding FLOAT[${EMBEDDING_DIMS}],
PRIMARY KEY (nodeId)
)`;

View file

@ -9,7 +9,13 @@ import { pipeline, env, type FeatureExtractionPipeline } from '@huggingface/tran
// Model config
const MODEL_ID = 'Snowflake/snowflake-arctic-embed-xs';
const EMBEDDING_DIMS = 384;
const EMBEDDING_DIMS = parseInt(process.env.GITNEXUS_EMBEDDING_DIMS ?? '384', 10);
// HTTP embedding config
const HTTP_URL = process.env.GITNEXUS_EMBEDDING_URL ?? '';
const HTTP_MODEL = process.env.GITNEXUS_EMBEDDING_MODEL ?? '';
const HTTP_KEY = process.env.GITNEXUS_EMBEDDING_API_KEY ?? 'unused';
const USE_HTTP = !!(HTTP_URL && HTTP_MODEL);
// Module-level state for singleton pattern
let embedderInstance: FeatureExtractionPipeline | null = null;
@ -20,6 +26,8 @@ let initPromise: Promise<FeatureExtractionPipeline> | null = null;
* Initialize the embedding model (lazy, on first search)
*/
export const initEmbedder = async (): Promise<FeatureExtractionPipeline> => {
if (USE_HTTP) return null as unknown as FeatureExtractionPipeline;
if (embedderInstance) {
return embedderInstance;
}
@ -87,12 +95,26 @@ export const initEmbedder = async (): Promise<FeatureExtractionPipeline> => {
/**
* Check if embedder is ready
*/
export const isEmbedderReady = (): boolean => embedderInstance !== null;
export const isEmbedderReady = (): boolean => USE_HTTP || embedderInstance !== null;
/**
* Embed a query text for semantic search
*/
export const embedQuery = async (query: string): Promise<number[]> => {
if (USE_HTTP) {
const resp = await fetch(`${HTTP_URL.replace(/\/+$/, '')}/embeddings`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${HTTP_KEY}`,
},
body: JSON.stringify({ input: [query], model: HTTP_MODEL }),
});
if (!resp.ok) throw new Error(`Embedding endpoint ${resp.status}`);
const data = (await resp.json()) as { data: Array<{ embedding: number[] }> };
return data.data[0].embedding;
}
const embedder = await initEmbedder();
const result = await embedder(query, {