Files
obsidian_ollama/src/indexing-pipeline/vectorization.ts
T
fegger cc97d77810 fix: use collection deletion instead of reset, add embedding retry, slow down indexing
- semantic-cache.clearCache: delete collection instead of client.reset()
  to avoid 403 Forbidden on newer ChromaDB versions
- vault-vector-store.clearIndex: same collection deletion approach
- ContentVectorizer: add 3-attempt retry with exponential backoff (1s, 2s, 4s)
- main.ts: reduce indexing batch size from 5 to 2, increase delay from 100ms to 500ms
- Update semantic-cache tests for deleteCollection
2026-05-19 23:41:05 +02:00

107 lines
3.0 KiB
TypeScript

// src/indexing-pipeline/vectorization.ts
import { ContentChunk } from './normalization';
import { Logger } from '../utils';
interface VectorizationConfig {
model: string;
ollamaUrl: string;
}
/**
* Vectorizes content chunks using Ollama embeddings
*/
export class ContentVectorizer {
private model: string;
private ollamaUrl: string;
private fetchFn: typeof fetch;
constructor(config: VectorizationConfig, fetchFn?: typeof fetch) {
this.model = config.model;
this.ollamaUrl = config.ollamaUrl;
this.fetchFn = fetchFn ?? ((url, init) => fetch(url, init));
}
/**
* Generates embeddings for a content chunk with retry logic
*/
async vectorize(chunk: ContentChunk): Promise<number[]> {
const prompt = this.createPrompt(chunk);
const maxRetries = 3;
const baseDelay = 1000;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
if (attempt > 0) {
const delay = baseDelay * Math.pow(2, attempt - 1);
Logger.info(
`Retrying embedding (attempt ${attempt + 1}/${maxRetries}) after ${delay}ms`,
'indexing-pipeline'
);
await new Promise((resolve) => setTimeout(resolve, delay));
}
const response = await this.fetchFn(`${this.ollamaUrl}/api/embeddings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: this.model,
prompt: prompt,
}),
});
if (!response.ok) {
throw new Error(`Embedding failed with status ${response.status}`);
}
const data: unknown = await response.json();
if (!this.isEmbeddingResponse(data)) {
throw new Error('Invalid embedding response');
}
return data.embedding;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
Logger.warn(
`Embedding attempt ${attempt + 1} failed: ${errorMessage}`,
'indexing-pipeline'
);
if (attempt === maxRetries - 1) {
Logger.warn(
`Failed to generate embedding after ${maxRetries} attempts`,
'indexing-pipeline'
);
return [];
}
}
}
return [];
}
private isEmbeddingResponse(data: unknown): data is { embedding: number[] } {
return (
typeof data === 'object' &&
data !== null &&
Array.isArray((data as { embedding?: unknown }).embedding) &&
(data as { embedding: unknown[] }).embedding.every((value) => typeof value === 'number')
);
}
/**
* Creates a prompt from content chunk for embedding
*/
private createPrompt(chunk: ContentChunk): string {
// Combine important elements for embedding
const parts = [
chunk.title,
chunk.firstParagraph,
chunk.content.substring(0, 1000), // Limit content to avoid long prompts
chunk.headings.join(' '),
JSON.stringify(chunk.frontmatter),
].filter(Boolean);
return parts.join('\n\n');
}
}