用 MGeo 和 Neo4j 搭建中文地址语义知识图谱
中文地址的表达方式千奇百怪,缩写、省略、别名到处都是。用 Levenshtein 距离或规则清洗去匹配它们,不仅累,准确率也上不去。阿里巴巴开源的 MGeo 模型专门解决这个问题,它基于 BERT 微调,输入一对地址就能给出 0~1 的语义相似度。像'上海徐汇漕溪路 123 号华鑫天地'和'上海市徐汇区漕溪路 123 号 B 栋'这种,得分能到 0.96。
有了这些匹配结果之后,怎么把它们变成可查询、可分析的知识库?我的选择是导入 Neo4j。节点存地址,边存相似关系,图算法的能力正好用来做地址聚类和归一化。下面把整个搭建过程记录下来。
MGeo 部署:镜像拉下来就能跑
阿里提供了 Docker 镜像,在单卡 GPU(比如 4090D)上直接启动就行:
docker run -itd --gpus all \
-p 8888:8888 \
-v /your/workspace:/root/workspace \
registry.aliyuncs.com/mgeo-public/mgeo-inference:latest
容器里自带 Conda 和 Jupyter Notebook,浏览器打开 http://localhost:8888 就能用。不过,官方给的推理脚本放在 /root/推理.py,我习惯先把它拷到 workspace 目录再改,免得误操作:
docker exec -it <container_id> /bin/bash
cp /root/推理.py /root/workspace
然后激活环境运行推理:
conda activate py37testmaas
python /root/workspace/推理.py
你可以在这个脚本里加日志输出、批量处理逻辑,或者把结果导出成 JSON/CSV,方便后面导入图库。
用 MGeo 做地址匹配
模型输出的典型结果长这样:
{
"addr1": "杭州市文三路 369 号",
"addr2": "杭州西湖文三路 369 号智博大厦",
"similarity": 0.93,
"is_match": true
}
超过 0.85 我就认为是'同指'实体。实际处理几万条地址时,两两配对太多了,可以先按城市分区粗略分组,或者用 Elasticsearch 的模糊召回先筛一遍,减少无效比对。
导入 Neo4j:建模与写入
图模型很简单:
- 节点标签
Address,属性:id(唯一ID)、raw_text(原始地址)、city、district、street等。 - 关系类型
SIMILAR_TO,属性:score(相似度)、source(来源)、timestamp。
用 Cypher 手动建两个节点试试:
CREATE (a1:Address { id: "addr_001", raw_text: "北京市朝阳区建国门外大街 1 号", city: "北京", district: "朝阳区" })
CREATE (a2:Address { id: "addr_002", raw_text: "北京朝阳建外大街 1 号国贸大厦", city: "北京", district: "朝阳区" })
CREATE (a1)-[:SIMILAR_TO { score: 0.95, source: "mgeo_v1", timestamp: datetime() }]->(a2)
实际批量导入,我直接写 Python 用 neo4j-driver 操作:
from neo4j import GraphDatabase
uri = "bolt://localhost:7687"
driver = GraphDatabase.driver(uri, auth=("neo4j", "your_password"))
def create_address_and_relations(tx, addr_data, relations):
for addr in addr_data:
tx.run("""
MERGE (a:Address {id: $id})
SET a.raw_text = $raw_text, a.province = $province,
a.city = $city, a.district = $district, a.street = $street
""", **addr)
for rel in relations:
tx.run("""
MATCH (a1:Address {id: $from_id})
MATCH (a2:Address {id: $to_id})
MERGE (a1)-[r:SIMILAR_TO]->(a2)
SET r.score = $score, r.source = $source, r.timestamp = datetime()
""", **rel)
# 示例数据
addresses = [
{"id": "addr_001", "raw_text": "上海市徐汇区漕溪路 123 号", "city": "上海", "district": "徐汇区", "street": "漕溪路 123 号"},
{"id": "addr_002", "raw_text": "上海徐汇漕溪路 123 号华鑫天地", "city": "上海", "district": "徐汇区", "street": "漕溪路 123 号"}
]
similarities = [{"from_id": "addr_001", "to_id": "addr_002", "score": 0.94, "source": "mgeo_v1"}]
with driver.session() as session:
session.execute_write(create_address_and_relations, addresses, similarities)
用 MERGE 而不用 CREATE 是因为重复跑脚本时不至于抛异常。大数据量时,可以加上 UNWIND 分批写入。
图上的查询与分析
数据进去后,最常用的就是找'连通组件'——把那些通过高相似度关系连在一起的地址归为一簇。前提是你的 Neo4j 装了 GDS 插件:
MATCH path = (a:Address)-[:SIMILAR_TO {score: 0.85}]-(b)
WITH collect(path) AS subgraph
CALL gds.alpha.connectedComponents.stream({
nodeProjection: 'Address',
relationshipProjection: {
SIMILAR_TO: {
type: 'SIMILAR_TO',
properties: 'score',
orientation: 'UNDIRECTED'
}
},
relationshipWeightProperty: 'score'
})
YIELD nodeId, componentId
RETURN gds.util.asNode(nodeId).raw_text AS address, componentId
ORDER BY componentId
每个 componentId 对应一组语义一致的地址,选代表地址时就方便了。
人工复核那些模棱两可的匹配也容易,查得分在 0.7~0.85 之间的关系:
MATCH (a)-[r:SIMILAR_TO]->(b)
WHERE r.score >= 0.7 AND r.score < 0.85
RETURN a.raw_text, b.raw_text, r.score
LIMIT 10
在 Neo4j Browser 里跑个可视化查询,也能直观看到关联网络:
MATCH (a:Address)-[r:SIMILAR_TO]->(b)
WHERE r.score > 0.9
RETURN a, r, b
LIMIT 50
维护小事
- 索引要建,否则节点多了查询慢:
CREATE INDEX FOR (a:Address) ON (a.id); - 定期用
neo4j-admin dump备份。 - 生产环境别用默认密码。
结尾
这套方案跑了几个月,在地址去重和归一化上省了不少人工,但 MGeo 的阈值设定仍得根据业务微调——0.8 可能太松,0.9 又太严。临界值附近的 case 最让人头疼,偶尔还得肉眼扫一遍。后面打算把 GPS 坐标也塞进图里,结合空间关系可能还能搞点别的好玩查询。如果你也在做地址库的治理,这种'语义模型 + 图数据库'的路子值得一试。

