Spring Data Solr 更多例子 详见 Spring Data Solr官方文档
一、引入pom文件依赖、修改配置文件
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-solr</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
在application.yml中配置solr的地址
spring:
data:
solr:
host: http://localhost:8983/solr
二、使用@SolrDocument注解标注solr中的core对应的实体类
/**
* @author jacksparrow414
* @date 2020-05-04
* @description: TODO
*/
@Builder
@Data
@SolrDocument(collection = "customData")
public class SolrSpringUserEntity {
@Id
private String userId;
@Indexed
private String name;
@Indexed
private Long age;
}
说明:
@SolrDocument中的collection代表solr中core的名字,还有一个solrCoreName,不过官方已经废弃了,建议都使用collection
@Id代表customData中的uniqueKey
@Index代表field中的indexed 和 stored等其他选项,前两个默认为true,具体参数可看下Index注解里的内容就知道了
@Data 和@Builder是Lombok的注解,更多关于Lombok的使用,可参见Lombok官方文档
三、继承官方接口(CrudRepository、PageAndSortingRepository),完成基本的CRUD和分页操作,还可以使用@Query注解,进行自定义查询
/**
* @author jacksparrow414
* @date 2020-05-03
* @description: TODO
*/
public interface SolrSpringService extends CrudRepository<SolrSpringUserEntity,String>,
PagingAndSortingRepository<SolrSpringUserEntity,String> {
@Query(value = "userId:*",fields = {"userId","name"})
List<SolrSpringUserEntity> customQueryOne();
@Query(value = "name:* AND age:*",filters = {"name:我的 And age:[* TO 19]"})
List<SolrSpringUserEntity> customQueryTwo();
@Query(value = "*:*",filters = {"name:我的","age:[* TO 19]"})
List<SolrSpringUserEntity> customQueryThree();
}
说明:
官方提供了基本的save、delete和分页操作,如果要根据实际业务场景进行灵活查询,则可以使用@Query注解或者SolrTemplate进行灵活组装查询参数
四、使用SolrTemplate进行查询
package com.example.mybatis.demomybatis.solr;
import com.example.mybatis.demomybatis.entity.SolrSpringUserEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Sort;
import org.springframework.data.solr.core.SolrTemplate;
import org.springframework.data.solr.core.query.SimpleQuery;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author jacksparrow414
* @date 2020-05-04
* @description: TODO
*/
@Service
public class SolrCustomServiceImpl implements SolrCustomService{
@Autowired
SolrTemplate solrTemplate;
@Override
public List<SolrSpringUserEntity> customFindAll() {
// 设置查询
SimpleQuery simpleQuery = new SimpleQuery("*:*");
simpleQuery.addSort(Sort.by("age").ascending());
// 调用solrTemplate方法
Page<SolrSpringUserEntity> query = solrTemplate.query("customData", simpleQuery, SolrSpringUserEntity.class);
return query.stream().collect(Collectors.toList());
}
@Override
public SolrSpringUserEntity customFindByParams() {
return null;
}
}
SolrTemplate提供了丰富的操作solr中数据的方法,可以按需使用
五、测试
/**
* @author jacksparrow414
* @date 2020-05-04
* @description: TODO
*/
@SpringBootTest()
public class SolrSpringTests {
@Autowired
SolrSpringService solrSpringService;
@Autowired
SolrCustomService solrCustomService;
@Test
public void testFindById(){
Optional.ofNullable(solrSpringService.findById("1977362"))
.ifPresent(u-> System.out.println(u.get()));
}
@Test
public void testDeleteById(){
solrSpringService.deleteById("897654");
}
@Test
public void testPage(){
Page<SolrSpringUserEntity> u = solrSpringService
.findAll(PageRequest.of(1, 2));
System.out.println(u.getTotalElements());
System.out.println(u.getTotalPages());
u.forEach(System.out::println);
}
@Test
public void testCustomQueryOne(){
Optional.ofNullable(solrSpringService.customQueryOne())
.filter(CollectionUtil::isNotEmpty)
.ifPresent(u-> u.forEach(System.out::println));
}
@Test
public void testCustomQueryTwo(){
Optional.ofNullable(solrSpringService.customQueryTwo())
.filter(CollectionUtil::isNotEmpty)
.ifPresent(u->u.forEach(System.out::println));
}
@Test
public void testCustomQueryThree(){
Optional.ofNullable(solrSpringService.customQueryThree())
.filter(CollectionUtil::isNotEmpty)
.ifPresent(u->u.forEach(System.out::println));
}
@Test
public void testCustomFindAll(){
Optional.ofNullable(solrCustomService.customFindAll())
.filter(CollectionUtil::isNotEmpty)
.ifPresent(u->u.forEach(System.out::println));
}
}
调用官方分页接口测试:
5-总条数
3-总页数
当前页的两条数据
六、Spring对于solr事务的支持,加入@Tranasctional即可
实际业务场景可能是:
接收前端的JSON数据->对MySQL数据库操作->操作成功,去同步操作Solr
Spring的事务注解同样对solr有效,这点还是不错的,不用再去手动提交,而且还和数据库操作处于一个事务中,数据库和solr中的数据操作都同时保证了ACID
/**
* @author jacksparrow414
* @date 2020-05-04
* @description: TODO
*/
@RestController
@RequestMapping(value = "solr")
public class SolrController {
@Autowired
private SolrSpringService solrSpringService;
@Autowired
private UserService userService;
@Transactional(rollbackFor = Exception.class)
@RequestMapping(value = "solrTransaction")
public void testSaveTransactionByAnnotation(){
SolrSpringUserEntity springUserEntity = SolrSpringUserEntity.builder()
.userId("238938212")
.name("testTransaction")
.age(122L).build();
UserEntity userEntity = new UserEntity();
BeanUtils.copyProperties(springUserEntity,userEntity);
userEntity.setId(123456);
userEntity.setAge(12);
// 入MySQL数据库user表
userService.addUser(userEntity);
// 将数据同步到solr中
solrSpringService.save(springUserEntity);
int wrong = 1/0;
}
}
当最后1/0报错时,整个事务回滚,数据库和solr中的数据都没有发生变化
七、根据Spring Data 拓展自定义的Repository.实际场景中,我们可能既想使用Spring Data提供的内置方法,又想加一下自己自定义的方法,最好用一个接口就可以实现。官方也提供了这样的机制。详情见官方文档这里
首先先自定义一个接口和对应的实现类。注意实现类的名字一定要以Impl结尾
/**
* 自定义的一些solr操作.
* @author jacksparrow414
* @date 2020/12/12
*/
public interface ArticleSolrService {
/**
* 批量删除solr中的数据
* @param ids solr中的ID集合
* @return
*/
boolean deleteSolrDataByIds(Collection<String> ids);
/**
* 根据查询参数查询solr中的数据
* @return
*/
List<ArticleSolrEntity> queryDataByParams(QuerySolrParamDto querySolrParamDto);
}
实现类如下:
/**
* 注意:类名必须以Impl结尾.见<a href="https://docs.spring.io/spring-data/solr/docs/current/reference/html/#repositories.single-repository-behavior">官方文档</a>
* @author jacksparrow414
* @date 2020/12/12
*/
@Service
public class ArticleSolrServiceImpl implements ArticleSolrService {
@Autowired
private SolrTemplate solrTemplate;
@Transactional(rollbackFor = Exception.class)
@Override
public boolean deleteSolrDataByIds(final Collection<String> ids) {
Preconditions.checkArgument(CollectionUtil.isNotEmpty(ids), "collection must not be null");
UpdateResponse response = solrTemplate.deleteByIds("article", ids);
// 记得提交,否则不会生效
solrTemplate.commit("article");
return response.getStatus() == 0;
}
@Override
public List<ArticleSolrEntity> queryDataByParams(final QuerySolrParamDto querySolrParamDto) {
Query simpleQuery = buildSimpleQuery(querySolrParamDto);
Page<ArticleSolrEntity> articles = solrTemplate.queryForPage("article", simpleQuery, ArticleSolrEntity.class);
return articles.getContent();
}
/**
* 通过具体参数构造Query.
* 这里为什么没有根据条件拼接具体的queryString?而是直接使用默认的*:*<br/>
* 根据stackOverFlow上的回答,使用fq效果更好.因为fq可以利用到FilterCache,可以大大提高查询性能
* <a href="https://stackoverflow.com/questions/11627427/solr-query-q-or-filter-query-fq">stackOverFlow关于此问题的地址</a><br/>
*
* 关于Criteria更多使用,可以点进去看下,有很多,这里只使用几种.<br/>
* 通过PageRequest.of(),方法构造分页时,spring data默认的分是从0开始。而一般我们传的参数都是从1开始,所以这里-1<br/>
* 通过Sort指定排序规则
* @param querySolrParamDto 查询参数
* @return SimpleQuery
*/
private SimpleQuery buildSimpleQuery(final QuerySolrParamDto querySolrParamDto) {
Preconditions.checkNotNull(querySolrParamDto.getPageNo(), "pageNo must not be null");
Preconditions.checkNotNull(querySolrParamDto.getPageSize(), "pageSize must not be null");
SimpleQuery result = new SimpleQuery("*:*");
Pageable pageable = PageRequest.of(querySolrParamDto.getPageNo()-1,
querySolrParamDto.getPageSize(),
Sort.by(SearchableArticleSolrDefinition.ID_FIELD).ascending());
result.setPageRequest(pageable);
if (ObjectUtil.isNotNull(querySolrParamDto.getId())) {
Criteria criteria = new Criteria(SearchableArticleSolrDefinition.ID_FIELD);
criteria.is(querySolrParamDto.getId());
result.addFilterQuery(buildSimpleFilterQuery(criteria));
}
if (StrUtil.isNotBlank(querySolrParamDto.getAuthorName())) {
Criteria criteria = new Criteria(SearchableArticleSolrDefinition.AUTHOR_NAME_FIELD);
criteria.contains(querySolrParamDto.getAuthorName());
result.addFilterQuery(buildSimpleFilterQuery(criteria));
}
if (CollectionUtil.isNotEmpty(querySolrParamDto.getSubjectId())) {
Criteria criteria = new Criteria(SearchableArticleSolrDefinition.SUBJECT_ID_FIELD);
criteria.in(querySolrParamDto.getSubjectId());
result.addFilterQuery(buildSimpleFilterQuery(criteria));
}
// solr中的词组查询,用""包裹起来
if (StrUtil.isNotBlank(querySolrParamDto.getContent())) {
StringBuilder phraseQueryString = new StringBuilder(SearchableArticleSolrDefinition.CONTENT_FIELD);
phraseQueryString.append(":").append("\"").append(querySolrParamDto.getContent()).append("\"");
Criteria criteria = new SimpleStringCriteria(phraseQueryString.toString());
result.addFilterQuery(buildSimpleFilterQuery(criteria));
}
return result;
}
/**
* 通过Criteria构建SimpleFilterQuery.
* @param criteria 具体的过滤条件
* @return SimpleFilterQuery
*/
private FilterQuery buildSimpleFilterQuery(final Criteria criteria) {
SimpleFilterQuery result = new SimpleFilterQuery();
result.addCriteria(criteria);
return result;
}
}
再定义一个公共接口,继承官方的接口、分页接口和自定义的接口ArticleSolrService
/**
* 自定义拓展spring data repository.可以定义自己的接口,
* 然后让该接口继承自定义的接口,直接使用该接口,即可达到又使用官方提供的方法又使用自己的自定义方法<br/>
* <a href="https://docs.spring.io/spring-data/solr/docs/current/reference/html/#repositories.custom-implementations">官方文档地址</a>
* @author jacksparrow414
* @date 2020/12/12
*/
public interface ArticleRepository extends CrudRepository<ArticleSolrEntity, String>,
PagingAndSortingRepository<ArticleSolrEntity, String>, ArticleSolrService {
}
接下来就可以使用了。使用示例如下。第一个方法是官方的,后面的方法是我们自定义的。只引入ArticleRepository这一个接口即可
/**
* @author jacksparrow414
* @date 2020/12/12
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public final class SolrExtendTest {
@Autowired
private ArticleRepository articleRepository;
@Test
public void assertSaveBatchToSolr() {
ArticleSolrEntity first = new ArticleSolrEntity();
first.setId(IdUtil.simpleUUID());
first.setContent("this is first article");
first.setAuthorName("jack");
first.setArticleName("jack article");
first.setDel(0);
first.setSubjectId(Lists.newArrayList(1, 2));
ArticleSolrEntity second = new ArticleSolrEntity();
second.setId(IdUtil.simpleUUID());
second.setContent("this is second article");
second.setAuthorName("mark");
second.setArticleName("mark article");
second.setDel(0);
second.setSubjectId(Lists.newArrayList(2, 3, 4));
articleRepository.saveAll(Lists.newArrayList(first, second));
}
@Test
public void assertDeleteBatchByIds() {
boolean actual = articleRepository.deleteSolrDataByIds(Lists.newArrayList("777777", "23128391283"));
Assert.assertTrue(actual);
}
@Test
public void assertQueryDataByParams() {
QuerySolrParamDto querySolrParamDto = new QuerySolrParamDto();
// querySolrParamDto.setId("98f1d57e708c425bb9c7d1e4f4b35150");
querySolrParamDto.setAuthorName("k");
// querySolrParamDto.setSubjectId(Lists.newArrayList(2));
querySolrParamDto.setContent("this is second");
querySolrParamDto.setPageNo(1);
querySolrParamDto.setPageSize(5);
List<ArticleSolrEntity> actual = articleRepository.queryDataByParams(querySolrParamDto);
Assert.assertTrue(CollectionUtil.isNotEmpty(actual));
}
}
温馨提示:
1、在使用SolrTemplate时,可以将对应的日志级别调成debug模式,会看到最终的查询语句。很方便的可以看到自己是不是写对了
logging:
level:
org.springframework.data: debug
如下图:
2、根据Spring官网最新显示。Spring Data Solr准备于2022年停止维护。另一方面,Spring Data对于Elastaic Search的支持倒是一直都有。也侧面说明了一些情况。solr在新时代的搜索方面确实有些不足了。新生代Elastaic Search势头正猛。不过入门ES之前懂一点solr也没有任何损失,学起来还容易些。因为二者的语法都很相似
最后注意:如果是公司内部大规模使用,建议建立统一的solr使用规范。使得Solr使用规范化,否则很容易出现有的地方在用SolrClient,有的地方在用Spring Data Solr.用的地方在用手写方式拼接查询,有的地方使用Spring Data Solr的Criterial进行构造查询的情况
所有单元测试和其他代码已放到Github上,如有需要,请自取。如有问题,欢迎留言,交流讨论。