找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖
Claude、GPT 海外模型 API 接入Claude skills 从入门到精通 吴恩达亲授 AI Agent 核心技能2026 瞪哥公务员考试全攻略 行测申论一站式系统备考
Agent 文心智能蒸馏模型实战 90G 课程智泊 AI 大模型训练营 基于 LangChain 的 RAG 与提示工程实战构建企业级 AI 大脑:大模型微调与 RAG / Agent 全栈实战

4758

积分

0

好友

614

主题
发表于 3 小时前 | 查看: 5| 回复: 0

关键词:向量数据库、Milvus、HNSW、IVF、DiskANN、Filtered Search、BM25、RRF、Reranker、RAG、Kubernetes、性能压测

阅读约定:本文以一个匿名化生产案例作为主线。案例中"事故前后"的数据只描述该系统在指定流量回放、版本和硬件配置下的观测;它们不是任何向量库的通用基准。所有参数均为压测起点,发布配置必须由读者在自己的数据集、过滤分布与目标 SLO 下复测决定。

很多团队第一次做 RAG ,会把注意力集中在 Embedding 模型和 Prompt 上。

但系统真正进入生产环境以后,最先把服务拖垮的往往不是大模型,而是检索层。

数据只有几十万条时,几乎任何向量索引都能跑得不错。数据增长到几百万、几千万条以后,问题会集中爆发:

  • HNSW 索引越来越大,QueryNode 内存不断上涨
  • 增量写入产生越来越多 Segment,查询合并成本持续增加
  • 一旦叠加部门、租户、时间范围等过滤条件,P99 延迟突然恶化
  • 单纯扩大 ef 可以提高 Recall,却会直接放大 CPU 和延迟
  • 只使用 Dense Vector Search,召回结果"语义上相关,但业务上不够准确"
  • 上线 Reranker 后准确率提升了,GPU 推理又成为新的延迟瓶颈
  • 平均延迟看起来很好,但少量慢查询已经把用户体验打穿

因此,千万级向量检索的优化,本质上不是"调一个参数"。

它是一个包含索引、过滤、分片、召回、重排序、资源隔离、容量规划和观测体系的系统工程。

本文用一个企业知识库检索系统作为主线,完整拆解一套可以真正落地的优化路径。


1. 事故现场:为什么 800 万向量把检索服务拖垮了

下面是经过脱敏的企业内部知识库案例。文档、用户和基础设施名称均已移除;规模、查询形态、故障现象与排查顺序保留,用于说明工程决策如何产生。

文档经过清洗、切分和 Embedding 后,形成约 800 万条 Chunk,每条向量维度为 768。

上线验收要求:

数据规模:8,000,000 chunks
向量维度:768
峰值请求:12,000 QPS
普通检索 P95:< 30 ms
复杂过滤检索 P99:< 80 ms
Recall@10:>= 0.95
服务可用性:99.95%

1.1 这组数据的测量口径

文中后续所有案例指标均遵循以下口径,避免把单次 SDK 调用耗时写成用户体验:

项目 案例口径
数据集 线上脱敏 chunk 的 embedding 与标量字段分布;不使用随机向量
查询集 最近 7 天日志分层抽样,覆盖无过滤、租户/部门过滤、时间范围和 ACL 组合过滤
延迟 API 网关收到请求至响应写出;统计预热后 30 分钟窗口的 P50/P95/P99
负载 以生产峰值形态回放,同时记录并发、QPS、队列等待和超时
召回 在相同过滤条件下,ANN TopK 与离线 exact KNN TopK 对比
排序 人工标注/审核后的 NDCG@K;不以 ANN Recall 代替业务相关性
版本 每轮记录 Milvus、ES、Embedding、Reranker、SDK、配置哈希与机器规格

最初只有 200 万条向量时,系统运行正常。

随着数据增长到 600 万以上,几个问题开始同时出现:

索引构建:
40 min  ->  5 h+

QueryNode RSS:
约 30 GB -> 60 GB+

带部门 + 时间过滤的查询:
P99 20~30 ms -> 500~800 ms

部分 Pod:
出现 OOMKilled

检索质量:
语义相关,但精确业务关键词命中不足

这些数字是这次事故的观测快照,不是读者应期待复现的基准结果。若缺少上述测量口径,任何"从 X ms 降至 Y ms"的描述都不应进入上线决策。

如果只看单点,很容易得到几个错误结论:

"内存不够,再加机器。"
"召回不够,把 ef 从 128 调到 512。"
"过滤慢,把过滤条件放到应用层。"
"向量不准,再换一个 Embedding 模型。"

这些做法可能短期有效,却会把问题推向下一个瓶颈。

真正需要先回答的是:

一次检索请求,从进入服务到返回 TopK,到底在哪些阶段消耗了时间?


2. 先拆延迟,而不是先调参数

一个真实的 RAG 检索请求通常不是一次 Vector Search。

完整链路更接近:

Client
  |
  v
API Gateway
  |
  v
Query Normalisation
  |
  +--------------------------+
  |                          |
  v                          v
Embedding                  BM25
  |                          |
  v                          |
Vector Search                |
  |                          |
  +------------+-------------+
               |
               v
            RRF Fusion
               |
               v
             Rerank
               |
               v
          Context Build
               |
               v
              LLM

如果检索阶段总预算是 60 ms,可以进一步拆成:

阶段 目标预算
Query 预处理 1~2 ms
Query Embedding 3~10 ms
Dense ANN Search 8~20 ms
BM25 Search 5~15 ms
RRF Fusion < 1 ms
Reranker 10~30 ms
Context Assembly 1~3 ms

这里有一个非常重要的原则:

不要用"向量数据库查询耗时"代替"用户真实检索延迟"。

线上应该至少监控:

retrieval_total_latency
embedding_latency
vector_search_latency
bm25_latency
rerank_latency
filter_hit_ratio
candidate_count
recall_k
rerank_k
timeout_count
fallback_count

只有拆开以后,才能知道应该优化索引、过滤、模型,还是网络与调度。


3. HNSW 为什么经常是第一选择,但不是最终答案

HNSW 是生产环境最常见的 ANN 索引之一。

它通过多层近邻图快速缩小搜索空间。

常见参数包括:

M
efConstruction
ef

其中:

  • M 控制图的连接程度
  • efConstruction 控制建图阶段候选搜索宽度
  • ef 控制查询阶段搜索宽度

它们都不是"越大越好"。

更大的参数通常意味着:

更高 Recall
    +
更高索引构建成本
    +
更高内存占用
    +
更高查询 CPU
    +
更高延迟

所以生产调优不能这样写:

M = 64
efConstruction = 512
ef = 512

然后希望所有问题自动消失。

正确方式应该是建立自己的 Recall / Latency 曲线。


3.1 参数应该如何压测

假设业务目标是:

Recall@10 >= 0.95
P99 <= 50 ms

可以建立如下参数矩阵:

M_VALUES = [16, 24, 32, 48]
EF_CONSTRUCTION_VALUES = [100, 160, 240]
EF_SEARCH_VALUES = [32, 64, 96, 128, 192, 256]

对每一组配置记录:

index_build_time
index_size
query_qps
p50
p95
p99
recall@10
cpu_usage
rss_memory

最终得到的不是"最佳参数",而是 Pareto Front:

              Recall
                ^
                |
            *   |
         *      |
      *         |
   *            |
----------------------------> Latency

你真正要找的是:

在 Recall 达到业务阈值之后,延迟和资源成本最低的那一组参数。


4. 一套可直接使用的 HNSW 基准测试脚本

下面给出一个可以直接改造的测试框架。

这里不把任何 M 或 ef 写成"标准答案",而是通过压测确定。

import time
import statistics
from dataclasses import dataclass
from typing import List

@dataclass
class BenchmarkResult:
    ef: int
    p50_ms: float
    p95_ms: float
    p99_ms: float
    qps: float
    recall_at_10: float

def percentile(values: List[float], p: float) -> float:
    if not values:
        return 0.0

    values = sorted(values)
    index = min(int(len(values) * p), len(values) - 1)
    return values[index]

def recall_at_k(actual_ids, ground_truth_ids, k=10):
    actual = set(actual_ids[:k])
    expected = set(ground_truth_ids[:k])

    if not expected:
        return 0.0

    return len(actual & expected) / len(expected)

def benchmark_search(
    search_func,
    queries,
    ground_truth,
    ef: int,
    top_k: int = 10,
):
    latencies = []
    recalls = []

    start_all = time.perf_counter()

    for query, expected in zip(queries, ground_truth):
        start = time.perf_counter()

        result_ids = search_func(
            query=query,
            top_k=top_k,
            ef=ef,
        )

        elapsed_ms = (time.perf_counter() - start) * 1000

        latencies.append(elapsed_ms)
        recalls.append(
            recall_at_k(
                actual_ids=result_ids,
                ground_truth_ids=expected,
                k=top_k,
            )
        )

    total_seconds = time.perf_counter() - start_all

    return BenchmarkResult(
        ef=ef,
        p50_ms=percentile(latencies, 0.50),
        p95_ms=percentile(latencies, 0.95),
        p99_ms=percentile(latencies, 0.99),
        qps=len(queries) / total_seconds,
        recall_at_10=statistics.mean(recalls),
    )

批量执行:

EF_VALUES = [32, 64, 96, 128, 192, 256]

results = []

for ef in EF_VALUES:
    result = benchmark_search(
        search_func=vector_search,
        queries=test_queries,
        ground_truth=ground_truth,
        ef=ef,
        top_k=10,
    )

    results.append(result)

    print(
        f"ef={ef:<3} "
        f"P95={result.p95_ms:.2f}ms "
        f"P99={result.p99_ms:.2f}ms "
        f"Recall@10={result.recall_at_10:.4f} "
        f"QPS={result.qps:.1f}"
    )

最终选型逻辑可以直接自动化:

def choose_best_config(results):
    candidates = [
        r for r in results
        if r.recall_at_10 >= 0.95
        and r.p99_ms <= 50
    ]

    if not candidates:
        return None

    return min(
        candidates,
        key=lambda r: r.p99_ms
    )

这比在文章里硬写:

ef=128 最好

更接近真实生产环境。

因为 Embedding 模型、维度、数据分布、CPU、NUMA、过滤条件都会改变最终结果。


5. 过滤搜索,才是很多生产事故真正的起点

企业知识库里很少只有:

query = "Kubernetes OOM"

更常见的是:

tenant_id = 10086
department = "engineering"
doc_type IN ["runbook", "postmortem"]
publish_time >= now - 90d
permission_tags CONTAINS "platform"

最终查询类似:

语义接近 "Kubernetes OOM"
并且
属于当前租户
并且
用户有权限
并且
最近 90 天
并且
文档类型是 Runbook 或 Postmortem

这时候最危险的不是 ANN 本身。

而是:

Filter Selectivity 发生巨大变化。


6. 不要只说"预过滤"和"后过滤",先计算 Selectivity

定义:

selectivity =
过滤后候选数 / 总数据量

假设总数据量:

10,000,000

三个过滤条件:

A:
tenant_id = 1
剩余 2,000,000
selectivity = 20%

B:
tenant_id = 1
department = engineering
剩余 180,000
selectivity = 1.8%

C:
tenant_id = 1
department = engineering
doc_type = postmortem
publish_time >= 30d
剩余 8,000
selectivity = 0.08%

这三类查询不应该使用同一个策略。


8.1 一个更实用的策略路由器

from dataclasses import dataclass
from enum import Enum

class FilterStrategy(str, Enum):
    ANN_WITH_FILTER = "ann_with_filter"
    PREFILTER_EXACT = "prefilter_exact"
    PARTITION_ROUTING = "partition_routing"
    ANN_OVERFETCH_POSTFILTER = "ann_overfetch_postfilter"

@dataclass
class FilterDecision:
    strategy: FilterStrategy
    filtered_count: int
    selectivity: float
    overfetch: int = 1

def choose_filter_strategy(
    total_count: int,
    estimated_filtered_count: int,
    top_k: int,
) -> FilterDecision:

    if total_count <= 0:
        raise ValueError("total_count must be positive")

    selectivity = (
        estimated_filtered_count / total_count
    )

    # 极小候选集:
    # ANN 的图遍历收益可能不如直接精确计算
    if estimated_filtered_count <= 50_000:
        return FilterDecision(
            strategy=FilterStrategy.PREFILTER_EXACT,
            filtered_count=estimated_filtered_count,
            selectivity=selectivity,
        )

    # 如果租户等字段已经完成物理分区,
    # 优先缩小搜索空间
    if selectivity <= 0.05:
        return FilterDecision(
            strategy=FilterStrategy.PARTITION_ROUTING,
            filtered_count=estimated_filtered_count,
            selectivity=selectivity,
        )

    # 宽过滤可以 ANN + 过滤
    if selectivity >= 0.30:
        return FilterDecision(
            strategy=FilterStrategy.ANN_WITH_FILTER,
            filtered_count=estimated_filtered_count,
            selectivity=selectivity,
        )

    # 中间区域通过 over-fetch 对冲后过滤损失
    overfetch = min(
        20,
        max(
            2,
            int(1 / max(selectivity, 0.01))
        )
    )

    return FilterDecision(
        strategy=FilterStrategy.ANN_OVERFETCH_POSTFILTER,
        filtered_count=estimated_filtered_count,
        selectivity=selectivity,
        overfetch=overfetch,
    )

这里的阈值同样只是初始值。

真正生产环境应该从查询日志学习:

filter fingerprint
filtered cardinality
actual latency
actual result count
recall

然后不断修正路由策略。


7. 权限过滤不能为了性能放到最后

这是知识库和企业 RAG 最容易犯的安全错误之一。

错误做法:

1. 全库向量检索
2. Top 100
3. 应用层检查 ACL
4. 删除无权限结果

这样可能造成两个问题。

第一,严格 ACL 下:

Top100
  ↓
只有 3 条有权限

召回质量会直接崩掉。

第二,更严重的是:

没有权限的数据已经进入了检索服务的候选路径。

对于严格隔离场景,至少应该把以下字段尽可能前置:

tenant_id
workspace_id
security_scope

如果业务允许,还可以按照租户做:

Partition
Collection
Shard

但不要为了"查询快",无限创建物理 Partition。

分区数量本身也会产生元数据和调度成本。


8. Milvus 索引配置:不要把 Demo 配置当生产配置

下面给出一种更适合工程项目的写法。

使用配置中心管理参数:

vector:
  index:
    type: HNSW
    metric: COSINE
    hnsw:
      m: 24
      efConstruction: 160

search:
  topK: 50
  ef: 128

rerank:
  candidateK: 20
  finalK: 5

Python 侧:

from pymilvus import MilvusClient

client = MilvusClient(
    uri="http://milvus:19530"
)

index_params = client.prepare_index_params()

index_params.add_index(
    field_name="embedding",
    index_type="HNSW",
    metric_type="COSINE",
    params={
        "M": 24,
        "efConstruction": 160,
    },
)

client.create_index(
    collection_name="knowledge_base",
    index_params=index_params,
)

查询:

def search_vector(
    query_vector,
    filter_expr: str,
    top_k: int = 50,
    ef: int = 128,
):
    return client.search(
        collection_name="knowledge_base",
        data=[query_vector],
        anns_field="embedding",
        limit=top_k,
        filter=filter_expr,
        search_params={
            "metric_type": "COSINE",
            "params": {
                "ef": ef,
            },
        },
        output_fields=[
            "doc_id",
            "chunk_id",
            "title",
            "content",
            "tenant_id",
            "department",
            "publish_ts",
        ],
    )

注意:

M=24
efConstruction=160
ef=128

只是示例起点。

正式上线必须经过自己的 Recall / Latency Benchmark。


9. Dense Search 不是最终排序

单纯的 Dense Retriever 有一个结构性问题。

Embedding 会把整段文本压缩成固定维度向量。

因此它很擅长:

语义近似
同义表达
概念关联

但不一定擅长:

精确错误码
产品型号
类名
API 名
订单号
技术术语
缩写

例如用户搜索:

Kubernetes OOMKilled exit code 137

Dense Search 可能把:

"容器资源管理最佳实践"

排在:

"Pod OOMKilled 与 Exit Code 137 排查"

前面。

这不是向量数据库坏了。

而是 Dense Retrieval 本来就不是精排模型。


10. 更稳定的生产方案:Dense + Sparse + Rerank

推荐架构:

                      Query
                        |
            +-----------+-----------+
            |                       |
            v                       v
      Query Embedding             BM25
            |                       |
            v                       v
      Dense Top 50             Sparse Top 50
            |                       |
            +-----------+-----------+
                        |
                        v
                    RRF Fusion
                        |
                    Top 20~30
                        |
                        v
                  Cross Encoder
                        |
                     Top 3~5

这里每一层解决的问题不同。

Dense:

提高语义 Recall

BM25:

补强关键词、错误码、专有名词

RRF:

融合不同量纲的排名

Reranker:

真正判断 Query 与 Document 的细粒度相关性

11. 为什么推荐 RRF,而不是直接加权分数

假设 Dense 返回:

0.91
0.87
0.83

BM25 返回:

16.2
11.7
8.3

这两个 Score 不是一个量纲。

直接:

final = 0.5 * dense_score + 0.5 * bm25_score

没有稳定含义。

RRF 只看排名:

score(d) =
Σ 1 / (k + rank(d))

实现:

from collections import defaultdict

def reciprocal_rank_fusion(
    ranking_lists,
    k: int = 60,
):
    scores = defaultdict(float)

    for ranking in ranking_lists:
        for rank, doc_id in enumerate(
            ranking,
            start=1,
        ):
            scores[doc_id] += (
                1.0 / (k + rank)
            )

    return sorted(
        scores.items(),
        key=lambda x: x[1],
        reverse=True,
    )

这样可以避免:

Dense Score
BM25 Score
Sparse Score

之间的归一化陷阱。


12. 一套可落地的两阶段 Retriever

下面给出完整骨架。

重点解决三个原文中经常被忽略的问题:

1. Dense 与 BM25 并行
2. RRF 之后必须回填完整 Document
3. Reranker 必须有 timeout 和 fallback
import asyncio
from dataclasses import dataclass
from typing import Optional

@dataclass
class Candidate:
    doc_id: str
    content: str = ""
    dense_rank: Optional[int] = None
    bm25_rank: Optional[int] = None
    rrf_score: float = 0.0
    rerank_score: Optional[float] = None

class RetrievalService:

    def __init__(
        self,
        vector_store,
        keyword_store,
        document_store,
        reranker,
    ):
        self.vector_store = vector_store
        self.keyword_store = keyword_store
        self.document_store = document_store
        self.reranker = reranker

    async def retrieve(
        self,
        query: str,
        filters: dict,
        recall_k: int = 50,
        rerank_k: int = 20,
        final_k: int = 5,
    ):
        dense_task = asyncio.create_task(
            self.vector_store.search(
                query=query,
                filters=filters,
                top_k=recall_k,
            )
        )

        bm25_task = asyncio.create_task(
            self.keyword_store.search(
                query=query,
                filters=filters,
                top_k=recall_k,
            )
        )

        dense_results, bm25_results = (
            await asyncio.gather(
                dense_task,
                bm25_task,
            )
        )

        fused_ids = self._rrf(
            dense_results,
            bm25_results,
        )

        candidate_ids = [
            doc_id
            for doc_id, _ in fused_ids[:rerank_k]
        ]

        documents = (
            await self.document_store.batch_get(
                candidate_ids
            )
        )

        candidates = [
            Candidate(
                doc_id=doc_id,
                content=documents[doc_id]["content"],
                rrf_score=score,
            )
            for doc_id, score
            in fused_ids[:rerank_k]
            if doc_id in documents
        ]

        try:
            reranked = await asyncio.wait_for(
                self.reranker.rank(
                    query=query,
                    candidates=candidates,
                ),
                timeout=0.030,
            )

            return reranked[:final_k]

        except asyncio.TimeoutError:
            # Reranker 超时不能拖垮整个 RAG
            return candidates[:final_k]

    @staticmethod
    def _rrf(
        dense_results,
        bm25_results,
        k: int = 60,
    ):
        scores = {}

        for rank, item in enumerate(
            dense_results,
            start=1,
        ):
            scores[item.doc_id] = (
                scores.get(item.doc_id, 0.0)
                + 1.0 / (k + rank)
            )

        for rank, item in enumerate(
            bm25_results,
            start=1,
        ):
            scores[item.doc_id] = (
                scores.get(item.doc_id, 0.0)
                + 1.0 / (k + rank)
            )

        return sorted(
            scores.items(),
            key=lambda x: x[1],
            reverse=True,
        )

相比只写"调用 Reranker API",这里多了一个关键生产设计:

Reranker 必须允许失败。

否则 GPU 服务一次抖动,就会把整个 RAG 请求链拖死。


13. Reranker 不应该吃 100 个候选

Cross Encoder 与 Bi-Encoder 最大的区别,是 Query 与 Document 会共同进入模型。

它更准,但成本更高。

如果:

每次请求 rerank 100 chunks
每个 chunk 800 tokens
QPS = 1000

推理量会迅速爆炸。

因此应该设置:

Dense TopK:50
BM25 TopK:50
RRF Candidate:20~30
Rerank:20
Final:3~5

Rerank 的最佳候选数量也应该通过实验得到。


15.1 Rerank K 的压测矩阵

RERANK_K_VALUES = [
    5,
    10,
    20,
    30,
    50,
]

记录:

NDCG@5
MRR
HitRate@5
rerank_p95
rerank_p99
GPU utilisation
tokens_per_request

可能得到:

K=5
延迟:8 ms
NDCG:0.78

K=20
延迟:18 ms
NDCG:0.86

K=50
延迟:41 ms
NDCG:0.87

这时候:

20 -> 50

只有很小的质量收益,却增加一倍以上推理成本。

那 rerank_k=20 才是工程上合理的点。


14. 异步写入:Kafka 解耦,但不掩盖一致性问题

同步把一份文档写进正文库、向量库与 Elasticsearch,最危险的是部分成功。推荐以 outbox 事件作为唯一事实来源,让每个消费者按 (chunk_id, content_version) 幂等处理:

Document DB 事务 → outbox 事件 → Kafka
                                   ├─ Embedding → Milvus upsert
                                   ├─ Lexical writer → ES bulk index
                                   └─ Content writer → 正文存储
                                             ↓
                                  记录各路完成水位线

检索侧应仅返回正文、向量与词法索引都达到最低可见版本的 chunk,或明确标记写入处理中。这样可避免"向量已是新版、正文仍是旧版"的错误回答。Kafka 的价值是解耦、削峰、可重放和可观测,不是保证业务自动一致。

Elasticsearch 的 refresh_interval=-1 会关闭自动刷新,适合批量导入;它不能用来解决"写后立即可搜索"。少量强实时写入可评估 refresh=wait_for,并将等待成本纳入写入 SLO。


15. QueryNode 和 IndexNode 必须资源隔离

索引构建通常是:

CPU 密集
内存密集
I/O 密集

在线查询则要求:

低尾延迟
稳定 CPU
稳定内存
稳定 I/O

如果二者混在同一 Kubernetes Node Pool:

Index Build
    ↓
CPU 100%
    ↓
Query P99 飙升

推荐:

Node Pool A
QueryNode

Node Pool B
IndexNode

Node Pool C
Embedding / Reranker GPU

16. Kubernetes 资源配置不能只写 requests / limits

至少要考虑:

requests
limits
topologySpreadConstraints
PodDisruptionBudget
antiAffinity
priorityClass
local SSD
NUMA
HugePages(视引擎需求)

一个示意配置:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: vector-query
spec:
  replicas: 6

  selector:
    matchLabels:
      app: vector-query

  template:
    metadata:
      labels:
        app: vector-query

    spec:
      priorityClassName: online-critical

      topologySpreadConstraints:
      - maxSkew: 1
        topologyKey: topology.kubernetes.io/zone
        whenUnsatisfiable: ScheduleAnyway
        labelSelector:
          matchLabels:
            app: vector-query

      containers:
      - name: query
        image: example/vector-query:1.0.0

        resources:
          requests:
            cpu: "8"
            memory: "32Gi"

          limits:
            cpu: "12"
            memory: "40Gi"

        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
            periodSeconds: 5

        livenessProbe:
          httpGet:
            path: /health
            port: 8080
            periodSeconds: 10

真正生产环境还应该加入:

PDB
HPA
节点亲和性
存储拓扑
滚动发布保护

结语:把优化变成可验证的工程闭环

这份案例的价值不在某一个 ef、某一种索引或某一个重排模型,而在可复测闭环:用真实日志和相同过滤条件生成真值集;以端到端 P99、Recall、NDCG、资源与写入可见性共同验收;让 Dense、BM25、正文和 ACL 使用同一版本约束;并为重排与依赖故障准备降级路径。

上线前至少完成三项检查:目标并发下重新压测参数曲线;验证复杂 ACL 查询不会少返回或越权;在写入、compaction、节点故障和重排超时同时发生时验证 P99 与降级行为。这样,向量检索才能从一次性 Demo 变成可持续运行的生产能力。这也是云栈社区持续关注的技术方向之一:让工程决策建立在可验证的数据之上,而不是拍脑袋或复制配置。

参考资料

  1. Milvus HNSW
  2. Milvus Filtered Search
  3. Milvus Scalar Index
  4. Elasticsearch refresh 参数
  5. Qdrant ACORN



上一篇:如何用 C++ 宏实现 FOR_EACH 循环:__VA_OPT__ 与延迟展开
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-9-27 04:21 , Processed in 0.633622 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表