Skip to content

Commit ac7ce32

Browse files
authored
Streaming create communities (#2237)
* add manual release instructions * create streaming * fix deleted file * addd file * fix check * add consistency * fix logic
1 parent 6d9f0dc commit ac7ce32

6 files changed

Lines changed: 1045 additions & 63 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "patch",
3+
"description": "create_communities streaming"
4+
}

RELEASE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,4 +184,4 @@ graphrag-common (no internal deps)
184184
├── graphrag-cache (common, storage)
185185
├── graphrag-llm (cache, common)
186186
└── graphrag (all of the above)
187-
```
187+
```

packages/graphrag/graphrag/index/operations/cluster_graph.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""A module containing cluster_graph method definition."""
55

66
import logging
7+
from collections import defaultdict
78

89
import pandas as pd
910

@@ -34,12 +35,9 @@ def cluster_graph(
3435

3536
clusters: dict[int, dict[int, list[str]]] = {}
3637
for level in levels:
37-
result = {}
38+
result: dict[int, list[str]] = defaultdict(list)
3839
clusters[level] = result
39-
for node_id, raw_community_id in node_id_to_community_map[level].items():
40-
community_id = raw_community_id
41-
if community_id not in result:
42-
result[community_id] = []
40+
for node_id, community_id in node_id_to_community_map[level].items():
4341
result[community_id].append(node_id)
4442

4543
results: Communities = []
@@ -64,15 +62,25 @@ def _compute_leiden_communities(
6462
# so we replicate that by normalizing direction then keeping last.
6563
lo = edge_df[["source", "target"]].min(axis=1)
6664
hi = edge_df[["source", "target"]].max(axis=1)
67-
edge_df = edge_df.assign(source=lo, target=hi)
68-
edge_df = edge_df.drop_duplicates(subset=["source", "target"], keep="last")
65+
edge_df["source"] = lo
66+
edge_df["target"] = hi
67+
edge_df.drop_duplicates(subset=["source", "target"], keep="last", inplace=True)
6968

7069
if use_lcc:
7170
edge_df = stable_lcc(edge_df)
7271

72+
weights = (
73+
edge_df["weight"].astype(float)
74+
if "weight" in edge_df.columns
75+
else pd.Series(1.0, index=edge_df.index)
76+
)
7377
edge_list: list[tuple[str, str, float]] = sorted(
74-
(str(row["source"]), str(row["target"]), float(row.get("weight", 1.0)))
75-
for _, row in edge_df.iterrows()
78+
zip(
79+
edge_df["source"].astype(str),
80+
edge_df["target"].astype(str),
81+
weights,
82+
strict=True,
83+
)
7684
)
7785

7886
community_mapping = hierarchical_leiden(

packages/graphrag/graphrag/index/workflows/create_communities.py

Lines changed: 108 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55

66
import logging
77
from datetime import datetime, timezone
8-
from typing import cast
8+
from typing import Any, cast
99
from uuid import uuid4
1010

1111
import numpy as np
1212
import pandas as pd
13+
from graphrag_storage.tables.table import Table
1314

1415
from graphrag.config.models.graph_rag_config import GraphRagConfig
1516
from graphrag.data_model.data_reader import DataReader
@@ -28,88 +29,122 @@ async def run_workflow(
2829
"""All the steps to transform final communities."""
2930
logger.info("Workflow started: create_communities")
3031
reader = DataReader(context.output_table_provider)
31-
entities = await reader.entities()
3232
relationships = await reader.relationships()
33+
3334
max_cluster_size = config.cluster_graph.max_cluster_size
3435
use_lcc = config.cluster_graph.use_lcc
3536
seed = config.cluster_graph.seed
3637

37-
output = create_communities(
38-
entities,
39-
relationships,
40-
max_cluster_size=max_cluster_size,
41-
use_lcc=use_lcc,
42-
seed=seed,
43-
)
44-
45-
await context.output_table_provider.write_dataframe("communities", output)
38+
async with (
39+
context.output_table_provider.open("entities") as entities_table,
40+
context.output_table_provider.open("communities") as communities_table,
41+
):
42+
sample_rows = await create_communities(
43+
communities_table,
44+
entities_table,
45+
relationships,
46+
max_cluster_size=max_cluster_size,
47+
use_lcc=use_lcc,
48+
seed=seed,
49+
)
4650

4751
logger.info("Workflow completed: create_communities")
48-
return WorkflowFunctionOutput(result=output)
52+
return WorkflowFunctionOutput(result=sample_rows)
4953

5054

51-
def create_communities(
52-
entities: pd.DataFrame,
55+
async def create_communities(
56+
communities_table: Table,
57+
entities_table: Table,
5358
relationships: pd.DataFrame,
5459
max_cluster_size: int,
5560
use_lcc: bool,
5661
seed: int | None = None,
57-
) -> pd.DataFrame:
58-
"""All the steps to transform final communities."""
62+
) -> list[dict[str, Any]]:
63+
"""Build communities from clustered relationships and stream rows to the table.
64+
65+
Args
66+
----
67+
communities_table: Table
68+
Output table to write community rows to.
69+
entities_table: Table
70+
Table containing entity rows.
71+
relationships: pd.DataFrame
72+
Relationships DataFrame with source, target, weight,
73+
text_unit_ids columns.
74+
max_cluster_size: int
75+
Maximum cluster size for hierarchical Leiden.
76+
use_lcc: bool
77+
Whether to restrict to the largest connected component.
78+
seed: int | None
79+
Random seed for deterministic clustering.
80+
81+
Returns
82+
-------
83+
list[dict[str, Any]]
84+
Sample of up to 5 community rows for logging.
85+
"""
5986
clusters = cluster_graph(
6087
relationships,
6188
max_cluster_size,
6289
use_lcc,
6390
seed=seed,
6491
)
6592

93+
title_to_entity_id: dict[str, str] = {}
94+
async for row in entities_table:
95+
title_to_entity_id[row["title"]] = row["id"]
96+
6697
communities = pd.DataFrame(
6798
clusters, columns=pd.Index(["level", "community", "parent", "title"])
6899
).explode("title")
69100
communities["community"] = communities["community"].astype(int)
70101

71102
# aggregate entity ids for each community
72-
entity_ids = communities.merge(entities, on="title", how="inner")
103+
entity_map = communities[["community", "title"]].copy()
104+
entity_map["entity_id"] = entity_map["title"].map(title_to_entity_id)
73105
entity_ids = (
74-
entity_ids.groupby("community").agg(entity_ids=("id", list)).reset_index()
106+
entity_map
107+
.dropna(subset=["entity_id"])
108+
.groupby("community")
109+
.agg(entity_ids=("entity_id", list))
110+
.reset_index()
75111
)
76112

77-
# aggregate relationships ids for each community
78-
# these are limited to only those where the source and target are in the same community
79-
max_level = communities["level"].max()
80-
all_grouped = pd.DataFrame(
81-
columns=["community", "level", "relationship_ids", "text_unit_ids"] # type: ignore
82-
)
83-
for level in range(max_level + 1):
84-
communities_at_level = communities.loc[communities["level"] == level]
85-
sources = relationships.merge(
86-
communities_at_level, left_on="source", right_on="title", how="inner"
113+
# aggregate relationship ids per community, limited to
114+
# intra-community edges (source and target in the same community).
115+
# Process one hierarchy level at a time to keep intermediate
116+
# DataFrames small, then concat the grouped results once at the end.
117+
level_results = []
118+
for level in communities["level"].unique():
119+
level_comms = communities[communities["level"] == level]
120+
with_source = relationships.merge(
121+
level_comms, left_on="source", right_on="title", how="inner"
87122
)
88-
targets = sources.merge(
89-
communities_at_level, left_on="target", right_on="title", how="inner"
123+
with_both = with_source.merge(
124+
level_comms, left_on="target", right_on="title", how="inner"
90125
)
91-
matched = targets.loc[targets["community_x"] == targets["community_y"]]
92-
text_units = matched.explode("text_unit_ids")
126+
intra = with_both[with_both["community_x"] == with_both["community_y"]]
127+
if intra.empty:
128+
continue
93129
grouped = (
94-
text_units
95-
.groupby(["community_x", "level_x", "parent_x"])
96-
.agg(relationship_ids=("id", list), text_unit_ids=("text_unit_ids", list))
130+
intra
131+
.explode("text_unit_ids")
132+
.groupby(["community_x", "parent_x"])
133+
.agg(
134+
relationship_ids=("id", list),
135+
text_unit_ids=("text_unit_ids", list),
136+
)
97137
.reset_index()
98138
)
99-
grouped.rename(
100-
columns={
101-
"community_x": "community",
102-
"level_x": "level",
103-
"parent_x": "parent",
104-
},
105-
inplace=True,
106-
)
107-
all_grouped = pd.concat([
108-
all_grouped,
109-
grouped.loc[
110-
:, ["community", "level", "parent", "relationship_ids", "text_unit_ids"]
111-
],
112-
])
139+
grouped["level"] = level
140+
level_results.append(grouped)
141+
142+
all_grouped = pd.concat(level_results, ignore_index=True).rename(
143+
columns={
144+
"community_x": "community",
145+
"parent_x": "parent",
146+
}
147+
)
113148

114149
# deduplicate the lists
115150
all_grouped["relationship_ids"] = all_grouped["relationship_ids"].apply(
@@ -146,7 +181,27 @@ def create_communities(
146181
final_communities["period"] = datetime.now(timezone.utc).date().isoformat()
147182
final_communities["size"] = final_communities.loc[:, "entity_ids"].apply(len)
148183

149-
return final_communities.loc[
150-
:,
151-
COMMUNITIES_FINAL_COLUMNS,
152-
]
184+
output = final_communities.loc[:, COMMUNITIES_FINAL_COLUMNS]
185+
rows = output.to_dict("records")
186+
sample_rows: list[dict[str, Any]] = []
187+
for row in rows:
188+
row = _sanitize_row(row)
189+
await communities_table.write(row)
190+
if len(sample_rows) < 5:
191+
sample_rows.append(row)
192+
return sample_rows
193+
194+
195+
def _sanitize_row(row: dict[str, Any]) -> dict[str, Any]:
196+
"""Convert numpy types to native Python types for table serialization."""
197+
sanitized = {}
198+
for key, value in row.items():
199+
if isinstance(value, np.ndarray):
200+
sanitized[key] = value.tolist()
201+
elif isinstance(value, np.integer):
202+
sanitized[key] = int(value)
203+
elif isinstance(value, np.floating):
204+
sanitized[key] = float(value)
205+
else:
206+
sanitized[key] = value
207+
return sanitized

0 commit comments

Comments
 (0)