From 5bcbcbe338ef5a1ed14f084311d013fbb25dabcf Mon Sep 17 00:00:00 2001 From: Rigel Kent Date: Fri, 22 Jan 2021 00:12:44 +0100 Subject: modularize abstract video list header and implement video hotness recommendation variant --- server/models/video/video-query-builder.ts | 42 +++++++++++++++++++++++++++--- server/models/video/video.ts | 2 ++ 2 files changed, 40 insertions(+), 4 deletions(-) (limited to 'server/models/video') diff --git a/server/models/video/video-query-builder.ts b/server/models/video/video-query-builder.ts index 9e5b6febb..65b72fe1c 100644 --- a/server/models/video/video-query-builder.ts +++ b/server/models/video/video-query-builder.ts @@ -32,6 +32,8 @@ export type BuildVideosQueryOptions = { videoPlaylistId?: number trendingDays?: number + hot?: boolean + user?: MUserAccountId historyOfUser?: MUserId @@ -239,14 +241,46 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions) } } - // We don't exclude results in this if so if we do a count we don't need to add this complex clauses + // We don't exclude results in this so if we do a count we don't need to add this complex clause if (options.trendingDays && options.isCount !== true) { const viewsGteDate = new Date(new Date().getTime() - (24 * 3600 * 1000) * options.trendingDays) joins.push('LEFT JOIN "videoView" ON "video"."id" = "videoView"."videoId" AND "videoView"."startDate" >= :viewsGteDate') replacements.viewsGteDate = viewsGteDate - attributes.push('COALESCE(SUM("videoView"."views"), 0) AS "videoViewsSum"') + attributes.push('COALESCE(SUM("videoView"."views"), 0) AS "score"') + + group = 'GROUP BY "video"."id"' + } else if (options.hot && options.isCount !== true) { + /** + * "Hotness" is a measure based on absolute view/comment/like/dislike numbers, + * with fixed weights only applied to their log values. + * + * This algorithm gives little chance for an old video to have a good score, + * for which recent spikes in interactions could be a sign of "hotness" and + * justify a better score. However there are multiple ways to achieve that + * goal, which is left for later. Yes, this is a TODO :) + * + * note: weights and base score are in number of half-days. + * see https://github.com/reddit-archive/reddit/blob/master/r2/r2/lib/db/_sorts.pyx#L47-L58 + */ + const weights = { + like: 3, + dislike: 3, + view: 1 / 12, + comment: 6 + } + + joins.push('LEFT JOIN "videoComment" ON "video"."id" = "videoComment"."videoId"') + + attributes.push( + `LOG(GREATEST(1, "video"."likes" - 1)) * ${weights.like} ` + // likes (+) + `- LOG(GREATEST(1, "video"."dislikes" - 1)) * ${weights.dislike} ` + // dislikes (-) + `+ LOG("video"."views" + 1) * ${weights.view} ` + // views (+) + `+ LOG(GREATEST(1, COUNT(DISTINCT "videoComment"."id") - 1)) * ${weights.comment} ` + // comments (+) + '+ (SELECT EXTRACT(epoch FROM "video"."publishedAt") / 47000) ' + // base score (in number of half-days) + 'AS "score"' + ) group = 'GROUP BY "video"."id"' } @@ -372,8 +406,8 @@ function buildOrder (value: string) { if (field.toLowerCase() === 'random') return 'ORDER BY RANDOM()' - if (field.toLowerCase() === 'trending') { // Sort by aggregation - return `ORDER BY "videoViewsSum" ${direction}, "video"."views" ${direction}` + if ([ 'trending', 'hot' ].includes(field.toLowerCase())) { // Sort by aggregation + return `ORDER BY "score" ${direction}, "video"."views" ${direction}` } let firstSort: string diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 9b0aa809e..c56fbfbf2 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -1090,6 +1090,7 @@ export class VideoModel extends Model { const trendingDays = options.sort.endsWith('trending') ? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS : undefined + const hot = options.sort.endsWith('hot') const serverActor = await getServerActor() @@ -1119,6 +1120,7 @@ export class VideoModel extends Model { user: options.user, historyOfUser: options.historyOfUser, trendingDays, + hot, search: options.search } -- cgit v1.2.3