简介
Spring Data Solr就是为了方便Solr的开发所研制的一个 框架,其底层是对SolrJ(官方 API)的封装。SolrJ的使用是通过它提供的若干接口,而Spring Data Solr 是Spring调用Solr接口的 进一步封装,简化了Solr的使用,可以通过Spring Data Solr提供的对象HttpSolrServer对文档索引的创建、搜索、分组、排序、分页等等进行操控。功能还是较为完善的。
Spring Data Solr使得 SolrJ用起来更简单, 可以把JavaBean通过注解的方式直接从Solr查询出来和写入.就和ORM框架做的工作很相似。这两个包有个版本对应关系,使用的时候要注意:
spring-data-solr | solr-solrj ————————-+—————- 2.1.15 | 5.5.0 3.0.10 | 6.5.0 4.0.0 | 7.4.0 4.2.3 | 8.5.2
Spring Data Solr就是把Solr的应用更好的集成到Spring框架中,它的使用方法就是使用SolrTemplate这个类来对索引库进行CRUD的操作,实际上是由SolrOperations这个接口里面的方法来操作的。
工程应用
方式1:SpringBoot + Spring Data Solr
1. 添加pom.xml依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-solr</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
2. Solr配置
■ 通过Solr URL连接
application.yml
spring:
application:
name: solr-springdata-example01
data:
solr:
host: http://bigdata-node1:8983/solr/db_sync
server:
port: 8081
application.properties
spring.application.name=solr-springdata-example01
server.port=8081
# solr Configuration
spring.data.solr.host=http://bigdata-node1:8983/solr/db_sync
■ 通过ZooKeeper连接
application.yml
spring:
application:
name: solr-springdata-example01
data:
solr:
zk-host: 192.168.0.101:2181,192.168.0.102:2181,192.168.0.103:2181
server:
port: 8081
application.properties
spring.application.name=solr-springdata-example01
server.port=8083
# solr Configuration
spring.data.solr.zk-host=192.168.0.101:2181,192.168.0.102:2181,192.168.0.103:2181
spring.data.solr.core=db_sync
3. 编写关键类
■ 配置类(SolrConfig.java) ```java import org.apache.solr.client.solrj.impl.CloudSolrClient; import org.apache.solr.common.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.solr.core.SolrTemplate; import java.util.Arrays; import java.util.Optional;
/**
@Description 自定义配置类(SolrTemplate) */ @Configuration public class SolrConfig {
@Value(“${spring.data.solr.zk-host}”) private String zk_host;
@Value(“${spring.data.solr.core}”) private String solrCore;
/**
- 配置SolrTemplate
*/
@Bean
public SolrTemplate solrTemplate() {
if (StringUtils.isEmpty(zk_host)) {
} CloudSolrClient client = new CloudSolrClient.Builder(return null;
).build(); // 设置默认的操作实例 client.setDefaultCollection(solrCore); client.setZkClientTimeout(5000); client.setZkConnectTimeout(5000); SolrTemplate template = new SolrTemplate(client); return template; }Arrays.asList(zk_host.split(",")),
Optional.empty()
- 配置SolrTemplate
*/
@Bean
public SolrTemplate solrTemplate() {
if (StringUtils.isEmpty(zk_host)) {
}
**■ 实体类(Article.java)**
```java
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.solr.client.solrj.beans.Field;
import org.springframework.data.solr.core.mapping.SolrDocument;
import java.io.Serializable;
/**
* Description VO-文章类
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@SolrDocument(solrCoreName = "db_sync")
public class Article implements Serializable {
@Field("id")
private Integer id;
@Field("content_id")
private Long contentId;
@Field("title")
private String title;
@Field("content")
private String content;
@Field("type")
private Integer type;
@Field("create_at")
private Long createAt;
@Field("publish_at")
private Long publishAt;
@Override
public String toString() {
return "Article{" +
"id=" + id +
", contentId=" + contentId +
", title='" + title + '\'' +
", content='" + content + '\'' +
", type=" + type +
", createAt=" + createAt +
", publishAt=" + publishAt +
'}';
}
}
注解说明:spring.application.name=solr-springdata-example01server.port=8083# solr Configurationspring.data.solr.zk-host=192.168.0.101:2181,192.168.0.102:2181,192.168.0.103:2181spring.data.solr.core=db_sync
- @Field:注解标识 。
如果属性与配置文件定义的域名称不一致,需要在注解中指定域名称。
- @Dynamic:声明这是一个动态域。
在代码层,将数据库中某一字段中文字解析成map对象,将给对象封装到实体类中即可完成动态域的配置。
// 示例
// 将spec字段中的json字符串转换为map,字段内容{"内存":"64G","网络":"4G"}
Map specMap = JSON.parseObject(item.getSpec());
// 给带注解的字段赋值
item.setSpecMap(specMap);
- @SolrDocument:指定集合的名称。
■ 服务接口(SolrTemplateService.java)
import org.springframework.data.solr.core.query.Criteria;
import org.springframework.data.solr.core.query.SimpleFilterQuery;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.Map;
public interface SolrTemplateService {
/**
* 将JavaBean对象添加(新增或修改)到Solr中
*
* @param bean VO/DTO/Pojo/Domain对象
* @param <T> 泛型(对象类型)
* @return
*/
<T> boolean saveOrUpdate(T bean);
/**
* 将Map对象添加(新增或修改)到Solr中
*
* @param map Map对象
* @param clz JavaBean的Class对象
* @param <T> 泛型(对象类型)
* @return
*/
<T> boolean saveOrUpdate(Map map, Class<T> clz);
/**
* 通过文档ID删除Solr中的文档
*
* @param id 索引ID
* @return
*/
boolean deleteById(String id);
/**
* 通过查询删除Solr中对应的数据集合
*
* @param query 查询条件
*/
boolean deleteByQuery(String query);
/**
* 批量新增或更新记录
*
* @param entities 新增或更新对象列表
* @param <T> 泛型
* @return
*/
<T> boolean batchSaveOrUpdate(List<T> entities);
/**
* 分页查询
*
* @param clz 泛型类对应java.lang.Class
* @param offset 开始索引(默认:0)
* @param rows 每页显示记录数(默认:10)
* @return
*/
<T> SolrResultInfo<T> queryForPage(Class<T> clz, Long offset, Integer rows);
/**
* 高亮查询
*
* @param clz 泛型类对应java.lang.Class
* @param keywords 关键字
* @param hlFieldsList 高亮字段列表
* @param <T> 泛型
* @return
*/
<T> SolrResultInfo<T> queryHeightLight(Class<T> clz, String keywords, List<String> hlFieldsList) throws InvocationTargetException, IllegalAccessException;
/**
* 通过泛型获取Solr中的对象集合
*
* @param clz 泛型类对应java.lang.Class
* @param criteria 查询条件
* @param hlFieldsList 高亮显示数据域名称,是List<String>集合
* @param fqList 过滤条件
* @param offset 开始索引(默认:0)
* @param rows 每页显示记录数(默认:10)
* @return
*/
<T> SolrResultInfo<T> query(Class<T> clz, Criteria criteria, List<String> hlFieldsList, List<SimpleFilterQuery> fqList, Long offset, Integer rows) throws InvocationTargetException, IllegalAccessException;
/**
* 根据ID获取记录
*
* @param id 索引ID
* @param clz Class对象
* @param <T> 泛型
* @return
*/
<T> T getById(String id, Class<T> clz);
}
■ 服务接口实现类(SolrTemplateServiceImpl.java)
import com.lonton.bigdata.service.SolrResultInfo;
import com.lonton.bigdata.service.SolrTemplateService;
import com.lonton.bigdata.util.CommonUtils;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.solr.client.solrj.response.UpdateResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.data.solr.core.SolrTemplate;
import org.springframework.data.solr.core.query.*;
import org.springframework.data.solr.core.query.result.HighlightEntry;
import org.springframework.data.solr.core.query.result.HighlightPage;
import org.springframework.data.solr.core.query.result.ScoredPage;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@Service
public class SolrTemplateServiceImpl implements SolrTemplateService {
private static final Logger log = LoggerFactory.getLogger(SolrTemplateServiceImpl.class);
@Value("${spring.data.solr.core}")
private String solrCore;
@Autowired
private SolrTemplate solrTemplate;
/**
* 将JavaBean对象添加(新增或修改)到Solr中
*
* @param bean VO/DTO/Pojo/Domain对象
* @param <T> 泛型(对象类型)
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public <T> boolean saveOrUpdate(T bean) {
try {
// 将对象存储到索引库中
UpdateResponse resp = solrTemplate.saveBean(solrCore, bean);
// 提交
solrTemplate.commit(solrCore);
if (resp.getStatus() == 0) {
log.debug("saveOrUpdate successfully!");
return true;
}
} catch (Exception e) {
log.error(e.getMessage());
e.printStackTrace();
}
log.debug("saveOrUpdate failure!");
return false;
}
/**
* 将Map对象添加(新增或修改)到Solr中
*
* @param map Map对象
* @param clz JavaBean的Class对象
* @param <T> 泛型(对象类型)
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public <T> boolean saveOrUpdate(Map map, Class<T> clz) {
try {
T bean = CommonUtils.mapToBean((Map<String, ?>) map, clz);
// 将对象存储到索引库中
UpdateResponse resp = solrTemplate.saveBean(solrCore, bean);
// 提交
solrTemplate.commit(solrCore);
if (resp.getStatus() == 0) {
log.debug("saveOrUpdate successfully!");
return true;
}
} catch (Exception e) {
log.error(e.getMessage());
e.printStackTrace();
}
log.debug("saveOrUpdate failure!");
return false;
}
/**
* 通过文档ID删除Solr中的文档
*
* @param id 索引ID
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteById(String id) {
try {
UpdateResponse resp = solrTemplate.deleteByIds(solrCore, id);
solrTemplate.commit(solrCore);
if (resp.getStatus() == 0) {
log.debug("delete successfully!");
return true;
}
} catch (Exception e) {
log.error(e.getMessage());
e.printStackTrace();
}
log.debug("delete failure!");
return false;
}
/**
* 通过查询删除Solr中对应的数据集合
*
* @param query 查询条件
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteByQuery(String query) {
try {
UpdateResponse resp = solrTemplate.delete(solrCore, new SimpleQuery(query));
solrTemplate.commit(solrCore);
if (resp.getStatus() == 0) {
log.debug("delete successfully!");
return true;
}
} catch (Exception e) {
log.error(e.getMessage());
e.printStackTrace();
}
log.debug("delete failure!");
return false;
}
/**
* 批量新增或更新记录
*
* @param entities 新增或更新对象列表
* @param <T> 泛型
* @return
*/
@Override
public <T> boolean batchSaveOrUpdate(List<T> entities) {
try {
// 将对象存储到索引库中
UpdateResponse resp = solrTemplate.saveBeans(solrCore, entities);
// 提交
solrTemplate.commit(solrCore);
if (resp.getStatus() == 0) {
log.debug("batchSaveOrUpdate successfully!");
return true;
}
} catch (Exception e) {
log.error(e.getMessage());
e.printStackTrace();
}
log.debug("batchSaveOrUpdate failure!");
return false;
}
/**
* 分页查询
*
* @param clz 泛型类对应java.lang.Class
* @param offset 开始索引(默认:0)
* @param rows 每页显示记录数(默认:10)
* @return
*/
@Override
public <T> SolrResultInfo<T> queryForPage(Class<T> clz, Long offset, Integer rows) {
// 定义返回自定义数据结构对象
SolrResultInfo<T> resultInfo = new SolrResultInfo<T>();
Query query = new SimpleQuery("*:*");
query.setOffset(offset); // 开始索引(默认 0)
query.setRows(rows); // 每页记录数(默认 10)
ScoredPage<T> page = solrTemplate.queryForPage(solrCore, query, clz);
resultInfo.setTotal(page.getTotalElements());
resultInfo.setList(page.getContent());
return resultInfo;
}
/**
* 高亮查询
*
* @param clz 泛型类对应java.lang.Class
* @param keywords 关键字
* @param hlFieldsList 高亮字段列表
* @param <T> 泛型
* @return
*/
@Override
public <T> SolrResultInfo<T> queryHeightLight(Class<T> clz, String keywords, List<String> hlFieldsList) throws InvocationTargetException, IllegalAccessException {
// 定义返回自定义数据结构对象
SolrResultInfo<T> resultInfo = new SolrResultInfo<T>();
// 封装高亮条件
HighlightQuery q = new SimpleHighlightQuery();
HighlightOptions highlightOptions = new HighlightOptions();
// 高亮显示字段
for (String s : hlFieldsList) {
highlightOptions.addField(s);
}
// 高亮样式
highlightOptions.setSimplePrefix("<em style='color: red'>");
highlightOptions.setSimplePostfix("</em>");
q.setHighlightOptions(highlightOptions);
Criteria criteria = new Criteria("keywords").is(keywords);
q.addCriteria(criteria);
HighlightPage<T> pages = solrTemplate.queryForHighlightPage(solrCore, q, clz);
// *********************** 设置高亮结果 ***********************
for (HighlightEntry<T> h : pages.getHighlighted()) { // 循环高亮入口集合
T t = h.getEntity(); // 获取原实体类
if (h.getHighlights().size() > 0 && h.getHighlights().get(0).getSnipplets().size() > 0) {
for (HighlightEntry.Highlight highlight : h.getHighlights()) {
for (String value : highlight.getSnipplets()) {
// 替换高亮部分的内容
BeanUtils.setProperty(t, highlight.getField().getName(), value);
}
}
}
}
resultInfo.setTotal(pages.getTotalElements());
resultInfo.setList(pages.getContent());
return resultInfo;
}
/**
* 通过泛型获取Solr中的对象集合
*
* @param clz 泛型类对应java.lang.Class
* @param criteria 查询条件
* @param hlFieldsList 高亮显示数据域名称,是List<String>集合
* @param fqList 过滤条件
* @param offset 开始索引(默认:0)
* @param rows 每页显示记录数(默认:10)
* @return
*/
@Override
public <T> SolrResultInfo<T> query(Class<T> clz, Criteria criteria, List<String> hlFieldsList, List<SimpleFilterQuery> fqList, Long offset, Integer rows) throws InvocationTargetException, IllegalAccessException {
// 定义返回自定义数据结构对象
SolrResultInfo<T> resultInfo = new SolrResultInfo<>();
// 封装高亮条件
HighlightQuery q = new SimpleHighlightQuery();
// 过滤条件
for (SimpleFilterQuery simpleFilterQuery : fqList) {
q.addFilterQuery(simpleFilterQuery);
}
// 分页查询
q.setOffset(offset);
q.setRows(rows);
// 高亮显示字段
HighlightOptions highlightOptions = new HighlightOptions();
for (String s : hlFieldsList) {
highlightOptions.addField(s);
}
// 高亮样式
highlightOptions.setSimplePrefix("<em style='color: red'>");
highlightOptions.setSimplePostfix("</em>");
q.setHighlightOptions(highlightOptions);
q.addCriteria(criteria);
HighlightPage<T> pages = solrTemplate.queryForHighlightPage(solrCore, q, clz);
// *********************** 设置高亮结果 ***********************
for (HighlightEntry<T> h : pages.getHighlighted()) { // 循环高亮入口集合
T t = h.getEntity(); // 获取原实体类
if (h.getHighlights().size() > 0 && h.getHighlights().get(0).getSnipplets().size() > 0) {
for (HighlightEntry.Highlight highlight : h.getHighlights()) {
for (String value : highlight.getSnipplets()) {
// 替换高亮部分的内容
BeanUtils.setProperty(t, highlight.getField().getName(), value);
}
}
}
}
resultInfo.setTotal(pages.getTotalElements());
resultInfo.setList(pages.getContent());
return resultInfo;
}
private <T> SolrResultInfo<T> queryNoHeightLight(Class<T> clz, String query, Criteria criteria, List<String> hlFieldsList, Long offset, Integer rows) {
// 定义返回自定义数据结构对象
SolrResultInfo<T> resultInfo = new SolrResultInfo<>();
// 查询条件构造
Query q = new SimpleQuery(query);
// 分页查询
q.setOffset(offset); // 开始索引(默认 0)
q.setRows(rows); // 每页记录数(默认 10)
// 条件查询
q.addCriteria(criteria);
Page<T> pages = solrTemplate.query(solrCore, q, clz);
resultInfo.setTotal(pages.getTotalElements());
resultInfo.setList(pages.getContent());
return resultInfo;
}
/**
* 根据ID获取记录
*
* @param id 索引ID
* @param clz Class对象
* @param <T> 泛型
* @return
*/
@Override
public <T> T getById(String id, Class<T> clz) {
Optional<T> bean = solrTemplate.getById(solrCore, id, clz);
return bean.isPresent() ? bean.get() : null;
}
}
■ 返回对象封装类(SolrResultInfo.java)
import java.util.List;
/**
* Description 查询Solr返回的对象,对象类型为T的集合,还包含Solr中符合条件记录总数
*/
public class SolrResultInfo<T> {
private List<T> list = null;
private Long total = null;
public List<T> getList() {
return list;
}
public void setList(List<T> list) {
this.list = list;
}
public Long getTotal() {
return total;
}
public void setTotal(Long total) {
this.total = total;
}
}
■ 工具类(CommonUtils.java)
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.apache.commons.beanutils.BeanUtils;
import org.springframework.cglib.beans.BeanMap;
import java.util.List;
import java.util.Map;
/**
* Description 工具类(Map与JavaBean互转)
*/
public class CommonUtils {
/**
* 把一个Map转换成指定类型的JavaBean对象
*
* @param map Map对象
* @param clazz Class对象
* @param <T> 泛型
* @return
*/
public static <T> T mapToBean(Map<String, ?> map, Class<T> clazz) {
try {
T bean = clazz.newInstance();
BeanUtils.populate(bean, map);
return bean;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 将List<Map<String,Object>>转换为List<T>
*
* @param maps Map对象列表
* @param clazz Class对象
* @return
* @throws InstantiationException
* @throws IllegalAccessException
*/
public static <T> List<T> mapsToBeans(List<Map<String, Object>> maps, Class<T> clazz)
throws InstantiationException, IllegalAccessException {
List<T> list = Lists.newArrayList();
if (maps != null && maps.size() > 0) {
Map<String, Object> map;
T bean;
for (int i = 0, size = maps.size(); i < size; i++) {
map = maps.get(i);
bean = clazz.newInstance();
list.add((T) mapToBean(map, bean.getClass()));
}
}
return list;
}
/**
* 将对象装换为map
*
* @param bean
* @return
*/
public static <T> Map<String, Object> beanToMap(T bean) {
Map<String, Object> map = Maps.newHashMap();
if (bean != null) {
BeanMap beanMap = BeanMap.create(bean);
for (Object key : beanMap.keySet()) {
map.put(key + "", beanMap.get(key));
}
}
return map;
}
/**
* 将List<T>转换为List<Map<String, Object>>
*
* @param beanList
* @return
*/
public static <T> List<Map<String, Object>> beansToMap(List<T> beanList) {
List<Map<String, Object>> list = Lists.newArrayList();
if (beanList != null && beanList.size() > 0) {
Map<String, Object> map;
T bean;
for (int i = 0, size = beanList.size(); i < size; i++) {
bean = beanList.get(i);
map = beanToMap(bean);
list.add(map);
}
}
return list;
}
}
4. 测试
■ 测试类(SolrTemplateServiceTests.java)
import com.lonton.bigdata.vo.Article;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.solr.core.query.Criteria;
import org.springframework.data.solr.core.query.SimpleFilterQuery;
import org.springframework.util.Assert;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@SpringBootTest
public class SolrTemplateServiceTests {
@Autowired
private SolrTemplateService service;
@Test
public void saveOrUpdate() {
Article bean = new Article();
bean.setId(1);
bean.setContentId(99L);
bean.setTitle("测试-SolrTemplateService.saveOrUpdate");
bean.setContent("测试-作为一个中国人,我感到很自豪");
bean.setType(1);
bean.setCreateAt(1578912619999L);
bean.setPublishAt(1578912619999L);
assert service.saveOrUpdate(bean);
}
@Test
public void saveOrUpdateByMap() {
Map<String, Object> map = new HashMap<>();
map.put("id", 1);
map.put("contentId", 101L);
map.put("title", "北京");
map.put("content", "saveOrUpdateByMap作为一个中国人,我感到很自豪。");
map.put("type", 1);
map.put("createAt", 1578912614123L);
map.put("publishAt", 1578912614125L);
assert service.saveOrUpdate(map, Article.class);
}
@Test
public void deleteById() {
saveOrUpdate();
assert service.deleteById("1");
}
@Test
public void deleteByQuery() {
saveOrUpdate();
assert service.deleteByQuery("id:[1 TO 12]");
}
@Test
public void deleteAll() {
assert service.deleteByQuery("*:*");
}
@Test
public void batchSaveOrUpdate() {
List<Article> entities = new ArrayList<>();
for (int i = 1; i <= 35; i++) {
entities.add(new Article(i, i * 10L, "我是中国人,打倒小日本" + i, "作为一个中国人,我感到很自豪。", 1, 1578912614123L, 1578912614123L));
}
assert service.batchSaveOrUpdate(entities);
}
@Test
public void queryForPage() {
batchSaveOrUpdate();
SolrResultInfo<Article> resultInfo = service.queryForPage(Article.class, (2 - 1) * 5L, 5);
Long total = resultInfo.getTotal();
System.out.println("total:" + total);
if (total > 0) {
for (Article article : resultInfo.getList()) {
System.out.println(article);
}
}
Assert.isTrue(total.intValue() == 35, "异常:返回数据与实际不符!");
}
@Test
public void queryHeightLight() throws InvocationTargetException, IllegalAccessException {
batchSaveOrUpdate();
List<String> hlFieldsList = new ArrayList<>();
hlFieldsList.add("title");
hlFieldsList.add("content");
SolrResultInfo<Article> resultInfo = service.queryHeightLight(Article.class, "中国人", hlFieldsList);
Long total = resultInfo.getTotal();
System.out.println("total:" + total);
if (total > 0) {
for (Article article : resultInfo.getList()) {
System.out.println(article);
}
}
}
@Test
public void query() throws InvocationTargetException, IllegalAccessException {
batchSaveOrUpdate();
saveOrUpdate();
// 高亮设置
List<String> hlFieldsList = new ArrayList<>();
hlFieldsList.add("title");
hlFieldsList.add("content");
// 关键字
Criteria criteria = new Criteria("keywords").is("中国");
// 过滤条件
List<SimpleFilterQuery> fqList = new ArrayList<>();
SimpleFilterQuery fq1 = new SimpleFilterQuery(new Criteria("content_id").between(70, 150));
SimpleFilterQuery fq2 = new SimpleFilterQuery(new Criteria("type").is(1));
SimpleFilterQuery fq3 = new SimpleFilterQuery(new Criteria("content").contains("测试"));
fqList.add(fq1);
fqList.add(fq2);
fqList.add(fq3);
// 执行查询
SolrResultInfo<Article> resultInfo = service.query(Article.class, criteria, hlFieldsList, fqList, (1 - 1) * 5L, 5);
Long total = resultInfo.getTotal();
System.out.println("total:" + total);
if (total > 0) {
for (Article article : resultInfo.getList()) {
System.out.println(article);
}
}
}
@Test
void getById() {
saveOrUpdate();
Article bean = service.getById("1", Article.class);
System.out.println(bean);
Assert.notNull(bean, "异常:查询结果为空!");
}
}
5. 完整示例代码
源码:solr-springdata-example01-src.zip
方式2:Spring + Spring Data Solr
1. 添加pom.xml依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.18.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-solr</artifactId>
<version>4.0.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.solr</groupId>
<artifactId>solr-solrj</artifactId>
<version>7.7.2</version>
</dependency>
2. Solr配置**
单机(application-solr.xml) ```xml <?xml version=”1.0” encoding=”UTF-8”?>
- **Solr配置(solr.properties)**
spring.data.solr.core=db_sync
- **集群(application-solrcloud.xml)**
```xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 集群版solr服务配置 -->
<bean id="emptyBuilder" class="org.apache.solr.client.solrj.impl.CloudSolrClient.Builder"/>
<bean id="builder" factory-bean="emptyBuilder" factory-method="withZkHost">
<constructor-arg value="192.168.0.101:2181,192.168.0.102:2181,192.168.0.103:2181" type="java.lang.String"/>
</bean>
<bean id="cloudSolrServer" factory-bean="builder" factory-method="build">
<property name="defaultCollection" value="db_sync"/>
</bean>
<bean id="solrTemplate" class="org.springframework.data.solr.core.SolrTemplate">
<constructor-arg ref="cloudSolrServer"/>
</bean>
<bean id="solrTemplateService" class="com.lonton.bigdata.service.impl.SolrTemplateServiceImpl"/>
</beans>
注意:由于CloudSolrClient是以非静态工程方法实例化的一个对象。所以我们不能用Spring构造实例。只能用Spring的非静态工程实例化Solr。
3. 编写关键类
■ 配置类(Article.java)
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.solr.client.solrj.beans.Field;
import org.springframework.data.solr.core.mapping.SolrDocument;
import java.io.Serializable;
/**
* Description VO-文章类
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@SolrDocument(solrCoreName = "db_sync")
public class Article implements Serializable {
// 必须实现可序列化接口,如果要在网络上传输
@Field("id")
private Integer id;
@Field("content_id")
private Long contentId;
@Field("title")
private String title;
@Field("content")
private String content;
@Field("type")
private Integer type;
@Field("create_at")
private Long createAt;
@Field("publish_at")
private Long publishAt;
@Override
public String toString() {
return "Article{" +
"id=" + id +
", contentId=" + contentId +
", title='" + title + '\'' +
", content='" + content + '\'' +
", type=" + type +
", createAt=" + createAt +
", publishAt=" + publishAt +
'}';
}
}
■ 服务接口(SolrTemplateService.java)
import org.springframework.data.solr.core.query.Criteria;
import org.springframework.data.solr.core.query.SimpleFilterQuery;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.Map;
public interface SolrTemplateService {
/**
* 将JavaBean对象添加(新增或修改)到Solr中
*
* @param bean VO/DTO/Pojo/Domain对象
* @param <T> 泛型(对象类型)
* @return
*/
<T> boolean saveOrUpdate(T bean);
/**
* 将Map对象添加(新增或修改)到Solr中
*
* @param map Map对象
* @param clz JavaBean的Class对象
* @param <T> 泛型(对象类型)
* @return
*/
<T> boolean saveOrUpdate(Map map, Class<T> clz);
/**
* 通过文档ID删除Solr中的文档
*
* @param id 索引ID
* @return
*/
boolean deleteById(String id);
/**
* 通过查询删除Solr中对应的数据集合
*
* @param query 查询条件
*/
boolean deleteByQuery(String query);
/**
* 批量新增或更新记录
*
* @param entities 新增或更新对象列表
* @param <T> 泛型
* @return
*/
<T> boolean batchSaveOrUpdate(List<T> entities);
/**
* 分页查询
*
* @param clz 泛型类对应java.lang.Class
* @param offset 开始索引(默认:0)
* @param rows 每页显示记录数(默认:10)
* @return
*/
<T> SolrResultInfo<T> queryForPage(Class<T> clz, Long offset, Integer rows);
/**
* 高亮查询
*
* @param clz 泛型类对应java.lang.Class
* @param keywords 关键字
* @param hlFieldsList 高亮字段列表
* @param <T> 泛型
* @return
*/
<T> SolrResultInfo<T> queryHeightLight(Class<T> clz, String keywords, List<String> hlFieldsList) throws InvocationTargetException, IllegalAccessException;
/**
* 通过泛型获取Solr中的对象集合
*
* @param clz 泛型类对应java.lang.Class
* @param criteria 查询条件
* @param hlFieldsList 高亮显示数据域名称,是List<String>集合
* @param fqList 过滤条件
* @param offset 开始索引(默认:0)
* @param rows 每页显示记录数(默认:10)
* @return
*/
<T> SolrResultInfo<T> query(Class<T> clz, Criteria criteria, List<String> hlFieldsList, List<SimpleFilterQuery> fqList, Long offset, Integer rows) throws InvocationTargetException, IllegalAccessException;
/**
* 根据ID获取记录
*
* @param id 索引ID
* @param clz Class对象
* @param <T> 泛型
* @return
*/
<T> T getById(String id, Class<T> clz);
}
■ 服务接口实现类(SolrTemplateServiceImpl.java)
import com.lonton.bigdata.service.SolrResultInfo;
import com.lonton.bigdata.service.SolrTemplateService;
import com.lonton.bigdata.util.CommonUtils;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.solr.client.solrj.response.UpdateResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.data.domain.Page;
import org.springframework.data.solr.core.SolrTemplate;
import org.springframework.data.solr.core.query.*;
import org.springframework.data.solr.core.query.result.HighlightEntry;
import org.springframework.data.solr.core.query.result.HighlightPage;
import org.springframework.data.solr.core.query.result.ScoredPage;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@Service
@PropertySource("classpath:solr.properties")
public class SolrTemplateServiceImpl implements SolrTemplateService, InitializingBean {
private static final Logger log = LoggerFactory.getLogger(SolrTemplateServiceImpl.class);
private String solrCore;
public String getSolrCore() {
return solrCore;
}
public void setSolrCore(String solrCore) {
this.solrCore = solrCore;
}
@Resource
private Environment environment;
@Autowired
private SolrTemplate solrTemplate;
/**
* 将JavaBean对象添加(新增或修改)到Solr中
*
* @param bean VO/DTO/Pojo/Domain对象
* @param <T> 泛型(对象类型)
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public <T> boolean saveOrUpdate(T bean) {
try {
// 将对象存储到索引库中
UpdateResponse resp = solrTemplate.saveBean(solrCore, bean);
// 提交
solrTemplate.commit(solrCore);
if (resp.getStatus() == 0) {
log.debug("saveOrUpdate successfully!");
return true;
}
} catch (Exception e) {
log.error(e.getMessage());
e.printStackTrace();
}
log.debug("saveOrUpdate failure!");
return false;
}
/**
* 将Map对象添加(新增或修改)到Solr中
*
* @param map Map对象
* @param clz JavaBean的Class对象
* @param <T> 泛型(对象类型)
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public <T> boolean saveOrUpdate(Map map, Class<T> clz) {
try {
T bean = CommonUtils.mapToBean((Map<String, ?>) map, clz);
// 将对象存储到索引库中
UpdateResponse resp = solrTemplate.saveBean(solrCore, bean);
// 提交
solrTemplate.commit(solrCore);
if (resp.getStatus() == 0) {
log.debug("saveOrUpdate successfully!");
return true;
}
} catch (Exception e) {
log.error(e.getMessage());
e.printStackTrace();
}
log.debug("saveOrUpdate failure!");
return false;
}
/**
* 通过文档ID删除Solr中的文档
*
* @param id 索引ID
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteById(String id) {
try {
UpdateResponse resp = solrTemplate.deleteByIds(solrCore, id);
solrTemplate.commit(solrCore);
if (resp.getStatus() == 0) {
log.debug("delete successfully!");
return true;
}
} catch (Exception e) {
log.error(e.getMessage());
e.printStackTrace();
}
log.debug("delete failure!");
return false;
}
/**
* 通过查询删除Solr中对应的数据集合
*
* @param query 查询条件
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteByQuery(String query) {
try {
UpdateResponse resp = solrTemplate.delete(solrCore, new SimpleQuery(query));
solrTemplate.commit(solrCore);
if (resp.getStatus() == 0) {
log.debug("delete successfully!");
return true;
}
} catch (Exception e) {
log.error(e.getMessage());
e.printStackTrace();
}
log.debug("delete failure!");
return false;
}
/**
* 批量新增或更新记录
*
* @param entities 新增或更新对象列表
* @param <T> 泛型
* @return
*/
@Override
public <T> boolean batchSaveOrUpdate(List<T> entities) {
try {
// 将对象存储到索引库中
UpdateResponse resp = solrTemplate.saveBeans(solrCore, entities);
// 提交
solrTemplate.commit(solrCore);
if (resp.getStatus() == 0) {
log.debug("batchSaveOrUpdate successfully!");
return true;
}
} catch (Exception e) {
log.error(e.getMessage());
e.printStackTrace();
}
log.debug("batchSaveOrUpdate failure!");
return false;
}
/**
* 分页查询
*
* @param clz 泛型类对应java.lang.Class
* @param offset 开始索引(默认:0)
* @param rows 每页显示记录数(默认:10)
* @return
*/
@Override
public <T> SolrResultInfo<T> queryForPage(Class<T> clz, Long offset, Integer rows) {
// 定义返回自定义数据结构对象
SolrResultInfo<T> resultInfo = new SolrResultInfo<T>();
Query query = new SimpleQuery("*:*");
query.setOffset(offset); // 开始索引(默认 0)
query.setRows(rows); // 每页记录数(默认 10)
ScoredPage<T> page = solrTemplate.queryForPage(solrCore, query, clz);
resultInfo.setTotal(page.getTotalElements());
resultInfo.setList(page.getContent());
return resultInfo;
}
/**
* 高亮查询
*
* @param clz 泛型类对应java.lang.Class
* @param keywords 关键字
* @param hlFieldsList 高亮字段列表
* @param <T> 泛型
* @return
*/
@Override
public <T> SolrResultInfo<T> queryHeightLight(Class<T> clz, String keywords, List<String> hlFieldsList) throws InvocationTargetException, IllegalAccessException {
// 定义返回自定义数据结构对象
SolrResultInfo<T> resultInfo = new SolrResultInfo<T>();
// 封装高亮条件
HighlightQuery q = new SimpleHighlightQuery();
HighlightOptions highlightOptions = new HighlightOptions();
// 高亮显示字段
for (String s : hlFieldsList) {
highlightOptions.addField(s);
}
// 高亮样式
highlightOptions.setSimplePrefix("<em style='color: red'>");
highlightOptions.setSimplePostfix("</em>");
q.setHighlightOptions(highlightOptions);
Criteria criteria = new Criteria("keywords").is(keywords);
q.addCriteria(criteria);
HighlightPage<T> pages = solrTemplate.queryForHighlightPage(solrCore, q, clz);
// *********************** 设置高亮结果 ***********************
for (HighlightEntry<T> h : pages.getHighlighted()) { // 循环高亮入口集合
T t = h.getEntity(); // 获取原实体类
if (h.getHighlights().size() > 0 && h.getHighlights().get(0).getSnipplets().size() > 0) {
for (HighlightEntry.Highlight highlight : h.getHighlights()) {
for (String value : highlight.getSnipplets()) {
// 替换高亮部分的内容
BeanUtils.setProperty(t, highlight.getField().getName(), value);
}
}
}
}
resultInfo.setTotal(pages.getTotalElements());
resultInfo.setList(pages.getContent());
return resultInfo;
}
/**
* 通过泛型获取Solr中的对象集合
*
* @param clz 泛型类对应java.lang.Class
* @param criteria 查询条件
* @param hlFieldsList 高亮显示数据域名称,是List<String>集合
* @param fqList 过滤条件
* @param offset 开始索引(默认:0)
* @param rows 每页显示记录数(默认:10)
* @return
*/
@Override
public <T> SolrResultInfo<T> query(Class<T> clz, Criteria criteria, List<String> hlFieldsList, List<SimpleFilterQuery> fqList, Long offset, Integer rows) throws InvocationTargetException, IllegalAccessException {
// 定义返回自定义数据结构对象
SolrResultInfo<T> resultInfo = new SolrResultInfo<>();
// 封装高亮条件
HighlightQuery q = new SimpleHighlightQuery();
// 过滤条件
for (SimpleFilterQuery simpleFilterQuery : fqList) {
q.addFilterQuery(simpleFilterQuery);
}
// 分页查询
q.setOffset(offset);
q.setRows(rows);
// 高亮显示字段
HighlightOptions highlightOptions = new HighlightOptions();
for (String s : hlFieldsList) {
highlightOptions.addField(s);
}
// 高亮样式
highlightOptions.setSimplePrefix("<em style='color: red'>");
highlightOptions.setSimplePostfix("</em>");
q.setHighlightOptions(highlightOptions);
q.addCriteria(criteria);
HighlightPage<T> pages = solrTemplate.queryForHighlightPage(solrCore, q, clz);
// *********************** 设置高亮结果 ***********************
for (HighlightEntry<T> h : pages.getHighlighted()) { // 循环高亮入口集合
T t = h.getEntity(); // 获取原实体类
if (h.getHighlights().size() > 0 && h.getHighlights().get(0).getSnipplets().size() > 0) {
for (HighlightEntry.Highlight highlight : h.getHighlights()) {
for (String value : highlight.getSnipplets()) {
// 替换高亮部分的内容
BeanUtils.setProperty(t, highlight.getField().getName(), value);
}
}
}
}
resultInfo.setTotal(pages.getTotalElements());
resultInfo.setList(pages.getContent());
return resultInfo;
}
private <T> SolrResultInfo<T> queryNoHeightLight(Class<T> clz, String query, Criteria criteria, List<String> hlFieldsList, Long offset, Integer rows) {
// 定义返回自定义数据结构对象
SolrResultInfo<T> resultInfo = new SolrResultInfo<>();
// 查询条件构造
Query q = new SimpleQuery(query);
// 分页查询
q.setOffset(offset); // 开始索引(默认 0)
q.setRows(rows); // 每页记录数(默认 10)
// 条件查询
q.addCriteria(criteria);
Page<T> pages = solrTemplate.query(solrCore, q, clz);
resultInfo.setTotal(pages.getTotalElements());
resultInfo.setList(pages.getContent());
return resultInfo;
}
/**
* 根据ID获取记录
*
* @param id 索引ID
* @param clz Class对象
* @param <T> 泛型
* @return
*/
@Override
public <T> T getById(String id, Class<T> clz) {
Optional<T> bean = solrTemplate.getById(solrCore, id, clz);
return bean.isPresent() ? bean.get() : null;
}
@Override
public void afterPropertiesSet() throws Exception {
setSolrCore(environment.getRequiredProperty("spring.data.solr.core"));
}
}
import java.util.List;
/**
* Description 查询Solr返回的对象,对象类型为T的集合,还包含Solr中符合条件记录总数
*/
public class SolrResultInfo<T> {
private List<T> list = null;
private Long total = null;
public List<T> getList() {
return list;
}
public void setList(List<T> list) {
this.list = list;
}
public Long getTotal() {
return total;
}
public void setTotal(Long total) {
this.total = total;
}
}
■ 工具类(CommonUtils.java)
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.apache.commons.beanutils.BeanUtils;
import org.springframework.cglib.beans.BeanMap;
import java.util.List;
import java.util.Map;
/**
* Description 工具类(Map与JavaBean互转)
*/
public class CommonUtils {
/**
* 把一个Map转换成指定类型的JavaBean对象
*
* @param map Map对象
* @param clazz Class对象
* @param <T> 泛型
* @return
*/
public static <T> T mapToBean(Map<String, ?> map, Class<T> clazz) {
try {
T bean = clazz.newInstance();
BeanUtils.populate(bean, map);
return bean;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 将List<Map<String,Object>>转换为List<T>
*
* @param maps Map对象列表
* @param clazz Class对象
* @return
* @throws InstantiationException
* @throws IllegalAccessException
*/
public static <T> List<T> mapsToBeans(List<Map<String, Object>> maps, Class<T> clazz)
throws InstantiationException, IllegalAccessException {
List<T> list = Lists.newArrayList();
if (maps != null && maps.size() > 0) {
Map<String, Object> map;
T bean;
for (int i = 0, size = maps.size(); i < size; i++) {
map = maps.get(i);
bean = clazz.newInstance();
list.add((T) mapToBean(map, bean.getClass()));
}
}
return list;
}
/**
* 将对象装换为map
*
* @param bean
* @return
*/
public static <T> Map<String, Object> beanToMap(T bean) {
Map<String, Object> map = Maps.newHashMap();
if (bean != null) {
BeanMap beanMap = BeanMap.create(bean);
for (Object key : beanMap.keySet()) {
map.put(key + "", beanMap.get(key));
}
}
return map;
}
/**
* 将List<T>转换为List<Map<String, Object>>
*
* @param beanList
* @return
*/
public static <T> List<Map<String, Object>> beansToMap(List<T> beanList) {
List<Map<String, Object>> list = Lists.newArrayList();
if (beanList != null && beanList.size() > 0) {
Map<String, Object> map;
T bean;
for (int i = 0, size = beanList.size(); i < size; i++) {
bean = beanList.get(i);
map = beanToMap(bean);
list.add(map);
}
}
return list;
}
}
4. 测试
■ 测试类(SolrTemplateServiceTests.java)
import com.lonton.bigdata.vo.Article;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.solr.core.query.Criteria;
import org.springframework.data.solr.core.query.SimpleFilterQuery;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.util.Assert;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RunWith(SpringJUnit4ClassRunner.class)
// 加载集群配置
@ContextConfiguration(locations = {"classpath*:application.xml", "classpath*:application-solrcloud.xml"})
// 加载单机配置
//@ContextConfiguration(locations = {"classpath*:application.xml", "classpath*:application-solr.xml"})
public class SolrTemplateServiceTests {
@Autowired
private SolrTemplateService service;
@Test
public void saveOrUpdate() {
Article bean = new Article();
bean.setId(1);
bean.setContentId(99L);
bean.setTitle("测试-SolrTemplateService.saveOrUpdate");
bean.setContent("测试-作为一个中国人,我感到很自豪");
bean.setType(1);
bean.setCreateAt(1578912619999L);
bean.setPublishAt(1578912619999L);
assert service.saveOrUpdate(bean);
}
@Test
public void saveOrUpdateByMap() {
Map<String, Object> map = new HashMap<>();
map.put("id", 1);
map.put("contentId", 101L);
map.put("title", "北京");
map.put("content", "saveOrUpdateByMap作为一个中国人,我感到很自豪。");
map.put("type", 1);
map.put("createAt", 1578912614123L);
map.put("publishAt", 1578912614125L);
assert service.saveOrUpdate(map, Article.class);
}
@Test
public void deleteById() {
saveOrUpdate();
assert service.deleteById("1");
}
@Test
public void deleteByQuery() {
saveOrUpdate();
assert service.deleteByQuery("id:[1 TO 12]");
}
@Test
public void deleteAll() {
assert service.deleteByQuery("*:*");
}
@Test
public void batchSaveOrUpdate() {
List<Article> entities = new ArrayList<>();
for (int i = 1; i <= 35; i++) {
entities.add(new Article(i, i * 10L, "我是中国人,打倒小日本" + i, "作为一个中国人,我感到很自豪。", 1, 1578912614123L, 1578912614123L));
}
assert service.batchSaveOrUpdate(entities);
}
@Test
public void queryForPage() {
batchSaveOrUpdate();
SolrResultInfo<Article> resultInfo = service.queryForPage(Article.class, (2 - 1) * 5L, 5);
Long total = resultInfo.getTotal();
System.out.println("total:" + total);
if (total > 0) {
for (Article article : resultInfo.getList()) {
System.out.println(article);
}
}
Assert.isTrue(total.intValue() == 35, "异常:返回数据与实际不符!");
}
@Test
public void queryHeightLight() throws InvocationTargetException, IllegalAccessException {
batchSaveOrUpdate();
List<String> hlFieldsList = new ArrayList<>();
hlFieldsList.add("title");
hlFieldsList.add("content");
SolrResultInfo<Article> resultInfo = service.queryHeightLight(Article.class, "中国人", hlFieldsList);
Long total = resultInfo.getTotal();
System.out.println("total:" + total);
if (total > 0) {
for (Article article : resultInfo.getList()) {
System.out.println(article);
}
}
}
@Test
public void query() throws InvocationTargetException, IllegalAccessException {
batchSaveOrUpdate();
saveOrUpdate();
// 高亮设置
List<String> hlFieldsList = new ArrayList<>();
hlFieldsList.add("title");
hlFieldsList.add("content");
// 关键字
Criteria criteria = new Criteria("keywords").is("中国");
// 过滤条件
List<SimpleFilterQuery> fqList = new ArrayList<>();
SimpleFilterQuery fq1 = new SimpleFilterQuery(new Criteria("content_id").between(70, 150));
SimpleFilterQuery fq2 = new SimpleFilterQuery(new Criteria("type").is(1));
SimpleFilterQuery fq3 = new SimpleFilterQuery(new Criteria("content").contains("测试"));
fqList.add(fq1);
fqList.add(fq2);
fqList.add(fq3);
// 执行查询
SolrResultInfo<Article> resultInfo = service.query(Article.class, criteria, hlFieldsList, fqList, (1 - 1) * 5L, 5);
Long total = resultInfo.getTotal();
System.out.println("total:" + total);
if (total > 0) {
for (Article article : resultInfo.getList()) {
System.out.println(article);
}
}
}
@Test
public void getById() {
saveOrUpdate();
Article bean = service.getById("1", Article.class);
System.out.println(bean);
Assert.notNull(bean, "异常:查询结果为空!");
}
}
5. 完整示例代码
源码:solr-springdata-example02-src.zip
参考
语雀:Spring Data Solr入门
https://www.yuque.com/carve/lhrlki/0d12e69e3bfb541d5262b49699daae30
https://www.yuque.com/itmoon/fzz84n/zso8rp
博客园:Spring Data Solr操作Solr的简单案例
https://www.cnblogs.com/blazeZzz/p/9526178.html
博客园:SpringBoot整合Spring Data Solr
https://www.cnblogs.com/kazetotori/p/8549458.html
博客园:Solr7.x学习(8)-使用spring-data-solr
https://www.cnblogs.com/zhi-leaf/p/11612367.html
CSDN:solr 7.X 与spring-data 3.X整合
https://blog.csdn.net/alistair_chow/article/details/80801402