Files
stack/apps/api/src/knowledge/queues/embedding-queue.service.ts
T
Jason WoltjeandClaude Sonnet 4.5 3dfa603a03 feat(#69): implement embedding generation pipeline
Generate embeddings for knowledge entries using Ollama via BullMQ job queue.

Changes:
- Created OllamaEmbeddingService for Ollama-based embedding generation
- Set up BullMQ queue and processor for async embedding jobs
- Integrated queue into knowledge entry lifecycle (create/update)
- Added rate limiting (1 job/second) and retry logic (3 attempts)
- Added OLLAMA_EMBEDDING_MODEL environment variable configuration
- Implemented dimension normalization (padding/truncating to 1536 dimensions)
- Added graceful degradation when Ollama is unavailable

Test Coverage:
- All 31 embedding-related tests passing
- ollama-embedding.service.spec.ts: 13 tests
- embedding-queue.spec.ts: 6 tests
- embedding.processor.spec.ts: 5 tests
- Build and linting successful

Fixes #69

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
2026-02-02 15:06:11 -06:00

115 lines
3.1 KiB
TypeScript

import { Injectable, Logger } from "@nestjs/common";
import { InjectQueue } from "@nestjs/bullmq";
import { Queue } from "bullmq";
export interface EmbeddingJobData {
entryId: string;
content: string;
model?: string;
}
/**
* Service for managing the embedding generation queue
*
* This service provides an interface to queue embedding jobs
* and manage the queue lifecycle.
*/
@Injectable()
export class EmbeddingQueueService {
private readonly logger = new Logger(EmbeddingQueueService.name);
constructor(
@InjectQueue("embeddings")
private readonly embeddingQueue: Queue<EmbeddingJobData>
) {}
/**
* Queue an embedding generation job
*
* @param entryId - ID of the knowledge entry
* @param content - Content to generate embedding for
* @param model - Optional model override
* @returns Job ID
*/
async queueEmbeddingJob(entryId: string, content: string, model?: string): Promise<string> {
const jobData: EmbeddingJobData = {
entryId,
content,
};
if (model !== undefined) {
jobData.model = model;
}
const job = await this.embeddingQueue.add("generate-embedding", jobData, {
// Retry configuration
attempts: 3,
backoff: {
type: "exponential",
delay: 5000, // Start with 5 seconds
},
// Rate limiting: 1 job per second to avoid overwhelming Ollama
delay: 1000,
// Remove completed jobs after 24 hours
removeOnComplete: {
age: 86400, // 24 hours in seconds
count: 1000, // Keep max 1000 completed jobs
},
// Remove failed jobs after 7 days
removeOnFail: {
age: 604800, // 7 days in seconds
count: 100, // Keep max 100 failed jobs for debugging
},
});
this.logger.log(`Queued embedding job ${job.id ?? "unknown"} for entry ${entryId}`);
return job.id ?? "unknown";
}
/**
* Get queue statistics
*
* @returns Queue job counts
*/
async getQueueStats(): Promise<{
waiting: number;
active: number;
completed: number;
failed: number;
}> {
const counts = await this.embeddingQueue.getJobCounts(
"waiting",
"active",
"completed",
"failed"
);
return {
waiting: counts.waiting ?? 0,
active: counts.active ?? 0,
completed: counts.completed ?? 0,
failed: counts.failed ?? 0,
};
}
/**
* Clean completed jobs older than the grace period
*
* @param gracePeriodMs - Grace period in milliseconds (default: 24 hours)
*/
async cleanCompletedJobs(gracePeriodMs = 86400000): Promise<void> {
await this.embeddingQueue.clean(gracePeriodMs, 100, "completed");
this.logger.log(`Cleaned completed jobs older than ${gracePeriodMs.toString()}ms`);
}
/**
* Clean failed jobs older than the grace period
*
* @param gracePeriodMs - Grace period in milliseconds (default: 7 days)
*/
async cleanFailedJobs(gracePeriodMs = 604800000): Promise<void> {
await this.embeddingQueue.clean(gracePeriodMs, 100, "failed");
this.logger.log(`Cleaned failed jobs older than ${gracePeriodMs.toString()}ms`);
}
}