索引优化性能
janusgraph支持两种不同的索引方式来加速query过程:graph indexes和vertex-centric indexes。大多数graph query启动一个指定属性的遍历检索顶点和边的集合,graph indexes在大图中让这些指定属性的全局检索更有效率。vertex-centric indexes加速graph中遍历需求,特别是遍历的顶点有非常多事件边的场景。
Graph Index 图索引
graph indexes是完整graph的全局索引结构,提供高效的通过可选属性查询条件检索顶点和边。例如下面的query:
g.V().has('name', 'hercules')
g.E().has('reason', textContains('loves'))
前一个查询name是hercules的所有顶点。第二个查询reason包含loves的所有边。如果没有graph indexes,这些query会全局scan图中所有顶点和边,来找到符合条件的结果,这在巨大图中是非常低效和不可行的。
janusgraph的graph indexes有区分了两种类型:composite index和mixed index索引。composite index是非常快读和高效的,但是查询限制在特定、预定义的属性key组合,value等于的查询条件。和mixed index能用在任意组合的属性key,且支持多种查询谓词(取决于后端index存储的能力)。
两种索引类型都通过janusgraph managerment系统创建,通过JanusGraphManagement.buildIndex(String, Class)返回index builder,前一个参数定义索引index的名字,第二个参数定义被索引的元素类型(例如:Vertex.class)。索引index的名字必须时唯一的。图索引index的build在新定义的属性key上会即时生效,例如在同一个management事务里定义属性key和索引index。同样图索引被约束到在同一management事务中创建的label。图索引build在已使用但未约束到新创建的label的属性key上,
图形索引要求执行reindex procedure,以确保index包含所有先前添加的元素。reindex procedure完成前,index将不可用。建议可以在schmame初始化时同一个事务中定义图索引。
笔记
如没有索引,janusgraph将默认全图scan去检索需要的顶点list。全图scan会非常低效和消耗全局大量的计算性能。激活*force-index*配置在生产环境可以避免全图scan。
信息
更多index状态信息看index lifecycle documentation
Composite Index 复合索引
composite index检索vertex和edge通过1个或1组keys。如下:
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一已经定义。然后一个简单的composite index只build在name上。下面的query会使用索引:
g.V().has('name', 'hercules')
第二个composite index是build在两个key上,下面的query会使用索引:
g.V().has('age', 30).has('name', 'hercules')
需要注意,索引只会用在query和定义完全匹配情况下,下面的query不会用索引,因为只有age而没有name:
g.V().has('age', 30)
还要注意,composite index索引的约束必须是等于,如下query只会用到name的索引,而age不会用到:
g.V().has('name', 'hercules').has('age', inside(20, 50))
composite index不需要外部index storage配置,可以通过primary storage支持。因此,composite index修改通过与图修改相同的事务持久化,这意味着如果基础存储后端支持原子性和/或一致性,则这些更改是原子性的和/或一致的。
注意
1个*composite index*可能由1个或多个key组成。仅有1个key也可叫key-index。
Index Uniqueness 索引唯一性
composite index也可以用来做图中的唯一性保证。如果composite index被定义为unique(),那么索引的key或keys必须是唯一的。例如:
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()
注意
为了对最终一致的存储后端实施唯一性,必须将索引的一致性显式设置为启用锁定。
Mixed Index 混合索引
mixed index混合索引通过属性key任何组合检索顶点和边。比复合索引更灵活,支持更多的条件谓词。反之,混合索引大多数情况也会更慢一些。
不想复合索引,混合索引需要单独索引storage后端的配置,使用索引storage执行查询操作。janusgraph支持在一个部署里使用多种索引后端。每个索引后端需要有独立命名在配置文件里,命名为indexing backend name。
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,这个buildMixedIndex()函数里的search需要在配置文件里定义的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")
Index Parameters and Full-Text Search有text和string更多选项,有关每个后端如何处理文本搜索和字符串搜索的更多详细信息,请参见各自正在使用的索引后端的文档部分。
虽然索引定义示例看起来与上面的复合索引相似,但是它提供了更强的query查询支持,并且可满足以下任何查询。
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)))
混合索引支持全文search、range search、geo search和其他。参考Search Predicates and Data Types看特定索引存储支持的谓词列表。
注意
不想复合索引,混合索引不支持唯一性。
Adding Property Keys 索引增加key
属性key可以被增加到一个已有的混合索引里,该索引允许后续查询在查询条件中包括此key。
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()
要添加新定义的key,我们首先按名称从management事务中检索现有索引,然后调用addIndexKey方法将键添加到该索引中。
如果添加的key也在同一management事务中被定义,则将立即可用于查询。如果属性key已在使用中,则添加键需要执行reindex procedure,以确保索引包含所有先前添加的元素。在重新索引过程完成之前,该key在混合索引中将不可用。
Mapping Parameters
当通过索引builder或addIndexKey方法将属性键添加到混合索引时,可以选择指定参数列表,以调整如何将属性值映射到索引后端。有关每个索引后端支持的参数类型的完整列表,请参阅mapping parameter overvier。
Ordering 排序
图query结果的排序,可以用用order().by()。order().by()方法需要两个参数:
- 用于排序结果的属性key名称。结果将按顶点或边此属性key的值排序。
- 排序顺序,
升序asc或降序desc。
例如:检索name包含hercules年龄最大的10个元素。
g.V().has('name', textContains('hercules')).order().by('age', desc).limit(10)
使用order().by()有几点比较重要的需要知道:
- 复合索引本身不支持对搜索结果进行排序。所有结果都将被检索,然后在内存中排序。对于大型结果集,这可能会非常昂贵。
- 混合索引支持本地有效排序。但必须事先将order().by()方法中使用的属性key添加到混合索引中,以支持本机结果排序。这在order().by()键key与查询键key不同的情况下非常重要。如果属性key不是索引的一部分,则排序需要将所有结果加载到内存中。
Label Constraint label约束
在许多情况下,只需要索引带有特定label的顶点或边即可。例如,只想索引label是god的顶点name属性,而不是每个具有name属性的顶点。定义索引时,可以使用索引builder的indexOnly方法将索引限制为特定label的顶点或边。下面为属性key是name创建的一个复合索引,该索引仅索引label为god的顶点。
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()
label限制同样适用于混合索引。将具有label限制的复合索引定义为唯一时,唯一性约束仅适用于指定label的顶点或边上的属性。
Composite versus Mixed Indexes 两种索引相比
- 使用复合索引进行精确匹配索引检索。复合索引不需要配置或操作外部索引系统,并且通常比混合索引快得多。
- 作为例外,当查询约束的distinct值的数量相对较小时,或者如果期望一个值与graph中的许多元素相关联(在选择性较低的情况下,如性别:男/女),请使用混合索引进行精确匹配。
- 将混合索引用于数字范围,全文本或地理空间索引。同样,使用混合索引可以加快order().by()查询。
Vertex-centric Indexes 顶点为中心的索引
以顶点为中心的索引是每个顶点单独构建的局部索引结构。在大图中,顶点可以具有数千个入边。遍历那些顶点可能非常慢,因为必须检索大量入边子集,然后在内存中进行过滤以匹配遍历的条件。以顶点为中心的索引可以通过使用局部索引结构仅检索需要遍历的那些边来加快此类遍历。
假设Hercules除了Graph of the Gods中捕获的三个monsters外还与数百个monsters战斗。如果没有vertex-centric index,查询战斗次数在10和20之间的怪物,查询将需要检索所有battled边,即使只有少数匹配的边也是如此。
h = g.V().has('name', 'hercules').next()
g.V(h).outE('battled').has('time', inside(10, 20)).inV()
通过次数time建立vertex-centric index可加快此类遍历查询的速度。注意,此初始索引示例已在Graph of the Gods中作为名为edges的索引存在。所以执行以下步骤将导致唯一性约束错误。
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.desc, 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.getRelationIndex(battled, "battlesByTime"), SchemaAction.REINDEX).get()
mgmt.commit()
这个示例构建了一个vertex-centric index,该索引以time降序索引了双向的battled边。
针对特定的边label构建vertex-centric index。
- 该边label是索引构建方法JanusGraphManagement.buildEdgeIndex()的第一个参数。
- 该索引仅适用于battled这个label的边。第二个参数是索引的唯一名称。
- 第三个参数是建立索引的边方向。该索引会作用于该方向,入边或出边的遍历。在示例中,以vertex-centric index是在双向上构建的,这意味着battled边进行的time限制遍历可以通过该索引在IN和OUT方向上进行。JanusGraph将在battled边的内外顶点均保持vertex-centric index。或者,可以将索引定义为仅适用于OUT方向,这样可以加快从Hercules到monstor的遍历,但不支持反向。这样仅需要维护一个索引,因此只需一半的索引维护和存储成本。
- 最后两个参数是索引的排序顺序以及用于索引的属性key列表。排序顺序是可选的,默认为升序(即Order.ASC)。属性key列表必须为非空,并定义用于索引指定label边的key。可以使用多个key定义vertex-centric index。
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.desc, 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属性key进一步扩展,构建一个vertex-centric index 索引通过rating和time降序,出边out-going 方向索引battled边。注意,指定属性key的顺序很重要,因为vertex-centric index 是前缀索引。这意味着,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索引会加速前两个,第三个不会。
可以为同一边label构建多个*vertex-centric index ,以支持不同的约束遍历。 JanusGraph的query优化器尝试为给定遍历选择最高效的索引。vertex-centric index *仅支持相等性和range范围/interval间隔约束。
注意:vertex-centric index 中使用的属性key必须具有显式定义的数据类型(即不是Object.class),该数据类型必须支持native排序顺序。这不仅意味着他们必须实现Comparable,而且他们的serializer必须实现OrderPreservingSerializer。当前支持的类型是Boolean,UUID,Byte,Float,Long,String,Integer,Date,Double,Character和Short。
如果是在同一managerment事务中定义的边label和至少一个属性key为基础,构建的vertex-centric index将立即可用于查询。如果边label和所有索引属性key都已被使用,则针对其建*vertex-centric index *需要执行reindex procedure,以确保索引包含所有先前添加的label。在reindex procedure完成之前,索引将不可用。
注意:
JanusGraph自动为每个边label和属性key构建vertex-centric index。这意味着,即使有数千个的battled事件边,诸如g.V(h).out('mother')或g.V(h).values('age')之类的查询也可以通过本地索引得到有效响应。
vertex-centric index无法加快无约束遍历,无约束遍历需要遍历一个特定label的所有入边。随着入边数量的增加,这些遍历将变慢。通常,此类遍历可以重写为可以利用vertex-centric index以确保在规模上可接受的性能的受约束遍历。
Ordered Traversals 有序遍历
以下query指定要遍历入边的顺序。使用localLimit命令为遍历的每个顶点检索边的子集(以给定的顺序)。
h = g..V().has('name', 'hercules').next()
g.V(h).local(outE('battled').order().by('time', desc).limit(10)).inV().values('name')
g.V(h).local(outE('battled').has('rating', 5.0).order().by('time', desc).limit(10)).values('place')
第一个query询问Hercules的time最多的10个battled关系monstor的name。第二个query询问rating为5.0的time最多的10个battled关系的place。在这两种情况下,查询都受属性key顺序的限制,该顺序限制了要返回的元素数。
如果顺序key与索引的key匹配并且请求的顺序(即升序或降序)与为索引定义的key相同,则也可以通过vertex-centric index有效地响应此类查询。 BattlesByTime索引将用于回答第一个查询,而BattlesByRatingAndTime则适用于第二个查询。请注意,因为必须存在对rating的相等约束才能使索引中的第二个键生效,所以无法使用BattlesByRatingAndTime索引来回答第一个查询。
网友评论