Graph Analytics
Purple8 Hyper Graph ships two complementary analytics layers that run directly on the live graph — no separate data warehouse, no ETL pipeline, no external ML cluster.
| Layer | What it does | API |
|---|---|---|
| Graph Algorithms | Authority scoring, path analysis, cluster discovery, link prediction, spatial autocorrelation | GET /algorithms/* / MCP graph.* |
| OLAP Analytics Engine | Structural aggregations, distributions, projections | AnalyticsEngine (Python SDK) |
Both layers are available in both self-hosted licensing tiers — Developer (Self-Hosted Free) (50K nodes, 1 MCP agent) and Enterprise / Custom (Self-Hosted) (unlimited nodes/agents, full compliance/HA). Algorithms are feature-complete in both tiers; only scale quotas differ. See Pricing.
Graph Algorithms
PageRank — influence & authority scoring
PageRank measures how often a node would be visited by a random graph traversal. High-scoring nodes are influential connectors — the most-cited documents, most-trusted knowledge sources, or most-connected entities in your graph.
Algorithm: Power iteration with dangling-node redistribution (Google's original method). Converges when the L1 delta between iterations falls below tolerance.
result = engine.pagerank(
damping=0.85, # probability of following an edge (default: 0.85)
max_iterations=100, # cap on iterations before giving up
tolerance=1e-6, # convergence threshold
top_k=20, # number of top nodes to surface
)
print(result.converged) # True/False
print(result.iterations) # how many rounds it took
for node in result.top_nodes:
print(node.node_id, node.score)REST API:
GET /algorithms/pagerank?damping=0.85&top_k=20Use cases:
- Rank documents by citation authority in a knowledge graph
- Score team members by collaboration centrality
- Find the most-referenced entities in an AI agent's memory graph
- Prioritise nodes for manual review or enrichment
Community Detection — cluster discovery
Community detection partitions the graph into groups of nodes that are more densely connected to each other than to the rest of the graph. Purple8 uses synchronous label propagation — fast (O(E) per pass), parameter-free, and reproducible with a seed.
Returns:
- Community ID per node
- Community size distribution
- Total number of communities
- Newman-Girvan modularity Q — a score in
[-1, 1]where values above0.3indicate well-defined clusters
result = engine.detect_communities(
max_iterations=50, # propagation rounds before stopping
seed=42, # optional — makes results reproducible
)
print(result.num_communities)
print(f"Modularity Q: {result.modularity:.4f}")
# Which community does a node belong to?
community_id = result.communities["node_abc"]
# Top communities by size
for c in sorted(result.community_sizes.items(), key=lambda x: -x[1])[:5]:
print(f"Community {c[0]}: {c[1]} nodes")REST API:
GET /algorithms/communities?max_iterations=50&seed=42&top_k=10Use cases:
- Discover topic clusters in a document or knowledge graph
- Map organisational clusters from collaboration or reporting relationships
- Identify customer segments from interaction graphs
- Detect anomalous isolated subgraphs (communities of size 1)
- Measure knowledge graph quality — low modularity means weak structure
Betweenness Centrality — critical connector identification
Betweenness centrality measures how often a node sits on the shortest path between other pairs of nodes. High-centrality nodes are bridges — remove them and the graph fragments.
Algorithm: Brandes' exact algorithm. Time complexity O(V·E) — suitable for graphs up to ~1M nodes. For very large graphs, use the top_k parameter to limit output without affecting computation.
result = engine.betweenness_centrality(
normalise=True, # divide by (n-1)(n-2) so scores are in [0, 1]
top_k=20,
)
for node in result.top_nodes:
print(node.node_id, f"{node.score:.6f}")REST API:
GET /algorithms/centrality?normalise=true&top_k=20Use cases:
- Find bottleneck nodes in a process or workflow graph
- Identify critical connectors in an access management graph (nodes that grant access to many paths)
- Surface bridge entities between otherwise separate knowledge clusters
- Detect single points of failure in infrastructure dependency graphs
Using PageRank/Betweenness as a ranking signal for RAG and enterprise search
PageRank and Betweenness Centrality aren't only for standalone analytics — they are the right structural-importance signal for retrieval seed ranking in Hybrid Graph-Vector RAG, and a materially stronger one than raw node degree (a plain edge count has no concept of importance propagated through the network).
HybridGraphVectorRetriever's dual-signal seed fusion reads a persisted _pagerank_score node property at query time — an O(1) lookup, not a per-query graph pass — and only falls back to a raw-degree probe if PageRank has never been computed on that graph. To keep the persisted score fresh:
result = engine.pagerank(persist=True) # writes _pagerank_score onto every nodeBecause PageRank/Betweenness are full-graph, batch operations, run this periodically or on a data-change cadence rather than per-query. The convenience helper purple8_graph.hybrid_rag.refresh_centrality_scores(engine) wraps this (and, optionally, Betweenness persistence) for exactly that purpose, and is pre-registered as a named callback ("refresh_centrality_scores") with the scheduler — attach a CRON or INTERVAL trigger to it via the scheduler API to keep scores current with no custom code:
POST /scheduler/triggers
{
"trigger_type": "cron",
"action_type": "callback",
"callback_name": "refresh_centrality_scores",
"cron_expression": "0 */6 * * *",
"payload": {"include_betweenness": false}
}Nothing runs automatically until a trigger is attached — retrieval quality degrades gracefully to the previous degree-only behavior on a graph where this has never been enabled.
When to enable graph fusion — and when NOT to
HybridGraphVectorRetriever and HybridRAGPipeline default to HybridRetrievalMode.VECTOR_FIRST (fusion_alpha=0.2) — not BALANCED. This is a deliberate, measured decision, not a conservative placeholder waiting to be tuned up.
Why graph fusion is opt-in, not default: PageRank-weighted seed fusion only improves retrieval when the graph's edges encode information orthogonal to the embedding space already being searched — real authority, citation, shared-entity, or curated hierarchy structure. It regresses retrieval quality when the graph was itself derived from the same embedding space it's meant to complement (for example, a CO_RELEVANT-style graph built by linking each query's ANN/kNN top-K results to each other — a common pattern for giving PageRank something to compute on a corpus with no native graph structure).
The mechanism: such a graph is a lossy re-encoding of the same vector geometry. Dense regions of the embedding space naturally produce high-in-degree "hub" nodes; PageRank concentrates score on those hubs because they are globally central, not because they are relevant to any specific query. Fusing that query-independent signal into a query-dependent ranking systematically pushes true positives below generically-popular hubs.
Measured impact (100K real MS MARCO passages, 200 real queries, graph built from ANN-kNN windows):
fusion_alpha | MRR@10 | nDCG@10 | Recall@100 |
|---|---|---|---|
| 0.0 (baseline, pure vector) | 0.756 | 0.795 | 0.973 |
| 0.1 | 0.719 (-4.9%) | 0.770 (-3.2%) | 0.982 (+1.0%) |
0.2 (VECTOR_FIRST default) | 0.619 (-18.1%) | 0.695 (-12.5%) | 0.978 (+0.5%) |
0.5 (BALANCED) | 0.143 (-81.1%) | 0.260 (-67.3%) | 0.980 (+0.8%) |
Ranking quality degrades monotonically as fusion_alpha rises, while Recall@100 barely moves — the candidate pool is essentially unchanged; only its order is corrupted. Full reproduction: benchmarks/pagerank_msmarco_bench.py, results in benchmarks/results/pagerank_msmarco_bench_100000.json.
Decision checklist — only raise fusion_alpha above the VECTOR_FIRST default (i.e. use BALANCED or GRAPH_FIRST, or an explicit fusion_alpha override) when you can answer "yes" to both:
- Are your graph's edges independent of the embeddings? Real citations, a CRM org chart, curated
ADVANCED_TOJourney transitions, AEC structural adjacency, explicit entity co-references extracted separately from the embedding model — yes. Edges built by linking nearest neighbours in the same vector space you're searching (kNN graphs, "co-relevant" links derived from ANN results) — no. - Have you validated it on this corpus? Run
rag.tune_collection(or the checklist above manually) on a held-out query sample and confirmfusion_alpha > 0does not regress MRR/nDCG relative tofusion_alpha=0.0. Becauserag.profile_collectionderives its baselinefusion_alphafrom edge density rather than edge provenance, always verify on a held-out sample withrag.tune_collectionbefore promoting a higherfusion_alphato production.
If you built graph edges purely to give PageRank something to rank over (no native graph structure existed otherwise), keep fusion_alpha=0.0 / mode=VECTOR_FIRST — the graph gives PageRank something to compute, but nothing informative to add to ranking.
Shortest Path — weighted path queries
Dijkstra weighted shortest path between any two nodes. Supports optional edge type filtering and a custom weight property.
result = engine.shortest_path(
source_id="node_a",
target_id="node_z",
weight_property="latency_ms", # optional — uniform weight if omitted
edge_type="DEPENDS_ON", # optional — restricts traversal to this edge type
)
print(result.found) # True/False
print(result.hops) # number of edges in the path
print(result.total_cost) # sum of weight_property along the path
print(result.path) # list of node IDs from source to targetREST API:
GET /nodes/{source_id}/shortest-path?target=node_z&weight_property=latency_msUse cases:
- Find the lowest-latency path through an infrastructure graph
- Determine the chain of approvals between a user and a resource (access management)
- Shortest dependency resolution path in a software component graph
- Minimum-cost routing in a logistics or supply chain graph
OLAP Analytics Engine
The AnalyticsEngine runs read-only OLAP-style computations over a live GraphEngine. All methods iterate over the storage layer — no data export required.
from purple8_graph import GraphEngine, AnalyticsEngine
engine = GraphEngine(storage)
ax = AnalyticsEngine()Node & edge aggregations
# How many nodes of each label exist?
ax.label_counts(engine)
# → {"Document": 12400, "Person": 830, "Concept": 4200}
# How many edges of each type?
ax.edge_type_counts(engine)
# → {"AUTHORED_BY": 8900, "REFERENCES": 31000, "TAGGED_WITH": 19200}
# Out-degree frequency histogram with min / max / mean
ax.degree_distribution(engine)
# → {"data": {0: 120, 1: 430, 2: 890, ...}, "min": 0, "max": 147, "mean": 4.3}
# Top-K nodes by degree (out / in / both)
ax.top_k_nodes_by_degree(engine, k=10, direction="out")
# → [("node_abc", 147), ("node_xyz", 134), ...]
# Average out-degree
ax.avg_degree(engine)
# → 4.3
# Graph density — ratio of actual to maximum possible directed edges
ax.density(engine)
# → 0.00031Structural analytics
# Weakly-connected components — find isolated subgraphs
result = ax.connected_components(engine)
# result.data → {"num_components": 3, "component_sizes": [12400, 8, 1], ...}Temporal analytics
# Group nodes (or edges) by a date property, bucketed by day / month / year
result = ax.temporal_grouping(
engine,
property_name="created_at",
interval="month",
entity="node",
)
# result.data → {"2026-01": 340, "2026-02": 890, "2026-03": 1200}Property analytics
# Numeric aggregation over any node or edge property
result = ax.property_stats(engine, property_name="score", entity="node")
# result.data → {"min": 0.01, "max": 0.99, "mean": 0.54, "count": 12400}Graph projection (for ML / BI pipelines)
GraphProjection extracts structured representations from the live graph for downstream use — pandas, networkx, PyTorch Geometric, or any BI tool.
from purple8_graph import GraphProjection
proj = GraphProjection()
# List of dicts for all "Document" nodes — ready for pandas
docs = proj.project_label(engine, label="Document")
df = pd.DataFrame(docs)
# Adjacency list: {node_id: [neighbour_ids]}
adj = proj.to_adjacency_list(engine)
# Edge list: [(source_id, target_id, edge_type)]
edges = proj.to_edge_list(engine)
# Node feature matrix: {node_id: {property: value, ...}}
features = proj.to_node_feature_matrix(engine, label="Document")Additional Built-In Graph Algorithms
All algorithms in core/algorithms.py operate directly on BrickCoreStorage (Purple8 Cortex) iterators (iter_nodes() / iter_edges()) without materialising the full graph into Python lists, and all return result objects with .to_dict() serialisation:
| Algorithm | SDK / Function | MCP Tool | Notes |
|---|---|---|---|
| PageRank | engine.pagerank() | graph.pagerank | Power-iteration with dangling-node redistribution |
| Temporal PageRank | temporal_pagerank() | — | Time-decayed edge weights |
| Dynamic PageRank | dynamic_pagerank() | — | Incremental re-propagation on graph deltas |
| Community detection | engine.detect_communities() | graph.communities | Louvain-style label propagation + Newman-Girvan modularity |
| Dynamic communities | dynamic_detect_communities() | — | Delta-aware re-clustering |
| Betweenness centrality | engine.betweenness_centrality() | graph.centrality | Brandes' exact algorithm, normalised 0–1 |
| Shortest path | engine.shortest_path() | graph.shortest_path | Weighted Dijkstra |
| Link prediction | engine.predict_links() | graph.link_prediction | Jaccard, Adamic-Adar, Resource Allocation, Preferential Attachment, Common Neighbours, PPR |
| Spatial autocorrelation | morans_i() / local_morans_i() | graph.morans_i, graph.local_morans_i | Global and local Moran's I over graph weights |
| Triangle counting | count_triangles() | — | Per-node clustering coefficients |
| Biconnected components | biconnected_components() | — | Tarjan DFS |
| Bridge detection | find_bridges() | — | Single points of failure |
| Bipartite detection & matching | is_bipartite() / bipartite_matching() | — | BFS 2-colouring + Hopcroft-Karp max matching |
| Minimum spanning tree | minimum_spanning_tree() | — | Kruskal + union-find |
Performance guidelines
| Algorithm | Complexity | Practical limit |
|---|---|---|
| PageRank | O(V + E) per iteration | 10M+ nodes in seconds |
| Community detection | O(E) per pass | 10M+ nodes in seconds |
| Betweenness centrality | O(V·E) | ~500K nodes comfortably; sample for larger |
| Shortest path (Dijkstra) | O((V + E) log V) | 0.79 ms p50 @ 10K nodes; real-time for most graphs |
| OLAP aggregations | O(V) or O(E) | Linear — handles full graph size |
Running analytics in production
On Enterprise / Custom (Self-Hosted) deployments with Raft sharding or WAL-streaming replicas, schedule heavy analytics (such as full-graph betweenness centrality) during off-peak hours or run them against a read replica to avoid competing with live query traffic.
Web UI
The Analytics tab in the LCNC admin console (/lcnc/graph) provides a no-code interface for graph algorithms:
- PageRank — configurable damping factor and top-K, bar chart of top nodes
- Communities — run detection, see modularity score, browse community sizes
- Centrality — normalised betweenness scores, ranked node list
REST API and MCP access (graph.pagerank, graph.communities, graph.centrality, graph.link_prediction, graph.shortest_path, graph.morans_i, graph.local_morans_i) are available in both Developer (Self-Hosted Free) and Enterprise / Custom (Self-Hosted) tiers — only scale quotas differ.