fix: process embeddings sequentially, shorten prompts, fix tests

- main.ts: process files sequentially (not Promise.all) to avoid concurrent
  embedding requests hammering Ollama; batch size reduced to 1
- vectorization.ts: shorten embedding prompts from 1000 to 500 chars,
  limit headings to 5, remove frontmatter from prompt to stay well within
  embedding model context window
- Update vectorization and indexing-pipeline tests for new prompt format
This commit is contained in:
2026-05-19 23:51:29 +02:00
parent cc97d77810
commit cb621c83b5
5 changed files with 42 additions and 47 deletions
+16 -18
View File
@@ -9507,10 +9507,10 @@ var ContentVectorizer = class {
const parts = [ const parts = [
chunk.title, chunk.title,
chunk.firstParagraph, chunk.firstParagraph,
chunk.content.substring(0, 1e3), chunk.content.substring(0, 500),
// Limit content to avoid long prompts // Limit content to avoid long prompts
chunk.headings.join(" "), chunk.headings.slice(0, 5).join(" ")
JSON.stringify(chunk.frontmatter) // Limit headings
].filter(Boolean); ].filter(Boolean);
return parts.join("\n\n"); return parts.join("\n\n");
} }
@@ -10033,7 +10033,7 @@ var OllamaPlugin = class extends import_obsidian4.Plugin {
const files = this.app.vault.getMarkdownFiles(); const files = this.app.vault.getMarkdownFiles();
Logger.info(`Starting background vault indexing for ${files.length} files...`, "main"); Logger.info(`Starting background vault indexing for ${files.length} files...`, "main");
let indexed = 0; let indexed = 0;
const BATCH_SIZE = 2; const BATCH_SIZE = 1;
const DELAY_MS = 500; const DELAY_MS = 500;
for (let i = 0; i < files.length; i += BATCH_SIZE) { for (let i = 0; i < files.length; i += BATCH_SIZE) {
if (signal.aborted) { if (signal.aborted) {
@@ -10041,20 +10041,18 @@ var OllamaPlugin = class extends import_obsidian4.Plugin {
return; return;
} }
const batch = files.slice(i, i + BATCH_SIZE); const batch = files.slice(i, i + BATCH_SIZE);
await Promise.all( for (const file of batch) {
batch.map(async (file) => { if (signal.aborted) break;
if (signal.aborted) return; try {
try { const content = await this.app.vault.read(file);
const content = await this.app.vault.read(file); if (signal.aborted) break;
if (signal.aborted) return; await this.vaultVectorStore.indexFile(file, content);
await this.vaultVectorStore.indexFile(file, content); indexed++;
indexed++; } catch (error) {
} catch (error) { const errorMessage = error instanceof Error ? error.message : String(error);
const errorMessage = error instanceof Error ? error.message : String(error); Logger.warn(`Failed to index ${file.path}: ${errorMessage}`, "main");
Logger.warn(`Failed to index ${file.path}: ${errorMessage}`, "main"); }
} }
})
);
if (i + BATCH_SIZE < files.length) { if (i + BATCH_SIZE < files.length) {
await new Promise((resolve) => setTimeout(resolve, DELAY_MS)); await new Promise((resolve) => setTimeout(resolve, DELAY_MS));
} }
+4 -4
View File
@@ -92,13 +92,13 @@ export class ContentVectorizer {
* Creates a prompt from content chunk for embedding * Creates a prompt from content chunk for embedding
*/ */
private createPrompt(chunk: ContentChunk): string { private createPrompt(chunk: ContentChunk): string {
// Combine important elements for embedding // Combine important elements for embedding, keeping it concise
// to avoid exceeding the embedding model's context window
const parts = [ const parts = [
chunk.title, chunk.title,
chunk.firstParagraph, chunk.firstParagraph,
chunk.content.substring(0, 1000), // Limit content to avoid long prompts chunk.content.substring(0, 500), // Limit content to avoid long prompts
chunk.headings.join(' '), chunk.headings.slice(0, 5).join(' '), // Limit headings
JSON.stringify(chunk.frontmatter),
].filter(Boolean); ].filter(Boolean);
return parts.join('\n\n'); return parts.join('\n\n');
+14 -15
View File
@@ -166,7 +166,7 @@ export default class OllamaPlugin extends Plugin {
Logger.info(`Starting background vault indexing for ${files.length} files...`, 'main'); Logger.info(`Starting background vault indexing for ${files.length} files...`, 'main');
let indexed = 0; let indexed = 0;
const BATCH_SIZE = 2; const BATCH_SIZE = 1;
const DELAY_MS = 500; const DELAY_MS = 500;
for (let i = 0; i < files.length; i += BATCH_SIZE) { for (let i = 0; i < files.length; i += BATCH_SIZE) {
@@ -176,20 +176,19 @@ export default class OllamaPlugin extends Plugin {
} }
const batch = files.slice(i, i + BATCH_SIZE); const batch = files.slice(i, i + BATCH_SIZE);
await Promise.all( // Process files sequentially to avoid concurrent embedding requests
batch.map(async (file) => { for (const file of batch) {
if (signal.aborted) return; if (signal.aborted) break;
try { try {
const content = await this.app.vault.read(file); const content = await this.app.vault.read(file);
if (signal.aborted) return; if (signal.aborted) break;
await this.vaultVectorStore!.indexFile(file, content); await this.vaultVectorStore!.indexFile(file, content);
indexed++; indexed++;
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error); const errorMessage = error instanceof Error ? error.message : String(error);
Logger.warn(`Failed to index ${file.path}: ${errorMessage}`, 'main'); Logger.warn(`Failed to index ${file.path}: ${errorMessage}`, 'main');
} }
}) }
);
// Delay between batches to avoid overloading Ollama // Delay between batches to avoid overloading Ollama
if (i + BATCH_SIZE < files.length) { if (i + BATCH_SIZE < files.length) {
+5 -5
View File
@@ -207,7 +207,7 @@ Content`;
firstParagraph: 'First paragraph', firstParagraph: 'First paragraph',
wordCount: 2, wordCount: 2,
chunkIndex: 0, chunkIndex: 0,
chunkSize: 100 chunkSize: 100,
}; };
const prompt = (vectorizer as any).createPrompt(mockChunk); const prompt = (vectorizer as any).createPrompt(mockChunk);
@@ -215,7 +215,7 @@ Content`;
expect(prompt).toContain('Test'); expect(prompt).toContain('Test');
expect(prompt).toContain('First paragraph'); expect(prompt).toContain('First paragraph');
expect(prompt).toContain('Heading'); expect(prompt).toContain('Heading');
expect(prompt).toContain('tags'); // Frontmatter is no longer included in embedding prompts
}); });
// Note: Actual embedding tests would require mocking fetch or integration testing // Note: Actual embedding tests would require mocking fetch or integration testing
@@ -233,7 +233,7 @@ Content`;
firstParagraph: 'First paragraph', firstParagraph: 'First paragraph',
wordCount: 2, wordCount: 2,
chunkIndex: 0, chunkIndex: 0,
chunkSize: 100 chunkSize: 100,
}; };
// Mock fetch to simulate an error // Mock fetch to simulate an error
@@ -290,12 +290,12 @@ This is a test document for pipeline processing.`;
it('should process files in batches', async () => { it('should process files in batches', async () => {
const files: MockVaultFile[] = [ const files: MockVaultFile[] = [
{ basename: 'file1', path: 'file1.md' }, { basename: 'file1', path: 'file1.md' },
{ basename: 'file2', path: 'file2.md' } { basename: 'file2', path: 'file2.md' },
]; ];
const fileContents = { const fileContents = {
'file1.md': '# File 1\n\nContent 1', 'file1.md': '# File 1\n\nContent 1',
'file2.md': '# File 2\n\nContent 2' 'file2.md': '# File 2\n\nContent 2',
}; };
const results = await pipeline.processFilesInBatches(files, fileContents, 1); const results = await pipeline.processFilesInBatches(files, fileContents, 1);
+3 -5
View File
@@ -200,8 +200,7 @@ describe('ContentVectorizer', () => {
expect(prompt).toContain('This is the first paragraph'); expect(prompt).toContain('This is the first paragraph');
expect(prompt).toContain('Main Heading'); expect(prompt).toContain('Main Heading');
expect(prompt).toContain('Sub Heading'); expect(prompt).toContain('Sub Heading');
expect(prompt).toContain('test'); // Frontmatter is no longer included in embedding prompts
expect(prompt).toContain('2024-01-01');
}); });
it('should handle empty content fields gracefully', () => { it('should handle empty content fields gracefully', () => {
@@ -221,8 +220,7 @@ describe('ContentVectorizer', () => {
const prompt = (vectorizer as any).createPrompt(chunk); const prompt = (vectorizer as any).createPrompt(chunk);
expect(prompt).toContain('Only content'); expect(prompt).toContain('Only content');
// JSON.stringify({}) produces "{}", which is truthy so it's included // Frontmatter is no longer included in embedding prompts
expect(prompt).toContain('{}');
}); });
it('should limit content length', () => { it('should limit content length', () => {
@@ -243,7 +241,7 @@ describe('ContentVectorizer', () => {
const prompt = (vectorizer as any).createPrompt(chunk); const prompt = (vectorizer as any).createPrompt(chunk);
expect(prompt).not.toContain('a'.repeat(1500)); expect(prompt).not.toContain('a'.repeat(1500));
expect(prompt).toContain('a'.repeat(1000)); expect(prompt).toContain('a'.repeat(500));
}); });
it('should handle missing frontmatter gracefully', () => { it('should handle missing frontmatter gracefully', () => {