Skip to content

feat: support betweenness centrality algorithm (#801)#818

Open
ArmxG wants to merge 1 commit into
apache:masterfrom
ArmxG:feat/betweenness-centrality
Open

feat: support betweenness centrality algorithm (#801)#818
ArmxG wants to merge 1 commit into
apache:masterfrom
ArmxG:feat/betweenness-centrality

Conversation

@ArmxG

@ArmxG ArmxG commented Jul 22, 2026

Copy link
Copy Markdown

What this PR does
Adds a built-in Betweenness Centrality graph algorithm (UDGA), as requested in #801. The algorithm computes the betweenness centrality score for every vertex and outputs (id, betweenness).

Approach
This implementation uses a vertex-centric version of Brandes' algorithm with every vertex treated as a source.

  • Forward phase: Performs BFS from each source to compute the number of shortest paths (sigma) and record predecessor information. ACK/NACK messages allow each vertex to determine when all child responses have been received.
  • Backward phase: Propagates dependency values (delta) from the leaves back to the source, accumulating each vertex's betweenness centrality score.

Per-source state is stored in the vertex value, following the same design pattern as AllSourceShortestPath. The algorithm has O(V·E) time complexity and O(V²) space complexity.

Tests

  • Added gql_algorithm_betweenness.sql and its expected output, verified against the standard Brandes algorithm.
  • Added testAlgorithmBetweennessCentrality to GQLAlgorithmTest.

@ArmxG
ArmxG force-pushed the feat/betweenness-centrality branch from 97fc5fc to aa9802a Compare July 22, 2026 15:21
arr[SIGMA] = 1;
arr[FORWARDED] = 1;
Map<String, Object> state = newState();
Map<Object, double[]> st = (Map<Object, double[]>) state.get("st");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your valuable contribution. I have some suggestions regarding this code and would like to discuss them with you.

Problem: The per-source states st (Map<Object, double[]>) and preds are kept in the vertex state. The current implementation does not release the corresponding entries after a source completes, causing them to accumulate and resulting in uncontrollable O(V^2) memory usage.

Suggestion: When a source's BACKWARD_DONE is set, immediately clear the entry for that source in st and preds (freeing up memory). It's possible to maintain a touched collection to only iterate over the sources actually involved (more efficient), but the minimum fix is ​​to remove it after completion.

Here is a code example for reference; you might find it helpful.

// --- Insert/Replace Front: Safely Read or Initialize State ---
Map<String, Object> state;

if (updatedValues ​​!= null && updatedValues.isPresent()) {

state = (Map<String, Object>) updatedValues.get().getField(0, null);

if (state == null) {

state = newState();

}
} else {

/ // The vertex doesn't have a state yet, safely initialize it (so subsequent logic can rely on a non-null state)

state = newState();

}
double bc = 0.0;

if (state.get("bc") != null) {

bc = (Double) state.get("bc");

} final Map<Object, double[]> st = (Map<Object, double[]>) state.get("st");

boolean childCountFinal = forwarded && arr[RESP_COUNT] >= outDegree;
if (childCountFinal && arr[RECV_CHILD] >= arr[CHILD_COUNT]) {
double sigma = arr[SIGMA];
double delta = sigma * arr[PENDING_SUM];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Problem: Calculating value = (1.0 + delta) / sigma and delta = sigma * pendingSum lacks protection against sigma == 0 (theoretically, sigma == 0 represents unaccessed data or an abnormal count), leading to division by zero or NaN.

Recommendation: Perform a sigma == 0 check (or sigma < eps) before using sigma for division; in this case, mark the source as complete and skip sending backwardMsg.

} else if (tag == ACK) {
double[] arr = st.get(source);
if (arr != null) {
arr[CHILD_COUNT]++;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Robustness of ACK/NACK and Response Count

Issue: The logic for RESP_COUNT and CHILD_COUNT currently relies on outDegree(outEdges.size()) as the expected number of responses. However, in scenarios with directed graphs, parallel edges, or different edge loading methods, this may not be a strict match, leading to premature or delayed entry into the backward phase.

Recommendation: Record "how many forward messages have been sent to neighbors (actual childCandidates)" instead of directly using outDegree, or determine whether all responses have been received based on the touchedNeighbors set.

@kitalkuyo-gita

Copy link
Copy Markdown
Contributor

Hello @ArmxG, thank you for your valuable contribution! I've been a bit busy lately and just saw your code. I'm leaving some comments for you to review. Please feel free to point out any issues you find unreasonable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants