分区键特性
什么是分区键
在分布式数据库中,一张表可能会被划分为多个分区或者分片,实现分布式扩展。在VectorDB中,也支持表的分区机制,目前支持基于哈希算法的分区机制。分区键(Partition Key)就是用来确定一行归属于哪个分区的关键所在。当写入一条新的行时,代理节点根据分区键的取值来计算出一个哈希值,然后将哈希值对总的分区数量求模,从而得到该行数据的目标分区。因此,在写入一行数据时,分区键的取值是不可缺失的。
在VectorDB中,分区键既可以是主键字段,也可以是非主键字段。如果客户希望让某个非主键做分区键,那么需要在建表接口参数中显式指定。若建表接口参数中没有显式指定分区键,那么默认将第一个主键字段作为分区键。
分区键的使用场景
通过设定分区键,客户可以有意识地将一张表中的不同归属的数据划分到不同的分区,让有相同归属的数据进入同一个分区,大大降低后续操作的代价。下面列举一些可能的场景:
- 场景一:不同组织的数据的分类集中
例如在一个企业中,不同的部门都可能产生相同类型的数据,例如技术部、运营部、法务部、行政部等,当这些属于不同部门的数据都存储到一张表中时,通过在表的Schema中引入一个代表部门枚举的字段,例如Department
,然后将该字段设置为表的分区键。后续写入数据时都填上该字段的值,实现将同一个部门的数据写入到同一个分区中,避免过度分散。后续不仅是在标量查询还是向量检索,都能让开销尽量降低。以向量检索为例,如果我们在检索的过滤器(Filter)中,指定部门属性,例如Department='运营部'
,那么VectorDB在检索时,能够确保只在运营部所有的数据中进行检索,检索请求只需要发送给该部门数据所在的分区执行。如果没有设定部门字段作为分区键,那么某个部门的数据可能分散在非常多的分区中,做向量检索时可能退化为多个分区的MPP检索,大大浪费计算资源,可能还会导致效果下降。
- 场景二:不同C端用户数据的分类集中
例如在一些面向C端客户的业务中,虽然单一C端用户的数据并不大,但表的总数据量可能比较大,需要建立多个分区来实现扩展。在这种场景下,可以在该表中建立一个例如UserId
的字段,并将其设定为分区键。通过这种方式,保证了每个C端用户的数据总是写入到同一个分区中。在做后续的各类查询检索操作时,相关请求只需要发送个该用户数据所在的分区执行即可,避免了MPP执行以及由此带来的开销。
分区键的使用限制
分区键在使用中,有如下限制:
- 独立分区键可能导致不同分片之间存在主键冲突的现象:在分区键不是主键的场景下,假定两行数据,主键值一样,但是分区键值不一样,那么这两条数据最后可能写入到不同的分片中,且都能够写入成功。建议您在使用独立分区键时,对主键的赋值更加谨慎一些。如果分区键是主键字段的话,那么可以保证主键全表唯一。
- 分区键不支持的数据类型:考虑到哈希值的计算方法,分区键字段不支持
BOOL
、FLOAT
、DOUBLE
和FLOAT_VECTOR
类型。 - 分区键仅支持1个字段:同样也是考虑到哈希值的计算方法,分区键仅支持1个字段。
- 写入数据时是否需要指定分区键值:只有一种情况,即,当分区键是主键中的一个字段,并且该字段具备AutoIncrement属性时,写入的数据中才可以不指定分区键值,否则都需要指定分区键值。
- 查询/更新/删除操作请求中需要提供分区键值:此处的查询(Query)特指标量查询。除非该表的分区键就是主键中的一个字段,否则需要在查询/更新和删除操作请求中显式提供分区键值,否则代理节点无法计算该请求应当发送给哪个分区。
- 检索操作请求中可以不提供分区键值:此处的检索(Search)特指向量检索。若在检索请求中指定了分区键值,则该检索请求只发送给目标分区;若不指定,则该检索请求退化为针对该表所有分区的MPP检索操作。
使用示例
我们以上述场景一为例,通过Python SDK展示一下分区键的应用。
- 创建一个莫愁客户端对象。
import time
import json
import random
import pymochow
import logging
from pymochow.configuration import Configuration
from pymochow.auth.bce_credentials import BceCredentials
from pymochow.exception import ClientError, ServerError
from pymochow.model.schema import Schema, Field, SecondaryIndex, VectorIndex, HNSWParams
from pymochow.model.enum import FieldType, IndexType, MetricType, ServerErrCode
from pymochow.model.enum import TableState, IndexState
from pymochow.model.table import Partition, Row, AnnSearch, HNSWSearchParams
logging.basicConfig(filename='documentinsight.log', level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
if __name__ == "__main__":
account = '$您的账户名称'
api_key = '$您的账户API密钥'
endpoint = '$您的实例访问端点' # 例如:'http://127.0.0.1:5287'
db_name = 'DocumentInsight'
table_name = 'DocumentContents'
# 根据配置创建一个MochowClient对象
config = Configuration(credentials=BceCredentials(account, api_key), endpoint=endpoint)
mochow_client = pymochow.MochowClient(config)
- 创建一个带分区键的表。
# 建库
db = mochow_client.create_database(db_name);
# 建表
# 该表的主键字段是'DocId',分区键字段是'Department',并针对'Vector'字段建立了一个HNSW索引
fields = []
fields.append(Field("DocId", FieldType.UUID, primary_key=True, not_null=True))
fields.append(Field("URL", FieldType.STRING, not_null=True))
fields.append(Field("Department", FieldType.STRING, partition_key=True, not_null=True))
fields.append(Field("Content", FieldType.TEXT, not_null=True))
fields.append(Field("Vector", FieldType.FLOAT_VECTOR, dimension=2, not_null=True))
indexes = []
indexes.append(VectorIndex(index_name="Vector_Idx", index_type=IndexType.HNSW,
field="Vector", metric_type=MetricType.L2,
params=HNSWParams(m=32, efconstruction=200)))
# 执行建表操作
# 该表建立了2个分区
table = db.create_table(
table_name=table_name,
replication=1,
partition=Partition(partition_num=2), # 2个分区
schema=Schema(fields=fields, indexes=indexes)
)
# 等待建表完成
while True:
time.sleep(1)
table = db.describe_table(table_name)
if table.state == TableState.NORMAL:
break
- 写入数条数据到上述表中。
# 插入数据
rows = [
Row(DocId="37a9523d-3afb-f576-91ad-7075d6e3c8eb",
URL="bos://hellocompany/tech/HelloWorld产品技术详解.pdf", Department="技术部",
Content="HelloWorld产品基于业界最先进的大模型技术构建,可以用于知识管理、智能对话和RAG类应用...",
Vector=[0.01438954, 0.38932824]),
Row(DocId="867b0da1-1f33-022e-9d9f-0d885bc7f0d0",
URL="bos://hellocompany/product/HelloWorld产品规格说明书.pdf", Department="技术部",
Content="HelloWorld产品具备单机版和分布式版,同时支持公有云和私有化部署,最大可支持PB级数据管理...",
Vector=[0.13358429, 0.43959881]),
Row(DocId="bfef64f1-c965-c0ff-bb99-c2ac7771c2f6",
URL="bos://hellocompany/finance/HelloWorld产品营收分析.docx", Department="运营部",
Content="HelloWorld产品去年总营收123.88亿,毛利率60%,净利润38%,下面是产品的详细财务分析...",
Vector=[0.74389511, 0.02371958])
]
table.insert(rows=rows)
- 构建向量索引并等待构建完成。
# 构建向量索引
index_name = "Vector_Idx"
table.rebuild_index(index_name)
while True:
time.sleep(1)
index = table.describe_index(index_name)
if index.state == IndexState.NORMAL:
break
- 查询数据,需指定主键值和分区键值。
# 标量查询
primaryKey = {}
primaryKey['DocId'] = "37a9523d-3afb-f576-91ad-7075d6e3c8eb"
partitionKey = {}
partitionKey['Department'] = "技术部"
results = table.query(primary_key=primaryKey, partition_key=partitionKey)
print("Query result: {}".format(results))
执行完成之后,我们会看到控制台输出了符合预期的目标数据:
Query result: {metadata:{content__length:u'305',content__type:u'application/json',request_id:u'f6468ed1-60ee-448d-9eec-fc403a6660de'},row:{'DocId': '37a9523d-3afb-f576-91ad-7075d6e3c8eb', 'URL': 'bos://hellocompany/tech/HelloWorld产品技术详解.pdf', 'Department': '技术部', 'Content': 'HelloWorld产品基于业界最先进的大模型技术构建,可以用于知识管理、智能对话和RAG类应用...'},code:0,msg:u'Success'}
- 向量检索,在Filter中指定标量过滤条件,这里我们指定
Department == '运营部'
作为过滤条件,并要求检索Top2,但是我们查询的目标向量其实是离技术部的数据更接近的,查询代码如下:
# 向量检索
vectorFloats = [0.12123456, 0.41840295]
anns = AnnSearch(vector_field="Vector", vector_floats=vectorFloats,
filter="Department == '运营部'",
params=HNSWSearchParams(ef=200, limit=2))
result = table.search(anns=anns, retrieve_vector=True)
print("Search result: {}".format(result))
执行完成之后,我们会看到控制台输出了如下结果:
Search result: {metadata:{content__length:u'434',content__type:u'application/json',request_id:u'8e8b5114-7880-4221-9c02-b90f724a8a34'},rows:[{'row': {'DocId': 'bfef64f1-c965-c0ff-bb99-c2ac7771c2f6', 'Vector': [0.7438951134681702, 0.023719580844044685], 'URL': 'bos://hellocompany/finance/HelloWorld产品营收分析.docx', 'Department': '运营部', 'Content': 'HelloWorld产品去年总营收123.88亿,毛利率60%,净利润38%,下面是产品的详细财务分析...'}, 'distance': 0.5434811115264893, 'score': 1.0}],code:0,msg:u'Success'}
从结果中我们可以发现,虽然我们期望返回Top2,但是实际返回的目标数据只有1条,而且是运营部的唯一一条,说明filter是完全生效的。
- 删除数据,我们删除属于运营部的唯一的那条数据。
# 删除数据
primaryKey = {}
primaryKey['DocId'] = "bfef64f1-c965-c0ff-bb99-c2ac7771c2f6"
partitionKey = {}
partitionKey['Department'] = "运营部"
table.delete(primary_key=primaryKey, partition_key=partitionKey)
- 再次执行上述向量检索。
# 再次执行上述向量检索,结果集为空
result = table.search(anns=anns, retrieve_vector=True)
print("Search result after delete target row: {}".format(result))
执行完成之后,我们会看到控制台输出了一个空的结果集,符合预期:
Search result after delete target row: {metadata:{content__length:u'36',content__type:u'application/json',request_id:u'f4b3f3ae-4a3f-4d45-b6ab-e88533505281'},rows:[],code:0,msg:u'Success'}