Chapter 22: Social Networks and Graph Applications
Overview
Learning Objectives
Prerequisites
- Completed Chapters 1-6 (Foundations and Core Features)
- Understanding of graph theory and social network concepts
- Knowledge of vector embeddings and similarity search
Core Concepts
Social Graph Modeling
Vektagraf treats relationships as first-class objects, making social graph modeling natural:
class User extends VektaObject {
late String username;
late String email;
late List<double> interestVector; // User's interest embedding
late Map<String, dynamic> profile;
late List<String> followers;
late List<String> following;
}
class Post extends VektaObject {
late String content;
late String authorId;
late List<double> contentVector; // Post content embedding
late List<String> tags;
late int likes;
late int shares;
}
// Relationships are explicit objects
class Friendship extends VektaObject {
late String userId1;
late String userId2;
late String status; // pending, accepted, blocked
late DateTime createdAt;
late double strength; // Relationship strength score
}
Graph Traversal Patterns
Vektagraf provides natural graph traversal syntax:
// Find friends of friends
final friendsOfFriends = await user
.traverse('friendships')
.traverse('friendships')
.where('id', notEquals: user.id)
.find();
// Multi-hop traversal with conditions
final recommendations = await user
.traverse('friendships', where: {'status': 'accepted'})
.traverse('posts', limit: 10)
.where('createdAt', greaterThan: DateTime.now().subtract(Duration(days: 7)))
.find();
Practical Examples
Complete Social Network Implementation
Let's build a comprehensive social networking platform:
1. Schema Definition
{
"name": "SocialNetworkPlatform",
"version": "1.0.0",
"objects": {
"User": {
"properties": {
"username": {"type": "string", "required": true, "unique": true},
"email": {"type": "string", "required": true, "unique": true},
"displayName": {"type": "string", "required": true},
"bio": {"type": "string"},
"avatar": {"type": "string"},
"interestVector": {
"type": "vector",
"dimensions": 128,
"algorithm": "hnsw",
"distance": "cosine"
},
"profile": {
"type": "object",
"properties": {
"location": {"type": "string"},
"website": {"type": "string"},
"birthDate": {"type": "date"},
"occupation": {"type": "string"},
"interests": {"type": "array", "items": {"type": "string"}},
"languages": {"type": "array", "items": {"type": "string"}}
}
},
"privacy": {
"type": "object",
"properties": {
"profileVisibility": {"type": "string", "enum": ["public", "friends", "private"], "default": "public"},
"postVisibility": {"type": "string", "enum": ["public", "friends", "private"], "default": "friends"},
"allowFriendRequests": {"type": "boolean", "default": true},
"showOnlineStatus": {"type": "boolean", "default": true}
}
},
"stats": {
"type": "object",
"properties": {
"followerCount": {"type": "integer", "default": 0},
"followingCount": {"type": "integer", "default": 0},
"postCount": {"type": "integer", "default": 0},
"reputation": {"type": "number", "default": 0.0}
}
},
"lastActive": {"type": "datetime"},
"createdAt": {"type": "datetime", "required": true}
}
},
"Post": {
"properties": {
"content": {"type": "text", "required": true},
"authorId": {"type": "string", "required": true},
"contentVector": {
"type": "vector",
"dimensions": 128,
"algorithm": "hnsw",
"distance": "cosine"
},
"type": {"type": "string", "enum": ["text", "image", "video", "link", "poll"], "default": "text"},
"media": {
"type": "array",
"items": {
"type": "object",
"properties": {
"url": {"type": "string"},
"type": {"type": "string"},
"thumbnail": {"type": "string"}
}
}
},
"tags": {"type": "array", "items": {"type": "string"}},
"mentions": {"type": "array", "items": {"type": "string"}},
"visibility": {"type": "string", "enum": ["public", "friends", "private"], "default": "friends"},
"engagement": {
"type": "object",
"properties": {
"likes": {"type": "integer", "default": 0},
"shares": {"type": "integer", "default": 0},
"comments": {"type": "integer", "default": 0},
"views": {"type": "integer", "default": 0}
}
},
"location": {"type": "string"},
"createdAt": {"type": "datetime", "required": true},
"updatedAt": {"type": "datetime"}
}
},
"Friendship": {
"properties": {
"requesterId": {"type": "string", "required": true},
"addresseeId": {"type": "string", "required": true},
"status": {"type": "string", "enum": ["pending", "accepted", "declined", "blocked"], "required": true},
"strength": {"type": "number", "default": 0.0},
"mutualFriends": {"type": "integer", "default": 0},
"interactionScore": {"type": "number", "default": 0.0},
"createdAt": {"type": "datetime", "required": true},
"updatedAt": {"type": "datetime"}
}
},
"Follow": {
"properties": {
"followerId": {"type": "string", "required": true},
"followeeId": {"type": "string", "required": true},
"createdAt": {"type": "datetime", "required": true}
}
},
"Community": {
"properties": {
"name": {"type": "string", "required": true},
"description": {"type": "string"},
"category": {"type": "string"},
"memberIds": {"type": "array", "items": {"type": "string"}},
"adminIds": {"type": "array", "items": {"type": "string"}},
"interestVector": {
"type": "vector",
"dimensions": 128,
"algorithm": "hnsw",
"distance": "cosine"
},
"privacy": {"type": "string", "enum": ["public", "private", "invite_only"], "default": "public"},
"stats": {
"type": "object",
"properties": {
"memberCount": {"type": "integer", "default": 0},
"postCount": {"type": "integer", "default": 0},
"activityScore": {"type": "number", "default": 0.0}
}
},
"createdAt": {"type": "datetime", "required": true}
}
},
"Activity": {
"properties": {
"userId": {"type": "string", "required": true},
"type": {"type": "string", "enum": ["post", "like", "comment", "share", "friend_request", "join_community"], "required": true},
"targetId": {"type": "string", "required": true},
"targetType": {"type": "string", "enum": ["user", "post", "community"], "required": true},
"metadata": {"type": "object"},
"visibility": {"type": "string", "enum": ["public", "friends", "private"], "default": "friends"},
"timestamp": {"type": "datetime", "required": true}
}
}
},
"relationships": {
"UserPosts": {
"from": "User",
"to": "Post",
"type": "one_to_many",
"foreignKey": "authorId"
},
"UserFriendships": {
"from": "User",
"to": "Friendship",
"type": "many_to_many",
"conditions": ["requesterId = User.id OR addresseeId = User.id"]
},
"UserFollows": {
"from": "User",
"to": "Follow",
"type": "one_to_many",
"foreignKey": "followerId"
},
"UserActivities": {
"from": "User",
"to": "Activity",
"type": "one_to_many",
"foreignKey": "userId"
}
}
}
2. Friend Recommendation System
class FriendRecommendationEngine {
final VektaDatabase db;
final EmbeddingService embeddingService;
FriendRecommendationEngine(this.db, this.embeddingService);
/// Generate friend recommendations using multiple algorithms
Future<List<FriendRecommendation>> generateRecommendations(
String userId, {
int limit = 20,
}) async {
final user = await db.users.findById(userId);
if (user == null) return [];
// Get recommendations from different algorithms
final mutualFriends = await _getMutualFriendsRecommendations(user, limit);
final interestBased = await _getInterestBasedRecommendations(user, limit);
final networkBased = await _getNetworkBasedRecommendations(user, limit);
final locationBased = await _getLocationBasedRecommendations(user, limit ~/ 4);
// Combine and rank recommendations
final combined = _combineRecommendations([
mutualFriends,
interestBased,
networkBased,
locationBased,
], weights: [0.4, 0.3, 0.2, 0.1]);
// Filter out existing friends and blocked users
final filtered = await _filterExistingConnections(combined, userId);
return filtered.take(limit).toList();
}
/// Recommendations based on mutual friends
Future<List<FriendRecommendation>> _getMutualFriendsRecommendations(
User user,
int limit,
) async {
// Get user's friends
final friendships = await db.friendships
.where('status', 'accepted')
.where(Query.or([
Query.where('requesterId', user.id),
Query.where('addresseeId', user.id),
]))
.find();
final friendIds = friendships.map((f) =>
f.requesterId == user.id ? f.addresseeId : f.requesterId).toList();
if (friendIds.isEmpty) return [];
// Find friends of friends
final friendsOfFriends = await db.friendships
.where('status', 'accepted')
.where(Query.or([
Query.where('requesterId', whereIn: friendIds),
Query.where('addresseeId', whereIn: friendIds),
]))
.find();
// Count mutual friends for each candidate
final mutualCounts = <String, int>{};
final mutualFriendsList = <String, List<String>>{};
for (final friendship in friendsOfFriends) {
final candidateId = friendIds.contains(friendship.requesterId)
? friendship.addresseeId
: friendship.requesterId;
if (candidateId != user.id && !friendIds.contains(candidateId)) {
mutualCounts[candidateId] = (mutualCounts[candidateId] ?? 0) + 1;
mutualFriendsList[candidateId] =
(mutualFriendsList[candidateId] ?? [])..add(
friendIds.contains(friendship.requesterId)
? friendship.requesterId
: friendship.addresseeId);
}
}
// Sort by mutual friend count
final sortedCandidates = mutualCounts.entries.toList()
..sort((a, b) => b.value.compareTo(a.value));
final recommendations = <FriendRecommendation>[];
for (final entry in sortedCandidates.take(limit)) {
final candidate = await db.users.findById(entry.key);
if (candidate != null) {
recommendations.add(FriendRecommendation(
user: candidate,
score: entry.value.toDouble() / friendIds.length,
algorithm: 'mutual_friends',
reason: '${entry.value} mutual friends',
mutualFriends: mutualFriendsList[entry.key] ?? [],
));
}
}
return recommendations;
}
/// Recommendations based on interest similarity
Future<List<FriendRecommendation>> _getInterestBasedRecommendations(
User user,
int limit,
) async {
if (user.interestVector.isEmpty) {
await _updateUserInterestVector(user);
}
// Find users with similar interests
final similarUsers = await db.users
.vectorSearch('interestVector', user.interestVector, limit: limit * 2)
.where('id', notEquals: user.id)
.find();
final recommendations = <FriendRecommendation>[];
for (final candidate in similarUsers) {
final similarity = _calculateCosineSimilarity(
user.interestVector,
candidate.interestVector,
);
if (similarity > 0.6) {
recommendations.add(FriendRecommendation(
user: candidate,
score: similarity,
algorithm: 'interest_similarity',
reason: 'Similar interests',
commonInterests: _findCommonInterests(user, candidate),
));
}
}
recommendations.sort((a, b) => b.score.compareTo(a.score));
return recommendations.take(limit).toList();
}
/// Network-based recommendations using graph algorithms
Future<List<FriendRecommendation>> _getNetworkBasedRecommendations(
User user,
int limit,
) async {
// Use PageRank-like algorithm to find influential users in network
final networkScores = await _calculateNetworkInfluence(user.id);
final recommendations = <FriendRecommendation>[];
for (final entry in networkScores.entries.take(limit)) {
final candidate = await db.users.findById(entry.key);
if (candidate != null) {
recommendations.add(FriendRecommendation(
user: candidate,
score: entry.value,
algorithm: 'network_influence',
reason: 'Popular in your network',
));
}
}
return recommendations;
}
/// Update user interest vector based on activities
Future<void> _updateUserInterestVector(User user) async {
// Get user's recent posts and interactions
final posts = await db.posts
.where('authorId', user.id)
.orderBy('createdAt', descending: true)
.limit(50)
.find();
final activities = await db.activities
.where('userId', user.id)
.where('type', whereIn: ['like', 'comment', 'share'])
.orderBy('timestamp', descending: true)
.limit(100)
.find();
// Combine content from posts and liked content
final contentTexts = <String>[];
// Add user's own posts
for (final post in posts) {
contentTexts.add(post.content);
}
// Add content from liked posts
final likedPostIds = activities
.where((a) => a.type == 'like' && a.targetType == 'post')
.map((a) => a.targetId)
.toList();
if (likedPostIds.isNotEmpty) {
final likedPosts = await db.posts
.where('id', whereIn: likedPostIds)
.find();
for (final post in likedPosts) {
contentTexts.add(post.content);
}
}
// Generate interest vector from combined content
if (contentTexts.isNotEmpty) {
final combinedContent = contentTexts.join(' ');
user.interestVector = await embeddingService.generateEmbedding(combinedContent);
await db.users.save(user);
}
}
}
3. Community Detection and Management
class CommunityDetectionEngine {
final VektaDatabase db;
CommunityDetectionEngine(this.db);
/// Detect communities using graph clustering algorithms
Future<List<Community>> detectCommunities({
int minSize = 5,
double threshold = 0.3,
}) async {
// Get all friendships
final friendships = await db.friendships
.where('status', 'accepted')
.find();
// Build adjacency graph
final graph = _buildFriendshipGraph(friendships);
// Apply Louvain algorithm for community detection
final communities = await _louvainClustering(graph, threshold);
// Filter communities by minimum size
final validCommunities = communities
.where((c) => c.memberIds.length >= minSize)
.toList();
// Create community objects
final results = <Community>[];
for (final communityData in validCommunities) {
final community = await _createCommunity(communityData);
results.add(community);
}
return results;
}
/// Suggest communities for user to join
Future<List<CommunityRecommendation>> suggestCommunities(
String userId, {
int limit = 10,
}) async {
final user = await db.users.findById(userId);
if (user == null) return [];
// Get user's current communities
final currentCommunities = await db.communities
.where('memberIds', arrayContains: userId)
.find();
final currentCommunityIds = currentCommunities.map((c) => c.id).toSet();
// Find communities with similar interests
final interestBasedCommunities = await db.communities
.vectorSearch('interestVector', user.interestVector, limit: limit * 2)
.where('id', whereNotIn: currentCommunityIds.toList())
.find();
// Find communities with friends
final friendIds = await _getUserFriendIds(userId);
final friendBasedCommunities = await db.communities
.where('memberIds', arrayContainsAny: friendIds)
.where('id', whereNotIn: currentCommunityIds.toList())
.find();
// Combine and score recommendations
final recommendations = <CommunityRecommendation>[];
// Interest-based recommendations
for (final community in interestBasedCommunities) {
final similarity = _calculateCosineSimilarity(
user.interestVector,
community.interestVector,
);
recommendations.add(CommunityRecommendation(
community: community,
score: similarity,
reason: 'Matches your interests',
algorithm: 'interest_similarity',
));
}
// Friend-based recommendations
for (final community in friendBasedCommunities) {
final mutualFriends = community.memberIds
.where((id) => friendIds.contains(id))
.length;
recommendations.add(CommunityRecommendation(
community: community,
score: mutualFriends / friendIds.length,
reason: '$mutualFriends friends are members',
algorithm: 'friend_based',
mutualFriends: mutualFriends,
));
}
// Remove duplicates and sort by score
final uniqueRecommendations = <String, CommunityRecommendation>{};
for (final rec in recommendations) {
final existing = uniqueRecommendations[rec.community.id];
if (existing == null || rec.score > existing.score) {
uniqueRecommendations[rec.community.id] = rec;
}
}
final result = uniqueRecommendations.values.toList()
..sort((a, b) => b.score.compareTo(a.score));
return result.take(limit).toList();
}
/// Create community from detected cluster
Future<Community> _createCommunity(CommunityCluster cluster) async {
// Calculate community interest vector
final memberVectors = <List<double>>[];
for (final memberId in cluster.memberIds) {
final user = await db.users.findById(memberId);
if (user != null && user.interestVector.isNotEmpty) {
memberVectors.add(user.interestVector);
}
}
final interestVector = _calculateCentroid(memberVectors);
// Generate community name based on common interests
final communityName = await _generateCommunityName(cluster.memberIds);
final community = Community()
..name = communityName
..description = 'Auto-detected community based on social connections'
..memberIds = cluster.memberIds
..adminIds = [cluster.memberIds.first] // First member as admin
..interestVector = interestVector
..privacy = 'public'
..stats = {
'memberCount': cluster.memberIds.length,
'postCount': 0,
'activityScore': cluster.cohesion,
}
..createdAt = DateTime.now();
await db.communities.save(community);
return community;
}
/// Apply Louvain algorithm for community detection
Future<List<CommunityCluster>> _louvainClustering(
Map<String, Set<String>> graph,
double threshold,
) async {
// Initialize each node as its own community
final communities = <String, String>{};
for (final node in graph.keys) {
communities[node] = node;
}
bool improved = true;
while (improved) {
improved = false;
for (final node in graph.keys) {
final currentCommunity = communities[node]!;
String bestCommunity = currentCommunity;
double bestGain = 0.0;
// Check neighboring communities
final neighbors = graph[node] ?? {};
final neighborCommunities = neighbors
.map((n) => communities[n]!)
.toSet();
for (final neighborCommunity in neighborCommunities) {
if (neighborCommunity != currentCommunity) {
final gain = _calculateModularityGain(
node, currentCommunity, neighborCommunity, graph, communities);
if (gain > bestGain && gain > threshold) {
bestGain = gain;
bestCommunity = neighborCommunity;
}
}
}
if (bestCommunity != currentCommunity) {
communities[node] = bestCommunity;
improved = true;
}
}
}
// Group nodes by community
final communityGroups = <String, List<String>>{};
for (final entry in communities.entries) {
communityGroups[entry.value] =
(communityGroups[entry.value] ?? [])..add(entry.key);
}
// Create community clusters
return communityGroups.values.map((memberIds) => CommunityCluster(
memberIds: memberIds,
cohesion: _calculateCohesion(memberIds, graph),
)).toList();
}
}
4. Activity Feed and Notification System
class ActivityFeedManager {
final VektaDatabase db;
final NotificationService notificationService;
ActivityFeedManager(this.db, this.notificationService);
/// Generate personalized activity feed
Future<List<FeedItem>> generateFeed(
String userId, {
int limit = 50,
String? cursor,
}) async {
final user = await db.users.findById(userId);
if (user == null) return [];
// Get user's social graph
final friendIds = await _getUserFriendIds(userId);
final followingIds = await _getUserFollowingIds(userId);
final communityIds = await _getUserCommunityIds(userId);
// Get activities from social network
final activities = await _getNetworkActivities(
friendIds, followingIds, communityIds, limit * 2, cursor);
// Score and rank activities
final scoredActivities = await _scoreActivities(activities, user);
// Apply diversity and freshness filters
final diverseActivities = _applyDiversityFilter(scoredActivities);
// Convert to feed items
final feedItems = await _convertToFeedItems(diverseActivities);
return feedItems.take(limit).toList();
}
/// Record user activity
Future<void> recordActivity(
String userId,
String type,
String targetId,
String targetType, {
Map<String, dynamic>? metadata,
String visibility = 'friends',
}) async {
final activity = Activity()
..userId = userId
..type = type
..targetId = targetId
..targetType = targetType
..metadata = metadata ?? {}
..visibility = visibility
..timestamp = DateTime.now();
await db.activities.save(activity);
// Trigger notifications for relevant users
await _triggerActivityNotifications(activity);
// Update user stats
await _updateUserStats(userId, type);
}
/// Score activities for personalized ranking
Future<List<ScoredActivity>> _scoreActivities(
List<Activity> activities,
User user,
) async {
final scoredActivities = <ScoredActivity>[];
for (final activity in activities) {
double score = 0.0;
// Recency score (exponential decay)
final hoursSince = DateTime.now().difference(activity.timestamp).inHours;
final recencyScore = math.exp(-hoursSince / 24.0); // 24-hour half-life
score += recencyScore * 0.3;
// Relationship strength score
final relationshipScore = await _getRelationshipStrength(user.id, activity.userId);
score += relationshipScore * 0.4;
// Content relevance score
if (activity.type == 'post') {
final post = await db.posts.findById(activity.targetId);
if (post != null && post.contentVector.isNotEmpty && user.interestVector.isNotEmpty) {
final relevanceScore = _calculateCosineSimilarity(
user.interestVector,
post.contentVector,
);
score += relevanceScore * 0.2;
}
}
// Engagement score
final engagementScore = await _getActivityEngagement(activity);
score += engagementScore * 0.1;
scoredActivities.add(ScoredActivity(
activity: activity,
score: score,
));
}
scoredActivities.sort((a, b) => b.score.compareTo(a.score));
return scoredActivities;
}
/// Apply diversity filter to avoid echo chambers
List<ScoredActivity> _applyDiversityFilter(List<ScoredActivity> activities) {
final diverseActivities = <ScoredActivity>[];
final seenAuthors = <String>{};
final seenTypes = <String, int>{};
for (final activity in activities) {
// Limit activities per author
if (seenAuthors.contains(activity.activity.userId)) {
if (seenAuthors.length > activities.length * 0.3) continue;
}
// Limit activities per type
final typeCount = seenTypes[activity.activity.type] ?? 0;
if (typeCount > activities.length * 0.4) continue;
diverseActivities.add(activity);
seenAuthors.add(activity.activity.userId);
seenTypes[activity.activity.type] = typeCount + 1;
}
return diverseActivities;
}
/// Trigger notifications for activity
Future<void> _triggerActivityNotifications(Activity activity) async {
switch (activity.type) {
case 'friend_request':
await notificationService.sendNotification(
activity.targetId,
NotificationType.friendRequest,
{
'requesterId': activity.userId,
'message': 'sent you a friend request',
},
);
break;
case 'like':
if (activity.targetType == 'post') {
final post = await db.posts.findById(activity.targetId);
if (post != null) {
await notificationService.sendNotification(
post.authorId,
NotificationType.postLike,
{
'likerId': activity.userId,
'postId': activity.targetId,
'message': 'liked your post',
},
);
}
}
break;
case 'comment':
if (activity.targetType == 'post') {
final post = await db.posts.findById(activity.targetId);
if (post != null) {
await notificationService.sendNotification(
post.authorId,
NotificationType.postComment,
{
'commenterId': activity.userId,
'postId': activity.targetId,
'message': 'commented on your post',
},
);
}
}
break;
}
}
}
5. Privacy Controls and Social Graph Analytics
class SocialPrivacyManager {
final VektaDatabase db;
SocialPrivacyManager(this.db);
/// Check if user can see another user's content
Future<bool> canViewContent(
String viewerId,
String contentOwnerId,
String contentType,
String visibility,
) async {
// Public content is always visible
if (visibility == 'public') return true;
// Owner can always see their own content
if (viewerId == contentOwnerId) return true;
// Private content only visible to owner
if (visibility == 'private') return false;
// Friends-only content requires friendship
if (visibility == 'friends') {
return await _areFriends(viewerId, contentOwnerId);
}
return false;
}
/// Apply privacy filtering to social graph queries
Future<List<User>> filterUsersByPrivacy(
List<User> users,
String viewerId,
) async {
final filteredUsers = <User>[];
for (final user in users) {
if (await canViewContent(
viewerId, user.id, 'profile', user.privacy['profileVisibility'])) {
filteredUsers.add(user);
}
}
return filteredUsers;
}
/// Anonymize social graph data for analytics
Future<Map<String, dynamic>> getAnonymizedGraphMetrics() async {
final friendships = await db.friendships
.where('status', 'accepted')
.find();
final users = await db.users.find();
// Calculate network metrics without exposing individual data
final metrics = {
'totalUsers': users.length,
'totalFriendships': friendships.length,
'averageFriends': friendships.length * 2 / users.length,
'networkDensity': _calculateNetworkDensity(users.length, friendships.length),
'clusteringCoefficient': await _calculateClusteringCoefficient(),
'communityCount': await _countCommunities(),
};
return metrics;
}
/// Generate privacy-preserving user insights
Future<Map<String, dynamic>> getUserInsights(String userId) async {
final user = await db.users.findById(userId);
if (user == null) return {};
// Only return aggregated, non-identifying insights
final friendships = await db.friendships
.where('status', 'accepted')
.where(Query.or([
Query.where('requesterId', userId),
Query.where('addresseeId', userId),
]))
.find();
final activities = await db.activities
.where('userId', userId)
.where('timestamp', greaterThan: DateTime.now().subtract(Duration(days: 30)))
.find();
return {
'socialScore': _calculateSocialScore(friendships, activities),
'activityLevel': _categorizeActivityLevel(activities.length),
'networkPosition': await _calculateNetworkPosition(userId),
'communityInvolvement': await _calculateCommunityInvolvement(userId),
};
}
Future<bool> _areFriends(String userId1, String userId2) async {
final friendship = await db.friendships
.where('status', 'accepted')
.where(Query.or([
Query.and([
Query.where('requesterId', userId1),
Query.where('addresseeId', userId2),
]),
Query.and([
Query.where('requesterId', userId2),
Query.where('addresseeId', userId1),
]),
]))
.findFirst();
return friendship != null;
}
}
Best Practices
1. Graph Query Optimization
class GraphQueryOptimizer {
/// Optimize friend-of-friend queries
Future<List<User>> optimizedFriendsOfFriends(String userId) async {
// Use indexed queries instead of multiple traversals
final friendIds = await db.friendships
.where('status', 'accepted')
.where(Query.or([
Query.where('requesterId', userId),
Query.where('addresseeId', userId),
]))
.select(['requesterId', 'addresseeId'])
.find()
.then((friendships) => friendships.map((f) =>
f.requesterId == userId ? f.addresseeId : f.requesterId).toList());
if (friendIds.isEmpty) return [];
// Single query for friends of friends
final friendsOfFriends = await db.friendships
.where('status', 'accepted')
.where(Query.or([
Query.where('requesterId', whereIn: friendIds),
Query.where('addresseeId', whereIn: friendIds),
]))
.find();
final candidateIds = <String>{};
for (final friendship in friendsOfFriends) {
final candidateId = friendIds.contains(friendship.requesterId)
? friendship.addresseeId
: friendship.requesterId;
if (candidateId != userId && !friendIds.contains(candidateId)) {
candidateIds.add(candidateId);
}
}
return await db.users
.where('id', whereIn: candidateIds.toList())
.find();
}
/// Batch load social graph data
Future<Map<String, List<String>>> batchLoadFriendships(
List<String> userIds,
) async {
final friendships = await db.friendships
.where('status', 'accepted')
.where(Query.or([
Query.where('requesterId', whereIn: userIds),
Query.where('addresseeId', whereIn: userIds),
]))
.find();
final userFriends = <String, List<String>>{};
for (final friendship in friendships) {
// Add friendship for requester
if (userIds.contains(friendship.requesterId)) {
userFriends[friendship.requesterId] =
(userFriends[friendship.requesterId] ?? [])..add(friendship.addresseeId);
}
// Add friendship for addressee
if (userIds.contains(friendship.addresseeId)) {
userFriends[friendship.addresseeId] =
(userFriends[friendship.addresseeId] ?? [])..add(friendship.requesterId);
}
}
return userFriends;
}
}
2. Real-Time Updates and Caching
class SocialGraphCache {
final Map<String, List<String>> _friendsCache = {};
final Map<String, DateTime> _cacheTimestamps = {};
final Duration _cacheExpiry = Duration(minutes: 30);
/// Get cached friends list
Future<List<String>> getCachedFriends(String userId) async {
final timestamp = _cacheTimestamps[userId];
if (timestamp != null &&
DateTime.now().difference(timestamp) < _cacheExpiry &&
_friendsCache.containsKey(userId)) {
return _friendsCache[userId]!;
}
// Load fresh data
final friends = await _loadUserFriends(userId);
_friendsCache[userId] = friends;
_cacheTimestamps[userId] = DateTime.now();
return friends;
}
/// Invalidate cache when friendships change
Future<void> invalidateFriendsCache(String userId1, String userId2) async {
_friendsCache.remove(userId1);
_friendsCache.remove(userId2);
_cacheTimestamps.remove(userId1);
_cacheTimestamps.remove(userId2);
}
/// Preload cache for active users
Future<void> preloadActiveUserCaches() async {
final activeUsers = await db.users
.where('lastActive', greaterThan: DateTime.now().subtract(Duration(hours: 24)))
.select(['id'])
.find();
// Batch load friendships for active users
final userIds = activeUsers.map((u) => u.id).toList();
final friendships = await batchLoadFriendships(userIds);
// Update cache
for (final entry in friendships.entries) {
_friendsCache[entry.key] = entry.value;
_cacheTimestamps[entry.key] = DateTime.now();
}
}
}
Advanced Topics
Social Graph Machine Learning
class SocialGraphML {
/// Predict friendship likelihood using graph features
Future<double> predictFriendshipProbability(
String userId1,
String userId2,
) async {
final features = await _extractGraphFeatures(userId1, userId2);
// Use trained model to predict friendship probability
final probability = await _friendshipModel.predict(features);
return probability;
}
/// Extract graph-based features for ML models
Future<Map<String, double>> _extractGraphFeatures(
String userId1,
String userId2,
) async {
final user1 = await db.users.findById(userId1);
final user2 = await db.users.findById(userId2);
if (user1 == null || user2 == null) return {};
// Calculate various graph features
final mutualFriends = await _countMutualFriends(userId1, userId2);
final commonCommunities = await _countCommonCommunities(userId1, userId2);
final interestSimilarity = _calculateCosineSimilarity(
user1.interestVector,
user2.interestVector,
);
final shortestPath = await _calculateShortestPath(userId1, userId2);
final clusteringCoefficient1 = await _calculateUserClusteringCoefficient(userId1);
const clusteringCoefficient2 = await _calculateUserClusteringCoefficient(userId2);
return {
'mutual_friends': mutualFriends.toDouble(),
'common_communities': commonCommunities.toDouble(),
'interest_similarity': interestSimilarity,
'shortest_path': shortestPath.toDouble(),
'clustering_coefficient_1': clusteringCoefficient1,
'clustering_coefficient_2': clusteringCoefficient2,
'age_difference': (user1.profile['age'] - user2.profile['age']).abs().toDouble(),
'location_match': user1.profile['location'] == user2.profile['location'] ? 1.0 : 0.0,
};
}
}
Summary
This chapter demonstrated how to build sophisticated social networks and graph applications using Vektagraf's native graph capabilities. Key takeaways include:
- Graph Modeling: Use relationships as first-class objects for complex social structures
- Friend Recommendations: Combine multiple algorithms for accurate suggestions
- Community Detection: Apply graph clustering algorithms to find natural communities
- Activity Feeds: Create personalized, diverse feeds with real-time updates
- Privacy Controls: Implement fine-grained privacy and content filtering
- Performance: Optimize graph queries and implement effective caching strategies
Vektagraf's graph-first approach makes it particularly well-suited for social applications, as relationships are treated as first-class objects with properties and can be queried efficiently.
Next Steps
- Chapter 23: AI/ML Integration Patterns - Learn advanced ML integration for social features
- Part VII: Reference documentation for complete API coverage
- Chapter 14: Performance Tuning - Optimize for social network scale
Related Resources
- Graph Operations Documentation (Chapter 6)
- Vector Search (Chapter 5)
- Security and Privacy (Chapters 8-10)
- Performance Optimization (Chapter 7)