【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()
首先,在圖模型中name
和age
這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個屬性鍵name
和age
。這個定義被關聯到名為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
出邊,屬性包括rating
和time
,索引順序為降序。注意,索引中屬性排序順序非常重要,因為中心頂點索引是字首索引。這意味著,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
)並支援排序。這以為著這個型別必須實現了Comparable
和OrderPreservingSerializer
。當前支援的資料型別包括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管道。