"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var GraphService_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.GraphService = void 0; const common_1 = require("@nestjs/common"); const prisma_service_1 = require("../prisma/prisma.service"); let GraphService = GraphService_1 = class GraphService { prisma; logger = new common_1.Logger(GraphService_1.name); adjacencyList = new Map(); constructor(prisma) { this.prisma = prisma; } async onModuleInit() { await this.loadEdgesFromDatabase(); } async loadEdgesFromDatabase() { try { const edges = await this.prisma.graphEdge.findMany(); for (const edge of edges) { const sourceId = edge.source; const targetId = edge.target; const storedEdge = { source: edge.source, target: edge.target, type: edge.type, weight: edge.weight, metadata: edge.metadata, }; if (!this.adjacencyList.has(sourceId)) { this.adjacencyList.set(sourceId, new Map()); } this.adjacencyList.get(sourceId).set(targetId, storedEdge); if (edge.type === "knows" || edge.type === "similar_to" || edge.type === "appeared_on") { const reverseEdge = { ...storedEdge, source: edge.target, target: edge.source }; if (!this.adjacencyList.has(edge.target)) { this.adjacencyList.set(edge.target, new Map()); } this.adjacencyList.get(edge.target).set(edge.source, reverseEdge); } if (edge.source.startsWith("guest:") || edge.source.startsWith("podcast:")) { const edges = this.adjacencyList.get(edge.source); if (edge.metadata && typeof edge.metadata === "object") { const meta = edge.metadata; if (meta.nodeData) { edges.nodeData = meta.nodeData; } } } } this.logger.log(`Loaded ${edges.length} graph edges from database`); } catch (error) { this.logger.warn("Failed to load graph edges from database, starting with empty graph", error); } } async ingestProfile(profileId) { this.logger.log(`Ingesting profile ${profileId} into graph`); const profile = await this.prisma.guestPublicProfile.findUnique({ where: { id: profileId }, include: { expertiseTags: true, bookingHistory: true, }, }); if (!profile) { this.logger.warn(`Profile ${profileId} not found`); return; } this.addNode({ type: "guest", id: `guest:${profileId}`, data: { name: profile.headline, country: profile.country, expertise: profile.expertiseTags.map((t) => t.name), tier: profile.tier, }, }); for (const booking of profile.bookingHistory) { const podcastNodeId = `podcast:${booking.podcastName}`; this.addNode({ type: "podcast", id: podcastNodeId, data: { name: booking.podcastName }, }); this.addEdge({ source: `guest:${profileId}`, target: podcastNodeId, type: "appeared_on", weight: 1, metadata: { date: booking.appearedAt, topics: booking.topicsDiscussed, }, }); } await this.calculateSimilarityEdges(profileId, profile.expertiseTags); this.logger.log(`Profile ${profileId} ingested successfully`); } addNode(node) { if (!this.adjacencyList.has(node.id)) { this.adjacencyList.set(node.id, new Map()); } const edges = this.adjacencyList.get(node.id); edges.nodeData = node.data; } addEdge(edge) { if (!this.adjacencyList.has(edge.source)) { this.adjacencyList.set(edge.source, new Map()); } this.adjacencyList.get(edge.source).set(edge.target, edge); if (edge.type === "knows" || edge.type === "similar_to" || edge.type === "appeared_on") { const reverseEdge = { ...edge, source: edge.target, target: edge.source }; if (!this.adjacencyList.has(edge.target)) { this.adjacencyList.set(edge.target, new Map()); } this.adjacencyList.get(edge.target).set(edge.source, reverseEdge); } this.persistEdge(edge).catch(err => this.logger.error(`Failed to persist graph edge`, err)); } async persistEdge(edge) { try { await this.prisma.graphEdge.upsert({ where: { source_target_type: { source: edge.source, target: edge.target, type: edge.type, }, }, create: { source: edge.source, target: edge.target, type: edge.type, weight: edge.weight, metadata: (edge.metadata || {}), }, update: { weight: edge.weight, metadata: (edge.metadata || {}), }, }); } catch (error) { this.logger.error(`Failed to persist edge ${edge.source} -> ${edge.target} (${edge.type})`, error); } } async calculateSimilarityEdges(profileId, expertiseTags) { const expertiseNames = expertiseTags.map((t) => t.name); if (expertiseNames.length === 0) return; const similarGuests = await this.prisma.guestPublicProfile.findMany({ where: { id: { not: profileId }, isPublic: true, expertiseTags: { some: { name: { in: expertiseNames }, }, }, }, include: { expertiseTags: true, }, take: 20, }); for (const other of similarGuests) { const otherExpertise = other.expertiseTags.map((t) => t.name); const sharedExpertise = expertiseNames.filter((e) => otherExpertise.includes(e)); if (sharedExpertise.length > 0) { const similarityScore = sharedExpertise.length / Math.max(expertiseNames.length, otherExpertise.length); this.addEdge({ source: `guest:${profileId}`, target: `guest:${other.id}`, type: "similar_to", weight: similarityScore, metadata: { sharedExpertise, score: similarityScore, }, }); } } } async getRecommendations(profileId, limit = 5) { const nodeId = `guest:${profileId}`; const scores = new Map(); const reasons = new Map(); const appearedOn = this.getNeighbors(nodeId, "appeared_on"); for (const podcastId of appearedOn) { const coGuests = this.getNeighbors(podcastId, "appeared_on") .filter((g) => g !== nodeId); for (const coGuest of coGuests) { const guestId = coGuest.replace("guest:", ""); const currentScore = scores.get(guestId) || 0; scores.set(guestId, currentScore + 1); if (!reasons.has(guestId)) { reasons.set(guestId, []); } const podcastName = this.getNodeData(podcastId)?.name || "a podcast"; reasons.get(guestId).push(`Appeared on ${podcastName}`); } } const similar = this.getNeighbors(nodeId, "similar_to"); for (const simGuest of similar) { const guestId = simGuest.replace("guest:", ""); const edge = this.getEdge(nodeId, simGuest); const currentScore = scores.get(guestId) || 0; const weight = edge?.weight || 0.5; scores.set(guestId, currentScore + weight); if (!reasons.has(guestId)) { reasons.set(guestId, []); } const shared = edge?.metadata?.sharedExpertise || ["similar expertise"]; reasons.get(guestId).push(`Shares expertise in ${shared.slice(0, 2).join(", ")}`); } const sorted = Array.from(scores.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, limit); return sorted.map(([guestId, score]) => ({ guestId, reason: [...new Set(reasons.get(guestId) || [])].join("; "), score, })); } async findTrustPath(fromProfileId, toProfileId) { const start = `guest:${fromProfileId}`; const end = `guest:${toProfileId}`; if (!this.adjacencyList.has(start) || !this.adjacencyList.has(end)) { return null; } const queue = [{ node: start, path: [start] }]; const visited = new Set(); while (queue.length > 0) { const { node, path } = queue.shift(); if (node === end) { return path.map((n) => n.replace("guest:", "").replace("podcast:", "")); } if (visited.has(node)) continue; visited.add(node); const neighbors = this.adjacencyList.get(node); if (neighbors) { for (const [neighbor] of Array.from(neighbors.entries())) { if (!visited.has(neighbor)) { queue.push({ node: neighbor, path: [...path, neighbor] }); } } } } return null; } async getNetworkStats(profileId) { const nodeId = `guest:${profileId}`; if (!this.adjacencyList.has(nodeId)) { return { connections: 0, reach: 0, centrality: 0, communities: [] }; } const directConnections = this.adjacencyList.get(nodeId).size; const visited = new Set(); visited.add(nodeId); const neighbors = this.getAllNeighbors(nodeId); for (const n of Array.from(neighbors)) { visited.add(n); } for (const n of Array.from(neighbors)) { const secondHop = this.getAllNeighbors(n); for (const sh of Array.from(secondHop)) { visited.add(sh); } } const communities = this.identifyCommunities(nodeId); return { connections: directConnections, reach: visited.size - 1, centrality: this.calculateCentrality(nodeId), communities, }; } async getCommunityMembers(communityType, value, limit = 20) { const members = []; for (const [nodeId, edges] of Array.from(this.adjacencyList.entries())) { if (!nodeId.startsWith("guest:")) continue; const data = edges.nodeData; if (!data) continue; if (communityType === "country" && data.country === value) { members.push(nodeId.replace("guest:", "")); } else if (communityType === "expertise" && data.expertise?.includes(value)) { members.push(nodeId.replace("guest:", "")); } } return members.slice(0, limit); } getNeighbors(nodeId, edgeType) { const edges = this.adjacencyList.get(nodeId); if (!edges) return []; return Array.from(edges.entries()) .filter(([, edge]) => edge.type === edgeType) .map(([target]) => target); } getAllNeighbors(nodeId) { const edges = this.adjacencyList.get(nodeId); if (!edges) return []; return Array.from(edges.keys()); } getEdge(source, target) { return this.adjacencyList.get(source)?.get(target); } getNodeData(nodeId) { const edges = this.adjacencyList.get(nodeId); if (!edges) return null; return edges.nodeData; } identifyCommunities(nodeId) { const data = this.getNodeData(nodeId); if (!data) return []; const communities = []; if (data.country) { communities.push(`${data.country} Community`); } if (data.expertise) { communities.push(...data.expertise.slice(0, 2).map((e) => `${e} Network`)); } return communities; } calculateCentrality(nodeId) { const direct = this.adjacencyList.get(nodeId)?.size || 0; const twoHop = new Set(); const neighbors = this.getAllNeighbors(nodeId); for (const n of neighbors) { const secondHop = this.getAllNeighbors(n); for (const sh of secondHop) { if (sh !== nodeId) { twoHop.add(sh); } } } return direct + twoHop.size * 0.5; } getStats() { let edges = 0; let guests = 0; let podcasts = 0; for (const [nodeId] of Array.from(this.adjacencyList.entries())) { if (nodeId.startsWith("guest:")) guests++; if (nodeId.startsWith("podcast:")) podcasts++; edges += this.adjacencyList.get(nodeId).size; } return { nodes: this.adjacencyList.size, edges: edges / 2, guests, podcasts, }; } }; exports.GraphService = GraphService; exports.GraphService = GraphService = GraphService_1 = __decorate([ (0, common_1.Injectable)(), __metadata("design:paramtypes", [prisma_service_1.PrismaService]) ], GraphService); //# sourceMappingURL=graph.service.js.map