ElasticSearch学习笔记(三)

在了解了ES的基本代码之后,下一步就是将ES集成进SpringBoot并与项目结合

首先,需要配置application.yml和config配置类

elasticsearch:
  url: localhost
  port: 9200

有了yaml之后我们从yaml中把url和port都读取出来作为配置类需要用到的参数

@Configuration
public class EsConfig {
    @Value("${elasticsearch.url}")
    private String url;

    @Value("${elasticsearch.port}")
    private int port;

    @Bean
    public ElasticsearchClient elasticsearchClient() {
        // 1. 创建底层 REST 客户端
        RestClient restClient = RestClient.builder(
                new HttpHost(url, port, "http")
        ).build();

        // 2. 创建传输层,配置 JSON 映射器为 Jackson
        RestClientTransport restClientTransport = new RestClientTransport(
                restClient, new JacksonJsonpMapper()
        );

        // 3. 创建高级 ES 客户端
        return new ElasticsearchClient(restClientTransport);
    }
}

与ES服务器建立连接分三步,第一步,创建底层Rest客户端,ES本身对外提供HTTP接口,这一步的操作其实就是组装ES服务的地址 http://localhost:9200 ; 第二步,创建传输层,配置配置JSON 映射器为 Jackson,因为ES对外的9200HTTP接口,只认 JSON 格式。 第三步,创建高级ES客户端,也就是我们的业务代码直接使用的ES客户端对象,就不用想之前那种格式去写ES相关操作,而是支持用.连接的链式操作。配置完成后之后我们的后端就可以成功与ES服务器进行连接。

我们之前做的demo里(详情见前几期博客)完成了旅游攻略页和目的地攻略页的内容,作为一个旅游攻略网站一个类似浏览器一样的搜索引擎是必不可少的,于是,再有了上面的连接操作之后,第一步,进行ES的初始化,这一步的目的是将数据库中的需要进行检索的内容放到ES中,来看看代码

@Data
@AllArgsConstructor
@NoArgsConstructor
public class StrategyEs {


    private Long id;
    private String title;
    private String subTitle;
    private String summary;
}


  //删库
        BooleanResponse exists = client.indices().exists(e -> e
                .index(INDEX_NAME));

        if(exists.value()) {
            client.indices().delete(d->d
                    .index(INDEX_NAME));
        }
        //建库,建立映射
        CreateIndexRequest createIndexRequest = CreateIndexRequest.of(r -> r.index(INDEX_NAME)
                .settings(s -> s.numberOfShards("1").numberOfShards("1"))
                .mappings(m -> m.properties("id", p -> p.long_(l -> l))
                        .properties("title", p -> p.text(t -> t.analyzer("ik_max_word")))
                        .properties("subTitle", p -> p.text(t -> t.analyzer("ik_max_word")))
                        .properties("summary", p -> p.text(t -> t.analyzer("ik_max_word")))
                        .properties("embedding", p -> p.denseVector(d -> d.dims(dimension).index(true)
                                .similarity(DenseVectorSimilarity.Cosine)))
                ));
        client.indices().create(createIndexRequest);
        System.out.println("strategy es 建库成功");
        //查询mysql
        List<Strategy> list = remoteStrategyService.list("inner").getData();
        //同步mysql
        for (Strategy strategy :list){
            StrategyEs es = new StrategyEs();
            BeanUtils.copyProperties(strategy, es);
            client.index(i->i.index(INDEX_NAME).id(es.getId().toString()).document(es));
        }
    }

第一段代码对应的是检索内容的实体类,详情根据需要来定就欧克,但是切记字段名要和数据库的名字一样,第二段代码对应的就是相应的初始化操作了,首先,在每次进行初始化操作的时候,先删出ES中对应的库,可以看到使用.连接的链式操作,对象类型用的是 BooleanResponse,因为是请求中返回结果所以这个名字中多了一个Response,相应的获取值的时候也要用.value,如果存在就采用delete方法,可以看到,方法名基本上和我们ES操作的方法名是一致的且操作的时候必须以x->x的方式来操作,可以理解为,先创建对象再调用对象这种感觉,删库之后,ES中此时是空的了,这个时候就可以进行对应的建库操作,可以到原生的建库操作是

    PUT /book
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 0
  },
  "mappings": {
    "properties": {
      "title":{"type":"text"},
      "author":{"type":"keyword"},
      "content":{"type":"text"}
    }
  }
}

springboot中是

CreateIndexRequest.of(r -> r.index(INDEX_NAME)
                .settings(s -> s.numberOfShards("1").numberOfShards("1"))
                .mappings(m -> m.properties("id", p -> p.long_(l -> l))
                        .properties("title", p -> p.text(t -> t.analyzer("ik_max_word")))
                        .properties("subTitle", p -> p.text(t -> t.analyzer("ik_max_word")))
                        .properties("summary", p -> p.text(t -> t.analyzer("ik_max_word")))
                        .properties("embedding", p -> p.denseVector(d -> d.dims(dimension).index(true)
                                .similarity(DenseVectorSimilarity.Cosine)))
                ));

除了Put不一样,剩下的,基本上是大差不差的,方法名都是一致的,建库完毕后,再从mysql读出List类型的数据,再使用BeanUtils将数据库对象赋值给Es对象再使用document传给ES,这样一整个建库初始化操作就完成了,现在我们ES中数据,下一步就是进行检索操作了,下面是通用ES检索操作的代码

public <T, K> Page<T> searchWithHighLight(String indexName, Class<T> clazz, Class<K> esClazz, SearchQuery qo, String... fields) throws IOException, InvocationTargetException, IllegalAccessException {
        //构建高亮配置
        Map<String, HighlightField> fieldMap = new HashMap<>();
        for (String field : fields) {
            fieldMap.put(field, HighlightField.of(h->h));
        }
        Highlight highlight = Highlight.of(h -> h.preTags("<span style='color:red'>")
                .postTags("</span>")
                .fields(fieldMap));
       
	   //执行 ES 搜索
        SearchResponse<K> resp = elasticsearchClient.search(sh -> sh.index(indexName)
                .from((qo.getPageNum() - 1) * qo.getPageSize())
                .size(qo.getPageSize())
                .query(q -> q.multiMatch(m -> m.query(qo.getKeyword()).fields(Arrays.asList(fields)).analyzer("ik_max_word")))
                .highlight(highlight), esClazz);
       
	   //处理搜索结果
		HitsMetadata<K> hits = resp.hits();
        long total = hits.total().value();
        List<Hit<K>> hits1 = hits.hits();
        List<T> list = new ArrayList<>();
        for (Hit<K> hit : hits1) {
            Long id = Long.valueOf(hit.id());
            T t = null;
            if("strategy".equals(indexName)){
                 t = (T) remoteStrategyService.getOne(id, "inner").getData();
            } else if ("note".equals(indexName)) {
                t = (T) remoteNoteService.getOne(id, "inner").getData();
            } else if ("destination".equals(indexName)) {
                t = (T) remoteDestinationService.getOne(id, "inner").getData();
            } else if ("userinfo".equals(indexName)) {
                t = (T) remoteUserInfoService.getOne(id, "inner").getData();
            }

            Map<String, List<String>> map = hit.highlight();
            for (String key : map.keySet()) {
                List<String> stringList = map.get(key);
                StringBuffer stringBuffer = new StringBuffer();
                for (String string : stringList) {
                    stringBuffer.append(string);
                }
                String value = stringBuffer.toString();
                BeanUtils.setProperty(t, key, value);
            }
            list.add(t);
        }

        //封装分页结果
        Pageable pageable = PageRequest.of(qo.getCurrentPage() - 1, qo.getPageSize());
        return new PageImpl<T>(list, pageable, total);
    }

这段代码比较长,我们可以把它分为四部分,第一步构建高亮配置,

  //构建高亮配置
        Map<String, HighlightField> fieldMap = new HashMap<>();
        for (String field : fields) {
            fieldMap.put(field, HighlightField.of(h->h));
        }
        Highlight highlight = Highlight.of(h -> h.preTags("<span style='color:red'>")
                .postTags("</span>")
                .fields(fieldMap));

啥是高亮,就是在浏览器搜索完的内容,在对应的文章中会被标记,下面是例子

image

图中加红的部分就是所谓的高亮,我们拆解来看,首先,遍历所有搜索字段,为每个字段创建 HighlightField,然后,配置高亮前后标签:红色 HTML 标签,搜索结果中匹配关键字会以 <span style='color:red'>关键字</span> 方式被包裹,在前端就会显示这种红色的高亮内容了

第二步,执行ES搜索

 SearchResponse<K> resp = elasticsearchClient.search(sh -> sh.index(indexName)
                .from((qo.getPageNum() - 1) * qo.getPageSize())
                .size(qo.getPageSize())
                .query(q -> q.multiMatch(m -> m.query(qo.getKeyword()).fields(Arrays.asList(fields)).analyzer("ik_max_word")))
                .highlight(highlight), esClazz);

关键查询参数说明:

参数 说明
index 要搜索的索引名称
from 分页偏移量 = (页码 - 1(索引是从0开始不是1)) × 每页大小
size 返回的文档数量
multiMatch 多字段匹配查询
analyzer 指定分词器为 ik_max_word
highlight 启用高亮功能

这样就能查回来一个resp返回对象,里面装着从ES中查出来的JSON格式的数据。

第三步处理搜索结果

HitsMetadata<K> hits = resp.hits();
        long total = hits.total().value();      // 获取总命中数
        List<Hit<K>> hits1 = hits.hits();       // 获取命中的文档列表
        List<T> list = new ArrayList<>();
        for (Hit<K> hit : hits1) {
            Long id = Long.valueOf(hit.id());
             // 1. 根据索引类型调用不同的 Feign 接口获取完整数据
			T t = null;
            if("strategy".equals(indexName)){
                 t = (T) remoteStrategyService.getOne(id, "inner").getData();
            } else if ("note".equals(indexName)) {
                t = (T) remoteNoteService.getOne(id, "inner").getData();
            } else if ("destination".equals(indexName)) {
                t = (T) remoteDestinationService.getOne(id, "inner").getData();
            } else if ("userinfo".equals(indexName)) {
                t = (T) remoteUserInfoService.getOne(id, "inner").getData();
            }
           // 2. 用高亮返回的片段覆盖原始字段(关键字高亮显示)
            Map<String, List<String>> map = hit.highlight();
            for (String key : map.keySet()) {
                List<String> stringList = map.get(key);
                StringBuffer stringBuffer = new StringBuffer();
                for (String string : stringList) {
                    stringBuffer.append(string);
                }
                String value = stringBuffer.toString();
                BeanUtils.setProperty(t, key, value);
            }
            list.add(t);
        }

第四步,封装

Pageable pageable = PageRequest.of(qo.getCurrentPage() - 1, qo.getPageSize());
return new PageImpl<T>(list, pageable, total);

这个是依据底层框架来的,这样一个通用的搜索就设计好了(可以根据一个服务抽象出来),最后再来讲讲这中间的参数

public interface ISearchService {
    <T, K> Page<T> searchWithHighLight(
        String indexName,    // 索引名
        Class<T> clazz,      // 返回的业务对象类型
        Class<K> esClazz,    // ES 文档类型
        SearchQuery qo,      // 查询参数
        String... fields     // 搜索字段(可变参数)
    ) throws IOException, InvocationTargetException, IllegalAccessException;
}
posted @ 2026-08-25 20:26  霜华序  阅读(8)  评论(0)    收藏  举报