易课寄在线购课系统开发笔记(十九)–完成搜索功能
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";
}
}