小易说IT 小易说IT

SpringBoot3 + Elasticsearch8 + IK 分词 完整 Demo

环境说明: SpringBoot 3.x、Java17、Elasticsearch 8.x、IK 分词器(版本和 ES 一致) ES8 默认开启 SSL + 账号密码,下面代码同时支持:账号密码 + 关闭 SSL(本地开发) 客户端用官方新客户端:Elasticsearch Java Client(新 API,不是老的 HighLevelRestClient)

1. 依赖(pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.0</version>
        <relativePath/>
    </parent>
    <groupId>com.esdemo</groupId>
    <artifactId>es-ik-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>es-ik-demo</name>

    <properties>
        <java.version>17</java.version>
        <!-- 和你的ES版本保持一致 -->
        <elasticsearch.version>8.15.0</elasticsearch.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- ES官方Java客户端 -->
        <dependency>
            <groupId>co.elastic.clients</groupId>
            <artifactId>elasticsearch-java</artifactId>
            <version>${elasticsearch.version}</version>
        </dependency>
        <!-- Jackson适配,SpringBoot3需要 -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <dependency>
            <groupId>jakarta.json</groupId>
            <artifactId>jakarta.json-api</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>

2. application.yml 配置

spring:
  application:
    name: es-ik-demo

elasticsearch:
  host: 127.0.0.1
  port: 9200
  username: elastic
  password: elastic123456
  # 本地开发关闭SSL,ES8默认是true
  ssl-enabled: false

3. ES 客户端配置类 EsConfig.java

package com.esdemo.config;

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class EsConfig {

    @Value("${elasticsearch.host}")
    private String host;
    @Value("${elasticsearch.port}")
    private int port;
    @Value("${elasticsearch.username}")
    private String username;
    @Value("${elasticsearch.password}")
    private String password;
    @Value("${elasticsearch.ssl-enabled}")
    private boolean sslEnabled;

    @Bean
    public ElasticsearchClient elasticsearchClient() {
        BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
        credentialsProvider.setCredentials(AuthScope.ANY,
                new UsernamePasswordCredentials(username, password));

        RestClientBuilder builder = RestClient.builder(new HttpHost(host, port, sslEnabled ? "https" : "http"))
                .setHttpClientConfigCallback(httpClientBuilder ->
                        httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider));

        RestClient restClient = builder.build();
        ElasticsearchTransport transport = new RestClientTransport(restClient, new co.elastic.clients.json.jackson.JacksonJsonpMapper());
        return new ElasticsearchClient(transport);
    }
}

4. 实体类 Article.java(文档模型)

索引:article,title、content 使用 IK 分词

package com.esdemo.entity;

import lombok.Data;

@Data
public class Article {
    private Long id;
    private String title;
    private String content;
    private String author;
}

5. ES 服务类 ArticleEsService.java

包含:创建索引(IK Mapping)、新增文档、删除、全文检索、高亮查询

package com.esdemo.service;

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.core.*;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch.indices.CreateIndexRequest;
import co.elastic.clients.elasticsearch.indices.CreateIndexResponse;
import co.elastic.clients.elasticsearch.indices.DeleteIndexRequest;
import co.elastic.clients.json.JsonData;
import com.esdemo.entity.Article;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@Service
@RequiredArgsConstructor
public class ArticleEsService {

    private final ElasticsearchClient esClient;
    private static final String INDEX_NAME = "article";

    /**
     * 创建索引,指定IK分词
     */
    public boolean createIndex() throws IOException {
        // mapping:title/content 索引用ik_max_word,搜索用ik_smart
        String mappingJson = """
                {
                  "properties": {
                    "id": {"type": "long"},
                    "title": {
                      "type": "text",
                      "analyzer": "ik_max_word",
                      "search_analyzer": "ik_smart"
                    },
                    "content": {
                      "type": "text",
                      "analyzer": "ik_max_word",
                      "search_analyzer": "ik_smart"
                    },
                    "author": {"type": "keyword"}
                  }
                }
                """;
        CreateIndexRequest request = CreateIndexRequest.of(b -> b
                .index(INDEX_NAME)
                .mappings(m -> m.withJson(mappingJson))
        );
        CreateIndexResponse resp = esClient.indices().create(request);
        return resp.acknowledged();
    }

    /**
     * 删除索引
     */
    public boolean deleteIndex() throws IOException {
        DeleteIndexRequest req = DeleteIndexRequest.of(b -> b.index(INDEX_NAME));
        return esClient.indices().delete(req).acknowledged();
    }

    /**
     * 新增/更新文档
     */
    public String saveDoc(Article article) throws IOException {
        IndexRequest<Article> req = IndexRequest.of(b -> b
                .index(INDEX_NAME)
                .id(article.getId().toString())
                .document(article)
        );
        IndexResponse resp = esClient.index(req);
        return resp.result().name();
    }

    /**
     * 根据id删除文档
     */
    public String deleteDoc(Long id) throws IOException {
        DeleteRequest req = DeleteRequest.of(b -> b.index(INDEX_NAME).id(id.toString()));
        DeleteResponse resp = esClient.delete(req);
        return resp.result().name();
    }

    /**
     * 全文检索:match查询,IK分词生效
     */
    public List<Article> search(String keyword) throws IOException {
        SearchRequest req = SearchRequest.of(b -> b
                .index(INDEX_NAME)
                .query(q -> q.multiMatch(m -> m
                        .query(keyword)
                        .fields("title", "content")
                ))
        );
        SearchResponse<Article> resp = esClient.search(req, Article.class);
        List<Article> list = new ArrayList<>();
        for (Hit<Article> hit : resp.hits().hits()) {
            list.add(hit.source());
        }
        return list;
    }

    /**
     * 带高亮的全文检索(前端标红关键词)
     */
    public List<Article> searchHighlight(String keyword) throws IOException {
        SearchRequest req = SearchRequest.of(b -> b
                .index(INDEX_NAME)
                .query(q -> q.multiMatch(m -> m
                        .query(keyword)
                        .fields("title", "content")
                ))
                .highlight(h -> h
                        .fields("title", f -> f)
                        .fields("content", f -> f)
                        .preTags("<em style='color:red'>")
                        .postTags("</em>")
                )
        );
        SearchResponse<Article> resp = esClient.search(req, Article.class);
        List<Article> result = new ArrayList<>();
        for (Hit<Article> hit : resp.hits().hits()) {
            Article article = hit.source();
            // 覆盖为高亮文本
            if (hit.highlight().containsKey("title")) {
                article.setTitle(hit.highlight().get("title").get(0));
            }
            if (hit.highlight().containsKey("content")) {
                article.setContent(hit.highlight().get("content").get(0));
            }
            result.add(article);
        }
        return result;
    }
}

6. Controller ArticleEsController.java

package com.esdemo.controller;

import com.esdemo.entity.Article;
import com.esdemo.service.ArticleEsService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.util.List;

@RestController
@RequestMapping("/es/article")
@RequiredArgsConstructor
public class ArticleEsController {

    private final ArticleEsService articleEsService;

    @PostMapping("/createIndex")
    public Boolean createIndex() throws IOException {
        return articleEsService.createIndex();
    }

    @DeleteMapping("/deleteIndex")
    public Boolean deleteIndex() throws IOException {
        return articleEsService.deleteIndex();
    }

    @PostMapping("/save")
    public String save(@RequestBody Article article) throws IOException {
        return articleEsService.saveDoc(article);
    }

    @DeleteMapping("/{id}")
    public String delete(@PathVariable Long id) throws IOException {
        return articleEsService.deleteDoc(id);
    }

    @GetMapping("/search")
    public List<Article> search(@RequestParam String keyword) throws IOException {
        return articleEsService.search(keyword);
    }

    @GetMapping("/search/highlight")
    public List<Article> searchHl(@RequestParam String keyword) throws IOException {
        return articleEsService.searchHighlight(keyword);
    }
}

7. 启动类

package com.esdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class EsIkDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(EsIkDemoApplication.class, args);
    }
}

✅ 测试流程(按顺序执行)

  1. 先确认 ES 已经装好 IK 分词器,版本和 ES 一致,ES 正常启动

  2. 启动 SpringBoot 项目

  3. 创建索引:POST http://127.0.0.1:8080/es/article/createIndex

  4. 插入测试数据

POST http://127.0.0.1:8080/es/article/save
Content-Type: application/json
{
    "id": 1,
    "title": "南京长江大桥游玩攻略",
    "content": "南京长江大桥是南京著名地标,适合亲子出游",
    "author": "张三"
}
POST http://127.0.0.1:8080/es/article/save
Content-Type: application/json
{
    "id": 2,
    "title": "长江航运发展报告",
    "content": "长江沿线城市经济发展,航运物流高速增长",
    "author": "李四"
}
  1. 全文检索:GET http://127.0.0.1:8080/es/article/search?keyword=南京大桥

IK 会自动拆分【南京、长江大桥】,两条相关文档都能命中

  1. 高亮查询:GET http://127.0.0.1:8080/es/article/search/highlight?keyword=南京大桥

⚠️ 重点坑说明

  1. IK 分词只在创建索引的时候指定 mapping 才生效! 索引建好之后,再改 analyzer 是无效的,需要删除重建索引。

  2. ES8 默认 https,本地开发一定要配置 ssl-enabled: false

  3. 版本对齐:spring-data-elasticsearch 尽量和 ES 版本对齐,这里直接使用官方原生 client,避开 spring-data 的版本坑

  4. match 查询会自动使用 mapping 里配置的 search_analyzer=ik_smart