(十二)Spring-Boot-Solr 踩坑记!

Spring Boot整合Solr

:tada: :tada: :tada: 这里有丰富的 Spring 框架学习案例

仓库地址:spring-learn
欢迎star、fork,给作者一些鼓励

写在前面

在之前我就写过一篇文档:Solr及Spring-Data-Solr入门学习,这篇文章其实来自于我学习黑马项目时写的笔记,其中用的版本都比较老。而最近这两天一直在研究Solr,发现还是老版本好用(坑

而今天既然学spring-boot,发现万能的spring-boot自己又对spring-data-solr进行了封装简化:spring-boot-starter-data-solr,今天就来学习一下。

之前我使用SSM项目写了一个ssm-solr的项目,如果你想看SSM项目中如何整合Solr,可以参考这个项目:ssm-redis-solr

欢迎star, fork

安装

如果你阅读过我的这篇文章:Solr及Spring-Data-Solr入门学习。会发现其中是将下载的solr source再提取出来放到tomcat容器中,过程比较复杂,也遇到很多坑,我自己再尝试将solr放到tomcat容器中也遇到很多很多的问题。Solr官网提供的源码其实已经将Solr放在了Jetty容器中,我们只需要通过命令运行Jetty容器也依然可以访问Solr。

So,我这里推荐使用Solr自带的Jetty容器运行Solr,详细的安装文档请看:

手摸手教你部署Solr-7.7.1

如果安装完成,启动Solr服务器,浏览器访问Solr-Admin主页,先吧数据库中的数据导入到Solr中,上面链接的文档中已经介绍了。如果你查询到了这些数据,证明Solr安装成功:

配置

按照 手摸手教你部署Solr-7.7.1 这篇文章中部署好的服务器,我们可以着手创建springboot_solr项目了。

修改application.yml

1
2
3
4
spring:
data:
solr:
host: http://127.0.0.1:8983/solr/new_core

其中就是solr服务器的连接URL,这个路径并不是可以直接访问的,在Solr-Admin中访问new_core的URL地址是:http://localhost:8983/solr/#/new_core/query,而实际的请求地址就是http://127.0.0.1:8983/solr/new_core了。

创建Entity

首先,创建Search.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Search implements Serializable {

@Field
@Id
private Long id;
@Field
private String username;
@Field
private String email;
@Field
private String qq;
@Field
private String password;
//省略getter/setter
}

因为我们在solr-7.7.1/server/solr/new_core/conf/managed-schemadb-data-config.xml中定义了solr和MySQL关联的配置,那么想要操控Solr数据库,就必须保证:

Solr字段约束=MySQL表字段=Entity属性

其中的@Field注解来自org.apache.solr.client.solrj.beans.Field,用于将Solr字段和Entity字段进行匹配。若名称相同直接加@Field,若不同加@Field("name")


本文中采用两种方式实现Solr的查询功能,这两种方式各自有坑,请注意!


SolrQuery

Solr提供最基本的方式就是用SolrClient完成对Solr的操作,那么必然要用org.apache.solr.client.solrj.SolrQuery进行查询条件封装,下面我们就用SolrQuery类进行操作。

最基本的查询

使用SolrQuery完成一个最基本的查询,创建测试类:SolrQuerySearchServiceTest

    1. 注入SolrClient类
    1. 构建查询类
    1. 封装查询条件
    1. 查询
    1. 从response中获取查询结果
    1. 打印查询结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@SpringBootTest
@RunWith(SpringRunner.class)
public class SolrQuerySearchServiceTest {
private Logger logger = LoggerFactory.getLogger(this.getClass());

//1. 注入SolrClient类
@Autowired
private SolrClient solrClient;

@Test
public void search() {
//2. 构建查询类
SolrQuery query = new SolrQuery();

//3. 封装查询条件,在solr中对应: localhost:8983/solr/new_core/select?q=keyword:*:*
query.set("q", "keyword:*");

try {
//4. 查询,获取response响应数据
QueryResponse response = solrClient.query(query);

//5. 从response中获取查询结果
SolrDocumentList documents = response.getResults();

//6. 打印查询结果
documents.forEach(document -> {
logger.info("id={} --> username={}", document.getFieldValue("id"), document.getFieldValue("username"));
});
} catch (Exception e) {
e.printStackTrace();
}
}
}

分页查询

通过设置setStart():从哪一行开始;设置setRows():查询到第几行结束,来实现分页查询。

注意:我们常用pageHelper进行数据库的分页查询,但是那里是:起始 –> 查询多少条记录。而在solr中起始查询的是记录,从0开始的。

1
2
query.setStart(current);
query.setRows(rows);

上面两段配置在solr中实际的构建的渲染条件:

http://localhost:8983/solr/new_core/select?start=0&rows=20

高亮查询

高亮查询是最坑的,主要有涉及以下几个过程:

  • 1.查询时构建高亮查询条件,并且限定需要高亮匹配的字段。

  • 2.从response查询数据中获取被高亮的集合,类似:[{id: 'xx', field: '<em>xx</em>'}] 这样的结构,field对应的数据是被标记高亮的。

  • 3.根据限定的高亮字段匹配高亮集合中对应标记高亮的数据。

  • 4.遍历response中返回的总数据集合,将高亮集合中的标记的高亮数据替换掉原未高亮的数据,于是,就实现了高亮。

1.构建查询条件

1
2
3
4
5
6
//高亮配置
String[] lightNames = {"username", "email", "qq"}; //设置需要高亮的域
query.setParam("hl", "true");
query.setParam("hl.fl", lightNames);
query.setHighlightSimplePre("<em style='color: red'>");
query.setHighlightSimplePost("</em>");

这些条件在solr中实际查询的URL是:

1
http://localhost:8983/solr/new_core/select?start=0&rows=20&hl=true&hl.fl=username&hl.fl=email&hl.fl=qq&hl.simple.pre=<em+style=%27color:red%27>&hl.simple.post=</em>

2.从response查询数据中获取被高亮的集合,类似:[{id: 'xx', field: '<em>xx</em>'}] 这样的结构,field对应的数据是被标记高亮的。

1
2
3
QueryResponse response = solrClient.query(query);
//获取被高亮的数据集合,其中的数据结构类似:[{id: "123", field: "<em>xxx</em>"}]
Map<String, Map<String, List<String>>> highlighting = response.getHighlighting();

highlighting数据结构:

3.根据限定的高亮字段匹配高亮集合中对应标记高亮的数据。

1
2
3
4
5
6
7
8
9
10
11
12
QueryResponse response = solrClient.query(query);
//获取被高亮的数据集合,其中的数据结构类似:[{id: "123", field: "<em>xxx</em>"}]
Map<String, Map<String, List<String>>> highlighting = response.getHighlighting();
SolrDocumentList documents = response.getResults(); //获取匹配结果
long numFound = documents.getNumFound(); //获取匹配的数据个数
if (numFound != 0) {
for (SolrDocument document : documents) {
//documents中存放了匹配的所有数据(未高亮),而highlighting中存放了高亮匹配的数据(高亮)
//通过id主键获取到id值,在highlighting中通过id值获取对应的高亮数据
Map<String, List<String>> listMap = highlighting.get(document.getFieldValue("id").toString());
}
}

首先,我们看一下查询原始数据结构documents:

可以看到这些数据是原始数据,并未高亮。而document.getFieldValue("id")就是获取到id值。

highlighting.get(document.getFieldValue("id").toString())就是根据id从高亮集合中获取对应的高亮字段和值,也就是:

4.遍历response中返回的总数据集合,将高亮集合中的标记的高亮数据替换掉原未高亮的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
 String[] fields = {"username", "email", "qq", "password"}; //设置solr中定义的域

//高亮配置
String[] lightNames = {"username", "email", "qq"}; //设置需要高亮的域
query.setParam("hl", "true");
query.setParam("hl.fl", lightNames);
query.setHighlightSimplePre("<em style='color: red'>");
query.setHighlightSimplePost("</em>");

/**
* 设置查询关键字的域
* 一般对应solr中的复制域(<copyFiled>)。
* 因为用户查询的数据不确定是什么,定义在复制域中的字段,Solr会自动进行多字段查询匹配
*/
query.set("q", "keyword:*" + keyword + "*"); //在Solr中查询语句:/select?q=keyword:xxx

QueryResponse response = solrClient.query(query);
//获取被高亮的数据集合,其中的数据结构类似:[{id: "123", field: "<em>xxx</em>"}]
Map<String, Map<String, List<String>>> highlighting = response.getHighlighting();
SolrDocumentList documents = response.getResults(); //获取匹配结果
long numFound = documents.getNumFound(); //获取匹配的数据个数
if (numFound != 0) {
List<Object> entityList = new ArrayList<>();
for (SolrDocument document : documents) {
//documents中存放了匹配的所有数据(未高亮),而highlighting中存放了高亮匹配的数据(高亮)
//通过id主键获取到id值,在highlighting中通过id值获取对应的高亮数据
Map<String, List<String>> listMap = highlighting.get(document.getFieldValue("id").toString());
for (int i = 0; i < lightNames.length; i++) {
if (listMap.get(lightNames[i]) != null) {
//根据设置的高亮域,将documents中未高亮的域的值替换为高亮的值
document.setField(lightNames[i], listMap.get(lightNames[i]).get(0));
}
}
Map<String, Object> fieldMap = new HashMap<>();
for (int i = 0; i < fields.length; i++) {
fieldMap.put(fields[i], String.valueOf(document.getFieldValue(fields[i])));
}
entityList.add(fieldMap);
}
return new ResponseCode(entityList, numFound);
} else {
return new ResponseCode("未搜索到任何结果");
}

通过document.setField()根据key,value将高亮的数据替换点原始未高亮的数据,实现的效果就是:

通过不断的循环替换,就能将原始未高亮的数据替换为高亮的数据,那么最终在前端页面实现的效果是:

最终的查询代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
private ResponseCode solrQuerySearch(String keyword, Integer current, Integer rows) {
if (StringUtils.isEmpty(keyword)) {
return new ResponseCode("请输入查询内容");
}
if (current == 1) {
current = 0;
} else {
current = (current - 1) * rows;
}
SolrQuery query = new SolrQuery();
query.setStart(current);
query.setRows(rows);
query.set("wt", "json");
try {
String[] fields = {"username", "email", "qq", "password"}; //设置solr中定义的域

//高亮配置
String[] lightNames = {"username", "email", "qq"}; //设置需要高亮的域
query.setParam("hl", "true");
query.setParam("hl.fl", lightNames);
query.setHighlightSimplePre("<em style='color: red'>");
query.setHighlightSimplePost("</em>");

/**
* 设置查询关键字的域
* 一般对应solr中的复制域(<copyFiled>)。
* 因为用户查询的数据不确定是什么,定义在复制域中的字段,Solr会自动进行多字段查询匹配
*/
query.set("q", "keyword:*" + keyword + "*"); //在Solr中查询语句:/select?q=keyword:xxx

QueryResponse response = solrClient.query(query);
//获取被高亮的数据集合,其中的数据结构类似:[{id: "123", field: "<em>xxx</em>"}]
Map<String, Map<String, List<String>>> highlighting = response.getHighlighting();
SolrDocumentList documents = response.getResults(); //获取匹配结果
long numFound = documents.getNumFound(); //获取匹配的数据个数
if (numFound != 0) {
List<Object> entityList = new ArrayList<>();
for (SolrDocument document : documents) {
//documents中存放了匹配的所有数据(未高亮),而highlighting中存放了高亮匹配的数据(高亮)
//通过id主键获取到id值,在highlighting中通过id值获取对应的高亮数据
Map<String, List<String>> listMap = highlighting.get(document.getFieldValue("id").toString());
for (int i = 0; i < lightNames.length; i++) {
if (listMap.get(lightNames[i]) != null) {
//根据设置的高亮域,将documents中未高亮的域的值替换为高亮的值
document.setField(lightNames[i], listMap.get(lightNames[i]).get(0));
}
}
Map<String, Object> fieldMap = new HashMap<>();
for (int i = 0; i < fields.length; i++) {
fieldMap.put(fields[i], String.valueOf(document.getFieldValue(fields[i])));
}
entityList.add(fieldMap);
}
return new ResponseCode(entityList, numFound);
} else {
return new ResponseCode("未搜索到任何结果");
}
} catch (Exception e) {
e.printStackTrace();
return new ResponseCode("服务器异常");
}
}

q=*xx* and type='string'

首先我们看下我在solr的managed-schema中定义的域如下:

1
2
3
4
5
6
7
8
9
10
<field name="id" type="string" indexed="true" stored="true" required="true" multiValued="false" />  
<field name="username" type="string" indexed="true" stored="true"/>
<field name="email" type="string" indexed="true" stored="true"/>
<field name="qq" type="string" indexed="true" stored="true"/>
<field name="password" type="string" indexed="true" stored="true"/>

<field name="keyword" type="text_ik" indexed="true" stored="false" multiValued="true"/>
<copyField source="username" dest="keyword"/>
<copyField source="email" dest="keyword"/>
<uniqueKey>id</uniqueKey>

最终渲染效果如图:

看到,实际高亮的是tycoding@163.com整个数据,并不是仅高亮163这三个字符串。

但是,在上面代码中,查询条件是:http://localhost:8983/solr/new_core/select?q=keyword:*163*,显然这是模糊查询,*匹配0个或多个字符。

关于Solr查询语句可以看 Solr之精确、匹配、排序、模糊查询

q=xx and type='string'

如果,我们把查询条件改为:http://localhost:8983/solr/new_core/select?q=keyword:163,即不加检索运算符,会怎样呢?

修改代码为:

1
query.set("q", "keyword:" + keyword)

看到高亮集合中虽然有key,但是没有value。那么最终也就没法实现高亮

q=xx and type='text_ik'

修改solr配置文件managed-schema:

1
2
<field name="username" type="text_ik" indexed="true" stored="true"/>
<field name="email" type="text_ik" indexed="true" stored="true"/>

即让usernameemail域使用text_ikIK分词器,那么实际的效果:

并且,仅对查询的数据163进行了高亮显示

q=*xx* and type='text_ik'

可以看到,数据被高亮显示了,但是还是整个字段值全被标记高亮。

综上,因为IK分词器能将查询数据分词匹配,且类似163,qq,com,cn 这些名词可以直接匹配到并可以精确到具体的字符串高亮显示。但是若solr域定义为string类型,就不能使用*运算符匹配数据。

可以看下这篇文章:搜索引擎solr系列—高亮配置及问题总结

SolrTemplate

使用SolrTemplate就相对简单很多,org.springframework.data.solr.core.SolrTemplate是spring-data-solr提供的一个solr模板类,类似spring-data-redis的RedisTemplate,使用SolrTemplate就像使用RedisTemplate或者spring-data-jpa一样方便的操作mysql, nosql数据库,快速实现CRUD业务。

Solr及Spring-Data-Solr入门学习 一文中我已经详细介绍过了,对应ssm-redis-solr项目,欢迎参考,start,fork。

所以在这里就不再详细介绍,按照上面使用SolrQuery的查询,这里使用SolrTemplate模板类完成数据查询:

创建SolrConfig

创建SolrConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class SolrConfig {

@Value("${spring.data.solr.host}")
private String solrHost;

@Bean
public SolrClient solrClient() {
return new HttpSolrClient.Builder(solrHost).build();
}

@Bean
public SolrTemplate solrTemplate(SolrClient client) {
return new SolrTemplate(client);
}
}

目的就是根据URL创建SolrClient,并将其注入到SolrTemplate中,以便可以使用SolrTemplate模板类操作Solr数据库。

编写Service服务层代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
private ResponseCode solrTemplateSearch(String keyword, Integer current, Integer rows) {
//高亮配置
HighlightQuery query = new SimpleHighlightQuery();
String[] fieldNames = {"username", "email", "qq"};
HighlightOptions highlightOptions = new HighlightOptions().addField(fieldNames); //设置高亮域
highlightOptions.setSimplePrefix("<em style='color: red'>"); //设置高亮前缀
highlightOptions.setSimplePostfix("</em>"); //设置高亮后缀
query.setHighlightOptions(highlightOptions); //设置高亮选项

if ("".equals(keyword)) {
return new ResponseCode("请输入查询内容");
}

try {
/**
* 通过Criteria构建查询过滤条件
* 其中这里的`keyword`等价于solr core中schema.xml配置的域,且`keyword`是复制域名
* 因为查询的内容是不确定的,solr提供了复制域实现同时查询多个域中的数据,并返回匹配的结果
*/
Criteria criteria = new Criteria("keyword");
//按照关键字查询
if (keyword != null && !"".equals(keyword)) {
criteria.contains(keyword);
}
query.addCriteria(criteria);

// //构建查询条件
// FilterQuery filterQuery = new SimpleFilterQuery();
// Criteria filterCriteria = new Criteria("field");
// filterCriteria.contains(keywords);
// filterQuery.addCriteria(filterCriteria);
// query.addFilterQuery(filterQuery);

if (current == null) {
current = 1; //默认第一页
}
if (rows == null) {
rows = 20; //默认每次查询20条记录
}
query.setOffset((long) ((current - 1) * rows)); //从第几条记录开始查询:= 当前页 * 每页的记录数
query.setRows(rows);

HighlightPage<Search> page = solrTemplate.queryForHighlightPage("", query, Search.class);
//循环高亮入口集合
for (HighlightEntry<Search> h : page.getHighlighted()) {
Search search = h.getEntity(); //获取原实体类
if (h.getHighlights().size() > 0) {
h.getHighlights().forEach(light -> {
if (search.getUsername().contains(keyword)) {
search.setUsername(light.getSnipplets().get(0));
}
if (search.getEmail().contains(keyword)) {
search.setEmail(light.getSnipplets().get(0));
}
if (search.getQq().contains(keyword)) {
search.setQq(light.getSnipplets().get(0));
}
});
}
}
return new ResponseCode(page.getContent(), page.getTotalElements());
} catch (Exception e) {
e.printStackTrace();
return new ResponseCode("查询失败");
}
}

SolrTemplate中提供了很多封装好的类进行调用,快速获取查询数据,相比SolrQuery逻辑和代码量都要简化很多。


交流

以上仅是个人的见解,可能有些地方是错误的,深知自己的菜鸡技术,欢迎大佬指出。

个人建了一个Java交流群:671017003。 欢迎大佬或是新人入驻一起交流学习Java技术。


联系

If you have some questions after you see this article, you can contact me or you can find some info by clicking these links.

如果你觉得这篇文章帮助到了你,你可以帮作者买一杯果汁表示鼓励