# 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` 覆盖,建议填全字段。
- 深色主题;页面自身不存储任何数据,所有请求均实时打到后端地址栏指定的服务。
----