新闻媒体转引转载图谱
借助neo4j实现相似新闻媒体聚类。
一、实现思路
使用simhash算法为每个新闻节点生成文本指纹,在neo4j图谱中进行聚类。然后根据新闻的转引转载关系,生成站点之间的转引转载关系和转引转载热度趋势图。
二、程序结构
程序整体结构:
Server监控event_info表,负责eid的分发。(避免同一个eid分配到多个client)
Client通过事件eid从event_news_ref表获取数据。(事件数据上次处理状态存在mysql,主要字段包括eid/auto_id/update_time)
1、服务端SERVER:casia.isi.reprint.news.Server
2、客户端CLIENT:casia.isi.reprint.news.Client
3、添加索引INDEX:casia.isi.reprint.index.Index
三、聚类算法
一、自定义聚类:
初始化一个聚类中心列表对象
repeat
1、聚类中心列表为空,则:将当前新闻作为一个聚类中心。
2、聚类中心列表不为空,则:计算当前新闻与哪个聚簇中心(最早的新闻作为聚簇中心)最相似,相似则放入同一个簇;不相似则新建一个聚簇中心;重新计算当前簇的中心。
将每个点指派到最相似的质心,形成K个簇。
until 簇不发生变化-遍历过所有数据
时间复杂度:O(n*m) best:O(n*1) bad:O(n*n)
备注:SimHash值计算Hamming distance<?作为相似性指标
四、借助NEO4J实现聚类
// 事件下聚簇ID使用聚簇中心的MD5值:casia.isi.reprint.news
repeat
获取当前eid的所有聚簇中心新闻节点列表,通过`相关新闻`关系的cluster_master=1获取
1.聚簇中心列表不为空,计算当前新闻与哪个聚簇中心最相似;
1.1.有相似:
1.1.1.当前新闻节点分配聚簇ID,将当前新闻分配到与之相似的聚簇,cluster_id=当前聚簇中心的md5;
1.1.2.更新聚簇中心,如果当前新闻的发布时间比当前聚簇中心的发布时间早则cluster_master=1,否则cluster_master=0;
1.1.3.更新聚簇网络,当前新闻是否有repub?
否->更新is_repub=2,删除当前簇中与当前新闻相连的关系,并与当前聚簇中最早的新闻建立转载关系;
没有比它更早的新闻则不执行构建关系,只更新节点属性is_repub=2。
是->site_name=repub?
是->有没有当前site_name站点下比当前新闻发布时间更早的新闻,有则与之建立转载关系且节点属性is_repub=1,
并删除当前簇中与当前新闻相连的之前的关系;没有则is_repub=0,只更新节点的属性。
否->当前聚簇中找到repub下发布时间最早的新闻,与之建立转载关系,并删除当前簇中与当前新闻相连的之前的关系;
没有找到则is_repub=1,只更新节点的属性。
1.2.无相似:
1.2.1.则新增一个聚簇中心并设置`相关新闻`关系的属性cluster_master=1,cluster_id=当前新闻md5。
2.聚簇中心列表为空,则新增一个聚簇中心并设置`相关新闻`关系的属性cluster_id=md5,cluster_master=1。
until 簇不发生变化-遍历过所有数据
时间复杂度:O(n*m) best:O(n*1) bad:O(n*n)
备注:SimHash值计算Hamming distance<3作为相似性指标
一、casia.isi.reprint.news:负责从MYSQL获取数据并构建相关新闻子图
二、casia.isi.reprint.cluster:负责新闻节点得分簇
三、casia.isi.reprint.renovate:负责对事件下分好聚簇的新闻节点进行转载关系构建(按聚簇进行处理)
四、casia.isi.reprint.site:负责对事件下站点之间的关系进行更新
五、图模型
1、节点
label:新闻
properties:
_unique_uuid(实体唯一url+channel_url-MD5)
_entity_name(实体名称-从title或者content截取前20个字符)
update_time_mills(更新时间毫秒)
pubtime_mills(发布时间毫秒)
// remove->is_repub(是否原创标记-true(0)或者false(1)或者未知(2))
sim_hash(文本的title+content的simhash值)
site_name(站点)
repub(转载来源站点)
domain(域名)
label:事件
properties:
_unique_uuid(实体唯一eid-md5)
_entity_name(实体名称-事件名称)
update_time_mills(更新时间毫秒)
start_time_mills(事件开始时间毫秒)
end_time_mills(事件结束时间毫秒)
eid(事件ID)
label:SITE_REPTINR_EID
properties:
_unique_uuid(实体唯一站点名称-md5)
_entity_name(实体名称-站点名称)
update_time_mills(更新时间毫秒)
domain(域名)
2、关系
relation:转载(关系指向转发的新闻 或 站点之间关系指向无具体定义通过被转载的新闻查看具体转载关系)
properties:
_unique_uuid(实体唯一:开始结束节点唯一值+关系类型生成MD5)
_entity_name(实体名称:转载)
update_time_mills(更新时间毫秒)
<直接使用查询实时查询统计>
站点之间的转载关系:转载站点相互关系(转载与被转载)/站点之间新闻转载统计量
(1)转载站点相互关系(转载与被转载)-
(2)站点之间新闻转载统计量(开始表示被转载站点)
statistics数组长度等于2表示相互转载,数组长度等于1表示单向转载
start被转载站点 end发布站点
statistics:[{start:A,end:B,reprintCount:12},{start:B,end:A,reprintCount:23}]
statistics:开始节点ID TO 结束节点ID 统计量 && 开始节点ID TO 结束节点ID 统计量
```
// 事件下两个站点A/B之间的转载量 - 无向
MATCH (event:事件) WHERE event.eid=2 WITH event
MATCH p=(event)<-[:相关新闻]-(m:新闻)-[r:转载]-(f:新闻)-[:相关新闻]->(event)
WHERE m.site_name='A' AND f.site_name='B' RETURN count(p)
// 事件下两个站点A/B之间的A站点转载B站点的转载量 - 单向
MATCH (event:事件) WHERE event.eid=2 WITH event
MATCH p=(event)<-[:相关新闻]-(m:新闻)<-[r:转载]-(f:新闻)-[:相关新闻]->(event)
WHERE m.site_name='A' AND f.site_name='B' RETURN count(p)
// 事件下两个站点A/B之间的B站点转载A站点的转载量 - 单向
MATCH (event:事件) WHERE event.eid=2 WITH event
MATCH p=(event)<-[:相关新闻]-(m:新闻)<-[r:转载]-(f:新闻)-[:相关新闻]->(event)
WHERE m.site_name='A' AND f.site_name='B' RETURN count(p)
```
relation:相关新闻(关系指向事件)
properties:
_unique_uuid(实体唯一:开始结束节点唯一值+关系类型生成MD5)
_entity_name(实体名称:转载)
update_time_mills(更新时间毫秒)
cluster_id(聚簇ID标记-簇心得唯一值,默认值-1未分到聚簇)
cluster_master(0表示不是聚簇中心,1表示是聚簇中心,-1表示事件下未被分簇的新闻)
cluster_renovate(0表示当前聚簇不需要更新,1表示当前聚簇需要更新)
// 删除站点名称错误的数据
match (n) where n._entity_name contains '://' with n match p=(n)-[r]-() delete n,r;
match (n:新闻) where n.site_name contains '://' with n match p=(n)-[r]-() delete n,r;
六、可视化转引转载效果图
七、图谱结构优化
新闻节点之间的转引转载关系是存在明确的方向性的,但是通过新闻转引转载图谱生成的站点之间的转引转载关系,在大量数据中不好明确方向,只能通过站点之间的转引转载关系回溯到当前站点之间新闻转引转载详情。为了优化性能,站点之间的双向关系可以被优化。优化关系的实现方式参考:
package casia.isi.reprint.bidirection;
import casia.isi.neo4j.common.CRUD;
import casia.isi.neo4j.compose.NeoComposer;
import casia.isi.neo4j.model.Label;
import casia.isi.reprint.common.DatabasesTool;
import casia.isi.reprint.common.GraphModel;
import casia.isi.reprint.common.SystemConstant;
import casia.isi.reprint.util.DatabasePool;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;
import org.neo4j.driver.v1.Config;
import java.io.File;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* @author YanchaoMa [email protected]
* @PACKAGE_NAME: casia.isi.reprint.bidirection
* @Description: TODO(删除站点的双向转载关系 - 只保留一条表示无向关系)
* @date 2019/10/24 14:14
*/
public class Remove {
private final Logger logger = Logger.getLogger(this.getClass());
private NeoComposer neoComposer;
private DatabasesTool databasesTool;
private void initializer() {
NeoComposer.HTTP_SERVICE_IS_OPEN = false;
this.neoComposer = new NeoComposer(SystemConstant.neo4jIpPort, SystemConstant.neo4jAuthAccount, SystemConstant.neo4jAuthPassword,
Config.builder().withMaxTransactionRetryTime(SystemConstant.withMaxTransactionRetryTime, TimeUnit.SECONDS).build());
this.neoComposer.setDEBUG(SystemConstant.DEBUG);
this.databasesTool = new DatabasesTool();
}
private void run() {
// 初始化
initializer();
while (true) {
// 获取EDIS
List<Map<String, Object>> eids = databasesTool.getSqlResult("event_info",
SystemConstant.monitor_event_info_sql, DatabasePool.getInstance(SystemConstant.db_event_data));
// 整理站点之间的转载关系-删除双向关系
for (Map<String, Object> event_info : eids) {
String label = GraphModel.Labels.SITES_REPRINT_ + String.valueOf(event_info.get("eid"));
bidirectionalRela(Label.label(label));
}
try {
this.logger.info("no new bidirection relationships, sleep " + SystemConstant.CLIENT_SLEEP_INTERVAL / 1_000 + "s...");
Thread.sleep(SystemConstant.CLIENT_SLEEP_INTERVAL);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* @param
* @return
* @Description: TODO(整理站点之间的转载关系 - 删除双向关系)
*/
private void bidirectionalRela(Label label) {
String cypher = "MATCH (n:" + label.name() + "),(m:" + label.name() + ") WITH n,m\n" +
"MATCH p=(n)-[r1]->(m)-[r2]->(n) RETURN id(r1) AS relation1,id(r2) AS relation2";
JSONObject result = neoComposer.execute(cypher, CRUD.RETRIEVE_PROPERTIES);
JSONArray properties = result.getJSONArray("retrieve_properties");
// 转为双向关系对象并去重
List<BidirectionalPath> bidirectionalPaths = properties.stream()
.map(v -> {
JSONObject bid = (JSONObject) v;
return new BidirectionalPath(bid.getLongValue("relation1"), bid.getLongValue("relation2"));
}).collect(Collectors.collectingAndThen(
Collectors.toCollection(() ->
new TreeSet<>(Comparator.comparing(o -> o.getRelation1() + o.getRelation2()))), ArrayList::new));
// 拿到双向关系的其中一条关系的ID
JSONArray endIds = bidirectionalPaths.stream()
.map(BidirectionalPath::getRelation2)
.collect(Collectors.toCollection(JSONArray::new));
String cypherIterate = "MATCH ()-[r]-() WHERE id(r) IN " + endIds.toJSONString() + " RETURN r";
String cypherAction = "WITH {r} AS r DELETE r";
neoComposer.executeIterate(cypherIterate, cypherAction, "batchSize", SystemConstant.CLIENT_NEO4J_COMMIT_SIZE, "parallel", false);
}
private static class BidirectionalPath {
private long relation1;
private long relation2;
public BidirectionalPath(long relation1, long relation2) {
this.relation1 = relation1;
this.relation2 = relation2;
}
public long getRelation1() {
return relation1;
}
public void setRelation1(long relation1) {
this.relation1 = relation1;
}
public long getRelation2() {
return relation2;
}
public void setRelation2(long relation2) {
this.relation2 = relation2;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BidirectionalPath that = (BidirectionalPath) o;
return (relation1 == that.relation1 &&
relation2 == that.relation2) || (relation1 == that.relation2 &&
relation2 == that.relation1);
}
@Override
public int hashCode() {
return Objects.hash(relation1, relation2);
}
@Override
public String toString() {
return "BidirectionalPath{" +
"relation1=" + relation1 +
", relation2=" + relation2 +
'}';
}
}
public static void main(String[] args) {
PropertyConfigurator.configureAndWatch("config" + File.separator + "log4j.properties");
new Remove().run();
}
}