Neo4j图数据库快速使用

发布时间 2023-06-03 12:04:03作者: strongmore

针对这个项目中用到的技术组件,只有filebeat和neo4j我们没有使用过
不过filebeat比较简单,类似于flume,在使用的时候主要是写配置文件,所以在后面用到的时候我们再具体分析。
下面我们来学习一下neo4j的使用,让大家快速了解它并掌握它的常见用法。

Neo4j介绍

Neo4j是一个高性能的图数据库,它和普通的关系型数据库是不一样的,它里面侧重于存储关系数据。
针对各种明星之间错综复杂的关系,如果使用mysql这种数据库存储,在查询所有人之间的关系的时候是非常复杂的,
但是使用Neo4j这种数据库,只需要一条命令就可以了。

它是一个嵌入式的、基于磁盘的、具备完全的事务特性的持久化引擎,它将结构化数据存储在网络(从数学角度叫做图)上而不是表中

目前neo4j有两种发行版,
一个是商业版:支持集群
另一个是社区版:只支持单机
目前我们平台用户量在三四千万的规模,单机的性能也是足够使用的。
等后期单机无法支撑之后再考虑使用商业版的。

Neo4j安装部署

Neo4j支持在windows和linux中进行安装
由于在实际工作中肯定是在linux中进行安装,所以在这我们就直接在linux中安装了。

下载地址,这里我们使用3.5.21社区版,可能会很慢。官网只能下载最新版本,没找到下载之前版本的地方。

修改配置 conf/neo4j.conf

dbms.connectors.default_listen_address=bigdata01
dbms.connectors.default_advertised_address=bigdata01

注意:这里的bigdata01是当前机器的主机名

启动和停止

bin/neo4j start
bin/neo4j stop

访问neo4j的web页面 http://bigdata01:7474/ 默认账号密码 neo4j/neo4j

image

注意:第一次访问的时候会提示修改密码,建议改为admin

Neo4j案例

Neo4j可以很方便的展示一些人物或者事物之间的错综复杂的关系

image

这张图里面展示了这些人物之间的关系,使用这种展示形式看起来是很清晰的,也方便理解
后期如果我们想查询某一个人的关系链 也是很方便的。

这里面有几个概念我们需要明确一下:
因为Neo4j是一个图数据库,可以认为它里面存储的都是图数据
图是由点(Vertex),边(Edge)和属性(Property)组成的

图里面的圆圈属于一个点、这个线属于边,圆圈中的这个姓名是属性。

点和边都可以设置属性,点也可以称作节点,边也可以称作关系,每个节点和关系都可以有一个或多个属性

在这里大家先对neo4j有一个整体的认识,下面我们开始学习neo4j中的具体使用

Neo4j的使用

那下面我们来看一下neo4j的常见操作

  1. 添加数据
  2. 查询数据
  3. 更新数据
  4. 建立索引
  5. 批量导入数据

添加数据

create:每次都创建新的点或边

创建两个节点

create (p1:Person {name:"zs"})
create (p2:Person {name:"ls"})

创建两个节点及它们之间的关系

create (p1:Person {name:"zs"}) -[:like]-> (p2:Person {name:"ls"})

注意:create会每次都创建新的节点或者关系

neo4j中除了有create命令,还要一个merge命令,这个命令在创建节点之前都会先查询一下,如果存在则不创建【这个merge命令就算是重复执行,也不会产生重复的结果】,所以在工作中建议使用merge

merge(p3:Person {name:"jack"})
merge(p4:Person {name:"tom"})
merge(p3) -[:like]->(p4)

注意:这个时候这三行命令需要在一个会话里面一起执行,否则merge里面无法识别p3和p4

在这里其实还有另外一种写法,如果节点已经存在了,我们只需要创建关系,还可以使用match来实现
所以在这里我们还需要引入一个match命令,使用match可以查询之前已有的节点或者关系等信息,这个命令还是很重要的,后面我们还会详细分析,在这里我们先使用match来查询已有的节点信息
在这里我们想让tom和jack也产生一个like关系
先查询tom和jack

match(a:Person {name:"tom"}),(b:Person {name:"jack"})
merge(a)-[:like]->(b)

这样就可以通过match查询之前已有的节点信息,然后再通过merge创建关系就行了,也不会额外产生重复的节点。

所以这两种方式都可以,按需选择即可。

查询数据

下面我们来查询一下neo4j中的数据
咱们前面说过match可以进行查询,下面我们就来具体使用一下
这个match其实有点类似于mysql中的select
在这注意一下:match不能单独存在,咱们前面在使用的时候match后面跟的也是有merge的
如果我们只想查询一些数据怎么办呢?

可以使用match+return,查看满足条件的数据并返回

match(p:Person {name:"tom"}) return p

那下面我们查询一些复杂一点的内容
首先初始化数据
注意:这里面这些创建点的操作和创建边的操作需要在一个会话里面一起执行

merge(a:User {name:"A"})
merge(b:User {name:"B"})
merge(c:User {name:"C"})
merge(x:User {name:"X"})
merge(y:User {name:"Y"})
merge(z:User {name:"Z"})
merge(a) -[:follow]-> (b)
merge(c) -[:follow]-> (b)
merge(a) -[:follow]-> (x)
merge(a) -[:follow]-> (y)
merge(c) -[:follow]-> (y)
merge(c) -[:follow]-> (z)

首先我们查询:
某个主播的粉丝信息

match (:User {name:"B"}) <-[:follow]- (n:User) return n

还有一种写法就是这样的

match (n:User) -[:follow]-> (:User {name:"B"}) return n

如果只想返回满足条件的粉丝的name的话,可以在n后面加上一个.name

match (n:User) -[:follow]-> (User {name:"B"}) return n.name

这个其实就是查询我要关注的主播的二度关系了

我–>主播B–>粉丝
这个时候 我 和 主播B的粉丝之间就属于二度关系了。

我们在这个项目中是想实现三度关系推荐
也就是当我要关注某个主播的时候,要给我推荐这个主播的粉丝又关注了哪些主播
所以说这个三度关系就是这样的

我–>主播B–>粉丝–>主播N
这个时候我和主播N之间就属于三度关系了。

那这个三度关系该如何查询呢?

match (a:User {name:"B"}) <-[:follow]- (b:User) -[:follow]-> (c:User)  return a.name as aname,b.name as bname,c.name as cname

结果是这样的,这里针对B的粉丝A和C关注了XYZ这三个主播,其中A和C都关注了Y

image

那在给我推荐的时候,是不是应该把B的粉丝关注比较多的主播推荐给我呢,在这里,理论上来说,Y最有可能是我喜欢的,Z和X的可能性就没那么大了

所以 在这里获取cname的时候,最好是做一下过滤,统计一下cname中相同主播出现的次数,然后按照倒序排序,最终取一个topn就可以了,这个topn里面的主播大概率是我也喜欢的主播

那这个该怎么实现呢?
其实match后面也支持count、order by 、limit等命令

match (a:User {name:"B"}) <-[:follow]- (b:User) -[:follow]-> (c:User)  return a.name as aname,c.name as cname,count(*) as sum order by sum desc limit 3

注意:这里相当于根据aname和cname进行分组,然后使用count统计每组的数据行数
这里的count(*) 和使用count(cname)是一样的效果。

image

这样就可以实现三度关系数据的查询了。
这里面其实还可以使用where加一些过滤条件。

注意,where需要放在return的前面。

match (a:User {name:"B"}) <-[:follow]- (b:User) -[:follow]-> (c:User) where c.name <> "X" return a.name as aname,c.name as cname,count(*) as sum order by sum desc limit 3

image

更新数据

更新数据这块其实总结一下有两种情况

  • 第一种是
    更新节点的属性, 使用match+set实现
  • 第二种是
    更新节点之间的关系(边),其实就是删除边,使用match+delete实现

首先看一下如何修改节点中的属性

match (a:User {name:"X"}) set a.age= 18

然后看一下如何删除关系

match (:User { name:"A"})-[r:follow]->(:User { name:"X"}) delete r

删除数据

match (n:User) delete n # 删除所有User节点,如果节点存在关系不能删除
match (n:User) detach delete n # 删除所有User节点,且删除存在的关系
match (n) detach delete n # 删除整个数据库

建立索引

neo4j中的索引可以细分为两种

  1. 普通索引 CREATE INDEX ON :User(name)
    第一种是普通索引,使用create index 可以实现,指定给节点中的某个属性建立索引,具体建立索引的依据是后期我们在查询的时候是否需要在where中根据这个属性进行过滤,如果需要则建立索引,如果不需要则不建立索引。
  2. 唯一约束 CREATE CONSTRAINT ON (u:User) ASSERT u.id IS UNIQUE
    第二种索引称之为唯一约束,类似于mysql数据库中主键的唯一约束。
    使用CREATE CONSTRAINT可以实现
    CREATE CONSTRAINT ON (u:User) ASSERT u.id IS UNIQUE

那这两种在使用的时候具体该如何选择呢?

如果某个字段的值是唯一的,并且后期也需要根据这个字段进行过滤操作,那么就可以建立唯一约束,唯一约束的查询性能比索引更快

批量导入数据

针对项目开始的时候有一批海量数据需要导入,就不能使用咱们前面讲的那些命令一条一条导入了,性能太差,我们需要有一个批量导入的方式来快速导入数据。
neo4j中批量导入数据有两种方式,一个是batch import 一个是load csv

  • 第一种是batch import,这种方式需要按照要求组装三个文件,导入性能比较快,但是比较麻烦
  • 第二种是load csv,只需要把数据组装到一个CSV文件即可,导入性能没有batch import快,但是使用起来方便

在这里我们考虑到易用性,由于我们的原始数据都在mysql中,可以通过mysql的命令直接把数据导出为一个文件,所以在这里我们使用load csv会更加方便。

注意:在load csv中使用了merge 或者match的时候,需要确认关键字段是否有索引,否则性能会很差。

例如:merge(a:User {name:“A”}),此时就需要提前对User中的name字段建立索引,否则在进行初始化的时候,数据量大了之后,初始化的性能会很差,因为merge在执行的时候会查询name等于A的数据在不在neo4j中,如果name字段没有建立索引,则会执行全表扫描。

那下面我们就先演示一下load csv的使用
先把neo4j中的数据清空了,然后重新启动一下,重新修改密码。

match (n) detach delete n

或者暴力删除

rm -rf data/

然后看一下我准备的测试数据文件,follower_demo.log

fuid	uid
1001	1000
1001	1004
1001	1005
1001	2001
1002	1000
1002	1004
1002	2001
1003	1000
1003	1004
1006	1000
1006	1005
2002	1004
2002	1005
2002	2004
2003	1000
2003	1005
2003	2004

文件中有2列,fuid和uid fuid表示是关注者的uid,uid表示是被关注者(也就是主播)的uid。

需要把这个文件上传到NEO4J_HOME的import目录下才可以使用。

然后我们使用neo4j的shell命令行执行下面命令。

bin/cypher-shell -a bolt://bigdata01:7687 -u neo4j -p admin

首先针对关键字段建立索引

CREATE CONSTRAINT ON (user:User) ASSERT user.uid IS UNIQUE;

然后批量导入数据

USING PERIODIC COMMIT 1000
       LOAD CSV WITH HEADERS FROM 'file:///follower_demo.log' AS line FIELDTERMINATOR '\t'
       MERGE (viewer:User { uid: toString(line.fuid)})
       MERGE (anchor:User { uid: toString(line.uid)})
       MERGE (viewer)-[:follow]->(anchor);

在neo4j的web界面上也可以执行,需要添加:auto命令。

解释:

  1. PERIODIC COMMIT 1000:每1000条提交一次,这个参数非常关键,如果在数据量很大的情况下内存。无法同时加载很多数据,所以需要批量提交事务,这样可以减小任务失败的风险,并且也可以提高数据导入的速度,当然这需要设置一个合适的数量。
  2. WITH HEADERS:是否使用列名,如果文件中有列名,则可以加这个参数,这样在读取数据的时候就会忽略第一行
  3. FIELDTERMINATOR ‘\t’:指定文件中的字段分隔符。

然后我们到页面上看一下导入的数据。

image

注意:此时我们会发现在页面中的圆圈中没有显示数据的具体内容,之前我在用3.2版本的时候是没有这个问题的,现在使用新的3.5版本之后会发现页面显示的时候会出现这种问题,这个问题倒没什么影响,就是在页面中看起来不太方便而已。

通过我的测试发现
如果我们在添加节点数据的时候,给节点指定一个name属性,那么name属性的值默认会显示在这个圆圈里面,如果不是name字段,则不显示。这个应该是新版本的一些特性。

可以尝试一下,将uid字段改为name字段

Java操作

<dependency>
    <groupId>org.neo4j.driver</groupId>
    <artifactId>neo4j-java-driver</artifactId>
    <version>4.1.1</version>
</dependency>
import org.neo4j.driver.AuthTokens;
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;
import org.neo4j.driver.Record;
import org.neo4j.driver.Result;
import org.neo4j.driver.Session;

import java.util.List;

public class TestNeo4jClient {
    public static void main(String[] args) {
        //获取neo4j的连接
        String boltUrl = "bolt://bigdata01:7687";
        String username = "neo4j";
        String password = "admin";
        Driver driver = GraphDatabase.driver(boltUrl, AuthTokens.basic(username, password));
        //开启一个会话
        Session session = driver.session();
        session.run("merge(p:Person {name:\"jack\"})");
        Result result = session.run("match(p:Person) return p.name");
        List<Record> recordList = result.list();
        for (Record record : recordList) {
            System.out.println(record.asMap());
        }
        //关闭会话
        session.close();
        //关闭连接
        driver.close();
    }
}

参考

Neo4j 第三篇:Cypher查询入门
Neo4j 入门教程 - 使用 Cypher 删除关系