当前位置:   article > 正文

pgvector 源码分析

pgvector

简介

pgvector 库是一种用于计算向量距离的库,它的核心是提供了聚类索引,棸类索引使用的算法是 kmeans++(相对于 kmeans 最主要的区别是初始化起点的位置)。

pgvector 索引的存储结构

meta page → list page → entry page

请添加图片描述

insert page 是最后一页,当发生过 vacuum 后它会指向第一个回收了 tuple 的页,下一次插入 indextuple 从这个页开始

对pgvector 来说,keys 是点的向量值

索引创建过程

用户创建时指定计算距离的方法(ivfflat 的参数)与棸类中心的数量(lists)

CREATE INDEX ON t USING ivfflat (val) WITH (lists = 1);
  • 1

build 过程: 计算中心节点-》创建meta/list页-》填充entry页

计算中心节点

1、初始化中心节点

使用 kmeans++ 算法:假设要聚n类

  • 采集一定的样本点(50×n个),
  • 随机选1个点作启动点,计算每个点到这个中心点的距离
  • 以每个点到这个中心点的距离为权重,重新筛选出一个点作为新的初始中心(认为离最近棸类中心最远的点,最可能成为棸类中心,InitCenters),总共选出n个新初始中心

2、计算每个中心点相对最近中心点的距离,检查是否所有其它点到中心点x的距离小于中心点x与最近中心y之间的距离

3、对大于中心点间距离的点,重新找到离它最近的中心点,加入对应的簇

4、重新计算每个簇的中心点(计算所有簇内点每个维度的平均值作为第一维度的值,并对这个算出来的点的所有维度做统一正则化)

5、计算新中心与老中心的距离,把这个距离更新到每个点与中心的距离的上下界上(因为d(x,old) c- d(old,new) < d(x,new) < d(x,old) + d(old,new) )

6、循环至棸类中心不再变

填充entry页

1、起并行 worker 去扫描每个 tuple 做棸类(AssignTuples)

2、由 leader 去合并每个 worker 的 tap,合并的过程实际是对每个点属于的簇号作排序(tuplesort_performsort),未对簇内的 vector 做排序

3、为每个簇(list)插入实际的 indextuple(InsertTuples),index key 是 点向量值

worker的并行度

worker的并行度取决于用户对源表 set 的 rel_parallel_workers,用户未指定时,并行度为 log3(表的页数),并且要满足每个 worker 可以分到 32m 内存,用户未配置的情况下,为worker分配的总默认内存为64m(默认无法并行worker,因为master + worker至少要64m)。

sort 的执行

索引的scan

1、找每个聚簇中心,计算中心与目标点的距离,利用堆排序找出最近的 topN 中心,N取决于用户设置的ivfflat.probes。(GetScanLists)

2、去中心点簇对应的entry页,一页页找取点算距离,最后sort 一下(GetScanItems)

3、后面就可以一条条tuplesort_gettupleslot了(index 的数据不会删除,依赖于 postgresql 的 HOT 机制,真正删除发生在 vacuum 中)

vacuum

vacuum 时遍历 list/entry 页,看是否在 vacuum 普通表时已经 vacuum 过 tuple(lazy_record_dead_tuple),如果删除过,则收集这个 indextuple,最后执行页的vacuum并记 xlog,并更新下一个 insert page number(相当于回收tuple 位置)

这里有个细节,第一次 delete 一页的 tuple 时,要记一个 copy(GenericXLogRegisterBuffer),每次记录delete tuple位置时,要从这个 copy 中去记录 delete 的位置,最后一并执行 delete(pg给的接口是这样的)

cost 计算

page cost

计算随机访问 index page 的 cost:根据扫描的输出的点数(ivfflat.probes),估算扫描的 tuple 数,再根据 index 过滤条件,判断扫描出的 index tuple 数,进而算出扫描 index 页数,从而得到 random 访问的 index page 的 cost

如果是 join 的 inner 部分,总共访问 index page cost 要乘上 loop_count,然后再考虑 cache 带来的 cost 消除,这部分公式有点复杂,可以不看

∣ p a g e _ f e t c h ∣ = { m i n ( 2 ∗ p a g e ∗ t u p l e ∗ f r a c 2 ∗ p a g e + t u p l e ∗ f r a c , p a g e ) p a g e ≤ t u p l e 2 ∗ p a g e ∗ t u p l e ∗ f r a c / ( 2 ∗ p a g e + t u p l e ∗ f r a c ) p a g e > t u p l e , t u p l e ∗ f r a c < = 2 ∗ p a g e ∗ b u f 2 ∗ p a g e − b u f b u f + ( t u p l e ∗ f r a c − 2 ∗ p a g e ∗ b u f 2 ∗ p a g e − b u f ) ∗ ( p a g e − b u f ) p a g e p a g e > t u p l e , t u p l e ∗ f r a c > 2 ∗ p a g e ∗ b u f 2 ∗ p a g e − b u f |page\_fetch| =

{min(2pagetuplefrac2page+tuplefrac,page)pagetuple2pagetuplefrac/(2page+tuplefrac)page>tuple,tuplefrac<=2pagebuf2pagebufbuf+(tuplefrac2pagebuf2pagebuf)(pagebuf)pagepage>tuple,tuplefrac>2pagebuf2pagebuf
page_fetch= min(2page+tuplefrac2pagetuplefrac,page)2pagetuplefrac/(2page+tuplefrac)buf+page(tuplefrac2pagebuf2pagebuf)(pagebuf)pagetuplepage>tuple,tuplefrac<=2pagebuf2pagebufpage>tuple,tuplefrac>2pagebuf2pagebuf

index 默认是 random 访问的,random 访问的代价是 sequence 访问的四倍,而实际实现的访问是 seq 访问的,所以减去 seq 与 random 访问的差值。

这个过程中,如果点 vector 的维度很大,导致用 toast 类型来存 index(现象是算的 index page 访问数比 index 实际 page 数高,说明 index 是 toast 存储,因为 toast 存储不记录为 index 页),这个时候,要记得减去 toast 页算出的随机访问 cost。

cpu cost

  • where 条件本身 cost,order by 条件本身 cost
  • cpu_index_tuple_cost + cpu_operator_cost × (where 条件数 + order by 数) * tuple 数
本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/325432
推荐阅读
相关标签
  

闽ICP备14008679号