概述
1、Neo4j是一个高性能的,NOSQL图形数据库,它将结构化数据存储在网络上而不是表中。它是一个嵌入式的、基于磁盘的、具备完全的事务特性的Java持久化引擎,但是它将结构化数据存储在网络(从数学角度叫做图)上而不是表中。
2、Neo4j也可以被看作是一个高性能的图引擎,该引擎具有成熟数据库的所有特性。程序员工作在一个面向对象的、灵活的网络结构下而不是严格、静态的表中——但是他们可以享受到具备完全的事务特性、企业级的数据库的所有好处。(摘自百度百科)
3、Neo4j图数据库遵循属性图模型来存储和管理其数据。
属性图模型规则
- 表示节点,关系和属性中的数据
- 节点和关系都包含属性
- 关系连接节点
- 属性是键值对
- 节点用圆圈表示,关系用方向键表示。
- 关系具有方向:单向和双向。
- 每个关系包含“开始节点”或“从节点”和“到节点”或“结束节点”
- 在属性图数据模型中,关系应该是定向的。如果我们尝试创建没有方向的关系,那么它将抛出一个错误消息。
4、在Neo4j中,关系也应该是有方向性的。如果我们尝试创建没有方向的关系,那么Neo4j会抛出一个错误消息,“关系应该是方向性的”。
5、Neo4j图数据库将其所有数据存储在节点和关系中。我们不需要任何额外的RRBMS数据库或无SQL数据库来存储Neo4j数据库数据。它以图形的形式存储其数据的本机格式。
6、图形数据库数据模型的主要构建块是:节点、关系、属性
7、neo4j主要存储节点和关系,其中关系必须为有向关系,描述节点和关系的数据以属性的形式存储,节点和关系上都能放键值对的属性。不同类型的节点和关系通过标签Label来区别,不同标签的节点代表不同类型节点,不同标签关系代表不同类型关系,示例:
创建一个标签为Person的节点,其有属性 name 和age:create (:Person{name:'小红',age:21});
查询一个节点:match (m:Person{name:'小红',age:21}) return n;
删除一个节点:match (m:Person{name:'小红',age:21}) delete n;
创建关系:create (a:Person{name:"a"}),(b:Person{name:"b"}) with a,b create (a)-[r:Friend]->(b);
查询关系:match (a:Person{name:"a"})-[r:Friend]->(b:Person{name:"b"}) return r;
删除关系:match p=(a:Person{name:"a"})-[r:Friend]->(b:Person{name:"b"}) delete p;
其中create还有个近似的操作merge,也可以创建数据,其中merge可以看做match和create的合体。merge会先去原始库match属性或标签,如果不存在会创建,可以结合on create 和on match使用,如:
- 在创建的时候使用on create(在创建时进行一些操作)---如果存在name为Keanu Reeves的Person节点,则新增或修改属性created 属性,否则新增节点
MERGE (keanu:Person { name: 'Keanu Reeves' }) ON CREATE SET keanu.created = timestamp() RETURN keanu.name, keanu.created
- 在创建的时候使用 on match---如果有Person标签的节点,则更新found 属性,否则不做操作
MERGE (person:Person) ON MATCH SET person.found = TRUE RETURN person.name, person.found
- 同时使用on create 和 on match---如果存在name为Keanu Reeves的Person节点,则执行on match操作,否则执行on create操作
MERGE (keanu:Person { name: 'Keanu Reeves' }) ON CREATE SET keanu.created = timestamp() ON MATCH SET keanu.lastSeen = timestamp() RETURN keanu.name, keanu.created, keanu.lastSeen
插入数据
目前主要有以下几种数据插入方式:
- Cypher create 语句,为每一条数据写一个create
- Cypher load csv 语句,将数据转成CSV格式,通过LOAD CSV读取数据。
- 官方提供的neo4j-import工具,未来将被neo4j-admin import代替
- 官方提供的Java API - BatchInserter
- 大牛编写的 batch-import 工具
- neo4j-apoc load.csv + apoc.load.relationship
just try | create语句 | load csv语句 | neo4j-import | BatchInserter | batch-import | apoc |
---|---|---|---|---|---|---|
适用场景 | 1 ~ 1w | 0 ~ 1000w | 千万以上 | 千万以上 | 千万以上 | 1 ~ 数千万 |
速度 | 很慢 1000/s | 一般 5000/s | 非常快 x w/s | 很快 x w/s | 很快x w/s | 1w /s |
实际测试 | 无 | 9.5k/s(节点+关系) 用到了merge,数据量越大,速度越慢 |
12w/s(节点+关系) | 1w/s(节点+关系) | 1w/s(节点+关系) | 4k/s(1亿数据上增量更新) 1w/s(百万数据上更新) 用到了merge,数据量越大,速度越慢 |
优点 | 1.使用方便 2.可实时插入 |
1.官方ETL工具 2.可以加载本地/远程CSV 3.可实时插入 |
1.官方工具 2.占用资源少 |
1.官方API | 1.可以增量更新 2.基于BatchInserter |
1.官方ETL工具 2.可以增量更新 3.支持在线导入 4.支持动态传Label RelationShip |
缺点 | 1.速度慢 2.处理数据,拼CQL复杂,很少使用 |
1.导入速度较慢 2.只能导入节点 3.不能动态传Label RelationShip |
1.需要脱机导入 停止Neo4j数据库 2.只能用于初始化导入 |
1.只能在JAVA中使用 2.需要脱机导入 停止Neo4j数据库 |
1.需要脱机导入 停止Neo4j数据库 | 1.速度一般 |
比对:
- neo4j-import导入速度快,但是要求是空库,导入时要停止neo4j,也就是脱机导入,而且你要提前处理好数据,数据最好不要有重复,如果有重复,可以导入时跳过,然后根据bad.log来查看或者修正这部分数据
- batch-import可以增量导入,但是要求导入时停止neo4j数据库(脱机导入),而且增量更新的数据不会和库里存在的数据对比,所以要求数据全是新的,否则会出现重复数据
- load csv比较通用,而且可以在neo4j数据库运行时导入,但是导入速度相对较慢,要提前整理好数据,而且不能动态创建 Label RelationShip
- apoc挺好用的,可以动态创建RelationShip,但是不能动态创建Label (动态创建Label只能在程序里通过拼接字符串的方法实现)
实际情况中,处理数据比导入数据更花费时间
(这里介绍我使用过的create、neo4j-import、load csv方法)
1、neo4j-import批量导入,如上所述,导入很快,需要停止neo4j服务才能进行导入,否则会导入失败(适合用于初始化,否则会影响正在使用的服务);需要导入的库不存在,如果存在需要删除对应的.db文件再执行,否则导入会失败;需要事先把要导入的数据按特定格式整理好放入csv中(这里可以参照ba-es项目的service.KnCallsBulkPut类),这里以导入通话记录关系为例
节点csv(person.csv)格式:(其中节点标签为Person,属性有personId、name,且personId的值为节点ID)
personId:ID(Person),name
13661909859,郑敏
angeloo68@163.com,李晓芊
15107253239,胡强
关系csv(rels.csv)格式:(其中:START_ID为关系起始点的id,对应上面节点的ID,:END_ID为关系结束点的id,对应上面节点的ID,in,out分别别关系的属性)
:START_ID(Person),in,out,:END_ID(Person)
13545342144,0,300,15207123026
18016315333,0,50000,1529703684@qq.com
13661909859,0,2000.0,855-0979212256
13661909859,0,22210.0,15880082973
执行初始化批量录入命令:
.../neo4j-community-3.3.5/bin/neo4j-import —multiline-fields=true —bad-tolerance=0 —into .../neo4j-community-3.3.5/data/databases/caifen.db —nodes:Pesron .../person.csv —relationships:CALLWITH .../rels.csv (其中bad-tolerance指定你能容忍的错误数据量,如果对数据精度要求不高可以放大,这样在导入过程中,即使有小于设置值数量的数据错误,导入不报错,正确的数据能导入成功。.../neo4j-community-3.3.5/data/databases目录下如果已经有caifen.db,执行命令会报错,需要手动删除。--nodes:、--relationships:可以拼接多个,后面跟的分别是节点标签和关系标签,再后面跟着的是对应生成对应节点和关系的csv文件)
2、load csv使用(当前公司用于导入虫洞数据更新到已有库里)
使用方式:
neo4j自带客户端浏览器访问:http://ip:7474,如 http://192.168.0.246:7474
进到neo4j的安装目录的bin目录下(需要先进到neo4j-community-3.3.5/conf/neo4j.conf放开dbms.shell.port的注释,再重启),执行neo4j-shell
把整理好的文档放入对应目录,然后通过上面两种方式进入命令行执行:
[USING PERIODIC COMMIT 500] LOAD CSV [ WITH HEADERS] FROM “file:///rels20190717.csv”[W] AS line with line merge (n:Suspect{personId:line[0]}) on match set n.name=case when line[1] <> line[0] then line[1] else n.name end on create set n.personId=line[0],n.name=line[1] with line,n merge(m:Suspect{personId:line[2]}) on match set m.name=case when line[2] <> line[3] then line[3] else m.name end on create set m.personId=line[2],m.name=line[3] with n,m,line merge (n)-[r:CAPITALFLOWING]->(m)
说明:上面命令中[]里的命令为可选,[USING PERIODIC COMMIT 500] 表示 每500 行进行一次事务提交, [ WITH HEADERS] 导入时是否带csv中的表头,其中带表头用的是 line.name ,反之用的是 line[0]。导入命令中还可以用 toInt(‘1’) toFloat(‘1.0’)toInteger(), toFloat(), split()对数据进行处理;其中fill:后面跟的是文档路径,可以使绝对路径也可以是相对路径,neo4j默认是从neo4j-community-3.3.5/import中读取文件的,上面示例的写法就是将csv放在import文件夹下的写法;windows下相对路径方式如:file:/Test.csv,windows下绝对路径方式如:file:///C:/User/wdb/2017-04-06_test.csv,linux下相对路径格式:file:/2017-04-06_test.csv,linux下绝对路径格式:file:/home/wkq/databases/data/2017-04-06_test.csv;
3、插入一条数据,前面已经介绍过了,上面两种方式进入到命令行,执行create或merge进行数据插入,这里介绍unwind list+create(一条一条数据录入)在java API中的使用(扫黑数据的录入程序中)
if (phSet.size() > 0) { for (String eachph : phSet) { if (!MiscUtils.isNullOrEmpty(sfz)) { String temp = "{sfz:"" + sfz + "",name:"" + name + "",sex:"" + sex + "",ph:"" + eachph + "",rksj:"" + rksj + ""}"; sfzPhoneSet.add(temp); } } } ... StringBuilder sb = new StringBuilder(); // 存在拥有手机关系 if (sfzPhoneSet.size() > 0) { sb.append("UNWIND " + sfzPhoneSet + " as row with row "); sb.append( " merge (n1:Mobile{phone:row.ph}) on create set n1.phone=row.ph,n1.iskey=toString(0),n1.personId=row.ph,n1.rksj=row.rksj with n1,row "); sb.append( " merge (n2:Person{sfz:row.sfz}) on match set n2.name=case when row.name<> '' then row.name else n2.name end,n2.sex=case when row.sex<> '' then row.sex else n2.sex end,n2.rksj=case when row.rksj<> '' then row.rksj else n2.rksj end "); sb.append(" on create set n2.sfz=row.sfz,n2.name=row.name,n2.sex=row.sex,n2.rksj=row.rksj "); sb.append(" with n1,n2,row merge (n2)-[r:OWNPHONE]->(n1) "); neo4jJDBCHelper.executeQuery(sb.toString().replace("\\'", "\'"), new QueryCallBack() { @Override public void process(ResultSet rs) throws Exception { } }); sb = null; }