Spring Boot整合Solr7.x

Posted 程序员涂陌

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Boot整合Solr7.x相关的知识,希望对你有一定的参考价值。

 


Spring Boot整合Solr

写在前面

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

(十)Spring Boot整合Solr7.x

安装

如果你阅读过我的这篇文章:https://tycoding.cn/2018/09/24/other/solr/。会发现其中是将下载的solr source再提取出来放到tomcat容器中,过程比较复杂,也遇到很多坑,我自己再尝试将solr放到tomcat容器中也遇到很多很多的问题。但,[Solr官网](http://www.apache.org/dyn/closer.lua/lucene/solr/7.7.1 提供的源码其实已经将Solr放在了Jetty容器中,我们只需要通过命令运行Jetty容器也依然可以访问Solr。

So,我这里推荐使用Solr自带的Jetty容器运行Solr,详细的安装文档请看: https://tycoding.cn/2019/03/05/other/solr-server/

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

(十)Spring Boot整合Solr7.x

配置

按照 https://tycoding.cn/2019/03/05/other/solr-server/ 这篇文章中部署好的服务器,我们可以着手创建springboot_solr项目了。

修改 application.yml

 
   
   
 
  1. spring:

  2. data:

  3. solr:

  4. host: http://127.0.0.1:8983/solr/new_core

创建Entity

首先,创建Search.java:

 
   
   
 
  1. public class Search implements Serializable {


  2. @Field

  3. @Id

  4. private Long id;

  5. @Field

  6. private String username;

  7. @Field

  8. private String email;

  9. @Field

  10. private String qq;

  11. @Field

  12. private String password;

  13. //省略getter/setter

  14. }

因为我们在 solr-7.7.1/server/solr/new_core/conf/managed-schema和 db-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. @SpringBootTest

  2. @RunWith(SpringRunner.class)

  3. public class SolrQuerySearchServiceTest {

  4. private Logger logger = LoggerFactory.getLogger(this.getClass());


  5. //1. 注入SolrClient类

  6. @Autowired

  7. private SolrClient solrClient;


  8. @Test

  9. public void search() {

  10. //2. 构建查询类

  11. SolrQuery query = new SolrQuery();


  12. //3. 封装查询条件,在solr中对应: localhost:8983/solr/new_core/select?q=keyword:*:*

  13. query.set("q", "keyword:*");


  14. try {

  15. //4. 查询,获取response响应数据

  16. QueryResponse response = solrClient.query(query);


  17. //5. 从response中获取查询结果

  18. SolrDocumentList documents = response.getResults();


  19. //6. 打印查询结果

  20. documents.forEach(document -> {

  21. logger.info("id={} --> username={}", document.getFieldValue("id"), document.getFieldValue("username"));

  22. });

  23. } catch (Exception e) {

  24. e.printStackTrace();

  25. }

  26. }

  27. }

分页查询

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

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

 
   
   
 
  1. query.setStart(current);

  2. 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. String[] lightNames = {"username", "email", "qq"}; //设置需要高亮的域

  3. query.setParam("hl", "true");

  4. query.setParam("hl.fl", lightNames);

  5. query.setHighlightSimplePre("<em style='color: red'>");

  6. 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. QueryResponse response = solrClient.query(query);

  2. //获取被高亮的数据集合,其中的数据结构类似:[{id: "123", field: "<em>xxx</em>"}]

  3. Map<String, Map<String, List<String>>> highlighting = response.getHighlighting();

highlighting数据结构:

(十)Spring Boot整合Solr7.x

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

 
   
   
 
  1. QueryResponse response = solrClient.query(query);

  2. //获取被高亮的数据集合,其中的数据结构类似:[{id: "123", field: "<em>xxx</em>"}]

  3. Map<String, Map<String, List<String>>> highlighting = response.getHighlighting();

  4. SolrDocumentList documents = response.getResults(); //获取匹配结果

  5. long numFound = documents.getNumFound(); //获取匹配的数据个数

  6. if (numFound != 0) {

  7. for (SolrDocument document : documents) {

  8. //documents中存放了匹配的所有数据(未高亮),而highlighting中存放了高亮匹配的数据(高亮)

  9. //通过id主键获取到id值,在highlighting中通过id值获取对应的高亮数据

  10. Map<String, List<String>> listMap = highlighting.get(document.getFieldValue("id").toString());

  11. }

  12. }

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

(十)Spring Boot整合Solr7.x

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

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

(十)Spring Boot整合Solr7.x

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

 
   
   
 
  1. String[] fields = {"username", "email", "qq", "password"}; //设置solr中定义的域


  2. //高亮配置

  3. String[] lightNames = {"username", "email", "qq"}; //设置需要高亮的域

  4. query.setParam("hl", "true");

  5. query.setParam("hl.fl", lightNames);

  6. query.setHighlightSimplePre("<em style='color: red'>");

  7. query.setHighlightSimplePost("</em>");


  8. /**

  9. * 设置查询关键字的域

  10. * 一般对应solr中的复制域(<copyFiled>)。

  11. * 因为用户查询的数据不确定是什么,定义在复制域中的字段,Solr会自动进行多字段查询匹配

  12. */

  13. query.set("q", "keyword:*" + keyword + "*"); //在Solr中查询语句:/select?q=keyword:xxx


  14. QueryResponse response = solrClient.query(query);

  15. //获取被高亮的数据集合,其中的数据结构类似:[{id: "123", field: "<em>xxx</em>"}]

  16. Map<String, Map<String, List<String>>> highlighting = response.getHighlighting();

  17. SolrDocumentList documents = response.getResults(); //获取匹配结果

  18. long numFound = documents.getNumFound(); //获取匹配的数据个数

  19. if (numFound != 0) {

  20. List<Object> entityList = new ArrayList<>();

  21. for (SolrDocument document : documents) {

  22. //documents中存放了匹配的所有数据(未高亮),而highlighting中存放了高亮匹配的数据(高亮)

  23. //通过id主键获取到id值,在highlighting中通过id值获取对应的高亮数据

  24. Map<String, List<String>> listMap = highlighting.get(document.getFieldValue("id").toString());

  25. for (int i = 0; i < lightNames.length; i++) {

  26. if (listMap.get(lightNames[i]) != null) {

  27. //根据设置的高亮域,将documents中未高亮的域的值替换为高亮的值

  28. document.setField(lightNames[i], listMap.get(lightNames[i]).get(0));

  29. }

  30. }

  31. Map<String, Object> fieldMap = new HashMap<>();

  32. for (int i = 0; i < fields.length; i++) {

  33. fieldMap.put(fields[i], String.valueOf(document.getFieldValue(fields[i])));

  34. }

  35. entityList.add(fieldMap);

  36. }

  37. return new ResponseCode(entityList, numFound);

  38. } else {

  39. return new ResponseCode("未搜索到任何结果");

  40. }

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

(十)Spring Boot整合Solr7.x

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

(十)Spring Boot整合Solr7.x

最终的查询代码如下:

 
   
   
 
  1. private ResponseCode solrQuerySearch(String keyword, Integer current, Integer rows) {

  2. if (StringUtils.isEmpty(keyword)) {

  3. return new ResponseCode("请输入查询内容");

  4. }

  5. if (current == 1) {

  6. current = 0;

  7. } else {

  8. current = (current - 1) * rows;

  9. }

  10. SolrQuery query = new SolrQuery();

  11. query.setStart(current);

  12. query.setRows(rows);

  13. query.set("wt", "json");

  14. try {

  15. String[] fields = {"username", "email", "qq", "password"}; //设置solr中定义的域


  16. //高亮配置

  17. String[] lightNames = {"username", "email", "qq"}; //设置需要高亮的域

  18. query.setParam("hl", "true");

  19. query.setParam("hl.fl", lightNames);

  20. query.setHighlightSimplePre("<em style='color: red'>");

  21. query.setHighlightSimplePost("</em>");


  22. /**

  23. * 设置查询关键字的域

  24. * 一般对应solr中的复制域(<copyFiled>)。

  25. * 因为用户查询的数据不确定是什么,定义在复制域中的字段,Solr会自动进行多字段查询匹配

  26. */

  27. query.set("q", "keyword:*" + keyword + "*"); //在Solr中查询语句:/select?q=keyword:xxx


  28. QueryResponse response = solrClient.query(query);

  29. //获取被高亮的数据集合,其中的数据结构类似:[{id: "123", field: "<em>xxx</em>"}]

  30. Map<String, Map<String, List<String>>> highlighting = response.getHighlighting();

  31. SolrDocumentList documents = response.getResults(); //获取匹配结果

  32. long numFound = documents.getNumFound(); //获取匹配的数据个数

  33. if (numFound != 0) {

  34. List<Object> entityList = new ArrayList<>();

  35. for (SolrDocument document : documents) {

  36. //documents中存放了匹配的所有数据(未高亮),而highlighting中存放了高亮匹配的数据(高亮)

  37. //通过id主键获取到id值,在highlighting中通过id值获取对应的高亮数据

  38. Map<String, List<String>> listMap = highlighting.get(document.getFieldValue("id").toString());

  39. for (int i = 0; i < lightNames.length; i++) {

  40. if (listMap.get(lightNames[i]) != null) {

  41. //根据设置的高亮域,将documents中未高亮的域的值替换为高亮的值

  42. document.setField(lightNames[i], listMap.get(lightNames[i]).get(0));

  43. }

  44. }

  45. Map<String, Object> fieldMap = new HashMap<>();

  46. for (int i = 0; i < fields.length; i++) {

  47. fieldMap.put(fields[i], String.valueOf(document.getFieldValue(fields[i])));

  48. }

  49. entityList.add(fieldMap);

  50. }

  51. return new ResponseCode(entityList, numFound);

  52. } else {

  53. return new ResponseCode("未搜索到任何结果");

  54. }

  55. } catch (Exception e) {

  56. e.printStackTrace();

  57. return new ResponseCode("服务器异常");

  58. }

  59. }

q=*xx*andtype='string'

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

 
   
   
 
  1. <field name="id" type="string" indexed="true" stored="true" required="true" multiValued="false" />

  2. <field name="username" type="string" indexed="true" stored="true"/>

  3. <field name="email" type="string" indexed="true" stored="true"/>

  4. <field name="qq" type="string" indexed="true" stored="true"/>

  5. <field name="password" type="string" indexed="true" stored="true"/>


  6. <field name="keyword" type="text_ik" indexed="true" stored="false" multiValued="true"/>

  7. <copyField source="username" dest="keyword"/>

  8. <copyField source="email" dest="keyword"/>

  9. <uniqueKey>id</uniqueKey>

最终渲染效果如图:

(十)Spring Boot整合Solr7.x

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

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

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

q=xxandtype='string'

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

修改代码为:

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

(十)Spring Boot整合Solr7.x

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

q=xxandtype='text_ik'

修改solr配置文件 managed-schema:

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

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

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

(十)Spring Boot整合Solr7.x

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

(十)Spring Boot整合Solr7.x

q=*xx*andtype='text_ik'

(十)Spring Boot整合Solr7.x

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

综上,因为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. @Configuration

  2. public class SolrConfig {


  3. @Value("${spring.data.solr.host}")

  4. private String solrHost;


  5. @Bean

  6. public SolrClient solrClient() {

  7. return new HttpSolrClient.Builder(solrHost).build();

  8. }


  9. @Bean

  10. public SolrTemplate solrTemplate(SolrClient client) {

  11. return new SolrTemplate(client);

  12. }

  13. }

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

编写Service服务层代码实现

 
   
   
 
  1. private ResponseCode solrTemplateSearch(String keyword, Integer current, Integer rows) {

  2. //高亮配置

  3. HighlightQuery query = new SimpleHighlightQuery();

  4. String[] fieldNames = {"username", "email", "qq"};

  5. HighlightOptions highlightOptions = new HighlightOptions().addField(fieldNames); //设置高亮域

  6. highlightOptions.setSimplePrefix("<em style='color: red'>"); //设置高亮前缀

  7. highlightOptions.setSimplePostfix("</em>"); //设置高亮后缀

  8. query.setHighlightOptions(highlightOptions); //设置高亮选项


  9. if ("".equals(keyword)) {

  10. return new ResponseCode("请输入查询内容");

  11. }


  12. try {

  13. /**

  14. * 通过Criteria构建查询过滤条件

  15. * 其中这里的`keyword`等价于solr core中schema.xml配置的域,且`keyword`是复制域名

  16. * 因为查询的内容是不确定的,solr提供了复制域实现同时查询多个域中的数据,并返回匹配的结果

  17. */

  18. Criteria criteria = new Criteria("keyword");

  19. //按照关键字查询

  20. if (keyword != null && !"".equals(keyword)) {

  21. criteria.contains(keyword);

  22. }

  23. query.addCriteria(criteria);


  24. // //构建查询条件

  25. // FilterQuery filterQuery = new SimpleFilterQuery();

  26. // Criteria filterCriteria = new Criteria("field");

  27. // filterCriteria.contains(keywords);

  28. // filterQuery.addCriteria(filterCriteria);

  29. // query.addFilterQuery(filterQuery);


  30. if (current == null) {

  31. current = 1; //默认第一页

  32. }

  33. if (rows == null) {

  34. rows = 20; //默认每次查询20条记录

  35. }

  36. query.setOffset((long) ((current - 1) * rows)); //从第几条记录开始查询:= 当前页 * 每页的记录数

  37. query.setRows(rows);


  38. HighlightPage<Search> page = solrTemplate.queryForHighlightPage("", query, Search.class);

  39. //循环高亮入口集合

  40. for (HighlightEntry<Search> h : page.getHighlighted()) {

  41. Search search = h.getEntity(); //获取原实体类

  42. if (h.getHighlights().size() > 0) {

  43. h.getHighlights().forEach(light -> {

  44. if (search.getUsername().contains(keyword)) {

  45. search.setUsername(light.getSnipplets().get(0));

  46. }

  47. }

  48. if (search.getQq().contains(keyword)) {

  49. search.setQq(light.getSnipplets().get(0));

  50. }

  51. });

  52. }

  53. }

  54. return new ResponseCode(page.getContent(), page.getTotalElements());

  55. } catch (Exception e) {

  56. e.printStackTrace();

  57. return new ResponseCode("查询失败");

  58. }

  59. }

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





/ 往期推荐 /






END

     








以上是关于Spring Boot整合Solr7.x的主要内容,如果未能解决你的问题,请参考以下文章

Solr7.x学习-使用spring-data-solr

Spring Boot:Spring Boot整合Mybatis案例

spring boot 系列之四:spring boot 整合JPA

Spring boot:thymeleaf 没有正确渲染片段

spring-boot+mybatis整合简写

Spring Boot2 系列教程Spring Boot 整合 Freemarker