ElasticSeach 集成 springboot

声明是ElasticSearch?

Elasticsearch 是基于 Lucene 的 Restful 风格的分布式实时全文搜索引擎,每个字段都被索引并可被搜索,可以快速存储、搜索、分析海量的数据。

全文检索是指对每一个词建立一个索引,指明该词在文章中出现的次数和位置。当查询时,根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。

Solr的架构不适合实时搜索的应用。

ElasticSearch的基本知识点

倒排索引

倒排索引是一种将文档中的每个单词与其出现的文档进行关联的数据结构,以便快速地进行搜索。传统的索引一般是按照文档的顺序建立的,而倒排索引则是按照单词的顺序建立的。
他有单词的ID,单词,文档频率,倒排列表,倒排列表是文档Id,出现次数,和出现的位置
请添加图片描述

Elasticsearch 的基本概念:

索引(index) 是一组具有共同特性的文档集合,类似数据库的表(table)

文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式

字段(Field),就是JSON文档中的字段,类似数据库中的列(Column)

(5)shard 分片:单台机器无法存储大量数据,es可以将一个索引中的数据切分为多个shard,分布在多台服务器上存储。有了shard就可以横向扩展,存储更多数据,让搜索和分析等操作分布到多台服务器上去执行,提升吞吐量和性能。

(6)replica 副本:任何服务器随时可能故障或宕机,此时 shard 可能会丢失,通过创建 replica 副本,可以在 shard 故障时提供备用服务,保证数据不丢失,另外 replica 还可以提升搜索操作的吞吐量。

shard 分片数量在建立索引时设置,设置后不能修改,默认5个;replica 副本数量默认1个,可随时修改数量;

ElasticSearch连接方式

Elasticsearch有两种连接方式: transport 、 rest 。 transport 通过TCP方式访问ES(只支持java), rest 方式通过http API 访问ES(没有语言限制)。
但是,通过官方文档可以得知,现在存在至少三种Java客户端。

Transport Client
Java High Level REST Client
Java Low Level Rest Client

造成这种混乱的原因是:
长久以来,ES并没有官方的Java客户端,并且Java自身是可以简单支持ES的API的,于是就先做成了
TransportClient 。但是 TransportClient 的缺点是显而易见的,它没有使用RESTful风格的接口,而是二进制的方式传输数据。
之后ES官方推出了 Java Low Level REST Client ,它支持RESTful,用起来也不错。但是缺点也很明显,因为 TransportClient 的使用者把代码迁移到 Low Level REST Client 的工作量比较大。官方文档专门为迁移代码出了一堆文档来提供参考。
现在ES官方推出 Java High Level REST Client ,它是基于 Java Low Level REST Client 的封装,并且API接收参数和返回值和 TransportClient 是一样的,使得代码迁移变得容易并且支持了RESTful的风格,兼容了这两种客户端的优点。当然缺点是存在的,就是版本的问题。ES的小版本更新
非常频繁,在最理想的情况下,客户端的版本要和ES的版本一致(至少主版本号一致),次版本号不一致的话,基本操作也许可以,但是新API就不支持了。

1、linux环境准备

(1)搭建es集群,开启集群中的三个节点(服务器)

分别进入三个集群的目录,es1,es2,es3,开启节点,搭建集群

bin/elasticsearch

(2)开启elasticSearch-head插件,查看es的运行状态以及数据,它位于集群es的plunings插件中。

在elasticsearch-head目录下执行命令, 运行head插件

 npm run start

(3)Kibana是一个软件,不是插件。它位于和ElasticSearch同级目录。

Kibana 是一款开源的数据分析和可视化平台,它是 Elastic Stack 成员之一,设计用于和Elasticsearch 协作。您可以使用 Kibana 对 Elasticsearch 索引中的数据进行搜索、查看、交互操作。
可以很方便的利用图表、表格及地图对数据进行多元化的分析和呈现。
在kibana目录下执行命令,开启kibana

bin/kibana --allow-root

(4)IK Analysis中文分词器

IK Analysis插件将Lucene IK分析器集成到elasticsearch中,支持自定义词典
ik分词器是每一个集群都需要有的,所有每一个es,如es1,es2,es3目录下的plunings目录下都有一个ik目录。
分词器会在节点开启的时候,自动开启,无需自动配置。

Analyzer分词配置解释: ik_smart:粗粒度分词,比如中华人民共和国国歌,会拆分为中华人民共和国,国歌;
ik_max_word:细粒度分词,比如中华人民共和国国歌,会拆分为中华人民共和国,中华人民,
中华,华人,人民共和国,人民,人,民,共和国,共和,和,国国,国歌,会穷尽各种可能的组 合。

(5)mysql在linux环境下,有的时候我们搜索引擎查询的数据可能来源于数据库,也就是说搜索引擎需要从数据库中导入数据到Lucenne库中。

因此我们需要在linux环境下开启我们所需要的数据库。
linux环境下安装mysql8.0.30
查看mysql是否被启动

ps -ef|grep mysql

若没有则启动mysql服务

systemctl start mysql

(6)Elasticsearch导入MySQL数据

Logstash 是开源的服务器端数据处理管道,能够同时从多个来源采集数据,转换数据,然后将数据
发送到您最喜欢的 “存储库” 中。(我们的存储库当然是 Elasticsearch。)
具体操作见文档。。。
因为在这一part我用到了rpc框架,所有我还需要开启zookeeper,也用到了redis缓存,所以我还需要开启zookeeper和redis,redis我设置了开机自启,所有我这里只需要手动开启zookeeper。

bin/zkServer.sh start

搜索引擎走的库 Lucene库

搜索引擎走的一般都是Lucene库。Lucene是一个开源的全文搜索引擎库,可以在Java应用程序中嵌入全文搜索功能。Elasticsearch就是基于Lucene库开发的,它在Lucene的基础上增加了分布式架构、集群管理、数据可靠性等功能,使得它可以处理PB级别的数据,并提供高效的实时搜索和分析能力。

ElasticSearch集成springboot

(1)导入依赖

父项目导入依赖的版本号,声明依赖

<properties>
<!-- elasticsearch 依赖 -->
    <elasticsearch.version>7.5.0</elasticsearch.version>
 </properties>
<dependencyManagement>
<dependencies>
<!-- elasticsearch 服务依赖 -->
      <dependency>
        <groupId>org.elasticsearch</groupId>
        <artifactId>elasticsearch</artifactId>
        <version>${elasticsearch.version}</version>
      </dependency>
      <!-- rest-client 客户端依赖 -->
      <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch-rest-client</artifactId>
        <version>${elasticsearch.version}</version>
      </dependency>
      <!-- rest-high-level-client 客户端依赖 -->
      <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch-rest-high-level-client</artifactId>
        <version>${elasticsearch.version}</version>
      </dependency>
   </dependencies>
 </denpendencyManagement>

因为ElatiscSearch在前台或者后台系统中都有应用,所有我们可以将它提取出来封装成一个服务,让其他系统充当消费者的角色在需要的时候调用服务。
rpc项目中导入依赖

<!-- elasticsearch 服务依赖 -->
        <dependency>
            <groupId>org.elasticsearch</groupId>
            <artifactId>elasticsearch</artifactId>
        </dependency>
        <!-- rest-client 客户端依赖 -->
        <dependency>
            <groupId>org.elasticsearch.client</groupId>
            <artifactId>elasticsearch-rest-client</artifactId>
        </dependency>
        <!-- rest-high-level-client 客户端依赖 -->
        <dependency>
            <groupId>org.elasticsearch.client</groupId>
            <artifactId>elasticsearch-rest-high-level-client</artifactId>
        </dependency>

配置yml文件

我们需要告诉Es.config我们es集群的地址,端口号,让EslatiscSearch的配置类给我们创建客户端。
application-dec.yml

# Elasticsearch
elasticsearch:
  address: 192.168.186.128:9201, 192.168.186.128:9202, 192.168.186.128:9203

EsConfig java配置类

构建HttpHost对象,创建RestHighLevelClient 客户端,让es集群连接到客户端

@Configuration
public class EsConfig {
	//ES服务器地址
	@Value("${elasticsearch.address}")
	private String[] address;
	//ES服务器连接方式
	private static final String SCHEME = "http";

	/**
	 * 根据服务器地址构建HttpHost对象
	 * @param s
	 * @return
	 */
	@Bean
	public HttpHost builderHttpHost(String s){
		String[] address = s.split(":");
		if (2!=address.length){
			return null;
		}
		String host = address[0];
		Integer port = Integer.valueOf(address[1]);
		return new HttpHost(host,port,SCHEME);
	}

	/**
	 * 创建RestClientBuilder对象
	 * @return
	 */
	@Bean
	public RestClientBuilder restClientBuilder(){
		HttpHost[] hosts = Arrays.stream(address)
				.map(this::builderHttpHost)
				.filter(Objects::nonNull)
				.toArray(HttpHost[]::new);
		return RestClient.builder(hosts);
	}

	/**
	 * 创建RestHighLevelClient对象
	 * @param restClientBuilder
	 * @return
	 */
	@Bean
	public RestHighLevelClient restHighLevelClient(@Autowired RestClientBuilder restClientBuilder){
		return new RestHighLevelClient(restClientBuilder);
	}

}

解释:
1、只要是配置类都需要加上@Configuration注解
2、配置ElasticSearch的节点,我们首先需要获取结点的地址和端口号,也就是获取HttpPost对象,通过传进来的地址获取HtppPost对象

HttpHost对象是Apache HttpComponents库提供的一种数据类型,用于表示一个HTTP主机地址,包括主机名、端口号和协议类型等信息。
在Elasticsearch的Java客户端中,HttpHost对象常用于配置Elasticsearch服务器的主机地址和端口号,创建连接到Elasticsearch服务器的RestClientBuilder和RestHighLevelClient对象等。

在yml文件中我们可以进行配置,然后再Config类中取出来就好了。从配置文件yml中取数据用@Value("${elasticsearch.address}"),赋值给数组。
已知String[] address={“192.168.186.128:9201”,“192.168.186.128:9202”,“192.168.186.128:9201”};
创建HttpPost对象的函数为new HttpHost(host,port,SCHEME)
获取host,post可以获取address的每一个元素然后对他进行分隔,得到host和post。SCHEME是协议。

SCHEME常量用于构建HttpHost对象时指定HTTP协议的类型,即"http"或"https"。定义的是一个私有的静态常量,因此它只能被本类中的其他方法调用,而且它不需要基于任何实例化对象而存在,不能被修改。

@Bean
public HostPost bulderHostPost(String s){
	String[] address=s.split(":");
	if(2!=address.length()){
		return null;
	}
	String host = address[0];
	int post = Integer.valueOf(address[1]);
	return new HttpPost(host,post,Scheme);
}

用@Bean是要把HostPost对象放入spring容器中进行管理。

RestClient是Elasticsearch官方提供的RESTful风格的Java客户端,封装了对Elasticsearch服务器的REST API的访问。它提供了更高层次的API,对JSON数据格式和HTTP请求和响应进行了更好的封装和管理。RestClient同时又支持低级别的查询和索引操作。RestClient分为Low Level REST Client和High Level REST Client两种,Low Level REST Client提供基础的 REST API 操作,而High Level REST Client提供了更加高级的 API 接口以及更强的可扩展性。
接下来就是HostPost创建连接到ElasticSearch服务器的客户端了。
创建客户端直接调用RestClient.builder(HttpPost)
通过传进来的所有地址和端口号,获取一个RestClient客户端

利用java8新特性,我们可以将传过来的数组进行改造,返回一个HttpPost类型的数组。

@Bean
	public RestClientBuilder restClientBuilder(){
		HttpHost[] hosts = Arrays.stream(address)
				.map(this::builderHttpHost)
				.filter(Objects::nonNull)
				.toArray(HttpHost[]::new);
		return RestClient.builder(hosts);
	}

创建RestHighLevelClient对象

/**
	 * @param restClientBuilder
	 * @return
	 */
	@Bean
	public RestHighLevelClient restHighLevelClient(@Autowired RestClientBuilder restClientBuilder){
		return new RestHighLevelClient(restClientBuilder);
	}

创建搜索页面的商品购物车实体类

因为把搜索引擎抽取出来当做服务了,因此需要用到Dubbo。
首先我们要清楚搜索引擎的入参和出参是什么。当我们输入关键字,点击搜索按钮的时候,出来的一个个的框框,这个框框里面有分页,还有List集合,我们把他砍成一个对象,结合在一起,我们需要把List集合中的对象抽取出来写一个实体类,还需要定义一个相关的分页对象,返回一个分页对象到前端,渲染页面。因为要在网络中传输,所以这个对象要实现序列化。
》实体类,List集合中的类

public class GoodsVo implements Serializable {
	private static final long serialVersionUID = -1905915184535584387L;
	private Integer goodsId;
	private String goodsName;
	private String goodsNameHl;
	private BigDecimal marketPrice;
	private String originalImg;

	public GoodsVo() {
	}

	public GoodsVo(Integer goodsId, String goodsName, String goodsNameHl, BigDecimal marketPrice, String originalImg) {
		this.goodsId = goodsId;
		this.goodsName = goodsName;
		this.goodsNameHl = goodsNameHl;
		this.marketPrice = marketPrice;
		this.originalImg = originalImg;
	}

	public Integer getGoodsId() {
		return goodsId;
	}

	public void setGoodsId(Integer goodsId) {
		this.goodsId = goodsId;
	}

	public String getGoodsName() {
		return goodsName;
	}

	public void setGoodsName(String goodsName) {
		this.goodsName = goodsName;
	}

	public String getGoodsNameHl() {
		return goodsNameHl;
	}

	public void setGoodsNameHl(String goodsNameHl) {
		this.goodsNameHl = goodsNameHl;
	}

	public BigDecimal getMarketPrice() {
		return marketPrice;
	}

	public void setMarketPrice(BigDecimal marketPrice) {
		this.marketPrice = marketPrice;
	}

	public String getOriginalImg() {
		return originalImg;
	}

	public void setOriginalImg(String originalImg) {
		this.originalImg = originalImg;
	}

	@Override
	public String toString() {
		return "GoodsVo{" +
				"goodsId=" + goodsId +
				", goodsName='" + goodsName + '\'' +
				", goodsNameHl='" + goodsNameHl + '\'' +
				", marketPrice=" + marketPrice +
				", originalImg='" + originalImg + '\'' +
				'}';
	}
}

》分页对象中里面有页数,总记录条数等等,还有一个List<T> result来存储分页查询出来的对象,这个之前我们在写商品列表的时候实现过,这里就不赘述了。可见他是经常用到,所以放在了common系统中。

为什么有了PageInfo ,我们还要自定义一个分页类呢?

虽然PageInfo是MyBatis提供的一个方便的分页对象,但在某些情况下,我们可能需要自定义分页对象来满足业务需求。

首先,PageInfo只提供了基本的分页信息,例如当前页、每页记录数、总记录数等。如果我们需要更多的分页信息,例如总页数、是否有上一页/下一页等,就需要自定义分页对象,添加这些额外的属性。

其次,PageInfo只提供了对单表的简单分页支持,如果我们需要进行复杂的分页查询,例如多表关联查询、嵌套查询等,就需要自定义分页对象,并添加相应的查询条件和排序规则。

最后,自定义分页对象可以根据具体的业务需求来设计,可以更加灵活地控制分页逻辑和数据展示方式。例如,我们可以自定义一个VO对象,将需要展示的字段存放在其中,然后在分页查询时返回该对象列表,从而避免返回整个实体对象,减少网络传输量和内存占用。

public class ShopPageInfo<T> implements Serializable {
    // 当前页
    private int currentPage;
    // 每页显示条数
    private int pageSize;
    // 总页数
    private int total;
    // 总记录数
    private int count;
    // 上一页
    private int prePage;
    // 下一页
    private int nextPage;
    // 是否有上一页
    private boolean hasPre;
    // 是否有下一页
    private boolean hasNext;
    // 返回结果
    private List<T> result;

    // 构造函数1
    public ShopPageInfo() {
        super();
    }

    // 构造函数2
    public ShopPageInfo(int currentPage, int pageSize) {
        super();
        this.currentPage = (currentPage < 1) ? 1 : currentPage;
        this.pageSize = pageSize;
        // 是否有上一页
        this.hasPre = (currentPage == 1) ? false : true;
        // 是否有下一页
        this.hasNext = (currentPage == total) ? false : true;
        // 上一页
        if (hasPre) {
            this.prePage = (currentPage - 1);
        }
        // 下一页
        if (hasNext) {
            this.nextPage = currentPage + 1;
        }

    }

    // 构造函数3
    public ShopPageInfo(int currentPage, int pageSize, int count) {
        super();
        this.currentPage = (currentPage < 1) ? 1 : currentPage;
        this.pageSize = pageSize;
        this.count = count;
        // 计算总页数
        if (count == 0) {
            this.total = 0;
        } else {
            this.total = (count % pageSize == 0) ? (count / pageSize) : (count / pageSize + 1);
        }
        // 是否有上一页
        this.hasPre = (currentPage == 1) ? false : true;
        // 是否有下一页
        this.hasNext = (currentPage == total) ? false : true;
        // 上一页
        if (hasPre) {
            this.prePage = (currentPage - 1);
        }
        // 下一页
        if (hasNext) {
            this.nextPage = currentPage + 1;
        }
    }

    public int getCurrentPage() {
        return currentPage;
    }

    public void setCurrentPage(int currentPage) {
        this.currentPage = currentPage;
    }

    public int getPageSize() {
        return pageSize;
    }

    public void setPageSize(int pageSize) {
        this.pageSize = pageSize;
    }

    public int getTotal() {
        return total;
    }

    public void setTotal(int total) {
        this.total = total;
    }

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }

    public int getPrePage() {
        return prePage;
    }

    public void setPrePage(int prePage) {
        this.prePage = prePage;
    }

    public int getNextPage() {
        return nextPage;
    }

    public void setNextPage(int nextPage) {
        this.nextPage = nextPage;
    }

    public boolean isHasPre() {
        return hasPre;
    }

    public void setHasPre(boolean hasPre) {
        this.hasPre = hasPre;
    }

    public boolean isHasNext() {
        return hasNext;
    }

    public void setHasNext(boolean hasNext) {
        this.hasNext = hasNext;
    }

    public List<T> getResult() {
        return result;
    }

    public void setResult(List<T> result) {
        this.result = result;
    }
}

搜索引擎的ServiceImpl返回一个ShopInfo

在进行搜索的时候,我需要传进去一个关键字,还有分页查询的关键字眼pageNum,pageSize,返回的是一个ShopInfo
Hits:
在这里插入图片描述

ServerImp

@Service(interfaceClass = SearchService.class)
@Component
public class SearchServiceImpl implements com.wll.shoprpc.service.SearchService {

	@Resource
	private RestHighLevelClient client;

	/**
	 * 搜索
	 * @param searchStr
	 * @param pageNum
	 * @param pageSize
	 * @return
	 */
	@Override
	public ShopPageInfo<GoodsVo> doSearch(String searchStr, Integer pageNum, Integer pageSize) {
		//构建分页对象
		ShopPageInfo<GoodsVo> shopPageInfo;
		try {
			//指定索引库
			SearchRequest searchRequest = new SearchRequest("shop");
			//构建查询对象
			SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
			//设置分页条件,从哪一页开始查,查多少页
			searchSourceBuilder.from((pageNum-1)*pageSize).size(pageSize);
			//构建高亮对象
			HighlightBuilder highlightBuilder = new HighlightBuilder();
			//设置高亮字段及高亮样式
			highlightBuilder.field("goodsName")
					.preTags("<span style='color:red'>")
					.postTags("</span>");
			searchSourceBuilder.highlighter(highlightBuilder);
			//添加查询条件
			searchSourceBuilder.query(QueryBuilders.multiMatchQuery(searchStr,"goodsName"));
			searchRequest.source(searchSourceBuilder);
			//客户端执行请求,实时搜索
			List<GoodsVo> list = new ArrayList<>();
			SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
			//总条数
			Long total = response.getHits().getTotalHits().value;
			if (0>total){
				return null;
			}
			
			SearchHit[] hits = response.getHits().getHits();
			
			for (SearchHit hit : hits) {
				Integer goodsId = Integer.valueOf((Integer) hit.getSourceAsMap().get("goodsId"));
				String goodsName = String.valueOf(hit.getSourceAsMap().get("goodsName"));
				String goodsNameHl = String.valueOf(hit.getHighlightFields().get("goodsName").fragments()[0]);
				BigDecimal marketPrice = new BigDecimal(String.valueOf(hit.getSourceAsMap().get("marketPrice")));
				String originalImg = String.valueOf(hit.getSourceAsMap().get("originalImg"));
				GoodsVo goodsVo = new GoodsVo(goodsId,goodsName,goodsNameHl,marketPrice,originalImg);
				list.add(goodsVo);
			}
			shopPageInfo = new ShopPageInfo<GoodsVo>(pageNum,pageSize,total.intValue());
			shopPageInfo.setResult(list);
			return shopPageInfo;
			//处理数据
		} catch (IOException e) {
			e.printStackTrace();
		}
		return null;
	}
这是一个搜索商品的方法,可以根据搜索关键字进行商品搜索。该方法使用Elasticsearch进行搜索,根据搜索结果返回一个ShopPageInfo对象,包含符合条件的商品列表和分页信息。

参数说明:

- searchStr: 搜索关键字
- pageNum: 当前页码
- pageSize: 每页显示条目数
-

方法实现:

该方法首先构造了一个SearchRequest对象,并指定搜索的索引库为 “shop”,然后构建一个SearchSourceBuilder对象,用于设置搜索条件。通过设置from()和size()方法实现分页功能,并使用highlighter()方法设置高亮字段及高亮样式。接着,使用multiMatchQuery()方法构建查询条件,指定需要匹配的字段和搜索关键字,将查询条件添加到SearchSourceBuilder对象中。

然后将SearchSourceBuilder对象设置到SearchRequest对象中,并通过Elasticsearch的Java客户端的search()方法执行搜索请求,并获取响应结果SearchResponse。从SearchResponse中获取到搜索结果的总条数total和匹配了查询条件的商品列表hits。遍历hits列表,从中获取商品信息,构造GoodsVo对象并添加到list中。

最后,构造ShopPageInfo对象,将list设置为结果列表,返回ShopPageInfo对象。

如果发生异常,返回null。

点击搜索按钮,传递关键字,页数,页码到后台,先是一个Controller跳转页面,然后是将首页传进来的关键字发送到搜索页面,搜索页面根据首页传进来的关键字发送ajax请求,然后lk分词器把关键字分成词项,搜索引擎到倒排列表中进行搜索,找到单词对应的文档,然后对文档的内容进行分割,一一把拿出来的属性赋值给我们定义的商品对象,将商品对象放到集合里面,然后再将集合放到shopInfo分页对象里面,返回到搜索页面渲染。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值