import { eq, and, desc, sql, lt, type Db, insights } from '@mosaic/db'; export type Insight = typeof insights.$inferSelect; export type NewInsight = typeof insights.$inferInsert; export interface SearchResult { insight: Insight; distance: number; } export function createInsightsRepo(db: Db) { return { async findByUser(userId: string, limit = 50): Promise { return db .select() .from(insights) .where(eq(insights.userId, userId)) .orderBy(desc(insights.createdAt)) .limit(limit); }, async findById(id: string): Promise { const rows = await db.select().from(insights).where(eq(insights.id, id)); return rows[0]; }, async create(data: NewInsight): Promise { const rows = await db.insert(insights).values(data).returning(); return rows[0]!; }, async update(id: string, data: Partial): Promise { const rows = await db .update(insights) .set({ ...data, updatedAt: new Date() }) .where(eq(insights.id, id)) .returning(); return rows[0]; }, async remove(id: string): Promise { const rows = await db.delete(insights).where(eq(insights.id, id)).returning(); return rows.length > 0; }, /** * Semantic search using pgvector cosine distance. * Requires the vector extension and an embedding for the query. */ async searchByEmbedding( userId: string, queryEmbedding: number[], limit = 10, maxDistance = 0.8, ): Promise { const embeddingStr = `[${queryEmbedding.join(',')}]`; const rows = await db.execute(sql` SELECT *, (embedding <=> ${embeddingStr}::vector) AS distance FROM insights WHERE user_id = ${userId} AND embedding IS NOT NULL AND (embedding <=> ${embeddingStr}::vector) < ${maxDistance} ORDER BY distance ASC LIMIT ${limit} `); return rows as unknown as SearchResult[]; }, /** * Decay relevance scores for old insights that haven't been accessed recently. */ async decayOldInsights(olderThan: Date, decayFactor = 0.95): Promise { const result = await db .update(insights) .set({ relevanceScore: sql`${insights.relevanceScore} * ${decayFactor}`, decayedAt: new Date(), updatedAt: new Date(), }) .where(and(lt(insights.updatedAt, olderThan), sql`${insights.relevanceScore} > 0.1`)) .returning(); return result.length; }, }; } export type InsightsRepo = ReturnType;