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;
    }
};
Previous
Article Search