JanusGraph支持两种不同类型的索引来为查询加速:Graph Index 和 Vertex-Centric Indexes。大多数的图查询都是从一组通过属性查找到的顶点或者边开始遍历。Graph Indexes让这些全局的检索在大图上也很有效。Vertex-Centric indexes可以加速实际图的遍历速度。特别是在遍历有很多关联边的顶点时。

Graph Index

Graph index是在构建在整个图上的索引,可以高效的通过对属性的查询获取顶点或者边。比如:


g.V().has('name', 'hercules')
g.E().has('reason', textContains('loves'))

第一个查询请求名称为hercules的所有顶点。第二个查询属性reason包含单词love的所有边。如果没有图索引来回答这些查询,则需要对图中的所有顶点或边进行全面扫描,以找到与给定条件匹配的顶点或边,这对于大型图来说是非常低效和不可行的。

JanusGraph区分了两种类型的图索引:组合索引和混合索引。组合索引非常快速和有效,但仅限于相等地查找特定的、提前定义的属性键组合。混合索引可用于查找索引键的任何组合,除了相等性比较外,支持多个条件谓词,但它依赖于索引存储的支持。

两种类型的索引都是通过JanusGraph的management系统的index builder来创建的

JanusGraphManagement.buildIndex(String,Class)

第一个参数定义了index的名字,第二个参数定义了要做索引元素的类型。(比如是Vertex.class)Graph index的名字必须唯一。根据新定义的property keys 比如将在同一个management transaction里定义的property key 作为indexes,那么这个index的定义立即生效。

这同样适用于图索引,这些图索引被约束在与索引相同的管理事务中创建的标签上。通过对已经定义好的属性来创建GraphIndex,需要执行reindex。只有reindex执行完成后,新创建的index才会生效。所以建议在创建graph schema的时候去创建graph index。

在没有索引的情况下,JanusGraph将默认为完整的图扫描,以便检索所需的顶点列表。虽然这会生成正确的结果集,但图扫描可能非常低效,并导致生产环境中的整体系统性能很差。在JanusGraph的生产部署中启用force-index配置选项,以禁止图形扫描。

Composite Index

组合索引通过一个或者固定个数key的组合来获取一组边或者顶点。可以看一下下边这个组合index的定义:


graph.tx().rollback() //Never create new indexes while a transaction is active
mgmt = graph.openManagement()
name = mgmt.getPropertyKey('name')
age = mgmt.getPropertyKey('age')
mgmt.buildIndex('byNameComposite', Vertex.class).addKey(name).buildCompositeIndex()
mgmt.buildIndex('byNameAndAgeComposite', Vertex.class).addKey(name).addKey(age).buildCompositeIndex()
mgmt.commit()
//Wait for the index to become available
ManagementSystem.awaitGraphIndexStatus(graph, 'byNameComposite').call()
ManagementSystem.awaitGraphIndexStatus(graph, 'byNameAndAgeComposite').call()
//Reindex the existing data
mgmt = graph.openManagement()
mgmt.updateIndex(mgmt.getGraphIndex("byNameComposite"), SchemaAction.REINDEX).get()
mgmt.updateIndex(mgmt.getGraphIndex("byNameAndAgeComposite"), SchemaAction.REINDEX).get()
mgmt.commit()

首先,name和age这两个属性已经定义了。接着,创建了一个简单的只用了name的组合索引,JanusGraph会用这个索引来回答下边这个查询:


g.V().has('name', 'hercules')

第二个图的组合索引用到了name和age两个key,JanusGraph会用这个index去回答下边的查询:


g.V().has('age', 30).has('name', 'hercules')

注意,要使用此索引,必须在查询的相等条件中找到复合图索引的所有的属性。例如,下面的查询不能用任意一个索引来回答,因为它只包含年龄约束,而不包含姓名。


g.V().has('age', 30)

还要注意,组合图索引只能用于类似于上面查询中的等式约束。下面的查询将仅使用在name键上定义的简单复合索引来回答,因为年龄约束不是等式约束。


g.V().has('name', 'hercules').has('age', inside(20, 50))

复合索引不需要配置外部索引后端,它通过主存储后端得到支持。因此,复合索引修改通过与图形修改相同的事务进行持久化,这意味着如果底层存储后端支持原子的and/or一致性,那么这些更改就是原子的and/or一致的。

一个复合索引可以只包含一个或多个键。只有一个键的复合索引有时称为键索引。

Index 的唯一性

组合索引可以用来对图里的属性进行唯一性约束。如果一个组合图索引被定义为unique(),那么这个组合索引用到的property的value组合只能最多有一个值。例如,为了确保名称在整个图中是惟一的,将定义以下组合图索引。


graph.tx().rollback()  //Never create new indexes while a transaction is active
mgmt = graph.openManagement()
name = mgmt.getPropertyKey('name')
mgmt.buildIndex('byNameUnique', Vertex.class).addKey(name).unique().buildCompositeIndex()
mgmt.commit()
//Wait for the index to become available
ManagementSystem.awaitGraphIndexStatus(graph, 'byNameUnique').call()
//Reindex the existing data
mgmt = graph.openManagement()
mgmt.updateIndex(mgmt.getGraphIndex("byNameUnique"), SchemaAction.REINDEX).get()
mgmt.commit()

混合索引

混合索引通过前面添加的属性键的任何组合检索顶点或边缘。混合索引比复合索引提供更大的灵活性,并且支持除相等性之外的其他条件谓词。另一方面,对于大多数等式查询,混合索引要比复合索引慢。
与组合索引不同,混合索引需要配置索引后端,并使用该索引后端执行查找操作。JanusGraph可以在一个安装中支持多个索引后端。每个索引后端必须在JanusGraph配置中通过名称唯一标识,该配置称为后端索引名。


graph.tx().rollback()  //Never create new indexes while a transaction is active
mgmt = graph.openManagement()
name = mgmt.getPropertyKey('name')
age = mgmt.getPropertyKey('age')
mgmt.buildIndex('nameAndAge', Vertex.class).addKey(name).addKey(age).buildMixedIndex("search")
mgmt.commit()
//Wait for the index to become available
ManagementSystem.awaitGraphIndexStatus(graph, 'nameAndAge').call()
//Reindex the existing data
mgmt = graph.openManagement()
mgmt.updateIndex(mgmt.getGraphIndex("nameAndAge"), SchemaAction.REINDEX).get()
mgmt.commit()

上面的示例定义了一个混合索引,其中包含属性:姓名和年龄。该定义引用索引后端名称 search,以便JanusGraph知道它应该为这个特定索引使用哪个配置的索引后端。buildMixedIndex调用中指定的search参数必须匹配JanusGraph配置定义中的第二个子句,比如:index.search.backend。如果索引名为solrsearch,那么配置定义应该为:index.solrsearch.backend。

上边演示的mgmt.buildIndex例子里使用了text search作为默认行为。一个通过显式指定text index的定义方法如下:


mgmt.buildIndex('nameAndAge',Vertex.class).addKey(name,Mapping.TEXT.getParameter()).addKey(age,Mapping.TEXT.getParameter()).buildMixedIndex("search")

这个链接有跟多关于文本和字符串查询的配置项,以及每个不同的后端是如何处理文本和字符串查找的。

虽然混合索引和组合索引定义看起来类似,但是它提供了更强大的查询支持,并且可以回答下边任意一种查询


g.V().has('name', textContains('hercules')).has('age', inside(20, 50))
g.V().has('name', textContains('hercules'))
g.V().has('age', lt(50))
g.V().has('age', outside(20, 50))
g.V().has('age', lt(50).or(gte(60)))
g.V().or(__.has('name', textContains('hercules')), __.has('age', inside(20, 50)))

混合索引支持全文搜索,区间搜索,地理位置搜索和其他搜索。

和组合约束不同,混合索引不支持唯一性约束

添加属性

可以对一个已经存在的混合索引添加属性,添加后,后续的查询可以在查询里添加这个属性。


graph.tx().rollback()  //Never create new indexes while a transaction is active
mgmt = graph.openManagement()
location = mgmt.makePropertyKey('location').dataType(Geoshape.class).make()
nameAndAge = mgmt.getGraphIndex('nameAndAge')
mgmt.addIndexKey(nameAndAge, location)
mgmt.commit()
//Previously created property keys already have the status ENABLED, but
//our newly created property key "location" needs to REGISTER so we wait for both statuses
ManagementSystem.awaitGraphIndexStatus(graph, 'nameAndAge').status(SchemaStatus.REGISTERED, SchemaStatus.ENABLED).call()
//Reindex the existing data
mgmt = graph.openManagement()
mgmt.updateIndex(mgmt.getGraphIndex("nameAndAge"), SchemaAction.REINDEX).get()
mgmt.commit()

要添加新定义的键,我们首先按其名称从管理事务检索现有索引,然后调用addIndexKey方法将该键添加到该索引。
如果在相同的管理事务中定义了添加的键,则可以立即查询它。如果属性键已经在使用中,则添加该键需要执行一个重索引过程,以确保索引包含所有先前添加的元素。在重索引过程完成之前,该键在混合索引中不可用。

映射参数

当向混合索引添加属性键时(通过索引生成器或addIndexKey方法),可以选择指定参数列表来调整属性值如何映射到索引后端。有关每个索引后端支持的参数类型的完整列表,请参阅映射参数概述。

排序

可以使用order().by()指令定义返回图形查询结果。order().by()方法需要两个参数:
1. Property 的名字,可以用来对结果排序。
2. 排序的顺序,可以是incr或者decr
例如,查询g.V().has(‘name’, textContains(‘hercules’)).order().by(‘age’, decr).limit(10)检索以hercules名字命名的最年长的10个人。

当使用 order().by() 需要注意:
1. 组合图索引本身不支持排序搜索结果。所有结果将被检索,然后在内存中排序。对于大型结果集,这可能非常低效。
2. 混合索引支持本地高效排序。但是,order().by()方法中使用的属性名必须先前添加到混合索引中,以支持本机结果排序。在order().by()属性名与查询属性名不同的情况下,这一点非常重要。如果属性名不是索引的一部分,那么排序需要将所有结果加载到内存中。

Label 限制

在很多情况下,是需要只对特定Label的点或者边进行索引。比如,我们想只对label为神的name增加索引,而不是对所有的点的name属性增加索引。在定义一个索引的时候,我们可以通过index builder 的indexOnly来限制对特定label的点或者边进行索引。下边的例子就只对label为神的顶点的name进行索引


graph.tx().rollback()  //Never create new indexes while a transaction is active
mgmt = graph.openManagement()
name = mgmt.getPropertyKey('name')
god = mgmt.getVertexLabel('god')
mgmt.buildIndex('byNameAndLabel', Vertex.class).addKey(name).indexOnly(god).buildCompositeIndex()
mgmt.commit()
//Wait for the index to become available
ManagementSystem.awaitGraphIndexStatus(graph, 'byNameAndLabel').call()
//Reindex the existing data
mgmt = graph.openManagement()
mgmt.updateIndex(mgmt.getGraphIndex("byNameAndLabel"), SchemaAction.REINDEX).get()
mgmt.commit()

标签限制同样适用于混合索引。当具有标签限制的复合索引定义为惟一时,惟一性约束仅适用于指定标签的顶点或边缘上的属性。

组合索引VS混合索引

  1. 使用复合索引检索精确匹配索引。复合索引不需要配置或操作外部索引系统,而且通常比混合索引快得多。
    作为一个例外,当查询约束的不同值的数量相对较小,或者期望一个值与图中的许多元素相关联时(即在选择性较低的情况下),使用混合索引进行精确匹配。
  2. 对数字区间,全文检索,地理空间可以使用混合索引。使用混合索引可以加速 order().by()查询。

点为中心的索引

以顶点为中心的索引是为每个顶点单独构建的本地索引结构。在大型图中,顶点可以有数千条关联边。遍历这些顶点可能非常慢,因为必须检索关联边的一个大子集,然后在内存中进行过滤,以匹配遍历的条件。以顶点为中心的索引可以通过使用本地化的索引结构只检索需要遍历的边缘来加快这种遍历。

假设大力神赫拉克勒斯除了在诸神图中捕获的三个怪物外,还与数百个怪物搏斗。如果没有以顶点为中心的索引,那么查询在发生10-20次战斗的怪物将需要检索所有战斗的边,即使只有少量匹配的边。


h = g.V().has('name', 'hercules').next()
g.V(h).outE('battled').has('time', inside(10, 20)).inV()

随着时间的推移,构建以顶点为中心的索引可以加快这种遍历查询的速度。注意,这个初始索引示例已经作为名为edge的索引存在于诸神图中。因此,运行下面的步骤将导致唯一性约束错误。


graph.tx().rollback()  //Never create new indexes while a transaction is active
mgmt = graph.openManagement()
time = mgmt.getPropertyKey('time')
battled = mgmt.getEdgeLabel('battled')
mgmt.buildEdgeIndex(battled, 'battlesByTime', Direction.BOTH, Order.decr, time)
mgmt.commit()
//Wait for the index to become available
ManagementSystem.awaitRelationIndexStatus(graph, 'battlesByTime').call()
//Reindex the existing data
mgmt = graph.openManagement()
mgmt.updateIndex(mgmt.getGraphIndex("battlesByTime"), SchemaAction.REINDEX).get()
mgmt.commit()

这个例子构建了一个以顶点为中心的索引,在两个方向上以时间递减的顺序对battled边进行索引。一个以顶点为中心的索引是根据一个特定的边的标签构建的,这个标签是索引构造方法JanusGraphManagement.buildEdgeIndex()的第一个参数。在上面的示例中索引只适用于以battled为的标签的边。第二个参数是索引的唯一名称’battlesByTime’。第三个参数是建立索引的边缘方向。索引只适用于沿此方向的边的遍历。在本例中,以顶点为中心的索引是在两个方向上构建的,这意味着该索引可以在输入和输出两个方向上提供受时间限制的沿battled边的遍历。JanusGraph将在battled的输入顶点和输出顶点上维护一个以顶点为中心的索引。或者,可以将索引定义为只应用于OUT方向,这将加速从大力神到怪物的遍历,但不是相反的方向。这只需要维护一个索引,因此需要一半的索引维护和存储成本。最后两个参数是索引的排序顺序和要按索引的属性名列表。排序顺序是可选的,默认为升序(即order . asc)。属性名的列表必须是非空的,并且是给定Label的边定义过的。可以用多个属性名定义以顶点为中心的索引。


graph.tx().rollback()  //Never create new indexes while a transaction is active
mgmt = graph.openManagement()
time = mgmt.getPropertyKey('time')
rating = mgmt.makePropertyKey('rating').dataType(Double.class).make()
battled = mgmt.getEdgeLabel('battled')
mgmt.buildEdgeIndex(battled, 'battlesByRatingAndTime', Direction.OUT, Order.decr, rating, time)
mgmt.commit()
//Wait for the index to become available
ManagementSystem.awaitRelationIndexStatus(graph, 'battlesByRatingAndTime', 'battled').call()
//Reindex the existing data
mgmt = graph.openManagement()
mgmt.updateIndex(mgmt.getRelationIndex(battled, 'battlesByRatingAndTime'), SchemaAction.REINDEX).get()
mgmt.commit()

这个例子对图的scheam进行了扩展,在label为battled的边上增加了rating属性,并且构建了一个以点为中心的索引。它对battled的边在输出方向进行了索引,并对rating和time进行了降序排列。属性名顺序决定了排序顺序。


h = g.V().has('name', 'hercules').next()
g.V(h).outE('battled').property('rating', 5.0) //Add some rating properties
g.V(h).outE('battled').has('rating', gt(3.0)).inV()
g.V(h).outE('battled').has('rating', 5.0).has('time', inside(10, 50)).inV()
g.V(h).outE('battled').has('time', inside(10, 50)).inV()

因此 battlesByRatingAndTime索引可以对前两个查询加速,但是不能对第三个查询加速。可以为相同的边缘标签构建多个以顶点为中心的索引,以支持不同的约束遍历。JanusGraph的查询优化器试图为任何给定的遍历选择最有效的索引。以顶点为中心的索引只支持等式和range/interval约束。

以顶点为中心的索引中使用的属性名必须具有显式定义的数据类型(即不支持Object.class),该数据类型支持本机排序顺序。这意味着它们不仅必须实现Comparable,而且它们的序列化器必须执行OrderPreservingSerializer。当前支持的类型有Boolean、UUID、Byte、Float、Long、String、Integer、Date、Double、Character和Short

如果以顶点为中心的索引是根据在同一管理事务中定义的边Lable构建的,那么该索引将立即可用来查询。如果边的Label已经在使用中,则对其构建以顶点为中心的索引需要执行一个重索引过程,以确保索引包含以前添加的所有边。在重新索引过程完成之前,该索引将不可用。

JanusGraph根据边缘标签和属性键自动构建以顶点为中心的索引。这意味着,即使有数千条的battled的边,像g.V(h).out(‘mother’)或g.V(h).values(‘age’)这样的查询仍然可以由本地索引有效地回答。

以顶点为中心的索引不能加速无约束的遍历,因为这种遍历需要遍历特定标签的所有关联边。随着接入边数量的增加,这些遍历将变得更慢。通常,这种遍历可以重写为受约束的遍历,可以使用以vertex为中心的索引来确保可接受的性能。以顶点为中心的索引(VCI)存储在存储后端(Cassandra, HBase)。不需要外部索引后端。

对于顶点上的关联边,它们在默认情况下按边标签排序。在《诸神图》中,“brother”边缘先于“lives”边缘出现。现在我们举个例子,一个顶点有1000条brother边和2000条lives边。所有的“brother”边都将在一起找到,但是如果您想更具体地按年龄找到兄弟,那么在“age”边属性上设置VCI将非常有用。

有序遍历

以下查询指定要遍历连入边的顺序。使用localLimit命令检索每个被遍历顶点的边的子集(按照给定的顺序)


h = g.V().has('name', 'hercules').next()
g.V(h).local(outE('battled').order().by('time', decr).limit(10)).inV().values('name')
g.V(h).local(outE('battled').has('rating', 5.0).order().by('time', decr).limit(10)).values('place')

第一个查询询问最近与大力神战斗的10个怪物的名字。第二个查询询问的是被评为5星的最近10场赫拉克勒斯战役的位置。在这两种情况下,查询都受到属性键上的顺序的约束,并且对返回的元素数量有限制。

如果顺序键与索引的键匹配,并且请求的顺序(即升序或者降序)与为索引定义的顺序相同,那么以顶点为中心的索引也可以有效地回答此类查询。battlesByTime 索引将用于回答第一个查询,battlesByRatingAndTime 索引将用于回答第二个查询。请注意,battlesByRatingAndTime索引不能用于回答第一个查询,因为要使索引中的第二个键有效,必须提供关于评级的等式约束。

有序顶点查询是JanusGraph对Gremlin语法的扩展,它会导致冗长的语法,并且需要_()步骤将JanusGraph结果转换回Gremlin管道。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

%d 博主赞过: