Elasticsearch 搜索引擎与 Spring Boot 应用整合

Elasticsearch 搜索引擎与 Spring Boot 应用整合:让你的数据“嗖”一下就找到!

各位看官,大家好!今天咱们聊点儿刺激的,聊聊如何给你的 Spring Boot 应用装上一个超级搜索引擎——Elasticsearch!想象一下,你的应用数据量越来越大,用户想找个东西,得翻个底朝天,那体验简直就像在浩瀚星空中寻找一颗流星。有了 Elasticsearch,一切都变得不一样了,用户输入关键词,数据“嗖”的一下就出来了,简直比火箭还快!

本文将用通俗易懂的语言,配合大量的代码示例,手把手教你如何将 Elasticsearch 集成到你的 Spring Boot 应用中,让你的数据检索能力瞬间提升N个档次!

1. 什么是 Elasticsearch?为什么我们需要它?

Elasticsearch,江湖人称“ES”,是一个开源的、分布式的搜索和分析引擎。它基于 Lucene 构建,提供了强大的全文搜索、结构化搜索、分析以及近实时搜索能力。

你可以把它想象成一个超级强大的图书馆管理员,它不仅能记住每一本书的标题、作者、内容,还能根据你的任何关键词,迅速找到相关的书籍。

那么,为什么我们需要它呢?

  • 全文搜索: 传统数据库的 LIKE 语句在面对复杂的全文搜索场景时,性能会急剧下降。ES 擅长处理海量文本数据,并提供高效的全文搜索能力。
  • 近实时搜索: 数据更新后,几乎可以立即被搜索到,满足实时性要求较高的场景。
  • 分布式架构: 可以轻松扩展到数百台服务器,处理 PB 级别的数据。
  • 灵活的数据模型: 支持多种数据类型,包括文本、数值、日期等。
  • 强大的分析功能: 可以对数据进行聚合、分析,挖掘潜在价值。

简单来说,如果你的应用需要处理大量的文本数据,并且需要快速、准确地进行搜索,那么 Elasticsearch 就是你的不二之选。

2. 准备工作:磨刀不误砍柴工

在开始之前,我们需要做一些准备工作:

  • 安装 Elasticsearch: 你可以从 Elasticsearch 官网下载安装包,或者使用 Docker 进行安装。这里推荐使用 Docker,方便快捷。

    docker pull docker.elastic.co/elasticsearch/elasticsearch:7.17.6
    docker run -d -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:7.17.6

    这个命令会下载并启动一个 Elasticsearch 容器,并将 9200(HTTP 端口)和 9300(TCP 端口)映射到宿主机。

  • 安装 Kibana (可选): Kibana 是 Elasticsearch 的可视化工具,可以方便地查看和分析数据。同样可以使用 Docker 安装。

    docker pull docker.elastic.co/kibana/kibana:7.17.6
    docker run -d -p 5601:5601 -e "ELASTICSEARCH_HOSTS=http://localhost:9200" docker.elastic.co/kibana/kibana:7.17.6

    这个命令会下载并启动一个 Kibana 容器,并将 5601 端口映射到宿主机,并配置 Kibana 连接到 Elasticsearch。

  • 创建 Spring Boot 项目: 使用 Spring Initializr 创建一个 Spring Boot 项目,并添加以下依赖:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    其中,spring-boot-starter-data-elasticsearch 是 Spring Data Elasticsearch 的依赖,它简化了与 Elasticsearch 的交互。lombok可以简化实体类的编写。

3. 配置 Spring Boot 连接 Elasticsearch

application.propertiesapplication.yml 文件中,配置 Elasticsearch 的连接信息:

spring:
  data:
    elasticsearch:
      cluster-nodes: localhost:9200 # Elasticsearch 的地址和端口
      cluster-name: docker-cluster  # 你的集群名称,默认是 elasticsearch

请根据你的实际情况修改 cluster-nodescluster-name

4. 定义数据模型 (Entity)

我们需要定义一个 Java 类来映射 Elasticsearch 中的文档。例如,我们创建一个 Product 类:

import lombok.Data;
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;

@Data
@Document(indexName = "products", shards = 1, replicas = 0)
public class Product {

    @Id
    private String id;

    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String name;

    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String description;

    @Field(type = FieldType.Double)
    private Double price;

    @Field(type = FieldType.Keyword)
    private String category;
}
  • @Document(indexName = "products", shards = 1, replicas = 0):指定该类映射到 Elasticsearch 中的索引名称为 products,设置 shards 和 replicas 的数量。
  • @Id:指定 id 字段为文档的 ID。
  • @Field(type = FieldType.Text, analyzer = "ik_max_word"):指定 namedescription 字段为文本类型,并使用 ik_max_word 分析器进行分词。
  • @Field(type = FieldType.Double):指定 price 字段为 Double 类型。
  • @Field(type = FieldType.Keyword):指定 category 字段为 Keyword 类型,Keyword 类型不会进行分词。
  • @Data:使用了 Lombok 的注解,自动生成 getter、setter、toString 等方法。

关于 analyzer:

analyzer 是 Elasticsearch 中用于将文本分解成词项的组件。不同的 analyzer 会产生不同的分词结果,影响搜索的准确性。

  • ik_max_word:IK 分词器的一种,会将文本分解成最细粒度的词项。
  • ik_smart:IK 分词器的另一种,会进行智能分词,更加符合语义。
  • standard:Elasticsearch 默认的分词器,对英文文本效果较好,对中文文本效果较差。

为了获得更好的中文分词效果,建议使用 IK 分词器。你需要先安装 IK 分词器插件,然后才能在 @Field 注解中使用它。

安装 IK 分词器插件:

  1. 进入 Elasticsearch 容器:

    docker exec -it <elasticsearch_container_id> bash
  2. 下载并安装 IK 分词器插件:

    ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.17.6/elasticsearch-analysis-ik-7.17.6.zip
  3. 重启 Elasticsearch 容器:

    docker restart <elasticsearch_container_id>

5. 创建 Repository

Spring Data Elasticsearch 提供了 ElasticsearchRepository 接口,可以简化对 Elasticsearch 的操作。我们创建一个 ProductRepository 接口:

import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import com.example.demo.entity.Product;

public interface ProductRepository extends ElasticsearchRepository<Product, String> {
}

ElasticsearchRepository 接口提供了常用的 CRUD 操作,例如 savefindByIdfindAlldeleteById 等。

6. 实现 CRUD 操作

现在,我们可以使用 ProductRepository 来实现对 Product 数据的 CRUD 操作。创建一个 ProductService 类:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.example.demo.entity.Product;
import com.example.demo.repository.ProductRepository;

import java.util.List;
import java.util.Optional;

@Service
public class ProductService {

    @Autowired
    private ProductRepository productRepository;

    public Product save(Product product) {
        return productRepository.save(product);
    }

    public Optional<Product> findById(String id) {
        return productRepository.findById(id);
    }

    public List<Product> findAll() {
        return (List<Product>) productRepository.findAll();
    }

    public void deleteById(String id) {
        productRepository.deleteById(id);
    }
}

这个 ProductService 类提供了 savefindByIdfindAlldeleteById 等方法,用于操作 Product 数据。

7. 创建 Controller

为了方便测试,我们创建一个 ProductController 类,提供 RESTful API 接口:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.example.demo.entity.Product;
import com.example.demo.service.ProductService;

import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping("/products")
public class ProductController {

    @Autowired
    private ProductService productService;

    @PostMapping
    public Product save(@RequestBody Product product) {
        return productService.save(product);
    }

    @GetMapping("/{id}")
    public Optional<Product> findById(@PathVariable String id) {
        return productService.findById(id);
    }

    @GetMapping
    public List<Product> findAll() {
        return productService.findAll();
    }

    @DeleteMapping("/{id}")
    public void deleteById(@PathVariable String id) {
        productService.deleteById(id);
    }
}

这个 ProductController 类提供了 POSTGETDELETE 等 HTTP 方法,用于操作 Product 数据。

8. 简单测试

启动 Spring Boot 应用,使用 Postman 或 curl 等工具进行测试。

  • 新增数据:

    curl -X POST 
      http://localhost:8080/products 
      -H 'Content-Type: application/json' 
      -d '{
        "name": "小米手机13",
        "description": "性能怪兽,拍照神器",
        "price": 4999.0,
        "category": "手机"
    }'
  • 查询数据:

    curl http://localhost:8080/products/1
  • 查询所有数据:

    curl http://localhost:8080/products
  • 删除数据:

    curl -X DELETE http://localhost:8080/products/1

9. 实现搜索功能

除了基本的 CRUD 操作,我们还需要实现搜索功能。Spring Data Elasticsearch 提供了多种搜索方式:

  • Query Methods: 通过方法名定义查询条件。
  • @Query 注解: 使用 Elasticsearch 的 Query DSL 定义查询条件。
  • ElasticsearchTemplate: 使用 ElasticsearchTemplate 提供的 API 进行查询。

9.1 使用 Query Methods 实现搜索

Query Methods 是 Spring Data Elasticsearch 提供的一种简单易用的搜索方式。我们只需要在 ProductRepository 接口中定义方法名,Spring Data Elasticsearch 就会自动生成对应的查询语句。

例如,我们可以定义一个 findByNameContaining 方法,用于根据商品名称进行模糊搜索:

import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import com.example.demo.entity.Product;

import java.util.List;

public interface ProductRepository extends ElasticsearchRepository<Product, String> {

    List<Product> findByNameContaining(String name);
}

然后在 ProductService 类中添加一个 searchByName 方法:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.example.demo.entity.Product;
import com.example.demo.repository.ProductRepository;

import java.util.List;
import java.util.Optional;

@Service
public class ProductService {

    @Autowired
    private ProductRepository productRepository;

    public Product save(Product product) {
        return productRepository.save(product);
    }

    public Optional<Product> findById(String id) {
        return productRepository.findById(id);
    }

    public List<Product> findAll() {
        return (List<Product>) productRepository.findAll();
    }

    public void deleteById(String id) {
        productRepository.deleteById(id);
    }

    public List<Product> searchByName(String name) {
        return productRepository.findByNameContaining(name);
    }
}

最后,在 ProductController 类中添加一个 searchByName 接口:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.example.demo.entity.Product;
import com.example.demo.service.ProductService;

import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping("/products")
public class ProductController {

    @Autowired
    private ProductService productService;

    @PostMapping
    public Product save(@RequestBody Product product) {
        return productService.save(product);
    }

    @GetMapping("/{id}")
    public Optional<Product> findById(@PathVariable String id) {
        return productService.findById(id);
    }

    @GetMapping
    public List<Product> findAll() {
        return productService.findAll();
    }

    @DeleteMapping("/{id}")
    public void deleteById(@PathVariable String id) {
        productService.deleteById(id);
    }

    @GetMapping("/search")
    public List<Product> searchByName(@RequestParam String name) {
        return productService.searchByName(name);
    }
}

现在,你可以使用以下命令进行搜索:

curl http://localhost:8080/products/search?name=小米

9.2 使用 @Query 注解实现搜索

@Query 注解允许我们使用 Elasticsearch 的 Query DSL 定义查询条件。这种方式更加灵活,可以实现复杂的搜索需求。

例如,我们可以定义一个 searchByNameAndDescription 方法,用于根据商品名称和描述进行模糊搜索:

import org.springframework.data.elasticsearch.annotations.Query;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import com.example.demo.entity.Product;

import java.util.List;

public interface ProductRepository extends ElasticsearchRepository<Product, String> {

    @Query("{"bool": {"should": [{"match": {"name": "?0"}}, {"match": {"description": "?0"}}]}}")
    List<Product> searchByNameAndDescription(String keyword);
}

然后在 ProductService 类中添加一个 searchByNameAndDescription 方法:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.example.demo.entity.Product;
import com.example.demo.repository.ProductRepository;

import java.util.List;
import java.util.Optional;

@Service
public class ProductService {

    @Autowired
    private ProductRepository productRepository;

    public Product save(Product product) {
        return productRepository.save(product);
    }

    public Optional<Product> findById(String id) {
        return productRepository.findById(id);
    }

    public List<Product> findAll() {
        return (List<Product>) productRepository.findAll();
    }

    public void deleteById(String id) {
        productRepository.deleteById(id);
    }

    public List<Product> searchByName(String name) {
        return productRepository.findByNameContaining(name);
    }

    public List<Product> searchByNameAndDescription(String keyword) {
        return productRepository.searchByNameAndDescription(keyword);
    }
}

最后,在 ProductController 类中添加一个 searchByNameAndDescription 接口:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.example.demo.entity.Product;
import com.example.demo.service.ProductService;

import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping("/products")
public class ProductController {

    @Autowired
    private ProductService productService;

    @PostMapping
    public Product save(@RequestBody Product product) {
        return productService.save(product);
    }

    @GetMapping("/{id}")
    public Optional<Product> findById(@PathVariable String id) {
        return productService.findById(id);
    }

    @GetMapping
    public List<Product> findAll() {
        return productService.findAll();
    }

    @DeleteMapping("/{id}")
    public void deleteById(@PathVariable String id) {
        productService.deleteById(id);
    }

    @GetMapping("/search2")
    public List<Product> searchByNameAndDescription(@RequestParam String keyword) {
        return productService.searchByNameAndDescription(keyword);
    }
}

现在,你可以使用以下命令进行搜索:

curl http://localhost:8080/products/search2?keyword=手机

9.3 使用 ElasticsearchTemplate 实现搜索

ElasticsearchTemplate 提供了更加底层的 API,可以实现更复杂的搜索需求。

首先,在 Spring Boot 容器中注入 ElasticsearchTemplate

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.stereotype.Service;
import com.example.demo.entity.Product;
import com.example.demo.repository.ProductRepository;

import java.util.List;
import java.util.Optional;

@Service
public class ProductService {

    @Autowired
    private ProductRepository productRepository;

    @Autowired
    private ElasticsearchTemplate elasticsearchTemplate;

    public Product save(Product product) {
        return productRepository.save(product);
    }

    public Optional<Product> findById(String id) {
        return productRepository.findById(id);
    }

    public List<Product> findAll() {
        return (List<Product>) productRepository.findAll();
    }

    public void deleteById(String id) {
        productRepository.deleteById(id);
    }

    public List<Product> searchByName(String name) {
        return productRepository.findByNameContaining(name);
    }

    public List<Product> searchByNameAndDescription(String keyword) {
        return productRepository.searchByNameAndDescription(keyword);
    }
}

然后,在 ProductService 类中添加一个 searchByTemplate 方法:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.SearchQuery;
import org.springframework.stereotype.Service;
import com.example.demo.entity.Product;
import com.example.demo.repository.ProductRepository;
import static org.elasticsearch.index.query.QueryBuilders.*;

import java.util.List;
import java.util.Optional;

@Service
public class ProductService {

    @Autowired
    private ProductRepository productRepository;

    @Autowired
    private ElasticsearchTemplate elasticsearchTemplate;

    public Product save(Product product) {
        return productRepository.save(product);
    }

    public Optional<Product> findById(String id) {
        return productRepository.findById(id);
    }

    public List<Product> findAll() {
        return (List<Product>) productRepository.findAll();
    }

    public void deleteById(String id) {
        productRepository.deleteById(id);
    }

    public List<Product> searchByName(String name) {
        return productRepository.findByNameContaining(name);
    }

    public List<Product> searchByNameAndDescription(String keyword) {
        return productRepository.searchByNameAndDescription(keyword);
    }

    public List<Product> searchByTemplate(String keyword) {
        SearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(boolQuery()
                        .should(matchQuery("name", keyword))
                        .should(matchQuery("description", keyword)))
                .build();

        return elasticsearchTemplate.queryForList(searchQuery, Product.class);
    }
}

最后,在 ProductController 类中添加一个 searchByTemplate 接口:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.example.demo.entity.Product;
import com.example.demo.service.ProductService;

import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping("/products")
public class ProductController {

    @Autowired
    private ProductService productService;

    @PostMapping
    public Product save(@RequestBody Product product) {
        return productService.save(product);
    }

    @GetMapping("/{id}")
    public Optional<Product> findById(@PathVariable String id) {
        return productService.findById(id);
    }

    @GetMapping
    public List<Product> findAll() {
        return productService.findAll();
    }

    @DeleteMapping("/{id}")
    public void deleteById(@PathVariable String id) {
        productService.deleteById(id);
    }

    @GetMapping("/search3")
    public List<Product> searchByTemplate(@RequestParam String keyword) {
        return productService.searchByTemplate(keyword);
    }
}

现在,你可以使用以下命令进行搜索:

curl http://localhost:8080/products/search3?keyword=手机

10. 总结

本文详细介绍了如何将 Elasticsearch 集成到 Spring Boot 应用中,并实现了基本的 CRUD 操作和搜索功能。通过学习本文,你应该能够:

  • 理解 Elasticsearch 的基本概念和优势。
  • 配置 Spring Boot 连接 Elasticsearch。
  • 定义数据模型 (Entity) 映射到 Elasticsearch 中的文档。
  • 使用 ElasticsearchRepository 实现 CRUD 操作。
  • 使用 Query Methods、@Query 注解和 ElasticsearchTemplate 实现搜索功能。

当然,Elasticsearch 的功能远不止这些,例如:

  • 聚合分析: 可以对数据进行聚合、分组、统计等操作,挖掘潜在价值。
  • 地理位置搜索: 可以根据地理位置信息进行搜索。
  • 自动补全: 可以根据用户输入的内容,自动提示可能的搜索关键词。

希望本文能帮助你入门 Elasticsearch,并在你的实际项目中发挥它的强大作用! 祝你编程愉快,早日成为 Elasticsearch 大师!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注