Open Source Algorithms
Article Search
Last Updated: September 26th, 2025
Article recommendations are based on proximity to a user in our social graph database, which can be viewed on your Explore Page on Aemula.
While our graph database runs on an Aemula-controlled server for efficient querying, the entire database can be recreated from publicly-accessible data at any time.
The graph is created by querying the Aemula subgraph, which is a publicly queryable GraphQL-indexed subgraph of the activity generated from the Aemula Smart Contract.
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:
// Article Search:
// Discovers relevant and recent articles near the user for consideration in ranking
export const improvedArticleRec = async (account, candidateLimit = 60) => {
// Create a session to connect to our Neo4j graph database
const session = driver.session();
// Ensure the user's account is not checksummed to align with database schema
const lowerAddress = account.toLowerCase();
// Weight to control how much we emphasize opinions of a user's neighbors
// To promote relevancy, we want to recommend articles that similar users have supported
// We can determine similarity by looking at a user's neighbors, or their close connections in the graph
// An articles "Near Support" is the support votes received from a user's immediate neighbors
const NEAR_SUPPORT_WEIGHT = 2.0;
// An articles "Global Support" counts all support votes from all users
const GLOBAL_SUPPORT_WEIGHT = 1.0;
try {
// Database Search Query:
// First MATCH the user's account to their node in our graph
// Set out to discover 60 articles to potentially recommend to the user
// Discover the user's neighbors (nbrs) in the graph by:
// Look at articles the user has authored or supported
// Look at other users that have authored or support the same articles
// Find articles the user's neighbors have authored or supported that the users hasn't read
// From these "candidate" articles, calculate a simple ranking score
// candidateScore is weighted near support plus weighted global support
// Sort these candidate articles by candidateScore, then by postTime
// Check how many more articles we need to discover
// If a user is new or has a small set of neighbors, we might not find 60 relevant articles
// For any remaining article need, fill it with the most recent articles published globally
const result = await session.run(
`
MATCH (u:User {id: $accountId})
OPTIONAL MATCH (u)-[:AUTHOR|SUPPORT]->(:Article {inCirculation:true})<-[:AUTHOR|SUPPORT]-(nbr:User)
WHERE nbr.id <> $accountId
WITH u, collect(DISTINCT nbr) as nbrs
OPTIONAL MATCH (a:Article {inCirculation:true})<-[:AUTHOR|SUPPORT]-(n:User)
WHERE n IN nbrs AND NOT (u)-[]->(a)
WITH u, a, count(DISTINCT n) AS nearSupport
OPTIONAL MATCH (a)<-[:SUPPORT]-(:User)
WITH u, a, nearSupport, count(*) AS globalSupport
WITH u, a,
(nearSupport * $NEAR_SUPPORT_WEIGHT) + (globalSupport * $GLOBAL_SUPPORT_WEIGHT) AS candidateScore
ORDER BY candidateScore DESC, a.postTime DESC
WITH u,
collect(CASE WHEN a IS NULL THEN NULL ELSE {id:a.id, score:candidateScore} END) AS tmp
WITH u, [x IN tmp WHERE x IS NOT NULL] AS localCands
WITH u, localCands, toInteger($candidateLimit) AS lim
WITH u, localCands, lim, (lim - size(localCands)) AS need
WITH u, localCands, lim, CASE WHEN need > 0 THEN need ELSE 0 END AS need
CALL {
WITH u, lim, need
WITH u, lim, need WHERE need > 0
MATCH (g:Article {inCirculation:true})
WHERE NOT (u)-[]->(g)
WITH u, lim, need, g
ORDER BY g.postTime DESC
LIMIT $candidateLimit
RETURN collect({id:g.id, score:0.0}) AS globalRecent
}
WITH localCands, lim, coalesce(globalRecent, []) AS globalRecent
WITH CASE
WHEN size(localCands) >= lim THEN localCands[0..lim]
ELSE localCands + globalRecent[0..(lim - size(localCands))]
END AS combined
UNWIND combined AS c
RETURN DISTINCT c.id AS articleId
`,
{
accountId: lowerAddress,
candidateLimit: neo4j.int(candidateLimit),
NEAR_SUPPORT_WEIGHT,
GLOBAL_SUPPORT_WEIGHT,
}
);
// Return the results of the article search to be used in Article Ranking
return result.records.map(r => r.get('articleId'));
} catch (err) {
console.error('Error fetching improved article recommendations:', err);
throw err;
} finally {
await session.close();
}
};