Design and Implementation of an Online Course Purchase System(19)

Complete the search function

Posted by Chen Xingxu on June 3, 2020

易课寄在线购课系统开发笔记(十九)–完成搜索功能

Solr 服务搭建

请参阅下面这篇笔记:

CentOS 6学习笔记(十)–CentOS6环境安装Solr

搜索工程搭建

要实现搜索功能,需要搭建 Solr 服务、搜索服务工程、搜索系统

搜索服务工程搭建

可以参考 易课寄在线购课系统开发笔记(七)–后台管理系统工程搭建分析 ecourses-bms 的创建过程。

  • ecourses-parent:父工程,打包方式 pom,管理 jar 包的版本号。
    • ecourses-common:通用的工具类及通用的 pojo。打包方式 jar
    • ecourses-search-web(war)
    • ecourses-search(聚合工程pom)
      • ecourses-search-interface(jar)
      • ecourses-search-service(war)

使用 SolrJ 管理索引库

使用 SolrJ 可以实现索引库的增删改查操作。

添加文档

第一步:把 SolrJ 的 jar 包添加到工程中。

第二步:创建一个 SolrServer,使用 HttpSolrServer 创建对象。

第三步:创建一个文档对象 SolrInputDocument 对象。

第四步:向文档中添加域。必须有 id 域,域的名称必须在 schema.xml 中定义。

第五步:把文档添加到索引库中。

第六步:提交。

@Test
public void addDocument() throws Exception {
    // 第一步:把solrJ的jar包添加到工程中。
    // 第二步:创建一个SolrServer,使用HttpSolrServer创建对象。
    SolrServer solrServer = new HttpSolrServer("http://192.168.25.155:8080/solr");
    // 第三步:创建一个文档对象SolrInputDocument对象。
    SolrInputDocument document = new SolrInputDocument();
    // 第四步:向文档中添加域。必须有id域,域的名称必须在schema.xml中定义。
    document.addField("id", "test");
    document.addField("item_title", "测试课程");
    document.addField("item_price", "199");
    // 第五步:把文档添加到索引库中。
    solrServer.add(document);
    // 第六步:提交。
    solrServer.commit();
}

删除文档

根据 id 删除

第一步:创建一个 SolrServer 对象。

第二步:调用 SolrServer 对象的根据 id 删除的方法。

第三步:提交。

@Test
public void deleteDocumentById() throws Exception {
    // 第一步:创建一个SolrServer对象。
    SolrServer solrServer = new HttpSolrServer("http://192.168.25.155:8080/solr");
    // 第二步:调用SolrServer对象的根据id删除的方法。
    solrServer.deleteById("1");
    // 第三步:提交。
    solrServer.commit();
}

根据查询删除

@Test
public void deleteDocumentByQuery() throws Exception {
    SolrServer solrServer = new HttpSolrServer("http://192.168.25.155:8080/solr");
    solrServer.deleteByQuery("title:change.me");
    solrServer.commit();
}

查询索引库

查询步骤: 第一步:创建一个 SolrServer 对象 第二步:创建一个 SolrQuery 对象。 第三步:向 SolrQuery 中添加查询条件、过滤条件等 第四步:执行查询,得到一个 Response 对象。 第五步:取查询结果。 第六步:遍历结果并打印。

简单查询

@Test
public void queryDocument() throws Exception {
    // 第一步:创建一个SolrServer对象
    SolrServer solrServer = new HttpSolrServer("http://192.168.25.155:8080/solr");
    // 第二步:创建一个SolrQuery对象。
    SolrQuery query = new SolrQuery();
    // 第三步:向SolrQuery中添加查询条件、过滤条件。。。
    query.setQuery("*:*");
    // 第四步:执行查询。得到一个Response对象。
    QueryResponse response = solrServer.query(query);
    // 第五步:取查询结果。
    SolrDocumentList solrDocumentList = response.getResults();
    System.out.println("查询结果的总记录数:" + solrDocumentList.getNumFound());
    // 第六步:遍历结果并打印。
    for (SolrDocument solrDocument : solrDocumentList) {
        System.out.println(solrDocument.get("id"));
        System.out.println(solrDocument.get("item_title"));
        System.out.println(solrDocument.get("item_price"));
    }
}

带高亮显示

@Test
public void queryDocumentWithHighLighting() throws Exception {
    // 第一步:创建一个SolrServer对象
    SolrServer solrServer = new HttpSolrServer("http://192.168.25.154:8080/solr");
    // 第二步:创建一个SolrQuery对象。
    SolrQuery query = new SolrQuery();
    //------------重点 start------------
    // 第三步:向SolrQuery中添加查询条件、过滤条件。。。
    query.setQuery("测试");
    //指定默认搜索域
    query.set("df", "item_keywords");
    //开启高亮显示
    query.setHighlight(true);
    //高亮显示的域
    query.addHighlightField("item_title");
    query.setHighlightSimplePre("<em>");
    query.setHighlightSimplePost("</em>");
    //------------重点 end------------
    // 第四步:执行查询。得到一个Response对象。
    QueryResponse response = solrServer.query(query);
    // 第五步:取查询结果。
    SolrDocumentList solrDocumentList = response.getResults();
    System.out.println("查询结果的总记录数:" + solrDocumentList.getNumFound());
    // 第六步:遍历结果并打印。
    for (SolrDocument solrDocument : solrDocumentList) {
        System.out.println(solrDocument.get("id"));
        //------------重点 start------------
        //取高亮显示
        Map<String, Map<String, List<String>>> highlighting = response.getHighlighting();
        List<String> list = highlighting.get(solrDocument.get("id")).get("item_title");
        String itemTitle = null;
        if (list != null && list.size() > 0) {
            itemTitle = list.get(0);
        } else {
            itemTitle = (String) solrDocument.get("item_title");
        }
        //------------重点 end------------
        System.out.println(itemTitle);
        System.out.println(solrDocument.get("item_price"));
    }
}

把课程数据导入到索引库中

Dao 层

SQL 语句

SELECT
	a.id,
	a.title,
	a.sell_point,
	a.price,
	a.image,
	b.`name` category_name
FROM
	`ecourses_item` a
LEFT JOIN ecourses_item_cat b ON a.cid = b.id
WHERE a.`status`=1

需要自己创建 Mapper 文件。

创建对应数据集的 pojo

package cn.ecourses.common.pojo;
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;
	public String getId() {
		return id;
	}
	public void setId(String id) {
		this.id = id;
	}
	public String getTitle() {
		return title;
	}
	public void setTitle(String title) {
		this.title = title;
	}
	public String getSell_point() {
		return sell_point;
	}
	public void setSell_point(String sell_point) {
		this.sell_point = sell_point;
	}
	public long getPrice() {
		return price;
	}
	public void setPrice(long price) {
		this.price = price;
	}
	public String getImage() {
		return image;
	}
	public void setImage(String image) {
		this.image = image;
	}
	public String getCategory_name() {
		return category_name;
	}
	public void setCategory_name(String category_name) {
		this.category_name = category_name;
	}
	
	public String[] getImages() {
		if (image != null && !"".equals(image)) {
			String[] strings = image.split(",");
			return strings;
		}
		return null;
	}

}

接口定义

package cn.ecourses.search.mapper;
public interface ItemMapper {
	List<SearchItem> getItemList();
	SearchItem getItemById(long itemId);
}

Mapper 映射文件

<?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.ecourses.search.mapper.ItemMapper" >
	<select id="getItemList" resultType="cn.ecourses.common.pojo.SearchItem">
		SELECT
			a.id,
			a.title,
			a.sell_point,
			a.price,
			a.image,
			b. NAME category_name
		FROM
			ecourses_item a
		LEFT JOIN ecourses_item_cat b ON a.cid = b.id
		WHERE
			a.`status` = 1
	</select>
	<select id="getItemById" parameterType="long" resultType="cn.ecourses.common.pojo.SearchItem">
		SELECT
			a.id,
			a.title,
			a.sell_point,
			a.price,
			a.image,
			b. NAME category_name
		FROM
			ecourses_item a
		LEFT JOIN ecourses_item_cat b ON a.cid = b.id
		WHERE
			a.`status` = 1
		AND a.id=#{itemid}
	</select>
</mapper>

Service层

功能分析

1、查询所有课程数据;

2、循环把课程数据添加到索引库。使用 SolrJ 实现;

3、返回成功。返回 ECoursesResult

参数:无

返回值:ECoursesResult

SolrJ 添加索引库

1、把 SolrJ 的 jar 包添加到工程;

2、创建一个 SolrServer 对象。创建一个和 Sorl 服务的连接。HttpSolrServer;

3、创建一个文档对象。SolrInputDocument;

4、向文档对象中添加域。必须有一个 id 域。而且文档中使用的域必须在 schema.xml 中定义;

5、把文档添加到索引库;

6、Commit。

@Test
public void addDocument() throws Exception {
    // 1、把solrJ的jar包添加到工程。
    // 2、创建一个SolrServer对象。创建一个和sorl服务的连接。HttpSolrServer。
    //如果不带Collection默认连接Collection1
    SolrServer solrServer = new HttpSolrServer("http://192.168.25.154:8080/solr");
    // 3、创建一个文档对象。SolrInputDocument。
    SolrInputDocument document = new SolrInputDocument();
    // 4、向文档对象中添加域。必须有一个id域。而且文档中使用的域必须在schema.xml中定义。
    document.addField("id", "test001");
    document.addField("item_title", "测试商品");
    // 5、把文档添加到索引库
    solrServer.add(document);
    // 6、Commit。
    solrServer.commit();
}

代码实现

package cn.ecourses.search.service.impl;
//索引库维护Service
@Service
public class SearchItemServiceImpl implements SearchItemService {

	@Autowired
	private ItemMapper itemMapper;
	@Autowired
	private SolrServer solrServer;
	
	@Override
	public ECoursesResult 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 ECoursesResult.ok();
		} catch (Exception e) {
			e.printStackTrace();
			return ECoursesResult.build(500, "数据导入时发生异常");
					
		}
	}
}

SolrServer 的配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
	xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
	http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd
	http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.2.xsd
	http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.2.xsd">

	<!-- 集群版solrJ -->
	<bean id="cloudSolrServer" class="org.apache.solr.client.solrj.impl.CloudSolrServer">
		<constructor-arg index="0" value="solr:2181,solr:2182,solr:2183"></constructor-arg>
		<property name="defaultCollection" value="collection2"></property>
	</bean>
</beans>

单机版 SolrJ

<bean id="httpSolrServer" class="org.apache.solr.client.solrj.impl.HttpSolrServer">
    <constructor-arg index="0" value="http://solr:8080/solr"/>
</bean>

发布服务

<dubbo:service interface="cn.ecourses.search.service.SearchItemService" ref="searchItemServiceImpl" timeout="600000"/>

表现层

后台管理工程中调用课程导入服务。

<dubbo:reference interface="cn.ecourses.search.service.SearchItemService" id="searchItemService" />

功能分析

请求的 url:/index/item/import

响应的结果:JSON 数据。可以使用 ECoursesResult

Controller

package cn.ecourses.controller;
//导入数据到索引库
@Controller
public class SearchItemController {
	
	@Autowired
	private SearchItemService searchItemService;

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

解决 Mapper 映射文件不存在异常

在 ecourses-search-service 的 pom 文件中需要添加资源配置。

<!-- 如果不添加此节点mybatis的mapper.xml文件都会被漏掉。 -->
<build>
<resources>
  <resource>
    <directory>src/main/java</directory>
    <includes>
      <include>**/*.properties</include>
      <include>**/*.xml</include>
    </includes>
    <filtering>false</filtering>
  </resource>
  <resource>
    <directory>src/main/resources</directory>
    <includes>
      <include>**/*.properties</include>
      <include>**/*.xml</include>
    </includes>
    <filtering>false</filtering>
  </resource>
</resources>
</build>

搜索功能实现

使用 sorlJ 查询索引库

//使用solrJ实现查询
@Test
public void queryDocument() throws Exception {
    //创建一个SolrServer对象
    SolrServer solrServer = new HttpSolrServer("http://192.168.25.155:8080/solr");
    //创建一个查询对象,可以参考solr的后台的查询功能设置条件
    SolrQuery query = new SolrQuery();
    //设置查询条件
//		query.setQuery("java");
    query.set("q","java");
    //设置分页条件
    query.setStart(1);
    query.setRows(2);
    //开启高亮
    query.setHighlight(true);
    query.addHighlightField("item_title");
    query.setHighlightSimplePre("<em>");
    query.setHighlightSimplePost("</em>");
    //设置默认搜索域
    query.set("df", "item_title");
    //执行查询,得到一个QueryResponse对象。
    QueryResponse queryResponse = solrServer.query(query);
    //取查询结果总记录数
    SolrDocumentList solrDocumentList = queryResponse.getResults();
    System.out.println("查询结果总记录数:" + solrDocumentList.getNumFound());
    //取查询结果
    Map<String, Map<String, List<String>>> highlighting = queryResponse.getHighlighting();
    for (SolrDocument solrDocument : solrDocumentList) {
        System.out.println(solrDocument.get("id"));
        //取高亮后的结果
        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");
        }
        System.out.println(title);
        System.out.println(solrDocument.get("item_sell_point"));
        System.out.println(solrDocument.get("item_price"));
        System.out.println(solrDocument.get("item_image"));
        System.out.println(solrDocument.get("item_category_name"));
    }

功能分析

把搜索结果相关的静态页面添加到工程中。

请求的url:/search

请求的方法:GET

参数:

keyword:查询条件

Page:页码。如果没有此参数,需要给默认值1。

返回的结果:

1)课程列表

2)总页数

3)总记录数

使用 jsp 展示,返回逻辑视图。

课程列表使用:SearchItem 表示。

需要把查询结果封装到一个 pojo 中:

1)课程列表List<SearchItem>

2)总页数。Int totalPages。总记录数/每页显示的记录数向上取整。把每页显示的记录是配置到属性文件中。

3)总记录数。Int recourdCount

package cn.ecourses.common.pojo;
public class SearchResult implements Serializable {

	private long recordCount;
	private int totalPages;
	private List<SearchItem> itemList;
	public long getRecordCount() {
		return recordCount;
	}
	public void setRecordCount(long recordCount) {
		this.recordCount = recordCount;
	}
	public int getTotalPages() {
		return totalPages;
	}
	public void setTotalPages(int totalPages) {
		this.totalPages = totalPages;
	}
	public List<SearchItem> getItemList() {
		return itemList;
	}
	public void setItemList(List<SearchItem> itemList) {
		this.itemList = itemList;
	}
}

Dao层

跟据查询条件查询索引库,返回对应的结果。

参数:SolrQuery

返回结果:SearchResult

package cn.ecourses.search.dao;
//搜索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;
	}
}

Service层

需要有一个接口一个实现类,需要对外发布服务。

参数:String keyWord

int page

int rows

返回值:SearchResult

业务逻辑:

1)根据参数创建一个查询条件对象。需要指定默认搜索域,还需要配置高亮显示。

2)调用 dao 查询。得到一个 SearchResult 对象

3)计算查询总页数,每页显示记录数就是 rows 参数。

package cn.ecourses.search.service.impl;
//搜索Service
@Service
public class SearchServiceImpl implements SearchService {
	
	@Autowired
	private SearchDao searchDao;

	@Override
	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.addHighlightField("item_title");
		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;
	}
}

发布服务

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

表现层

引用服务

在 ecourses-search-web 中添加接口依赖

<dependency>
  <groupId>cn.ecourses</groupId>
  <artifactId>ecourses-search-interface</artifactId>
  <version>1.0-SNAPSHOT</version>
</dependency>

springmvc.xml

<dubbo:reference interface="cn.ecourses.search.service.SearchService" id="searchService" />

Controller

请求的url:/search

请求的方法:GET

参数:

keyword:查询条件

Page:页码。如果没有此参数,需要给默认值1。

返回的结果:

使用 jsp 展示,返回逻辑视图。

package cn.ecourses.search.controller;
//搜索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 searchResult = searchService.search(keyword, page, SEARCH_RESULT_ROWS);
		//把结果传递给页面
		model.addAttribute("query", keyword);
		model.addAttribute("totalPages", searchResult.getTotalPages());
		model.addAttribute("page", page);
		model.addAttribute("recourdCount", searchResult.getRecordCount());
		model.addAttribute("itemList", searchResult.getItemList());
		
		//返回逻辑视图
		return "search";
	}
}