Elasticsearch 使用 HNSW 演算法進行近似 knn 搜尋。HNSW 是一種基於圖的演算法,只有在大多數向量資料儲存在記憶體中時才能有效運作。因此,您應確保資料節點具備足夠的堆外記憶體,以儲存向量資料及索引結構。本文向您介紹在使用向量引擎時,需要重點關注的效能最佳化要點。
設定合理參數
您可以選擇設定合理的m、ef_construction參數,上述參數是建立索引時,dense_vector類型的進階參數,詳情請參見Dense vector field type。
HNSW是一種近似knn搜尋方法,無法確保100%返回最相鄰的資料。影響召回率的主要參數為m和ef_construction。
參數 | 內容 |
| 表示一個節點的鄰居數量,預設值為16。鄰居數量越多,召回率會相應提高,但這將對效能產生較大影響,並增加記憶體佔用。如果對召回率有嚴格要求,可以將其設定為64或更大的值。 |
| 是在構建 |
降低記憶體消耗
Elasticsearch採用量化技術來降低記憶體佔用。通過量化,向量的記憶體容量可以減少4倍、8倍甚至32倍。以預設的float類型為例,一個向量的值佔用4位元組。如果使用int8量化,每個值僅佔用1位元組;採用int4量化時,每個值佔用半個位元組;而使用BBQ(Better Binary Quantization)量化,每個值僅佔用1位元,8個值合計為1位元組。與未量化情況相比,記憶體需求僅為原來的1/32。
計算向量資料所需的記憶體:
需要考慮向量資料的記憶體和HNSW圖索引的記憶體這兩部分。在未量化或進行int8量化時,圖索引所佔記憶體比例較小。然而,在進行bbq量化時,圖索引的記憶體佔比將顯著增加。因此,計算向量資料所使用的記憶體時,必須重視這部分記憶體的影響。
向量資料記憶體計算方式:
element_type: float:num_vectors * num_dimensions * 4element_type: floatwithquantization: int8:num_vectors * (num_dimensions + 4)element_type: floatwithquantization: int4:num_vectors * (num_dimensions/2 + 4)element_type: floatwithquantization: bbq:num_vectors * (num_dimensions/8 + 12)element_type: byte:num_vectors * num_dimensionselement_type: bit:num_vectors * (num_dimensions/8)
如果採用flat類型且未建立HNSW索引,則向量資料的記憶體佔用如前計算方式所述;如果採用HNSW類型,則還需計算圖索引的大小,以下為圖索引大小的估算:
num_vectors * 4 * HNSW.m,HNSW.m的預設值為 16,因此預設情況下為 num_vectors * 4 * 16。
因此,向量資料的總記憶體為上述兩個部分的大小之和。
另外,需要關注number_of_replicas(索引複本數量)的數量,上面計算的是一份資料的記憶體容量,接下來需乘以replica的資料,number_of_replicas的預設值為1,因此記憶體容量為一份資料的記憶體容量 * 2。
開啟量化後,索引容量將會比之前有所增加,因為Elasticsearch保留了原始向量,並同時增加了量化後的向量資料。增加的容量來源於上述向量資料記憶體計算的第一部分。例如,當對40 GB的浮點向量進行int8量化時,將為量化向量額外儲存10 GB的資料。因此,整體磁碟使用量為50 GB,但快速搜尋時的記憶體使用量量將減少至10 GB。
堆外記憶體容量是否充足
在計算記憶體容量以及檢查節點記憶體是否充足時,需重點關注節點的堆外記憶體。
擷取堆外記憶體:一個節點要為Java的堆預留足夠的記憶體,一個節點的堆外記憶體在記憶體小於等於64 GB的節點上一般是記憶體的一半,超過64 GB後,堆外預設是節點記憶體減去31 GB,精確的計算方式可以直接使用如下命令:
GET _nodes/stats?human&filter_path=**.os.mem.total,**.jvm.mem.heap_max一個節點具體的堆外記憶體容量為:os.mem.total - jvm.mem.heap_max。
向量索引記憶體計算
樣本如下:
假設一份1000w的1024維資料,使用向量的預設值,開啟int8量化,m=16,索引number_of_replicas使用預設值1,則向量資料的總記憶體為:
2* (10000000 * (1024 + 4) + 10000000 * 4 * 16) = 20.34 GB。
如果用2個16 GB記憶體的資料節點儲存這個索引,那麼節點堆外總記憶體為16 / 2 * 2 = 16 GB,記憶體是不足以存下向量資料的。
如果用2個32 GB記憶體的資料節點儲存這個索引,那麼節點堆外總記憶體為32 / 2 * 2 = 32 GB,記憶體可以存下向量資料。
在實際計算堆外記憶體時,還需為其他索引、原文以及資料讀寫所產生的網路流量預留一部分記憶體。在生產環境中,當堆外記憶體不足時,通常會導致磁碟ioutil指標持續滿負荷運行,並伴隨大量的隨機讀流量。
預熱檔案系統快取
如果運行 Elasticsearch 的機器重新啟動,檔案系統快取將被清空,因此作業系統需要一些時間才能將索引的熱地區載入到記憶體中,以確保搜尋操作的快速性。您可以使用index.store.preload設定顯式告訴作業系統哪些檔案應根據副檔名立即載入到記憶體中。
如果檔案系統快取不夠大,無法容納所有資料,則在太多索引或太多檔案上急切地將資料載入到檔案系統快取中將使搜尋速度變慢,因此,請謹慎使用上述方法。
使用樣本:
PUT /my_vector_index
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1,
"index.store.preload": ["vex", "veq"]
},
"mappings": {
"properties": {
"my_vector": {
"type": "dense_vector",
"dims": 3
},
"my_text" : {
"type" : "keyword"
}
}
}
}index.store.preload具體載入哪些索引檔案,請參考下面的副檔名稱尾碼說明:
以下副檔名稱尾碼適用於近似 knn 搜尋:每個副檔名均根據量化類型進行細分。
vex是儲存HNSW圖結構的檔案。vec表示所有非量化向量值。這包括所有元素類型:浮點數、位元組和位。veq用於通過量化索引的量化向量:int4或int8。veb用於通過量化索引的二進位向量:bbq。vem、vemf、vemq和vemb用於中繼資料,通常很小,不需要預先載入。
通常情況下,使用量化索引時,應僅預先載入相關的量化值和 HNSW 圖。預先載入原始向量並無必要,且可能會產生相反的效果。
配置樣本:
hnsw:"index.store.preload": ["vex", "vec"]
int8、int4:"index.store.preload": ["vex", "veq"]
bbq:"index.store.preload": ["vex", "veb"]
針對現有索引的設定方式,index.store.preload 是一個靜態參數。一旦索引建立完成,便無法直接修改該參數。如果可以暫時接受索引停用狀態,則可以採取以下步驟進行設定:首先關閉索引,索引關閉後即可進行參數設定,設定完成後再重新開啟索引即可。以下是使用樣本:
POST my_vector_index/_close
PUT my_vector_index/_settings
{
"index.store.preload": ["vex", "veq"]
}
POST my_vector_index/_open減少索引segment數量
Elasticsearch 分區由段(segment)組成,段是索引中的內部儲存元素。對於近似 knn搜尋,Elasticsearch 將每個段的向量值儲存為單獨的HNSW圖,因此knn搜尋必須檢查每個段。最近的knn搜尋並行化使得跨多個片段的搜尋速度更快,但如果片段較少,knn搜尋的速度仍然可以提高數倍。預設情況下,Elasticsearch 通過後台合并過程定期將較小的段合并為較大的段。如果這還不夠,您可以採取以下明確的步驟來減少索引段的數量。
1. 增加最大段大小
Elasticsearch 提供了許多可調設定來控制合并過程。一項重要的設定是index.merge.policy.max_merged_segment。這控制合并過程中建立的段的最大值大小。通過增加該值,可以減少索引中的段數。預設值為 5 GB,但這對於較大維度向量來說可能太小。考慮將此值增加到 10 GB 或 20 GB,可以協助減少段數。使用樣本:
PUT my_vector_index/_settings
{
"index.merge.policy.max_merged_segment": "10gb"
}2. 在批量索引期間建立大段
常見的流程是首先進行初始批量上傳,隨後使索引可供搜尋。您可以調整索引設定,以促使Elasticsearch建立更大的初始段,而不是強制進行合并。確保批量上傳期間禁用搜尋,並通過將其設定為 -1 來禁用index.refresh_interval。這將有效防止重新整理操作,並避免產生額外的段。為 Elasticsearch 配置一個較大的索引緩衝,以便其在重新整理之前能夠接收更多文檔。預設情況下,indices.memory.index_buffer_size 設定為堆大小的 10%。對於像 32 GB 這樣的大堆大小,這通常就足夠了。為了允許使用完整的索引緩衝,您還應該增加限制index.translog.flush_threshold_size。
在_source中排除向量欄位
Elasticsearch 將在索引時傳遞的原始 JSON 文檔儲存在 _source 欄位中。預設情況下,搜尋結果中的每個命中都包含完整文檔 _source。當文檔包含高維密集向量欄位時,_source的大小可能非常大且載入成本昂貴。這可能會顯著降低knn搜尋的速度。
reindex, update, update by query操作通常需要 _source 欄位。排除欄位 _source 可能會導致這些操作出現意外行為。例如,重新索引時,可能實際不包含新索引中的dense_vector 欄位。
您可以通過 excludes 映射參數排除在 _source 中儲存的稠密向量欄位。這可以防止在搜尋期間載入和返回大量原始向量資料,同時也能減少索引的大小。雖然在 _source 中排除的向量仍然可以在 knn 搜尋中使用,因為該過程依賴於獨立的資料結構執行搜尋,但在使用 excludes 參數之前,請務必查看排除_source欄位的潛在缺點,缺點見上方說明。
PUT /my_vector_index
{
"mappings": {
"_source": {
"excludes": [
"my_vector"
]
},
"properties": {
"my_vector": {
"type": "dense_vector",
"dims": 3
},
"my_text": {
"type": "keyword"
}
}
}
}要查看doc中的向量內容,Elasticsearch版本為8.17或以上時,可以使用:
GET my_vector_index/_search
{
"docvalue_fields": ["my_vector"]
}
其他版本可以使用:
GET my_vector_index/_search
{
"script_fields": {
"vector_field": {
"script": {
"source" : "doc['my_vector'].vectorValue"
}
}
}
}除了在_source中排除向量欄位,另一種選擇使用方法,請參見synthetic _source。
升級機型配置
由於向量相似性計算屬於計算密集型任務,對CPU效能要求較高。因此,可選擇Turbo機型來擷取一倍以上的效能提升。您可通過藍綠變更來選擇同規格下的Turbo機型。