【JanusGraph】第十一章:使用索引提升性能

11. 使用索引提升性能

Chapter 11. Indexing for Better Performance

JanusGraph支持2种类型的索引用以提升查询处理速度,分别是图索引(graph indexes)和中心节点索引(vertex-centric indexes)。大多数图查询都是从它们的属性标识的顶点或边的列表开始遍历的。图索引使在大图中进行全局检索的时候变得非常高效。中心顶点索引可以提升实际图遍历的性能,特别是顶点具有大量入边的时候。

11.1图索引

11.1. Graph Index

图索引是一个全局的索引结构,通过足够的选择条件可以在整个图中高效选择顶点或者边。举个例子,思考一下下面的查询语句:

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

第一条查询语句查询名字为hercules的所有顶点,第二条查询语句查询reason属性包含loves字符串的所有边。如果没有图索引,将在整个图中对顶点和边进行全库扫描,在大图中这是不够高效也不可取的。JanusGraph的图索引(graph indexes)分为2种类型:组合索引(composite index)和混合索引(mixed index)。组合索引非常快并且很高效,但是只能用于相等查询,并且索引需要根据属性键组合预先定义好。混合索引可以用于任何索引建的组合查询,并且支持更多的后端索引存储的条件谓词。

这2种类型的索引都通过JanusGraph管理系统创建,调用JanusGraphManagement.buildIndex(String, Class)返回一个索引构造器,第一个参数为索引名,第二参数区分索引元素类型(比如:Vertex.class)。图中索引名必须唯一。不建议对新定义的属性键马上建索引,因为在与创建索引在一个管理事物中定义的属性key不会立即生效。已经加入索引的所有元素属性key正在重建索引的时候,不建议在立刻对这些元素的属性key构建索引,直到重建索引任务跑完。建议在初始化图模型(schema)相同的事物中定义索引。

注意

缺少索引的时候,JanusGraph将进行全图扫描以找到希望的顶点。当然这样也能返回正确的结果,但是全图扫描非常低效,会降低整个生产环境的系统性能。JanusGraph部署的时候可以设置force-index配置项,禁止全图扫描。

11.1.1 组合索引

11.1.1. Composite Index

组合索引通过一个或者一组key组成的固定的key组合进行检索,就像下面的组合索引定义:

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()

首先,在图模型中nameage这2个属性已经被定义,接下来在name属性上创建组合索引。JanusGraph将在遇到下面查询的时候使用索引。

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

第二个组合索引包含2个属性key,JanusGraph遇到下面查询的时候使用索引。

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

注意,组合索引的所有key都被找到并且都是相等查询的时候组合索引才会被用到。例如下面的查询,定义的2个索引都不会被用到,因为查询条件只包含了age并没有name

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

另外也需要注意的是,组合索引(composite index)仅仅支持相等查询,下面的查询仅仅使用定义在name上的简单组合索引,因为age查询条件并不是相等查询。

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

组合索引不需要配置额外的索引后端,只需要主存储后端即可。组合索引的持久化修改和图修改在一个事物中,这意味着如果存储后端支持原子性和一致性,则创建索引操作也支持原子性和一致性。

11.1.1.1 唯一索引

11.1.1.1. Index Uniqueness

组合索引也可以用于限制图中属性的唯一性。在定义组合索引的时候如果如果指定了unique(),那么该索引关联的属性key对应的顶点或者边最多只能有一个。例如,下面定义的组合索引限定了name在整个图中唯一。

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()

混合索引

11.1.2. Mixed Index

使用混合索引遍历顶点或边的时候,可以使用任何预先添加的属性key的组合。混合索引比组合索引更灵活,支持更多的条件谓词,而组合索引只支持相等判断查询。混合索引比组合索引查询速度要慢。

和组合索引不同,混合索引需要配置索引后端,并使用索引后端执行查找操作。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()

上面定义的混合索引包含2个属性键nameage。这个定义被关联到名为search的索引后端,以使JanusGraph能够知道使用哪个索引后端来创建这个索引。buildMixedIndex方法中search参数值,是需要在JanusGraph配置文件中已经明确配置了的,像index.search.backend这个,如果后端索引名指定为solrsearch,那么配置文件中配置项应该是index.solrsearch.backend这样的。

上面的例子中mgmt.buildIndex使用文本查询作为默认的查询处理方式。明确定义索引为文本索引,语句可以像下面这样写。

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)))

混合索引支持全文检索、范围查询、地理位置查询等。参看 第二十三章:查询谓词和数据类型 ,可以了解特定索引后端支持的查询谓词和数据类型。

注意

和组合索引不一样,混合索引不支持唯一性检查。

添加属性

11.1.2.1. Adding Property Keys

可以往已经存在的混合索引中添加属性,后面的查询如果包含这个属性,就可以利用索引了。

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已经在被使用,执行重建索引操作,以使索引包含所有属性值。在索引重建完成前,混合索引将不可用。

11.1.2.2 参数映射

11.1.2.2. Mapping Parameters

当往混合索引中添加一个属性key时(无论是通过索引构造器还是调用addIndexKey方法),可以调整一些可选的参数以明确索引后端和属性值如何进行映射。可以参看 参数映射概述以了解索引后端支持的完整参数列表。

11.1.3 排序

11.1.3. Ordering

图查询的返回结果顺序可以通过order().by()指令明确规定。order().by()提供了2个参数。

  • 用于结果集排序的属性key。顶点或者边的查询结果将通过这个属性key进行排序后返回。
  • 顺序,incr升序,decr降序。

例如:g.V().has('name', textContains('hercules')).order().by('age', decr).limit(10)这个查询找出姓名包含hercules年龄最大的10个人。

在使用order().by()的时候,知道下面这2条非常重要:

  • 组合索引不支持存储后端本地排序。取回所有结果集后在内存中排序。在大数据集情况下,这种操作代价非常高。
  • 混合索引支持索引后端本地排序,且非常高效。使用混合索引排序必须提前把需要排序的属性key加入混合索引中,并且该属性支持排序。如果属性key不是索引的一部分,将把所有数据加载到内存中进行排序。

11.1.4标签约束

11.1.4. Label Constraint

在某些应用场景下,只需要对某些特定顶点或者边建立索引。比对只想对神通过名字创建索引,而不是所有包含名字的其他类型的顶点都创建索引。当创建索引的时候通过indexOnly方法限定需要创建索引的顶点或者边。下面这个例子,只对神(god)的名字(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()

混合索引也可以做同样的限制。当对组合索引加上了唯一索引标记,那么这个唯一索引将作用到对一的顶点或者边上。

组合索引(composite)和混合索引(mixed)对比

11.1.5. Composite versus Mixed Indexes

1.精准匹配的时候使用组合索引。组合索引不需要配置或操作额外的索引系统并且比混合索引要明显快很多。

  • 也有例外,查询的为数值类型,该值相对较小或在图中关联的元素很多时会使用混合索引(当然这种查询场景较少)。

2.混合索引应用于数值范围查询、全文检索、geo地理位置查询。使用混合索引可以提高order().by()的查询速度。

11.2 中心顶点索引

11.2. Vertex-centric Indexes

中心顶点索引是分别为每个顶点创建的本地索引结构,在大图中,顶点可能有上千条入边。遍历这些定点会非常慢,因为有大量边需要遍历并且需要在内存中对查询条件进行过滤。中心顶点索引可以通过本地索引结构找出需要遍历的边以提升查询性能。

赫拉克勒斯除了在前面神的图谱中介绍的3次与怪兽战斗事件外还有数百次与怪兽战斗的事迹。如果不使用中心顶点索引,那么查询与战斗次数10到20次的怪兽,那么需要遍历所有战斗边,然而可能只有一半的边命中。

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

通过次数的中心顶点索引以提高查询效率。

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.awaitGraphIndexStatus(graph, 'battlesByTime').call()
//Reindex the existing data
mgmt = graph.openManagement()
mgmt.updateIndex(mgmt.getGraphIndex("battlesByTime"), SchemaAction.REINDEX).get()
mgmt.commit()

这个例子中通过battled边的次数属性降序构建中心顶点索引。一个中心顶点的构建是通过JanusGraphManagement.buildEdgeIndex()方法第一次构建的那个特定边构建的。例子中索引仅会作用在battled这个边标签的边上。第二个参数是索引的唯一名字,第三个参数是构建索引时边的方向,查询的时候索引仅对指定方向的边起作用。在这个例子中,中心顶点的构建会作用在具有次数的边上,不管是出边还是入边,2个方向的都会用到。JanusGraph 将会维护一个有battled边无论是入边顶点、还是出边顶点的中心顶点索引。或者定义一个只需要出边的索引,以提升赫拉克勒斯与怪兽战斗遍历效率,而不需要管相反方向的边。这个索引只需要一半的维护成本和存储成本。最后2个参数是索引顺序和加入索引的属性列表。排序顺序是一个可选参数,默认是升序,属性列表不能为空,必须是给定标签边的属性。

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()

这个例子中扩展了模型定义,增加了battled边的rating属性,构建中心顶点索引的条件为battled出边,属性包括ratingtime,索引顺序为降序。注意,索引中属性排序顺序非常重要,因为中心顶点索引是前缀索引。这意味着,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的查询选择器会尝试最有效的索引。中心顶点索引仅支持相当或者范围查询。

注意

中心顶点索引所使用的属性必须是明确定义的(不能是Object.class)并支持排序。这以为着这个类型必须实现了ComparableOrderPreservingSerializer。当前支持的数据类型包括Boolean, UUID, Byte, Float, Long, String, Integer, Date, Double, Character, Short。

中心顶点索引如果在相同的管理事物中被定义,那么在进行查询的时候索引立即生效。如果边标签已经应用于已经构建好的中心顶点索引,重建索引的时候必须确保以前添加的所有边都类型都还存在。直到索引重建完成前,索引将不可用。

注意

JanusGraph会自动为每种类型的边和属性创建中心顶点索引,意思是说battled边有上千条入边的时候,像查询g.V(h).out('mother')或者g.V(h).values('age')本地索引会生效。

中心顶点索引不能提升无约束的所有入射边遍历性能,当入边数量不断增加的时候,查询性能会不断下降。通常这种遍历可以改写为受约束的遍历。

11.2.1排序遍历

11.2.1. Ordered Traversals

下面的查询在遍历入边的时候指定了排序顺序。使用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个怪兽的名字。第二个查询是最近10次获得5星战斗的地点。在这2个查询例子中,都限定了查询结果的返回数量。

这类查询中心顶点索引也会起作用,如果排序key和定义的中心顶点索引键的排序顺序一致,battlesByTime这个索引将会对第一个查询起作用,battlesByRatingAndTime这个索引将会对第二个查询起作用。注意,battlesByRatingAndTime索引将不会对第一个查询生效,因为rating的相等查询只会对第二个查询起作用。

注意

顶点查询排序是JanusGraph对Gremlin的扩展,这种场景中语法会变长并且需要_()步骤把JanusGraph转换为Gremlin管道。


原文:https://docs.janusgraph.org/latest/indexes.html

猜你喜欢

转载自blog.csdn.net/clj198606061111/article/details/82313818