Filter trending articles by user’s preferred languagesUnverified
e130c67 parent: 041a564 modified
internal/cluster/scoring.go +15 -11 | @@ -234,6 +234,19 @@ func (e *Engine) ComputeFeedRecommendationsOnDemand(ctx context.Context, userDID | ||
| 234 | 234 | return results, rows.Err() |
| 235 | 235 | } |
| 236 | 236 | |
| 237 | +func buildLangFilter(languages []string, prefix string) (string, []any) { | |
| 238 | + if len(languages) == 0 { | |
| 239 | + return "", nil | |
| 240 | + } | |
| 241 | + ph := make([]string, len(languages)) | |
| 242 | + args := make([]any, len(languages)) | |
| 243 | + for i, l := range languages { | |
| 244 | + ph[i] = "?" | |
| 245 | + args[i] = l | |
| 246 | + } | |
| 247 | + return "AND (" + prefix + "language IN (" + strings.Join(ph, ",") + ") OR " + prefix + "language = '')", args | |
| 248 | +} | |
| 249 | + | |
| 237 | 250 | func normalizeFeedScores(recs []*FeedRecommendation) { |
| 238 | 251 | if len(recs) < 2 { |
| 239 | 252 | return |
| @@ -409,16 +422,7 @@ func (e *Engine) ComputeArticleRecommendationsOnDemand(ctx context.Context, user | ||
| 409 | 422 | } |
| 410 | 423 | } |
| 411 | 424 | |
| 412 | - langFilter := "" | |
| 413 | - langArgs := []any{} | |
| 414 | - if len(languages) > 0 { | |
| 415 | - ph := make([]string, len(languages)) | |
| 416 | - for i, l := range languages { | |
| 417 | - ph[i] = "?" | |
| 418 | - langArgs = append(langArgs, l) | |
| 419 | - } | |
| 420 | - langFilter = " AND (a.language IN (" + strings.Join(ph, ",") + ") OR a.language = '')" | |
| 421 | - } | |
| 425 | + langFilter, langArgs := buildLangFilter(languages, "a.") | |
| 422 | 426 | |
| 423 | 427 | query := fmt.Sprintf(` |
| 424 | 428 | WITH similar_users AS ( |
| @@ -465,7 +469,7 @@ func (e *Engine) ComputeArticleRecommendationsOnDemand(ctx context.Context, user | ||
| 465 | 469 | LEFT JOIN social_likes sl ON sl.feed_url = la.feed_url AND sl.article_url = la.article_url |
| 466 | 470 | LEFT JOIN _content_boost cb ON cb.article_id = a.id |
| 467 | 471 | LEFT JOIN articles.read_state rs ON rs.article_id = a.id AND rs.user_did = ? |
| 468 | - WHERE COALESCE(rs.is_read, 0) = 0%s | |
| 472 | + WHERE COALESCE(rs.is_read, 0) = 0 %s | |
| 469 | 473 | ORDER BY score DESC, (CASE WHEN a.published > 'now' THEN 1 ELSE 0 END), a.published DESC |
| 470 | 474 | LIMIT ? |
| 471 | 475 | `, langFilter) |
| @@ -234,6 +234,19 @@ func (e *Engine) ComputeFeedRecommendationsOnDemand(ctx context.Context, userDID | |||
| 234 | return results, rows.Err() | 234 | return results, rows.Err() |
| 235 | } | 235 | } |
| 236 | 236 | ||
| 237 | +func buildLangFilter(languages []string, prefix string) (string, []any) { | ||
| 238 | + if len(languages) == 0 { | ||
| 239 | + return "", nil | ||
| 240 | + } | ||
| 241 | + ph := make([]string, len(languages)) | ||
| 242 | + args := make([]any, len(languages)) | ||
| 243 | + for i, l := range languages { | ||
| 244 | + ph[i] = "?" | ||
| 245 | + args[i] = l | ||
| 246 | + } | ||
| 247 | + return "AND (" + prefix + "language IN (" + strings.Join(ph, ",") + ") OR " + prefix + "language = '')", args | ||
| 248 | +} | ||
| 249 | + | ||
| 237 | func normalizeFeedScores(recs []*FeedRecommendation) { | 250 | func normalizeFeedScores(recs []*FeedRecommendation) { |
| 238 | if len(recs) < 2 { | 251 | if len(recs) < 2 { |
| 239 | return | 252 | return |
| @@ -409,16 +422,7 @@ func (e *Engine) ComputeArticleRecommendationsOnDemand(ctx context.Context, user | |||
| 409 | } | 422 | } |
| 410 | } | 423 | } |
| 411 | 424 | ||
| 412 | - langFilter := "" | 425 | + langFilter, langArgs := buildLangFilter(languages, "a.") |
| 413 | - langArgs := []any{} | ||
| 414 | - if len(languages) > 0 { | ||
| 415 | - ph := make([]string, len(languages)) | ||
| 416 | - for i, l := range languages { | ||
| 417 | - ph[i] = "?" | ||
| 418 | - langArgs = append(langArgs, l) | ||
| 419 | - } | ||
| 420 | - langFilter = " AND (a.language IN (" + strings.Join(ph, ",") + ") OR a.language = '')" | ||
| 421 | - } | ||
| 422 | 426 | ||
| 423 | query := fmt.Sprintf(` | 427 | query := fmt.Sprintf(` |
| 424 | WITH similar_users AS ( | 428 | WITH similar_users AS ( |
| @@ -465,7 +469,7 @@ func (e *Engine) ComputeArticleRecommendationsOnDemand(ctx context.Context, user | |||
| 465 | LEFT JOIN social_likes sl ON sl.feed_url = la.feed_url AND sl.article_url = la.article_url | 469 | LEFT JOIN social_likes sl ON sl.feed_url = la.feed_url AND sl.article_url = la.article_url |
| 466 | LEFT JOIN _content_boost cb ON cb.article_id = a.id | 470 | LEFT JOIN _content_boost cb ON cb.article_id = a.id |
| 467 | LEFT JOIN articles.read_state rs ON rs.article_id = a.id AND rs.user_did = ? | 471 | LEFT JOIN articles.read_state rs ON rs.article_id = a.id AND rs.user_did = ? |
| 468 | - WHERE COALESCE(rs.is_read, 0) = 0%s | 472 | + WHERE COALESCE(rs.is_read, 0) = 0 %s |
| 469 | ORDER BY score DESC, (CASE WHEN a.published > 'now' THEN 1 ELSE 0 END), a.published DESC | 473 | ORDER BY score DESC, (CASE WHEN a.published > 'now' THEN 1 ELSE 0 END), a.published DESC |
| 470 | LIMIT ? | 474 | LIMIT ? |
| 471 | `, langFilter) | 475 | `, langFilter) |
modified
internal/db/social.go +18 -2 | @@ -9,6 +9,19 @@ import ( | ||
| 9 | 9 | |
| 10 | 10 | var ErrDuplicateLike = errors.New("already liked this article") |
| 11 | 11 | |
| 12 | +func buildLangFilter(languages []string, prefix string) (string, []any) { | |
| 13 | + if len(languages) == 0 { | |
| 14 | + return "", nil | |
| 15 | + } | |
| 16 | + ph := make([]string, len(languages)) | |
| 17 | + args := make([]any, len(languages)) | |
| 18 | + for i, l := range languages { | |
| 19 | + ph[i] = "?" | |
| 20 | + args[i] = l | |
| 21 | + } | |
| 22 | + return "AND (" + prefix + "language IN (" + strings.Join(ph, ",") + ") OR " + prefix + "language = '')", args | |
| 23 | +} | |
| 24 | + | |
| 12 | 25 | type Annotation struct { |
| 13 | 26 | ID int64 |
| 14 | 27 | URI string |
| @@ -367,7 +380,9 @@ type TrendingItem struct { | ||
| 367 | 380 | HasLiked bool |
| 368 | 381 | } |
| 369 | 382 | |
| 370 | -func (s *ArticleStore) ListTrendingArticlesForUser(ctx context.Context, userDID, since string, limit, offset int) ([]*TrendingItem, error) { | |
| 383 | +func (s *ArticleStore) ListTrendingArticlesForUser(ctx context.Context, userDID, since string, languages []string, limit, offset int) ([]*TrendingItem, error) { | |
| 384 | + langFilter, langArgs := buildLangFilter(languages, "ar.") | |
| 385 | + | |
| 371 | 386 | rows, err := s.db.QueryContext(ctx, ` |
| 372 | 387 | SELECT ar.id, ar.title, COALESCE(ar.url, ''), COALESCE(ar.author, ''), |
| 373 | 388 | COALESCE(ar.summary, ''), l.feed_url, COALESCE(f.title, ''), |
| @@ -388,11 +403,12 @@ func (s *ArticleStore) ListTrendingArticlesForUser(ctx context.Context, userDID, | ||
| 388 | 403 | UNION SELECT ? |
| 389 | 404 | UNION SELECT f.target_did FROM follows f WHERE f.user_did = ? |
| 390 | 405 | ) |
| 406 | + `+langFilter+` | |
| 391 | 407 | GROUP BY ar.id |
| 392 | 408 | -- Future-published articles (e.g., scheduled) sort last |
| 393 | 409 | ORDER BY like_count DESC, annotation_count DESC, (CASE WHEN ar.published > 'now' THEN 1 ELSE 0 END), ar.published DESC |
| 394 | 410 | LIMIT ? OFFSET ? |
| 395 | - `, since, userDID, since, userDID, userDID, userDID, userDID, userDID, limit, offset) | |
| 411 | + `, append(append([]any{since, userDID, since, userDID, userDID, userDID, userDID, userDID}, langArgs...), limit, offset)...) | |
| 396 | 412 | if err != nil { |
| 397 | 413 | return nil, err |
| 398 | 414 | } |
| @@ -9,6 +9,19 @@ import ( | |||
| 9 | 9 | ||
| 10 | var ErrDuplicateLike = errors.New("already liked this article") | 10 | var ErrDuplicateLike = errors.New("already liked this article") |
| 11 | 11 | ||
| 12 | +func buildLangFilter(languages []string, prefix string) (string, []any) { | ||
| 13 | + if len(languages) == 0 { | ||
| 14 | + return "", nil | ||
| 15 | + } | ||
| 16 | + ph := make([]string, len(languages)) | ||
| 17 | + args := make([]any, len(languages)) | ||
| 18 | + for i, l := range languages { | ||
| 19 | + ph[i] = "?" | ||
| 20 | + args[i] = l | ||
| 21 | + } | ||
| 22 | + return "AND (" + prefix + "language IN (" + strings.Join(ph, ",") + ") OR " + prefix + "language = '')", args | ||
| 23 | +} | ||
| 24 | + | ||
| 12 | type Annotation struct { | 25 | type Annotation struct { |
| 13 | ID int64 | 26 | ID int64 |
| 14 | URI string | 27 | URI string |
| @@ -367,7 +380,9 @@ type TrendingItem struct { | |||
| 367 | HasLiked bool | 380 | HasLiked bool |
| 368 | } | 381 | } |
| 369 | 382 | ||
| 370 | -func (s *ArticleStore) ListTrendingArticlesForUser(ctx context.Context, userDID, since string, limit, offset int) ([]*TrendingItem, error) { | 383 | +func (s *ArticleStore) ListTrendingArticlesForUser(ctx context.Context, userDID, since string, languages []string, limit, offset int) ([]*TrendingItem, error) { |
| 384 | + langFilter, langArgs := buildLangFilter(languages, "ar.") | ||
| 385 | + | ||
| 371 | rows, err := s.db.QueryContext(ctx, ` | 386 | rows, err := s.db.QueryContext(ctx, ` |
| 372 | SELECT ar.id, ar.title, COALESCE(ar.url, ''), COALESCE(ar.author, ''), | 387 | SELECT ar.id, ar.title, COALESCE(ar.url, ''), COALESCE(ar.author, ''), |
| 373 | COALESCE(ar.summary, ''), l.feed_url, COALESCE(f.title, ''), | 388 | COALESCE(ar.summary, ''), l.feed_url, COALESCE(f.title, ''), |
| @@ -388,11 +403,12 @@ func (s *ArticleStore) ListTrendingArticlesForUser(ctx context.Context, userDID, | |||
| 388 | UNION SELECT ? | 403 | UNION SELECT ? |
| 389 | UNION SELECT f.target_did FROM follows f WHERE f.user_did = ? | 404 | UNION SELECT f.target_did FROM follows f WHERE f.user_did = ? |
| 390 | ) | 405 | ) |
| 406 | + `+langFilter+` | ||
| 391 | GROUP BY ar.id | 407 | GROUP BY ar.id |
| 392 | -- Future-published articles (e.g., scheduled) sort last | 408 | -- Future-published articles (e.g., scheduled) sort last |
| 393 | ORDER BY like_count DESC, annotation_count DESC, (CASE WHEN ar.published > 'now' THEN 1 ELSE 0 END), ar.published DESC | 409 | ORDER BY like_count DESC, annotation_count DESC, (CASE WHEN ar.published > 'now' THEN 1 ELSE 0 END), ar.published DESC |
| 394 | LIMIT ? OFFSET ? | 410 | LIMIT ? OFFSET ? |
| 395 | - `, since, userDID, since, userDID, userDID, userDID, userDID, userDID, limit, offset) | 411 | + `, append(append([]any{since, userDID, since, userDID, userDID, userDID, userDID, userDID}, langArgs...), limit, offset)...) |
| 396 | if err != nil { | 412 | if err != nil { |
| 397 | return nil, err | 413 | return nil, err |
| 398 | } | 414 | } |
modified
internal/server/dashboard_handler.go +1 -1 | @@ -72,7 +72,7 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) { | ||
| 72 | 72 | |
| 73 | 73 | since := time.Now().AddDate(0, 0, -7).Format(time.RFC3339) |
| 74 | 74 | |
| 75 | - personalTrending, err := s.dbs.Articles.ListTrendingArticlesForUser(ctx, user.DID, since, 5, 0) | |
| 75 | + personalTrending, err := s.dbs.Articles.ListTrendingArticlesForUser(ctx, user.DID, since, userLangs, 5, 0) | |
| 76 | 76 | if err != nil { |
| 77 | 77 | s.logger.Warn("failed to list personal trending", "error", err, "did", user.DID) |
| 78 | 78 | } |
| @@ -72,7 +72,7 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) { | |||
| 72 | 72 | ||
| 73 | since := time.Now().AddDate(0, 0, -7).Format(time.RFC3339) | 73 | since := time.Now().AddDate(0, 0, -7).Format(time.RFC3339) |
| 74 | 74 | ||
| 75 | - personalTrending, err := s.dbs.Articles.ListTrendingArticlesForUser(ctx, user.DID, since, 5, 0) | 75 | + personalTrending, err := s.dbs.Articles.ListTrendingArticlesForUser(ctx, user.DID, since, userLangs, 5, 0) |
| 76 | if err != nil { | 76 | if err != nil { |
| 77 | s.logger.Warn("failed to list personal trending", "error", err, "did", user.DID) | 77 | s.logger.Warn("failed to list personal trending", "error", err, "did", user.DID) |
| 78 | } | 78 | } |
modified
internal/server/trending_handler.go +6 -1 | @@ -30,10 +30,15 @@ func (s *Server) handleTrending(w http.ResponseWriter, r *http.Request) { | ||
| 30 | 30 | userDID = user.DID |
| 31 | 31 | } |
| 32 | 32 | |
| 33 | + var userLangs []string | |
| 34 | + if user != nil { | |
| 35 | + userLangs, _ = s.dbs.Users.GetLanguages(ctx, user.DID) | |
| 36 | + } | |
| 37 | + | |
| 33 | 38 | var trending []*db.TrendingItem |
| 34 | 39 | var err error |
| 35 | 40 | if scope == "for-me" { |
| 36 | - trending, err = s.dbs.Articles.ListTrendingArticlesForUser(ctx, userDID, since, page.Limit()+1, page.Offset()) | |
| 41 | + trending, err = s.dbs.Articles.ListTrendingArticlesForUser(ctx, userDID, since, userLangs, page.Limit()+1, page.Offset()) | |
| 37 | 42 | } else { |
| 38 | 43 | trending, err = s.dbs.Articles.ListTrendingArticles(ctx, userDID, since, page.Limit()+1, page.Offset()) |
| 39 | 44 | } |
| @@ -30,10 +30,15 @@ func (s *Server) handleTrending(w http.ResponseWriter, r *http.Request) { | |||
| 30 | userDID = user.DID | 30 | userDID = user.DID |
| 31 | } | 31 | } |
| 32 | 32 | ||
| 33 | + var userLangs []string | ||
| 34 | + if user != nil { | ||
| 35 | + userLangs, _ = s.dbs.Users.GetLanguages(ctx, user.DID) | ||
| 36 | + } | ||
| 37 | + | ||
| 33 | var trending []*db.TrendingItem | 38 | var trending []*db.TrendingItem |
| 34 | var err error | 39 | var err error |
| 35 | if scope == "for-me" { | 40 | if scope == "for-me" { |
| 36 | - trending, err = s.dbs.Articles.ListTrendingArticlesForUser(ctx, userDID, since, page.Limit()+1, page.Offset()) | 41 | + trending, err = s.dbs.Articles.ListTrendingArticlesForUser(ctx, userDID, since, userLangs, page.Limit()+1, page.Offset()) |
| 37 | } else { | 42 | } else { |
| 38 | trending, err = s.dbs.Articles.ListTrendingArticles(ctx, userDID, since, page.Limit()+1, page.Offset()) | 43 | trending, err = s.dbs.Articles.ListTrendingArticles(ctx, userDID, since, page.Limit()+1, page.Offset()) |
| 39 | } | 44 | } |