|
| 1 | +# 向量索引技术指南 |
| 2 | + |
| 3 | +## 概述 |
| 4 | +向量索引是加速大规模向量数据集检索的关键技术。Datalayers 支持多种向量索引类型,通过三层式架构实现高效的近似最近邻搜索。 |
| 5 | +使用向量索引会带来额外的构建索引开销、检索开销,同时可能会降低召回率(recall),因此需要根据具体场景选择合适的索引、以及配置合适的参数。 |
| 6 | + |
| 7 | +## 向量索引模型 |
| 8 | + |
| 9 | +Datalayers 使用三层式结构对主流的向量索引进行统一建模。 |
| 10 | + |
| 11 | + |
| 12 | + |
| 13 | +### IVF Model |
| 14 | + |
| 15 | +IVF(Inverted File)模型指质心 -> 向量组的倒排索引。在构建向量索引时,我们首先利用 K-Means 聚簇算法将向量数据集分成若干个向量组,每个向量组存在一个质心(Centroid)。IVF 模型维护了每个质心与其所在向量组的映射关系。 |
| 16 | + |
| 17 | +### Cell Index |
| 18 | + |
| 19 | +我们将每一个向量组称为一个 Cell,该名称延用知名向量检索库 Faiss 中的命名。Datalayers 支持给向量组内的向量构建索引,称为 Cell Index,以加速向量组内的近似最近邻搜索。例如,当我们使用 HNSW 向量索引时,我们会为每个向量组构建一个图索引,利用图数据结构来加速检索。 |
| 20 | + |
| 21 | +### Vector Store |
| 22 | + |
| 23 | +Vector Store 是向量的存储抽象。为了节省存储空间,我们支持对向量进行量化。量化指将原始向量投影到另一个更紧凑的向量空间,以达到数据压缩的目的。主流的量化算法包括乘积量化(PQ)、标量量化(SQ)等,它们均是有损、不可逆的量化算法,因此在搜索时会降低召回率。 |
| 24 | + |
| 25 | +## 基于索引的向量检索 |
| 26 | + |
| 27 | +给定查询向量 Q 以及 top-K 中的 K,基于索引的三层式结构,Datalayers 的向量检索分成如下步骤: |
| 28 | + |
| 29 | +1. 模糊搜索:我们首先访问 IVF Model,计算 Q 与所有质心的距离,并取最近的 P 个质心所对应的向量组。 |
| 30 | +2. 精确搜索:对于每个向量组,我们使用 Cell Index 加速向量组内的近似最近邻搜索,每个质心得到 top-N 个与 Q 距离最近的向量。 |
| 31 | +3. 精炼:考虑到向量索引会降低召回率,对于搜索得到的 `P * N` 个向量,计算它们与 Q 的距离,得到最终的 top-K 个距离最近的向量。其中 `N / K` 称为 `refine_factor`,表示为了补偿召回率,我们在精确搜索时每个向量组额外检索了多少个向量。这个步骤称为精炼(Refine)。 |
| 32 | + |
| 33 | +## 索引类型 |
| 34 | + |
| 35 | +| | IVF Model | Cell Index | Vector Store | 是否已支持 | |
| 36 | +| :----- | :----------: | :---------: | :----------: | :-----: | |
| 37 | +| FLAT | Cell 个数固定为 1 | FLAT | FLAT | 是 | |
| 38 | +| IVF_FLAT | 支持配置 Cell 个数 | FLAT | FLAT | 是 | |
| 39 | +| IVF_PQ | 支持配置 Cell 个数 | FLAT | PQ | 是 | |
| 40 | +| IVF_SQ | 支持配置 Cell 个数 | FLAT | SQ | 否 | |
| 41 | +| IVF_RQ | 支持配置 Cell 个数 | FLAT | RQ | 否 | |
| 42 | +| HNSW | Cell 个数固定为 1 | HNSW | FLAT | 否 | |
| 43 | +| IVF_HNSW | 支持配置 Cell 个数 | HNSW | FLAT | 否 | |
| 44 | +| IVF_HNSW_PQ | 支持配置 Cell 个数 | HNSW | PQ | 否 | |
| 45 | +| IVF_HNSW_SQ | 支持配置 Cell 个数 | HNSW | SQ | 否 | |
| 46 | +| IVF_HNSW_RQ | 支持配置 Cell 个数 | HNSW | RQ | 否 | |
| 47 | + |
| 48 | +注: |
| 49 | + |
| 50 | +- Cell Index 为 FLAT,表示向量组内的搜索退回到平搜(Flat Search),即搜索所有向量。 |
| 51 | +- Cell Index 为 HNSW,表示使用 HNSW(Hierarchical Navigable Small Worlds)索引加速向量组内的搜索。 |
| 52 | +- Vector Store 为 FLAT,表示不使用任何量化算法,而存储原始、未经压缩的向量。 |
| 53 | +- PQ 指 Product Quantization,即乘积量化。 |
| 54 | +- SQ 指 Scalar Quantization,即标量量化。 |
| 55 | +- RQ 指 RaBit Quantization。 |
| 56 | + |
| 57 | +## 示例 |
| 58 | + |
| 59 | +我们提供了一个 Python 脚本,展示如何使用向量索引来加速向量检索。这个脚本执行的步骤如下: |
| 60 | + |
| 61 | +1. 创建数据库 `demo`。 |
| 62 | +2. 创建表 `t`。表中包含一个向量列 `embed`,维度为 64。同时为该列指定 IVF_PQ 索引,同时设置构建索引的距离函数为 L2。 |
| 63 | +3. 写入 5000 条随机数据。 |
| 64 | +4. Flush 数据。 |
| 65 | +5. 等待索引构建完成,默认等待 15 秒。 |
| 66 | +6. 使用随机向量,执行向量检索。 |
| 67 | + |
| 68 | +``` python |
| 69 | +import http |
| 70 | +import json |
| 71 | +import random |
| 72 | +import time |
| 73 | +from http.client import HTTPConnection |
| 74 | + |
| 75 | + |
| 76 | +def main(): |
| 77 | + host = "0.0.0.0" |
| 78 | + port = 8361 |
| 79 | + url = "http://{}:{}/api/v1/sql".format(host, port) |
| 80 | + headers = { |
| 81 | + "Content-Type": "application/binary", |
| 82 | + "Authorization": "Basic YWRtaW46cHVibGlj" |
| 83 | + } |
| 84 | + conn = http.client.HTTPConnection(host=host, port=port) |
| 85 | + |
| 86 | + # Create database `demo`. |
| 87 | + sql = "CREATE DATABASE IF NOT EXISTS demo;" |
| 88 | + conn.request(method="POST", url=url, headers=headers, body=sql) |
| 89 | + print_response("创建数据库", conn) |
| 90 | + |
| 91 | + # Create table `t`. |
| 92 | + sql = ''' |
| 93 | + CREATE TABLE IF NOT EXISTS `demo`.`t` ( |
| 94 | + `ts` TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| 95 | + `sid` INT32 NOT NULL, |
| 96 | + `value` REAL, |
| 97 | + `flag` INT8, |
| 98 | + `embed` VECTOR(64), |
| 99 | + TIMESTAMP KEY(`ts`), |
| 100 | + VECTOR INDEX `my_vector_index`(`embed`) WITH (TYPE=IVF_PQ, DISTANCE=L2) |
| 101 | + ) |
| 102 | + PARTITION BY HASH (`sid`) PARTITIONS 1 |
| 103 | + ENGINE=TimeSeries |
| 104 | + WITH ( |
| 105 | + MEMTABLE_SIZE=1024MB, |
| 106 | + STORAGE_TYPE=LOCAL, |
| 107 | + UPDATE_MODE=APPEND |
| 108 | + ); |
| 109 | + ''' |
| 110 | + conn.request(method="POST", url=url, headers=headers, body=sql) |
| 111 | + print_response("创建表", conn) |
| 112 | + |
| 113 | + # 分批插入数据 |
| 114 | + insert_data( |
| 115 | + conn, url, headers, total_rows=5000, batch_size=1000) |
| 116 | + |
| 117 | + # Flush 数据 |
| 118 | + flush_data(conn, url, headers) |
| 119 | + |
| 120 | + # 等待索引构建完成 |
| 121 | + print("等待索引构建完成...") |
| 122 | + time.sleep(15) |
| 123 | + |
| 124 | + # Vector search with random query vector |
| 125 | + query_vector = generate_random_vector(64) |
| 126 | + sql = f"SELECT value FROM demo.t WHERE sid = 1 ORDER BY l2_distance(embed, {query_vector}) LIMIT 1" |
| 127 | + print(f"执行向量检索: {sql}") |
| 128 | + conn.request(method="POST", url=url, headers=headers, body=sql) |
| 129 | + print_query_result(conn) |
| 130 | + |
| 131 | + |
| 132 | +def generate_random_vector(dim: int) -> str: |
| 133 | + """生成随机向量字符串表示""" |
| 134 | + vector = [round(random.uniform(-1.0, 1.0), 6) for _ in range(dim)] |
| 135 | + return "[" + ", ".join(map(str, vector)) + "]" |
| 136 | + |
| 137 | + |
| 138 | +def insert_data(conn: HTTPConnection, url: str, headers: dict, total_rows: int, batch_size: int): |
| 139 | + """分批插入数据""" |
| 140 | + num_batches = total_rows // batch_size |
| 141 | + |
| 142 | + print(f"开始插入 {total_rows} 条数据,分 {num_batches} 批次,每批 {batch_size} 条") |
| 143 | + |
| 144 | + for batch in range(num_batches): |
| 145 | + print(f"插入第 {batch + 1}/{num_batches} 批次...") |
| 146 | + |
| 147 | + values = [] |
| 148 | + for _ in range(batch_size): |
| 149 | + sid = random.randint(0, 5000) |
| 150 | + value = round(random.uniform(0.0, 100.0), 2) |
| 151 | + flag = random.randint(0, 1) |
| 152 | + embed = generate_random_vector(64) |
| 153 | + |
| 154 | + values.append(f"({sid}, {value}, {flag}, {embed})") |
| 155 | + |
| 156 | + sql = f"INSERT INTO demo.t (sid, value, flag, embed) VALUES {', '.join(values)}" |
| 157 | + conn.request(method="POST", url=url, headers=headers, body=sql) |
| 158 | + |
| 159 | + response = conn.getresponse() |
| 160 | + |
| 161 | + if response.status == 200: |
| 162 | + print(f"✓ 第 {batch + 1} 批次插入成功") |
| 163 | + else: |
| 164 | + print(f"✗ 第 {batch + 1} 批次插入失败: {response.status} {response.reason}") |
| 165 | + |
| 166 | + response.read() |
| 167 | + |
| 168 | + time.sleep(0.5) |
| 169 | + |
| 170 | + |
| 171 | +def flush_data(conn: HTTPConnection, url: str, headers: dict): |
| 172 | + print("正在 Flush 数据") |
| 173 | + sql = "FLUSH TABLE demo.t SYNC" |
| 174 | + conn.request(method="POST", url=url, headers=headers, body=sql) |
| 175 | + |
| 176 | + response = conn.getresponse() |
| 177 | + |
| 178 | + if response.status == 200: |
| 179 | + print(f"Flush 数据成功") |
| 180 | + else: |
| 181 | + print(f"Flush 数据失败: {response.status} {response.reason}") |
| 182 | + |
| 183 | + response.read() |
| 184 | + |
| 185 | + |
| 186 | +def print_response(msg: str, conn: HTTPConnection): |
| 187 | + with conn.getresponse() as response: |
| 188 | + if response.status == 200: |
| 189 | + print(f"{msg} 成功") |
| 190 | + else: |
| 191 | + print(f"{msg} 失败: {response.status} {response.reason}") |
| 192 | + |
| 193 | + response.read() |
| 194 | + |
| 195 | +def print_query_result(conn: HTTPConnection): |
| 196 | + print("检索结果:") |
| 197 | + |
| 198 | + with conn.getresponse() as response: |
| 199 | + data = response.read().decode('utf-8') |
| 200 | + obj = json.loads(data) |
| 201 | + |
| 202 | + columns = obj['result']['columns'] |
| 203 | + rows = obj['result']['values'] |
| 204 | + |
| 205 | + print(columns) |
| 206 | + for row in rows: |
| 207 | + print(row) |
| 208 | + |
| 209 | +if __name__ == "__main__": |
| 210 | + main() |
| 211 | +``` |
| 212 | + |
| 213 | +在测试机器上,为 5000 条数据构建向量索引大致需要 2 秒。为了确认在您的机器上索引已经构建完成,您可以通过执行 `SHOW TASKS` 命令检视当前正在执行、被挂起的索引构建任务。如果 `build_index` 任务的 `running` 和 `pending` 数量为 0,说明所有索引已经构建完毕。 |
| 214 | + |
| 215 | +``` sql |
| 216 | +> show tasks |
| 217 | + |
| 218 | ++-------------+---------+---------+-------------------+-------------+----------------------------------------------------+ |
| 219 | +| type | running | pending | concurrence_limit | queue_limit | description | |
| 220 | ++-------------+---------+---------+-------------------+-------------+----------------------------------------------------+ |
| 221 | +| build_index | 1 | 1 | 1 | 10000 | Build index. | |
| 222 | +| compact | 0 | 0 | 3 | 10000 | Compaction, TTL clean. | |
| 223 | +| flush | 0 | 0 | 10 | 10000 | Flush memtable into file. | |
| 224 | +| gc | 0 | 0 | 100 | 10000 | Delete files when drop table, truncate table, etc. | |
| 225 | +| timer | 0 | 18 | 0 | 0 | Delayed tasks executed at specified time | |
| 226 | +| workflow | 0 | 0 | 10 | 10000 | Table DDL operation. | |
| 227 | ++-------------+---------+---------+-------------------+-------------+----------------------------------------------------+ |
| 228 | +``` |
| 229 | + |
| 230 | +## 注意事项 |
| 231 | + |
| 232 | +- 构建索引时的距离函数与搜索时的距离函数必须一致,否则无法触发向量索引。 |
| 233 | +- 目前仅支持为 32 维以上的向量列构建向量索引。 |
| 234 | +- 目前不支持为索引配置构建参数、搜索参数,仅支持使用内部默认参数。 |
0 commit comments