Skip to content

Commit 106ee1a

Browse files
committed
fix: singleton embedding provider and LanceDB schema validation
- Implement module-level singleton for TransformersEmbeddingProvider to prevent duplicate model loads - Add getEmbeddingProvider() factory function with lazy initialization - Add schema validation in LanceDB to detect and auto-drop stale tables missing vector column - Update logging to use MCP server.sendLoggingMessage instead of console.error
1 parent 94f3f13 commit 106ee1a

5 files changed

Lines changed: 47 additions & 57 deletions

File tree

src/embeddings/index.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,48 @@
1-
/**
2-
* Embeddings module
3-
* Provides local embedding generation using Transformers.js
4-
*/
5-
61
export * from './types.js';
72
export * from './transformers.js';
83

94
import { EmbeddingProvider, EmbeddingConfig, DEFAULT_EMBEDDING_CONFIG } from './types.js';
105
import { TransformersEmbeddingProvider } from './transformers.js';
116

12-
/**
13-
* Get an embedding provider based on configuration
14-
*/
7+
let cachedProvider: EmbeddingProvider | null = null;
8+
let cachedProviderType: string | null = null;
9+
1510
export async function getEmbeddingProvider(
1611
config: Partial<EmbeddingConfig> = {}
1712
): Promise<EmbeddingProvider> {
1813
const mergedConfig = { ...DEFAULT_EMBEDDING_CONFIG, ...config };
14+
const providerKey = `${mergedConfig.provider}:${mergedConfig.model}`;
15+
16+
if (cachedProvider && cachedProviderType === providerKey) {
17+
return cachedProvider;
18+
}
1919

2020
if (mergedConfig.provider === 'openai') {
2121
const { OpenAIEmbeddingProvider } = await import('./openai.js');
2222
const provider = new OpenAIEmbeddingProvider(
23-
mergedConfig.model === 'Xenova/bge-small-en-v1.5' ? 'text-embedding-3-small' : mergedConfig.model,
23+
mergedConfig.model || 'text-embedding-3-small',
2424
mergedConfig.apiKey,
2525
mergedConfig.apiEndpoint
2626
);
2727
await provider.initialize();
28+
cachedProvider = provider;
29+
cachedProviderType = providerKey;
2830
return provider;
2931
}
3032

3133
if (mergedConfig.provider === 'custom') {
32-
throw new Error("Custom provider requires implementing 'IEmbeddingProvider' and bundling it. Use 'openai' or 'transformers' for now.");
34+
throw new Error("Custom provider not implemented. Use 'openai' or 'transformers'.");
3335
}
3436

35-
// Ollama support can be added later
3637
if (mergedConfig.provider === 'ollama') {
3738
console.warn('Ollama provider not yet implemented, falling back to Transformers.js');
3839
}
3940

4041
const provider = new TransformersEmbeddingProvider(mergedConfig.model);
4142
await provider.initialize();
43+
cachedProvider = provider;
44+
cachedProviderType = providerKey;
4245

4346
return provider;
4447
}
48+

src/embeddings/transformers.ts

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
1-
/**
2-
* Transformers.js Embedding Provider
3-
* Uses local models via @xenova/transformers
4-
*/
1+
import { EmbeddingProvider, DEFAULT_MODEL } from "./types.js";
52

6-
import { EmbeddingProvider } from "./types.js";
7-
8-
// Model configurations
93
const MODEL_CONFIGS: Record<string, { dimensions: number }> = {
104
"Xenova/bge-small-en-v1.5": { dimensions: 384 },
115
"Xenova/all-MiniLM-L6-v2": { dimensions: 384 },
@@ -21,7 +15,7 @@ export class TransformersEmbeddingProvider implements EmbeddingProvider {
2115
private ready = false;
2216
private initPromise: Promise<void> | null = null;
2317

24-
constructor(modelName: string = "Xenova/bge-small-en-v1.5") {
18+
constructor(modelName: string = DEFAULT_MODEL) {
2519
this.modelName = modelName;
2620
this.dimensions = MODEL_CONFIGS[modelName]?.dimensions || 384;
2721
}
@@ -39,12 +33,10 @@ export class TransformersEmbeddingProvider implements EmbeddingProvider {
3933
console.error(`Loading embedding model: ${this.modelName}`);
4034
console.error("(First run will download ~130MB model)");
4135

42-
// Dynamic import to avoid issues at require time
4336
const { pipeline } = await import("@xenova/transformers");
4437

45-
// Create feature extraction pipeline
4638
this.pipeline = await pipeline("feature-extraction", this.modelName, {
47-
quantized: true, // Use quantized model for speed
39+
quantized: true,
4840
});
4941

5042
this.ready = true;
@@ -61,13 +53,11 @@ export class TransformersEmbeddingProvider implements EmbeddingProvider {
6153
}
6254

6355
try {
64-
// Get embeddings
6556
const output = await this.pipeline(text, {
6657
pooling: "mean",
6758
normalize: true,
6859
});
6960

70-
// Convert to array
7161
return Array.from(output.data);
7262
} catch (error) {
7363
console.error("Failed to generate embedding:", error);
@@ -81,24 +71,19 @@ export class TransformersEmbeddingProvider implements EmbeddingProvider {
8171
}
8272

8373
const embeddings: number[][] = [];
84-
85-
// Process in smaller batches to manage memory
8674
const batchSize = 32;
75+
8776
for (let i = 0; i < texts.length; i += batchSize) {
8877
const batch = texts.slice(i, i + batchSize);
89-
90-
// Process batch
9178
const batchEmbeddings = await Promise.all(
9279
batch.map((text) => this.embed(text))
9380
);
9481

9582
embeddings.push(...batchEmbeddings);
9683

97-
// Log progress for large batches
9884
if (texts.length > 100 && (i + batchSize) % 100 === 0) {
9985
console.error(
100-
`Embedded ${Math.min(i + batchSize, texts.length)}/${texts.length
101-
} chunks`
86+
`Embedded ${Math.min(i + batchSize, texts.length)}/${texts.length} chunks`
10287
);
10388
}
10489
}
@@ -111,13 +96,11 @@ export class TransformersEmbeddingProvider implements EmbeddingProvider {
11196
}
11297
}
11398

114-
/**
115-
* Create an embedding provider based on config
116-
*/
11799
export async function createEmbeddingProvider(
118-
modelName: string = "Xenova/bge-base-en-v1.5"
100+
modelName: string = DEFAULT_MODEL
119101
): Promise<EmbeddingProvider> {
120102
const provider = new TransformersEmbeddingProvider(modelName);
121103
await provider.initialize();
122104
return provider;
123105
}
106+

src/embeddings/types.ts

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,11 @@
1-
/**
2-
* Types for embedding providers
3-
*/
4-
51
export interface EmbeddingProvider {
62
readonly name: string;
73
readonly modelName: string;
84
readonly dimensions: number;
95

10-
/**
11-
* Initialize the provider (load model, etc.)
12-
*/
136
initialize(): Promise<void>;
14-
15-
/**
16-
* Generate embedding for a single text
17-
*/
187
embed(text: string): Promise<number[]>;
19-
20-
/**
21-
* Generate embeddings for multiple texts (batch)
22-
*/
238
embedBatch(texts: string[]): Promise<number[][]>;
24-
25-
/**
26-
* Check if provider is ready
27-
*/
289
isReady(): boolean;
2910
}
3011

@@ -37,10 +18,13 @@ export interface EmbeddingConfig {
3718
apiEndpoint?: string;
3819
}
3920

21+
export const DEFAULT_MODEL = process.env.EMBEDDING_MODEL || "Xenova/bge-small-en-v1.5";
22+
4023
export const DEFAULT_EMBEDDING_CONFIG: EmbeddingConfig = {
4124
provider: (process.env.EMBEDDING_PROVIDER as any) || "transformers",
42-
model: process.env.EMBEDDING_MODEL || "Xenova/bge-small-en-v1.5",
25+
model: DEFAULT_MODEL,
4326
batchSize: 32,
4427
maxRetries: 3,
4528
apiKey: process.env.OPENAI_API_KEY,
4629
};
30+

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ const server = new Server(
7575
capabilities: {
7676
tools: {},
7777
resources: {},
78+
logging: {}, // Enable structured logging for clients that support it
7879
},
7980
}
8081
);

src/storage/lancedb.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,29 @@ export class LanceDBStorageProvider implements VectorStorageProvider {
3535
// Connect to database
3636
this.db = await lancedb.connect(storagePath);
3737

38-
// Check if table exists
38+
// Check if table exists and has valid schema
3939
const tableNames = await this.db.tableNames();
4040
if (tableNames.includes('code_chunks')) {
4141
this.table = await this.db.openTable('code_chunks');
42-
console.error('Opened existing LanceDB table');
42+
43+
// Validate schema has vector column (required for semantic search)
44+
try {
45+
const schema = await this.table.schema();
46+
const hasVectorColumn = schema.fields.some((f: any) => f.name === 'vector');
47+
48+
if (!hasVectorColumn) {
49+
console.error('Stale index detected (missing vector column). Rebuilding...');
50+
await this.db.dropTable('code_chunks');
51+
this.table = null;
52+
} else {
53+
console.error('Opened existing LanceDB table');
54+
}
55+
} catch (schemaError) {
56+
// If schema check fails, table is likely corrupted - drop and rebuild
57+
console.error('Failed to validate table schema, rebuilding index...');
58+
await this.db.dropTable('code_chunks');
59+
this.table = null;
60+
}
4361
}
4462

4563
this.initialized = true;

0 commit comments

Comments
 (0)