Open Source Algorithms
Article Ranking
Last Updated: January 6th, 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 a utility function to pull necessary data from our database
import { fetchArticleRankData } from './neo4jService.mjs';
// variable to set number of articles we return to the user
const TOP_N_ARTICLES = 5;
// variable controlling the minimum reads required to calculate quality score
const MIN_ENGAGEMENT = 3;
// function to calculate the quality of an article
// # of support votes / # of total interactions
function calcQualityScore(supportCount, totalCount) {
// check if the article has exceeded the minimum engagement threshold
if (totalCount < MIN_ENGAGEMENT) {
return 0.5; // set neutral score if not
}
return normalizeValue(supportCount / totalCount, 0, 1);
};
// function to calculate the recency score of an article
function calcRecencyScore(postTime) {
// calculate the difference between the current time and when the article was published
const now = Date.now();
const timeDifference = (now - (postTime * 1000)) / (1000 * 60 * 60); // hours
// time decay function
// highest score if posted in the last 12 hours
if (timeDifference <= 12) return 1;
// slight dropoff for articles posted in the last day
if (timeDifference <= 24) return 0.8;
// large dropoff for articles posted in the last week
if (timeDifference <= 48) return 0.6;
// largest dropoff for articles posted in the last month
if (timeDifference <= 72) return 0.3;
// drop to lowest score for articles older than a month
return 0.1;
};
// function used to normalize scores to be between 0 and 1
function normalizeValue(value, min, max) {
if (min === max) return 0.5; // return neutral score if min and max are the same
return (value - min) / (max - min);
};
// function to ensure we return a variety of authors
function authorDiversityCheck(articles) {
const authorSet = new Set();
// check the articles we plan to return to the user
for (let i = 0; i < TOP_N_ARTICLES; i++) {
const article = articles[i];
// if there is a duplicate author
if (authorSet.has(article.author)) {
// search the remaining recommendation list for another author
for (let j = TOP_N_ARTICLES; j < articles.length; j++) {
// check if the new author is already in the return set
if (!authorSet.has(articles[j].author)) {
// if not, replace duplicate author article with new author article
[articles[i], articles[j]] = [articles[j], articles[i]];
break;
}
}
}
authorSet.add(articles[i].author);
}
};
// main function handling article ranking flow
export async function articleRanking(articleCIDs) {
// the function receives an array of IPFS CIDs pointing to articles
try {
// pull necessary data for each article from our graph database
// includes author address, post time, # of support votes, # of total interactions
// (all data is onchain/IPFS, but cached in database for efficiency)
const articleRankData = await fetchArticleRankData(articleCIDs);
const rankedArticles = articleRankData.map((article) => {
// ensuring all article metrics are numbers
const supportCount = Number(article.supportCount);
const totalCount = Number(article.totalCount);
const postTime = Number(article.postTime);
// finding maximum interaction count to set top end of normalization range for engagement
const maxTotalCount = Math.max(...articleRankData.map((a) => totalCount));
// calculate the 3 scores - quality, recency, engagement
const qualityScore = calcQualityScore(supportCount, totalCount);
const recencyScore = calcRecencyScore(postTime);
const engagementScore = normalizeValue(totalCount, 0, maxTotalCount);
// set the weights for each score when determining ranking score
const qualityWeight = 0.4;
const recencyWeight = 0.4;
const engagementWeight = 0.2;
// calculate total ranking score with weights
const totalScore =
qualityWeight * qualityScore +
recencyWeight * recencyScore +
engagementWeight * engagementScore;
// return an object containing the IPFS CID, author address, and ranking score
return {
articleId: article.articleId,
author: article.author,
totalScore,
};
});
// sort the articles in descending order by ranking score
rankedArticles.sort((a, b) => b.totalScore - a.totalScore);
// check authors of ranked articles to ensure diversity
authorDiversityCheck(rankedArticles);
// return the IPFS CIDs of the ranked articles
// CIDs are used to download article data from IPFS
return rankedArticles.map((article) => article.articleId);
} catch (err) {
// statement to catch any errors
console.error('Error ranking articles:', err);
throw error;
}
};