在官方的最新文档(0.3.0)基础上做一个解读, 然后结合实际的调用例子和底层源码进行结合分析.力争提升当前的查询速度. 因为
外出采风, 后续的结合源码和具体的实例可能要等到下半月.
0x00.索引种类
Janus支持两种索引来加快查询,分别是图索引和以顶点为中心的索引。(特点?区别?什么是顶点为中心的索引?..)
在图中, 大多数的查询是从被属性标识的顶点或边开始的,普通
图索引可以让这些查询操作更有效率。以顶点为中心的索引可显著加快图遍历巨型顶点的情况(很多关联边).
1.图索引(Graph Index)
普通的图索引分为两种,这个在很早就说过,精准索引是通过Hbase/Cassandra的单独index表去映射(空间换时间),模糊索引是通过ES/Lucene去做倒排,支持复杂的逻辑. 本质上这二者都是基于Tinkerpop的Index数据结构设定而来,那么先用gremlin看看查询的例子:
1 | //通过属性的K-V去查询顶点或边,是最常见的搜索条件之一 |
如果没有索引,上面的查询需要遍历整张图的顶点/边,在海量数据时肯定是没法接受的. 所以常用属性字段一般都会加上至少精准索引.索引的创建需要两个基本参数:
- 全局唯一的索引名
- 索引位置(vertex/edge)
并且需要主要的是,最好是初始化图结构(Schema)的时候就定义好索引,如果是在写入数据后才加索引(特别是修改索引),那么会有一个漫长且易出错的索引重建过程(在重建全部完成前,是不会部分生效的,详细参考),再就是实际生产使用中,因为全图的检索几乎是不可接受的,所以Janus推荐开启force-index配置避免整图扫描.(注:默认值是:false,需要单独指定)
1.1 精准索引(Composite Index)
那么来看看精准索引的具体使用方式和查询语句:
1 | graph.tx().rollback() //禁止创建索引的时候事务开启 |
在建立了索引的基础上,类似这样的gremlin查询就会使用到K-V映射,速度极大提高:
1 | g.V().has('name', 'jin') //用到第一个索引 |
那进一步想想,如果一个顶点有很多的属性,查询的时候可能条件组合是很多的,难道我们都需要把两两/三三/甚至NN组合去构建联合索引么?那样索引的效率会大大降低吧. 这个使用考虑使用模糊索引? (待确认)
1.1.1 索引的唯一性
创建索引的时候有一个unique选择(true/false),是起什么用的呢?它约束这个属性(比如name)至多只能有一个顶点/边与其关联.举例看看?
1.2 模糊索引(Mixed Index)
模糊索引是可选的, 不管是ES还是Solr,底层都是基于Lucene的倒排索引结构和算法实现的,因为条件支持灵活许多,所以一般比精准索引要慢.看看使用例子:
1 | //前后都与精准索引是一样的,只有创建的时候稍有区别,注意"search"是一个前缀.并非随便取的字符串 |
有了模糊索引之后,大部分查询其实都可以匹配到了,详细信息见全文检索和参数设置 和搜索条件和类型. (注意如果同时有精准和模糊索引,优先匹配精准),看看查询例子:
1 | g.V().has('name', textContains('jin')).has('age', inside(20, 50)) |
并且因为模糊索引的特性,显然它是不支持索引唯一性这个设置的.
1.2.1 添加属性(PropertyKeys)
由于模糊索引的特性,你可以在模糊索引存在后再去另外新的属性. 语法如下:
1 | location = mgmt.makePropertyKey('location').dataType(Geoshape.class).make() |
先获取之前已有的模糊索引名,然后通过addIndexKey()添加新的属性.如果新的属性还没有数据写入,那么提交后会立刻生效,如果已有数据,那么还是需要索引重建(reindex过程 ),同之前.
1.2.2 映射
当往模糊索引添加属性名的时候,无论是用索引构建器indexBuilder 还是addIndexKey()方法,都可以选择指定的参数调整属性值是如何映射到索引存储端(ES/Lucene)的. 这里是详细索引映射参数参考 (这个地方还没有具体看清楚,待补充,感觉选择不太是人话.)
1.3 排序(order/sort?)
可以用order().by()语法对查询结果排序,方法需要两个参数:
- 排序的属性名
- 排序方式: 升序-
incror 降序-decr
用例:g.V().has('name', textContains('jin')).order().by('age', decr).limit(10) 返回年龄前十(老)名字中包含”jin”的元素
需要主要:
- 精准索引对应的存储端不支持处理结果排序,本质是在内存中排序, 所以数据大的时候资源消耗可能很高.
- 模糊索引就原生的支持存储端高效排序.但是,
order().by中的属性名必须已提前加到了模糊索引中,如果属性名不是模糊索引的一部分,那么同上,也会全加到内存排序. (比如上面例子中的age必须也添加了模糊索引)
1.4 约束Label
默认的构建索引的方式是在所有的顶点/边的属性上构建索引,但在一些场景下.业务只需要某个特定的顶点(比如学生)/边上的属性有索引,那么可以使用indexOnly(v/eLabel)方法.
1 | name = mgmt.getPropertyKey('name') |
这样理论上可以减少许多不必要的索引浪费,但特别需要注意的是,这样做了之后,后续如果需要新增另一个顶点也拥有这个索引,目前还没有API提供,可能也得需要重建索引 (代价很大). 所以提前慎重考虑清楚,是否只有这个顶点/边需要索引,否则可能弄巧成拙..
2.顶点为中心的索引(Vertex-centric Indexes)
顶点中心索引是每个顶点单独拥有的索引结构.用途是缓解遍历到超级顶点的问题.
超级顶点就是有非常多出边的点,比如google.com这个url就不比普通的网址,与其关联的边可能有几 百/上千万个,如果每次经过它都遍历一次这么多的出边,再去做查询,那时间肯定会大幅提高, 甚至引发OOM(比如order().by()的时候使用了超级点,那都加到内存中还要排序..).问题我想大家都很好理解,但是这个地方官方文档说的不清不楚,我试着撸顺它之后,大致这样理解:
顶点中心索引只遍历所需的边,而非全遍历出边
假设有顶点person和things,边used,属性time. 然后我想查某人所花时间在10~20之间所做的事情, 如果没有顶点中心索引的话,需要遍历所有used的边,因为你不遍历也不可能得到一个具体范围的值.
1 | h = g.V().has('name', 'jin').next() |
那么怎么构建顶点中心的索引呢—>buildEdgeIndex()就可以实现,如下:
1 | time = mgmt.getPropertyKey('time') |
方法中的索引名也需要全局唯一,方向指定IN/OUT/BOTH会直接影响索引生效的条件(当然BOTH会带来双份的存储占用,也就是说也需要考虑实际需求是否需要, 而不是无脑BOTH). 方法最后的参数其实就是对指定属性按升/降次序排序 ,可以传入多个属性值,此时排序优先级随着属性的出现次序依次递减.比如下面的例子:
1 | time = mgmt.getPropertyKey('time') |
完成后,上面的battlesByRatingAndTime 索引就能加速下面前两个,而不能加速第三个
1 | h = g.V().has('name', 'jin').next() |
在同一个边上可以构建多个顶点中心的索引,JanusGraph的查询适配器会自动匹配最适合的索引.需要注意的是,顶点中心索引只支持等值的查询和范围(1~10)查询. (这是最常用的查询条件了). 最后还是索引的基本原则,添加的时候如果这条边是没有数据的,那么索引会马上生效,否则也需要先等之前数据补全索引.
| Note(Todo): |
|---|
JanusGraph会自动给每条边和属性构建顶点中心索引,这意味着即使有大量出/入边, 类似这样结构的查询: g.V(h).out('student') 或 g.V(h).values('age')都会被索引加速. (这里有个大问题是,Janus自动给每条边和属性创建的索引是如何定义的(比如边上的属性排序是取的哪个?随机?复合?还是每个单独构建…),复合属性应该是需要用户自己定义的?) |
顶点中心索引只能加速有查询约束条件的遍历,无法为全遍历加速. 通常来说,无约束遍历/全遍历可以重新用带约束条件的遍历改写达到类似效果,但是如何改写,可能还需要深入理解gremlin的语法区别和组合了…
2.1 排序后的查询
下面的例子是遍历边的有序查询. 我们可以使用localLimit(local(xx.limit())语法来搜索每个经过顶点的边的子集 (并采用指定的次序).
1 | h = g..V().has('name', 'jin').next() |
上面两个查询都能在顶点中心索引下更高效的得到返回值, 条件是排序的属性名和索引的属性名一致,并且排序顺序(升/降序)是和索引的设置保持一致(注意默认是升序). 比如之前设置过的顶点中心索引–usedByTime就能加速上面的第一条查询, 而usedByRatingAndTime 复合索引可以加速第二条查询, 由于复合索引必须同时匹配到两个索引的属性才会生效, 所以usedByRatingAndTime 不能加速第一条查询.(缺失time)
| Note |
|---|
基于顶点条件排序后的查询是Janus扩展gremlin的用法, 因此为了满足gremlin的语法要求, 需要使用_()这样的奇怪语法流程,把Janus的排序结果丢回gremlin显示. (这里也不是人话,我已经尽力撸顺了一下,之后待验证) |
总结
大致可以发现JanusGraph中索引分为几种类型, 一种是按存储后端分类 : 主存储 和索引存储
还有一种是根据功能分类, 一种是通用索引(建立在边/顶点上的属性), 另外一种是基于顶点的边/属性索引(本质应该是建立在顶点上, 然后关联某个属性, 单个的属性是自动建立的) …?