JanusGraph支持两种不同的索引来加速查询处理:图索引和以顶点为中心的索引。 大多数图查询从由其属性标识的顶点或边的列表开始遍历。 图索引使这些全局检索操作在大图上有效。 以顶点为中心的索引可加快图中的实际遍历,特别是在遍历具有许多入边的顶点时。

1. 图索引

图索引是整个图上的全局索引结构,允许通过其属性有效地检索顶点或边,以获得足够的选择条件。 例如,请思考以下查询:

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

第一条语句是查询所有name为hercules的顶点。 第二条语句是查询所有属性reason包含单词loves的边。 如果没有图索引来检索这些查询,则需要对图中的所有顶点或边进行全量扫描,以找到与给定条件匹配的那些顶点或边,这对于大图而言是非常低效且不可行的。

JanusGraph区分两种类型的图索引:复合索引和混合索引。 复合索引非常快速且高效,但仅限于特定的,先前定义的属性键组合的相等条件查询。 混合索引可用于对索引键的任何组合条件进行查询,并且除了相等之外还支持多个条件谓词,具体情况取决于所使用的后端存储索引。

两种类型的索引都是通过JanusGraph管理系统和JanusGraphManagement.buildIndex(String,Class)返回的索引构建器创建的,其中第一个参数定义索引的名称,第二个参数指定要索引的元素的类型(例如:Vertex.class)。图索引的名称必须是唯一的。针对新定义的属性键构建的图索引,即在与索引相同的管理事务中定义的属性键,立即可用。这同样适用于受限于与索引在同一管理事务中创建的标签的图索引。针对已经在使用但未限制为新创建的标签的属性键构建的图索引需要执行reindex过程以确保索引包含所有先前添加的元素。在reindex过程完成之前,索引将不可用。建议在与初始模式相同的事务中定义图索引。

注意:在没有索引的情况下,JanusGraph将默认为完整图扫描,以便检索所需的顶点列表。 虽然这会产生正确的结果集,但图扫描效率非常低,并且会导致生产环境中整体系统性能不佳。 在JanusGraph的生产部署中启用强制索引配置选项以禁止图扫描。

1.1. 复合索引

复合索引通过一个或多个(固定)组合的多个键来检索顶点或边。 请思考以下复合索引定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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将使用索引来进行以下查询。

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

第二个复合图索引包括两个键。 JanusGraph将使用索引来进行以下查询。

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

请注意,必须在查询的相等条件中找到复合图索引的所有键才能使用此索引。 例如,以下查询无法使用任一索引来查询,因为它仅包含对age的约束,但不包含name。

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

另请注意,复合图索引只能用于上述查询中的相等约束。 仅使用名称键上定义的简单复合索引来进行以下查询,因为age约束不是等式约束。

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

复合索引不需要配置外部索引后端,并且通过主存储后端支持。 因此,复合索引修改通过与图修改相同的事务持久化,这意味着如果底层存储后端支持原子性和/或一致性,这些更改也支持原子性和/或一致性。

注意:复合索引可以仅包括一个或多个键。 只有一个键的复合索引有时被称为键索引。

1.1.1. 索引唯一性

复合索引还可用于在图中强制执行属性唯一性。 如果复合图索引定义为unique(),则对于与该索引的键关联的任何给定的属性值串联,最多只能有一个顶点或边。 例如,要强制在整个图中名称是唯一的,将定义以下复合图形索引。

1
2
3
4
5
6
7
8
9
10
11
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()

注意:要针对最终一致的存储后端强制实现唯一性,必须将索引的一致性显式设置为启用锁定。

1.2. 混合索引

混合索引通过以前添加的属性键的任意组合来检索顶点或边。 混合索引比复合索引提供更多灵活性,并支持超出相等性的其他条件谓词。 但是,对于大多数相等查询,混合索引比复合索引慢。

与复合索引不同,混合索引需要配置索引后端,并使用该索引后端执行查找操作。 JanusGraph可以在单个安装中支持多个索引后端。 必须在JanusGraph配置中通过名称唯一标识每个索引后端,该配置称为索引后端名称。

1
2
3
4
5
6
7
8
9
10
11
12
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()

上面的示例定义了包含属性name和age的混合索引。 该定义指的是索引后端名称search,以便JanusGraph知道它应该为此特定索引使用哪个配置的索引后端。 buildMixedIndex调用中指定的search参数必须与JanusGraph配置定义中的第二个子句匹配,如下所示:index.search.backend如果索引名为solrsearch,则配置定义将如下所示:index.solrsearch.backend。

上面指定的mgmt.buildIndex示例使用文本搜索作为其默认行为。 将索引显式定义为文本索引的索引语句可以写成如下:

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

有关文本和字符串搜索选项的详细信息,请参阅第24章,索引参数和全文搜索,有关每个后端如何处理文本与字符串搜索的更多详细信息,请参阅特定于所使用的索引后端的文档部分。

虽然索引定义示例看起来类似于上面的复合索引,但它提供了更多的查询支持,并且可以进行以下任何查询。

1
2
3
4
5
6
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)))

混合索引支持全文搜索,范围搜索,地理搜索等。 有关特定索引后端支持的谓词列表,请参阅第23章,搜索谓词和数据类型。

注意:与复合索引不同,混合索引不支持唯一性。

1.2.1. 添加属性

可以将属性键添加到现有的混合索引,这允许后续查询在查询条件中包含此键。

1
2
3
4
5
6
7
8
9
10
11
12
13
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方法将键添加到此索引。

如果添加的key在同一管理事务中定义,则它将立即可用于查询。 如果属性键已被使用,则添加key需要执行reindex过程以确保索引包含所有先前添加的元素。 在reindex过程完成之前,key将不会在混合索引中可用。

1.2.2. 映射参数

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

1.3. 排序

可以使用order().by()指令定义返回图查询结果的顺序。 order().by()方法需要两个参数:

  • 用于对结果进行排序的属性键的名称,结果将按此属性键的顶点或边的值排序。
  • 排序顺序:升序incr或降序decr

例如,查询g.V().has('name', textContains('hercules')).order().by('age', decr).limit(10)名字中带有hercules的十个最老的个体。

使用order().by()时,请务必注意:

  • 复合图索引本身不支持对搜索结果进行排序。 将检索所有结果,然后在内存中进行排序。 对于大型结果集,这可能非常昂贵。
  • 混合索引支持本地和高效的排序。 但是,order().by()方法中使用的属性键必须先前已添加到用于本机结果排序支持的混合索引中。 这在order().by()键与查询键不同的情况下很重要。 如果属性键不是索引的一部分,则排序需要将所有结果加载到内存中。
1.4. 标签约束

在许多情况下,希望仅使用特定标签索引顶点或边。 例如,人们可能希望仅通过名称来索引神,而不是每个具有name属性的顶点。 定义索引时,可以使用索引构建器的indexOnly方法将索引限制为特定的顶点或边标签。 下面为属性键名称创建一个复合索引,该索引仅索引标记为god的顶点。

1
2
3
4
5
6
7
8
9
10
11
12
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()

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

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

2. 以顶点为中心的索引

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

假设Hercules除了在众神图中捕获了三个怪物之外还与数百个怪物作战。 如果没有以顶点为中心的索引,要求在10点和20点之间作战的怪物的查询将需要检索所有战斗边,即使只有少数匹配边。

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

按时间构建以顶点为中心的索引会加速此类遍历查询。 注意,这个初始索引示例已经作为名为edges的索引存在于众神图中。 因此,运行以下步骤将导致唯一性约束错误。

1
2
3
4
5
6
7
8
9
10
11
12
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()

此示例构建一个以顶点为中心的索引,该索引按递减顺序按时间对两个方向的战斗边缘进行索引。以特定边标签构建以顶点为中心的索引,该标签是索引构造方法JanusGraphManagement.buildEdgeIndex()的第一个参数。索引仅适用于此标签的边缘 - 在上面的示例中进行了争斗。第二个参数是索引的唯一名称。第三个参数是构建索引的边缘方向。索引仅适用于沿此方向的边缘遍历。在此示例中,以顶点为中心的索引在两个方向上构建,这意味着沿着战斗边缘的时间限制遍历可以在IN和OUT方向上由该索引提供。 JanusGraph将在战斗边缘的入顶点和出顶点上保持以顶点为中心的索引。或者,可以定义仅应用于OUT方向的索引,这将加速从Hercules到怪物的遍历,但不能反向。这只需要维护一个索引,因此需要维护和存储成本的一半。最后两个参数是索引的排序顺序和索引的属性键列表。排序顺序是可选的,默认为升序(即Order.ASC)。属性键列表必须为非空,并定义用于索引给定标签边缘的键。可以使用多个键定义以顶点为中心的索引。

1
2
3
4
5
6
7
8
9
10
11
12
13
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()

此示例通过在战斗边上的评级属性扩展架构,并构建以顶点为中心的索引,该索引通过按降序排列的评级和时间来指出前进方向上的战斗边。 注意,指定属性键的顺序很重要,因为以顶点为中心的索引是前缀索引。 这意味着,战斗边通过先评级和秒时间来索引。

1
2
3
4
5
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的查询优化器试图为任何给定的遍历选择最有效的索引。 以顶点为中心的索引仅支持相等性和范围/间隔约束。

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

如果针对在同一管理事务中定义的边标签构建以顶点为中心的索引,则索引将立即可用于查询。 如果边标签已被使用,则为其构建以顶点为中心的索引需要执行reindex过程以确保索引包含所有先前添加的边。 在reindex过程完成之前,索引将不可用。

注意:JanusGraph自动为每个边标签和属性键构建以顶点为中心的索引。 这意味着,即使有成千上万的事件争夺边,g.V(h).out(‘mother’) 或g.V(h).values(‘age’) 等查询也可以通过本地索引有效地回答。

以顶点为中心的索引无法加快无约束遍历,这需要遍历特定标签的所有事件边。 随着入射边数量的增加,这些遍历将变慢。 通常,这种遍历可以被重写为约束遍历,其可以利用以顶点为中心的索引来确保大规模可接受的性能。

2.1. 有序遍历

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

1
2
3
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')

第一个是查询最近与Hercules对战的10个的名字。 第二个是查询最近10次rating为5的战斗过地方。 在这两种情况下,查询都受到属性键上的顺序的约束,并且对要返回的元素数量有限制。

如果查询条件与索引key匹配并且所请求的顺序(即增加或减少)与为索引定义的顺序相同,则这些查询也可以通过以顶点为中心的索引有效地查询。 battlesByTime索引将用于处理第一个查询,battlesByRatingAndTime应用于第二个查询。 请注意,battlesByRatingAndTime索引不能用于处理第一个查询,因为必须存在对评级中的第二个键有效的等级约束才能生效。

注意:有序顶点查询是Gremlin的JanusGraph扩展,它导致详细语法并需要 _() 步骤将JanusGraph结果转换回Gremlin管道。