Skip to content

Commit

Permalink
add best trending strategy based on Reddit's best
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
rigelk committed Feb 3, 2021
1 parent ba5a8d8 commit 6a20304
Show file tree
Hide file tree
Showing 15 changed files with 55 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ <h1 class="sr-only" i18n>Configuration</h1>
<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>
Expand All @@ -288,6 +289,7 @@ <h1 class="sr-only" i18n>Configuration</h1>
<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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion client/src/app/core/server/server.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
}
Expand Down
3 changes: 2 additions & 1 deletion client/src/app/shared/shared-icons/global-icon.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions client/src/assets/images/feather/award.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion config/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
3 changes: 2 additions & 1 deletion config/production.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 1 addition & 1 deletion server/initializers/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' ],
Expand Down
32 changes: 22 additions & 10 deletions server/models/video/video-query-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export type BuildVideosQueryOptions = {

trendingDays?: number
hot?: boolean
best?: boolean

user?: MUserAccountId
historyOfUser?: MUserId
Expand Down Expand Up @@ -252,7 +253,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 (options.hot || options.best) {
/**
* "Hotness" is a measure based on absolute view/comment/like/dislike numbers,
* with fixed weights only applied to their log values.
Expand All @@ -269,28 +270,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.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
Expand Down Expand Up @@ -410,7 +422,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}`
}

Expand Down
2 changes: 2 additions & 0 deletions server/models/video/video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1091,6 +1091,7 @@ export class VideoModel extends Model {
? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS
: undefined
const hot = options.sort.endsWith('hot')
const best = options.sort.endsWith('best')

const serverActor = await getServerActor()

Expand Down Expand Up @@ -1121,6 +1122,7 @@ export class VideoModel extends Model {
historyOfUser: options.historyOfUser,
trendingDays,
hot,
best,
search: options.search
}

Expand Down
2 changes: 1 addition & 1 deletion server/tests/api/check-params/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
}
Expand Down
2 changes: 1 addition & 1 deletion server/tests/api/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
}
Expand Down
8 changes: 8 additions & 0 deletions server/tests/api/videos/single-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion shared/extra-utils/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
}
Expand Down
3 changes: 2 additions & 1 deletion shared/models/videos/video-sort-field.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export type VideoSortField =

// trending sorts
'trending' | '-trending' |
'hot' | '-hot'
'hot' | '-hot' |
'best' | '-best'

0 comments on commit 6a20304

Please sign in to comment.