work hard work smart

专注于AI+Java后端开发。 不断总结,举一反三。
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

如何使用 Elasticsearch 进行全文检索和向量检索

Posted on 2026-06-20 15:08  work hard work smart  阅读(0)  评论(0)    收藏  举报

Elasticsearch 具有全文检索能力和向量检索能力。全文检索适合解决“关键词匹配、分词、相关性排序”的问题,向量检索适合解决“语义相似、意思接近但表达不同”的问题。

在实际的 AI 应用,尤其是 RAG 知识库场景中,这两种检索方式通常不会孤立使用,而是会组合起来使用:

  • 全文检索:找到和用户问题中关键词高度相关的内容
  • 向量检索:找到语义上相似的内容
  • 混合检索:同时利用关键词匹配和语义匹配,提高召回质量

本文单独介绍如何使用 Elasticsearch 实现全文检索和向量检索。


一、为什么 Elasticsearch 既能做全文检索,也能做向量检索?

Elasticsearch 最早是典型的全文搜索引擎,底层基于 Lucene,擅长处理倒排索引、分词、相关性评分等搜索场景。

随着 AI 和大模型应用的发展,Elasticsearch 也支持了向量字段和向量相似度搜索,可以将文本 embedding 之后的向量存储在 ES 中,并通过相似度算法进行检索。

所以现在的 Elasticsearch 可以同时支持两类数据:

{
  "content": "Elasticsearch 是一个分布式搜索和分析引擎",
  "content_vector": [0.12, 0.31, -0.22]
}

其中:

  • content 用于全文检索
  • content_vector 用于向量检索

二、准备索引结构

假设我们要构建一个知识库,每条文档切片包含以下字段:

  • doc_id:文档 ID
  • chunk_id:切片 ID
  • title:标题
  • content:文本内容
  • content_vector:文本向量
  • metadata:额外元数据

可以创建如下索引:

PUT knowledge_chunks
{
  "mappings": {
    "properties": {
      "doc_id": {
        "type": "keyword"
      },
      "chunk_id": {
        "type": "keyword"
      },
      "title": {
        "type": "text",
        "analyzer": "ik_max_word",
        "search_analyzer": "ik_smart"
      },
      "content": {
        "type": "text",
        "analyzer": "ik_max_word",
        "search_analyzer": "ik_smart"
      },
      "content_vector": {
        "type": "dense_vector",
        "dims": 1024,
        "index": true,
        "similarity": "cosine"
      },
      "metadata": {
        "type": "object",
        "enabled": true
      },
      "created_at": {
        "type": "date"
      }
    }
  }
}

这里有几个关键点。

1. text 字段用于全文检索

"content": {
  "type": "text",
  "analyzer": "ik_max_word",
  "search_analyzer": "ik_smart"
}

如果是中文场景,通常会配合 IK 分词器:

  • ik_max_word:索引阶段尽可能细粒度分词,提高召回
  • ik_smart:搜索阶段更智能地分词,减少噪音

2. dense_vector 字段用于向量检索

"content_vector": {
  "type": "dense_vector",
  "dims": 1024,
  "index": true,
  "similarity": "cosine"
}

其中:

  • dims 要和 embedding 模型输出维度一致
  • index: true 表示支持近似最近邻检索
  • similarity: cosine 表示使用余弦相似度

不同 embedding 模型的维度不同,比如 384、768、1024、1536 等,建索引时一定要保持一致。


三、写入文档和向量

在写入 ES 之前,需要先对文档进行切片,然后对每个切片生成 embedding。

例如原始文档是:

Elasticsearch 是一个分布式搜索和分析引擎,支持全文检索、结构化查询、聚合分析和向量检索。

经过 embedding 模型处理后,得到一个向量:

[0.12, -0.04, 0.33]

然后写入 ES:

POST knowledge_chunks/_doc/1
{
  "doc_id": "doc_001",
  "chunk_id": "chunk_001",
  "title": "Elasticsearch 简介",
  "content": "Elasticsearch 是一个分布式搜索和分析引擎,支持全文检索、结构化查询、聚合分析和向量检索。",
  "content_vector": [0.12, -0.04, 0.33],
  "metadata": {
    "source": "es-guide.md",
    "page": 1
  },
  "created_at": "2026-06-20T10:00:00"
}

实际使用时,content_vector 会是完整维度的向量,这里只是简化示例。


四、使用 ES 进行全文检索

全文检索主要依赖 matchmulti_matchbool 等查询。

1. 单字段全文检索

如果只搜索正文内容,可以这样写:

POST knowledge_chunks/_search
{
  "query": {
    "match": {
      "content": "Elasticsearch 如何进行全文检索"
    }
  }
}

ES 会对用户输入进行分词,然后到倒排索引中查找相关文档,并根据 BM25 算法计算相关性得分。

2. 多字段全文检索

在知识库场景中,标题和正文都可能命中,所以更常见的是 multi_match

POST knowledge_chunks/_search
{
  "query": {
    "multi_match": {
      "query": "Elasticsearch 向量检索怎么做",
      "fields": [
        "title^2",
        "content"
      ]
    }
  }
}

这里的:

"title^2"

表示标题字段权重更高。如果标题命中,得分会比正文命中更高。

3. 带过滤条件的全文检索

如果只想搜索某个文档、某个知识库、某个用户的数据,可以加 filter

POST knowledge_chunks/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "multi_match": {
            "query": "全文检索和向量检索的区别",
            "fields": [
              "title^2",
              "content"
            ]
          }
        }
      ],
      "filter": [
        {
          "term": {
            "doc_id": "doc_001"
          }
        }
      ]
    }
  }
}

must 会影响相关性评分,filter 不参与评分,适合放权限、租户、文档 ID、知识库 ID 这类精确条件。


五、使用 ES 进行向量检索

向量检索的核心思路是:

  1. 用户输入一个问题
  2. 使用 embedding 模型将问题转换成向量
  3. 用问题向量到 ES 中查找最相似的文档向量

例如用户问题是:

ES 能不能做语义搜索?

经过 embedding 模型后得到:

[0.21, -0.13, 0.08]

然后使用 knn 查询:

POST knowledge_chunks/_search
{
  "knn": {
    "field": "content_vector",
    "query_vector": [0.21, -0.13, 0.08],
    "k": 5,
    "num_candidates": 100
  },
  "_source": [
    "doc_id",
    "chunk_id",
    "title",
    "content",
    "metadata"
  ]
}

参数说明:

  • field:向量字段名
  • query_vector:用户问题的向量
  • k:最终返回最相似的前 N 条
  • num_candidates:候选数量,越大召回越充分,但查询成本也越高

一般来说:

num_candidates >= k

例如:

k = 5
num_candidates = 100

表示先从近似向量索引中召回 100 个候选,再返回最相似的 5 个。


六、带过滤条件的向量检索

实际业务中,向量检索通常也需要过滤。例如只检索某个知识库、某个用户、某个文件下的内容。

可以这样写:

POST knowledge_chunks/_search
{
  "knn": {
    "field": "content_vector",
    "query_vector": [0.21, -0.13, 0.08],
    "k": 5,
    "num_candidates": 100,
    "filter": {
      "term": {
        "doc_id": "doc_001"
      }
    }
  },
  "_source": [
    "doc_id",
    "chunk_id",
    "title",
    "content",
    "metadata"
  ]
}

这样 ES 只会在符合过滤条件的文档中进行向量相似度检索。


七、全文检索和向量检索的区别

全文检索和向量检索解决的是两类不同问题。

对比项 全文检索 向量检索
核心能力 关键词匹配 语义相似
底层方式 倒排索引 向量相似度
典型算法 BM25 cosine、dot_product、l2_norm
优点 精确、可解释、适合关键词 能理解语义,适合同义表达
缺点 对同义词、改写表达不敏感 结果可解释性较弱,依赖 embedding 质量
适合场景 标题、术语、错误码、编号、关键词 问答、知识库、语义搜索、RAG

举个例子。

用户搜索:

ES 怎么做语义搜索?

如果文档里写的是:

Elasticsearch 支持基于 dense_vector 的相似度召回。

全文检索可能因为关键词不完全匹配而得分不高。

但向量检索可以理解“语义搜索”和“相似度召回”之间的语义关系,从而更容易召回这条内容。

反过来,如果用户搜索:

ERROR_CODE_10086

这种精确错误码,全文检索通常会比向量检索更可靠。


八、混合检索:同时使用全文检索和向量检索

在 RAG 场景中,单独使用全文检索或者单独使用向量检索都可能有问题。

只用全文检索:

  • 关键词不匹配时召回不到
  • 同义表达、改写表达效果不好

只用向量检索:

  • 对专有名词、编号、代码、错误码不一定稳定
  • 结果可解释性弱
  • embedding 模型质量会直接影响召回效果

所以更推荐使用混合检索。

常见做法有两种。

方案一:分别检索,再合并排序

先执行全文检索:

POST knowledge_chunks/_search
{
  "size": 10,
  "query": {
    "multi_match": {
      "query": "Elasticsearch 如何实现向量检索",
      "fields": [
        "title^2",
        "content"
      ]
    }
  }
}

再执行向量检索:

POST knowledge_chunks/_search
{
  "knn": {
    "field": "content_vector",
    "query_vector": [0.21, -0.13, 0.08],
    "k": 10,
    "num_candidates": 100
  }
}

然后在应用层合并结果:

  1. 根据 chunk_id 去重
  2. 对全文检索分数和向量检索分数做归一化
  3. 按加权分数重新排序
  4. 取 Top N 结果交给大模型

伪代码如下:

List<SearchResult> textResults = elasticsearchFullTextSearch(query);
List<SearchResult> vectorResults = elasticsearchVectorSearch(queryVector);

Map<String, SearchResult> merged = new HashMap<>();

for (SearchResult result : textResults) {
    result.setTextScore(normalize(result.getScore()));
    merged.put(result.getChunkId(), result);
}

for (SearchResult result : vectorResults) {
    SearchResult existing = merged.get(result.getChunkId());
    if (existing == null) {
        result.setVectorScore(normalize(result.getScore()));
        merged.put(result.getChunkId(), result);
    } else {
        existing.setVectorScore(normalize(result.getScore()));
    }
}

List<SearchResult> finalResults = merged.values().stream()
    .peek(result -> result.setFinalScore(
        result.getTextScore() * 0.4 + result.getVectorScore() * 0.6
    ))
    .sorted(Comparator.comparing(SearchResult::getFinalScore).reversed())
    .limit(5)
    .toList();

这种方式最灵活,也最容易控制业务逻辑。

方案二:使用 ES 的混合查询

部分 Elasticsearch 版本支持在一次查询中同时使用 queryknn

POST knowledge_chunks/_search
{
  "size": 5,
  "query": {
    "multi_match": {
      "query": "Elasticsearch 如何实现向量检索",
      "fields": [
        "title^2",
        "content"
      ]
    }
  },
  "knn": {
    "field": "content_vector",
    "query_vector": [0.21, -0.13, 0.08],
    "k": 10,
    "num_candidates": 100
  }
}

这种方式写起来更简单,但是不同 ES 版本对混合查询的支持方式可能不同,实际使用时需要结合具体版本确认。

如果业务需要更精细的去重、归一化、重排序、权限过滤,通常还是建议在应用层分别检索后合并。


九、RAG 场景中的推荐流程

在知识库问答系统中,可以采用如下流程:

用户问题
   |
   v
问题改写 / 查询增强
   |
   v
生成 query embedding
   |
   v
全文检索 + 向量检索
   |
   v
结果合并去重
   |
   v
重排序 rerank
   |
   v
拼接上下文
   |
   v
调用大模型生成答案

其中 ES 主要负责:

  • 存储文档切片
  • 存储切片向量
  • 执行全文检索
  • 执行向量检索
  • 支持过滤条件,比如知识库 ID、文件 ID、用户 ID

大模型主要负责:

  • 理解用户问题
  • 生成 query embedding
  • 根据召回内容生成自然语言答案

十、在 Java 中如何调用

如果项目是 Java 或 Spring Boot,可以通过 Elasticsearch Java Client 调用。

全文检索示例:

SearchResponse<KnowledgeChunk> response = client.search(s -> s
        .index("knowledge_chunks")
        .query(q -> q
                .multiMatch(m -> m
                        .query("Elasticsearch 如何实现全文检索")
                        .fields("title^2", "content")
                )
        )
        .size(5),
    KnowledgeChunk.class
);

向量检索示例:

SearchResponse<KnowledgeChunk> response = client.search(s -> s
        .index("knowledge_chunks")
        .knn(knn -> knn
                .field("content_vector")
                .queryVector(queryVector)
                .k(5)
                .numCandidates(100)
        )
        .source(src -> src
                .filter(f -> f
                        .includes("doc_id", "chunk_id", "title", "content", "metadata")
                )
        ),
    KnowledgeChunk.class
);

如果使用 Spring AI,也可以把 Elasticsearch 作为 VectorStore 的实现,让框架帮我们封装一部分向量写入和相似度搜索逻辑。


十一、实践建议

1. 文档切片不要太大

如果切片太大,召回结果会包含很多无关内容;如果切片太小,又可能缺少上下文。

常见切片大小可以从下面范围开始调试:

chunk size: 500 ~ 1000 tokens
overlap: 50 ~ 150 tokens

具体大小要结合业务文档类型调整。

2. 向量维度必须和索引 mapping 一致

如果 mapping 中定义:

"dims": 1024

那么写入的每个向量都必须是 1024 维,否则会写入失败。

3. 不要只依赖向量检索

向量检索擅长语义召回,但对于以下内容,全文检索通常更可靠:

  • 订单号
  • 错误码
  • 类名
  • 方法名
  • API 名称
  • 专有名词
  • 精确短语

所以生产环境更推荐混合检索。

4. 需要做权限过滤

如果是多用户、多租户知识库,检索时必须增加过滤条件,例如:

"filter": {
  "term": {
    "user_id": "user_001"
  }
}

避免用户检索到不属于自己的文档内容。

5. 检索后最好增加 rerank

全文检索和向量检索负责召回,召回结果不一定是最终最适合给大模型的内容。

可以在召回 Top 20 或 Top 50 后,再通过 rerank 模型重新排序,最后取 Top 5 或 Top 10 作为上下文。


十二、总结

Elasticsearch 现在不仅可以做传统的全文检索,也可以做向量检索,非常适合作为 RAG 知识库的检索底座。

全文检索适合解决关键词匹配问题,向量检索适合解决语义相似问题。两者各有优势,也各有短板。

在真实业务中,更推荐使用混合检索:

全文检索负责精确匹配
向量检索负责语义召回
应用层负责合并、去重、重排序
大模型负责基于上下文生成答案

这样既能保证关键词、术语、错误码等精确内容不丢失,也能提升用户自然语言提问时的语义召回效果。

对于 AI 知识库、智能问答、企业文档搜索等场景,Elasticsearch 的全文检索 + 向量检索组合,是一个非常实用且容易落地的方案。