从0到1手写向量索引(三):接口设计

80 阅读3分钟

接口的定义应基于原理和需求,同时预留扩展和修改的灵活性。

1. 向量检索基本原理

  • 首先给定一个向量集合
V={v1,v2,,vm}\mathbf{V} = \{\mathbf{v}_1, \mathbf{v}_2, \ldots, \mathbf{v}_m\}

其中每个 vi\mathbf{v}_i是集合中的一个向量,检索的过程就是从 V\mathbf{V} 中找到 k个符合条件的向量

  • 检索条件

输入一个查询向量 vq\mathbf{v}_q,它可能是任何一个向量,不一定在上述集合中,但它们表示同一种类的含义,所以维度需要一致,故满足条件

vqRd\mathbf{v}_q \in \mathbb{R}^d

其中d是向量维度,同样的存储的向量也在这个范围内

  • 检索过程

给定查询向量 vq\mathbf{v}_q,在向量集合 VV 中找到使得

d(vq,vi)=vqvipd(\mathbf{v}_q, \mathbf{v}_i) = \|\mathbf{v}_q - \mathbf{v}_i\|_p

值最小的 k 个 vi\mathbf{v}_i,其中 d(q,vi)d(\mathbf{q}, \mathbf{v}_i) 表示查询目标向量 vq\mathbf{v}_q与集合中的向量 vi\mathbf{v}_i之间的 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;
}