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.
pull/3695/head
Rigel Kent 2021-02-02 12:59:41 +01:00 committed by Chocobozzz
parent f6267b6101
commit 3d4e112d16
15 changed files with 57 additions and 22 deletions

View File

@ -271,6 +271,7 @@
<option i18n value="/videos/overview">Discover videos</option>
<optgroup i18n-label label="Trending pages">
<option i18n value="/videos/trending">Default trending page</option>
<option i18n value="/videos/trending?alg=best" [disabled]="!trendingVideosAlgorithmsEnabledIncludes('best')">Best videos</option>
<option i18n value="/videos/trending?alg=hot" [disabled]="!trendingVideosAlgorithmsEnabledIncludes('hot')">Hot videos</option>
<option i18n value="/videos/trending?alg=most-viewed" [disabled]="!trendingVideosAlgorithmsEnabledIncludes('most-viewed')">Most viewed videos</option>
<option i18n value="/videos/trending?alg=most-liked" [disabled]="!trendingVideosAlgorithmsEnabledIncludes('most-liked')">Most liked videos</option>
@ -288,6 +289,7 @@
<label i18n for="trendingVideosAlgorithmsDefault">Default trending page</label>
<div class="peertube-select-container">
<select id="trendingVideosAlgorithmsDefault" formControlName="default" class="form-control">
<option i18n value="best">Best videos</option>
<option i18n value="hot">Hot videos</option>
<option i18n value="most-viewed">Most viewed videos</option>
<option i18n value="most-liked">Most liked videos</option>

View File

@ -35,6 +35,13 @@ export class VideoTrendingHeaderComponent extends VideoListHeaderComponent imple
super(data)
this.buttons = [
{
label: $localize`:A variant of Trending videos based on the number of recent interactions, minus user history:Best`,
iconName: 'award',
value: 'best',
tooltip: $localize`Videos totalizing the most interactions for recent videos, minus user history`,
hidden: true
},
{
label: $localize`:A variant of Trending videos based on the number of recent interactions:Hot`,
iconName: 'flame',

View File

@ -131,7 +131,7 @@ export class ServerService {
videos: {
intervalDays: 0,
algorithms: {
enabled: [ 'hot', 'most-viewed', 'most-liked' ],
enabled: [ 'best', 'hot', 'most-viewed', 'most-liked' ],
default: 'most-viewed'
}
}

View File

@ -71,7 +71,8 @@ const icons = {
'live': require('!!raw-loader?!../../../assets/images/feather/live.svg').default,
'repeat': require('!!raw-loader?!../../../assets/images/feather/repeat.svg').default,
'message-circle': require('!!raw-loader?!../../../assets/images/feather/message-circle.svg').default,
'codesandbox': require('!!raw-loader?!../../../assets/images/feather/codesandbox.svg').default
'codesandbox': require('!!raw-loader?!../../../assets/images/feather/codesandbox.svg').default,
'award': require('!!raw-loader?!../../../assets/images/feather/award.svg').default
}
export type GlobalIconName = keyof typeof icons

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-award"><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></svg>

After

Width:  |  Height:  |  Size: 325 B

View File

@ -110,7 +110,8 @@ trending:
interval_days: 7 # Compute trending videos for the last x days
algorithms:
enabled:
- 'hot' # adaptation of the Reddit 'Hot' algorithm
- 'best' # adaptation of Reddit's 'Best' algorithm (Hot minus History)
- 'hot' # adaptation of Reddit's 'Hot' algorithm
- 'most-viewed' # default, used initially by PeerTube as the trending page
- 'most-liked'
default: 'most-viewed'

View File

@ -108,7 +108,8 @@ trending:
interval_days: 7 # Compute trending videos for the last x days
algorithms:
enabled:
- 'hot' # adaptation of the Reddit 'Hot' algorithm
- 'best' # adaptation of Reddit's 'Best' algorithm (Hot minus History)
- 'hot' # adaptation of Reddit's 'Hot' algorithm
- 'most-viewed' # default, used initially by PeerTube as the trending page
- 'most-liked'
default: 'most-viewed'

View File

@ -72,7 +72,7 @@ const SORTABLE_COLUMNS = {
FOLLOWERS: [ 'createdAt', 'state', 'score' ],
FOLLOWING: [ 'createdAt', 'redundancyAllowed', 'state' ],
VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'trending', 'hot' ],
VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'trending', 'hot', 'best' ],
// Don't forget to update peertube-search-index with the same values
VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'match' ],

View File

@ -31,8 +31,8 @@ export type BuildVideosQueryOptions = {
videoPlaylistId?: number
trendingAlgorithm?: string // best, hot, or any other algorithm implemented
trendingDays?: number
hot?: boolean
user?: MUserAccountId
historyOfUser?: MUserId
@ -252,7 +252,7 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions)
attributes.push('COALESCE(SUM("videoView"."views"), 0) AS "score"')
group = 'GROUP BY "video"."id"'
} else if (options.hot) {
} else if ([ 'best', 'hot' ].includes(options.trendingAlgorithm)) {
/**
* "Hotness" is a measure based on absolute view/comment/like/dislike numbers,
* with fixed weights only applied to their log values.
@ -269,28 +269,39 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions)
*/
const weights = {
like: 3,
dislike: 3,
dislike: -3,
view: 1 / 12,
comment: 2 // a comment takes more time than a like to do, but can be done multiple times
comment: 2, // a comment takes more time than a like to do, but can be done multiple times
history: -2
}
joins.push('LEFT JOIN "videoComment" ON "video"."id" = "videoComment"."videoId"')
attributes.push(
let attribute =
`LOG(GREATEST(1, "video"."likes" - 1)) * ${weights.like} ` + // likes (+)
`- LOG(GREATEST(1, "video"."dislikes" - 1)) * ${weights.dislike} ` + // dislikes (-)
`+ LOG(GREATEST(1, "video"."dislikes" - 1)) * ${weights.dislike} ` + // dislikes (-)
`+ LOG("video"."views" + 1) * ${weights.view} ` + // views (+)
`+ LOG(GREATEST(1, COUNT(DISTINCT "videoComment"."id"))) * ${weights.comment} ` + // comments (+)
'+ (SELECT EXTRACT(epoch FROM "video"."publishedAt") / 47000) ' + // base score (in number of half-days)
'AS "score"'
)
'+ (SELECT EXTRACT(epoch FROM "video"."publishedAt") / 47000) ' // base score (in number of half-days)
if (options.trendingAlgorithm === 'best' && options.user) {
joins.push(
'LEFT JOIN "userVideoHistory" ON "video"."id" = "userVideoHistory"."videoId" AND "userVideoHistory"."userId" = :bestUser'
)
replacements.bestUser = options.user.id
attribute += `+ POWER(COUNT(DISTINCT "userVideoHistory"."id"), 2.0) * ${weights.history} `
}
attribute += 'AS "score"'
attributes.push(attribute)
group = 'GROUP BY "video"."id"'
}
}
if (options.historyOfUser) {
joins.push('INNER JOIN "userVideoHistory" on "video"."id" = "userVideoHistory"."videoId"')
joins.push('INNER JOIN "userVideoHistory" ON "video"."id" = "userVideoHistory"."videoId"')
and.push('"userVideoHistory"."userId" = :historyOfUser')
replacements.historyOfUser = options.historyOfUser.id
@ -410,7 +421,7 @@ function buildOrder (value: string) {
if (field.toLowerCase() === 'random') return 'ORDER BY RANDOM()'
if ([ 'trending', 'hot' ].includes(field.toLowerCase())) { // Sort by aggregation
if ([ 'trending', 'hot', 'best' ].includes(field.toLowerCase())) { // Sort by aggregation
return `ORDER BY "score" ${direction}, "video"."views" ${direction}`
}

View File

@ -1090,7 +1090,9 @@ export class VideoModel extends Model {
const trendingDays = options.sort.endsWith('trending')
? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS
: undefined
const hot = options.sort.endsWith('hot')
let trendingAlgorithm
if (options.sort.endsWith('hot')) trendingAlgorithm = 'hot'
if (options.sort.endsWith('best')) trendingAlgorithm = 'best'
const serverActor = await getServerActor()
@ -1120,7 +1122,7 @@ export class VideoModel extends Model {
user: options.user,
historyOfUser: options.historyOfUser,
trendingDays,
hot,
trendingAlgorithm,
search: options.search
}

View File

@ -141,7 +141,7 @@ describe('Test config API validators', function () {
trending: {
videos: {
algorithms: {
enabled: [ 'hot', 'most-viewed', 'most-liked' ],
enabled: [ 'best', 'hot', 'most-viewed', 'most-liked' ],
default: 'most-viewed'
}
}

View File

@ -375,7 +375,7 @@ describe('Test config', function () {
trending: {
videos: {
algorithms: {
enabled: [ 'hot', 'most-viewed', 'most-liked' ],
enabled: [ 'best', 'hot', 'most-viewed', 'most-liked' ],
default: 'hot'
}
}

View File

@ -363,6 +363,14 @@ describe('Test a single server', function () {
expect(videos.length).to.equal(2)
})
it('Should list and sort by best in descending order', async function () {
const res = await getVideosListPagination(server.url, 0, 2, '-best')
const videos = res.body.data
expect(res.body.total).to.equal(6)
expect(videos.length).to.equal(2)
})
it('Should update a video', async function () {
const attributes = {
name: 'my super video updated',

View File

@ -164,7 +164,7 @@ function updateCustomSubConfig (url: string, token: string, newConfig: DeepParti
trending: {
videos: {
algorithms: {
enabled: [ 'hot', 'most-viewed', 'most-liked' ],
enabled: [ 'best', 'hot', 'most-viewed', 'most-liked' ],
default: 'hot'
}
}

View File

@ -8,4 +8,5 @@ export type VideoSortField =
// trending sorts
'trending' | '-trending' |
'hot' | '-hot'
'hot' | '-hot' |
'best' | '-best'