SpringBoot 整合Jest

 

【1】添加Elasticsearch-starter

pom文件添加starter如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

SpringBoot默认支持两种技术和Elasticsearch进行交互:Spring Data Elasticsearch和Jest。

Jest默认不生效,需要导入io.searchbox.client.JestClient。

这里写图片描述
maven依赖如下:

<!--导入jest依赖-->
<dependency>
    <groupId>io.searchbox</groupId>
    <artifactId>jest</artifactId>
    <version>5.3.3</version>
</dependency>

Spring Data Elasticsearch主要作用如下:

① ElasticsearchAutoConfiguration中注册了client,属性有clusterNodes和clusterName。

这里写图片描述


② ElasticsearchDataAutoConfiguration注册了ElasticsearchTemplate来操作ES

@Configuration
@ConditionalOnClass({ Client.class, ElasticsearchTemplate.class })
@AutoConfigureAfter(ElasticsearchAutoConfiguration.class)
public class ElasticsearchDataAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnBean(Client.class)
    public ElasticsearchTemplate elasticsearchTemplate(Client client,
            ElasticsearchConverter converter) {
        try {
            return new ElasticsearchTemplate(client, converter);
        }
        catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
    }

    @Bean
    @ConditionalOnMissingBean
    public ElasticsearchConverter elasticsearchConverter(
            SimpleElasticsearchMappingContext mappingContext) {
        return new MappingElasticsearchConverter(mappingContext);
    }

    @Bean
    @ConditionalOnMissingBean
    public SimpleElasticsearchMappingContext mappingContext() {
        return new SimpleElasticsearchMappingContext();
    }

}
  •  

③ ElasticsearchRepositoriesAutoConfiguration 启用了ElasticsearchRepository

@Configuration
@ConditionalOnClass({ Client.class, ElasticsearchRepository.class })
@ConditionalOnProperty(prefix = "spring.data.elasticsearch.repositories", name = "enabled", havingValue = "true", matchIfMissing = true)
@ConditionalOnMissingBean(ElasticsearchRepositoryFactoryBean.class)
@Import(ElasticsearchRepositoriesRegistrar.class)
public class ElasticsearchRepositoriesAutoConfiguration {

}

ElasticsearchRepository接口源码如下(类似于JPA中的接口):

@NoRepositoryBean
public interface ElasticsearchRepository<T, ID extends Serializable> extends ElasticsearchCrudRepository<T, ID> {
    <S extends T> S index(S var1);

    Iterable<T> search(QueryBuilder var1);

    Page<T> search(QueryBuilder var1, Pageable var2);

    Page<T> search(SearchQuery var1);

    Page<T> searchSimilar(T var1, String[] var2, Pageable var3);

    void refresh();

    Class<T> getEntityClass();
}

【2】JestClient操作测试

application.properties配置如下:

# jest url配置
spring.elasticsearch.jest.uris=http://192.168.2.110:9200

测试类如下:

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringBootJestTest {

    @Autowired
    JestClient jestClient;

    @Test
    public void index(){

        Article article = new Article();
        article.setId(1);
        article.setAuthor("Tom");
        article.setContent("hello world !");
        article.setTitle("今日消息");
        //构建一个索引功能,类型为news
        Index index = new Index.Builder(article).index("jest").type("news").build();

        try {
            jestClient.execute(index);
            System.out.println("数据索引成功!");
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    @Test
    public void search(){
        //查询表达式
        String json = "{\n" +
                "    \"query\" : {\n" +
                "        \"match\" : {\n" +
                "            \"content\" : \"hello\"\n" +
                "        }\n" +
                "    }\n" +
                "}";
        //构建搜索功能
        Search search = new Search.Builder(json).addIndex("jest").addType("news").build();

        try {
            SearchResult result = jestClient.execute(search);
            System.out.println(result.getJsonString());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

测试存储数据结果如下:

这里写图片描述


测试查询数据结果如下:

这里写图片描述


【3】 Elasticsearch版本调整

application.properties进行配置:

# Spring data elasticsearch配置
spring.data.elasticsearch.cluster-name=elasticsearch
spring.data.elasticsearch.cluster-nodes=192.168.2.110:9300

这里节点名取自如下图: 
这里写图片描述


启动主程序,可能报错如下(ES版本不合适):

这里写图片描述


查看Spring Data官网,其中spring data elasticsearch与elasticsearch适配表如下:

这里写图片描述

官网地址:https://github.com/spring-projects/spring-data-elasticsearch

我们在上篇博文中安装的ES版本为5.6.10,项目中SpringBoot版本为1.5.12,spring-boot-starter-data-elasticsearch为2.1.11,elasticsearch版本为2.4.6。

两种解决办法:① 升级SpringBoot版本;② 安装2.4.6版本的elasticsearch。

这里修改暴露的端口,重新使用docker安装2.4.6版本:

# 拉取2.4.6 镜像
docker pull registry.docker-cn.com/library/elasticsearch:2.4.6
# 启动容器
docker run -e ES_JAVA_OPTS="-Xms256m -Xmx256m" -d -p 9201:9200 -p 9301:9300 --name ES02 bc337c8e4f

这里写图片描述

application.properties配置文件同步修改:

# jest url配置
spring.elasticsearch.jest.uris=http://192.168.2.110:9201

# Spring data elasticsearch配置
spring.data.elasticsearch.cluster-name=elasticsearch
spring.data.elasticsearch.cluster-nodes=192.168.2.110:9301

此时再次启动程序:

这里写图片描述


【4】ElasticsearchRepository使用

类似于JPA,编写自定义Repository接口,继承自ElasticsearchRepository:

public interface BookRepository extends ElasticsearchRepository<Book,Integer> {

    public List<Book> findByBookNameLike(String bookName);
}

这里第一个参数为对象类型,第二个参数为对象的主键类型。

BookRepository 所拥有的方法如下图:

这里写图片描述


Book源码如下:

// 这里注意注解
@Document(indexName = "elastic",type = "book")
public class Book {

    private Integer id;
    private String bookName;
    private String author;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getBookName() {
        return bookName;
    }

    public void setBookName(String bookName) {
        this.bookName = bookName;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    @Override
    public String toString() {
        return "Book{" +
                "id=" + id +
                ", bookName='" + bookName + '\'' +
                ", author='" + author + '\'' +
                '}';
    }
}

测试类如下:

    @Autowired
    BookRepository bookRepository;

    @Test
    public void testRepository(){
        Book book = new Book();
        book.setAuthor("吴承恩");
        book.setBookName("西游记");
        book.setId(1);
        bookRepository.index(book);
        System.out.println("BookRepository 存入数据成功!");
    }

测试结果如下图:

这里写图片描述


测试获取示例如下:

     @Test
    public void testRepository2(){
        for (Book book : bookRepository.findByBookNameLike("游")) {
            System.out.println("获取的book : "+book);
        } ;
        Book book = bookRepository.findOne(1);
        System.out.println("根据id查询 : "+book);
    }

测试结果如下图:

这里写图片描述


Elasticsearch支持方法关键字如下图所示

这里写图片描述
这里写图片描述
这里写图片描述

即,在BookRepository中使用上述关键字构造方法,即可使用,Elastic自行实现其功能!


支持@Query注解

如下所示,直接在方法上使用注解:

public interface BookRepository extends ElasticsearchRepository<Book, String> {
    @Query("{"bool" : {"must" : {"field" : {"name" : "?0"}}}}")
    Page<Book> findByName(String name,Pageable pageable);
}

【5】ElasticsearchTemplate使用

存入数据源码示例如下:

    @Autowired
    ElasticsearchTemplate elasticsearchTemplate;

    @Test
    public void testTemplate01(){

        Book book = new Book();
        book.setAuthor("曹雪芹");
        book.setBookName("红楼梦");
        book.setId(2);
        IndexQuery indexQuery = new IndexQueryBuilder().withId(String.valueOf(book.getId())).withObject(book).build();
        elasticsearchTemplate.index(indexQuery);
    }

测试结果如下:

这里写图片描述


查询数据示例如下:

    @Test
    public void testTemplate02(){
        QueryStringQueryBuilder stringQueryBuilder = new QueryStringQueryBuilder("楼");
        stringQueryBuilder.field("bookName");
        SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(stringQueryBuilder).build();
        Page<Book> books = elasticsearchTemplate.queryForPage(searchQuery,Book.class);
        Iterator<Book> iterator = books.iterator();
        while(iterator.hasNext()){
            Book book = iterator.next();
            System.out.println("该次获取的book:"+book);
        }
    }

测试结果如下:

这里写图片描述

开源项目:https://github.com/spring-projects/spring-data-elasticsearch

https://github.com/searchbox-io/Jest/tree/master/jest

1.说明

本文主要讲解如何使用Spring Boot快速搭建Web框架,结合Spring Data 和 Jest 快速实现对阿里云ElasticSearch的全文检索功能。
主要使用组件:
Spring Boot Starter:可以帮助我们快速的搭建spring mvc 环境
Jest:一种rest访问es的客户端
elasticsearch:全文检索
spring data elasticsearch:结合spring data
thymeleaf:web前端模版框架
jquery:js框架
bootstrap:前端样式框架

2.项目Maven配置

以下为项目Maven配置,尤其需要注意各个组件的版本,以及注释部分。
各个组件的某些版本组合下回出现各种异常,以下maven为测试可通过的一个版本。

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.lewis</groupId>
    <artifactId>esweb</artifactId>
    <version>0.1</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <!--必须用2.0+,否则会有一个类
        Caused by: java.lang.NoSuchMethodError: org.elasticsearch.common.settings.Settings.settingsBuilder()Lorg/elasticsearch/common/settings/Settings$Builder;
        -->
        <version>2.0.0.M7</version>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <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-jpa</artifactId>
        </dependency>
        -->

        <!--不可使用version 5.3.3,会有一个类的方法找不到-->
        <dependency>
            <groupId>io.searchbox</groupId>
            <artifactId>jest</artifactId>
            <version>5.3.2</version>
        </dependency>

        <!--必须用5.0+,否则会有一个类找不到org/elasticsearch/node/NodeValidationException-->
        <dependency>
            <groupId>org.elasticsearch</groupId>
            <artifactId>elasticsearch</artifactId>
            <version>5.3.3</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-elasticsearch</artifactId>
            <version>3.0.0.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>com.github.vanroy</groupId>
            <artifactId>spring-boot-starter-data-jest</artifactId>
            <version>3.0.0.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <!--
        不需要引用
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>
        -->

        <!--spring boot elasticsearch 缺少的jar,需要单独引入-->
        <dependency>
            <groupId>net.java.dev.jna</groupId>
            <artifactId>jna</artifactId>
            <version>4.5.1</version>
        </dependency>

        <!--webjars 前端框架,整体管理前端js框架-->
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>jquery</artifactId>
            <version>3.3.0</version>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>bootstrap</artifactId>
            <version>4.0.0</version>
        </dependency>

        <!--When using Spring Boot version 1.3 or higher, it will automatically detect the webjars-locator library on the classpath and use it to automatically resolve the version of any WebJar assets for you. In order to enable this feature, you will need to add the webjars-locator library as a dependency of your application in the pom.xml file-->
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>webjars-locator</artifactId>
            <version>0.30</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <fork>true</fork>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

创建完成后,项目目录结构如下:
image

3.Spring Starter配置

  1. 需使用SpringBootApplication启动
  2. 需禁用ElasticsearchAutoConfiguration,ElasticsearchDataAutoConfiguration,否则会有异常
  3. HighLightJestSearchResultMapper Bean留待下面解释,主要为了解决spring data不支持elasticsearch检索highlight问题,此处为该Bean的注册
@SpringBootApplication
@EnableAutoConfiguration(exclude = {ElasticsearchAutoConfiguration.class, ElasticsearchDataAutoConfiguration.class})
public class App {

    public static void main(String[] args) throws Exception {
        SpringApplication.run(App.class, args);
    }

    @Bean
    public HighLightJestSearchResultMapper highLightJestSearchResultMapper(){
        return new HighLightJestSearchResultMapper();
    }

}

3.Entity配置

a) 歌曲Entity如下:

通过对Class进行Document注解,实现与ElasticSearch中的Index和Type一一对应。
该类在最终与ES返回结果映射时,仅映射其中_source部分。即如下图部分(highlight另说,后面单独处理了):
image

@Document(indexName = "songs",type = "sample",shards = 1, replicas = 0, refreshInterval = "-1")
public class Song extends HighLightEntity{

    @Id
    private Long id;

    private String name;
    private String href;
    private String lyric;
    private String singer;
    private String album;

    public Song(Long id, String name, String href, String lyric, String singer, String album, Map<String, List<String>> highlight) {
       //省略
    }

    public Song() {
    }
    //getter setter 省略...
}

b) 为了解决Spring data elasticsearch问题,此处增加一个抽象类:HighLightEntity,其他Entity需要继承该类。

package org.leiws.esweb.entity;

import java.io.Serializable;
import java.util.List;
import java.util.Map;

public abstract class HighLightEntity implements Serializable{

    private Map<String, List<String>> highlight;

    public Map<String, List<String>> getHighlight() {
        return highlight;
    }

    public void setHighlight(Map<String, List<String>> highlight) {
        this.highlight = highlight;
    }
}

4.Repository配置

package org.leiws.esweb.repository;
import org.leiws.esweb.entity.Song;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
public interface SongRepository extends ElasticsearchRepository<Song,Long> {
}

5.Service配置

a) 接口

package org.leiws.esweb.service;

import org.leiws.esweb.entity.Song;
import org.springframework.data.domain.Page;

import java.util.List;

/**
 * The interface Song service.
 */
public interface SongService {

    /**
     * Search song list.
     *
     * @param pNum     the p num
     * @param pSize    the p size
     * @param keywords the keywords
     * @return the list
     */
    public Page<Song> searchSong(Integer pNum, Integer pSize, String keywords);
}

b) 实现类

该类实现了具体如何分页,如何查询等

package org.leiws.esweb.service.impl;

import com.github.vanroy.springdata.jest.JestElasticsearchTemplate;
import org.apache.log4j.Logger;
import org.elasticsearch.common.lucene.search.function.FiltersFunctionScoreQuery;
import org.elasticsearch.index.query.MatchPhraseQueryBuilder;
import org.elasticsearch.index.query.MatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.leiws.esweb.entity.Song;
import org.leiws.esweb.repository.HighLightJestSearchResultMapper;
import org.leiws.esweb.repository.SongRepository;
import org.leiws.esweb.service.SongService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.SearchQuery;
import org.springframework.stereotype.Service;
import static org.elasticsearch.index.query.QueryBuilders.functionScoreQuery;
import static org.elasticsearch.index.query.QueryBuilders.matchPhraseQuery;

import java.util.List;

@Service
public class SongServiceImpl implements SongService{
    private static final Logger LOGGER = Logger.getLogger(SongServiceImpl.class);
    /* 分页参数 */
    private final static Integer PAGE_SIZE = 12;          // 每页数量
    private final static Integer DEFAULT_PAGE_NUMBER = 0; // 默认当前页码

    /* 搜索模式 */
    private final static String SCORE_MODE_SUM = "sum"; // 权重分求和模式
    private final static Float  MIN_SCORE = 10.0F;      // 由于无相关性的分值默认为 1 ,设置权重分最小值为 10

    @Autowired
    SongRepository songRepository;

    @Autowired
    JestElasticsearchTemplate jestElasticsearchTemplate;

    @Autowired
    HighLightJestSearchResultMapper jestSearchResultMapper;

    @Override
    public Page<Song> searchSong(Integer pNum, Integer pSize, String keywords) {
        // 校验分页参数
        if (pSize == null || pSize <= 0) {
            pSize = PAGE_SIZE;
        }

        if (pNum == null || pNum < DEFAULT_PAGE_NUMBER) {
            pNum = DEFAULT_PAGE_NUMBER;
        }

        LOGGER.info("\n searchCity: searchContent [" + keywords + "] \n ");
        // 构建搜索查询
        SearchQuery searchQuery = getCitySearchQuery(pNum,pSize,keywords);
        LOGGER.info("\n searchCity: searchContent [" + keywords + "] \n DSL  = \n " + searchQuery.getQuery().toString());
//        Page<Song> cityPage = songRepository.search(searchQuery);
        Page<Song> cityPage = jestElasticsearchTemplate.queryForPage(searchQuery,Song.class,jestSearchResultMapper);
        return cityPage;
    }
    /**
     * 根据搜索词构造搜索查询语句
     *
     * 代码流程:
     *      - 权重分查询
     *      - 短语匹配
     *      - 设置权重分最小值
     *      - 设置分页参数
     *
     * @param pNum 当前页码
     * @param pSize 每页大小
     * @param searchContent 搜索内容
     * @return
     */
    private SearchQuery getCitySearchQuery(Integer pNum, Integer pSize,String searchContent) {

        /* elasticsearch 2.4.6 版本写法
        FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery()
                .add(QueryBuilders.boolQuery().should(QueryBuilders.matchQuery("lyric", searchContent)),
                        ScoreFunctionBuilders.weightFactorFunction(1000))
                .scoreMode(SCORE_MODE_SUM).setMinScore(MIN_SCORE);
        */


        FunctionScoreQueryBuilder.FilterFunctionBuilder[] functions = {
                new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                        matchPhraseQuery("lyric", searchContent),
                        ScoreFunctionBuilders.weightFactorFunction(1000))
        };
        FunctionScoreQueryBuilder functionScoreQueryBuilder =
                functionScoreQuery(functions).scoreMode(FiltersFunctionScoreQuery.ScoreMode.SUM).setMinScore(MIN_SCORE);

        // 分页参数
//        Pageable pageable = new PageRequest(pNum, pSize);
        Pageable pageable = PageRequest.of(pNum, pSize);

        //高亮提示
        HighlightBuilder.Field highlightField =  new HighlightBuilder.Field("lyric")
                .preTags(new String[]{"<font color='red'>", "<b>", "<em>"})
                .postTags(new String[]{"</font>", "</b>", "</em>"})
                .fragmentSize(15)
                .numOfFragments(5)

                //highlightQuery必须单独设置,否则在使用FunctionScoreQuery时,highlight配置不生效,返回结果无highlight元素
                //官方解释:Highlight matches for a query other than the search query. This is especially useful if you use a rescore query because those are not taken into account by highlighting by default.
                .highlightQuery(matchPhraseQuery("lyric", searchContent));

        return new NativeSearchQueryBuilder()
                .withPageable(pageable)
    //            .withSourceFilter(new FetchSourceFilter(new String[]{"name","singer","lyric"},new String[]{}))
                .withHighlightFields(highlightField)
                .withQuery(functionScoreQueryBuilder).build();
    }
}

c) 解决Spring Data ElasticSearch不支持Highlight的问题

通过自定义实现一个如下的JestSearchResultMapper,解决无法Highlight的问题

package org.leiws.esweb.repository;

//import 省略
public class HighLightJestSearchResultMapper extends DefaultJestResultsMapper {

    private EntityMapper entityMapper;
    private MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext;

    public HighLightJestSearchResultMapper() {
        this.entityMapper = new DefaultEntityMapper();
        this.mappingContext = new SimpleElasticsearchMappingContext();
    }

    public HighLightJestSearchResultMapper(MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext, EntityMapper entityMapper) {
        this.entityMapper = entityMapper;
        this.mappingContext = mappingContext;
    }

    public EntityMapper getEntityMapper() {
        return entityMapper;
    }

    public void setEntityMapper(EntityMapper entityMapper) {
        this.entityMapper = entityMapper;
    }

    @Override
    public <T> AggregatedPage<T> mapResults(SearchResult response, Class<T> clazz) {
        return mapResults(response, clazz, null);
    }

    @Override
    public <T> AggregatedPage<T> mapResults(SearchResult response, Class<T> clazz, List<AbstractAggregationBuilder> aggregations) {
        LinkedList<T> results = new LinkedList<>();
        for (SearchResult.Hit<JsonObject, Void> hit : response.getHits(JsonObject.class)) {
            if (hit != null) {
                T result = mapSource(hit.source, clazz);
                HighLightEntity highLightEntity = (HighLightEntity) result;
                highLightEntity.setHighlight(hit.highlight);
                results.add((T) highLightEntity);
            }
        }

        String scrollId = null;
        if (response instanceof ExtendedSearchResult) {
            scrollId = ((ExtendedSearchResult) response).getScrollId();
        }

        return new AggregatedPageImpl<>(results, response.getTotal(), response.getAggregations(), scrollId);
    }

    private  <T> T mapSource(JsonObject source, Class<T> clazz) {
        String sourceString = source.toString();
        T result = null;
        if (!StringUtils.isEmpty(sourceString)) {
            result = mapEntity(sourceString, clazz);
            setPersistentEntityId(result, source.get(JestResult.ES_METADATA_ID).getAsString(), clazz);
        } else {
            //TODO(Fields results) : Map Fields results
            //result = mapEntity(hit.getFields().values(), clazz);
        }
        return result;
    }

    private <T> T mapEntity(String source, Class<T> clazz) {
        if (isBlank(source)) {
            return null;
        }
        try {
            return entityMapper.mapToObject(source, clazz);
        } catch (IOException e) {
            throw new ElasticsearchException("failed to map source [ " + source + "] to class " + clazz.getSimpleName(), e);
        }
    }
    private <T> void setPersistentEntityId(Object entity, String id, Class<T> clazz) {

        ElasticsearchPersistentEntity<?> persistentEntity = mappingContext.getRequiredPersistentEntity(clazz);
        ElasticsearchPersistentProperty idProperty = persistentEntity.getIdProperty();

        // Only deal with text because ES generated Ids are strings !
        if (idProperty != null) {
            if (idProperty.getType().isAssignableFrom(String.class)) {
                persistentEntity.getPropertyAccessor(entity).setProperty(idProperty, id);
            }
        }
    }
}

上面类的大部分代码来源于:DefaultJestResultsMapper
重点修改部分为:

@Override
    public <T> AggregatedPage<T> mapResults(SearchResult response, Class<T> clazz, List<AbstractAggregationBuilder> aggregations) {
        LinkedList<T> results = new LinkedList<>();
        for (SearchResult.Hit<JsonObject, Void> hit : response.getHits(JsonObject.class)) {
            if (hit != null) {
                T result = mapSource(hit.source, clazz);
                HighLightEntity highLightEntity = (HighLightEntity) result;
                highLightEntity.setHighlight(hit.highlight);
                results.add((T) highLightEntity);
            }
        }

        String scrollId = null;
        if (response instanceof ExtendedSearchResult) {
            scrollId = ((ExtendedSearchResult) response).getScrollId();
        }

        return new AggregatedPageImpl<>(results, response.getTotal(), response.getAggregations(), scrollId);
    }

6.Controller

相对简单,如普通的Spring Controller

@Controller
@RequestMapping(value = "/search")
public class SearchController {

    @Autowired
    SongService songService;

    /**
     * Song list string.
     *
     * @param map the map
     * @return the string
     */
    @RequestMapping(method = RequestMethod.GET)
    public String songList(@RequestParam(value = "pNum") Integer pNum,
                           @RequestParam(value = "pSize", required = false) Integer pSize,
                           @RequestParam(value = "keywords") String keywords,ModelMap map){
       map.addAttribute("pageSong",songService.searchSong(pNum,pSize,keywords));
       return "songList";
    }
}

7.前端页面thymeleaf模版

存放目录为:resources/templates/songList.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
    <meta charset="UTF-8"/>
    <title>Title</title>
    <link rel='stylesheet' href='/webjars/bootstrap/css/bootstrap.min.css'>
    <script src="/webjars/jquery/jquery.min.js"></script>
    <script src="/webjars/bootstrap/js/bootstrap.min.js"></script>
</head>
<body>
<form action="/search" class="px-5 py-3" >
    <div class="input-group">
        <input name="keywords" type="text" class="form-control" placeholder="歌词搜索,请输入歌词内容" aria-label="歌词搜索,请输入歌词内容" aria-describedby="basic-addon2">
        <div class="input-group-append">
            <button class="btn btn-outline-secondary" type="button">搜索</button>
        </div>
        <input type="hidden" name="pNum" value="0"/>
    </div>
</form>
<div class="alert alert-light" role="alert">
    为您找到0个结果:
</div>
<ul class="list-group">
    <li th:each="song : ${pageSong.content}" class="list-group-item">
        <div class="row">
            <a th:href="${song.href}">
            <h4 scope="row" th:text="${song.name}" ></h4>
            </a>
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
            <h6 scope="row" th:text="${song.singer}" class="align-bottom" ></h6>
        </div>
        <!--
            <td><a th:href="@{/users/update/{userId}(userId=${user.id})}" th:text="${user.name}"></a></td>
        -->
        <div class="row">
            <span th:each="highlight : ${song.highlight}">
                <span th:each="word : ${highlight.value}">
                    <span th:utext="${word}"></span>...
                </span>
            </span>
        </div>
    </li>
</ul>

<nav aria-label="..." class="">
    <ul class="pagination pagination-sm justify-content-center py-5">
        <li class="page-item ">
            <a class="page-link" href="#">
                <span aria-hidden="true">&laquo;</span>
                <span class="sr-only">Previous</span>
            </a>
        </li>
        <li class="page-item"><a class="page-link" href="#">1</a></li>
        <li class="page-item"><a class="page-link" href="#">2</a></li>
        <li class="page-item"><a class="page-link" href="#">3</a></li>
        <li class="page-item">
            <a class="page-link" href="#">
            <span aria-hidden="true">&raquo;</span>
            <span class="sr-only">Next</span>
            </a>
        </li>
    </ul>
</nav>
</body>
</html>

8.阿里云ElasticSearch连接配置

在resources/application.properties中配置如下:

spring.data.jest.uri=http://1xx.xxx.xxx.xxx:8080
spring.data.jest.username=username
spring.data.jest.password=password
spring.data.jest.maxTotalConnection=50
spring.data.jest.defaultMaxTotalConnectionPerRoute=50
spring.data.jest.readTimeout=5000

9.其他

a) thymeleaf 热启动配置,便于测试

  1. 在resources/application.properties中配置如下:
spring.thymeleaf.cache=false
  1. 在pom.xml中增加:
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
<build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <fork>true</fork>
                </configuration>
            </plugin>
        </plugins>
    </build>

3.每次还是需要重新compile后,修改的thymeleaf模版代码才会自动生效,因为spring boot启动是以target目录为准的

b) 阿里云elasticsearch在esc上配置ngnix代理,以支持本机可以公网访问,便于开发

  1. 购买一台esc
  2. 在ecs上安装ngnix,并配置代理信息server 部分如下:
    server {
        listen       8080;
        #listen       [::]:80 default_server;
        server_name  {本机内网ip} {本机外网ip};
        #root         /usr/share/nginx/html;

        # Load configuration files for the default server block.
        #include /etc/nginx/default.d/*.conf;

        location / {
                        proxy_pass http://{elasticsearch 内网 ip}:9200;
        }
    }

10. 最后,查询效果:

image

===============================================================================================

Elastic Search是一个开源的,分布式,实时搜索和分析引擎。Spring Boot为Elasticsearch及Spring Data Elasticsearch提供的基于它的抽象提供了基本的配置。Spring Boot提供了一个用于聚集依赖的spring-boot-starter-data-elasticsearch 'StarterPOM'。

ElasticSearch作为搜索引擎,我们需要解决2大问题:

1,  如何将被搜索的数据在ES上创建反向索引
2,  Java代码如何与ES交互

其中第一个大问题又分为两个小问题

1.1,如何初始化已有的数据
1.2,如何同步增量数据

第二个大问题也有两种集成方式

2.1 Spring Data 9300端口集成
2.2 Restful API 9200端口集成

本篇先解决第二大问题。

第一种方式,利用RestAPI方式,也叫Jest方式:

示例代码:https://github.com/yejingtao/forblog/tree/master/demo-jest-elasticsearch

Pom.xml:

<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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 
 <modelVersion>4.0.0</modelVersion> 
 
 <groupId>yejingtao.demo.springcloud</groupId> 
 <artifactId>demo-jest-elasticsearch</artifactId> 
 <version>0.0.1-SNAPSHOT</version> 
 <packaging>jar</packaging> 
 
 <name>demo-jest-elasticsearch</name> 
 <url>http://maven.apache.org</url> 
 
 <properties> 
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 
 </properties> 
  
 <parent> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-parent</artifactId> 
    <version>1.5.6.RELEASE</version> 
  </parent> 
   
  <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>io.searchbox</groupId> 
      <artifactId>jest</artifactId> 
    </dependency> 
    <dependency> 
      <groupId>net.java.dev.jna</groupId> 
      <artifactId>jna</artifactId> 
    </dependency> 
  </dependencies> 
</project> 

Application.yml:

server: 
 port: 7081 
 
spring: 
 elasticsearch: 
  jest: 
   uris: 
   - http://192.168.226.133:9200 
   read-timeout: 5000 

注意这里是9200端口

主程序:最简单的Spring boot启动程序:

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

定义好ES中的实体类和对ES操作的接口:

public class Entity implements Serializable{ 
 
  private static final long serialVersionUID = -763638353551774166L; 
   
  public static final String INDEX_NAME = "index_entity"; 
   
  public static final String TYPE = "tstype"; 
 
  private Long id; 
   
  private String name; 
   
  public Entity() { 
    super(); 
  } 
   
  public Entity(Long id, String name) { 
    this.id = id; 
    this.name = name; 
  } 
 
  public Long getId() { 
    return id; 
  } 
 
  public void setId(Long id) { 
    this.id = id; 
  } 
 
  public String getName() { 
    return name; 
  } 
 
  public void setName(String name) { 
    this.name = name; 
  } 
   
   
} 
public interface CityESService { 
   
  void saveEntity(Entity entity); 
   
  void saveEntity(List<Entity> entityList); 
   
  List<Entity> searchEntity(String searchContent); 
} 

接口实现:

@Service 
public class CityESServiceImpl implements CityESService{ 
   
  private static final Logger LOGGER = LoggerFactory.getLogger(CityESServiceImpl.class); 
   
  @Autowired 
  private JestClient jestClient; 
   
  @Override 
  public void saveEntity(Entity entity) { 
    Index index = new Index.Builder(entity).index(Entity.INDEX_NAME).type(Entity.TYPE).build(); 
    try { 
      jestClient.execute(index); 
      LOGGER.info("ES 插入完成"); 
    } catch (IOException e) { 
      e.printStackTrace(); 
      LOGGER.error(e.getMessage()); 
    } 
  } 
   
   
  /** 
   * 批量保存内容到ES 
   */ 
  @Override 
  public void saveEntity(List<Entity> entityList) { 
    Bulk.Builder bulk = new Bulk.Builder(); 
    for(Entity entity : entityList) { 
      Index index = new Index.Builder(entity).index(Entity.INDEX_NAME).type(Entity.TYPE).build(); 
      bulk.addAction(index); 
    }     
    try { 
      jestClient.execute(bulk.build()); 
      LOGGER.info("ES 插入完成"); 
    } catch (IOException e) { 
      e.printStackTrace(); 
      LOGGER.error(e.getMessage()); 
    } 
  } 
   
  /** 
   * 在ES中搜索内容 
   */ 
  @Override 
  public List<Entity> searchEntity(String searchContent){ 
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); 
    //searchSourceBuilder.query(QueryBuilders.queryStringQuery(searchContent)); 
    //searchSourceBuilder.field("name"); 
    searchSourceBuilder.query(QueryBuilders.matchQuery("name",searchContent)); 
    Search search = new Search.Builder(searchSourceBuilder.toString()) 
        .addIndex(Entity.INDEX_NAME).addType(Entity.TYPE).build(); 
    try { 
      JestResult result = jestClient.execute(search); 
      return result.getSourceAsObjectList(Entity.class); 
    } catch (IOException e) { 
      LOGGER.error(e.getMessage()); 
      e.printStackTrace(); 
    } 
    return null;     
  } 
} 

这里插入数据的方式给了两种,一种是单次API直接插入,一种是利用ES的bulk批量插入。

做一个controller方面我们测试:

启动后在浏览器中请求http://localhost:7081/entityController/search?name=%E4%BA%BA%E6%89%8B%E4%BA%95

得到结果:

这里只返回了9条记录,而理论上ES默认的size是10,应该不是分页的问题,而是只能检索出9条匹配记录,用Kibana连上相同的搜索确认下:

这里用的是standard分词方式,将每个中文都作为了一个term,凡是包含“人”“手”“井”的都被搜索了出来,只是评分不同,如果想支持只能中文索引需要依赖ik插件

OK,RestFul方式对ElasticSearch的检索已经搞定了,更多的扩展可以慢慢研究下QueryBuilders里的源码和批注。

第二种方式,利用Spring Data客户端方式:

事先说明此方式有个弊端,让我掉了坑里好久才爬上来,Spring Data ElasticSearch必须与ElasticSearch版本相匹配,否则在对接时ES端会报版本不匹配错误,例如我ES是5.6.1版本,Spring boot是1.5.6版本,错误如下:

为解决这个问题我查找了一些资料,Spring Data与elasticsearch版本对应关系如下:

spring data elasticsearch

elasticsearch

3.0.0.RC2

5.5.0

3.0.0.M4

5.4.0

2.0.4.RELEASE

2.4.0

2.0.0.RELEASE

2.2.0

1.4.0.M1

1.7.3

1.3.0.RELEASE

1.5.2

1.2.0.RELEASE

1.4.4

1.1.0.RELEASE

1.3.2

1.0.0.RELEASE

1.1.1

而我用的Spring Boot 1.5.6版本对应的Spring Data ElasticSearch是2.1.6版本,不支持5.X的ES,所以报错。到本博文撰写为止,Spring Boot的RELEASE版本最新的是1.5.8,对应的Spring Data ElasticSearch是2.1.8,仍不支持5.X的ES,所以如果一定要使用Java客户端方式集成ES只能放弃Spring Boot直接使用Spring Data和Spring MVC,或者降低ES的版本使之与Spring boot匹配。

示例代码:https://github.com/yejingtao/forblog/tree/master/demo-data-elasticsearch

pom.xml依赖:

<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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 
 <modelVersion>4.0.0</modelVersion> 
 
 <groupId>yejingtao.demo.springcloud</groupId> 
 <artifactId>demo-data-elasticsearch</artifactId> 
 <version>0.0.1-SNAPSHOT</version> 
 <packaging>jar</packaging> 
 
 <name>demo-data-elasticsearch</name> 
 <url>http://maven.apache.org</url> 
 
 <properties> 
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 
 </properties> 
  
 <parent> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-parent</artifactId> 
    <version>1.5.8.RELEASE</version> 
  </parent> 
   
  <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> 
  </dependencies> 
</project> 

不再引用Jest。

application.yml:

server: 
 port: 7081 
 
spring: 
 data: 
  elasticsearch: 
   cluster-nodes: 192.168.226.133:9300 
   cluster-name: my-es 
   repositories: 
    enabled: true 

注意这里是9300端口

Controller、主程序、Service接口同Jest项目不变,不再罗列

实体类稍作变化,指定ES中的index和type:

@Document(indexName="index_entity", type="tstype") 

多一个Repository接口,无需实现类,spring data标准用法:

/** 
 * Entity ES操作类 
 * @author yejingtao 
 * 
 */ 
public interface EntityRepository extends ElasticsearchRepository<Entity,Long>{ 
 
} 

Service实现类与Jest的天壤之别了,从语法上可以看出更像是对数据库层的操作:

@Service 
public class CityESServiceImpl implements CityESService{ 
   
  private static final Logger LOGGER = LoggerFactory.getLogger(CityESServiceImpl.class); 
   
  int PAGE_SIZE = 15; //默认分页大小 
   
  int PAGE_NUMBER = 0; //默认当前分页 
   
  String SCORE_MODE_SUM = "sum"; //权重分求和模式 
   
  Float MIN_SCORE = 10.0F; //由于无相关性的分值默认为1, 设置权重分最小值为10 
   
  @Autowired 
  EntityRepository entityRepository; 
   
  /** 
   * 保存内容到ES 
   */ 
  @Override 
  public Long saveEntity(Entity entity) { 
    Entity entityResult = entityRepository.save(entity); 
    return entityResult.getId(); 
  } 
   
  /** 
   * 在ES中搜索内容 
   */ 
  @Override 
  public List<Entity> searchEntity(int pageNumber, int pageSize, String searchContent){ 
    if(pageSize==0) { 
      pageSize = PAGE_SIZE; 
    } 
    if(pageNumber<0) { 
      pageNumber = PAGE_NUMBER; 
    } 
     
    SearchQuery searchQuery = getEntitySearchQuery(pageNumber,pageSize,searchContent); 
     
    LOGGER.info("\n searchCity: searchContent [" + searchContent + "] \n DSL = \n "  
        + searchQuery.getQuery().toString()); 
 
     
    Page<Entity> cityPage = entityRepository.search(searchQuery); 
    return cityPage.getContent(); 
  } 
   
  /** 
   * 组装搜索Query对象 
   * @param pageNumber 
   * @param pageSize 
   * @param searchContent 
   * @return 
   */ 
  private SearchQuery getEntitySearchQuery(int pageNumber, int pageSize, String searchContent) { 
    FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery() 
        .add(QueryBuilders.matchPhraseQuery("name", searchContent), 
            ScoreFunctionBuilders.weightFactorFunction(1000)) 
        //.add(QueryBuilders.matchPhraseQuery("other", searchContent), 
            //ScoreFunctionBuilders.weightFactorFunction(1000)) 
        .scoreMode(SCORE_MODE_SUM).setMinScore(MIN_SCORE); 
    //设置分页,否则只能按照ES默认的分页给 
    Pageable pageable = new PageRequest(pageNumber, pageSize); 
    return new NativeSearchQueryBuilder().withPageable(pageable).withQuery(functionScoreQueryBuilder).build(); 
  } 
   
} 

测试方式同Jest。

这两种方式,从设计上来讲属于两种思路,Spring Data的思路就是将ElasticSearch当自家的数据仓库来管理,直接通过Java客户端代码操作ES;Jest的思路是将ElasticSearch当为独立的服务端,自己作为客户端用兼容性最强的RestFul格式来与之交互。
个人比较倾向于Jest方式,第一兼容性好,不需要考虑版本的问题。第二,从ElasticSearch本身的设计上来分析,9200是对外服务端口,9300是内部管理和集群通信端口,请求9200获取搜索服务更符合ES的设计初衷,不会影响集群内部的通信。

转自:https://yq.aliyun.com/articles/538160

https://blog.csdn.net/j080624/article/details/80953393

https://www.jb51.net/article/127390.htm

猜你喜欢

转载自blog.csdn.net/ZYC88888/article/details/81873038
今日推荐