Skip to content

14.10-Milvus实践

要点

  • 数据量超过 10 亿向量,或需要强隔离的多租户架构时,Milvus 才值得上;低于这个规模,pgvector 或 Qdrant 更快上线
  • 存储计算分离——查询、写入、索引节点独立扩缩容,代价是 etcd + MinIO + Pulsar 三个外部依赖
  • Collection schema 显式定义所有字段类型,没有 JSON 类型,用动态字段兜底非结构化 metadata
  • 索引选型:默认 HNSW(快但费内存),内存不够换 IVF_SQ8(压缩到 1/4),有 GPU 用 GPU_IVF_FLAT
  • 混合查询:标量过滤 + 向量检索在一条请求中完成,partition key 自动路由多租户数据
  • 写入后必须 loadCollection 才能查询——这是和 pgvector、Qdrant 最大的行为差异
  • 生产部署用 Kubernetes + Milvus Operator 或 Zilliz Cloud 托管

1. 什么时候值得上 Milvus

pgvector 和 Qdrant 都能处理百万级向量。如果你的数据量在 1000 万以下,这两者足够——pgvector 少一个组件,Qdrant 单机能跑。Milvus 的价值区间在十亿级向量,或者你需要分布式架构提供的独立扩缩容能力。

三种方案的规模阈值:

维度pgvectorQdrantMilvus
数据规模百万级千万级十亿级
部署复杂度低(复用 PostgreSQL)中(单进程 + 可选集群)高(etcd + MinIO + Pulsar)
扩展模型受 PostgreSQL 单机限制支持分布式,但运维较简单各节点独立扩缩容
运维成本
适合团队全栈 / 小团队中型团队有专职基础设施团队

如果不确定是否需要 Milvus,先用 pgvector 或 Qdrant。 从简单方案迁移到 Milvus 的成本,远低于维护一个你不需要的基础设施。

这一篇带你走完 Milvus 的完整流程:部署、schema 设计、索引选型、写入、查询、分区和性能调优。每一步会说清楚操作、原因和验证方式。

2. 分布式架构:为什么需要这些组件

Milvus 和 pgvector、Qdrant 最大的区别是存储计算分离。pgvector 的数据和查询都在 PostgreSQL 进程里,Qdrant 也是单进程承载全部功能。Milvus 把查询、写入、索引构建拆到不同节点,各层可以独立扩展。

┌─────────────────────────────────────────────────────────┐
│                      Access Layer                        │
│                  (SDK / REST / gRPC)                    │
├─────────────────────────────────────────────────────────┤
│                Coordinator Service                       │
│    ┌──────────┬──────────┬──────────────┐               │
│    │ Root     │ Query    │ Data         │               │
│    │ Coord    │ Coord    │ Coord        │               │
│    └──────────┴──────────┴──────────────┘               │
├─────────────────────────────────────────────────────────┤
│                 Worker Nodes                             │
│    ┌──────────┬──────────┬──────────────┐               │
│    │ Query    │ Data     │ Index        │               │
│    │ Nodes    │ Nodes    │ Nodes        │               │
│    └──────────┴──────────┴──────────────┘               │
├─────────────────────────────────────────────────────────┤
│                Storage Layer                             │
│    ┌──────────┬──────────┬──────────────┐               │
│    │ etcd     │ MinIO/S3 │ Pulsar/      │               │
│    │ (meta)   │ (object) │ Kafka (log)  │               │
│    └──────────┴──────────┴──────────────┘               │
└─────────────────────────────────────────────────────────┘

三层各司其职:

  • Access Layer——SDK 入口,支持 gRPC 和 REST。你的应用代码只和这一层打交道
  • Coordinator Service——集群大脑。Root Coord 管 DDL 和元数据(创建 collection、管理 schema 变更);Query Coord 管查询拓扑和负载均衡(决定哪个 Query Node 处理哪些请求);Data Coord 管数据分配和 Compaction(决定数据写到哪个 Data Node,定期合并小文件)
  • Worker Nodes——干活的节点。Query Node 执行向量检索——从对象存储加载索引到内存,处理查询请求。Data Node 处理写入——消费 WAL(Write-Ahead Log),把数据刷写到对象存储。Index Node 专门构建索引——索引构建是 CPU 密集型任务,单独拆出来避免影响查询延迟
  • Storage Layer——三个外部依赖各有分工。etcd 存元数据(schema、索引配置、checkpoint);MinIO/S3 存向量数据和索引文件;Pulsar/Kafka 存 WAL 和变更日志

这种架构的代价:你需要运维 etcd、MinIO、Pulsar 三个外部组件。好处是查询密集就加 Query Node,写入密集就加 Data Node,不用整体扩容。

如果团队没有运维分布式系统的经验,建议先用 Zilliz Cloud(Milvus 的托管版)或从 Standalone 模式开始。

3. 部署:三种方式

Milvus 提供三种部署方式,按场景选择:

方式适用场景组件数运维成本
Docker Compose开发 / 测试4(etcd + MinIO + Pulsar + Milvus)
Standalone本地开发 / 小规模1(全部在一个进程)极低
Kubernetes生产完整集群

3.1 Docker Compose(开发环境)

Milvus 依赖 etcd、MinIO、Pulsar,Docker Compose 一次启动所有组件:

yaml
# docker-compose.yml
services:
  etcd:
    image: quay.io/coreos/etcd:v3.5.18
    environment:
      ETCD_AUTO_COMPACTION_MODE: revision
      ETCD_AUTO_COMPACTION_RETENTION: "1000"
      ETCD_QUOTA_BACKEND_BYTES: "4294967296"
    command: etcd -advertise-client-urls=http://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd

  minio:
    image: minio/minio:RELEASE.2024-09-22T00-33-43Z
    environment:
      MINIO_ACCESS_KEY: minioadmin
      MINIO_SECRET_KEY: minioadmin
    command: minio server /minio_data --console-address ":9001"

  pulsar:
    image: apachepulsar/pulsar:2.11.0
    command: bin/pulsar standalone --no-functions-worker --no-stream-storage

  milvus:
    image: milvusdb/milvus:v2.5.4
    depends_on:
      - etcd
      - minio
      - pulsar
    environment:
      ETCD_ENDPOINTS: etcd:2379
      MINIO_ADDRESS: minio:9000
      PULSAR_ADDRESS: pulsar:6650
    ports:
      - "19530:19530"   # gRPC
      - "9091:9091"     # metrics
bash
docker compose up -d

原因:Docker Compose 把四个容器放在同一个网络,Milvus 自动发现 etcd、MinIO、Pulsar 的地址。

验证

bash
# 检查容器是否全部运行
docker compose ps

# 检查 Milvus 是否就绪
curl http://localhost:9091/healthz
# 预期:返回 "ok"

3.2 Standalone(轻量开发)

如果不想跑四个容器,Standalone 模式把所有组件放在一个进程里:

bash
docker run -d --name milvus-standalone \
  -p 19530:19530 \
  -p 9091:9091 \
  milvusdb/milvus:v2.5.4 standalone

Standalone 功能完整,但不支持分布式扩展。适合本地开发和小规模数据(百万级以下)。生产环境不要用。

3.3 生产部署

生产环境建议 Kubernetes + Milvus Operator:

bash
# 安装 Milvus Operator
kubectl apply -f https://raw.githubusercontent.com/milvus-io/milvus-operator/main/manifests/namespace.yaml
kubectl apply -f https://raw.githubusercontent.com/milvus-io/milvus-operator/main/manifests/deployment.yaml

# 创建集群
kubectl apply -f milvus-cluster.yaml

Milvus Operator 自动处理故障恢复、滚动升级和扩缩容。如果不想运维 K8s 集群,用 Zilliz Cloud(Milvus 官方托管服务)——省去 etcd、MinIO、Pulsar 的全部运维工作。

4. Schema 设计:显式定义每一个字段

Milvus 的 schema 比 pgvector 和 Qdrant 严格——所有字段类型必须显式声明,没有 JSON 类型。

typescript
import { MilvusClient, DataType } from '@zilliz/milvus2-sdk-node'

const client = new MilvusClient({ address: 'localhost:19530' })

// 创建 collection
await client.createCollection({
  collection_name: 'documents',
  description: '文档向量集合',
  fields: [
    {
      name: 'id',
      data_type: DataType.VarChar,
      is_primary_key: true,
      type_params: { max_length: 64 },
    },
    {
      name: 'document_id',
      data_type: DataType.VarChar,
      type_params: { max_length: 64 },
    },
    {
      name: 'document_title',
      data_type: DataType.VarChar,
      type_params: { max_length: 256 },
    },
    {
      name: 'chunk_index',
      data_type: DataType.Int64,
    },
    {
      name: 'content',
      data_type: DataType.VarChar,
      type_params: { max_length: 65535 },
    },
    {
      name: 'embedding',
      data_type: DataType.FloatVector,
      type_params: { dim: 768 },
    },
    {
      name: 'user_id',
      data_type: DataType.VarChar,
      type_params: { max_length: 64 },
    },
    {
      name: 'tenant_id',
      data_type: DataType.VarChar,
      type_params: { max_length: 64 },
    },
    {
      name: 'created_at',
      data_type: DataType.Int64,  // Milvus 没有原生时间类型,用毫秒时间戳
    },
  ],
  // 开启动态字段——存储未预定义的字段
  enable_dynamic_field: true,
})

原因:显式 schema 让 Milvus 在写入前就知道每个字段的类型和长度,可以在存储层做列式优化。这和 Qdrant 的 JSON payload 思路不同——Qdrant 灵活但查询时需要动态解析。

预期结果:collection 创建成功后,Milvus 为每个字段分配存储空间。向量字段在创建索引后才会构建索引结构。

验证

typescript
const collections = await client.showCollections()
console.log(collections.data)  // 应该看到 'documents'

几个关键区别:

  • 没有原生时间类型——用 Int64 存毫秒时间戳,查询时手动转换
  • 没有 JSON 类型——如果 metadata 结构不固定,开启 enable_dynamic_field 兜底。动态字段有性能开销,高频查询的字段还是应该显式定义
  • 主键可以是 VarChar 或 Int64——VarChar 主键更灵活,但 Int64 主键在范围查询时更快

5. 索引选型:按数据规模决定

索引类型决定检索性能和资源消耗。先说结论:不确定时,先用 HNSW。

5.1 HNSW(默认推荐)

typescript
await client.createIndex({
  collection_name: 'documents',
  field_name: 'embedding',
  index_type: 'HNSW',
  metric_type: 'COSINE',
  params: { M: 16, efConstruction: 256 },
})
  • M(每个节点的最大连接数):越大越精确,索引越大。16 是多数场景的合理起点
  • efConstruction(构建时的搜索范围):越大索引质量越好,但构建越慢。256 是常用值
  • 查询速度快,但内存占用高——所有向量数据都在内存中

原因:HNSW 用多层图结构组织向量,查询时从顶层向下逐层搜索,时间复杂度接近 O(log n)。不需要调太多参数就能获得好效果。

5.2 IVF_SQ8(内存受限时的降级选择)

typescript
await client.createIndex({
  collection_name: 'documents',
  field_name: 'embedding',
  index_type: 'IVF_SQ8',
  metric_type: 'COSINE',
  params: { nlist: 1024 },
})

每个维度从 32 bit float 压缩到 8 bit,内存占用降到 1/4,精度略有损失。nlist 是聚类中心数量,数据量大时设为 sqrt(向量数)4 * sqrt(向量数) 之间。

5.3 标量字段索引

标量字段的过滤查询也需要索引,否则过滤会全表扫描:

typescript
await client.createIndex({
  collection_name: 'documents',
  field_name: 'tenant_id',
  index_type: 'Trie',        // 适合基数较低的字符串字段
})

await client.createIndex({
  collection_name: 'documents',
  field_name: 'created_at',
  index_type: 'STL_SORT',    // 适合数值和时间范围查询
})

5.4 索引类型选择表

索引类型内存占用查询速度精度适用场景
HNSW近似默认选择,延迟敏感
IVF_FLAT精确百万级,内存适中
IVF_SQ8近似大规模,内存有限
IVF_PQ极低近似超大规模,精度要求不高
GPU_IVF_FLAT精确有 GPU 资源
GPU_CAGRA极快近似有 GPU,极低延迟

如果 HNSW 内存不够,换 IVF_SQ8。 如果数据量超过十亿且精度要求不高,换 IVF_PQ。如果有 GPU 资源,GPU_IVF_FLAT 可以大幅加速检索。

6. 数据写入与加载

6.1 批量写入

Milvus 单次写入限制默认 100MB。大批量数据需要分批:

typescript
export async function bulkInsert(
  client: MilvusClient,
  collectionName: string,
  chunks: Array<{
    id: string
    document_id: string
    content: string
    embedding: number[]
    tenant_id: string
  }>
): Promise<void> {
  // 500 条一批——示例可用,生产环境根据单条大小调整
  const BATCH_SIZE = 500

  for (let i = 0; i < chunks.length; i += BATCH_SIZE) {
    const batch = chunks.slice(i, i + BATCH_SIZE)

    await client.insert({
      collection_name: collectionName,
      data: batch.map((chunk) => ({
        id: chunk.id,
        document_id: chunk.document_id,
        content: chunk.content,
        embedding: chunk.embedding,
        tenant_id: chunk.tenant_id,
      })),
    })
  }
}

原因:分批写入避免单次请求超过 100MB 限制。如果单条向量维度很高(如 1536 维),把 BATCH_SIZE 调小到 200-300。

6.2 Upsert

Milvus 2.3+ 支持 upsert——如果主键已存在则更新,否则插入:

typescript
await client.upsert({
  collection_name: 'documents',
  data: [
    {
      id: 'doc-1-chunk-0',
      document_id: 'doc-1',
      content: '更新后的内容...',
      embedding: newEmbedding,
      tenant_id: 'tenant-456',
    },
  ],
})

6.3 写入后必须加载

Milvus 写入数据后,必须手动加载到内存才能查询——这是和 pgvector、Qdrant 最大的行为差异:

typescript
await client.loadCollection({
  collection_name: 'documents',
})

原因:Milvus 数据存在 MinIO/S3(对象存储),查询时需要把索引加载到 Query Node 内存。不 load 就查询会返回空结果或报错。

验证

typescript
const info = await client.getCollectionStatistics({
  collection_name: 'documents',
})
console.log(info.data.row_count)  // 应该等于写入的条数

生产环境注意:数据量大时不要一次加载整个 collection。按 partition 分批加载,只加载需要查询的 partition。

7. 混合查询:标量过滤 + 向量检索

Milvus 支持在一条请求中同时做标量过滤和向量检索。

7.1 基础查询

typescript
const results = await client.search({
  collection_name: 'documents',
  vectors: [queryVector],
  limit: 5,
  output_fields: ['document_id', 'content', 'chunk_index'],
  // 搜索参数和索引类型对应:HNSW 用 ef,IVF 用 nprobe
  params: { ef: 128 },
})

预期结果:返回 5 条最相似的结果,每条包含 id、score(相似度分数)和 output_fields 中指定的字段。

7.2 带过滤的查询

typescript
const results = await client.search({
  collection_name: 'documents',
  vectors: [queryVector],
  limit: 5,
  filter: 'tenant_id == "tenant-456" && created_at >= 1704067200000',
  params: { ef: 128 },
})

Milvus 的过滤语法类似 SQL:

typescript
// 等于 / 不等于
'tenant_id == "tenant-456"'
'status != "archived"'

// 范围
'created_at >= 1704067200000 && created_at < 1735689600000'

// IN
'document_id in ["doc-1", "doc-2", "doc-3"]'

// LIKE(模糊匹配)
'document_title LIKE "%退款%"'

// 组合
'tenant_id == "tenant-456" && (status == "active" || status == "draft")'

原因:Milvus 的混合查询先做标量过滤缩小候选集,再在候选集上做向量检索。如果过滤后候选集很小,检索速度会显著提升。

生产环境注意:过滤字段的基数影响性能。高基数字段(如 user_id)过滤效率低于低基数字段(如 status)。如果频繁按某个字段过滤,确保给它建了标量索引(参考 5.3)。

7.3 范围搜索和分页

除了 topK,Milvus 还支持范围搜索——返回所有距离在阈值内的结果:

typescript
const results = await client.search({
  collection_name: 'documents',
  vectors: [queryVector],
  params: {
    radius: 0.3,       // 距离阈值
    range_filter: 0.8,  // 返回 score 在 [0.8, 0.3) 之间
  },
})

分页查询用 offset + limit:

typescript
const results = await client.search({
  collection_name: 'documents',
  vectors: [queryVector],
  limit: 5,
  offset: 10,  // 跳过前 10 条
})

8. 多租户:分区与 Partition Key

多租户场景需要数据隔离。Milvus 提供三种策略:

8.1 Partition Key(推荐)

设置 is_partition_key 后,Milvus 自动按字段值把数据路由到不同 partition:

typescript
await client.createCollection({
  collection_name: 'documents',
  fields: [
    {
      name: 'id',
      data_type: DataType.VarChar,
      is_primary_key: true,
      type_params: { max_length: 64 },
    },
    {
      name: 'embedding',
      data_type: DataType.FloatVector,
      type_params: { dim: 768 },
    },
    {
      name: 'tenant_id',
      data_type: DataType.VarChar,
      type_params: { max_length: 64 },
      is_partition_key: true,  // 按 tenant_id 自动分区
    },
  ],
})

查询时 Milvus 自动路由——不需要手动指定 partition:

typescript
const results = await client.search({
  collection_name: 'documents',
  vectors: [queryVector],
  limit: 5,
  filter: 'tenant_id == "tenant-456"',  // 自动路由到对应 partition
})

原因:partition key 让每个租户的数据物理隔离,查询时只扫描对应 partition,避免跨租户数据泄露和性能干扰。

8.2 显式 Partition

如果需要更细粒度的控制,可以手动管理 partition:

typescript
// 创建 partition
await client.createPartition({
  collection_name: 'documents',
  partition_name: 'tenant_456',
})

// 写入指定 partition
await client.insert({
  collection_name: 'documents',
  partition_name: 'tenant_456',
  data: chunks,
})

// 查询指定 partition
const results = await client.search({
  collection_name: 'documents',
  partition_names: ['tenant_456'],
  vectors: [queryVector],
  limit: 5,
})

8.3 三种策略对比

策略隔离级别管理成本适用场景
Partition Key逻辑隔离,自动路由多租户 SaaS,租户数量多
显式 Partition逻辑隔离,手动管理需要按时间 / 业务手动分区
独立 Collection完全隔离大客户 / 合规要求

大多数 SaaS 场景用 partition key。 显式 partition 适合需要按时间归档或手动控制加载策略的场景。独立 collection 只在大客户需要完全隔离时使用——每个 collection 都有独立的索引和内存开销。

9. 性能调优与运维

9.1 查询参数调优

不同索引的调优参数不同,本质都是精度和速度的权衡

typescript
// HNSW:ef 越大越精确但越慢(默认 64)
params: { ef: 128 }

// IVF_FLAT / IVF_SQ8:nprobe 越大越精确但越慢(默认 1)
params: { nprobe: 16 }

可以按环境变量动态调整——对精度要求高的场景用大值,对延迟敏感的场景用小值:

typescript
params: { ef: parseInt(process.env.HNSW_EF ?? '128') }

9.2 容量估算

以 HNSW 为例,内存占用的粗估公式:

内存 ≈ 向量数 × (维度 × 4 + M × 8) 字节

以 100 万条 768 维向量、M=16 为例:每条向量约 3.2 KB,总计约 3.2 GB。加上索引图的开销,实际内存在 4-5 GB 左右。

如果内存预算不够,换 IVF_SQ8——同样数据量大约 1 GB。但精度会下降,需要在真实数据上做 recall 测试。

9.3 监控

Milvus 暴露 Prometheus metrics(默认 9091 端口):

http://localhost:9091/metrics

关键指标:

  • milvus_query_latency——查询延迟。P99 超过 100ms 需要关注
  • milvus_insert_rate——写入速率。突然下降可能是 Data Node 瓶颈
  • milvus_collection_count——collection 数量
  • milvus_index_size——索引大小。和容量估算对比

9.4 健康检查

typescript
app.get('/health/milvus', async (c) => {
  try {
    const res = await client.checkHealth()
    return c.json({
      status: res.isHealthy ? 'ok' : 'degraded',
      reasons: res.reasons,
    })
  } catch (err) {
    return c.json({ status: 'error', error: String(err) }, 503)
  }
})

生产环境注意:除了 Milvus 本身,还要监控 etcd 的健康状态——etcd 挂掉意味着元数据不可用,整个集群无法工作。MinIO 和 Pulsar 同理。三个外部依赖中任何一个故障,Milvus 都无法正常工作。

10. 完整实现:MilvusVectorStore

生产可用的完整实现——封装 collection 初始化、写入、查询和删除:

typescript
// src/services/rag/milvus-store.ts
import { MilvusClient, DataType } from '@zilliz/milvus2-sdk-node'

interface ChunkWithEmbedding {
  id: string
  content: string
  embedding: number[]
  metadata: {
    documentId: string
    tenantId?: string
  }
}

interface SearchResult {
  id: string
  score: number
  content: string
  metadata: { documentId: string }
}

export class MilvusVectorStore {
  private client: MilvusClient

  constructor() {
    this.client = new MilvusClient({
      address: process.env.MILVUS_ADDRESS ?? 'localhost:19530',
    })
  }

  async initCollection(name: string, dimensions: number): Promise<void> {
    const exists = await this.client.hasCollection({ collection_name: name })

    if (!exists.value) {
      await this.client.createCollection({
        collection_name: name,
        fields: [
          {
            name: 'id',
            data_type: DataType.VarChar,
            is_primary_key: true,
            type_params: { max_length: 64 },
          },
          {
            name: 'document_id',
            data_type: DataType.VarChar,
            type_params: { max_length: 64 },
          },
          {
            name: 'content',
            data_type: DataType.VarChar,
            type_params: { max_length: 65535 },
          },
          {
            name: 'embedding',
            data_type: DataType.FloatVector,
            type_params: { dim: dimensions },
          },
          {
            name: 'tenant_id',
            data_type: DataType.VarChar,
            type_params: { max_length: 64 },
            is_partition_key: true,
          },
        ],
        enable_dynamic_field: true,
      })

      await this.client.createIndex({
        collection_name: name,
        field_name: 'embedding',
        index_type: 'HNSW',
        metric_type: 'COSINE',
        params: { M: 16, efConstruction: 256 },
      })

      // 必须加载才能查询
      await this.client.loadCollection({ collection_name: name })
    }
  }

  async upsertChunks(
    collectionName: string,
    chunks: ChunkWithEmbedding[]
  ): Promise<void> {
    const BATCH_SIZE = 500
    for (let i = 0; i < chunks.length; i += BATCH_SIZE) {
      const batch = chunks.slice(i, i + BATCH_SIZE)
      await this.client.insert({
        collection_name: collectionName,
        data: batch.map((chunk) => ({
          id: chunk.id,
          document_id: chunk.metadata.documentId,
          content: chunk.content,
          embedding: chunk.embedding,
          tenant_id: chunk.metadata.tenantId ?? '',
        })),
      })
    }
  }

  async search(
    collectionName: string,
    queryVector: number[],
    options: { topK?: number; tenantId?: string } = {}
  ): Promise<SearchResult[]> {
    const { topK = 5, tenantId } = options

    const filter = tenantId ? `tenant_id == "${tenantId}"` : undefined

    const results = await this.client.search({
      collection_name: collectionName,
      vectors: [queryVector],
      limit: topK,
      filter,
      output_fields: ['document_id', 'content'],
      params: { ef: 128 },
    })

    return results.results.map((r) => ({
      id: String(r.id),
      score: r.score,
      content: r.content,
      metadata: { documentId: r.document_id },
    }))
  }

  async deleteDocument(
    collectionName: string,
    documentId: string
  ): Promise<void> {
    await this.client.delete({
      collection_name: collectionName,
      filter: `document_id == "${documentId}"`,
    })
  }
}

这个实现区分了「示例可用」和「生产可用」的边界:initCollection 做了幂等检查(hasCollection),upsertChunks 做了分批写入,search 支持 tenant 过滤。生产环境还需要补连接池管理、重试逻辑和 metrics 上报。

11. 验收清单与下一步

到这里,你应该完成了以下验证:

  • [ ] Milvus 集群或 Standalone 部署成功,/healthz 返回 ok
  • [ ] Collection 创建成功,schema 字段类型正确
  • [ ] HNSW 索引创建成功
  • [ ] 批量写入数据,getCollectionStatistics 显示正确的 row_count
  • [ ] loadCollection 执行成功
  • [ ] 基础向量查询返回正确结果
  • [ ] 带过滤的混合查询正常工作
  • [ ] Partition key 配置正确,多租户查询隔离
  • [ ] 健康检查端点正常响应
  • [ ] Prometheus metrics 可访问

Milvus 的选择判断:如果你的数据量还在百万级,回退到 pgvector(08 篇)或 Qdrant(09 篇)。Milvus 的运维成本在数据量不够大时不值得。

下一步:向量检索的准确率不只看 topK 结果。检索质量还取决于混合检索策略、重排序和 Query 改写。下一篇讲相似度检索的进阶优化。

基于 MIT 协议开源