diff options
author | Rigel Kent <sendmemail@rigelk.eu> | 2021-02-02 12:59:41 +0100 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2021-02-04 09:04:47 +0100 |
commit | 3d4e112d16471703f51a542c0cc6e73a6f5db628 (patch) | |
tree | cb4a53a50f9bc14a87b62ccfa9d398feb4bbcbc8 /server | |
parent | f6267b610145033ee26ca8a4a7c2b97eca65072e (diff) | |
download | PeerTube-3d4e112d16471703f51a542c0cc6e73a6f5db628.tar.gz PeerTube-3d4e112d16471703f51a542c0cc6e73a6f5db628.tar.zst PeerTube-3d4e112d16471703f51a542c0cc6e73a6f5db628.zip |
add best trending strategy based on Reddit's best
inspired from https://www.reddit.com/r/changelog/comments/7spgg0/best_is_the_new_hotness/
this implementation only adds freshness, and doesn't personalize based
on subscribed communities yet.
Diffstat (limited to 'server')
-rw-r--r-- | server/initializers/constants.ts | 2 | ||||
-rw-r--r-- | server/models/video/video-query-builder.ts | 33 | ||||
-rw-r--r-- | server/models/video/video.ts | 6 | ||||
-rw-r--r-- | server/tests/api/check-params/config.ts | 2 | ||||
-rw-r--r-- | server/tests/api/server/config.ts | 2 | ||||
-rw-r--r-- | server/tests/api/videos/single-server.ts | 8 |
6 files changed, 37 insertions, 16 deletions
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 0fab872a9..9d9b3966c 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -72,7 +72,7 @@ const SORTABLE_COLUMNS = { | |||
72 | FOLLOWERS: [ 'createdAt', 'state', 'score' ], | 72 | FOLLOWERS: [ 'createdAt', 'state', 'score' ], |
73 | FOLLOWING: [ 'createdAt', 'redundancyAllowed', 'state' ], | 73 | FOLLOWING: [ 'createdAt', 'redundancyAllowed', 'state' ], |
74 | 74 | ||
75 | VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'trending', 'hot' ], | 75 | VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'trending', 'hot', 'best' ], |
76 | 76 | ||
77 | // Don't forget to update peertube-search-index with the same values | 77 | // Don't forget to update peertube-search-index with the same values |
78 | VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'match' ], | 78 | VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'match' ], |
diff --git a/server/models/video/video-query-builder.ts b/server/models/video/video-query-builder.ts index e2145fb9a..822d0c89b 100644 --- a/server/models/video/video-query-builder.ts +++ b/server/models/video/video-query-builder.ts | |||
@@ -31,8 +31,8 @@ export type BuildVideosQueryOptions = { | |||
31 | 31 | ||
32 | videoPlaylistId?: number | 32 | videoPlaylistId?: number |
33 | 33 | ||
34 | trendingAlgorithm?: string // best, hot, or any other algorithm implemented | ||
34 | trendingDays?: number | 35 | trendingDays?: number |
35 | hot?: boolean | ||
36 | 36 | ||
37 | user?: MUserAccountId | 37 | user?: MUserAccountId |
38 | historyOfUser?: MUserId | 38 | historyOfUser?: MUserId |
@@ -252,7 +252,7 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions) | |||
252 | attributes.push('COALESCE(SUM("videoView"."views"), 0) AS "score"') | 252 | attributes.push('COALESCE(SUM("videoView"."views"), 0) AS "score"') |
253 | 253 | ||
254 | group = 'GROUP BY "video"."id"' | 254 | group = 'GROUP BY "video"."id"' |
255 | } else if (options.hot) { | 255 | } else if ([ 'best', 'hot' ].includes(options.trendingAlgorithm)) { |
256 | /** | 256 | /** |
257 | * "Hotness" is a measure based on absolute view/comment/like/dislike numbers, | 257 | * "Hotness" is a measure based on absolute view/comment/like/dislike numbers, |
258 | * with fixed weights only applied to their log values. | 258 | * with fixed weights only applied to their log values. |
@@ -269,28 +269,39 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions) | |||
269 | */ | 269 | */ |
270 | const weights = { | 270 | const weights = { |
271 | like: 3, | 271 | like: 3, |
272 | dislike: 3, | 272 | dislike: -3, |
273 | view: 1 / 12, | 273 | view: 1 / 12, |
274 | comment: 2 // a comment takes more time than a like to do, but can be done multiple times | 274 | comment: 2, // a comment takes more time than a like to do, but can be done multiple times |
275 | history: -2 | ||
275 | } | 276 | } |
276 | 277 | ||
277 | joins.push('LEFT JOIN "videoComment" ON "video"."id" = "videoComment"."videoId"') | 278 | joins.push('LEFT JOIN "videoComment" ON "video"."id" = "videoComment"."videoId"') |
278 | 279 | ||
279 | attributes.push( | 280 | let attribute = |
280 | `LOG(GREATEST(1, "video"."likes" - 1)) * ${weights.like} ` + // likes (+) | 281 | `LOG(GREATEST(1, "video"."likes" - 1)) * ${weights.like} ` + // likes (+) |
281 | `- LOG(GREATEST(1, "video"."dislikes" - 1)) * ${weights.dislike} ` + // dislikes (-) | 282 | `+ LOG(GREATEST(1, "video"."dislikes" - 1)) * ${weights.dislike} ` + // dislikes (-) |
282 | `+ LOG("video"."views" + 1) * ${weights.view} ` + // views (+) | 283 | `+ LOG("video"."views" + 1) * ${weights.view} ` + // views (+) |
283 | `+ LOG(GREATEST(1, COUNT(DISTINCT "videoComment"."id"))) * ${weights.comment} ` + // comments (+) | 284 | `+ LOG(GREATEST(1, COUNT(DISTINCT "videoComment"."id"))) * ${weights.comment} ` + // comments (+) |
284 | '+ (SELECT EXTRACT(epoch FROM "video"."publishedAt") / 47000) ' + // base score (in number of half-days) | 285 | '+ (SELECT EXTRACT(epoch FROM "video"."publishedAt") / 47000) ' // base score (in number of half-days) |
285 | 'AS "score"' | 286 | |
286 | ) | 287 | if (options.trendingAlgorithm === 'best' && options.user) { |
288 | joins.push( | ||
289 | 'LEFT JOIN "userVideoHistory" ON "video"."id" = "userVideoHistory"."videoId" AND "userVideoHistory"."userId" = :bestUser' | ||
290 | ) | ||
291 | replacements.bestUser = options.user.id | ||
292 | |||
293 | attribute += `+ POWER(COUNT(DISTINCT "userVideoHistory"."id"), 2.0) * ${weights.history} ` | ||
294 | } | ||
295 | |||
296 | attribute += 'AS "score"' | ||
297 | attributes.push(attribute) | ||
287 | 298 | ||
288 | group = 'GROUP BY "video"."id"' | 299 | group = 'GROUP BY "video"."id"' |
289 | } | 300 | } |
290 | } | 301 | } |
291 | 302 | ||
292 | if (options.historyOfUser) { | 303 | if (options.historyOfUser) { |
293 | joins.push('INNER JOIN "userVideoHistory" on "video"."id" = "userVideoHistory"."videoId"') | 304 | joins.push('INNER JOIN "userVideoHistory" ON "video"."id" = "userVideoHistory"."videoId"') |
294 | 305 | ||
295 | and.push('"userVideoHistory"."userId" = :historyOfUser') | 306 | and.push('"userVideoHistory"."userId" = :historyOfUser') |
296 | replacements.historyOfUser = options.historyOfUser.id | 307 | replacements.historyOfUser = options.historyOfUser.id |
@@ -410,7 +421,7 @@ function buildOrder (value: string) { | |||
410 | 421 | ||
411 | if (field.toLowerCase() === 'random') return 'ORDER BY RANDOM()' | 422 | if (field.toLowerCase() === 'random') return 'ORDER BY RANDOM()' |
412 | 423 | ||
413 | if ([ 'trending', 'hot' ].includes(field.toLowerCase())) { // Sort by aggregation | 424 | if ([ 'trending', 'hot', 'best' ].includes(field.toLowerCase())) { // Sort by aggregation |
414 | return `ORDER BY "score" ${direction}, "video"."views" ${direction}` | 425 | return `ORDER BY "score" ${direction}, "video"."views" ${direction}` |
415 | } | 426 | } |
416 | 427 | ||
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index ea6c9d44b..0ecb8d600 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -1090,7 +1090,9 @@ export class VideoModel extends Model { | |||
1090 | const trendingDays = options.sort.endsWith('trending') | 1090 | const trendingDays = options.sort.endsWith('trending') |
1091 | ? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS | 1091 | ? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS |
1092 | : undefined | 1092 | : undefined |
1093 | const hot = options.sort.endsWith('hot') | 1093 | let trendingAlgorithm |
1094 | if (options.sort.endsWith('hot')) trendingAlgorithm = 'hot' | ||
1095 | if (options.sort.endsWith('best')) trendingAlgorithm = 'best' | ||
1094 | 1096 | ||
1095 | const serverActor = await getServerActor() | 1097 | const serverActor = await getServerActor() |
1096 | 1098 | ||
@@ -1120,7 +1122,7 @@ export class VideoModel extends Model { | |||
1120 | user: options.user, | 1122 | user: options.user, |
1121 | historyOfUser: options.historyOfUser, | 1123 | historyOfUser: options.historyOfUser, |
1122 | trendingDays, | 1124 | trendingDays, |
1123 | hot, | 1125 | trendingAlgorithm, |
1124 | search: options.search | 1126 | search: options.search |
1125 | } | 1127 | } |
1126 | 1128 | ||
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts index e6309b5f7..d6c20f7af 100644 --- a/server/tests/api/check-params/config.ts +++ b/server/tests/api/check-params/config.ts | |||
@@ -141,7 +141,7 @@ describe('Test config API validators', function () { | |||
141 | trending: { | 141 | trending: { |
142 | videos: { | 142 | videos: { |
143 | algorithms: { | 143 | algorithms: { |
144 | enabled: [ 'hot', 'most-viewed', 'most-liked' ], | 144 | enabled: [ 'best', 'hot', 'most-viewed', 'most-liked' ], |
145 | default: 'most-viewed' | 145 | default: 'most-viewed' |
146 | } | 146 | } |
147 | } | 147 | } |
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts index e5bab0b77..26df8373e 100644 --- a/server/tests/api/server/config.ts +++ b/server/tests/api/server/config.ts | |||
@@ -375,7 +375,7 @@ describe('Test config', function () { | |||
375 | trending: { | 375 | trending: { |
376 | videos: { | 376 | videos: { |
377 | algorithms: { | 377 | algorithms: { |
378 | enabled: [ 'hot', 'most-viewed', 'most-liked' ], | 378 | enabled: [ 'best', 'hot', 'most-viewed', 'most-liked' ], |
379 | default: 'hot' | 379 | default: 'hot' |
380 | } | 380 | } |
381 | } | 381 | } |
diff --git a/server/tests/api/videos/single-server.ts b/server/tests/api/videos/single-server.ts index 52c6800c1..da90223b8 100644 --- a/server/tests/api/videos/single-server.ts +++ b/server/tests/api/videos/single-server.ts | |||
@@ -363,6 +363,14 @@ describe('Test a single server', function () { | |||
363 | expect(videos.length).to.equal(2) | 363 | expect(videos.length).to.equal(2) |
364 | }) | 364 | }) |
365 | 365 | ||
366 | it('Should list and sort by best in descending order', async function () { | ||
367 | const res = await getVideosListPagination(server.url, 0, 2, '-best') | ||
368 | |||
369 | const videos = res.body.data | ||
370 | expect(res.body.total).to.equal(6) | ||
371 | expect(videos.length).to.equal(2) | ||
372 | }) | ||
373 | |||
366 | it('Should update a video', async function () { | 374 | it('Should update a video', async function () { |
367 | const attributes = { | 375 | const attributes = { |
368 | name: 'my super video updated', | 376 | name: 'my super video updated', |