搜索或推荐场景,需要将非结构化的物料(媒资)结构化,也即提取特征,然后将特征存储向量数据库,从而实现海量数据快速检索功能。
当前,开源市场比较火的搜索引擎有Faiss,但Faiss更类似于es的lucene,需要上层解决分布式水平扩容、数据一致性、高可用等问题。所以对于数据量大,要求高可用等架构场景,使用milvus。
Lucene——Faiss
Milvus——Elasticsearch
专注向量检索框架,解决数据一致性,分布式水平扩容等问题
设计思想:
做一个数据库,而不是引擎。如何做管理、计费、可视化,数据迁移。数据库不仅要提供传统的增删改查能力,还提供数据转换、迁移、多租户加密管理、计费、限流、可视化、备份快找等更加多样的服务
各个点的具体坐标数值对结果会有比较大的影响。在推荐系统场景下,欧式距离一般用于需要从维度的数值大小中体现差异的相关度分析
例如以登陆次数和平均观看时长作为特征时,余弦相似度会认为(1,10)、(10,100)两个用户距离很近,但显然这两个用户的活跃度是有着很大差异的,(10,100)这个用户的价值更高,此时我们更关注数值绝对差异,应当使用欧氏距离
跟欧式距离的差别主要在于它对具体数值的差异并不敏感。一句话总结就是,虽然数值上确实有差异,但是两者的x,y轴相对应的数值的分值之差保持相近,所以两者的相似度还是很高。余弦相似度更倾向于衡量两者在方向趋势上的差异,余弦相似度更多的适用于使用用户对内容评分来区分兴趣的相似度和差异
也就是大家常说的暴力搜索,这种方式是典型的牺牲性能和成本换取准确性,是唯一可以实现 100% 召回率的方式,同时可以较好地使用显卡等异构硬件加速。
基于 locality sensitive hashing 将数据分到不同的哈希桶中。这种方式实现简单,性能较高,但是召回率不够理想。
代表是 KDTree 或者 BallTree,通过将高维空间进行分割,并在检索时通过剪枝来减少搜索的数据量,这种方式性能不高,尤其是在维度较高时性能不理想。
通过 k-means 算法找到数据的一组中心点,并在查询时利用查询向量和中心点距离选择部分桶进行查询。倒排这一类又拥有很多的变种,比如可以通过 PCA 将数据进行降维,进行标量量化,或者通过乘积量化 PQ 将数据降精度,这些都有助于减少系统的内存使用和单次数据计算量。
是一种基于图存储的数据结构,这种索引基于一种朴素的假设,通过在构建图连接相邻的友点,然后在查询时不断寻找距离更近的节点实现局部最优。在 NSW 的基础上,HNSW(Navigable Small World)图借鉴了跳表的机制,通过层状结构构建了快速通道,提升了查询效率。
hnsw参考:https://www.pinecone.io/learn/series/faiss/hnsw/
k-means动态算法:
https://www.naftaliharris.com/blog/visualizing-k-means-clustering/
dbscan动态算法:
https://www.naftaliharris.com/blog/visualizing-dbscan-clustering/
相比较其他向量数据库,Milvus:
https://zhuanlan.zhihu.com/p/364923722
https://www.jianshu.com/p/43cc19426113
database:类比关系数据库database, 2.2.9之后支持;为多租户,一个租户一个database设计
collection:类比关系数据库表
Entity: 是传统数据库里面“一行”的概念
Field:字段
创建一个collection
# We're going to create a collection with 3 fields.
# +-+------------+------------+------------------+------------------------------+
# | | field name | field type | other attributes | field description |
# +-+------------+------------+------------------+------------------------------+
# |1| "pk" | VarChar | is_primary=True | "primary field" |
# | | | | auto_id=False | |
# +-+------------+------------+------------------+------------------------------+
# |2| "random" | Double | | "a double field" |
# +-+------------+------------+------------------+------------------------------+
# |3|"embeddings"| FloatVector| dim=8 | "float vector with dim 8" |
# +-+------------+------------+------------------+------------------------------+
fields = [
FieldSchema(name="pk", dtype=DataType.VARCHAR, is_primary=True, auto_id=False, max_length=100),
FieldSchema(name="random", dtype=DataType.DOUBLE),
FieldSchema(name="embeddings", dtype=DataType.FLOAT_VECTOR, dim=dim)
]
schema = CollectionSchema(fields, "hello_milvus is the simplest demo to introduce the APIs")
print(fmt.format("Create collection `hello_milvus`"))
hello_milvus = Collection("hello_milvus", schema, consistency_level="Strong")
参考:
https://raw.githubusercontent.com/milvus-io/pymilvus/master/examples/hello_milvus.py
DML:任何传入的插入/删除请求都根据主键的哈希值被路由到shard,默认情况下是两个 Shard,推荐 Shard 的规模做到 Data Node 的两到三倍。
DDL:仅分享一个shard。
一个 proxy 都会对应所有的 VChannel
多个 V channel 可以对应到同一个 PChannel
一个data node/query node对应多个PChannel
collection 级别的 VChannel可以很多,而且不同 collection 之间也可以共用 PChannel;从而利用消息系统高并发特性提高吞吐量。
https://zhuanlan.zhihu.com/p/517553501?utm_id=0
Segment 在内存中的状态有 3 种,分别是 growing、sealed 和 flushed。 Growing:当新建了一个 segment 时就是 growing 的状态,它在一个可分配的状态。 Sealed:Segment 已经被关闭了,它的空间不可以再往外分配。 Flushed:Segment 已经被写入磁盘
Growing segment 内部的空间可以分为三部份:
Sealed segment 表示这个 segment 的空间不可以再进行分配。有几种条件可以 seal 一个 segment:
insert_log
bucketName/file/insert_log/ collectionId/ partitionId/ segmentId/ field_ids
featureId: 100
libId: 101
feature: 102
index_files
bucketName/file/index_files/ index build id/IndexTaskVersion/ partitionId/ segmentId/index file
delta_log
bucketName/file/delta_log/ collectionId/ partitionId/ segmentId/unique ID
stats_log
bucketName/file/stats_log/ collectionId/ partitionId/ segmentId/field_id
@TODO
Binlog 里面分成了很多 event,每个 event 都会有两部分,一个是 event header 和 event data。Event header 存的就是一些元信息,比如说创建时间、写入节点 ID、event length 和 NextPosition(下个 event 的偏移量)
INSERT_EVENT 的 event data 固定的部分主要有三个,StartTimestamp、EndTimestamp 和 reserved。Reserved 也就是保留了一部分空间来扩展这个 fixed part。 Variable part 存的就是实际的插入数据。我们把这个数据序列化成一个 parquet 的形式存到这个文件里
https://zhuanlan.zhihu.com/p/486971488
https://milvus.io/docs/limitations.md
主要内容:1)将segment 由growing改为sealed状态,数据不可再写入 2)将数据持久化到Object storage
两个问题:
https://github.com/milvus-io/milvus/blob/master/docs/design_docs/20211109-milvus_flush_collections.md
参考:https://zhuanlan.zhihu.com/p/517553501?utm_id=0
索引按照segment进行构建(索引异步删除逻辑类似)
参考:
https://milvus.io/docs/data_processing.md
https://github.com/milvus-io/milvus/blob/master/docs/design_docs/20211227-milvus_create_index.md
具体流程:
对于 Knowhere,不区分训练数据和查询数据。对于每一个 segment,Knowhere 都是用该 segment 的全量数据做训练,再基于该训练结果插入全量数据构建索引
milvus的索引内存数据,存储在query node中,当query扩容(或缩容)时,由于索引文件持久化在对象存储中,query coord会进行重新分配,从而拥有水平扩(缩)容的能力
插入的数据,只要写入消息系统,就不会丢失;索引数据、插入日志也持久化到了对象存储中
Milvus每一条 insert message 中都有分配了一个时间戳,如果 service time 大于 query message 中的 guarantee timestamp,那么就会执行这个查询;从而通过配置,达到不同级别的数据一致性
如何使用 Milvus 向量数据库实现实时查询
Milvus针对一个segment构建一个索引,最后proxy合并检索结果,默认一个segment 1g,从而避免单个索引过大导致效果问题
# Add Milvus Helm repository.
$ helm repo add milvus https://milvus-io.github.io/milvus-helm/
# Update charts locally.
$ helm repo update
# show chart
helm show chart milvus/milvus
# pull chart
helm pull milvus/milvus
https://milvus.io/docs/monitor.md
参考
https://zhuanlan.zhihu.com/p/473617910
https://zhuanlan.zhihu.com/p/491030589
https://zhuanlan.zhihu.com/p/500551056
https://zhuanlan.zhihu.com/p/486703915
https://zhuanlan.zhihu.com/p/486971488
https://zhuanlan.zhihu.com/p/502880424
https://zhuanlan.zhihu.com/p/506698319
https://www.modb.pro/db/590924