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