V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
cxy1234
V2EX  ›  外包

有偿,有偿,寻求大佬优化查询

  •  
  •   cxy1234 · 17 天前 · 1693 次点击

    查询一次时间,大约 30 秒。 elasticsearch 8.15,单机,总数据量大约 200 万,filter 过滤后大约 1 万~20 万

    部分索引如下

    "mappings": {
            "properties": {
                "brandId": {
                    "type": "long"
                },
                "pageContent": {
                    "analyzer": "store_analyzer_page",
                    "search_analyzer": "search_analyzer_page",
                    "type": "text"
                },
                "pageEmbedding": {
                  "type": "dense_vector",
                  "dims": 1024,
                  "index": true,
                  "similarity": "cosine",
                  "index_options": {
                    "type": "int8_hnsw",
                    "m": 16,
                    "ef_construction": 100
                  }
                }
            }
        }
    
    

    部分查询语句如下,

    {
      "knn": [
        {
          "field": "pageEmbedding",
          "query_vector": {{embedding}},
          "k": 200,
          "num_candidates": 1000,
          "boost": 1.0,
          "filter": [
            {
              "bool": {
                "filter": [
                  {
                    "term": {
                      "brandId": {
                        "value": {{brandId}}
                      }
                    }
                  }
                ]
              }
            }
          ]
        }
      ],
      "query": {
        "bool": {
          "filter": [
            {
              "term": {
                "brandId": {
                  "value": {{brandId}}
                }
              }
            },
          ],
          "should": [
            {
              "match": {
                "pageContent": {
                  "boost": 0.5,
                  "query": "{{keyword}}"
                }
              }
            }
          ]
        }
      },
    }
    

    部分查询条件会 400ms 左右返回,偶尔会出现查询出现 30 秒,应该如何优化?

    19 条回复    2025-11-05 12:59:30 +08:00
    chihiro2014
        1
    chihiro2014  
       17 天前
    机械硬盘?
    cxy1234
        2
    cxy1234  
    OP
       17 天前
    @chihiro2014 是的,机械盘
    yangxin0
        3
    yangxin0  
       17 天前
    下面给出一个**可直接落地的优化方案**,按“先易后难”的顺序:先把**延迟稳定**下来,再逐步把**平均耗时**做低。你现在的规模(单机、≈200 万文档,过滤后 1–20 万、1024 维向量、int8 HNSW 、k=200 、num_candidates=1000 )在 8.15 完全可以做到**稳定 < 300–500 ms**;出现偶发 30s ,大概率是**冷数据/段太多/合并或 I/O 抖动**叠加**不必要的计算量**导致。

    ---

    ## 一、立刻可做(无需重建索引)

    ### 1) 把“取多少”和“算多少”对齐

    * **让 `k` 接近你实际返回的 `size`**(例如前端只需要 20 条,那就 `k=40~60`;最多 `k<=2×size`)。
    现在 `k=200` 很可能远大于你最终 `size`,会放大 HNSW 探索和重排序成本。
    * **把 `num_candidates` 控制在 `3–6 × k`**。先用 `3×k` 起步,再按召回调。你现在 `num_candidates=1000` 配 `k=200` 还算合理,但如果把 `k` 降到 50 ,就把 `num_candidates` 降到 200–300 ,可显著降时延。

    ### 2) 只在一个地方做品牌过滤

    * 你在 `knn.filter` 和外层 `query.bool.filter` 里**重复**了 `brandId` 过滤。
    **保留 `knn.filter`**(它能让 HNSW 预过滤),外层 `query` 用来做文本匹配即可,避免引擎做两套交集计算。

    ### 3) 采用“混合检索 + RRF 融合”,减少单路的计算压力

    Elasticsearch 8.15 支持把 `query`( BM25 )和 `knn`(向量)并行检索,然后用 **RRF** 融合,通常比单路大 `k` 更稳更快。

    **推荐查询改写:**

    ```json
    POST your_index/_search
    {
    "track_total_hits": false,
    "_source": ["id","brandId","title","url"], // 避免把大字段一次性拉回
    "size": 40, // 例如前端展示 20 ,这里取 2×size 给 RRF
    "knn": [
    {
    "field": "pageEmbedding",
    "query_vector": {{embedding}},
    "k": 60, // ≈ 返回 size
    "num_candidates": 300, // 3–5×k
    "filter": [
    { "term": { "brandId": {{brandId}} } }
    ]
    }
    ],
    "query": {
    "bool": {
    "filter": [ // 供文本子检索复用
    { "term": { "brandId": {{brandId}} } }
    ],
    "should": [
    { "match": { "pageContent": { "query": "{{keyword}}", "boost": 1.0 } } },
    { "match_phrase": { "pageContent": { "query": "{{keyword}}", "slop": 2, "boost": 2.0 } } }
    ],
    "minimum_should_match": 0
    }
    },
    "rank": { "rrf": { "window_size": 200, "rank_constant": 60 } } // 融合得分,更稳
    }
    ```

    ### 4) 减少结果抓取成本

    * `_source` 只保留列表页需要的字段;长文本延迟按需再取(第二跳 `mget`)。
    * 若需要排序稳定,可加 `"track_total_hits": false`(默认足够,但显式关闭能避免一些统计开销)。
    * 如果还要更省:`"stored_fields": ["id"]` + `"docvalue_fields"` 提取结构化字段。

    ### 5) 避免“回退到精确暴力搜索”

    当过滤后命中很少而 `k` 又很大时,引擎可能为“凑满 k 个结果”而对过滤集**暴力算相似度**,在 1024 维上会抖很厉害。
    **做法**:把 `k` 控制在合理范围(见第 1 点),并把 `num_candidates` 设为 `3–6×k`,基本能避开这种回退。

    ---

    ## 二、几分钟完成(不改语义、不重建数据)

    ### 6) 稳定 I/O:预热与段合并

    * **强制合并段**(只读或低写入场景,最有效的稳定手段):

    ```
    POST your_index/_forcemerge?max_num_segments=1&flush=true
    ```

    *说明:HNSW 是“每段一个图”,段越多合并成本越大、延迟越抖。*
    * **预加载向量相关文件进页缓存**(避免冷启动 30s ):

    ```json
    PUT your_index/_settings
    {
    "index" : {
    "store.preload": ["nvd","dvd","tim","doc"] // Lucene 文件:含向量/倒排/词典等
    }
    }
    ```
    * 若写入不多,把 `refresh_interval` 调到 `30s` 或 `60s`,减少段生成频率;批量导入时可先 `-1`,导完再恢复并合并段。

    ### 7) 用分片路由把“品牌”打散(同一品牌落同一分片)

    单机也有收益:每次只打到**一个分片**,而不是全分片并行、最后再归并。

    ```json
    PUT your_index
    {
    "settings": {
    "number_of_shards": 4,
    "number_of_replicas": 0,
    "routing_path": ["brandId"]
    },
    "mappings": { ...原 mapping... }
    }
    ```

    **搜索时携带 `?routing={{brandId}}`**。这需要重建索引,但对你这种“固定 brandId 过滤”的场景非常合适。

    ---

    ## 三、需要重建(可选的结构优化,性价比很高)

    ### 8) 降维或换更紧凑的嵌入

    * 从 **1024 维→512/384 维**(如用更紧凑的文本嵌入模型或做 PCA/投影校准)。
    **检索延迟、内存/磁盘占用基本按比例下降**,而语义召回通常损失很小(需离线验证)。

    ### 9) HNSW 图参数

    * 你现在 `m=16, ef_construction=100`。如果索引空间允许:

    * 把 **`ef_construction` 提到 200**(构图多连几条边),
    * 或把 **`m` 提到 24**。
    这能在**查询时**用更小的 `num_candidates` 达成同等召回,综合延迟更低更稳(代价是建索与磁盘略增)。

    ---

    ## 四、为什么会偶发 30s (以及如何确认)

    最常见的三个来源:

    1. **冷数据**:节点重启 / 段合并后,向量图和倒排不在页缓存里,首批查询全靠磁盘 I/O → 秒级到十秒级。

    * 佐证:重启或合并后第一枪慢,后续恢复正常。
    * 解决:第 6 点的预加载与合并段,或做“热身查询”。
    2. **段过多 + 合并/刷新频繁**:并发读时要在多个段做 knn ,再做归并,遇上后台合并抢 I/O/CPU ,长尾飙升。

    * 解决:增大 `refresh_interval`、forcemerge 。
    3. **计算量设置偏大**:`k`、`num_candidates` 取值与业务 `size` 不匹配,或触发“凑满 k 的暴力回退”。

    * 解决:第 1 、5 点的取值策略。

    **排查命令(线上也安全):**

    ```bash
    # 看段与大小
    GET /your_index/_segments
    GET /_cat/segments/your_index?v

    # 看 GC / 热线程 / I/O
    GET /_nodes/stats/jvm,fs,indices
    GET /_nodes/hot_threads

    # 看一次慢查询的真正耗时在哪
    POST /your_index/_search
    { "profile": true, ...你的查询... }
    ```

    `profile` 输出里若 `Fetch phase` 占比高,多半是 `_source` 太大或网络;若 `Query phase` 某些段特别慢,通常是冷段或段太多。

    ---

    ## 五、对你当前 mapping 的具体建议

    你现在的 mapping 基本可用,针对向量字段可考虑:

    ```json
    "pageEmbedding": {
    "type": "dense_vector",
    "dims": 512, // 建议逐步验证更低维
    "index": true,
    "similarity": "cosine",
    "index_options": {
    "type": "int8_hnsw",
    "m": 24, // 16→24 (可选)
    "ef_construction": 200 // 100→200 (可选)
    }
    }
    ```

    > 维度下调和 HNSW 参数的调整需要**离线评测**一下召回与相关性曲线,再决定是否全量重建。

    ---

    ## 六、落地清单(按优先级)

    1. **改查询**:用上面的混合检索 + RRF ;把 `k`、`num_candidates` 与 `size` 对齐;删除重复过滤;收紧 `_source`。
    2. **forcemerge & 预加载**:合并至 1 段并开启 `index.store.preload`,刷新间隔改长。
    3. **观察**:用 `profile`、`_segments`、`_nodes/hot_threads` 看一次慢查询的瓶颈。
    4. **路由分片**(重建时做):按 `brandId` 路由,单机也能减负。
    5. **向量维度与 HNSW 参数**(评测后再决定):512/384 维 + `m=24`/`ef_construction=200`。

    按以上步骤执行,通常能把你现在的 400ms 稳定进一步拉低,并消除 30s 的长尾。需要我根据你的机器配置( CPU/内存/磁盘)和返回字段再细化 `k/num_candidates/size` 的组合,也可以直接给出一套“场景化参数表”。
    chen11
        4
    chen11  
       17 天前
    @yangxin0 直接贴 AI 要被 ban
    chihiro2014
        5
    chihiro2014  
       17 天前
    @cxy1234 机械盘,换 ssd 比啥都立竿见影啊
    softnero
        6
    softnero  
       17 天前
    ES 的向量化做的比较差,如果要求高性能最好用 VectorDB 比如 Milvus 这种
    softnero
        7
    softnero  
       17 天前
    之前我们有实践,300w 的文本 embedding 后分别放在 ES 和 Vector DB 中,响应能差 10 倍左右
    midsolo
        8
    midsolo  
       17 天前
    “部分查询条件 400ms 左右返回”,可能走的 filesystem 或者返回数据量偏小,"偶尔查询出现 30s",大概率扫的机械硬盘。

    在单机部署且是机械硬盘的情况下,ES 想提速就只能想办法把数据扔到 filesystem 中,让查询走 os cache ,可以写个后台预热的程序,定时刷。

    我这也是用 ES 做的向量库,一共 2000 多万条数据,一般查询 200ms 左右就能返回响应。
    adgfr32
        9
    adgfr32  
       17 天前 via Android   ❤️ 2
    @yangxin0 不是哥们,问 AI 他自己不会啊,你发的东西验证过吗,我手机看还得往下滚好久。
    在论坛发 AI 生成的东西,就像是吃拉出来的东西一样。
    yangxin0
        10
    yangxin0  
       16 天前
    GPT5 Pro 的回答,这条回答 5 美金。
    yangxin0
        11
    yangxin0  
       16 天前
    @adgfr32 GPT5 Pro 的回答,这条回答 5 美金。
    kcross
        12
    kcross  
       16 天前
    内存呢? 内存对 knn 影响也挺大的
    adgfr32
        13
    adgfr32  
       16 天前 via Android
    @yangxin0 是这么算的吗,prompt ,completion token 加价格算下呢,用 GPT 数学会变差?
    a812159920
        14
    a812159920  
       16 天前
    我可以看,加我 wx:dXVoMjAxNA== (base 64)
    nekoneko
        15
    nekoneko  
       15 天前
    才 200w 数据而且用的 KNN 怎么会这么慢. 贴下机器配置和网络配置吧.
    我之前做的向量匹配 500w 数据用余弦相似度才 2s
    cxy1234
        16
    cxy1234  
    OP
       15 天前
    @softnero 嗯嗯,效果实在提升不上去就得换其他库来存了
    cxy1234
        17
    cxy1234  
    OP
       15 天前
    @midsolo 这个目前再试,通过 index.store.preload 来设置,把一些索引放到内存里,速度还比较快。后续先扩一下内存
    cxy1234
        18
    cxy1234  
    OP
       15 天前
    @kcross 总内存 128G ,分配 es 32G,buff/cache 大约 45G
    cxy1234
        19
    cxy1234  
    OP
       15 天前
    @a812159920 大佬,我这申请了
    关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   Solana   ·   3086 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 12:55 · PVG 20:55 · LAX 04:55 · JFK 07:55
    ♥ Do have faith in what you're doing.