feat(cluster): implement cluster detection and bridge notes discovery
Add automatic note clustering using density-based algorithm (DBSCAN variant) and bridge notes detection for connecting different thematic clusters. Features: - NoteCluster, ClusterMember, BridgeNote, BridgeSuggestion models - Clustering service with pgvector cosine similarity - Bridge notes detection (notes connecting >=2 clusters) - AI-powered suggestions for missing cluster connections - /insights page with React Flow visualization - Cron endpoint for automatic recalculation Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@ generator client {
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
extensions = [pgvector]
|
||||
extensions = [vector]
|
||||
}
|
||||
|
||||
model User {
|
||||
@@ -50,6 +50,10 @@ model User {
|
||||
subscription Subscription?
|
||||
usageLogs UsageLog[]
|
||||
apiKeys UserAPIKey[]
|
||||
aiConsentLogs AiConsentLog[]
|
||||
noteClusters NoteCluster[]
|
||||
bridgeNotes BridgeNote[]
|
||||
bridgeSuggestions BridgeSuggestion[]
|
||||
}
|
||||
|
||||
model Account {
|
||||
@@ -185,6 +189,8 @@ model Note {
|
||||
brainstormNoteRefs BrainstormNoteRef[]
|
||||
outgoingLinks NoteLink[] @relation("SourceLinks")
|
||||
incomingLinks NoteLink[] @relation("TargetLinks")
|
||||
clusterMemberships ClusterMember[]
|
||||
bridgeNote BridgeNote?
|
||||
|
||||
@@index([isPinned])
|
||||
@@index([isArchived])
|
||||
@@ -309,6 +315,7 @@ model UserAISettings {
|
||||
noteHistory Boolean @default(false)
|
||||
noteHistoryMode String @default("manual")
|
||||
autoSave Boolean @default(true)
|
||||
aiProcessingConsent Boolean @default(false)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([memoryEcho])
|
||||
@@ -317,6 +324,16 @@ model UserAISettings {
|
||||
@@index([preferredLanguage])
|
||||
}
|
||||
|
||||
model AiConsentLog {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
consent Boolean @default(true)
|
||||
ipAddress String?
|
||||
userAgent String?
|
||||
createdAt DateTime @default(now())
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model NoteEmbedding {
|
||||
id String @id @default(cuid())
|
||||
noteId String @unique
|
||||
@@ -747,3 +764,76 @@ model FeatureFlag {
|
||||
metadata String?
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
// ===== CLUSTERING & BRIDGE NOTES =====
|
||||
|
||||
model NoteCluster {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
clusterId Int // Cluster number for this user (0, 1, 2, ...)
|
||||
name String? // Auto-generated cluster name
|
||||
centroid Unsupported("vector(1536)")? // Optional: centroid embedding for cluster
|
||||
noteCount Int @default(0)
|
||||
lastCalculated DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
@@index([userId, clusterId])
|
||||
@@unique([userId, clusterId])
|
||||
}
|
||||
|
||||
model ClusterMember {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
noteId String
|
||||
clusterId Int // Cluster number
|
||||
membershipScore Float @default(0.0) // How strongly this note belongs to the cluster
|
||||
isCentral Boolean @default(false) // Is this a central note in the cluster?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
note Note @relation(fields: [noteId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId, clusterId])
|
||||
@@index([noteId])
|
||||
@@unique([noteId, clusterId])
|
||||
}
|
||||
|
||||
model BridgeNote {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
noteId String @unique
|
||||
bridgeScore Float // len(clusters_touched) / max_clusters
|
||||
clustersConnected String // JSON array: ["0", "2", "5"]
|
||||
lastCalculated DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
note Note @relation(fields: [noteId], references: [id], onDelete: Cascade)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
@@index([bridgeScore])
|
||||
}
|
||||
|
||||
model BridgeSuggestion {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
clusterAId Int
|
||||
clusterBId Int
|
||||
clusterAName String
|
||||
clusterBName String
|
||||
suggestedTitle String
|
||||
suggestedContent String // @default(dbgenerated)
|
||||
justification String // Why this connection makes sense
|
||||
isDismissed Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
@@index([userId, isDismissed])
|
||||
@@index([clusterAId, clusterBId])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user