使用 Spring Data Elasticsearch 进行开发:入门篇
1. 引言:拥抱全文检索与 Spring Data 的力量
在现代应用程序中,数据的存储和访问是核心需求。传统的基于关系型数据库的查询方式在处理结构化数据方面表现出色,但在面对非结构化或半结构化数据(如日志、文档内容、产品描述等)的全文检索、模糊匹配、实时分析以及高并发写入/查询等场景时,往往力不从心。这时,分布式搜索和分析引擎如 Elasticsearch 应运而生。
Elasticsearch 是一个基于 Lucene 的开源搜索引擎,它提供了强大的搜索能力、实时数据分析功能以及分布式架构带来的高可用性和扩展性。它以其 RESTful API、JSON 格式的数据交互以及强大的查询 DSL (Domain Specific Language) 赢得了广泛应用。
然而,直接使用 Elasticsearch 的原生 REST API 或低级别客户端进行开发,需要开发者手动处理大量的 HTTP 请求、JSON 序列化/反序列化、连接管理等底层细节。这不仅增加了开发复杂度,也容易引入错误,并且与 Spring 生态系统的其他部分集成不够紧密。
这就是 Spring Data Elasticsearch (SDES) 出现的原因。作为 Spring Data 项目家族的一员,SDES 旨在通过提供一个一致的、基于 Spring 的编程模型来简化 Elasticsearch 的数据访问。它借鉴了 Spring Data JPA 或 Spring Data MongoDB 等模块的设计思想,允许开发者使用熟悉的 Repository 抽象进行 Elasticsearch 操作,大大减少了样板代码,提高了开发效率。
通过本文,你将学习如何:
- 搭建一个基本的 Spring Boot 项目并引入 Spring Data Elasticsearch 依赖。
- 连接到 Elasticsearch 实例。
- 定义映射到 Elasticsearch 文档的 Java 类。
- 使用 Spring Data Repository 进行基本的 CRUD (创建、读取、更新、删除) 操作。
- 实现基于方法名派生的简单查询。
- 理解核心概念:Document、Index 和 Repository。
本文将是你的 Spring Data Elasticsearch 开发之旅的起点。让我们开始吧!
2. 前置准备与项目搭建
在开始编写代码之前,我们需要做一些准备工作。
2.1 必备条件
- Java Development Kit (JDK): 推荐使用 JDK 8 或更高版本。
- Maven 或 Gradle: 用于项目构建和依赖管理。
- Elasticsearch 实例: 你需要一个运行中的 Elasticsearch 实例。对于开发和学习,你可以选择:
- 本地安装: 从 Elastic 官网下载并运行 Elasticsearch。
- Docker: 使用 Docker 镜像启动一个或多个 Elasticsearch 容器。这是最推荐的本地开发方式,简单快捷。例如:
docker run -d --name elasticsearch -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" elasticsearch:7.17.0
(请注意版本,不同版本的 Elasticsearch 可能需要不同版本的 SDES)。 - Elastic Cloud: 使用 Elastic 提供的云服务。
2.2 创建 Spring Boot 项目
最简单的方式是使用 Spring Initializr (start.spring.io)。
- 访问 start.spring.io。
- 选择你的项目元数据(Maven Project 或 Gradle Project, Java, Spring Boot 版本等)。
- 添加依赖项:
Spring Web
(可选,但通常用于构建 Web 应用或简单的测试接口)Spring Data Elasticsearch
- 点击 “Generate” 下载项目压缩包。
- 解压并在你喜欢的 IDE (如 IntelliJ IDEA, Eclipse, VS Code) 中导入项目。
导入项目后,你的 pom.xml
(Maven) 或 build.gradle
(Gradle) 文件会包含 Spring Data Elasticsearch 的相关依赖。
Maven (pom.xml
) 示例片段:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Gradle (build.gradle
) 示例片段:
gradle
implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'
implementation 'org.springframework.boot:spring-boot-starter-web'
2.3 配置 Elasticsearch 连接
接下来,你需要在 src/main/resources/application.properties
或 application.yml
文件中配置 Spring Data Elasticsearch 连接到你的 Elasticsearch 实例。
SDES 支持多种客户端,但常用的且推荐的是基于 High-Level REST Client 或 Reactive Client 的自动配置。对于大多数入门场景,指定 Elasticsearch 的连接 URI 就足够了。
application.properties
示例:
“`properties
Elasticsearch connection URI(s)
Connects to Elasticsearch running on localhost:9200
spring.elasticsearch.uris=localhost:9200
Optional: Logging to see Elasticsearch queries executed by SDES
logging.level.org.springframework.data.elasticsearch=DEBUG
logging.level.org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate=DEBUG
logging.level.org.springframework.data.elasticsearch.core.query=DEBUG
“`
application.yml
示例:
“`yaml
spring:
elasticsearch:
uris: localhost:9200 # Connects to Elasticsearch running on localhost:9200
logging:
level:
org.springframework.data.elasticsearch: DEBUG
org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate: DEBUG
org.springframework.data.elasticsearch.core.query: DEBUG
“`
请将 localhost:9200
替换为你实际的 Elasticsearch 实例地址和端口。如果你使用了 Docker 启动,通常就是 localhost:9200
。9300
端口用于节点间通信,9200
端口用于 HTTP REST API,SDES 主要通过 9200
端口进行通信。
配置完成后,Spring Boot 的自动配置将负责创建连接 Elasticsearch 所需的客户端 bean。
3. 核心概念:Document, Index, Repository
在使用 Spring Data Elasticsearch 之前,理解几个核心概念至关重要。
3.1 Document (文档)
在 Elasticsearch 中,数据存储的基本单元是文档 (Document)。一个文档是一个 JSON 对象,包含了一系列字段 (Field) 及其对应的值。这类似于关系型数据库中的一行数据,但没有固定的模式 (Schema)。
在 Spring Data Elasticsearch 中,你将使用一个普通的 Java POJO (Plain Old Java Object) 来表示一个 Elasticsearch 文档。你需要使用 @Document
注解来标记这个类,表示它是一个可以被 Spring Data Elasticsearch 持久化的文档类。
@Document
注解的关键属性:
indexName
: 必须指定,表示该文档将存储在哪个 Elasticsearch 索引中。索引名通常是小写的。createIndex
: 可选,布尔值,默认为true
。如果设置为true
且对应的索引不存在,Spring Data Elasticsearch 会在应用启动时尝试创建该索引。在生产环境或需要精细控制索引设置时,通常会设置为false
并手动管理索引。version
: 可选,用于乐观锁。
示例:创建一个简单的图书文档类
“`java
package com.example.sdesdemo.document;
import org.springframework.data.annotation.Id;
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;
@Document(indexName = “books”, createIndex = true) // 指定索引名为 “books”,并允许自动创建索引
public class Book {
@Id // 标记为文档的唯一标识符,对应 Elasticsearch 中的 _id
private String id;
@Field(type = FieldType.Text) // 标记为文本字段,会进行分词处理,适合全文检索
private String title;
@Field(type = FieldType.Keyword) // 标记为关键词字段,不会分词,适合精确匹配、排序、聚合
private String author;
@Field(type = FieldType.Integer) // 标记为整数类型
private Integer publicationYear;
@Field(type = FieldType.Date) // 标记为日期类型
private Date publishDate;
@Field(type = FieldType.Text)
private String content;
// 构造函数、Getter 和 Setter
public Book() {
}
public Book(String id, String title, String author, Integer publicationYear, Date publishDate, String content) {
this.id = id;
this.title = title;
this.author = author;
this.publicationYear = publicationYear;
this.publishDate = publishDate;
this.content = content;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public Integer getPublicationYear() {
return publicationYear;
}
public void setPublicationYear(Integer publicationYear) {
this.publicationYear = publicationYear;
}
public Date getPublishDate() { return publishDate; }
public void setPublishDate(Date publishDate) { this.publishDate = publishDate; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
@Override
public String toString() {
return "Book{" +
"id='" + id + '\'' +
", title='" + title + '\'' +
", author='" + author + '\'' +
", publicationYear=" + publicationYear +
", publishDate=" + publishDate +
", content='" + (content != null ? content.substring(0, Math.min(content.length(), 50)) + "..." : "null") + '\'' +
'}';
}
}
“`
关于 @Field
注解:
@Field
注解用于定义文档字段的映射信息。这是非常重要的,因为它决定了 Elasticsearch 如何存储、索引和搜索该字段。
type
: 指定字段的数据类型。常用的类型包括:FieldType.Text
: 文本类型,用于可被分词进行全文搜索的字符串字段(如文章内容、标题)。FieldType.Keyword
: 关键词类型,用于不可分词的字符串字段(如标签、ID、作者名、国家)。适合精确匹配、聚合和排序。FieldType.Integer
,FieldType.Long
,FieldType.Float
,FieldType.Double
: 数字类型。FieldType.Boolean
: 布尔类型。FieldType.Date
: 日期类型。FieldType.Object
: 用于嵌套对象。- 还有其他类型如
GeoPoint
,Completion
等用于高级功能。
analyzer
: 仅用于Text
类型字段,指定用于分词的分析器(如standard
,english
,ik_smart
等)。searchAnalyzer
: 仅用于Text
类型字段,指定用于搜索时查询字符串的分词分析器。store
: 是否在索引中单独存储原始字段值(默认为 false,Elasticsearch 默认只存储索引信息,检索时从_source
获取原始文档)。通常不需要设置为 true。index
: 是否创建该字段的倒排索引(默认为 true)。如果设置为 false,该字段将无法被搜索。
正确选择 type
(特别是 Text
vs Keyword
) 对于搜索行为和性能至关重要。
3.2 Index (索引)
在 Elasticsearch 中,索引 (Index) 是一个逻辑上的命名空间,用于组织相关的文档集合。它类似于关系型数据库中的数据库或表。所有存储在同一个索引中的文档通常具有相似的结构,尽管 Elasticsearch 是无模式的 (schemaless,或者说动态模式),但为了高效搜索和分析,通常会预定义好文档的映射 (Mapping)。
如前所述,@Document
注解的 indexName
属性就指定了文档所属的索引。Spring Data Elasticsearch 会根据 @Document
和 @Field
注解的定义,在需要时自动创建索引并配置映射(如果 createIndex
为 true)。
自动创建的映射通常比较基础。对于复杂的应用,你可能需要手动定义更详细的映射,或者使用 @Mapping
注解指定外部映射文件。但在入门阶段,依赖 @Field
注解生成的映射通常足够了。
3.3 Repository (仓储)
这是 Spring Data 提供的核心抽象。Repository 是一个接口,通过继承 Spring Data 提供的特定 Repository 接口,可以自动获得一套标准的 CRUD 操作方法,而无需编写任何实现代码。Spring Data 会在运行时根据接口定义自动生成实现。
对于 Spring Data Elasticsearch,我们通常继承 ElasticsearchRepository
接口。
ElasticsearchRepository
接口:
“`java
package org.springframework.data.elasticsearch.repository;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.QueryByExampleExecutor;
import java.util.List;
// T: 文档类型 (例如 Book)
// ID: 文档ID的类型 (例如 String)
public interface ElasticsearchRepository
// 继承了 PagingAndSortingRepository,所以自动拥有以下方法:
// <S extends T> S save(S entity);
// <S extends T> Iterable<S> saveAll(Iterable<S> entities);
// Optional<T> findById(ID id);
// boolean existsById(ID id);
// Iterable<T> findAll();
// Iterable<T> findAllById(Iterable<ID> ids);
// long count();
// void deleteById(ID id);
// void delete(T entity);
// void deleteAllById(Iterable<? extends ID> ids);
// void deleteAll(Iterable<? extends T> entities);
// void deleteAll();
// Page<T> findAll(Pageable pageable);
// Sort sort(Sort sort);
// List<T> findAll(Sort sort);
// 继承了 QueryByExampleExecutor 用于 Example 查询
// ElasticsearchRepository 可能还会有一些自身特有的方法,但核心是继承自父接口
}
“`
要创建一个针对 Book
文档的 Repository,你只需定义一个接口:
“`java
package com.example.sdesdemo.repository;
import com.example.sdesdemo.document.Book;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;
@Repository // 标记为 Spring Bean
public interface BookRepository extends ElasticsearchRepository
// Spring Data 会自动生成实现
// 你可以在这里添加基于方法名派生的查询方法
}
“`
现在,你就可以在你的 Spring 组件 (如 Service 或 Controller) 中通过依赖注入使用 BookRepository
,并直接调用它提供的 CRUD 方法了。
4. 基本 CRUD 操作实践
有了 Book
文档类和 BookRepository
接口,我们就可以进行基本的文档操作了。
我们可以在一个简单的 Spring 组件 (例如一个 Service 类或一个命令行运行器) 中演示这些操作。
示例:创建一个简单的 Service 类使用 Repository
“`java
package com.example.sdesdemo.service;
import com.example.sdesdemo.document.Book;
import com.example.sdesdemo.repository.BookRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
import java.util.Optional;
@Service
public class BookService {
private static final Logger log = LoggerFactory.getLogger(BookService.class);
private final BookRepository bookRepository;
@Autowired
public BookService(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
/**
* 创建或更新图书
* @param book 要保存的图书对象
* @return 保存后的图书对象
*/
public Book saveBook(Book book) {
log.info("Saving book: {}", book);
Book savedBook = bookRepository.save(book);
log.info("Book saved: {}", savedBook);
return savedBook;
}
/**
* 根据ID查找图书
* @param id 图书ID
* @return 包含图书的 Optional,如果未找到则为 Optional.empty()
*/
public Optional<Book> findBookById(String id) {
log.info("Finding book by id: {}", id);
Optional<Book> book = bookRepository.findById(id);
book.ifPresent(b -> log.info("Found book: {}", b));
if (!book.isPresent()) {
log.info("Book with id {} not found", id);
}
return book;
}
/**
* 查找所有图书
* @return 所有图书的列表
*/
public Iterable<Book> findAllBooks() {
log.info("Finding all books");
Iterable<Book> books = bookRepository.findAll();
books.forEach(book -> log.info("Found book: {}", book));
return books;
}
/**
* 根据ID删除图书
* @param id 图书ID
*/
public void deleteBookById(String id) {
log.info("Deleting book by id: {}", id);
if (bookRepository.existsById(id)) {
bookRepository.deleteById(id);
log.info("Book with id {} deleted", id);
} else {
log.warn("Book with id {} not found, cannot delete", id);
}
}
/**
* 删除所有图书
*/
public void deleteAllBooks() {
log.info("Deleting all books");
bookRepository.deleteAll();
log.info("All books deleted");
}
/**
* 获取文档总数
* @return 文档总数
*/
public long countBooks() {
long count = bookRepository.count();
log.info("Total number of books: {}", count);
return count;
}
}
“`
示例:在 Spring Boot 应用启动时执行一些 CRUD 操作 (使用 CommandLineRunner
)
创建一个类实现 CommandLineRunner
接口,并在其中注入 BookService
。
“`java
package com.example.sdesdemo;
import com.example.sdesdemo.document.Book;
import com.example.sdesdemo.service.BookService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import java.util.Date;
import java.util.Optional;
import java.util.UUID;
@SpringBootApplication
public class SdesdemoApplication {
private static final Logger log = LoggerFactory.getLogger(SdesdemoApplication.class);
public static void main(String[] args) {
SpringApplication.run(SdesdemoApplication.class, args);
}
@Autowired
private BookService bookService;
@Bean
public CommandLineRunner demoElasticsearch() {
return args -> {
log.info("---------------- Starting Elasticsearch Demo ----------------");
// 1. 清空现有数据 (可选)
bookService.deleteAllBooks();
log.info("Current book count: {}", bookService.countBooks());
// 2. 创建一些图书文档
Book book1 = new Book(UUID.randomUUID().toString(), "Java Programming", "John Smith", 2020, new Date(), "A comprehensive guide to Java programming.");
Book book2 = new Book(UUID.randomUUID().toString(), "Spring Boot in Action", "Craig Walls", 2019, new Date(), "Learn how to build production-ready applications with Spring Boot.");
Book book3 = new Book(UUID.randomUUID().toString(), "Elasticsearch Essentials", "Jane Doe", 2021, new Date(), "A practical introduction to Elasticsearch and its features.");
bookService.saveBook(book1);
bookService.saveBook(book2);
bookService.saveBook(book3);
log.info("Added {} books", bookService.countBooks());
// 3. 查找所有图书
log.info("--- All Books ---");
bookService.findAllBooks();
// 4. 根据ID查找特定图书
log.info("--- Find Book by ID ---");
Optional<Book> foundBook = bookService.findBookById(book2.getId());
foundBook.ifPresent(book -> log.info("Found book by ID: {}", book));
Optional<Book> nonExistentBook = bookService.findBookById("non-existent-id");
nonExistentBook.ifPresentOrElse(
book -> log.info("Should not find this: {}", book),
() -> log.info("Book with non-existent-id not found as expected.")
);
// 5. 更新图书 (通过同一个ID保存)
log.info("--- Updating Book ---");
book1.setTitle("Advanced Java Programming");
bookService.saveBook(book1); // 如果ID存在,save会执行更新操作
bookService.findBookById(book1.getId()).ifPresent(book -> log.info("Updated book: {}", book));
// 6. 删除图书
log.info("--- Deleting Book ---");
bookService.deleteBookById(book3.getId());
log.info("Current book count after deletion: {}", bookService.countBooks());
bookService.findBookById(book3.getId()).ifPresentOrElse(
book -> log.info("Should not find this: {}", book),
() -> log.info("Book with id {} successfully deleted.", book3.getId())
);
log.info("---------------- Elasticsearch Demo Finished ----------------");
};
}
}
“`
运行 SdesdemoApplication
类,你将看到日志输出,展示了文档的创建、查找、更新和删除过程。同时,如果你查看 Elasticsearch 的 logs 或使用 Kibana 等工具,你会看到 books
索引被创建,并且文档被添加、更新和删除了。
注意: bookRepository.save()
方法非常智能。如果传入的 Book
对象中 id
字段为 null
,Spring Data Elasticsearch 会让 Elasticsearch 自动生成一个唯一的 ID 并返回保存后的对象;如果 id
字段不为 null
且 Elasticsearch 中已存在该 ID 的文档,save()
会执行更新操作。
5. 基于方法名派生的查询
Spring Data 的一个强大特性是它能够根据 Repository 接口中方法的名称自动创建查询。Spring Data Elasticsearch 也支持这一特性。你只需要按照特定的命名规则定义接口方法,Spring Data 就会解析方法名,并构建相应的 Elasticsearch 查询。
常见的关键字包括:find...By
, read...By
, get...By
, count...By
, delete...By
等,后接需要查询的字段名称以及可能的条件。
在 BookRepository
接口中添加自定义查询方法:
“`java
package com.example.sdesdemo.repository;
import com.example.sdesdemo.document.Book;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface BookRepository extends ElasticsearchRepository
// 根据作者查找图书 (精确匹配,因为author字段是Keyword类型)
List<Book> findByAuthor(String author);
// 查找出版年份大于等于指定年份的图书
List<Book> findByPublicationYearGreaterThanEqual(Integer publicationYear);
// 查找书名包含指定关键词的图书 (全文匹配,因为title字段是Text类型)
List<Book> findByTitleContaining(String keyword);
// 查找书名或内容包含指定关键词的图书 (全文匹配)
List<Book> findByTitleContainingOrContentContaining(String keyword);
// 查找作者和出版年份都匹配的图书
List<Book> findByAuthorAndPublicationYear(String author, Integer publicationYear);
// 统计某个作者的图书数量
long countByAuthor(String author);
// 删除某个作者的所有图书
long deleteByAuthor(String author); // 返回删除的数量
// 支持分页和排序 (Pageable, Sort 是 Spring Data 提供的接口)
// Page<Book> findByTitleContaining(String keyword, Pageable pageable);
}
“`
在 BookService
中使用这些查询方法:
“`java
package com.example.sdesdemo.service;
// … imports …
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
@Service
public class BookService {
// ... existing code ...
/**
* 根据作者查找图书
* @param author 作者名
* @return 图书列表
*/
public List<Book> findBooksByAuthor(String author) {
log.info("Finding books by author: {}", author);
List<Book> books = bookRepository.findByAuthor(author);
log.info("Found {} books by author {}: {}", books.size(), author, books);
return books;
}
/**
* 查找书名或内容包含指定关键词的图书
* @param keyword 关键词
* @return 图书列表
*/
public List<Book> searchBooksByTitleOrContent(String keyword) {
log.info("Searching books by title or content for keyword: {}", keyword);
List<Book> books = bookRepository.findByTitleContainingOrContentContaining(keyword);
log.info("Found {} books for keyword {}: {}", books.size(), keyword, books);
return books;
}
// 可以为其他自定义方法添加类似的服务方法
}
“`
在 CommandLineRunner
中调用新的服务方法:
“`java
package com.example.sdesdemo;
// … imports …
@SpringBootApplication
public class SdesdemoApplication {
// ... main method, bookRepository, bookService ...
@Bean
public CommandLineRunner demoElasticsearch() {
return args -> {
// ... existing CRUD demo code ...
log.info("---------------- Starting Custom Query Demo ----------------");
// Add more data for better search examples
Book book4 = new Book(UUID.randomUUID().toString(), "Effective Java", "Joshua Bloch", 2018, new Date(), "Tips for writing better Java code.");
Book book5 = new Book(UUID.randomUUID().toString(), "Spring Microservices", "Jane Doe", 2022, new Date(), "Building microservices with Spring Boot and Spring Cloud.");
bookService.saveBook(book4);
bookService.saveBook(book5);
log.info("Added more books, total count: {}", bookService.countBooks());
Thread.sleep(1000); // Wait for index refresh (optional, but good for immediate search visibility)
// Custom queries
log.info("--- Find Books by Author ---");
bookService.findBooksByAuthor("Jane Doe");
log.info("--- Search Books by Title or Content ---");
bookService.searchBooksByTitleOrContent("Spring"); // Will find book2 and book5
bookService.searchBooksByTitleOrContent("programming"); // Will find book1 and book4
log.info("---------------- Custom Query Demo Finished ----------------");
};
}
}
“`
运行应用程序,你会看到自定义查询方法的输出。Spring Data Elasticsearch 会根据方法名自动生成 Elasticsearch 的 match
或 term
(取决于字段类型) 查询并执行。
方法名派生查询的优点: 简单直观,无需编写查询 DSL。
方法名派生查询的限制: 对于复杂的查询 (如布尔组合、范围查询、聚合、高亮等) 支持有限或方法名会变得非常长且难以理解。
6. 进一步探索:@Query 注解
当方法名派生查询无法满足需求时,你可以使用 @Query
注解直接在 Repository 接口方法上定义 Elasticsearch 的查询 DSL (使用 JSON 字符串)。这提供了更大的灵活性。
在 BookRepository
中添加 @Query
方法:
“`java
package com.example.sdesdemo.repository;
import com.example.sdesdemo.document.Book;
import org.springframework.data.elasticsearch.annotations.Query;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface BookRepository extends ElasticsearchRepository
// ... existing method-derived queries ...
// 使用 @Query 注解执行一个简单的 match 查询
@Query("{\"match\":{\"title\":\"?0\"}}") // ?0 表示方法的第一个参数
List<Book> findByTitleCustom(String title);
// 使用 @Query 注解执行一个 bool 查询 (查找作者是 ?0 且出版年份大于 ?1 的图书)
@Query("{" +
" \"bool\": {" +
" \"must\": [" +
" { \"term\": { \"author.keyword\": \"?0\" } }," + // 注意:对Keyword字段使用term查询,字段名通常是fieldName.keyword
" { \"range\": { \"publicationYear\": { \"gt\": ?1 } } }" +
" ]" +
" }" +
"}")
List<Book> findByAuthorAndPublicationYearGreaterThan(String author, Integer year);
// 使用 @Query 支持分页和排序
// @Query("{\"match\":{\"content\":\"?0\"}}")
// Page<Book> searchByContentPaged(String keyword, Pageable pageable);
}
“`
解释 @Query
注解中的 JSON:
?0
,?1
等占位符对应方法的参数顺序。- JSON 字符串内部是 Elasticsearch 的 Query DSL。例如:
{"match":{"title":"?0"}}
: 在title
字段上执行match
查询,匹配参数?0
。{"bool":{"must":[...]} }
: 组合多个查询条件,must
表示所有条件都必须满足 (逻辑 AND)。{"term":{"author.keyword":"?0"}}
: 在author.keyword
字段上执行term
查询。注意这里使用了.keyword
后缀,这是因为我们在@Field
中将author
定义为FieldType.Keyword
。 Elasticsearch 对keyword
字段的默认存储方式通常是其本身的值,所以进行精确匹配时使用term
查询,字段名是原始字段名加.keyword
。如果直接对Text
字段使用term
查询,行为可能不是你期望的。{"range":{"publicationYear":{"gt":?1}}}
: 在publicationYear
字段上执行range
查询,查找大于 (gt) 参数?1
的值。其他范围操作符包括gte
(大于等于),lt
(小于),lte
(小于等于)。
在 BookService
和 CommandLineRunner
中调用新的 @Query
方法:
“`java
package com.example.sdesdemo.service;
// … imports …
@Service
public class BookService {
// ... existing methods ...
/**
* 使用 @Query 根据书名查找图书
* @param title 书名
* @return 图书列表
*/
public List<Book> findBooksByTitleCustom(String title) {
log.info("Finding books by title using @Query: {}", title);
List<Book> books = bookRepository.findByTitleCustom(title);
log.info("Found {} books for title {}: {}", books.size(), title, books);
return books;
}
/**
* 使用 @Query 根据作者和出版年份查找图书
* @param author 作者名
* @param year 出版年份阈值
* @return 图书列表
*/
public List<Book> findBooksByAuthorAndYearGreaterThan(String author, Integer year) {
log.info("Finding books by author {} and year > {} using @Query", author, year);
List<Book> books = bookRepository.findByAuthorAndPublicationYearGreaterThan(author, year);
log.info("Found {} books: {}", books.size(), books);
return books;
}
// ... 可以为其他自定义方法添加类似的服务方法 ...
}
“`
“`java
package com.example.sdesdemo;
// … imports …
@SpringBootApplication
public class SdesdemoApplication {
// ... main method, bookRepository, bookService ...
@Bean
public CommandLineRunner demoElasticsearch() {
return args -> {
// ... existing CRUD and method-derived query demo code ...
log.info("---------------- Starting @Query Demo ----------------");
// Add one more book for testing range query
Book book6 = new Book(UUID.randomUUID().toString(), "Clean Code", "Robert C. Martin", 2008, new Date(), "A handbook of agile software craftsmanship.");
bookService.saveBook(book6);
Thread.sleep(1000); // Wait for refresh
// Call @Query methods
log.info("--- Find Books by Title Custom Query ---");
bookService.findBooksByTitleCustom("Spring Boot in Action");
log.info("--- Find Books by Author and Year Greater Than Query ---");
bookService.findBooksByAuthorAndYearGreaterThan("Jane Doe", 2021); // Should find book5 (2022)
bookService.findBooksByAuthorAndYearGreaterThan("John Smith", 2019); // Should find book1 (2020)
bookService.findBooksByAuthorAndYearGreaterThan("Robert C. Martin", 2010); // Should not find book6 (2008)
log.info("---------------- @Query Demo Finished ----------------");
};
}
}
“`
运行应用程序,你将看到 @Query
方法的输出。使用 @Query
注解让你能够利用 Elasticsearch 强大的 Query DSL,实现各种复杂的搜索逻辑。理解 Elasticsearch Query DSL 是写好 @Query
的关键。
7. 索引映射和配置
在入门阶段,Spring Data Elasticsearch 自动根据 @Field
注解生成的映射通常足够了。但是,理解映射的概念以及如何配置它仍然很重要。
如前所述,@Field
注解允许你指定字段的类型 (FieldType
)、分析器 (analyzer
)、是否存储 (store
) 等。这些信息会被 Spring Data Elasticsearch 用来构建 Elasticsearch 的映射定义。
自动创建索引和映射:
默认情况下,@Document(createIndex = true)
会让 Spring Data 在应用程序启动时检查索引是否存在,如果不存在则创建。它还会基于 @Field
注解生成映射。在 DEBUG 级别的日志下,你会看到类似这样的日志输出,表明索引正在被创建或更新映射:
DEBUG ... Creating index 'books'
DEBUG ... Mapping for [class com.example.sdesdemo.document.Book] to index 'books'
DEBUG ... JSON mapping: {"properties":{"author":{"type":"keyword"},"content":{"type":"text"},"id":{"type":"keyword"},"publicationYear":{"type":"integer"},"publishDate":{"type":"date"},"title":{"type":"text"}}}
DEBUG ... Index 'books' created
你可以通过 Elasticsearch 的 REST API 来查看创建的索引和映射:
bash
GET /books/_mapping
这将返回 books
索引的映射信息,你应该会看到与 @Field
注解对应的字段类型。
手动管理映射 (入门阶段了解即可):
对于更复杂的场景,你可能需要手动定义映射,例如:
- 使用自定义分析器 (如中文分词
ik_analyzer
)。 - 配置
copy_to
字段将多个字段的内容合并到一个字段进行搜索。 - 配置
fields
为一个字段添加多种不同的索引方式 (例如title
字段既需要全文搜索,又需要精确匹配或排序,可以定义一个title.keyword
子字段)。 - 配置动态模板 (
dynamic_templates
)。
你可以将映射定义在一个 JSON 文件中 (例如 src/main/resources/es/book-mapping.json
),然后在 @Document
注解中指定 mappingPath
属性:
java
@Document(indexName = "books", createIndex = false, mappingPath = "es/book-mapping.json") // 关闭自动创建,指定映射文件
public class Book {
// ...
}
在这种情况下,你需要确保 Elasticsearch 实例中已经存在 books
索引,或者在应用启动前/启动时通过其他方式创建索引和应用映射。将 createIndex
设置为 false
并手动管理索引是生产环境中更常见的做法。
8. Spring Data Elasticsearch 客户端和配置
Spring Data Elasticsearch 支持多种与 Elasticsearch 交互的客户端。在 Spring Boot 2.4+ 版本中,默认推荐使用的是基于 Elasticsearch High-Level REST Client 的自动配置。在更老的版本中,可能是 Transport Client 或 Low-Level REST Client。在 Spring Boot 3.x 中,推荐使用的是新的 Elasticsearch Java Client。
幸运的是,对于入门用户,这些客户端的大部分配置细节都被 Spring Boot 的自动配置隐藏了。你只需要通过 spring.elasticsearch.uris
配置连接地址即可。
其他可能的配置项 (根据 Spring Boot 和 SDES 版本有所不同):
spring.elasticsearch.username
,spring.elasticsearch.password
: 如果 Elasticsearch 需要认证。spring.elasticsearch.restclient.connection-timeout
,spring.elasticsearch.restclient.read-timeout
: 配置连接和读取超时时间。spring.data.elasticsearch.client.reactive.*
或spring.data.elasticsearch.client.rest.*
: 如果需要更细粒度的客户端配置。
关于自动配置:
Spring Boot 会检查类路径,如果找到了 Elasticsearch 相关的客户端库和 Spring Data Elasticsearch 库,它会自动配置连接信息,并创建必要的 Spring Bean,如 ElasticsearchOperations
的实现 (例如 ElasticsearchRestTemplate
或 ReactiveElasticsearchTemplate
)。Repository 就是通过这些 Bean 来执行操作的。
你可以通过注入 ElasticsearchOperations
bean 来执行一些 Repository 接口未提供或者需要更底层控制的操作,例如执行复杂的原生查询、管理索引等。但在入门阶段,专注于 Repository 接口的使用即可。
9. 总结与展望
通过本文,你已经迈出了使用 Spring Data Elasticsearch 进行开发的第一步。我们学习了:
- 如何在 Spring Boot 项目中集成 Spring Data Elasticsearch。
- 如何配置与 Elasticsearch 实例的连接。
- 核心概念:将 Java POJO 映射为 Elasticsearch 的
Document
,通过indexName
指定Index
。 - 如何使用
@Field
注解定义字段映射类型,理解Text
和Keyword
的区别。 - 如何定义
ElasticsearchRepository
接口来继承标准的 CRUD 操作。 - 通过
BookService
演示了基本的文档创建、查找、更新和删除操作。 - 学习了如何通过方法名派生创建简单的查询方法。
- 探索了如何使用
@Query
注解执行更灵活的 Elasticsearch Query DSL 查询。
Spring Data Elasticsearch 极大地简化了与 Elasticsearch 的交互,让你能够专注于业务逻辑而不是底层的客户端 API。
接下来你可以继续深入学习的方向包括:
- 更复杂的 Elasticsearch 查询 DSL 和
@Query
注解的使用。 - 分页 (
Pageable
) 和排序 (Sort
)。 - 聚合 (
Aggregations
)。 - 高亮 (
Highlighting
)。 - 复杂的索引管理和映射配置。
- 使用
ElasticsearchOperations
进行更底层的操作。 - 响应式 Spring Data Elasticsearch (使用 Reactive Client)。
- 性能优化、集群配置、生产环境部署等。
掌握 Spring Data Elasticsearch 将使你在需要强大的搜索和分析功能的应用程序开发中更加得心应手。现在,是时候开始在你自己的项目中实践了!