登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  科技周边 >  人工智能

向量检索怎样按文档语言过滤召回结果

来源:17golang原创

时间:2026-10-21 01:08:59 172浏览 收藏

向量检索默认按 embedding 的相似度排序,但“相似”不等于“符合语言要求”。如果知识库同时包含中文、英文和多语言版本,直接扩大 topK 再在应用层筛选,常见结果是候选被过滤掉后数量不足,甚至让不同语言的版本互相竞争。更稳妥的做法是把语言当作规范化元数据,在向量检索时作为硬约束,再让相似度在约束范围内排序。

官方地址:https://qdrant.tech/

下面用 Qdrant 说明这种架构,重点不在某个数据库的 API 记忆,而在“字段怎么定义、过滤放在哪里、结果不足怎么办”这三个决策。

为什么不能只把语言写进查询文本

“请用中文回答”适合约束生成模型,“只检索中文文档”却是检索集合的约束。把 中文 追加到用户问题中,embedding 可能仍然把语义相近但语言不同的文档排在前面,因为语言信息未必在向量空间里形成可靠的硬边界。

应用层后过滤也有一个容易忽略的损失:假设系统先取 5 条,再丢掉其中 3 条英文文档,最终只剩 2 条中文结果。把 topK 调大可以缓解,但它不是保证;当目标语言占比很低、文档分布不均或相似度阈值较紧时,仍可能没有足够候选。

文档分块、语言元数据、向量和 payload 索引之间的静态关系说明图
图1:语言字段与向量分开承担职责,属于静态结构图,不是运行截图或执行证据。

因此要先回答一个架构问题:语言是“希望结果尽量满足的偏好”,还是“结果必须满足的业务条件”。多语言客服知识库、合规文档和按地区发布的说明,通常属于后者;这时语言字段应该进入过滤条件,而不是只进入提示词。

入库时把语言设计成稳定的元数据

过滤能否可靠,首先取决于入库数据是否一致。建议每个文档分块至少保存 language、document_id 和 chunk_id,其中语言值采用有限且明确的编码,例如 zh-CN、zh-TW、en,不要一部分记录写“中文”、另一部分写“简体中文”。

{
  "document_id": "manual-042",
  "chunk_id": "manual-042-07",
  "language": "zh-CN",
  "text": "向量检索会先根据过滤条件缩小可参与排序的文档集合。"
}

上面的 JSON 是数据形状示例,保持严格 JSON 格式,不在其中插入注释。真正入库时要把同一套规范应用到所有分块:语言识别失败时不要悄悄写成默认语言,可以保存 und 或进入待处理队列;否则“未知”会被误当成“中文”,造成难以解释的召回偏差。

语言字段适合做精确匹配,而不是全文检索。Qdrant 文档将这类字段放在 payload 中,并建议为参与过滤的字段创建对应索引;对于语言代码这类离散字符串,使用 keyword 类型比把它当作普通文本拆词更符合意图。

用 keyword 索引在检索阶段过滤

先创建语言字段的 payload index,再在查询中把它放进过滤条件。索引最好在批量写入前建立;如果数据已经写入,新增索引可能还需要重建向量图,才能获得完整的过滤优化。

{
  "field_name": "language",
  "field_schema": "keyword"
}

查询时使用精确值:

{
  "query": [0.12, -0.08, 0.44, 0.31],
  "filter": {
    "must": [
      {"key": "language", "match": {"value": "zh-CN"}}
    ]
  },
  "limit": 8,
  "with_payload": true
}

这里的关键不是把 limit 写成 8,而是让过滤条件在向量搜索范围内生效。这样返回的每个候选都先满足语言约束,再按照向量相似度竞争;如果还要加租户、产品线或时间范围,就把它们作为额外的结构化条件组合,而不是把这些值拼进自然语言查询。

如果应用使用 Go,可以把请求边界收敛在一个函数中,避免不同调用方自行拼写语言值:

package search

import (
    "context"
    "fmt"

    "github.com/qdrant/go-client/qdrant"
)

func LanguageCondition(language string) (*qdrant.Filter, error) {
    // 只允许已登记的语言代码,避免把用户输入直接变成过滤字段。
    switch language {
    case "zh-CN", "zh-TW", "en":
        return &qdrant.Filter{
            Must: []*qdrant.Condition{qdrant.NewMatch("language", language)},
        }, nil
    default:
        return nil, fmt.Errorf("unsupported document language: %s", language)
    }
}

func BuildRequest(ctx context.Context, language string) (*qdrant.Filter, error) {
    // 统一入口让上层在空结果时可以识别是过滤条件导致的限制。
    if err := ctx.Err(); err != nil {
        return nil, err
    }
    return LanguageCondition(language)
}

示例只负责构造过滤对象,没有在本地连接 Qdrant,也没有假设某个查询结果。生产代码还应把语言代码作为内部枚举或配置值传入,并在请求取消时及时停止后续调用。

结果变少时先查过滤边界

加入语言条件后结果减少并不一定是索引故障。先把问题分成三类:第一,目标语言的数据本来就少;第二,语言字段缺失、大小写不一致或使用了错误别名;第三,过滤和向量搜索组合后候选集合太窄。

用户问题向量、语言过滤器、候选集合、相似度排序和空结果策略的静态关系图
图2:过滤条件改变候选集合后,相似度排序只在剩余集合内工作;这是结果边界说明图,不是运行结果截图。

可以分别记录“过滤前可用文档数”“过滤后候选数”“最终返回数”和“未满足数量的请求比例”。这些是观测指标,不是向量库自动给出的统一字段;应用需要在检索适配层自行记录,才能区分标注问题和召回能力问题。

当结果不足时,降级策略要显式化。对必须满足语言的场景,应该返回少量结果或空结果,并提示知识库没有足够的目标语言内容;对语言只是偏好的场景,可以先按目标语言过滤,低于阈值时再扩大到同语系或全部语言,但必须把降级状态传给生成层,避免模型把跨语言资料误说成同语言原文。

不要用“扩大 topK 后再静默过滤”替代这个决策。它会把结果数量和延迟一起交给经验值,且无法解释为什么某次请求跨过了语言边界。

一张清单判断方案是否稳

  • 语言值是否有固定字典,且所有分块遵守同一种大小写和地区码规则?
  • 语言字段是否以精确匹配索引,而不是依赖全文分词或查询文本提示?
  • 过滤是否发生在向量候选参与排序之前,还是只在应用层做事后删除?
  • 目标语言没有足够内容时,系统是返回空结果、扩大范围,还是交给人工补充?这个选择是否可观测?
  • 新增字段索引后,是否考虑已有向量图的重建成本和上线窗口?

如果语言是硬性业务约束,就把它放进检索过滤器;如果语言只是排序偏好,则可以做多路召回或重排,但不要把两种语义混在同一个字段里。稳定的元数据、正确的索引类型和明确的空结果策略,才是多语言向量检索可维护的边界。

常见问题

语言字段应该存语言名称还是代码?

优先存稳定代码,例如 zh-CN 或 en,展示名称可以在应用层映射。代码更容易做精确匹配,也便于后续增加地区或脚本维度。

只过滤中文,能保证返回内容全是中文吗?

只能保证“语言元数据为中文”的记录进入结果。若入库标注错误、一个分块混合多种语言,过滤器不会替你重新识别正文,所以标注质量仍是召回边界的一部分。

为什么加了过滤器后相似度看起来变低?

过滤把竞争集合缩小了,返回的是目标语言集合中的相似结果,不再是全库的前几名。应分别观察过滤前后候选规模和业务可用性,不要直接拿两个分数做结论。

参考资料

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>