07 | 把字段与指标同步到 Qdrant(生成阶段)
项目地址:github.com/frontzhm/n2...
每一步对应的完整代码都在仓库里,跟着文档卡住了就去翻源码。
这是一篇系列文,请按顺序阅读。
本文目标
NL2SQL 生成 SQL 之前,需要先召回与用户问题相关的字段和指标。
上一篇已经把结构化元数据同步进 MySQL:
column_info:字段信息metric_info:指标信息
本文继续完成生成阶段的第 2 步:
把字段和指标的
name + description + alias转换成向量,再写入 Qdrant,为后续语义召回做准备。
本文正文只讲这个项目怎样完成同步。向量、向量模型、向量数据库等通用知识,统一放在文末的三个科普模块:
1. 先看完整同步链路
text
MySQL meta 库
├── column_info
└── metric_info
│
▼
组装 embedding_text
name + description + alias
│
▼
EmbeddingClient → TEI
│
▼
1024 维向量
│
▼
VectorSyncService 组装 Point
│
▼
QdrantRepository → Qdrant
├── column_info_collection
└── metric_info_collection
各层职责:
| 层 | 文件 | 职责 |
|---|---|---|
| Infrastructure | app/infrastructure/embedding_client.py |
调用 TEI,把文本变成向量 |
| Infrastructure | app/infrastructure/qdrant_client.py |
创建 Qdrant 官方异步客户端 |
| Repository | app/repositories/qdrant_repository.py |
重建 Collection、写入 Point |
| Service | app/services/vector_sync_service.py |
元数据转文本、批量向量化、组装 Point |
| Script | conf/sync_db.py |
读取配置、装配依赖、执行同步、释放资源 |
2. 一条元数据怎样变成一个 Point
本文采用"一条元数据记录对应一个向量",不是给 name、description、alias 分别生成三个向量。
字段记录:
python
ColumnInfo(
id="fact_order.order_amount",
name="order_amount",
description="订单金额。",
alias=["销售额", "订单金额", "收入"],
...
)
先转换成待向量化输入:
text
business_id:
fact_order.order_amount
embedding_text:
字段名称:order_amount
字段描述:订单金额。
字段别名:销售额、订单金额、收入
payload:
字段 ID、表 ID、类型、角色、别名、样例等原始信息
TEI 生成向量后,最终写入 Qdrant:
python
{
"id": "根据业务 ID 稳定生成的 UUID v5",
"vector": [0.021, -0.138, 0.407, ...],
"payload": {
"metadata_type": "column_info",
"id": "fact_order.order_amount",
"name": "order_amount",
"description": "订单金额。",
"alias": ["销售额", "订单金额", "收入"],
"embedding_text": "字段名称:...",
"embedding_model": "BAAI/bge-large-zh-v1.5",
...
},
}
这里有两个 ID:
- Point
id:Qdrant 存储 ID,使用 UUID payload["id"]:业务 ID,供应用识别字段或指标
3. 配置两个服务
conf/app_config.yaml:
yaml
qdrant:
host: localhost
port: 6333
embedding_size: 1024
column_collection: column_info_collection
metric_collection: metric_info_collection
timeout: 60
embedding:
host: localhost
port: 8081
model: BAAI/bge-large-zh-v1.5
# 当前 CPU TEI 后端最多并行处理 4 条,批次过大会导致请求排队并超时。
batch_size: 4
timeout: 120
两个服务不要混淆:
| 服务 | 端口 | 负责 |
|---|---|---|
| TEI | 8081 |
把文本转换成向量 |
| Qdrant | 6333 |
保存向量并执行相似度检索 |
依赖:
bash
uv add httpx "qdrant-client==1.16.2"
httpx用于调用 TEI/embedqdrant-client是 Qdrant 官方 Python 客户端;这里与 Docker 中的 Qdrant v1.16 保持同一 minor 版本
4. 定义 EmbeddingClient
文件:app/infrastructure/embedding_client.py
这个 Client 只负责调用本地 TEI:
python
class EmbeddingClient:
def __init__(self, base_url: str, timeout: float = 60.0):
self._client = httpx.AsyncClient(
base_url=base_url.rstrip("/"),
timeout=timeout,
trust_env=False,
)
async def embed(self, texts: list[str]) -> list[list[float]]:
if not texts:
return []
response = await self._client.post(
"/embed",
json={
"inputs": texts,
"truncate": True,
},
)
response.raise_for_status()
vectors = response.json()
if not isinstance(vectors, list) or len(vectors) != len(texts):
raise ValueError("Embedding 服务返回数量异常")
return vectors
LangChain 常用"文档向量"和"查询向量"两个接口,因此增加两个薄封装:
python
async def aembed_documents(
self,
texts: list[str],
) -> list[list[float]]:
return await self.embed(texts)
async def aembed_query(self, text: str) -> list[float]:
return (await self.embed([text]))[0]
它们底层都调用同一个 /embed:
text
aembed_documents ─┐
├─ embed() → TEI /embed
aembed_query ─────┘
生成索引时调用 aembed_documents();以后用户提问时调用 aembed_query()。
单独测试 Embedding
文件底部提供了:
python
if __name__ == "__main__":
asyncio.run(main())
可以直接运行:
bash
uv run python app/infrastructure/embedding_client.py 订单金额 客单价
控制台会显示文本、向量维度和前 8 个值,不打印完整的 1024 维向量。
这里只需要 Embedding 容器健康;不需要 Qdrant 参与生成向量。
5. 定义 QdrantClient
文件:app/infrastructure/qdrant_client.py
项目使用官方 AsyncQdrantClient。为了让 Client 的构造方式统一,增加一个薄适配类:
python
from qdrant_client import AsyncQdrantClient
class QdrantClient(AsyncQdrantClient):
def __init__(
self,
base_url: str,
timeout: float = 60.0,
):
super().__init__(
url=base_url.rstrip("/"),
timeout=int(timeout),
)
现在两个 Client 的创建方式具有可预测性:
python
embedding_client = EmbeddingClient(
base_url=embedding_url,
timeout=60,
)
qdrant_client = QdrantClient(
base_url=qdrant_url,
timeout=60,
)
QdrantClient 不重复封装官方方法,仍然可以直接调用:
python
await qdrant_client.collection_exists(...)
await qdrant_client.create_collection(...)
await qdrant_client.upsert(...)
await qdrant_client.query_points(...)
await qdrant_client.close()
6. 定义 QdrantRepository
文件:app/repositories/qdrant_repository.py
Repository 接收外部创建好的 Client:
python
class QdrantRepository:
def __init__(self, client: AsyncQdrantClient):
self._client = client
它不读取配置、不创建 Client,也不关闭 Client。谁创建 Client,谁负责关闭。
重建 Collection
python
async def reset_collection(
self,
collection_name: str,
vector_size: int,
) -> None:
if await self._client.collection_exists(collection_name):
await self._client.delete_collection(collection_name)
await self._client.create_collection(
collection_name=collection_name,
vectors_config=models.VectorParams(
size=vector_size,
distance=models.Distance.COSINE,
),
)
当前是生成阶段的全量重建策略:删除旧 Collection,再按 1024 维和 Cosine 距离重建。
批量 Upsert Point
python
async def upsert(
self,
collection_name: str,
points: list[dict[str, Any]],
) -> None:
qdrant_points = [
models.PointStruct(
id=point["id"],
vector=point["vector"],
payload=point.get("payload"),
)
for point in points
]
await self._client.upsert(
collection_name=collection_name,
points=qdrant_points,
wait=True,
)
upsert 表示:ID 不存在就插入,ID 已存在就更新。
稳定生成 Point ID
业务 ID 是:
text
fact_order.order_amount
GMV
Qdrant Point ID 使用整数或 UUID,因此通过 UUID v5 稳定转换:
python
def stable_point_id(
collection_name: str,
document_id: str,
) -> str:
return str(
uuid5(
NAMESPACE_URL,
f"{collection_name}:{document_id}",
)
)
相同 Collection 和业务 ID 每次生成相同 UUID,因此同步可重复执行。
7. 定义 VectorSyncService
文件:app/services/vector_sync_service.py
Service 负责这条业务链路:
text
ColumnInfo / MetricInfo
→ _EmbeddingInput
→ 分批调用 TEI
→ 检查向量维度
→ 组装 Qdrant Point
→ 重建 Collection
→ 分批 Upsert
统一内部输入
字段和指标结构不同,先转换成统一内部类型:
python
@dataclass
class _EmbeddingInput:
business_id: str
embedding_text: str
payload: dict[str, Any]
三个字段含义明确:
business_id:用于稳定生成 Point UUIDembedding_text:送给 TEI 生成向量payload:随 Point 写入 Qdrant 的业务信息
字段与指标入口
python
async def sync_columns(
self,
collection_name: str,
columns: list[ColumnInfo],
) -> int:
inputs = [ self._column_to_embedding_input(column) for column in columns ]
return await self._sync_inputs(collection_name, inputs)
async def sync_metrics(
self,
collection_name: str,
metrics: list[MetricInfo],
) -> int:
inputs = [ self._metric_to_embedding_input(metric) for metric in metrics ]
return await self._sync_inputs(collection_name, inputs)
两个入口只负责转换数据,公共同步流程放在 _sync_inputs()。
公共同步流程
python
async def _sync_inputs(
self,
collection_name: str,
inputs: list[_EmbeddingInput],
) -> int:
points = []
# 1. 先生成全部向量,此时不修改旧 Collection。
for start in range(0, len(inputs), self.batch_size):
batch = inputs[start : start + self.batch_size]
vectors = await self.embedding_client.aembed_documents(
[item.embedding_text for item in batch]
)
self._validate_vectors(vectors)
for item, vector in zip(batch, vectors, strict=True):
points.append(
self._build_point(collection_name, item, vector)
)
# 2. 全部向量成功后,再重建并写入 Collection。
await self.qdrant_repository.reset_collection(
collection_name,
self.vector_size,
)
for start in range(0, len(points), self.batch_size):
await self.qdrant_repository.upsert(
collection_name,
points[start : start + self.batch_size],
)
return len(inputs)
为什么先生成全部向量,再删除旧 Collection?
如果 TEI 不可用或向量维度配置错误,同步会在修改 Qdrant 前失败,已有索引仍然可以使用。
文本模板
字段文本:
text
字段名称:order_amount
字段描述:订单金额。
字段别名:销售额、订单金额、收入
指标文本:
text
指标名称:GMV
指标描述:所有订单的成交金额总和。
指标别名:成交总额、订单总额
只有 name + description + alias 参与向量化;类型、角色、所属表和相关字段等结构化信息保留在 payload。
8. 在 sync_db.py 中装配
文件:conf/sync_db.py
python
async def sync_to_qdrant(
column_infos: list[ColumnInfo],
metric_infos: list[MetricInfo],
) -> tuple[int, int]:
embedding_config = app_config["embedding"]
qdrant_config = app_config["qdrant"]
embedding_client = EmbeddingClient(
base_url=(
f"http://{embedding_config['host']}:"
f"{embedding_config['port']}"
),
timeout=embedding_config.get("timeout", 60),
)
qdrant_client = QdrantClient(
base_url=(
f"http://{qdrant_config['host']}:"
f"{qdrant_config['port']}"
),
timeout=qdrant_config.get("timeout", 60),
)
repository = QdrantRepository(qdrant_client)
service = VectorSyncService(
embedding_client=embedding_client,
qdrant_repository=repository,
vector_size=qdrant_config["embedding_size"],
model_name=embedding_config["model"],
batch_size=embedding_config.get("batch_size", 4),
)
try:
column_count = await service.sync_columns(
qdrant_config["column_collection"],
column_infos,
)
metric_count = await service.sync_metrics(
qdrant_config["metric_collection"],
metric_infos,
)
return column_count, metric_count
finally:
await asyncio.gather(
embedding_client.close(),
qdrant_client.close(),
)
这里是 Composition Root:
- 读取配置
- 创建 Client
- 注入 Repository 和 Service
- 执行同步
- 关闭资源
9. 执行与验证
确认服务健康:
bash
curl http://localhost:8081/health
curl http://localhost:6333/healthz
执行:
bash
uv run python conf/sync_db.py
预期看到:
text
已写入 Qdrant column_info 24 条
已写入 Qdrant metric_info 2 条
网页看Qdrant Dashboard:
text
http://localhost:6333/dashboard#/collections
也可以通过 API 查看少量 Point:
bash
curl http://localhost:6333/collections/column_info_collection/points/scroll \
-X POST \
-H 'Content-Type: application/json' \
-d '{"limit":3,"with_payload":true,"with_vector":false}'
⚠️ 当前脚本会重建 MySQL meta 表和两个 Qdrant Collection,只适用于开发阶段。
10. 项目内常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
连接 8081 失败 |
TEI 未健康 | 检查 Embedding 容器日志和 /health |
连接 6333 失败 |
Qdrant 未启动 | 检查 Docker 和端口映射 |
| 向量维度不是 1024 | 模型与配置不一致 | 核对实际模型和 embedding_size |
| 重复同步出现多条 Point | 使用随机 UUID | 使用业务 ID 派生的 UUID v5 |
| 删除配置后旧 Point 还存在 | 只做 upsert,没有清理旧数据 | 生成阶段重建 Collection |
| 一批请求失败 | 批次或 Token 总量过大 | 调小 embedding.batch_size |
11. 将 MySQL 基础设施移动到 infrastructure
前面的项目结构中,两个 MySQL 数据库分别使用两个模块:
text
app/dbs/
├── dw_db.py
└── meta_db.py
它们的代码几乎完全相同,都需要完成:
text
读取数据库配置
→ 创建数据库 URL
→ 创建 AsyncEngine
→ 创建 Session 工厂
→ 对外提供 Session
两个模块真正不同的只有数据库配置:
dw_db:数据仓库,用来读取真实字段类型和样例值meta_db:元数据库,用来保存表、字段、指标及其关系
因此没有必要维护两套重复的连接代码。本项目将它们合并成一个通用的 MySQLDatabase:
text
app/infrastructure/
├── embedding_client.py
├── mysql_database.py
└── qdrant_client.py
为什么放在 infrastructure
infrastructure 是"基础设施"的意思,通常用于存放与外部系统交互的技术实现,例如:
- MySQL
- Qdrant
- TEI
- Elasticsearch
- 第三方 HTTP API
本项目中的职责可以简单理解为:
text
Service:组织业务流程
Repository:表达数据读写操作
Infrastructure:建立并管理真实的外部连接
MySQLDatabase 不只是发送一次请求的 Client。它还管理 Engine、连接池、Session 和资源释放,因此使用 Database 比 Client 更准确。
当前只有三个基础设施文件,直接放在 app/infrastructure/ 下更加直观。等以后外部服务明显增多,再拆分 clients/、database/ 等子目录也不迟。
定义 MySQLDatabase
文件:app/infrastructure/mysql_database.py
python
from sqlalchemy import URL
from sqlalchemy.ext.asyncio import (
AsyncEngine,
AsyncSession,
async_sessionmaker,
create_async_engine,
)
class MySQLDatabase:
def __init__(
self,
host: str,
port: int,
user: str,
password: str,
database: str,
*,
echo: bool = False,
pool_recycle: int = 3600,
):
database_url = URL.create(
drivername="mysql+asyncmy",
username=user,
password=password,
host=host,
port=port,
database=database,
)
self.engine: AsyncEngine = create_async_engine(
database_url,
pool_pre_ping=True,
pool_recycle=pool_recycle,
echo=echo,
connect_args={"charset": "utf8mb4"},
)
self.session_factory = async_sessionmaker(
bind=self.engine,
autocommit=False,
autoflush=False,
expire_on_commit=False,
)
这里使用 URL.create(),而不是手工拼接:
python
f"mysql+asyncmy://{user}:{password}@{host}:{port}/{database}"
这样能够正确处理密码中的 @、:、/ 等 URL 保留字符。
重要参数:
| 参数 | 作用 |
|---|---|
pool_pre_ping=True |
从连接池取连接前先检查连接是否可用 |
pool_recycle=3600 |
定期回收旧连接,避免超过 MySQL wait_timeout |
echo=False |
默认不打印 SQL,调试时可以临时开启 |
charset=utf8mb4 |
支持完整 Unicode,包括 emoji 和生僻字 |
expire_on_commit=False |
提交后保留已加载属性,避免异步访问时意外触发 IO |
Engine、连接池和 Session
三者的关系是:
text
MySQLDatabase
└── AsyncEngine(应用生命周期内复用)
└── Connection Pool(管理多条真实连接)
├── AsyncSession(一次业务操作)
├── AsyncSession(另一次业务操作)
└── AsyncSession(另一个请求)
- Engine:管理数据库驱动和连接池,通常长期复用
- Connection:与 MySQL 建立的真实网络连接,由连接池管理
- Session:一次请求或业务操作的工作单元,用完就关闭
- Transaction:一组需要一起提交或一起回滚的数据库操作
因此不要每查询一个字段就创建一个 Engine,也不要让所有请求长期共用同一个 Session。
创建和关闭 Session
MySQLDatabase 提供 session() 上下文管理器:
python
@asynccontextmanager
async def session(self) -> AsyncIterator[AsyncSession]:
async with self.session_factory() as session:
yield session
同步脚本中的使用方式:
python
async with meta_database.session() as session, session.begin():
...
执行过程:
text
创建 Session
→ 从连接池借用连接
→ 开启事务
→ 执行业务操作
→ 成功时提交,异常时回滚
→ 关闭 Session
→ 连接归还连接池
类中还提供了 get_session(),以后接入 FastAPI 时可以作为 Depends 使用。session() 和 get_session() 底层共用同一个 Session 工厂。
使用一个类创建两个数据库实例
数据库连接参数继续放在 conf/app_config.yaml 中,MySQLDatabase 本身不读取全局配置。
在 conf/sync_db.py 中创建两个实例:
python
dw_database = MySQLDatabase(**app_config["dw_db"])
meta_database = MySQLDatabase(**app_config["meta_db"])
可以把它理解为:
text
同一个 MySQLDatabase 类
├── dw_db 配置 → dw_database
└── meta_db 配置 → meta_database
两个实例分别拥有自己的 Engine 和连接池,但复用了相同的连接管理代码。
同步函数通过参数接收数据库实例:
python
await sync_to_meta_db(
meta_config,
dw_database,
meta_database,
)
这种显式传入的方式有几个好处:
- 从函数参数就能看出依赖哪些数据库
- 导入模块时不会隐式创建 Engine
- 测试时可以替换数据库实例
- 一个进程可以创建多个数据库连接配置
- 创建和关闭资源的职责更加明确
关闭 Engine 和连接池
关闭 Session 只是把连接归还连接池,并没有关闭整个连接池。程序结束时还需要释放 Engine:
python
async def close(self) -> None:
await self.engine.dispose()
同步入口使用 finally,保证正常结束或发生异常时都会释放资源:
python
try:
await sync_to_meta_db(
meta_config,
dw_database,
meta_database,
)
finally:
await asyncio.gather(
dw_database.close(),
meta_database.close(),
)
这里遵循统一的生命周期原则:
谁创建基础设施对象,谁负责在任务结束时关闭它。
科普向量
本模块只讲通用概念,不依赖当前项目代码。
1. 什么是向量
向量可以理解成一组有顺序的数字:
text
[0.12, -0.37, 0.88]
文本向量是模型对文本特征的数字表示:
text
"订单金额" → [0.021, -0.138, 0.407, ...]
单个数字通常没有可读业务含义,整组数字共同表达模型学习到的特征。
2. 什么是维度
向量包含多少个数字,就是多少维:
text
[0.2, 0.5, -0.1] → 3 维
本项目模型输出 1024 个数字,所以是 1024 维。
维度由模型决定,不是越高越好,也不能随意截掉一部分数字。维度越高,存储、传输和计算成本通常也越高。
3. 怎样比较两个向量
常见方法:
| 方法 | 关注点 | 常见场景 |
|---|---|---|
| Cosine | 方向是否接近 | 文本语义检索 |
| Dot Product | 点积大小 | 已归一化模型、推荐系统 |
| Euclidean | 空间直线距离 | 数值特征、聚类 |
Cosine 可以先理解成:
text
方向越接近 → 语义可能越接近
相似度分数不是正确率。0.82 不代表有 82% 的概率正确,阈值需要通过真实问题集评估。
4. 什么是归一化
归一化常指把向量长度缩放为 1,同时保留方向:
text
[2, 0] → [1, 0]
生成文档向量和查询向量时,归一化方式必须一致。更改归一化策略后,通常需要重新生成已有向量。
5. 向量常见操作
- 相似度检索:寻找最接近的 Top K
- 聚类:把相近向量自动分组
- 分类:把向量作为分类特征
- 去重:寻找语义重复内容
- 异常检测:寻找远离大多数样本的记录
科普向量模型
1. 什么是 Embedding 模型
Embedding 模型把文本、图片或音频转换成向量。本文使用文本模型:
text
文本 → Tokenizer → 模型 → Pooling → 向量
语义相近的文本通常会得到相近向量,但模型只是提供"相关候选",并不保证业务判断一定正确。
2. Token、Tokenizer 和上下文长度
Tokenizer 会先把文本切成 Token。Token 可能是字、词的一部分、标点或特殊符号。
模型能处理的 Token 数量有上限:
- 短文本可以直接生成向量
- 超长文本可以截断
- 长文档通常应该先切块,再分别向量化
字段和指标描述较短,因此本文使用 truncate: true;长文检索不能只依赖截断,否则尾部信息会丢失。
3. Pooling
模型内部通常会为每个 Token 产生表示。为了得到整段文本的单个向量,需要 Pooling:
- CLS Pooling:使用特殊 CLS Token
- Mean Pooling:对 Token 向量取平均
- Max Pooling:各维取最大值
具体方式由模型训练方式和配置决定,不应随意更改。
4. 文档向量和查询向量
语义检索有两个角色:
text
生成阶段:被搜索内容 → 文档向量 → 存入数据库
查询阶段:用户问题 → 查询向量 → 搜索数据库
有些模型要求 Query 和 Document 使用不同前缀:
text
passage: 订单金额字段
query: 销售额是多少
是否需要前缀以及前缀内容,必须查看具体模型说明。不能把其他模型的规则直接照搬过来。
5. 怎样选择模型
常见考虑因素:
- 业务语言:中文、英文或多语言
- 任务:检索、聚类、分类
- 向量维度
- 最大输入长度
- CPU / GPU 推理速度
- 内存占用
- 模型许可证
- 在真实业务问题集上的召回效果
参数量和排行榜只能作为参考,最终要用自己的字段、指标和用户问题评测。
6. 什么是 TEI
TEI 全称 Text Embeddings Inference,是一个部署文本向量模型的推理服务。
text
应用 POST /embed
↓
TEI:分词、批处理、模型推理
↓
返回向量数组
TEI 负责文本转向量,不负责保存向量或执行相似度检索。
独立部署 TEI 的好处:
- 应用不需要自己加载大型模型
- 多个应用可以共享模型服务
- 模型服务可以独立使用 CPU / GPU
- 支持动态批处理
- 应用和模型可以分别扩缩容
7. 批处理、截断和 Prompt
常用推理参数与方法:
- Batch:一次提交多段文本,减少网络往返
- Truncate:文本超过上限时截断
- Normalize:统一向量长度
- Prompt / Prefix:按模型要求区分 Query 与 Document
批次不是越大越好,还受总 Token 数、内存、并发和超时限制。
8. 常见应用
- 语义搜索
- RAG 文档召回
- 商品或内容推荐
- 文本聚类
- 语义去重
- 字段、指标和知识库召回
科普向量数据库
1. 为什么需要向量数据库
普通数据库擅长精确条件:
sql
WHERE id = 'GMV'
WHERE amount > 100
向量数据库擅长寻找"最相近"的内容:
text
查询向量 → 找到最相似的 Top K 向量
它不是 MySQL 的完全替代品。常见架构是:
text
MySQL:保存结构化事实和关系
向量数据库:保存向量索引和检索 payload
2. Qdrant 核心概念
text
Collection
└── Point
├── id
├── vector
└── payload
| 概念 | 粗略类比 MySQL | 含义 |
|---|---|---|
| Collection | Table | 一类向量的集合 |
| Point | Row | 一条向量记录 |
| Point ID | Primary Key | 唯一标识 |
| Payload | JSON Columns | 可过滤、可返回的业务数据 |
| Vector Index | Index | 加速近邻搜索 |
这只是帮助理解,Qdrant 没有 SQLAlchemy Session,也不是关系型数据库。
3. 近似最近邻和 HNSW
数据量大时,逐个比较所有向量成本很高。向量数据库通常使用 Approximate Nearest Neighbor(ANN,近似最近邻)索引。
HNSW 是常见 ANN 算法,通过多层图结构快速找到相近向量。它牺牲少量绝对精确性,换取更快查询速度。
常见调优目标:
- Recall:真正相关结果被找回的比例
- Latency:查询耗时
- Memory:索引内存占用
- Build Time:索引构建时间
4. Payload 与过滤
向量相似度可以和结构化过滤组合:
text
语义相似
+ metadata_type = column_info
+ table_id = fact_order
经常用于过滤的 payload 字段可以建立 Payload Index,避免扫描大量 Point。
5. Upsert 和稳定 ID
Upsert:
text
ID 不存在 → 插入
ID 已存在 → 更新
同步任务应使用稳定 ID。随机 UUID v4 每次都不同,重复执行可能产生重复 Point;由业务 ID 派生的 UUID v5 每次相同,更适合幂等同步。
6. Client、连接和 Session
QdrantClient 会复用底层 HTTP 或 gRPC 连接,可以被多个 Repository 和请求共享。
它没有 MySQL ORM Session 的直接对应物:
text
MySQL:Engine → 多个 Session → 事务
Qdrant:一个 Client → 多个并发 API 请求
不要每查询一条数据就创建一个新 Client;应用关闭或一次性任务结束时再统一关闭。
7. 全量重建和生产切换
Qdrant 没有 SQLAlchemy 那种跨多个 API 操作的事务回滚。
开发阶段可以:
text
删除旧 Collection → 重建 → 写入
生产环境更稳妥的方式是:
text
创建临时 Collection
→ 写入全部 Point
→ 验证数量和查询
→ 切换 Alias
→ 删除旧 Collection
这样 Embedding 或写入失败时,线上仍然使用旧索引。
8. 常用检索参数
limit:返回多少条候选score_threshold:最低相似度阈值- Filter:payload 过滤条件
with_payload:是否返回业务数据with_vector:是否返回原始向量
阈值和 Top K 需要通过真实业务问题集调优,不能只看单个演示结果。
下一步
本文完成了字段和指标向量索引的生成。下一篇继续把维度值同步到 Elasticsearch,之后再实现 Agent 中的:
recall_columnrecall_metricrecall_value