全文检索solr在商城案例中的使用

时间:2022-11-28 06:16:01

1、案例介绍

以前自己练手的时候做的ssm、ssh的案例(一些传统项目比如crm,oa,物流项目等)基本上都是在一个工程里面写表现层(Controller)、服务层(Service)、持久层(Dao)再到数据库,但是在并发太高的情况下就显得力不从心,所以在商城中会采用分布式架构。

先说3个概念

  1. 分布式:按照功能点把系统拆分,拆分成独立的功能。单独为某一个节点添加服务器。需要系统之间配合才能完成整个业务逻辑。叫做分布式。
  2. 分布式架构:多个子系统相互协作才能完成业务流程。系统之间需要进行通信。
  3. SOA:Service Oriented Architecture面向服务的架构。也就是把工程拆分成服务层、表现层两个工程。服务层中包含业务逻辑,只需要对外提供服务即可。表现层只需要处理和页面的交互,业务逻辑都是调用服务层的服务来实现。而电子商城项目就是基于soa架构的。
    商城架构图如下:
    全文检索solr在商城案例中的使用
    最后我这里实现了需要的工程如下。
    全文检索solr在商城案例中的使用

    在这里要介绍的是商品搜索功能。需要用到的工程是

e3-manager-web(war)(后台管理系统表现层)
e3-search(pom)(搜索服务层)
|--e3-search-interface(jar)
|--e3-search-service(war)
e3-search-web(war)(搜索系统表现层)

2、商品信息导入到solr索引库

需求
商品搜索是站内在索引库中搜索,所以索引库中必须有商品相关信息。那么就需要将数据库中需要的商品信息,以及商品分类等信息(根据业务需求)导入到索引库。后台管理页面如下:
全文检索solr在商城案例中的使用
当点击意见淡入商品数据到索引库按钮的时候,要求发送请求将商品信息导入到索引库。在介绍solr的时候说过多次,我们这里索引库中需要的商品信息如下:
1、商品Id
2、商品标题
3、商品卖点
4、商品价格
5、商品图片
6、分类名称

对应业务类型跟业务域如下:

<fieldType name="text_ik" class="solr.TextField">
<analyzer class="org.wltea.analyzer.lucene.IKAnalyzer"/>
</fieldType>

<field name="item_title" type="text_ik" indexed="true" stored="true"/>
<field name="item_sell_point" type="text_ik" indexed="true" stored="true"/>
<field name="item_price" type="long" indexed="true" stored="true"/>
<field name="item_image" type="string" indexed="false" stored="true" />
<field name="item_category_name" type="string" indexed="true" stored="true" />

<field name="item_keywords" type="text_ik" indexed="true" stored="false" multiValued="true"/>
<copyField source="item_title" dest="item_keywords"/>
<copyField source="item_sell_point" dest="item_keywords"/>
<copyField source="item_category_name" dest="item_keywords"/>

分析:
全文检索solr在商城案例中的使用
当点击按钮时候,发送请求,当返回状态码为200提示导入成功
请求参数:无
请求地址:/index/item/import
返回类型:E3Result
具体实现:发送请求后,在服务层需要先将商品信息从dao层查到,但是查到的数据要求跟索引库里面的一一对应,也就是说不仅仅只有商品本身信息,还有关联的商品类别表的类别名称category_name,所以需要自己再封装个pojo(名字叫SearchItem)属性跟域一一对应。因为涉及到了两个表一个是商品表(tb_item)和商品类别表(tb_item_catagory),所以不要能用逆向工程生成的mapper(可认为是Dao,mybatis逆向工程生成的代码主要针对单表)。
所以在服务层需要自己写mapper,查询所有SearchItem.
服务层查询到了所有SearchItem之后,遍历,将这些数据写入到索引库中。

实现
在工程e3-search-service中
1、Dao
映射文件ItemMapper.xml中:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.e3mall.search.mapper.ItemMapper" >

<select id="getItemList" resultType="cn.e3mall.common.pojo.SearchItem">
SELECT
a.id,
a.title,
a.sell_point,
a.price,
a.image,
b. NAME category_name
FROM
tb_item a
LEFT OUTER JOIN tb_item_cat b ON a.cid = b.id
WHERE
a.`status` = 1;<!--1表示商品未删除或者下架-->
</select>
</mapper>

封装的实体类pojo如下:写在e3-common中

public class SearchItem implements Serializable{

private String id;
private String title;
private String sell_point;
private Long price;
private String image;
private String category_name;
get、set方法

对应的ItemMapper如下:

public interface ItemMapper {
List<SearchItem> getItemList();
}

注:用的框架是Spring+SpringMVC+Mybatis

2、Service
该层需要查询所有数据(用到mapper),并且导入数据到索引库(用到HttpSolrServer)所以需要在容器中配置一个HttpSolrServer。

<!-- 单击版solrJ -->
<bean id="httpSolrServer" class="org.apache.solr.client.solrj.impl.HttpSolrServer">
<constructor-arg index="0" value="http://192.168.25.128:8080/solr/collection1"/>
</bean>
/*
* 索引库维护Service
*/

@Service
public class SearchItemServiceImpl implements SearchItemService{

@Autowired
private ItemMapper itemMapper;
@Autowired
private SolrServer solrServer;

public E3Result importAllItems() {
try {

//查询商品列表
List<SearchItem> itemList = itemMapper.getItemList();
//遍历商品列表
for (SearchItem searchItem : itemList) {
//创建文档对象
SolrInputDocument document = new SolrInputDocument();
//向文档对象中添加域
document.addField("id", searchItem.getId());
document.addField("item_title", searchItem.getTitle());
document.addField("item_sell_point", searchItem.getSell_point());
document.addField("item_price", searchItem.getPrice());
document.addField("item_image", searchItem.getImage());
document.addField("item_category_name", searchItem.getCategory_name());
//把文档对象写入索引库
solrServer.add(document);
}
//提交
solrServer.commit();
//返回导入成功
return E3Result.ok();
}catch (Exception e) {
e.printStackTrace();
return E3Result.build(500, "数据导入时发生异常");
}
}

}

注:E3Result是自定义返回类型,代码如下

public class E3Result implements Serializable{

// 响应业务状态
private Integer status;

// 响应消息
private String msg;

// 响应中的数据
private Object data;

public static E3Result build(Integer status, String msg, Object data) {
return new E3Result(status, msg, data);
}

public static E3Result ok(Object data) {
return new E3Result(data);
}

public static E3Result ok() {
return new E3Result(null);
}

public E3Result() {

}

public static E3Result build(Integer status, String msg) {
return new E3Result(status, msg, null);
}

public E3Result(Integer status, String msg, Object data) {
this.status = status;
this.msg = msg;
this.data = data;
}

public E3Result(Object data) {
this.status = 200;
this.msg = "OK";
this.data = data;
}
get、set方法
}

服务层写完了就只用发布服务让表现层去调用服务即可,这里用的是Dubbo来发布服务,使用Zookeeper作为注册中心。跟Web Service或者CXF类似。

<!-- 使用dubbo发布服务 -->
<!-- 提供方应用信息,用于计算依赖关系 -->
<dubbo:application name="e3-search" />
<dubbo:registry protocol="zookeeper"
address="192.168.25.128:2181" />

<!-- 用dubbo协议在20882端口暴露服务 -->
<dubbo:protocol name="dubbo" port="20882" /><!-- 一个服务对应一个端口 -->
<!-- 声明需要暴露的服务接口 -->
<dubbo:service interface="cn.e3mall.search.service.SearchItemService" ref="searchItemServiceImpl" timeout="600000"/>

3、表现层
e3-manager-web
首先要调用服务

配置文件中:

<!-- 引用dubbo服务 -->
<dubbo:application name="e3-search-web"/>
<dubbo:registry protocol="zookeeper" address="192.168.25.128:2181"/>
<dubbo:reference interface="cn.e3mall.search.service.SearchItemService" id="searchItemService"/>

Controller:

@Controller
public class SearchItemController {

@Autowired
private SearchItemService searchItemService;

@RequestMapping("/index/item/import")
@ResponseBody
public E3Result importItemList(){
E3Result result = searchItemService.importAllItems();
return result;
}
}

点击导入到索引库按钮后,提示导入成功,去solr后台管理界面去查询所有,看看记录是不是跟数据库中一样的,一样就表示成功了。

注:其实存在这么个问题,就是如果数据库中又添加了商品信息或者修改了商品信息,那么索引库跟数据库就不同步了,虽然可以再通过一键导入使得同步,但是商品信息频繁的变化那么就要频繁使用一键导入,数据量那么大,频繁导入的话效率会降低很多。而且如果某个商品下架,这个时候即使使用一键导入也不会同步了,因为导入的时候是id相同则覆盖存在没有的id的索引则添加,索引原先存在于索引库的商品信息并不会删除。存在这种情况的话,可以使用消息队列来解决,会在消息队列中(如ActiveMq)讲到。

3、商品搜索实现

需求
搜索框中输入关键字:如 苹果7 plus,显示搜索结果,要求标题中关键字高亮(红色)
全文检索solr在商城案例中的使用

全文检索solr在商城案例中的使用

服务层

在e3-search-service工程中
1、Dao
搜索dao层就写在e3-search-service模块
应该是封装了一个通用了方法:当传来查询对象SolrQuery时,返回查询结果。
根据页面e3-search-web中搜索页面需要参数:总记录数recordCount,
总页数totalPages,搜索的商品列表itemList。当前页码由页面传来,每页显示有开发人员固定好。所以查询开始位置就能知道,页数也可以有总记录数跟每页显示条数记录下来,
所以需要个pojo来封装这些信息.写在e3-common中。

/*
* 商品搜索Dao
*/

@Repository
public class SearchDao {

@Autowired
private SolrServer solrServer;
/*
* 根据查询条件查询索引
*/

public SearchResult search(SolrQuery query) throws Exception{
//根据query查询索引库
QueryResponse queryResponse = solrServer.query(query);
//取查询结果
SolrDocumentList solrDocumentList = queryResponse.getResults();
//取查询结果总记录数
long numFound = solrDocumentList.getNumFound();
SearchResult result = new SearchResult();
result.setRecordCount(numFound);
//取商品列表,需要取高亮显示
Map<String, Map<String, List<String>>> highlighting = queryResponse.getHighlighting();


List<SearchItem> itemList = new ArrayList<>();
for (SolrDocument solrDocument : solrDocumentList) {
SearchItem item = new SearchItem();
item.setId((String)solrDocument.get("id"));
item.setCategory_name((String)solrDocument.get("item_category_name"));
item.setImage((String)solrDocument.get("item_image"));
item.setPrice((long)solrDocument.get("item_price"));
item.setSell_point((String)solrDocument.get("item_sell_point"));
//取高亮
List<String> list = highlighting.get(solrDocument.get("id")).get("item_title");
String title = "";
if(list!=null&&list.size()>0){
title = list.get(0);
}else{
title = (String)solrDocument.get("item_title");
}
item.setTitle(title);
//添加到商品列表
itemList.add(item);
}
result.setItemList(itemList);
//返回结果
return result;
}

}

2、Service
从表现层传来搜索关键字,当前页,每页显示条数,创建搜索对象SolrQuery,设置好查询条件,调用上面写的dao的search方法获得查询结果返回。

/*
* 商品搜索service
*/

@Service
public class SearchServiceImpl implements SearchService{

@Autowired
private SearchDao searchDao;

public SearchResult search(String keyword, int page, int rows) throws Exception {
//创建一个SolrQuery对象
SolrQuery query = new SolrQuery();
//设置查询条件
query.setQuery(keyword);
//设置分页条件
if(page <= 0){
page = 1;
}
query.setStart((page-1)*rows);
query.setRows(rows);
//设置默认搜索域
query.set("df", "item_title");
//开启高亮显示,显示颜色为红色
query.setHighlight(true);
query.setHighlightSimplePre("<em style='color:red'>");
query.setHighlightSimplePost("</em>");
//调用Dao执行查询
SearchResult searchResult = searchDao.search(query);

//计算总页数
long recordCount = searchResult.getRecordCount();
int totalPage = (int) (recordCount/rows);
if(recordCount%rows > 0) totalPage++;
//返回结果
searchResult.setTotalPages(totalPage);
return searchResult;
}

}

注:计算页数可以用:(总记录数+每页显示条数-1)/每页显示条数。
然后要发布服务。

<dubbo:service interface="cn.e3mall.search.service.SearchService" ref="searchServiceImpl" timeout="600000"/>

表现层
在e3-search-web中
调用上面服务层发布的服务,接收页面请求参数,调用服务层方法。根据页面需要(当前页,每页显示条数,总记录数,总页数,搜索到的商品信息列表)对返回结果进行处理,绑定到request域,跳转的逻辑视图。

/*
* 商品搜索Controller
*/

@Controller
public class SearchController {

@Autowired
private SearchService searchService;
@Value("${SEARCH_RESULT_ROWS}")
private Integer SEARCH_RESULT_ROWS;
@RequestMapping("/search")
public String searchItemList(String keyword,
@RequestParam(defaultValue="1") Integer page,Model model) throws Exception{
keyword = new String(keyword.getBytes("iso-8859-1"), "utf-8");
//查询商品列表
SearchResult result = searchService.search(keyword, page, SEARCH_RESULT_ROWS);
//把结果传递给页面
model.addAttribute("query", keyword);
model.addAttribute("totalPages", result.getTotalPages());
model.addAttribute("page", page);
model.addAttribute("recordCount", result.getRecordCount());
model.addAttribute("itemList", result.getItemList());
return "search";
}
}

效果如下:
全文检索solr在商城案例中的使用

注:如果出现了下面的这种绑定异常
全文检索solr在商城案例中的使用
解决办法:

<!-- 不添加这个那么所有的mapper映射文件都会漏掉 -->
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
<!-- src/main/resources也作为映射文件存放位置 -->
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>