Fix Jaccard similarity calculation to include incoming followsUnverified
e635425 parent: e7fd77c modified
internal/cluster/jaccard.go +9 -6 | @@ -606,18 +606,21 @@ func (e *Engine) ComputeUserSimilarityForUser(ctx context.Context, userDID strin | ||
| 606 | 606 | followQuery := fmt.Sprintf(` |
| 607 | 607 | INSERT INTO user_similarity (user_a, user_b, jaccard, common_feeds, common_likes, common_tags) |
| 608 | 608 | SELECT |
| 609 | - MIN(?, f.target_did), | |
| 610 | - MAX(?, f.target_did), | |
| 609 | + MIN(?, peer_did), | |
| 610 | + MAX(?, peer_did), | |
| 611 | 611 | %g, |
| 612 | 612 | 0, 0, 0 |
| 613 | - FROM follows f | |
| 614 | - WHERE f.user_did = ? AND f.target_did != ? | |
| 615 | - GROUP BY MIN(?, f.target_did), MAX(?, f.target_did) | |
| 613 | + FROM ( | |
| 614 | + SELECT target_did AS peer_did FROM follows WHERE user_did = ? AND target_did != ? | |
| 615 | + UNION | |
| 616 | + SELECT user_did AS peer_did FROM follows WHERE target_did = ? AND user_did != ? | |
| 617 | + ) | |
| 618 | + GROUP BY MIN(?, peer_did), MAX(?, peer_did) | |
| 616 | 619 | ON CONFLICT(user_a, user_b) DO UPDATE SET |
| 617 | 620 | jaccard = jaccard + %g |
| 618 | 621 | `, e.config.FollowBoost, e.config.FollowBoost) |
| 619 | 622 | |
| 620 | - if _, err := tx.ExecContext(ctx, followQuery, userDID, userDID, userDID, userDID, userDID, userDID); err != nil { | |
| 623 | + if _, err := tx.ExecContext(ctx, followQuery, userDID, userDID, userDID, userDID, userDID, userDID, userDID, userDID); err != nil { | |
| 621 | 624 | return err |
| 622 | 625 | } |
| 623 | 626 | |
| @@ -606,18 +606,21 @@ func (e *Engine) ComputeUserSimilarityForUser(ctx context.Context, userDID strin | |||
| 606 | followQuery := fmt.Sprintf(` | 606 | followQuery := fmt.Sprintf(` |
| 607 | INSERT INTO user_similarity (user_a, user_b, jaccard, common_feeds, common_likes, common_tags) | 607 | INSERT INTO user_similarity (user_a, user_b, jaccard, common_feeds, common_likes, common_tags) |
| 608 | SELECT | 608 | SELECT |
| 609 | - MIN(?, f.target_did), | 609 | + MIN(?, peer_did), |
| 610 | - MAX(?, f.target_did), | 610 | + MAX(?, peer_did), |
| 611 | %g, | 611 | %g, |
| 612 | 0, 0, 0 | 612 | 0, 0, 0 |
| 613 | - FROM follows f | 613 | + FROM ( |
| 614 | - WHERE f.user_did = ? AND f.target_did != ? | 614 | + SELECT target_did AS peer_did FROM follows WHERE user_did = ? AND target_did != ? |
| 615 | - GROUP BY MIN(?, f.target_did), MAX(?, f.target_did) | 615 | + UNION |
| 616 | + SELECT user_did AS peer_did FROM follows WHERE target_did = ? AND user_did != ? | ||
| 617 | + ) | ||
| 618 | + GROUP BY MIN(?, peer_did), MAX(?, peer_did) | ||
| 616 | ON CONFLICT(user_a, user_b) DO UPDATE SET | 619 | ON CONFLICT(user_a, user_b) DO UPDATE SET |
| 617 | jaccard = jaccard + %g | 620 | jaccard = jaccard + %g |
| 618 | `, e.config.FollowBoost, e.config.FollowBoost) | 621 | `, e.config.FollowBoost, e.config.FollowBoost) |
| 619 | 622 | ||
| 620 | - if _, err := tx.ExecContext(ctx, followQuery, userDID, userDID, userDID, userDID, userDID, userDID); err != nil { | 623 | + if _, err := tx.ExecContext(ctx, followQuery, userDID, userDID, userDID, userDID, userDID, userDID, userDID, userDID); err != nil { |
| 621 | return err | 624 | return err |
| 622 | } | 625 | } |
| 623 | 626 | ||
modified
internal/cluster/jaccard_test.go +26 -0 | @@ -242,6 +242,32 @@ func TestComputeUserSimilarityForUser_WithFollowBoost(t *testing.T) { | ||
| 242 | 242 | assert.Assert(t, jaccard > 0.6, "follow boost should add 0.5 to subscription jaccard, got %f", jaccard) |
| 243 | 243 | } |
| 244 | 244 | |
| 245 | +func TestComputeUserSimilarityForUser_IncomingFollow(t *testing.T) { | |
| 246 | + ctx := context.Background() | |
| 247 | + database := setupClusterTestDB(t) | |
| 248 | + seedClusterData(t, ctx, database) | |
| 249 | + | |
| 250 | + _, err := database.ExecContext(ctx, `INSERT INTO follows (user_did, target_did) VALUES (?, ?)`, "did:test:bob", "did:test:alice") | |
| 251 | + assert.NilError(t, err) | |
| 252 | + | |
| 253 | + engine := NewEngine(database.DB, slog.Default()) | |
| 254 | + assert.NilError(t, engine.ComputeUserSimilarity(ctx)) | |
| 255 | + | |
| 256 | + var jaccardBefore float64 | |
| 257 | + assert.NilError(t, database.QueryRowContext(ctx, | |
| 258 | + `SELECT jaccard FROM user_similarity WHERE user_a = ? AND user_b = ?`, | |
| 259 | + "did:test:alice", "did:test:bob").Scan(&jaccardBefore)) | |
| 260 | + | |
| 261 | + assert.NilError(t, engine.ComputeUserSimilarityForUser(ctx, "did:test:alice")) | |
| 262 | + | |
| 263 | + var jaccardAfter float64 | |
| 264 | + assert.NilError(t, database.QueryRowContext(ctx, | |
| 265 | + `SELECT jaccard FROM user_similarity WHERE user_a = ? AND user_b = ?`, | |
| 266 | + "did:test:alice", "did:test:bob").Scan(&jaccardAfter)) | |
| 267 | + | |
| 268 | + assert.Assert(t, jaccardAfter > 0, "incoming follow boost should survive per-user recomputation, got %f", jaccardAfter) | |
| 269 | +} | |
| 270 | + | |
| 245 | 271 | func TestComputeRecommendationsForUser(t *testing.T) { |
| 246 | 272 | ctx := context.Background() |
| 247 | 273 | database := setupClusterTestDB(t) |
| @@ -242,6 +242,32 @@ func TestComputeUserSimilarityForUser_WithFollowBoost(t *testing.T) { | |||
| 242 | assert.Assert(t, jaccard > 0.6, "follow boost should add 0.5 to subscription jaccard, got %f", jaccard) | 242 | assert.Assert(t, jaccard > 0.6, "follow boost should add 0.5 to subscription jaccard, got %f", jaccard) |
| 243 | } | 243 | } |
| 244 | 244 | ||
| 245 | +func TestComputeUserSimilarityForUser_IncomingFollow(t *testing.T) { | ||
| 246 | + ctx := context.Background() | ||
| 247 | + database := setupClusterTestDB(t) | ||
| 248 | + seedClusterData(t, ctx, database) | ||
| 249 | + | ||
| 250 | + _, err := database.ExecContext(ctx, `INSERT INTO follows (user_did, target_did) VALUES (?, ?)`, "did:test:bob", "did:test:alice") | ||
| 251 | + assert.NilError(t, err) | ||
| 252 | + | ||
| 253 | + engine := NewEngine(database.DB, slog.Default()) | ||
| 254 | + assert.NilError(t, engine.ComputeUserSimilarity(ctx)) | ||
| 255 | + | ||
| 256 | + var jaccardBefore float64 | ||
| 257 | + assert.NilError(t, database.QueryRowContext(ctx, | ||
| 258 | + `SELECT jaccard FROM user_similarity WHERE user_a = ? AND user_b = ?`, | ||
| 259 | + "did:test:alice", "did:test:bob").Scan(&jaccardBefore)) | ||
| 260 | + | ||
| 261 | + assert.NilError(t, engine.ComputeUserSimilarityForUser(ctx, "did:test:alice")) | ||
| 262 | + | ||
| 263 | + var jaccardAfter float64 | ||
| 264 | + assert.NilError(t, database.QueryRowContext(ctx, | ||
| 265 | + `SELECT jaccard FROM user_similarity WHERE user_a = ? AND user_b = ?`, | ||
| 266 | + "did:test:alice", "did:test:bob").Scan(&jaccardAfter)) | ||
| 267 | + | ||
| 268 | + assert.Assert(t, jaccardAfter > 0, "incoming follow boost should survive per-user recomputation, got %f", jaccardAfter) | ||
| 269 | +} | ||
| 270 | + | ||
| 245 | func TestComputeRecommendationsForUser(t *testing.T) { | 271 | func TestComputeRecommendationsForUser(t *testing.T) { |
| 246 | ctx := context.Background() | 272 | ctx := context.Background() |
| 247 | database := setupClusterTestDB(t) | 273 | database := setupClusterTestDB(t) |