fix: bind knowledge maps to explicit regions

This commit is contained in:
xuelong committed 2026-08-26 02:26:24 -07:00
1 parent 329a94d457
commit 90f75d41ae
15 files changed
+1126 -275

No files matched your search

+165 -61
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
"""Export the current Yunyou Libo FalkorDB graph as a lossless import bundle.
"""Export any live FalkorDB graph as a lossless project import bundle.
The live ``yunyou_libo`` graph is the authority for this export. Earlier
versions rebuilt a different graph from PostgreSQL rows: duplicate AMap POIs
The selected live graph is the authority for this export. Earlier project
fixtures rebuilt a different graph from PostgreSQL rows: duplicate AMap POIs
were split, media and links were modeled twice, and unrelated custom tables
were imported automatically. That made the JSON totals differ from the graph
shown by the project.
@@ -40,8 +40,6 @@ PROJECT_ID = "yunyou_libo"
GRAPH_NAME = "yunyou_libo"
DISPLAY_NAME = "云游荔波"
SCHEMA_VERSION = "3.0.0"
SPATIAL_MAP = {"enabled": True, "scope": "libo"}
LABEL_PRIORITY = (
"Hotel",
"FoodPlace",
@@ -196,46 +194,69 @@ def export_live_graph(graph: Any) -> tuple[list[dict[str, Any]], list[dict[str,
exported_id_by_internal: dict[str, str] = {}
exported_ids: set[str] = set()
for row in graph.query("MATCH (n) RETURN n").result_set:
raw_node = row[0]
source_internal_id = internal_id(raw_node)
properties = properties_of(raw_node)
node_id = str(properties.get("__kg_node_id") or f"falkor:{source_internal_id}")
if node_id in exported_ids:
raise RuntimeError(f"图谱存在重复导出节点 ID:{node_id}")
labels = labels_of(raw_node)
exported_ids.add(node_id)
exported_id_by_internal[source_internal_id] = node_id
nodes.append(
{
"id": node_id,
"type": primary_label(labels),
"labels": labels,
"properties": properties,
}
)
page_size = 5000
offset = 0
while True:
rows = graph.query(
"MATCH (n) RETURN n ORDER BY id(n) "
f"SKIP {offset} LIMIT {page_size}"
).result_set
for row in rows:
raw_node = row[0]
source_internal_id = internal_id(raw_node)
properties = properties_of(raw_node)
node_id = str(properties.get("__kg_node_id") or f"falkor:{source_internal_id}")
if node_id in exported_ids:
raise RuntimeError(f"图谱存在重复导出节点 ID:{node_id}")
labels = labels_of(raw_node)
exported_ids.add(node_id)
exported_id_by_internal[source_internal_id] = node_id
nodes.append(
{
"id": node_id,
"type": primary_label(labels),
"labels": labels,
"properties": properties,
}
)
if len(rows) < page_size:
break
offset += page_size
relations: list[dict[str, Any]] = []
rows = graph.query("MATCH (source)-[relation]->(target) RETURN source, relation, target")
for source, edge, target in rows.result_set:
source_id = exported_id_by_internal.get(internal_id(source))
target_id = exported_id_by_internal.get(internal_id(target))
if not source_id or not target_id:
raise RuntimeError("关系引用了未导出的节点")
relations.append(
{
"type": relation_type_of(edge),
"source": source_id,
"target": target_id,
"properties": properties_of(edge),
}
)
offset = 0
while True:
rows = graph.query(
"MATCH (source)-[relation]->(target) "
"RETURN source, relation, target ORDER BY id(relation) "
f"SKIP {offset} LIMIT {page_size}"
).result_set
for source, edge, target in rows:
source_id = exported_id_by_internal.get(internal_id(source))
target_id = exported_id_by_internal.get(internal_id(target))
if not source_id or not target_id:
raise RuntimeError("关系引用了未导出的节点")
relations.append(
{
"type": relation_type_of(edge),
"source": source_id,
"target": target_id,
"properties": properties_of(edge),
}
)
if len(rows) < page_size:
break
offset += page_size
return nodes, relations
def build_schema(
nodes: list[dict[str, Any]],
relations: list[dict[str, Any]],
*,
project_id: str,
graph_name: str,
display_name: str,
) -> dict[str, Any]:
rows_by_label: dict[str, list[dict[str, Any]]] = defaultdict(list)
primary_type_by_id: dict[str, str] = {}
@@ -271,10 +292,10 @@ def build_schema(
}
return {
"namespace": PROJECT_ID,
"namespace": project_id,
"version": SCHEMA_VERSION,
"display_name": "云游荔波当前图谱快照 Schema v3",
"description": "从当前 yunyou_libo FalkorDB 图谱无损导出,保留多标签节点。",
"display_name": f"{display_name}当前图谱快照 Schema v3",
"description": f"从当前 {graph_name} FalkorDB 图谱无损导出,保留多标签节点。",
"entity_types": entity_types,
"relation_types": relation_types,
}
@@ -302,9 +323,14 @@ def exported_counts(
primary_type_counts[item["type"]] += 1
properties = item["properties"]
has_coordinates = properties.get("lng") is not None and properties.get("lat") is not None
if not has_coordinates:
has_coordinates = (
properties.get("longitude") is not None
and properties.get("latitude") is not None
)
if has_coordinates:
coordinate_nodes += 1
if labels & MAP_POI_LABELS:
if labels & MAP_POI_LABELS or properties.get("place_type") or properties.get("type_label"):
map_poi_nodes += 1
if "BusStop" in labels:
bus_stop_nodes += 1
@@ -328,7 +354,8 @@ def source_counts(graph: Any) -> dict[str, Any]:
relation_count = int(graph.query("MATCH ()-[r]->() RETURN count(r)").result_set[0][0])
coordinate_count = int(
graph.query(
"MATCH (n) WHERE n.lng IS NOT NULL AND n.lat IS NOT NULL RETURN count(n)"
"MATCH (n) WHERE coalesce(n.lng,n.longitude) IS NOT NULL "
"AND coalesce(n.lat,n.latitude) IS NOT NULL RETURN count(n)"
).result_set[0][0]
)
label_counts: Counter[str] = Counter()
@@ -405,18 +432,51 @@ def dump_json(path: Path, value: Any, *, pretty: bool = False) -> None:
handle.write("\n")
def export(output_dir: Path) -> dict[str, Any]:
def safe_prefix(value: str) -> str:
normalized = "".join(
char.lower() if char.isalnum() else "_"
for char in value.strip()
)
return "_".join(part for part in normalized.split("_") if part) or "graph"
def export(
output_dir: Path,
*,
project_id: str = PROJECT_ID,
graph_name: str = GRAPH_NAME,
display_name: str = DISPLAY_NAME,
map_enabled: bool = True,
spatial_scope: str = "libo",
region_name: str | None = None,
region_adcode: str | None = None,
region_level: str | None = None,
output_prefix: str | None = None,
) -> dict[str, Any]:
output_dir.mkdir(parents=True, exist_ok=True)
generated_at = datetime.now(timezone.utc).isoformat()
default_region = (
("荔波县", "522722", "district")
if spatial_scope == "libo"
else ("贵州省", "520000", "province")
)
spatial_map = {
"enabled": map_enabled,
"scope": spatial_scope,
"region_name": (region_name or default_region[0]) if map_enabled else "",
"region_adcode": (region_adcode or default_region[1]) if map_enabled else "",
"region_level": (region_level or default_region[2]) if map_enabled else "province",
}
prefix = safe_prefix(output_prefix or project_id)
client = make_client()
try:
graph_names = {
item.decode("utf-8") if isinstance(item, bytes) else str(item)
for item in client.list_graphs()
}
if GRAPH_NAME not in graph_names:
raise RuntimeError(f"FalkorDB 中不存在图谱 {GRAPH_NAME!r}")
graph = client.select_graph(GRAPH_NAME)
if graph_name not in graph_names:
raise RuntimeError(f"FalkorDB 中不存在图谱 {graph_name!r}")
graph = client.select_graph(graph_name)
live_counts = source_counts(graph)
nodes, relations = export_live_graph(graph)
finally:
@@ -424,15 +484,21 @@ def export(output_dir: Path) -> dict[str, Any]:
counts = exported_counts(nodes, relations)
checks = validate_snapshot(live_counts, counts, nodes, relations)
schema = build_schema(nodes, relations)
schema = build_schema(
nodes,
relations,
project_id=project_id,
graph_name=graph_name,
display_name=display_name,
)
graph_data = {
"_bundle": {
"format": "znkg-falkordb-snapshot-v3",
"source": "falkordb",
"project_id": PROJECT_ID,
"graph_name": GRAPH_NAME,
"project_id": project_id,
"graph_name": graph_name,
"generated_at": generated_at,
"spatial_map": SPATIAL_MAP,
"spatial_map": spatial_map,
"source_snapshot_counts": live_counts,
},
"nodes": nodes,
@@ -440,9 +506,9 @@ def export(output_dir: Path) -> dict[str, Any]:
}
provision = {
"format": "znkg-project-bundle-v3",
"project_id": PROJECT_ID,
"display_name": DISPLAY_NAME,
"spatial_map": SPATIAL_MAP,
"project_id": project_id,
"display_name": display_name,
"spatial_map": spatial_map,
"schema": schema,
"graph_data": graph_data,
}
@@ -452,10 +518,10 @@ def export(output_dir: Path) -> dict[str, Any]:
if normalized["counts"]["relations"] != counts["relations"]:
raise RuntimeError("后端校验后的关系数量不一致")
schema_path = output_dir / "yunyou_libo_full_schema.v3.json"
graph_path = output_dir / "yunyou_libo_full_graph_data.v3.json"
bundle_path = output_dir / "yunyou_libo_full_bundle.v3.json"
manifest_path = output_dir / "yunyou_libo_full_manifest.v3.json"
schema_path = output_dir / f"{prefix}_full_schema.v3.json"
graph_path = output_dir / f"{prefix}_full_graph_data.v3.json"
bundle_path = output_dir / f"{prefix}_full_bundle.v3.json"
manifest_path = output_dir / f"{prefix}_full_manifest.v3.json"
dump_json(schema_path, schema, pretty=True)
dump_json(graph_path, graph_data)
dump_json(bundle_path, provision)
@@ -466,12 +532,12 @@ def export(output_dir: Path) -> dict[str, Any]:
}
manifest = {
"format": "znkg-full-graph-manifest-v3",
"project_id": PROJECT_ID,
"graph_name": GRAPH_NAME,
"project_id": project_id,
"graph_name": graph_name,
"schema_version": SCHEMA_VERSION,
"generated_at": generated_at,
"source": "falkordb",
"spatial_map": SPATIAL_MAP,
"spatial_map": spatial_map,
"source_snapshot_counts": live_counts,
"export_counts": counts,
"validation": "passed",
@@ -491,8 +557,46 @@ def main() -> None:
required=True,
help="Directory for schema, graph-data, bundle and manifest JSON files.",
)
parser.add_argument("--project-id", default=PROJECT_ID)
parser.add_argument("--graph-name", default=GRAPH_NAME)
parser.add_argument("--display-name", default=DISPLAY_NAME)
parser.add_argument(
"--map-enabled",
action=argparse.BooleanOptionalAction,
default=True,
help="Whether the exported project should enable the knowledge-map template.",
)
parser.add_argument(
"--map-scope",
choices=("libo", "guizhou"),
default="libo",
help="Boundary scope used by the single knowledge-map template.",
)
parser.add_argument(
"--output-prefix",
default=None,
help="Optional filename prefix; defaults to project-id.",
)
parser.add_argument("--region-name", default=None)
parser.add_argument("--region-adcode", default=None)
parser.add_argument(
"--region-level",
choices=("province", "city", "district"),
default=None,
)
args = parser.parse_args()
manifest = export(args.output_dir.expanduser().resolve())
manifest = export(
args.output_dir.expanduser().resolve(),
project_id=args.project_id,
graph_name=args.graph_name,
display_name=args.display_name,
map_enabled=args.map_enabled,
spatial_scope=args.map_scope,
region_name=args.region_name,
region_adcode=args.region_adcode,
region_level=args.region_level,
output_prefix=args.output_prefix,
)
print(json.dumps(manifest, ensure_ascii=False, indent=2))