1. 介紹與安裝
參考https://milvus.io/cn/docs/home。Milvus 是一款基于雲原生架構開發的開源向量資料庫,支援查詢和管理由機器學習模型或神經網絡生成的向量資料。Milvus 在一流的近似最近鄰(ANN)搜尋庫(例如 Faiss、NMSLIB、Annoy)的功能基礎上進行擴充,具有按需擴充、流批一體和高可用等特點。
下面介紹幾種安裝方式:
-
Docker compose方法
官網說了,隻做測試使用。下載下傳docker-compose.standalone.yml 配置檔案并儲存為 docker-compose.yml,然後docker-compose up -d即可
-
使用k8s
使用 Kubernetes 包管理工具 Helm 添加 Milvus chart 倉庫
- 從源碼安裝
# Clone github repository.
$ git clone https://github.com/milvus-io/milvus.git
# Install third-party dependencies.
$ cd milvus/
$ ./scripts/install_deps.sh
# Compile Milvus.
$ make
-
使用dockerhub
參見這篇http://www.qishunwang.net/news_show_32024.aspx
這裡檢視GPU使用方式:https://milvus.io/cn/docs/v1.1.1/milvus_docker-gpu.md
這裡是一些注意事項:https://zhuanlan.zhihu.com/p/91444753
另外可以參考一下訓練營,benchmark測試也在裡面。給出的案例包括:
- 近似圖檔搜尋,使用YOLOv3進行物體檢測,ResNet-50提取特征向量
- 問答系統,使用BERT模型提取語言的特征向量
- 推薦系統,使用paddlepaddle的模型
- 視訊相似系統,使用VGG神經網絡
- 音頻相似系統,使用PANNs模型
2. 基本概念
2.1 索引選擇
Milvus 資料段存儲海量資料。在建立索引時,Milvus 為每個資料段單獨建立索引。
調用 create_index() 接口時,Milvus 會對該字段上的已有資料同步建立索引。每當後續插入的資料的大小達到系統配置的 index_file_size 時,Milvus 會為其在背景自動建立索引。
當插入的資料段少于 4096 行時,Milvus 不會為其建立索引。
衆所周知,建索引是一個比較消耗計算資源和時間的工作。當查詢任務和背景建索引任務并發時,Milvus 通常把計算資源優先配置設定給查詢任務,即使用者發起的任何查詢指令都會打斷背景正在執行的建索引任務。之後僅當使用者持續 5 秒不再發起查詢任務,Milvus 才會恢複執行背景建索引任務。此外,如果查詢指令指定的資料段尚未建成指定索引,Milvus 會直接在段内做全量搜尋。
- FLAT:FLAT 索引類型是指對向量進行原始檔案存儲。搜尋時,所有向量都會與目标向量進行距離計算和比較。FLAT 索引類型提供 100% 的檢索召回率。與其他索引相比,當查詢數量較少時,它是最有效的索引方法。metric_type是距離計算方式
- IVF(Inverted File,倒排檔案)是一種基于量化的索引類型。它通過聚類方法把空間裡的點劃分成 nlist 個單元。查詢時先把目标向量與所有單元的中心做距離比較,選出 nprobe 個最近單元。然後比較這些被選中單元裡的所有向量,得到最終的結果。IVF_FLAT 是最基礎的 IVF 索引,存儲在各個單元中的資料編碼與原始資料一緻。注意GPU 版 Milvus 在 nprobe > 2048 時由 GPU 查詢切換為 CPU 查詢。
- IVF_SQ8 是在 IVF 的基礎上對放入單元裡的每條向量做一次标量量化(Scalar Quantization)。标量量化會把原始向量的每個次元從 4 個位元組的浮點數轉為 1 個位元組的無符号整數,是以 IVF_SQ8 索引檔案占用的存儲空間遠小于 IVF_FLAT。但是,标量量化會導緻查詢時的精度損失。建索引和查詢參數同 IVF_FLAT
-
IVF_SQ8H 是一種優化查詢執行的 IVF_SQ8 索引類型。在不同的 nq(Number of queries,查詢數量)與系統參數 gpu_search_threshold 的關系下,查詢方式如下:nq ≥ gpu_search_threshold:整個查詢過程都在 GPU 上執行;nq < gpu_search_threshold:在 GPU 上執行在 IVF 裡尋找 nprobe 個最近單元的運算,在 CPU 上執行其它運算。建索引和查詢參數同 IVF_FLAT
IVF_PQ:PQ(Product Quantization,乘積量化)會将原來的高維向量空間均勻分解成 m 個低維向量空間的笛卡爾積,然後對分解得到的低維向量空間分别做矢量量化。最終每條向量會存儲在 m × nbits 個 bit 位裡。乘積量化能将全樣本的距離計算轉化為到各低維空間聚類中心的距離計算,進而大大降低算法的時間複雜度。IVF_PQ 是先對向量做乘積量化,然後進行 IVF 索引聚類。其索引檔案甚至可以比 IVF_SQ8 更小,不過同樣地也會導緻查詢時的精度損失。
- RNSG(Refined Navigating Spreading-out Graph)是一種基于圖的索引算法。它把全圖中心位置設為導航點,然後通過特定的選邊政策來控制每個點的出度(小于等于 out_degree),使得搜尋時既能減少記憶體使用,又能快速定位到目标位置附近。RNSG 的建圖流程如下為每個點精确尋找 knng 個最近鄰結點;以 knng 個最近鄰結點為基礎疊代至少 search_length 次,以選出 candidate_pool_size 個可能的最鄰近結點;在選出的 candidate_pool_size 個結點裡按擇邊政策建構每個點的出邊。RNSG 的查詢流程與建圖流程類似,以導航點為起點至少疊代 search_length 次以得到最終結果。
- HNSW(Hierarchical Small World Graph)是一種基于圖的索引算法。它會為一張圖按規則建成多層導航圖,并讓越上層的圖越稀疏,結點間的距離越遠;越下層的圖越稠密,結點間的距離越近。搜尋時從最上層開始,找到本層距離目标最近的結點後進入下一層再查找。如此疊代,快速逼近目标位置。為了提高性能,HNSW 限定了每層圖上結點的最大度數 M 。此外,建索引時可以用 efConstruction,查詢時可以用 ef 來指定搜尋範圍。
- Annoy(Approximate Nearest Neighbors Oh Yeah)是一種用超平面把高維空間分割成多個子空間,并把這些子空間以樹型結構存儲的索引方式。在查詢時,Annoy 會順着樹結構找到距離目标向量較近的一些子空間,然後比較這些子空間裡的所有向量(要求比較的向量數不少于 search_k 個)以獲得最終結果。顯然,當目标向量靠近某個子空間的邊緣時,有時需要大大增加搜尋的子空間數以獲得高召回率。是以,Annoy 會使用 n_trees 次不同的方法來劃分全空間,并同時搜尋所有劃分方法以減少目标向量總是處于子空間邊緣的機率。
2.2 存儲相關
建立集合時,Milvus 根據參數 index_file_size 控制資料段的大小。另外,Milvus 提供分區功能,你可以根據需要将資料劃分為多個分區。對資料的合理組織和劃分可以有效提高查詢性能。
-
資料段(segment)
為了能處理海量的資料,Milvus 會将資料分段,每段資料擁有數萬甚至數十萬個實體。每個資料段的資料又按照字段(field)分開,每個字段的資料單獨存為一個資料檔案。目前的版本中,實體僅包含一個 ID 字段和一個向量字段,是以每個資料段的資料檔案主要包括一個 UID 檔案以及一個原始向量資料檔案。
資料段的大小是由建立集合時的參數 index_file_size 來決定的,預設為 1024 MB,上限為 128 GB。
建立索引時,集合中的每個資料段依次建立索引,并将索引單獨存為一個檔案。索引檔案之間互相獨立。索引可以顯著地提高檢索性能。
-
分區(partition)
當一個集合累積了大量資料之後,查詢性能會逐漸下降。而某些場景隻需查詢集合中的部分資料,這時就要考慮把集合中的資料根據一定規則在實體存儲上分成多個部分。這種對集合資料的劃分就叫分區。每個分區可包含多個資料段。
分區以标簽(tag)作為辨別。插入向量資料時,你可以指定将資料插入到某個标簽對應的分區中。查詢向量資料時,你可以根據标簽來指定在某個分區的資料中進行查詢。Milvus 既支援對分區标簽的精确比對,也支援正規表達式比對。
每個集合的分區數量上限是 4096 個
3. 基本操作
3.1 資料插入
用戶端通過調用 insert 接口來插入資料,單次插入的資料量不能大于 256 MB。插入資料的流程如下:
服務端接收到插入請求後,将資料寫入預寫日志(WAL)。
當預寫日志成功記錄後,傳回插入操作。
将資料寫入可寫緩沖區(mutable buffer)。
每個集合都有獨立的可寫緩沖區。每個可寫緩沖區的容量上限是 128 MB。所有集合的可寫緩沖區總容量上限由系統參數 insert_buffer_size 決定,預設是 1 GB。
3.2 資料落盤
緩沖區中的資料落盤有三種觸發機制:
-
定時觸發
系統會定時觸發落盤任務。定時間隔由系統參數 auto_flush_interval 決定,預設是 1 秒。
落盤操作的流程如下:
系統開辟一塊新的可寫緩沖區,用于容納後續插入的資料。
系統将之前的可寫緩沖區設為隻讀(immutable buffer)。
系統把隻讀緩沖區的資料寫入磁盤,并将新資料段的描述資訊寫入中繼資料後端服務。
完成以上流程後,系統就成功建立了一個資料段(segment)。
-
用戶端觸發
由用戶端調用 flush 接口觸發落盤。
-
緩沖區達到上限觸發
累積資料達到可寫緩沖區的上限(128MB)會觸發落盤操作。
每個資料段的所有相關檔案都被存放在以段 ID 命名的檔案夾中,比如記錄實體 ID 的 UID 檔案、用于标記已被删除實體的 delete_docs 檔案,以及用于快速查找實體的布隆過濾器(bloom-filter)檔案。
3.3 資料合并
小資料段過多會導緻查詢性能低下。為了避免此問題,Milvus 會在需要的時候觸發背景段合并任務,即把小資料段合并成新的資料段,并删除小資料段、更新中繼資料。其中,新資料段的大小不低于 index_file_size。
合并操作的觸發時機如下:
啟動服務時
完成落盤任務後
建索引前
删除索引後
3.4 删除
-
删除集合:用戶端調用 drop_collection 接口來删除一個集合。
服務端接收到請求後,僅在中繼資料中把該集合(包括它的分區和段)标記為删除狀态。對于已标記為删除狀态的集合,将無法再對其進行任何新操作(比如插入和查詢)。
背景的清理任務将被标記為删除狀态的集合(包括它的分區和段)從中繼資料中删除,然後将該集合的資料檔案和檔案夾從磁盤上删除。如果在删除操作之前已經有對該集合的操作正在執行,背景清理任務不會删除正在使用的段,直到操作完成。
-
删除分區:用戶端調用 drop_partition 接口來删除一個分區。
服務端接收到請求後,僅在中繼資料中把該分區(包括它的段)标記為删除狀态。
背景清理任務按照删除集合的流程來删除該分區和中繼資料。
-
删除實體:Milvus 為每個資料段建立了一個 delete_docs 檔案,用來記錄被删除向量在段内的位置。
Milvus 使用布隆過濾器(bloom filter)來快速判斷一個實體 ID 是否可能存在于某個資料段中。是以,在每個資料段下都建立了一個名為 bloom_filter 的檔案。
删除實體的流程如下:
用戶端調用 delete_entity_by_id 接口删除集合中的實體。
服務端接收到請求後,執行以下操作删除實體:
如果該實體在插入緩沖區中,直接删除該實體。
否則,根據每個資料段的布隆過濾器判斷該實體所處的資料段,然後更新該資料段的 delete_docs 以及 bloom_filter 檔案。
-
資料段整理:查詢一個資料段時,Milvus 會将該資料段的實體資料以及 delete_docs 檔案讀入記憶體。雖然被删除的實體不參與計算,但它們也會被讀入記憶體。是以,一個資料段中被删除的實體越多,浪費的記憶體資源和磁盤空間越多。為了減少此類不必要的資源消耗,Milvus 提供了資料段整理(compact)的操作,流程如下:
用戶端調用 compact 接口。
服務端接收到請求後,根據 delete_docs 所記錄的資訊,将段内未被删除的實體寫入一個新的資料段,并把舊資料段标記為删除狀态。之後将由背景清理任務負責清理被标記為删除狀态的資料段。如果舊資料段已建立索引,新資料段産生之後會重建索引。
compact 操作會忽略被删除向量占比小于 10% 的資料段。
基準測試
從SIFT1B Dataset(10億)中提取資料進行性能測試。測試過程見https://github.com/milvus-io/bootcamp/blob/master/benchmark_test/lab1_sift1b_1m.md和https://github.com/milvus-io/bootcamp/blob/master/benchmark_test/lab2_sift1b_100m.md
5. python接口
5.1 連接配接
使用以下任意一種方法連接配接 Milvus 服務端:
milvus = Milvus(host='localhost', port='19530')
milvus = Milvus(uri='tcp://localhost:19530')
milvus.drop_collection(collection_name='test01')
5.2 建立集合、分區
建立集合:相當于建表
param = {'collection_name':'test01', 'dimension':256, 'index_file_size':1024, 'metric_type':MetricType.L2}
milvus.create_collection(param)
建立分區:你可以通過标簽将集合分割為若幹個分區,進而提高搜尋效率。每個分區實際上也是一個集合。
milvus.create_partition('test01', 'tag01')
milvus.drop_partition(collection_name='test01', partition_tag='tag01')
5.3 插入、删除向量
插入向量時,如果你不指定向量 ID,Milvus 自動為向量配置設定 ID。Milvus 中資料是分檔案存儲的,後續新增向量會存在新的資料檔案中。該檔案達到一定量後會自動觸發建立索引,生成一個新的索引檔案,不會影響之前已經建立過的索引。
import random
# Generate 20 vectors of 256 dimensions.
vectors = [[random.random() for _ in range(256)] for _ in range(20)]
milvus.insert(collection_name='test01', records=vectors)
vector_ids = [id for id in range(20)]
milvus.insert(collection_name='test01', records=vectors, ids=vector_ids)
milvus.insert('test01', vectors, partition_tag="tag01")
假設你的集合中存在以下向量 ID:ids = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19],你可以通過以下指令删除向量:
milvus.delete_entity_by_id(collection_name='test01', id_array=ids)
5.4 建立索引
create_index() 會指定該集合的索引類型,并同步為之前插入的資料建立索引,後續插入的資料在大小達到 index_file_size 時,索引會在背景自動建立。在實際生産環境中,如果是流式資料,建議在插入向量之前先建立索引,以便後續系統自動建立;如果是靜态資料,建議導入所有資料後再一次性建立索引。更多索引用法請參考https://github.com/milvus-io/pymilvus/blob/master/examples/old_style_example_index.py。
ivf_param = {'nlist': 16384}
# Create an index.
milvus.create_index('test01', IndexType.IVF_FLAT, ivf_param)
milvus.drop_index('test01')
删除索引後,集合再次使用預設索引類型 FLAT。
5.5 進行搜尋
對于不同的索引類型,搜尋所需參數也有差別。所有的搜尋參數都必須指派。詳細資訊請參考 Milvus 索引類型。
search_param = {'nprobe': 16}
# Create 5 vectors of 256 dimensions.
q_records = [[random.random() for _ in range(256)] for _ in range(5)]
milvus.search(collection_name='test01', query_records=q_records, top_k=2, params=search_param)
如果你不指定 partition_tags, Milvus 會在整個集合中搜尋。下面是在分區中查詢向量
# Create 5 vectors of 256 dimensions.
q_records = [[random.random() for _ in range(256)] for _ in range(5)]
milvus.search(collection_name='test01', query_records=q_records, top_k=1, partition_tags=['tag01'], params=search_param)
5.6 落盤、整理
milvus.flush(collection_name_array=['test01'])
milvus.compact(collection_name='test01', timeout=1)
5.9 綜合例子
# -*- coding: utf-8 -*-
# 導入相應的包
import numpy as np
from milvus import Milvus, MetricType
# 初始化一個Milvus類,以後所有的操作都是通過milvus來的
milvus = Milvus(host='localhost', port='19530')
# 向量個數
num_vec = 5000
# 向量次元
vec_dim = 768
# name
collection_name = "test_collection"
# 建立collection,可了解為mongo的collection
collection_param = {
'collection_name': collection_name,
'dimension': vec_dim,
'index_file_size': 32,
'metric_type': MetricType.IP # 使用内積作為路徑成本
}
milvus.create_collection(collection_param)
# 随機生成一批向量資料
# 支援ndarray,也支援list
vectors_array = np.random.rand(num_vec, vec_dim)
# 把向量添加到剛才建立的collection中
status, ids = milvus.insert(collection_name=collection_name, records=vectors_array) # 傳回 狀态和這一組向量的ID
milvus.flush([collection_name])
# 輸出統計資訊
print(milvus.get_collection_stats(collection_name))
# 建立查詢向量
query_vec_array = np.random.rand(1, vec_dim)
# 進行查詢,
status, results = milvus.search(collection_name=collection_name, query_records=query_vec_array, top_k=5)
print(status)
print(results)
# 如果不用可以删掉
status = milvus.drop_collection(collection_name)
# 斷開、關閉連接配接
milvus.close()
6. 圖形管理界面
如下代碼安裝控制台
docker pull milvusdb/milvus-em:v0.4.2
docker run -d -p 3000:80 milvusdb/milvus-em:v0.4.2
登入http://localhost:3000/,在url填入http://localhost:19121,就可以進行管理界面了