LangChain4j + Elasticsearch 实现企业级向量存储(支持混合检索、元数据过滤)

LangChain4j + Elasticsearch 实现企业级向量存储(支持混合检索、元数据过滤)

一、前言

在Java RAG项目开发中,很多项目不想额外部署 Milvus、Qdrant 等专用向量库,希望复用现有Elasticsearch集群实现向量存储+语义检索+全文检索。

本文基于 ES8.14.3 + LangChain4j 自研实现一套完整的 EmbeddingStore 向量存储实现类,替代官方简陋实现,支持动态索引创建、余弦向量检索、BM25全文检索、元数据过滤、批量删除,完全满足企业级知识库RAG场景。

二、技术栈说明

  • 核心框架:LangChain4j(标准EmbeddingStore接口)

  • 存储引擎:Elasticsearch 8.x(dense_vector 原生向量类型)

  • 检索能力:余弦相似度向量检索 + BM25关键词检索(混合检索)

  • 扩展能力:支持元数据过滤、按知识库批量删除、单条删除

三、核心设计亮点(区别官方原生实现)

1. 动态自适应索引创建

官方ES向量存储需要手动指定向量维度,极易报错。本实现:

  • 首次写入向量时自动获取真实向量维度创建索引

  • 双重检查锁保证线程安全,避免多服务启动重复创建索引

  • 索引默认开启 余弦相似度(cosine) 匹配

2. 双检索模式(向量+全文混合)

  • 语义检索:基于 script_score 实现余弦相似度检索,解决语义匹配问题

  • 关键词检索:自定义BM25接口,支持传统文本召回,弥补向量检索关键词丢失问题

  • 分数归一化处理,统一返回结构,适配RAG召回逻辑

3. 完善的元数据过滤能力

原生LangChain4j过滤器支持不完善,本实现手动适配:

  • 支持 IsEqualTo 精准匹配(知识库隔离、文件过滤)

  • 支持 IsIn 多值匹配

  • 自动映射 metadata 层级字段,业务隔离更方便

4. 完善的数据管理能力

  • 按知识库ID批量删除向量数据

  • 按单文档ID精准删除

  • 全覆盖异常捕获+日志打点,线上问题好排查

四、ES索引结构设计

自定义结构化文档,兼顾可读性与检索性能:

json 复制代码
{
  "id": "文档唯一UUID",
  "text": "文本分片内容",
  "embedding": [向量浮点数组],
  "metadata": {
    "knowledgeBaseId": "知识库ID",
    "fileName": "文件名",
    "chunkIndex": "分片序号"
  }
}
  • id:keyword 唯一主键

  • text:text 分词字段,用于BM25检索

  • embedding:dense_vector 向量字段,余弦相似度检索

  • metadata:object 扩展字段,用于业务过滤隔离

五、核心功能原理

1. 余弦向量检索原理

通过 script_score 自定义打分公式,解决ES原生向量打分范围为[-1,1]的问题,统一转为[0,2]后业务使用更友好:

cosineSimilarity\(params\.queryVector, \&\#39;embedding\&\#39;\) \+ 1\.0

2. 线程安全索引初始化

使用 volatile + 双重检查锁,保证多线程、多实例部署下,索引只会初始化一次,避免索引重复创建、mapping冲突问题。

3. 兼容LangChain4j标准接口

完全实现 EmbeddingStore<TextSegment> 所有抽象方法,可无缝对接:

  • 向量单条/批量新增

  • 带元数据文本分片存储

  • 标准EmbeddingSearchRequest检索

六、解决的业务痛点

  1. 无需新增中间件:复用现有ES集群,降低运维成本

  2. 解决维度固定问题:官方实现需固定维度,本代码动态适配

  3. 解决知识库隔离问题:通过metadata过滤完美实现多租户、多知识库隔离

  4. 解决召回单一问题:向量语义召回 + BM25关键词召回混合使用,准确率更高

七、适配场景

  • Java SpringBoot + LangChain4j 企业级RAG知识库系统

  • 不想部署 Milvus/Weaviate 等专用向量库的项目

  • 需要 全文检索+向量检索混合召回 的AI问答场景

  • 多知识库、多租户隔离的业务系统

八、核心代码逐段深度解析

本节针对上述完整源码,拆解核心模块、关键方法、技术细节,帮助大家读懂每一段代码的设计思路,方便二次开发与改造。

8.1 基础类结构与依赖注入

当前类实现 LangChain4j 标准 EmbeddingStore 接口,遵循框架规范,可无缝接入RAG框架,同时基于Spring容器托管,统一依赖注入。

对应核心代码片段:

java 复制代码
import dev.langchain4j.data.document.Metadata;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.store.embedding.EmbeddingMatch;
import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
import dev.langchain4j.store.embedding.EmbeddingSearchResult;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.filter.Filter;
import dev.langchain4j.store.embedding.filter.comparison.IsEqualTo;
import dev.langchain4j.store.embedding.filter.comparison.IsIn;
import lombok.Builder;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.core.*;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch.indices.CreateIndexRequest;
import co.elastic.clients.elasticsearch.indices.ExistsRequest;
import co.elastic.clients.json.JsonData;

import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 自研ES向量存储实现类
 * 支持动态索引创建、余弦向量检索、BM25全文检索、元数据过滤、批量删除
 * 适配ES8.x + LangChain4j,替代官方简陋实现
 */
@Slf4j
@Component
public class ElasticsearchEmbeddingStore implements EmbeddingStore<TextSegment> {

    private final ElasticsearchClient esClient;

    @Value("${elasticsearch.vector.index:langchain4j-vector}")
    private String indexName;

    /**
     * 索引是否已初始化,volatile保证可见性
     */
    private volatile boolean indexInitialized = false;
    private final Object lock = new Object();

    public ElasticsearchEmbeddingStore(ElasticsearchClient esClient) {
        this.esClient = esClient;
    }

    // 核心实体类
    @Data
    @Builder
    private static class VectorDocument {
        private String id;
        private String text;
        private List<Float> embedding;
        private Map<String, String> metadata;
    }
}
  • 注入ES官方客户端 ElasticsearchClient:适配ES8.x新版API,摒弃废弃的RestHighLevelClient

  • 配置化索引名称:通过 @Value 读取配置,支持多环境动态切换索引

  • 线程安全变量:volatile 修饰索引初始化状态,搭配锁对象实现安全初始化

  • 注入ES官方客户端 ElasticsearchClient:适配ES8.x新版API,摒弃废弃的RestHighLevelClient

  • 配置化索引名称:通过 @Value 读取配置,支持多环境动态切换索引

  • 线程安全变量:volatile 修饰索引初始化状态,搭配锁对象实现安全初始化

内部自定义 VectorDocument 实体,完全对齐ES索引文档结构,实现数据序列化与反序列化统一,字段涵盖id、文本、向量、元数据四大核心模块。

8.2 核心亮点:动态索引初始化方法

ensureIndexExists\(\) 是整个工具类最核心的方法,解决官方插件维度固定、索引手动创建的痛点。

对应核心代码片段:

java 复制代码
/**
 * 索引初始化(动态适配向量维度)
 * 双重检查锁保证多线程、多实例线程安全
 */
private void ensureIndexExists(int dimension) {
    // 双重检查锁,保证线程安全
    if (indexInitialized) {
        return;
    }
    synchronized (lock) {
        if (indexInitialized) {
            return;
        }
        try {
            // 判断索引是否存在
            ExistsRequest existsRequest = new ExistsRequest.Builder().index(indexName).build();
            boolean exists = esClient.indices().exists(existsRequest).value();
            if (!exists) {
                // 构建索引Mapping,动态适配向量维度
                String mappingJson = String.format("""
                        {
                          "mappings": {
                            "properties": {
                              "id": {"type": "keyword"},
                              "text": {"type": "text"},
                              "embedding": {
                                "type": "dense_vector",
                                "dims": %d,
                                "index": true,
                                "similarity": "cosine"
                              },
                              "metadata": {"type": "object"}
                            }
                          }
                        }
                        """, dimension);
                CreateIndexRequest request = new CreateIndexRequest.Builder()
                        .index(indexName)
                        .withJson(mappingJson)
                        .build();
                esClient.indices().create(request);
                log.info("ES向量索引创建成功,索引名:{},向量维度:{}", indexName, dimension);
            }
            indexInitialized = true;
        } catch (IOException e) {
            log.error("ES向量索引初始化失败", e);
            throw new RuntimeException("ES索引初始化异常", e);
        }
    }
}
  • 双重检查锁机制:先判断状态、再竞争锁、再次校验状态,保证多实例、多线程场景下,索引仅初始化一次,杜绝重复创建、Mapping冲突报错

  • 动态维度适配 :从当前写入的向量中实时获取维度 embedding\.dimension\(\),无需代码硬编码维度

  • 索引参数定制:强制开启向量索引、指定余弦相似度算法,适配语义检索场景

  • 容错机制:索引创建失败捕获异常,抛出自定义业务异常,方便全局异常处理

  • 双重检查锁机制:先判断状态、再竞争锁、再次校验状态,保证多实例、多线程场景下,索引仅初始化一次,杜绝重复创建、Mapping冲突报错

  • 动态维度适配 :从当前写入的向量中实时获取维度 embedding\.dimension\(\),无需代码硬编码维度

  • 索引参数定制:强制开启向量索引、指定余弦相似度算法,适配语义检索场景

  • 容错机制:索引创建失败捕获异常,抛出自定义业务异常,方便全局异常处理

8.3 向量写入逻辑(单条/批量)

统一入口 addInternal\(\) 封装所有向量写入逻辑,所有重载的add方法最终都会进入该内部方法,代码复用性极高。

对应核心代码片段:

java 复制代码
// ==================== 向量写入核心方法 ====================
@Override
public String add(Embedding embedding) {
    return addInternal(null, embedding, null);
}

@Override
public String add(Embedding embedding, TextSegment textSegment) {
    return addInternal(textSegment.text(), embedding, textSegment.metadata().toMap());
}

@Override
public List<String> addAll(List<Embedding> embeddings) {
    return embeddings.stream().map(this::add).collect(Collectors.toList());
}

@Override
public List<String> addAll(List<Embedding> embeddings, List<TextSegment> textSegments) {
    List<String> idList = new ArrayList<>();
    for (int i = 0; i < embeddings.size(); i++) {
        TextSegment segment = textSegments.get(i);
        idList.add(addInternal(segment.text(), embeddings.get(i), segment.metadata().toMap()));
    }
    return idList;
}

/**
 * 统一向量写入内部方法
 */
private String addInternal(String text, Embedding embedding, Map<String, Object> metadata) {
    // 动态初始化索引
    ensureIndexExists(embedding.dimension());
    String docId = UUID.randomUUID().toString();
    // 格式转换
    List<Float> floatList = toFloatList(embedding.vector());
    Map<String, String> stringMetadata = convertMetadataToString(metadata);

    VectorDocument document = VectorDocument.builder()
            .id(docId)
            .text(text)
            .embedding(floatList)
            .metadata(stringMetadata)
            .build();

    // 写入ES
    try {
        IndexRequest<VectorDocument> request = new IndexRequest.Builder<VectorDocument>()
                .index(indexName)
                .id(docId)
                .document(document)
                .build();
        esClient.index(request);
        return docId;
    } catch (IOException e) {
        log.error("ES向量数据写入失败", e);
        throw new RuntimeException("向量数据写入异常", e);
    }
}
  • 自动触发索引校验:每次写入前自动判断索引是否存在,不存在则动态创建

  • 数据格式兼容:将LangChain4j的 Embedding 向量数组转为ES支持的 List\&lt;Float\&gt; 集合

  • 元数据统一序列化:将所有元数据统一转为String类型,规避ES字段类型不匹配导致的查询失效问题

  • 唯一ID生成:默认使用UUID作为文档主键,保证分布式场景数据唯一性

  • 自动触发索引校验:每次写入前自动判断索引是否存在,不存在则动态创建

  • 数据格式兼容:将LangChain4j的 Embedding 向量数组转为ES支持的 List\&lt;Float\&gt; 集合

  • 元数据统一序列化:将所有元数据统一转为String类型,规避ES字段类型不匹配导致的查询失效问题

  • 唯一ID生成:默认使用UUID作为文档主键,保证分布式场景数据唯一性

同时重载了单条、批量、带文本分片、无文本分片等多种写入方法,完全适配LangChain4j所有写入场景。

8.4 余弦向量检索核心逻辑

向量检索基于ES script\_score 实现,是语义匹配的核心,解决原生向量分数区间为[-1,1]的负数问题。

对应核心代码片段:

java 复制代码
// ==================== 向量检索(余弦相似度) ====================
@Override
public EmbeddingSearchResult<TextSegment> search(EmbeddingSearchRequest request) {
    Embedding queryEmbedding = request.queryEmbedding();
    int maxResults = request.maxResults();
    double minScore = request.minScore();
    Filter filter = request.filter();

    try {
        // 构建script_score余弦检索语句
        String script = "cosineSimilarity(params.queryVector, 'embedding') + 1.0";
        co.elastic.clients.elasticsearch.core.SearchRequest.Builder searchBuilder =
                new co.elastic.clients.elasticsearch.core.SearchRequest.Builder()
                        .index(indexName)
                        .size(maxResults)
                        .query(q -> q.scriptScore(s -> s
                                .script(sc -> sc.inline(i -> i.source(script)
                                        .params("queryVector", JsonData.of(toFloatList(queryEmbedding.vector())))))
                        ));

        // 拼接元数据过滤条件
        if (filter != null) {
            searchBuilder.postFilter(buildFilterQuery(filter));
        }

        co.elastic.clients.elasticsearch.core.SearchResponse<VectorDocument> response =
                esClient.search(searchBuilder.build(), VectorDocument.class);

        // 结果封装
        List<EmbeddingMatch<TextSegment>> matchList = response.hits().hits().stream()
                .filter(hit -> hit.score() >= minScore)
                .map(hit -> convertToMatch(hit))
                .collect(Collectors.toList());

        return new EmbeddingSearchResult<>(matchList);
    } catch (IOException e) {
        log.error("ES向量检索失败", e);
        return new EmbeddingSearchResult<>(Collections.emptyList());
    }
}
  • 打分公式优化:cosineSimilarity \+ 1\.0,将分数归一化到 [0,2],完美适配框架 minScore 阈值过滤

  • 结果封装适配:将ES查询结果反向解析,重构 TextSegmentMetadataEmbedding 对象,完全对齐LangChain4j返回规范

  • 异常兜底:检索异常时返回空集合,避免RAG流程报错中断

  • 打分公式优化:cosineSimilarity \+ 1\.0,将分数归一化到 [0,2],完美适配框架 minScore 阈值过滤

  • 结果封装适配:将ES查询结果反向解析,重构 TextSegmentMetadataEmbedding 对象,完全对齐LangChain4j返回规范

  • 异常兜底:检索异常时返回空集合,避免RAG流程报错中断

8.5 元数据过滤器适配逻辑

重写框架原生不完善的过滤器能力,通过 buildFilterQuery 自定义过滤器解析逻辑,支撑业务数据隔离。

对应核心代码片段:

java 复制代码
// ==================== 过滤器解析 ====================
private co.elastic.clients.elasticsearch._types.query_dsl.Query buildFilterQuery(Filter filter) {
    if (filter instanceof IsEqualTo isEqualTo) {
        String field = "metadata." + isEqualTo.key();
        String value = String.valueOf(isEqualTo.comparisonValue());
        return co.elastic.clients.elasticsearch._types.query_dsl.Query.of(
                q -> q.term(t -> t.field(field).value(value))
        );
    }
    if (filter instanceof IsIn isIn) {
        String field = "metadata." + isIn.key();
        List<String> values = isIn.comparisonValues().stream()
                .map(String::valueOf)
                .collect(Collectors.toList());
        return co.elastic.clients.elasticsearch._types.query_dsl.Query.of(
                q -> q.terms(t -> t.field(field).terms(v -> v.value(values)))
        );
    }
    log.warn("未适配的过滤器类型:{}", filter.getClass().getName());
    return co.elastic.clients.elasticsearch._types.query_dsl.Query.of(q -> q.matchAll(m -> m));
}
  • 适配 IsEqualTo 精准过滤:支持按知识库ID、文件ID等字段精准匹配数据

  • 适配 IsIn 多值过滤:满足多条件批量筛选场景

  • 默认兜底策略:未知过滤器类型自动走全量匹配,保证程序不报错

  • 层级字段映射:自动拼接 metadata\.xxx 路径,适配ES嵌套对象查询规则

  • 适配 IsEqualTo 精准过滤:支持按知识库ID、文件ID等字段精准匹配数据

  • 适配 IsIn 多值过滤:满足多条件批量筛选场景

  • 默认兜底策略:未知过滤器类型自动走全量匹配,保证程序不报错

  • 层级字段映射:自动拼接 metadata\.xxx 路径,适配ES嵌套对象查询规则

8.6 自定义BM25全文检索实现

拓展框架原生能力,新增独立BM25检索方法,实现「向量语义+关键词」混合召回。

对应核心代码片段:

java 复制代码
// ==================== 自定义BM25全文检索 ====================
public List<EmbeddingMatch<TextSegment>> searchByBM25(String keyword, Filter filter, int maxResults) {
    try {
        co.elastic.clients.elasticsearch.core.SearchRequest.Builder searchBuilder =
                new co.elastic.clients.elasticsearch.core.SearchRequest.Builder()
                        .index(indexName)
                        .size(maxResults)
                        .query(q -> q.match(m -> m.field("text").query(keyword)));

        if (filter != null) {
            searchBuilder.postFilter(buildFilterQuery(filter));
        }

        co.elastic.clients.elasticsearch.core.SearchResponse<VectorDocument> response =
                esClient.search(searchBuilder.build(), VectorDocument.class);

        // BM25分数归一化 0-1
        return response.hits().hits().stream()
                .map(hit -> {
                    EmbeddingMatch<TextSegment> match = convertToMatch(hit);
                    return new EmbeddingMatch<>(hit.score() / 20.0, match.embeddingId(),
                            match.embedding(), match.embedded());
                })
                .collect(Collectors.toList());
    } catch (IOException e) {
        log.error("ES全文检索失败", e);
        return Collections.emptyList();
    }
}
  • 布尔组合查询:通过bool+filter实现「关键词匹配+元数据隔离」双重筛选

  • 分数归一化:ES原生BM25分数区间不固定,通过score/20\.0 归一化到0-1区间,统一检索分数标准

  • 兼容通用返回体:复用 EmbeddingMatch 返回结构,上层业务无需适配多套返回逻辑

  • 布尔组合查询:通过bool+filter实现「关键词匹配+元数据隔离」双重筛选

  • 分数归一化:ES原生BM25分数区间不固定,通过 score/20\.0 归一化到0-1区间,统一检索分数标准

  • 兼容通用返回体:复用 EmbeddingMatch 返回结构,上层业务无需适配多套返回逻辑

8.7 数据删除能力实现

针对性适配知识库业务场景,提供两种删除逻辑,覆盖日常运维需求。

对应核心代码片段:

java 复制代码
// ==================== 数据删除方法 ====================
@Override
public void remove(String embeddingId) {
    try {
        DeleteRequest request = new DeleteRequest.Builder()
                .index(indexName)
                .id(embeddingId)
                .build();
        esClient.delete(request);
        log.info("单条向量数据删除成功,ID:{}", embeddingId);
    } catch (IOException e) {
        log.error("单条向量删除失败,ID:{}", embeddingId, e);
    }
}

/**
 * 根据知识库ID批量删除向量
 */
public long removeByKnowledgeBaseId(String knowledgeBaseId) {
    try {
        co.elastic.clients.elasticsearch.core.DeleteByQueryRequest request =
                new co.elastic.clients.elasticsearch.core.DeleteByQueryRequest.Builder()
                        .index(indexName)
                        .query(q -> q.term(t -> t.field("metadata.knowledgeBaseId").value(knowledgeBaseId)))
                        .build();
        DeleteByQueryResponse response = esClient.deleteByQuery(request);
        log.info("批量删除知识库向量成功,知识库ID:{},删除数量:{}", knowledgeBaseId, response.deleted());
        return response.deleted();
    } catch (IOException e) {
        log.error("批量删除知识库向量失败,知识库ID:{}", knowledgeBaseId, e);
        return 0;
    }
}

@Override
public void removeAll() {
    try {
        co.elastic.clients.elasticsearch.core.DeleteByQueryRequest request =
                new co.elastic.clients.elasticsearch.core.DeleteByQueryRequest.Builder()
                        .index(indexName)
                        .query(q -> q.matchAll(m -> m))
                        .build();
        esClient.deleteByQuery(request);
        log.info("清空所有向量数据成功");
    } catch (IOException e) {
        log.error("清空向量数据失败", e);
    }
}
  • 批量删除(按知识库ID) :基于ES deleteByQuery,批量清空指定知识库所有向量,适配知识库重置、删除场景,返回实际删除数量

  • 单条删除(按文档ID):精准删除单条分片向量,适配单文件更新、局部数据刷新场景

  • 全链路日志打点:记录删除条件、删除数量,方便线上问题追溯

  • 批量删除(按知识库ID) :基于ES deleteByQuery,批量清空指定知识库所有向量,适配知识库重置、删除场景,返回实际删除数量

  • 单条删除(按文档ID):精准删除单条分片向量,适配单文件更新、局部数据刷新场景

  • 全链路日志打点:记录删除条件、删除数量,方便线上问题追溯

8.8 工具方法封装

统一封装向量格式转换工具方法,解耦业务逻辑,代码更整洁,同时补充结果转换核心方法,适配ES查询结果解析:

对应核心代码片段:

java 复制代码
// ==================== 工具转换方法 ====================
private List<Float> toFloatList(float[] array) {
    if (array == null || array.length == 0) {
        return Collections.emptyList();
    }
    List<Float> list = new ArrayList<>(array.length);
    for (float v : array) {
        list.add(v);
    }
    return list;
}

private float[] toFloatArray(List<Float> list) {
    if (list == null || list.isEmpty()) {
        return new float[0];
    }
    float[] array = new float[list.size()];
    for (int i = 0; i < list.size(); i++) {
        array[i] = list.get(i);
    }
    return array;
}

private Map<String, String> convertMetadataToString(Map<String, Object> metadata) {
    if (metadata == null || metadata.isEmpty()) {
        return Collections.emptyMap();
    }
    Map<String, String> stringMap = new HashMap<>();
    metadata.forEach((k, v) -> stringMap.put(k, String.valueOf(v)));
    return stringMap;
}

/**
 * ES查询结果转为LangChain4j标准返回体
 */
private EmbeddingMatch<TextSegment> convertToMatch(Hit<VectorDocument> hit) {
    VectorDocument doc = hit.source();
    Embedding embedding = Embedding.from(toFloatArray(doc.getEmbedding()));
    Metadata metadata = new Metadata();
    if (doc.getMetadata() != null) {
        doc.getMetadata().forEach(metadata::add);
    }
    TextSegment textSegment = TextSegment.from(doc.getText(), metadata);
    return new EmbeddingMatch<>(hit.score(), doc.getId(), embedding, textSegment);
}
  • toFloatList:原生float数组转ES存储的Float集合

  • toFloatArray:ES查询结果集合转回框架所需数组格式

  • 元数据统一转换:规避不同类型元数据导致的查询异常

  • 结果自动封装:标准化适配LangChain4j返回结构,上层业务无需额外处理

  • toFloatList:原生float数组转ES存储的Float集合

  • toFloatArray:ES查询结果集合转回框架所需数组格式

九、项目配置文件(application.yml)

如需正常运行上述代码,需引入以下核心依赖,适配 SpringBoot 2.7+/3.x 版本:

xml 复制代码
<!-- LangChain4j 核心依赖 -->
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j</artifactId>
    <version>1.0.0-beta1</version>
</dependency>

<!-- Elasticsearch 8.x 官方客户端 -->
<dependency>
    <groupId>co.elastic.clients</groupId>
    <artifactId>elasticsearch-java</artifactId>
    <version>8.14.3</version>
</dependency>

<!-- SpringBoot Web 基础依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Lombok 简化代码 -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

十、总结

该自研 ES 向量存储框架,补足了 LangChain4j 官方ES插件的短板,实现了动态索引、混合检索、元数据过滤、便捷数据管理全套能力。在中小体量RAG项目中,完全可以替代专业向量数据库,兼顾性能、成本、可维护性,是Java AI项目低成本落地RAG的最优方案之一。

十一、常见踩坑记录

  • ES向量索引一旦创建不支持修改维度,所以首次初始化必须用真实向量维度

  • 余弦相似度存在负值,必须 +1.0 归一化,否则 minScore 过滤失效

  • metadata全部转为String存储,避免不同类型字段导致查询匹配失败

  • 批量插入建议外层批量封装,原生单条写入性能一般

  • ES8.x 默认开启SSL,本地测试可关闭SSL校验,线上务必开启安全配置

  • 多实例部署时,依靠双重检查锁彻底杜绝索引重复创建报错问题

(注:文档部分内容可能由 AI 生成)

相关推荐
逆境不可逃7 小时前
【与我学 ClaudeCode】规划与协调篇 之 Skills:按需加载的领域知识框架
大数据·人工智能·elasticsearch·搜索引擎·agent·claudecode
做个文艺程序员7 小时前
第02篇:搭建 ES 集群 + Spring Boot 整合实战——从 Docker Compose 到 Java 客户端全覆盖
java·spring boot·elasticsearch
peper_pig8 小时前
小智医疗-尚硅谷Java大模型应用项目
学习笔记·langchain4j·ai应用开发·java + ai·小智医疗
逸Y 仙X8 小时前
文章二:Elasticsearch跨集群能力探查
java·大数据·服务器·elasticsearch·搜索引擎·全文检索
海兰8 小时前
使用 ES|QL 调试 LLM 延迟、成本与 GPU 饱和度
大数据·elasticsearch·jenkins
Elastic 中国社区官方博客8 小时前
用于调试 LLM 延迟、成本和 GPU 饱和度的 ES|QL 查询
大数据·人工智能·elasticsearch·搜索引擎·ai·云原生·serverless
weixin_423533998 小时前
windows11安装claude code模型用deepseek,跳过国内校验。
大数据·elasticsearch·搜索引擎
liu_sir_1 天前
升级谷歌webview
大数据·elasticsearch·搜索引擎
Elastic 中国社区官方博客1 天前
Elasticsearch 下采样方法:最后值采样 vs. 聚合采样
大数据·运维·elasticsearch·搜索引擎·全文检索