Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add best trending strategy based on Reddit's best #3681

Merged
merged 1 commit into from
Feb 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
33 changes: 22 additions & 11 deletions server/models/video/video-query-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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}`
}

Expand Down
6 changes: 4 additions & 2 deletions server/models/video/video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -1120,7 +1122,7 @@ export class VideoModel extends Model {
user: options.user,
historyOfUser: options.historyOfUser,
trendingDays,
hot,
trendingAlgorithm,
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'