Open Source Algorithms
Article Ranking
Last Updated: September 26th, 2025
For suggested improvements or requested changes, email community@aemula.com.
We are in process of setting up the community governance of the open source algorithms, allowing community members to make pull requests of the source code from Github and propose changes for vote by the community.
Function explained with comments throughout:
import { fetchArticleRankData } from './neo4jService.mjs';
// Set the number of articles we want to show on the user's Front Page
// This is used in the author diversity check
const TOP_N_ARTICLES = 5;
// Set the minimum amount of reads an article must have to consider its quality
const MIN_ENGAGEMENT = 3;
// function to calculate quality
function calcQualityScore(supportCount, totalCount) {
// check if the article has enough engagement to calculate quality
if (totalCount < MIN_ENGAGEMENT) {
return 0.5;
}
// set a score between 1 and 0 based on support votes vs total engagement
return normalizeValue(supportCount / totalCount, 0, 1);
};
// function to calculate the article's recency
function calcRecencyScore(postTime) {
// what time is it now
const now = Date.now();
// how long has it been since the postTime of the article (in hours)
const timeDifference = (now - (postTime * 1000)) / (1000 * 60 * 60);
// set a time decay so recency scores decreased based on hours since posting
if (timeDifference <= 12) return 1;
if (timeDifference <= 24) return 0.8;
if (timeDifference <= 48) return 0.6;
if (timeDifference <= 72) return 0.3;
return 0.1;
};
// utility function to normalize score values between 0 and 1
function normalizeValue(value, min, max) {
if (min === max) return 0.5;
return (value - min) / (max - min);
};
// author diversity check to make sure we show a mix of authors on the user's Front Page
function authorDiversityCheck(articles) {
const authorSet = new Set();
// step through our ranked articles and add the top to our top 5 slots one by one
for (let i = 0; i < TOP_N_ARTICLES; i++) {
// read an article from our rankings to potentially add
const article = articles[i];
// check if we already have another article from this author
if (authorSet.has(article.author)) {
// find a replacement article further down the rankings if we do
for (let j = TOP_N_ARTICLES; j < articles.length; j++) {
if (!authorSet.has(articles[j].author)) {
[articles[i], articles[j]] = [articles[j], articles[i]];
break;
}
}
}
// add the new author to our author set to continue checking
authorSet.add(articles[i].author);
}
};
// Main function for ranking articles
// receives 60 candidate articles from our Article Search algorithm
export async function articleRanking(articleCIDs, { topN = 10 } = {}) {
try {
// pull the necessary data we need to rank each article from our database
const data = await fetchArticleRankData(articleCIDs);
// find the maximum engagement received from an article in our candidate set
// this is used to normalize other engagement values
const maxTotal = Math.max(1, ...data.map(a => Number(a.totalCount ?? 0)));
// for each article in our candidate articles
const ranked = data.map(a => {
// read the support votes it has received
const supportCount = Number(a.supportCount ?? 0);
// read the total support it has received
const totalCount = Number(a.totalCount ?? 0);
// read when it was posted
const postTime = Number(a.postTime ?? 0);
// calculate our quality, recency, and engagement scores
const qualityScore = calcQualityScore(supportCount, totalCount);
const recencyScore = calcRecencyScore(postTime);
const engagementScore = normalizeValue(totalCount, 0, maxTotal);
// weights we apply to each score in ranking
const qualityWeight = 0.4;
const recencyWeight = 0.4;
const engagementWeight = 0.2;
// calculate the ranking score
const totalScore =
qualityWeight * qualityScore +
recencyWeight * recencyScore +
engagementWeight * engagementScore;
return {
articleId: a.articleId,
author: a.author,
postTime,
totalScore,
};
});
// sort our returned articles by their ranking score
ranked.sort((a, b) => b.totalScore - a.totalScore || b.postTime - a.postTime);
// make sure we are returning a mix of authors in the top 5 slots
authorDiversityCheck(ranked);
// return the rankings to the user
return ranked.slice(0, topN).map(a => a.articleId);
} catch (err) {
console.error('Error ranking articles:', err);
throw err;
}
};