JAVA程序员自救之路——Elasticsearch向量搜索

createh511小时前技术教程1


什么是向量搜索

向量搜索是一种基于向量相似度的信息检索技术,它通过将数据表示为高维向量,然后计算这些向量之间的距离或相似度来找到最相关的结果。适用于相似商品推荐,语义搜索,个性化匹配等场景。常用的数据工具有Milvus,Pinecone,ES,Redis Vector等。此篇文章重点介绍ES的向量搜索功能。

ES向量搜索

Elasticsearch 从 7.x 版本开始逐步引入了向量搜索功能,并在后续版本中不断增强。Elasticsearch 的向量搜索功能使其成为 传统搜索 + AI 搜索 的混合解决方案,适用于:

  • 已经使用 ES 的用户,希望平滑过渡到向量搜索。
  • 需要结合关键词 + 语义搜索的场景。
  • 推荐系统、多模态搜索等 AI 驱动应用。

场景

示例

语义搜索

搜索 "苹果" 同时返回 "iPhone" 和 "水果"

推荐系统

"看了 A 电影的用户也喜欢 B"

图像/视频搜索

上传图片搜索相似图片

异常检测

查找与正常模式差异较大的数据

向量类型

ES支持两种向量类型,一种是稠密向量(dense_vector),用于存储浮点数数组(如神经网络生成的嵌入向量)。BERT/Word2Vec 生成的文本嵌入、CLIP 生成的图像嵌入、openai的embending嵌入等。另一种是稀疏向量(sparse_vector),这个是8.0+版本后新加入的类型,适用于高维但大部分值为 0 的向量(如 TF-IDF、BM25 生成的向量),传统信息检索模型生成的向量。

"embedding": {
  "type": "dense_vector",
  "dims": 384,  // 向量维度(如 OpenAI text-embedding-ada-002 是 1536 维)
  "index": true, // 是否建立索引(用于近似搜索)
  "similarity": "cosine"  // 相似度计算方式
}
"sparse_embedding": {
  "type": "sparse_vector"
}

向量索引方式

(1) 精确搜索(Exact k-NN)

  • 计算查询向量与索引中所有向量的距离,返回最相似的 k 个结果。
  • 优点:结果精确。
  • 缺点:计算量大,不适合大规模数据。

(2) 近似最近邻搜索(Approximate k-NN,8.0+)

  • 使用 HNSW(Hierarchical Navigable Small World) 算法加速搜索。
  • 核心参数
    • m("index_options": { "type": "hnsw", "m": 16 }):影响索引构建速度和搜索精度。
    • ef_construction:控制索引构建时的候选数量,影响召回率。
  • 优点:搜索速度快,适用于大规模数据。
  • 缺点:牺牲少量精度换取性能。

相似度计算方式

ES 支持多种向量相似度计算方式:

相似度类型

计算方式

适用场景

cosine

余弦相似度

文本相似度(默认推荐)

dot_product

点积(需归一化)

推荐系统

l2_norm

欧几里得距离

图像检索

l1_norm

曼哈顿距离

较少使用

向量搜索查询方式

(1) 纯向量搜索(knn查询)

GET /my_vector_index/_search
{
  "knn": {
    "field": "embedding",
    "query_vector": [0.1, 0.2, ..., 0.9], // 查询向量
    "k": 5,  // 返回 Top 5 结果
    "num_candidates": 100  // 近似搜索的候选数量
  }
}

(2) 混合搜索(结合关键词 + 向量)

GET /my_vector_index/_search
{
  "query": {
    "match": { "title": "手机" }  // 传统关键词搜索
  },
  "knn": {
    "field": "embedding",
    "query_vector": [0.1, 0.2, ..., 0.9],
    "k": 5,
    "boost": 0.5  // 向量搜索的权重
  }
}

(3) 多向量搜索(8.9+)

GET /my_vector_index/_search
{
  "knn": [
    {
      "field": "text_embedding",
      "query_vector": [0.1, 0.2, ...],
      "k": 3
    },
    {
      "field": "image_embedding",
      "query_vector": [0.5, 0.6, ...],
      "k": 2
    }
  ]
}

性能优化建议

1.调整 HNSW 参数

  • m(默认 16):增大可提升召回率,但会降低索引速度。
  • ef_construction(默认 100):增大可提升索引质量,但会占用更多内存。

2.使用量化(PQ):减少内存占用,适用于超大规模数据。(8.4+)

3.结合过滤:先使用 filter 减少搜索范围,再执行向量搜索。

ES在Spring中如何使用向量

  1. 首先引入spring-boot-starter-data-elasticsearch,本文使用的jdk版本是17,springboot版本是3.4.4,ES版本的8.15.5。如果是其他版本,需要对应其他版本的实现方式。
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>
  1. 然后在已有的ES实体中加入向量字段List<Float> vector。

注意:有的SDK版本是Float[],新版本是List<Float>。

package cn.pt.search.star.dto;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

import java.util.Date;
import java.util.List;

@Data
@Document(indexName = "custom_index_search")
public class CustomerDTO {
    @Id
    @Field(type = FieldType.Long)
    private Long id;
    @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
    private String name;
    @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
    private String description;
    @Field(type = FieldType.Text, analyzer = "whitespace", searchAnalyzer = "whitespace")
    private String label;
    @Field(name = "detail_address", type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
    private String detailAddress;
    @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
    private String remark;
    @Field(type = FieldType.Dense_Vector, dims = 1024, index = true, similarity = "cosine")
    private List<Float> vector;
}
  1. 重建索引,我们会看到ES的索引映射会多一个vector的字段。
"vector": {
        "type": "dense_vector",
        "dims": 1024,
        "index": true,
        "similarity": "cosine",
        "index_options": {
          "type": "int8_hnsw",
          "m": 16,
          "ef_construction": 100
        }
}
  1. 使用自建BERT计算向量,或者使用大模型embending,将数据向量化。这里使用了硅基流动的api。
    public static List<Float> callSiJiAPI(String text) {
        String result = HttpRequest.post("https://api.siliconflow.cn/v1/embeddings")
                .header("Authorization", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
                .header("Content-Type", "application/json")
                .body("{\n  \"model\": \"xxxxxxxxxxxxxxxx\",\n  \"input\": \""+text+"\",\n  \"encoding_format\": \"float\"\n}")
                .execute().body();
        JSONObject jsonObject = JSONUtil.parseObj(result);
        JSONArray embedding =  jsonObject.getJSONArray("data").getJSONObject(0).getJSONArray("embedding");
        List<Float> vector = new ArrayList<>();
        for(int i=0;i<embedding.size();i++){
            vector.add(embedding.getFloat(i));
        }
        return vector;
    }
  1. 将生成的vector,赋值到CustomerDTO中,直接保存。这样具有向量字段的索引就建好并且有数据了。接下来,我们就可以搜索了。
  2. 搜索的api有点恶心,吐槽一下SpringDataElasticsearch的sdk,每个版本的实现方式都不太一样,从网上找到的代码COPY到本地就行不通。问deepseek就是版本不一致,或者直接就瞎说。走了很多弯路。其实还是看官方文档比较靠谱。下面是代码,还是比较简单的。

首先,需要把搜索条件向量化,然后构造搜索条件。

        List<Float> queryVector = VectorUtil.callSiJiAPI(keyword);        
				BoolQuery boolQuery = BoolQuery.of(bq -> bq
                .should(
                        Query.of(q -> q.match(MatchQuery.of(mq-> mq
                                .field("name")
                                .query(keyword).analyzer("ik_smart")
                                .minimumShouldMatch("1")
                                .boost(1.0f)
                        ))),
                        Query.of(q -> q.match(MatchQuery.of(mq-> mq
                                .field("description")
                                .query(keyword).analyzer("ik_smart")
                                .minimumShouldMatch("1")
                                .boost(1.0f)
                        ))),
                        Query.of(q -> q.match(MatchQuery.of(mq-> mq
                                .field("label")
                                .query(keyword).analyzer("whitespace")
                                .minimumShouldMatch("1")
                                .boost(1.0f)
                        ))),
                        Query.of(q -> q.match(MatchQuery.of(mq-> mq
                                .field("detailAddress")
                                .query(keyword).analyzer("ik_smart")
                                .minimumShouldMatch("1")
                                .boost(1.0f)
                        )))
                )
        );

        // 构建 NativeQuery
        NativeQuery query = NativeQuery.builder()
                .withQuery(Query.of(q -> q.bool(boolQuery)))
                .withKnnSearches(Lists.newArrayList(
                        KnnSearch.of(kq -> kq
                        .field("vector").queryVector(queryVector)
                        .k(10)
                        .numCandidates(100).boost(0.5f).similarity(0.8f)
                )))
                .build();

        log.info("查询条件: {},{}", query.getQuery(), query.getKnnSearches());

        // 执行搜索
        SearchHits<CustomerDTO> searchHits = elasticsearchTemplate.search(query, CustomerDTO.class);

这样,带有分词+向量的混合检索就实现好了。

效果如下:返回的内容并没有传媒两字,肯定不是传统分词查出来的,所以应该是向量搜索的效果。

相关文章

java数据类型的转换以及精度丢失

一.浮点类型在计算机当中的存储float存储需求是4字节(32位), 其中1位最高位是符号位,中间8位表示阶位,后32位表示值 float的范围: -2^128 ~ +2^128,也即-3.40E+3...

一个时间戳精度问题,引发了一个MySQL血案

文章来源:https://dwz.cn/ajsWJTWv作者:阿杜的世界最近工作中遇到两例mysql时间戳相关的问题,一个是mysql-connector-java和msyql的精度不一致导致数据查不...

java学习笔记,一个学霸朋友的超详细java笔记

java学习笔记这是我一个学霸朋友学习java时一点一点积累下来的笔记,分享给大家,希望能帮到刚学java或是想学java的你。绝不是什么网上复制粘贴下来的,内容都是很基础很重要的知识点!一、基础知识...

当Java遇见大模型:本地化AI集成方案

一、DJL框架运行Stable Diffusion原理剖析痛点分析传统Java应用处理生成式AI面临三大挑战:显存管理缺失导致OOM崩溃(GPU利用率<30%)Python生态工具链难以复用多模...

别再堆技术栈了!90%的Java简历死在这3个坑,改完涨薪30%

别再堆技术栈了!90%的Java简历死在这3个坑,改完涨薪30%你知道为啥你投100份简历都没回音吗?因为你的项目描述写的像产品说明书!我见过最蠢的写法:使用SpringBoot+MyBatis开发后...