
1. Maven 依赖 (pom.xml)xmldependencies !-- Spring Boot Web -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- Elasticsearch 8.x 官方 Java Client -- dependency groupIdco.elastic.clients/groupId artifactIdelasticsearch-java/artifactId version8.12.0/version /dependency !-- ES Client 底层通信依赖 -- dependency groupIdorg.elasticsearch.client/groupId artifactIdelasticsearch-rest-client/artifactId version8.12.0/version /dependency !-- Jackson JSON 处理 -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.15.2/version /dependency /dependencies2. 模拟 Embedding 模型服务 (EmbeddingService.java)在实际业务中这里会替换为调用 OpenAI、通义千问等真实 API 的代码。这里为了演示我们生成一个固定维度的随机向量。javaimport org.springframework.stereotype.Service; import java.util.Random; Service public class EmbeddingService { private static final int VECTOR_DIM 768; // 假设模型输出 768 维 private final Random random new Random(); /** * 将文本转换为向量 * 【实际业务中在此处通过 HTTP 调用大模型 Embedding API】 */ public float[] embed(String text) { // 模拟生成 768 维的随机向量 float[] vector new float[VECTOR_DIM]; for (int i 0; i VECTOR_DIM; i) { vector[i] random.nextFloat(); } return vector; } }3. ES 客户端配置 (ElasticsearchConfig.java)javaimport co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.json.jackson.JacksonJsonpMapper; import co.elastic.clients.transport.ElasticsearchTransport; import co.elastic.clients.transport.rest_client.RestClientTransport; import org.apache.http.HttpHost; import org.elasticsearch.client.RestClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class ElasticsearchConfig { Bean public RestClient restClient() { return RestClient.builder(new HttpHost(localhost, 9200, http)).build(); } Bean public ElasticsearchClient elasticsearchClient(RestClient restClient) { ElasticsearchTransport transport new RestClientTransport(restClient, new JacksonJsonpMapper()); return new ElasticsearchClient(transport); } }4. 核心业务 Controller (VectorController.java)这个 Controller 包含了创建索引、写入文档自动调用 Embedding、以及 kNN 检索的完整流程。javaimport co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.elasticsearch.core.SearchResponse; import co.elastic.clients.elasticsearch.core.search.Hit; import org.springframework.web.bind.annotation.*; import java.util.*; RestController RequestMapping(/api/vector) public class VectorController { private final ElasticsearchClient esClient; private final EmbeddingService embeddingService; private static final String INDEX_NAME my_native_vector_index; private static final int VECTOR_DIM 768; public VectorController(ElasticsearchClient esClient, EmbeddingService embeddingService) { this.esClient esClient; this.embeddingService embeddingService; } /** * 1. 初始化索引 (只需调用一次) */ PostMapping(/init-index) public String initIndex() throws Exception { boolean exists esClient.indices().exists(e - e.index(INDEX_NAME)).value(); if (exists) return 索引已存在无需重复创建; esClient.indices().create(c - c .index(INDEX_NAME) .mappings(m - m .properties(content, p - p.text(t - t)) .properties(content_vector, p - p.denseVector(dv - dv .dims(VECTOR_DIM) .index(true) .similarity(cosine) )) ) ); return 索引创建成功; } /** * 2. 写入文档 (核心在这里调用 Embedding 模型并将向量放入文档) */ PostMapping(/add) public String addDocument(RequestBody MapString, String payload) throws Exception { String content payload.get(content); // 【关键步骤】调用模型生成向量 float[] vector embeddingService.embed(content); // 组装文档必须包含 content_vector 字段 MapString, Object doc new HashMap(); doc.put(content, content); doc.put(content_vector, vector); // 将生成的向量塞入文档 // 写入 ES esClient.index(i - i.index(INDEX_NAME).document(doc)); return 文档向量化并存储成功; } /** * 3. kNN 向量检索 */ GetMapping(/search) public ListMapString, Object search(RequestParam String query, RequestParam(defaultValue 5) int topK) throws Exception { // 【关键步骤】查询文本也需要调用模型生成向量 float[] queryVector embeddingService.embed(query); SearchResponseMap response esClient.search(s - s .index(INDEX_NAME) .knn(k - k .field(content_vector) .queryVector(queryVector) .k(topK) .numCandidates(topK * 10) ), Map.class ); // 格式化返回结果 ListMapString, Object results new ArrayList(); for (HitMap hit : response.hits().hits()) { MapString, Object map new HashMap(); map.put(score, hit.score()); map.put(content, hit.source().get(content)); results.add(map); } return results; } }ES索引示例PUT /vector_docs { settings: { number_of_shards: 3, number_of_replicas: 1, index.knn: true, // 必须开启KNN插件总开关 index.knn.algo_param.ef_search: 100 }, mappings: { properties: { content: { type: text }, // 原始文本全文检索 doc_id: { type: keyword }, embedding: { type: dense_vector, dims: 1536, index: true, index_options: { type: hnsw }, similarity: cosine } } } }注意这里dims要和embedding模型维度一致维度由embedding决定(特例:动态embedding可以指定维度) 核心实现逻辑总结解耦设计ES 客户端只负责“存”和“搜”不负责“文本转向量”。写入流程在addDocument接口中先调用embeddingService.embed(content)拿到float[]然后doc.put(content_vector, vector)最后交给 ES 存储。检索流程在search接口中用户的查询语句query也必须先经过embeddingService.embed(query)变成向量才能传给 ES 的.queryVector(queryVector)进行比对。