接口的定义应基于原理和需求,同时预留扩展和修改的灵活性。
1. 向量检索基本原理
- 首先给定一个向量集合
其中每个 是集合中的一个向量,检索的过程就是从 中找到 k个符合条件的向量
- 检索条件
输入一个查询向量 ,它可能是任何一个向量,不一定在上述集合中,但它们表示同一种类的含义,所以维度需要一致,故满足条件
其中d是向量维度,同样的存储的向量也在这个范围内
- 检索过程
给定查询向量 ,在向量集合 中找到使得
值最小的 k 个 ,其中 表示查询目标向量 与集合中的向量 之间的 p-范数距离
向量在代码里可以表示为
std::vector<float> x;
2. 需求分析
2.1 检索
- 需要返回k个最相似向量,k需要支持每次请求制定,并且后期可能会有一些扩展性的需求,例如查询的时候需要增加一些过滤条件,所以查询接口参数不能是一个vector,可以设计为一个结构体,vector是其中一个成员,后续要再增加参数在结构体里增加就好了
struct SearchRequest {
std::vector<float> x; // 输入的目标向量
int k; // 期望返回k个相似向量
// 其他扩展参数
};
- 返回的结果有k个向量,同时还可能需要知道每个向量和目标向量的相似度,即用一个分数衡量,还有向量的其他属性字段,故返回数据也需要设计一个结构体保存 首先定义单个向量结构体
struct Document {
std::vector<float> x; // 返回的相似向量
float similarity; // 和目标向量的相似度
// 其他扩展参数
};
- 多个向量结构体组合为返回结构体
struct SearchResponse {
std::vector<Document> vecs; // 返回的多个相似向量
// 其他扩展参数
};
这样就得到了向量检索接口定义
SearchResponse Search(const SearchRequest& request);
2.2 添加
- 请求参数添加操作应该和查询匹配,所有查询的字段都需要对应的添加方法,但不是所有的添加字段都需要被查询,例如设置过期时间,已经过期的向量不应该被查询到了。这里可以复用单个向量结构体
struct AddDocument {
Document vec;
// 其他扩展参数
};
- 添加是要添加一个向量集合,故应该支持批量批量添加
struct AddRequest {
std::vector<AddDocument> vecs;
// 其他扩展参数
};
- 添加有可能失败,需要返回错误信息和状态信息,定义状态结构体
struct Status {
int status_code;
std::string message;
// 其他扩展参数
};
组合起来就得到了添加向量的接口
Status Add(const AddRequest& request);
2.3 删除
- 删除的操作是需要先找到目标向量,然后执行删除操作,找不到则不执行。例如满足某个条件的向量删除这样的操作,可以使用检索接口查询到满足条件的向量,然后调用删除接口,故最简单的删除接口只需要目标向量参数即可
struct DeleteRequest {
std::vector<float> x; // 期望被删除的向量
// 其他扩展参数
};
类似的,删除也可能删除失败,例如存储里没有这个向量,需要返回错误信息和状态信息,这里可以复用状态结构体 于是得到了删除接口定义
Status Delete(const DeleteRequest& request);
3. 接口定义
每个向量实例都应该有对应的检索,添加,删除接口,可以将这些接口组合起来变成一个抽象类,就得到了向量索引的接口定义
struct Index {
// 检索
virtual SearchResponse Search(const SearchRequest& request) const = 0;
// 添加
virtual Status Add(const AddRequest& request) = 0;
// 删除
virtual Status Delete(const DeleteRequest& request) = 0;
}