1. 说明
update_vectors用于更新索引中的dataset而不需要重新生成和训练的过程,从代码追踪的情况来看,update_vectors()函数只有在IndexIVFFlat索引中有实际的定义,所以只有该类型以及该类型索引的子类有update的功能。本篇文档也以IndexIVFFlat为基础进行分析和实验。
2. 过程分析
2.1 Python接口
上篇文章中说明,faiss的python接口定义都是在swigfaiss.py文件内,update_vectors也一样,如下所示:
# n: 要更新的数据集的大小,如n条列表
# idx: 要更新的数据集的标签列表
# v: 数据集的地址
def update_vectors(self, nv, idx, v):
return _swigfaiss.IndexIVFFlat_update_vectors(self, nv, idx, v)
上述方法的功能是在index中替换nv条数据向量,其中替换的位置由idx确定,数据向量地址为v。这里的idx是大小的列表,如n条向量组成的数据集,要完整替换所有向量,则idx为[0,1,2,…,n]。
update_vectors在faiss.py的替换方法会省去第一个参数nv,该值直接通过idx的大小获取,如下所示:
def replacement_update_vectors(self, keys, x):
n = keys.size
assert keys.shape == (n, )
assert x.shape == (n, self.d)
self.update_vectors_c(n, swig_ptr(keys), swig_ptr(x))
测试程序中关于update相关的代码如下:
# 生成更新的dataset
np.random.seed(1024)
ub = np.random.random((nb,d)).astype('float32')
ub[:,0] += np.arange(nb) * 1.0/nb
# 调用update_vectors
index.update_vectors(np.arange(nb), ub)
del ub
程序中现有numpy产生nb条维度为d的数据集,然后使用该数据集ub更新index.
2.2 C++源代码
源代码定义在IndexIVFFlat.cpp文件下
/*
* n: 要更新的向量个数
* new_ids: 新向量在x中的id列表
* x: 新的数据集地址
*/
void IndexIVFFlat::update_vectors (int n, idx_t *new_ids, const float *x)
{
// 1. 检查direct_map是否已经打开
FAISS_THROW_IF_NOT (maintain_direct_map);
// 2. 要更新的索引必须是训练过的
FAISS_THROW_IF_NOT (is_trained);
std::vector<idx_t> assign (n);
// 3. 找出最接近查询x的1个向量的索引集合,即n*1个向量
quantizer->assign (n, x, assign.data());
// 依次对每个向量进行替换操作
for (size_t i = 0; i < n; i++) {
idx_t id = new_ids[i];
FAISS_THROW_IF_NOT_MSG (0 <= id && id < ntotal,
"id to update out of range");
{ // remove old one
int64_t dm = direct_map[id]; // dm是索引中第id个向量的映射值
int64_t ofs = dm & 0xffffffff; // 取dm的低32bits, 向量的最大偏移
int64_t il = dm >> 32; // 取dm的高32bits, list_no
size_t l = invlists->list_size (il); // 获取编号为il的向量的大小, code_size
// 如果 ofs != l -1,说明direct_map与inverted_list中的向量对不上,那么需要调整invlists中的vectors
if (ofs != l - 1) { // move l - 1 to ofs
int64_t id2 = invlists->get_single_id (il, l - 1); // 生成新的向量id
direct_map[id2] = (il << 32) | ofs; // 将新的id的映射值替换原有映射值
invlists->update_entry (il, ofs, id2,
invlists->get_single_code (il, l - 1));
}
invlists->resize (il, l - 1);
}
{ // insert new one
int64_t il = assign[i];
size_t l = invlists->list_size (il);
int64_t dm = (il << 32) | l;
direct_map[id] = dm;
invlists->add_entry (il, id, (const uint8_t*)(x + i * d));
}
}
}
2.3 主要变量及成员
maintain_direct_map
maintain_direct_map是定义在struct IndexIVF中的一个bool型变量,用于描述是否开启映射以直接访问元素,若开启会使能reconstruct()函数。默认为"False",所以要使用update功能需在IndexIVF.cpp的IndexIVF构造函数中改为"True"。
direct_map
direct_map的数据定义为std::vector<idx_t> direct_map; 是一个用于存放索引中每个已添加向量的映射值的容器,该容器内元素为int64_t类型,其中高32bits表示向量在invertedList中的编号,低32bits表示该向量的大小。
当maintain_direct_map标志为设置时,在add过程中会将每个向量的映射地址push_back到本容器中。
invlists
invilists是父类IndexIVF的一个成员指针,类型为struct InvertedLists,其中存放了索引的实际数据。其包含的关键内容如下:
size_t nlist: number of possible key value
size_t code_size: code size per vector in bytes
std::vector<std:vector<idx_t>> ids : Inverted list for indexed
std::vector<std::vector<uint8_t>> codes : binary codes
ids和codes分别是一个二维的可变长容器,codes存放的是原dataset中的向量,这个比较好理解,一条向量就按行存放在一个vector<uint8_t>中其中向量编号由list_no即行号表示。
关键是这里的ids,既然是id号,那么我理解这里按行号与codes里面一一对应就好了,实际不然,这里仍然采用一个二维容器的方式,行号虽然对应,每一行里面存放一个offset用于表示codes的size大小。
3. 总结
因为IndexIVFFlat对原有向量集进行倒序划分,不是简单地一条一条平铺在索引中,所以在更新之前需要找到要更新的向量。这里采用最接近替换,即会先在index中搜索出与新向量集最接近的n个向量(n为新的向量集大小)。
要使用update功能,需要在创建索引时打开"maintain_direct_map"flag,这样会对每个向量进行映射以记录向量位置,方便后续替换。
IndexIVFFlat的方法主要存放在quantizer中,而数据主要保存在InvertedLists成员中。IndexIVFFlat的主要操作用于管理这两个类。
Note
- direct_map和invilists内ids的对应关系仍然没有完全理清,需要后续继续研究。
- invilists中ids和codes的关系,以及存放内容如何,需要继续研究
- invlists->update_entry和invlists->add_entry 的具体操作过程如何?