Graph RAG & Iceberg Edge Tables Guide¶
BenoStreamDB provides native graph analytics and edge-level semantic search directly over Apache Iceberg edge tables. This architecture eliminates the need to run separate vector databases (e.g. Pinecone/Milvus) alongside dedicated graph databases (e.g. Neo4j) or Spark GraphX clusters.
1. Standard Edge Table Schema Convention¶
An edge table in BenoStreamDB represents relationships between entities. To guarantee high performance across graph analytics UDFs and vector search, edge tables follow this standard convention:
Column Name |
Arrow / Iceberg Data Type |
Nullable |
Description |
|---|---|---|---|
|
|
No |
Origin node / entity identifier. Using integer IDs provides maximum speed in graph traversals. |
|
|
No |
Destination node / entity identifier. |
|
|
Yes |
Relationship predicate (e.g. |
|
|
No |
Edge weight / strength (defaults to |
|
|
Yes |
Dense semantic embedding vector describing the relationship, interaction context, or anchor text. |
Creating an Edge Table¶
You can create a standardized edge table with automated indexing using Table.create_edge_table:
import benostreamdb as bsdb
# Create an edge table with automated sidecar index generation
edge_table = bsdb.Table.create_edge_table(
uri="file:///data/knowledge_graph/edges",
node_id_type="uint64", # 'uint64' (optimal for Graph UDFs) or 'string'
embedding_dim=384, # Vector dimension for edge semantics
with_relation=True, # Include 'relation' string column
with_weight=True, # Include 'weight' float column
index_endpoints=True, # Auto-generate Roaring Bitmap sidecars on source & target
index_embedding=True, # Auto-generate HNSW index on embedding column
partition_by_relation=False # Optional: partition Iceberg data files by relation type
)
2. Automated Sidecar Indexing¶
Graph traversals and vector searches require distinct physical acceleration structures:
A. Endpoint Roaring Bitmaps (source and target)¶
When
index_endpoints=Trueis specified, BenoStreamDB automatically configures Roaring Bitmap sidecar indexes on thesourceandtargetcolumns.Why: Allows $O(1)$ neighborhood lookups and instantaneous candidate filtering during breadth-first search (BFS) and induced subgraph extraction without table scanning.
B. HNSW Vector Index on embedding¶
When
index_embedding=Trueis specified and anembeddingcolumn exists, BenoStreamDB automatically configures an HNSW vector index on the relationship vectors.Why: Enables semantic similarity search directly over relationships (e.g. finding relationships describing “mergers and acquisitions” or “patent disputes”).
3. NetworkX Interoperability¶
BenoStreamDB provides complete bi-directional interoperability with NetworkX, supporting all graph types: Graph, DiGraph, MultiGraph, and MultiDiGraph.
Ingesting from NetworkX (Table.from_networkx)¶
import networkx as nx
import benostreamdb as bsdb
# Build or load a graph in NetworkX
G = nx.DiGraph()
G.add_edge(1, 2, relation="cites", weight=2.5, paper_id="arXiv:2401.001")
G.add_edge(2, 3, relation="co_author", weight=1.0)
G.add_edge(3, 1, relation="mentions", weight=0.8)
# Ingest directly into an Iceberg edge table with sidecar indexes
table = bsdb.Table.from_networkx(
uri="file:///data/graphs/citation_graph",
graph=G,
index_endpoints=True
)
table.commit()
Exporting to NetworkX (table.to_networkx)¶
You can export any BenoStreamDB edge table back into a NetworkX graph while preserving all edge attributes and multi-edges:
# Export to a directed NetworkX graph
G_out = table.to_networkx(directed=True, multigraph=False)
assert G_out.has_edge(1, 2)
assert G_out[1][2]["relation"] == "cites"
assert G_out[1][2]["weight"] == 2.5
4. End-to-End Hybrid Graph RAG Workflow¶
Graph RAG combines semantic vector search with structural graph context to retrieve coherent subgraphs rather than isolated text chunks.
Step 1: Semantic Edge Search¶
Find the most relevant edges in vector space matching the user’s question:
query_vector = [0.12, -0.45, 0.88, ...] # 384-dimensional query embedding
# Search edge embeddings for relevant relationships
matched_edges = table.search("embedding", query_vector, k=5)
# matched_edges contains: source, target, relation, weight, distance
Step 2: Seed Node Extraction¶
Extract the endpoint nodes of the top semantic relationships:
seed_nodes = list(set(matched_edges["source"]).union(set(matched_edges["target"])))
Step 3: Multi-Hop Subgraph Extraction¶
Extract the multi-hop induced subgraph surrounding the semantic seeds:
# Extract 2-hop induced subgraph starting from seed nodes
subgraph_edges = table.subgraph(seed_nodes, hops=2, directed=False).to_pandas()
Step 4: Personalized PageRank Grounding¶
Rank nodes in the extracted context by their structural relevance to the seed entities:
# Compute Personalized PageRank with teleportation biased toward the seed entities
pagerank_scores = table.personalized_pagerank(
seeds=seed_nodes,
alpha=0.85,
iterations=30
).to_pandas()
Step 5: Connecting Paths for Multi-Entity Reasoning¶
If the query mentions two or more distinct entities, find all connecting paths between them:
paths = table.connecting_paths(
seeds=[entity_a_id, entity_b_id],
max_depth=3,
directed=False
).to_pandas()
5. Iceberg Partitioning & Optimization Best Practices¶
Node ID Types:
Prefer
uint64forsourceandtargetwhen performance is critical. Integer comparisons in DataFusion graph accumulators avoid string allocation overhead.For heterogeneous entity graphs (e.g.
user:101,product:502), map string entity IDs to 64-bit integer hashes or maintain a dictionary mapping table.
Partitioning by Relation:
If your graph has millions of edges with distinct predicates (
partition_by_relation=True), BenoStreamDB partitions data files into directories byrelation.Queries filtering on specific edge types (e.g.
WHERE relation = 'cites') prune irrelevant data files at the manifest level before scanning Parquet files.
Combined BM25 + Vector Search:
For edge tables with descriptive text in
relationorsummarycolumns, combine lexical matching with vector search using the REST gateway (port 9200), DataFusion SQL, ortable.hybrid_search(...).
6. End-to-End Graph RAG Search & Community Summarization¶
BenoStreamDB provides full native pipeline integration connecting vector document retrieval, knowledge graph traversal, and topological relevance scoring:
Local Graph RAG Search¶
Local search finds seed entities via vector search, expands the neighborhood via an induced multi-hop subgraph, ranks entities using Personalized PageRank (PPR), and formats the context into prompt-ready markdown for an LLM:
# Query document table using embedding vector + edge table graph topology
result = doc_table.graph_rag_search(
query=query_embedding, # List[float] dense embedding
edge_table=edge_table, # Edge table containing relational edges
mode="local", # 'local' or 'global'
hops=2, # 2-hop induced subgraph
top_k=5, # Initial seed count
alpha=0.85, # Personalized PageRank restart probability
allowed_relations=["invested_in", "supplies"], # Filter out hub noise like 'mentioned_in'
search_edges=True # Dual Vector-Graph RAG: search edge embeddings in parallel
)
# Inspect retrieved entities (ranked by PageRank score) and induced edges
print(result.nodes) # pandas.DataFrame with columns: id, title, content, pagerank
print(result.edges) # pandas.DataFrame with columns: source, target, weight
# Format prompt-ready markdown for direct LLM injection
llm_context = result.format_context(max_tokens=2000)
HippoRAG-Style Continuous Seed-Weighted Personalized PageRank¶
Unlike standard uniform seed PPR where teleportation probability is split uniformly across seeds ($p_0(v) = 1/|S|$), BenoStreamDB supports HippoRAG-style continuous similarity weighting ($p_0(v) \propto w(v)$):
# Pass continuous relevance weights from vector similarity scores or reciprocal distances
pagerank_scores = edge_table.personalized_pagerank(
seeds=[10, 20],
seed_weights=[0.92, 0.15], # Modulates teleportation mass toward node 10
damping=0.85,
iterations=30
).to_pandas()
Predicate & Relation Pushdown in Subgraphs¶
Eliminate high-degree hub noise (e.g. 'mentioned_in', 'contains') by restricting traversal to high-signal predicates:
# Extract 2-hop induced subgraph traversing ONLY 'founded' and 'invested_in' edges
subgraph_df = edge_table.subgraph(
seeds=[1, 2],
hops=2,
allowed_relations=["founded", "invested_in"]
).to_pandas()
Dual Vector-Graph RAG (Semantic Edge Search)¶
Query relationship edge embeddings in parallel with entity doc embeddings to uncover relation-phrase seeds (e.g. “patent licensing agreement between chipmakers”):
result = doc_table.graph_rag_search(
query=query_embedding,
edge_table=edge_table,
mode="local",
search_edges=True, # Searches edge embeddings simultaneously
edge_vector_column="embedding"
)
Hierarchical Community Summarization Workflow (Global Search)¶
Generate multi-level community trees across Louvain resolution pyramids (level, parent_community_id) for Microsoft GraphRAG parity:
# Materialize multi-level hierarchical community pyramid as an Iceberg table
comm_table = edge_table.summarize_communities(
doc_table=doc_table,
target_uri="file:///tmp/hierarchical_communities",
hierarchical=True,
resolutions=[0.5, 1.0, 2.0], # Macro (L0) -> Meso (L1) -> Micro (L2)
top_entities_per_comm=5
)
# Global Graph RAG search over hierarchical community tree
result = doc_table.graph_rag_search(
query=query_embedding,
edge_table=edge_table,
mode="global",
community_table=comm_table,
top_k=3
)
print(result.format_context())
Bounded Traversal (CRS Fast Path)¶
When the edge table has a CSR graph index, subgraph / graph_neighbors run over the
memory-mapped CSR instead of re-materializing the edge set. Two independent caps bound
the frontier — important on scale-free graphs like the Wikipedia link network, where a
handful of super-nodes reach most of the graph in two hops:
# Skip super-nodes (>500 links) and cap the total visited set.
edges = edge_table.subgraph(
seeds, hops=2, graph_column="source",
max_degree=500, # report but do not expand hubs
max_nodes=10_000, # hard token-budget cap on the visited set
)
# Node set only — no edge materialization (Graph RAG uses this internally).
nodes = edge_table.subgraph_nodes(seeds, hops=2, graph_column="source", max_nodes=10_000)
Table.communities() also runs over the CSR when available, keeping only O(V) state
resident regardless of the edge count. Choose Louvain (default) or Leiden (connected
communities):
parts = edge_table.communities(resolution=1.0, algorithm="leiden") # community_id + community
# Incremental update: warm-start from the previous partition so unchanged
# communities keep their ids.
parts2 = edge_table.update_communities(previous=parts)
Knowledge-Graph Extraction (Table.extract_graph)¶
Build an edge table (plus an optional claims table) from a document table using an LLM. Entity names map to stable uint64 ids via SHA-256, so the graph is reproducible:
kg = doc_table.extract_graph(
doc_table,
target_uri="file:///tmp/kg",
llm=lambda prompt: my_llm(prompt), # -> JSON {entities, relationships, claims}
claims_uri="file:///tmp/claims",
)
Entity Equivalence Resolution (Table.resolve_entities)¶
Automatically resolve synonyms, aliases, and entity duplicates across same_as edges using transitive equivalence closure:
# Compute transitive alias closure over 'same_as' edges
mapping = edge_table.resolve_entities(relation="same_as")
# Returns: {10: 10, 20: 10, 30: 10, 40: 40, 50: 40}
Reciprocal Rank Fusion (RRF) Hybrid Search¶
Combine dense vector search and BM25 text keyword search into a fused ranked result:
results = doc_table.hybrid_search(
text_column="content",
query_text="semiconductor GPU foundry",
vector_column="embedding",
query_vector=query_embedding,
k=10,
rrf_k=60
)
7. Analytics Engineering with dbt (dbt-benostreamdb)¶
The official dbt-benostreamdb adapter plugin provides native Jinja macros for all Graph UDFs, allowing analytics engineers to model, transform, and materialize graph analytics tables into Apache Iceberg directly within dbt projects:
-- models/entity_pagerank.sql
{{ config(materialized='table') }}
{{ pagerank(ref('knowledge_graph_edges'), damping=0.85, iterations=30) }}
Supported dbt Graph Macros:¶
Macro |
Description |
|---|---|
|
Iterative PageRank node importance scores |
|
HippoRAG-style continuous seed-weighted PPR |
|
Louvain or Label Propagation community clusters |
|
N-hop neighborhood expansion |
|
Multi-hop induced subgraph extraction |
|
Pairwise shortest connecting paths between seeds |
|
Shortest path node sequences |
|
Weakly / strongly connected components |
|
In/out/total degree centrality distribution |
|
Link prediction scores (Jaccard, Adamic-Adar, etc.) |
|
Directed acyclic graph execution ordering |