使用 APOC NLP 进行实体提取
实体提取简介
实体提取接收非结构化文本,并返回该文本中包含的命名实体列表。
AWS、GCP 和 Azure 各自提供了 NLP API,这些 API 被 apoc.nlp 存储过程包装。
导入文本文档
我们将从卫报足球 RSS 源导入一些文本文档。我们可以使用 apoc.load.xml
来实现,如下面的查询所示
CALL apoc.load.xml("https://www.theguardian.com/football/rss", "rss/channel/item") YIELD value WITH [child in value._children WHERE child._type = "title" | child._text][0] AS title, [child in value._children WHERE child._type = "link" | child._text][0] AS guid, apoc.text.regreplace( [child in value._children WHERE child._type = "description" | child._text][0], '<[^>]*>', ' ' ) AS body, [child in value._children WHERE child._type = "category" | child._text] AS categories MERGE (a:Article {id: guid}) SET a.body = body, a.title = title WITH a, categories CALL { WITH a, categories UNWIND categories AS category MERGE (c:Category {name: category}) MERGE (a)-[:IN_CATEGORY]->(c) RETURN count(*) AS categoryCount } RETURN a.id AS articleId, a.title AS title, categoryCount;
我们可以在下面的图表中看到导入图谱的 Neo4j Browser 可视化效果
构建实体图谱
现在我们将构建我们的实体图谱。我们将结合使用 apoc.periodic.iterate
和特定于云的流式处理存储过程来
-
提取每篇文章的实体,并按特定实体类型过滤
-
为每个实体创建一个节点
-
从每个实体到文章创建一个关系
我们可以通过以下查询来实现
CALL apoc.periodic.iterate(
"MATCH (a:Article) WHERE not(exists(a.processed)) RETURN a",
"CALL apoc.nlp.aws.entities.stream([item in $_batch | item.a], {
key: $awsApiKey,
secret: $awsApiSecret,
nodeProperty: 'body'
})
YIELD node AS a, value
SET a.processed = true
WITH a, value
UNWIND value.entities AS entity
WITH a, entity
WHERE entity.type IN ['COMMERCIAL_ITEM', 'PERSON', 'ORGANIZATION', 'LOCATION', 'EVENT']
CALL apoc.merge.node(
['Entity', apoc.text.capitalize(toLower(entity.type))],
{name: entity.text}, {}, {}
)
YIELD node AS e
MERGE (a)-[entityRel:AWS_ENTITY]->(e)
SET entityRel.score = entity.score
RETURN count(*)",
{ batchMode: "BATCH_SINGLE",
batchSize: 25,
params: {awsApiKey: $awsApiKey, awsApiSecret: $awsApiSecret}}
)
YIELD batches, total, timeTaken, committedOperations, errorMessages, batch, operations
RETURN batches, total, timeTaken, committedOperations, errorMessages, batch, operations;
批次 | 总计 | 耗时 | 已提交操作 | 错误消息 | 批次 | 操作 |
---|---|---|---|---|---|---|
3 |
59 |
2 |
59 |
{} |
{总计: 3, 已提交: 3, 失败: 0, 错误: {}} |
{总计: 59, 已提交: 59, 失败: 0, 错误: {}} |
CALL apoc.periodic.iterate(
"MATCH (a:Article) WHERE not(exists(a.processed)) RETURN a",
"CALL apoc.nlp.gcp.entities.stream([item in $_batch | item.a], {
key: $gcpApiKey,
nodeProperty: 'body'
})
YIELD node AS a, value
SET a.processed = true
WITH a, value
UNWIND value.entities AS entity
WITH a, entity
WHERE entity.type IN ['PERSON', 'LOCATION', 'ORGANIZATION', 'EVENT']
CALL apoc.merge.node(
['Entity', apoc.text.capitalize(toLower(entity.type))],
{name: entity.name}, {}, {}
)
YIELD node AS e
MERGE (a)-[entityRel:GCP_ENTITY]->(e)
SET entityRel.score = entity.score
RETURN count(*)",
{ batchMode: "BATCH_SINGLE",
batchSize: 25,
params: {gcpApiKey: $gcpApiKey}}
)
YIELD batches, total, timeTaken, committedOperations, errorMessages, batch, operations
RETURN batches, total, timeTaken, committedOperations, errorMessages, batch, operations;
批次 | 总计 | 耗时 | 已提交操作 | 错误消息 | 批次 | 操作 |
---|---|---|---|---|---|---|
3 |
59 |
46 |
59 |
{} |
{总计: 3, 已提交: 3, 失败: 0, 错误: {}} |
{总计: 59, 已提交: 59, 失败: 0, 错误: {}} |
CALL apoc.periodic.iterate(
"MATCH (a:Article) WHERE not(exists(a.processed)) RETURN a",
"CALL apoc.nlp.azure.entities.stream([item in $_batch | item.a], {
key: $azureApiKey,
url: $azureApiUrl,
nodeProperty: 'body'
})
YIELD node AS a, value
SET a.processed = true
WITH a, value
UNWIND value.entities AS entity
WITH a, entity
WHERE entity.type IN ['Person', 'Organization', 'Location', 'Event']
CALL apoc.merge.node(
['Entity', apoc.text.capitalize(toLower(entity.type))],
{name: entity.name}, {}, {}
)
YIELD node AS e
MERGE (a)-[entityRel:AZURE_ENTITY]->(e)
SET entityRel.score = entity.score
RETURN count(*)",
{ batchMode: "BATCH_SINGLE",
batchSize: 25,
params: {azureApiUrl: $azureApiUrl, azureApiKey: $azureApiKey}}
)
YIELD batches, total, timeTaken, committedOperations, errorMessages, batch, operations
RETURN batches, total, timeTaken, committedOperations, errorMessages, batch, operations;
批次 | 总计 | 耗时 | 已提交操作 | 错误消息 | 批次 | 操作 |
---|---|---|---|---|---|---|
3 |
59 |
3 |
59 |
{} |
{总计: 3, 已提交: 3, 失败: 0, 错误: {}} |
{总计: 59, 已提交: 59, 失败: 0, 错误: {}} |
查询实体图谱
现在我们已经提取了实体,接下来是查询实体图谱。让我们从返回每篇文章的实体开始,如下面的查询所示
MATCH (a:Article)
RETURN a.title AS title, [(a)-[:AWS_ENTITY]->(entity) | entity.name] AS entities
LIMIT 5;
标题 | 实体 |
---|---|
"曼联球员埃丁森·卡瓦尼为“种族主义”Instagram 帖子道歉" |
["卡瓦尼", "足总", "埃丁森·卡瓦尼", "南安普顿", "曼联", "足球协会"] |
"脑震荡换人制的必要性 – 足球周刊" |
["费耶·卡拉瑟斯", "Soundcloud", "Mixcloud", "阿森纳", "巴里·格伦登宁", "Facebook", "伊万·默里", "劳尔·希门尼斯", "播客", "狼队", "大卫·路易斯", "Twitter", "Acast", "Stitcher", "南安普顿", "Audioboom", "曼联", "马克斯·拉什登", "拉斯·西弗森"] |
"列侬成为凯尔特人未能巩固国内统治地位的替罪羊" |
["流浪者队", "莱斯特城", "布伦丹·罗杰斯", "郡队", "凯尔特人", "托尼·莫布雷", "罗斯", "戈登·斯特拉坎", "尼尔·列侬", "马丁·奥尼尔", "Easy Street", "联赛杯", "罗尼·戴拉"] |
"那不勒斯通过模仿马拉多纳对阵罗马时的魔力,找到了最真实的致敬方式 | 尼基·班迪尼" |
["圣保罗球场", "洛伦佐·因西涅", "欧洲", "那不勒斯", "罗马", "利昂内尔·梅西", "那不勒斯", "迭戈·马拉多纳", "Curva"] |
"英超联赛明年年初可能开始脑震荡换人试验" |
["大卫·路易斯 试验", "大卫·路易斯", "劳尔·希门尼斯", "墨西哥", "卫报", "阿森纳", "狼队", "英超联赛", "达妮埃拉"] |
MATCH (a:Article)
RETURN a.title AS title, [(a)-[:GCP_ENTITY]->(entity) | entity.name] AS entities
LIMIT 5;
标题 | 实体 |
---|---|
"曼联球员埃丁森·卡瓦尼为“种族主义”Instagram 帖子道歉" |
["南安普顿", "问候", "俱乐部", "前锋", "事件", "曼联", "胜利", "身体", "乌拉圭人", "足总", "埃丁森·卡瓦尼", "朋友", "足球协会"] |
"脑震荡换人制的必要性 – 足球周刊" |
["狼队", "Acast", "巴里·格伦登宁", "Apple", "伊万·默里", "胜利", "阿森纳", "Soundcloud", "费耶·卡拉瑟斯", "马克斯·拉什登", "劳尔·希门尼斯", "Audioboom", "拉斯·西弗森", "大卫·路易斯", "曼联"] |
"列侬成为凯尔特人未能巩固国内统治地位的替罪羊" |
["布伦丹·罗杰斯", "俱乐部", "赛季", "酒吧", "Easy Street", "流浪者队", "莱斯特", "戈登·斯特拉坎", "罗尼·戴拉", "竞赛", "联赛杯", "失败", "城市经理", "球迷", "马丁·奥尼尔", "凯尔特人", "托尼·莫布雷", "池塘", "尼尔·列侬", "罗斯郡"] |
"那不勒斯通过模仿马拉多纳对阵罗马时的魔力,找到了最真实的致敬方式 | 尼基·班迪尼" |
["球迷", "洛伦佐·因西涅", "圣保罗球场", "那不勒斯", "那不勒斯", "家人", "迭戈·马拉多纳", "罗马", "胜利", "死亡", "利昂内尔·梅西", "欧洲", "球员"] |
"英超联赛明年年初可能开始脑震荡换人试验" |
["俱乐部", "冲突", "达妮埃拉", "劳尔·希门尼斯", "手术", "英超联赛", "大卫·路易斯", "球队", "阿森纳", "恢复", "卫报", "前锋", "墨西哥", "狼队", "规则变更", "大卫·路易斯 试验"] |
MATCH (a:Article)
RETURN a.title AS title, [(a)-[:AZURE_ENTITY]->(entity) | entity.name] AS entities
LIMIT 5;
标题 | 实体 |
---|---|
"曼联球员埃丁森·卡瓦尼为“种族主义”Instagram 帖子道歉" |
["前锋", "埃丁森·卡瓦尼", "曼联足球俱乐部", "乌拉圭国家足球队", "英格兰足球协会", "南安普顿足球俱乐部"] |
"脑震荡换人制的必要性 – 足球周刊" |
["阿森纳足球俱乐部", "AudioBoom", "Stitcher Radio", "马克斯·拉什登", "拉斯·西弗森", "曼联足球俱乐部", "SoundCloud", "劳尔·希门尼斯", "Acast", "Facebook", "Mixcloud", "巴里·格伦登宁", "南安普顿率", "伊万·默里", "Twitter", "南安普顿足球俱乐部", "大卫·路易斯", "Apple Podcasts", "费耶·卡拉瑟斯"] |
"列侬成为凯尔特人未能巩固国内统治地位的替罪羊" |
["马丁·奥尼尔", "莱斯特城足球俱乐部", "托尼·莫布雷", "罗尼·戴拉", "流浪者队足球俱乐部", "戈登·斯特拉坎", "联赛杯", "尼尔·列侬", "罗斯郡足球俱乐部", "英格兰足球联盟杯", "凯尔特人足球俱乐部", "布伦丹·罗杰斯"] |
"那不勒斯通过模仿马拉多纳对阵罗马时的魔力,找到了最真实的致敬方式 | 尼基·班迪尼" |
["那不勒斯", "洛伦佐·因西涅", "迭戈·马拉多纳", "罗马", "欧洲", "利昂内尔·梅西", "利昂内尔·梅西", "圣保罗球场", "那不勒斯足球俱乐部", "圣保罗球场"] |
"英超联赛明年年初可能开始脑震荡换人试验" |
["英超联赛", "墨西哥", "达妮埃拉", "卫报", "劳尔·希门尼斯", "伍尔弗汉普顿流浪足球俱乐部", "大卫·路易斯", "阿森纳足球俱乐部", "卫报"] |
我们还可以利用文章对之间的共有实体来确定文章相似度。如果我们想找到与加里·莱因克尔关于马拉多纳视频相似的文章,可以编写以下查询
MATCH (a1:Article {title: "Gary Lineker: Maradona was 'like a messiah' in Argentina – video"})
MATCH (a1:Article)-[:AWS_ENTITY]-(entity)<-[:AWS_ENTITY]-(a2:Article)
RETURN a2.title AS otherArticle, collect(entity.name) AS entities
ORDER BY size(entities) DESC
LIMIT 5;
其他文章 | 实体 |
---|---|
"缅怀迭戈·马拉多纳:足球传奇在 60 岁时去世 – 视频讣告" |
["世界杯", "迭戈·马拉多纳", "布宜诺斯艾利斯", "那不勒斯", "马拉多纳", "阿根廷", "巴塞罗那"] |
"经典 YouTube | 迭戈·阿曼多·马拉多纳,冷静的踢球和足球经理小球员" |
["英格兰", "世界杯", "迭戈·马拉多纳", "阿根廷", "马拉多纳", "莱因克尔", "加里·莱因克尔"] |
"阿根廷和那不勒斯的球迷哀悼迭戈·马拉多纳去世 – 视频" |
["世界杯", "迭戈·马拉多纳", "布宜诺斯艾利斯", "那不勒斯", "阿根廷", "马拉多纳"] |
"天才的负担:马拉多纳提醒我们年轻时达到巅峰会带来问题 | 维克·马克斯" |
["世界杯", "迭戈·马拉多纳", "墨西哥", "马拉多纳", "阿根廷"] |
"致敬迭戈·马拉多纳 – 足球周刊" |
["世界杯", "迭戈·马拉多纳", "布宜诺斯艾利斯", "墨西哥", "Twitter"] |
MATCH (a1:Article {title: "Gary Lineker: Maradona was 'like a messiah' in Argentina – video"})
MATCH (a1:Article)-[:GCP_ENTITY]-(entity)<-[:GCP_ENTITY]-(a2:Article)
RETURN a2.title AS otherArticle, collect(entity.name) AS entities
ORDER BY size(entities) DESC
LIMIT 5;
其他文章 | 实体 |
---|---|
"缅怀迭戈·马拉多纳:足球传奇在 60 岁时去世 – 视频讣告" |
["巴塞罗那", "阿根廷", "那不勒斯", "布宜诺斯艾利斯"] |
"经典 YouTube | 迭戈·阿曼多·马拉多纳,冷静的踢球和足球经理小球员" |
["阿根廷", "英格兰", "加里·莱因克尔"] |
"迭戈·马拉多纳的私人医生否认对死亡负责" |
["家", "足球运动员", "布宜诺斯艾利斯"] |
"永葆青春的兹拉坦·伊布拉希莫维奇继续为米兰打理业务 | 尼基·班迪尼" |
["家", "那不勒斯"] |
"'他带我们所有人都上了天堂':足球球迷对迭戈·马拉多纳去世的反应" |
["阿根廷", "布宜诺斯艾利斯"] |
MATCH (a1:Article {title: "Gary Lineker: Maradona was 'like a messiah' in Argentina – video"})
MATCH (a1:Article)-[:AZURE_ENTITY]-(entity)<-[:AZURE_ENTITY]-(a2:Article)
RETURN a2.title AS otherArticle, collect(entity.name) AS entities
ORDER BY size(entities) DESC
LIMIT 5;
其他文章 | 实体 |
---|---|
"缅怀迭戈·马拉多纳:足球传奇在 60 岁时去世 – 视频讣告" |
["巴塞罗那足球俱乐部", "马拉多纳", "那不勒斯足球俱乐部", "那不勒斯", "阿根廷", "布宜诺斯艾利斯", "迭戈·马拉多纳"] |
"经典 YouTube | 迭戈·阿曼多·马拉多纳,冷静的踢球和足球经理小球员" |
["阿根廷国家足球队", "英格兰", "英格兰国家足球队", "迭戈·马拉多纳"] |
"致敬迭戈·马拉多纳 – 足球周刊" |
["谢菲尔德星期三足球俱乐部", "马拉多纳", "墨西哥", "Twitter", "布宜诺斯艾利斯", "迭戈·马拉多纳"] |
"阿根廷和那不勒斯的球迷哀悼迭戈·马拉多纳去世 – 视频" |
["马拉多纳", "那不勒斯足球俱乐部", "阿根廷", "布宜诺斯艾利斯", "迭戈·马拉多纳"] |
"马拉多纳的反抗精神最激励我 | 凯南·马利克" |
["阿根廷国家足球队", "英格兰", "英格兰国家足球队", "迭戈·马拉多纳"] |
这些文章共有的一些实体实际上没有意义(例如“家”或“Twitter”),但总的来说,这些文章是加里·莱因克尔文章“推荐文章”部分的不错候选。
如果我们想进一步过滤提取的实体,可以尝试仅包含那些在维基百科中有条目的实体。有关该方法的更多详细信息,请参阅教程:使用 NLP 和本体构建知识图谱。