# springboot-springdata-es **Repository Path**: enzoism/springboot-springdata-es ## Basic Information - **Project Name**: springboot-springdata-es - **Description**: Springboot中使用SpringData进行数据的增删改查-基础业务操作,使用的是JDK17+ElasticSearch9.0.0(开始时间2025-09-06) - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-09-06 - **Last Updated**: 2026-10-15 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Spring Data Elasticsearch 9.0.0 完整可运行示例 > 编写时间:2025-09-06,项目是【Elasticsearch 9.0.0】+【JDK17】 - [x] Docker部署ES方案 - [x] postman请求后端API接口示例->当前只记录了[ESUsrController.java](src/main/java/com/example/es/controller/ESUsrController.java)API请求 - [x] ES普通查询+向量查询 - [x] ES向量查询->添加了数据的导出[scripts](scripts)文件夹中 项目基于 **Spring Boot 3.2.x** + **Elasticsearch 9.0.0**(服务器端)+ **Spring Data Elasticsearch 5.2.x** (客户端)的完整可运行示例,实现对你提供的 `index_popyu` 索引的 **增删改查** 逻辑。 (Spring Data Elasticsearch 在底层已自动使用官方推荐的 Java API Client,无需额外引入 TransportClient/JestClient。) -------------------------------------------------- 一、依赖与版本 ``` org.springframework.boot spring-boot-starter-parent 3.2.5 17 org.springframework.boot spring-boot-starter-data-elasticsearch org.springframework.boot spring-boot-starter-web org.projectlombok lombok true ``` -------------------------------------------------- 二、YAML 配置 ```yaml # application.yml spring: elasticsearch: uris: http://localhost:9200 # 9.0 默认端口 username: # 如未开启 x-pack 可留空 password: ``` -------------------------------------------------- 三、实体类(与 mapping 一一对应) ```java package com.example.es.entity; import lombok.*; import org.springframework.data.annotation.Id; import org.springframework.data.elasticsearch.annotations.*; @Document(indexName = "index_popyu") @Setting(replicas = 0, shards = 1) // 开发阶段简化 @Data @NoArgsConstructor @AllArgsConstructor @Builder public class UsrDoc { @Id @Field(type = FieldType.Keyword) private String usrId; @Field(type = FieldType.Text) private String usrNm; @Field(type = FieldType.Integer) private Integer usrAge; @Field(type = FieldType.Keyword) private String usrCity; @Field(type = FieldType.Keyword) private String usrPhone; @Field(type = FieldType.Date, format = DateFormat.date) private String joinDate; // yyyy-MM-dd @Field(type = FieldType.Date, format = DateFormat.hour_minute_second) private String joinTime; // HH:mm:ss } ``` -------------------------------------------------- 四、Repository 接口(自带 CRUD) ```java package com.example.es.repo; import com.example.es.entity.UsrDoc; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; import org.springframework.stereotype.Repository; @Repository public interface UsrRepository extends ElasticsearchRepository { // 可按需追加自定义方法,Spring Data 会帮你生成实现 } ``` -------------------------------------------------- 五、Service 层(完整增删改查) ```java package com.example.es.service; import com.example.es.entity.UsrDoc; import com.example.es.repo.UsrRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import java.util.Optional; @Service @RequiredArgsConstructor public class UsrService { private final UsrRepository repo; /* ---------- 增 ---------- */ public UsrDoc save(UsrDoc doc) { return repo.save(doc); } /* ---------- 删 ---------- */ public void deleteById(String usrId) { repo.deleteById(usrId); } /* ---------- 改 ---------- */ public UsrDoc update(UsrDoc doc) { // Spring Data 的 save 根据 @Id 存在即覆盖 return repo.save(doc); } /* ---------- 查 ---------- */ public Optional findById(String usrId) { return repo.findById(usrId); } public Page findAll(Pageable pageable) { return repo.findAll(pageable); } } ``` -------------------------------------------------- 六、Controller(REST 入口) ```java package com.example.es.controller; import com.example.es.entity.UsrDoc; import com.example.es.service.UsrService; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/usr") @RequiredArgsConstructor public class UsrController { private final UsrService service; @PostMapping public UsrDoc create(@RequestBody UsrDoc doc) { return service.save(doc); } @GetMapping("/{usrId}") public ResponseEntity get(@PathVariable String usrId) { return service.findById(usrId) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } @GetMapping public Page list(@PageableDefault(size = 10) Pageable pageable) { return service.findAll(pageable); } @PutMapping("/{usrId}") public UsrDoc update(@PathVariable String usrId, @RequestBody UsrDoc doc) { doc.setUsrId(usrId); // 确保主键一致 return service.update(doc); } @DeleteMapping("/{usrId}") public void delete(@PathVariable String usrId) { service.deleteById(usrId); } } ``` -------------------------------------------------- 七、启动类 ```java @SpringBootApplication public class Es900Application { public static void main(String[] args) { SpringApplication.run(Es900Application.class, args); } } ``` -------------------------------------------------- 八、验证 1. 启动 Elasticsearch 9.0.0(默认端口 9200)。 2. 启动 Spring Boot,首次访问任意接口时,Spring Data 会自动根据 `@Document` 注解创建 `index_popyu` 并写入你给出的 mapping(已内置)。 3. 使用 curl / Postman 调用: ``` POST http://localhost:8080/usr { "usrId": "u001", "usrNm": "Popyu", "usrAge": 18, "usrCity": "shenzhen", "usrPhone": "13800138000", "joinDate": "2025-09-06", "joinTime": "14:30:00" } ``` ``` GET http://localhost:8080/usr/u001 PUT http://localhost:8080/usr/u001 DEL http://localhost:8080/usr/u001 ``` -------------------------------------------------- 九、scripts 目录(索引创建 + 默认数据导入) > 全部为自包含的 curl 脚本,默认请求 `http://localhost:9200`,可用环境变量覆盖:`ES_HOST=http://es.example.com:9200 bash scripts/03_create_vector_index.sh` ``` scripts/ ├── 01_create_usr_index.sh # 创建 index_popyu(mapping 与 UsrDoc 注解一一对应) ├── 02_import_usr_data.sh # 向 index_popyu 批量导入 6 条默认 usr 数据 ├── 03_create_vector_index.sh # 创建 index_vector_demo(dense_vector mapping) ├── 04_import_vector_mock_data.sh # 导入 5 条 ChromaDB 同款 mock 向量文档 └── 05_vector_knn_examples.sh # kNN 原始 JSON 查询示例(基础 / filter / 混合检索) ``` -------------------------------------------------- 十、向量检索模块(ESVectorController) > 编写时间:2026-10-14。基于 Elasticsearch 9.0 的 `dense_vector` + kNN(近似最近邻)实现向量相似度检索。 > 模拟数据与查询向量完全对标 ChromaDB Demo(3 维伪造向量),**不依赖真实 embedding 接口**。 1)核心概念 - `dense_vector` 字段:存向量。`dims` 必须与 embedding 模型输出一致(Demo 里用 3 维伪造向量);`index: true` 开启 HNSW 图索引(否则只能脚本打分,不能高效 ANN);`similarity` 可选 `cosine` / `dot_product` / `l2_norm`。 - kNN 检索:`k` 返回最相似的 top k;`num_candidates` 是每个分片先取多少候选,越大越准但越慢。 - 过滤 + 向量:`filter` 先过滤(如 category),再在过滤结果里做向量相似度。 - 混合检索:顶层 `knn` 与 `query` 同时给出时,向量得分 + BM25 关键词得分求和。 - 实际生产中向量由外部 embedding 模型生成(OpenAI text-embedding-3-small / Cohere Embed / BGE / E5 / GTE / ELSER 等),Demo 使用伪造向量。 2)索引 mapping(`scripts/03_create_vector_index.sh`) ```json PUT index_vector_demo { "mappings": { "properties": { "docId": { "type": "keyword" }, "content": { "type": "text" }, "vector": { "type": "dense_vector", "dims": 3, "index": true, "similarity": "cosine" }, "category": { "type": "keyword" }, "popularity": { "type": "integer" } } } } ``` 3)代码结构 | 文件 | 说明 | |---|---| | `entity/EsVectorDoc.java` | 实体,`@Field(type = FieldType.Dense_Vector, dims = 3, similarity = "cosine")` | | `repo/EsVectorRepository.java` | 基础 CRUD(saveAll / findById / count) | | `service/EsVectorService.java` | mock 数据 + init(删索引→重建→导入)+ kNN 检索 | | `controller/ESVectorController.java` | REST 入口 `/es-vector` | | `dto/VectorQueryDTO.java` / `vo/VectorSearchVO.java` | 通用 kNN 请求体 / 检索结果(含 score 与 cosine 相似度) | > 注意:kNN 写在 Service 层用 `ElasticsearchOperations` + `NativeQuery.withKnnQuery(KnnQuery)`,而不是 `@Query` 注解—— > 因为 `knn` 是搜索请求体的顶层字段(与 `query` 平级),`@Query` 只能替换 query 部分。 > > 另外:pom 里用 `1.18.42` 覆盖了 Boot 3.2.5 托管的 1.18.32——JDK 21+(如本机 JDK 25)编译需要新版 lombok。 4)接口清单 | 方法 | 路径 | 说明 | |---|---|---| | POST | `/es-vector/init` | 删索引 → 重建 → 导入 5 条 mock 文档 | | GET | `/es-vector/knn?k=3&numCandidates=100` | 基础 kNN,默认查询向量 `[0.15,0.25,0.35]` | | GET | `/es-vector/knn/filter?category=数据库&k=5` | 过滤 + kNN(先 filter 再向量相似度) | | POST | `/es-vector/knn/query` | 通用 kNN,body:`{"vector":[...],"k":3,"numCandidates":100,"category":"数据库"}` | | GET | `/es-vector/list` | 全量文档 | | GET | `/es-vector/{docId}` | 单条文档 | 5)快速验证 ``` # 方式一:Java 接口(先启动应用,端口 8076) curl -X POST http://localhost:8076/es-vector/init curl "http://localhost:8076/es-vector/knn" # 方式二:纯 curl 脚本(不启动应用) bash scripts/03_create_vector_index.sh bash scripts/04_import_vector_mock_data.sh bash scripts/05_vector_knn_examples.sh ``` 预期结果(实测于 ES 8.15.3): - 基础 kNN:**doc4 → doc1 → doc2**(cosine 相似度降序)。注意与 ChromaDB Demo 的 **doc1 → doc4 → doc2** 不同—— ChromaDB 用的是 L2 距离,doc1/doc4 距离打平(都是 0.0075);换成 cosine 后 doc4 的方向与查询向量更接近。 - 过滤 `category=数据库`:只剩 **doc4 → doc3**。 - ES 8.15 默认使用 `int8_hnsw` 量化索引(可在 mapping 的 `index_options` 看到),仅 5 条数据时量化误差可能使 doc1/doc2 次序轻微浮动; 生产中数据量大时这正是省内存的默认优化。 -------------------------------------------------- 十一、前端调试台(api-console.html) > 编写时间:2026-10-15。项目根目录下的 `api-console.html` 是一个**原生 HTML/CSS/JS 单文件调试页**(零依赖、无框架、无构建), > 把两个 Controller 的全部 14 个接口做成可视化表单,用于直观调试接口运作。 1)启动方式 ``` 1. 启动 Elasticsearch(docker,端口 9200) 2. 启动 Spring Boot 应用(端口 8076) 3. 双击 api-console.html 用浏览器打开(file:// 协议即可,无需部署到服务器) 4. 顶栏点「测试连接」,出现绿点「已连接」即可开始使用 ``` 2)为什么需要 CorsConfig `file://` 打开的页面调用 `http://localhost:8076` 属于跨域请求(浏览器发送的 `Origin` 为 `null`), 没有 CORS 配置时浏览器会拦截响应。项目中的 `config/CorsConfig.java` 放开了全部来源与方法(GET/POST/PUT/DELETE/OPTIONS), 页面才能直接调通接口。**把页面部署到同源环境下时该配置非必需,但保留不影响。** 3)功能说明 | 区域 | 功能 | |---|---| | 顶栏 | 后端地址输入框(默认 `http://localhost:8076`,自动存入 localStorage)+ 连接测试(可区分「后端可达但 ES 未就绪」) | | 页签 | 「用户接口 /usr」(8 个)与「向量检索 /es-vector」(6 个)两组 | | 接口卡片 | 每个接口一张卡片:方法徽章(GET 蓝 / POST 青 / PUT 紫 / DELETE 红)、路径、参数表单,回车即可发送 | | 响应区 | HTTP 状态码(2xx 绿 / 4xx 橙 / 5xx 红)、耗时 ms、实际请求 URL、JSON 语法高亮 + 一键复制 | | 数据可视化 | `Page` 结果渲染为统计块 + 表格,带「上一页 / 下一页」快捷键(自动改写 page 参数重发);kNN 结果带 cosine 相似度进度条;向量文档展示 3 维向量值 | 4)典型使用流程 ``` # 向量检索首次体验: 1. 切到「向量检索」页签 2. 点 01 号卡片「执行重建」→ POST /es-vector/init(重建索引并导入 5 条 mock 文档) 3. 点 02 号卡片「发送请求」→ GET /es-vector/knn,表格中查看相似度进度条 # 用户 CRUD 体验: 1. 切到「用户接口」页签 2. 01 号卡片填表单新增文档(usrId 留空由 ES 自动生成) 3. 03 号卡片分页列表,用响应区的翻页按钮翻页 ``` 5)注意事项 - `POST /es-vector/init` 会**删除并重建** `index_vector_demo`,属于危险操作,按钮文案已特别标注。 - `PUT /usr/{usrId}` 以路径 usrId 为准覆盖整条文档,表单留空的字段会以 `null` 覆盖,建议填全字段。 - 深色主题;页面自身不存储任何数据,所有请求均实时打到后端地址栏指定的服务。 ----