Solr是基于Lucene的全文搜索引擎,服务部署依赖web容器,如tomcat。Solr支持json, xml, csv等数据格式,相比于查询性能更高的ElasticSearch
Solr更适用于传统搜索应用服务。
本篇简述springboot集成solr (单机版,暂不用solrCloud)
准备工作:
-
搭建springboot脚手架并成功运行,可参考历史分享springboot+mybatis
-
启动Solr服务(搭建配置Solr及ES,后续会在运维章节另行讲述)
1. maven添加solr依赖
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-solr</artifactId>
</dependency>
2. solr 配置
2.1 yml
spring:
data:
solr:
host: http://192.168.2.9:8983/solr
2.2 SolrTemplate配置
@Configuration
public class SolrConfig {
@Autowired
private SolrClient solrClient;
@Bean
public SolrTemplate getSolrTemplate(){
return new SolrTemplate(solrClient);
}
}
2.3 SolrDocument
import lombok.Data;
import org.apache.solr.client.solrj.beans.Field;
import org.springframework.data.solr.core.mapping.Indexed;
import org.springframework.data.solr.core.mapping.SolrDocument;
import java.io.Serializable;
import java.util.List;
@SolrDocument(collection = "goods_core")
@Data
public class SearchGoods implements Serializable {
@Indexed
@Field("id")
private String id; // goodsId
@Field
private String name;
@Field
private String detail;
/**
* copyField复值域: name, detail
* <field name="goodsSearchWord" type="text_ik" multiValued="true" indexed="true" stored="true"/>
* <field name="name" type="string" indexed="false" stored="true"/>
* <field name="detail" type="string" indexed="false" stored="true"/>
*
* <copyField source="name" dest="goodsSearchWord" maxChars="256"/>
* <copyField source="detail" dest="goodsSearchWord" maxChars="256"/>
*/
@Field
private List<String> goodsSearchWord;
@Indexed
@Field
private Integer shopId;
@Indexed
@Field
private String position; // latitude,longitude
@Field
private Integer salePrice; // 零售价(分)
@Field
private Integer activePrice; // 活动价(分)
@Field
private Double inventory; // 实时总库存
@Field
private Double saleCount; // 已售数量
/**
* 注意solr中最小的整型为pint, byte及short都要转换成int类型
*/
@Indexed
@Field
private Integer saleType; // 营销类型
@Field
private Long createTime; // 创建时间 单位毫秒
@Field
private Long updateTime; // 更新时间 单位毫秒
}
3. solr 封装
3.1 solr service
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.solr.core.query.HighlightQuery;
import org.springframework.data.solr.core.query.Query;
import org.springframework.data.solr.core.query.result.HighlightPage;
import org.springframework.data.solr.core.query.result.ScoredPage;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
public interface SolrService<T, ID extends Serializable> {
void save(T t, String solrCore);
void save(Collection<T> beans, String solrCore);
T searchById(ID id, String solrCore);
List<T> searchByIds(List<ID> ids, String solrCore);
Page<T> query(Query query, String solrCore);
ScoredPage<T> pageQuery(Query query, Pageable pageable, String solrCore);
HighlightPage<T> queryForHighlightPage(HighlightQuery query, Pageable pageable, String solrCore);
void deleteById(ID id, String solrCore);
void deleteByIds(Collection<String> ids, String solrCore);
void deleteAll(String solrCore);
interface SolrCollection {
String GOODS_CORE = "goods_core";
}
}
3.2 solr abstract serviceImpl
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.solr.core.SolrTemplate;
import org.springframework.data.solr.core.query.HighlightQuery;
import org.springframework.data.solr.core.query.Query;
import org.springframework.data.solr.core.query.SimpleQuery;
import org.springframework.data.solr.core.query.result.HighlightPage;
import org.springframework.data.solr.core.query.result.ScoredPage;
import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.util.Collection;
import java.util.List;
public abstract class SolrServiceImpl<T, ID extends Serializable> implements SolrService<T, ID> {
@Autowired
private SolrTemplate solrTemplate;
private Class<T> clazz;
@SuppressWarnings("unchecked")
public SolrServiceImpl() {
ParameterizedType parameterizedType = ((ParameterizedType) getClass().getGenericSuperclass());
clazz = (Class<T>) parameterizedType.getActualTypeArguments()[0];
}
@Override
public void save(T t, String solrCore) {
solrTemplate.saveBean(solrCore, t);
solrTemplate.commit(solrCore);
}
@Override
public void save(Collection<T> beans, String solrCore) {
if(CollectionUtils.isNotEmpty(beans)){
solrTemplate.saveBeans(solrCore, beans);
// 提交时报 mime type 错误,需要指定solrCore
solrTemplate.commit(solrCore);
}
}
@Override
public T searchById(ID id, String solrCore) {
return solrTemplate.getById(solrCore, id, clazz).orElse(null);
}
@Override
public List<T> searchByIds(List<ID> ids, String solrCore) {
return (List<T>)solrTemplate.getByIds(solrCore, ids, clazz);
}
@Override
public Page<T> query(Query query, String solrCore) {
return solrTemplate.query(solrCore, query, clazz);
}
@Override
public ScoredPage<T> pageQuery(Query query, Pageable pageable, String solrCore) {
this.buildPageAndSort(query, pageable);
return solrTemplate.queryForPage(solrCore, query, clazz);
}
@Override
public HighlightPage<T> queryForHighlightPage(HighlightQuery query, Pageable pageable, String solrCore) {
this.buildPageAndSort(query, pageable);
return solrTemplate.queryForHighlightPage(solrCore, query, clazz);
}
@Override
public void deleteById(ID id, String solrCore) {
solrTemplate.deleteByIds(solrCore, id.toString());
solrTemplate.commit(solrCore);
}
@Override
public void deleteByIds(Collection<String> ids, String solrCore) {
if(CollectionUtils.isNotEmpty(ids)){
solrTemplate.deleteByIds(solrCore, ids);
solrTemplate.commit(solrCore);
}
}
@Override
public void deleteAll(String solrCore) {
Query query = new SimpleQuery("*:*");
solrTemplate.delete(solrCore, query);
solrTemplate.commit(solrCore);
}
private void buildPageAndSort(Query query, Pageable pageable){
query.setOffset(pageable.getOffset()); // 开始索引(默认0) (pageNum-1) * pageSize
query.setRows(pageable.getPageSize()); // 每页记录数
query.addSort(pageable.getSort());
}
}
3.3 search service
可结合使用SolrClient 或 SolrTemplate
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.response.Group;
import org.apache.solr.client.solrj.response.GroupCommand;
import org.apache.solr.client.solrj.response.GroupResponse;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.solr.core.query.Criteria;
import org.springframework.data.solr.core.query.SimpleQuery;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
@Slf4j
@Service
public class SearchGoodsServiceImpl extends SolrServiceImpl<SearchGoods, String> implements SearchGoodsService {
@Autowired
private SolrClient solrClient;
@Override
public void saveGoodsToSolr(Goods goods){
SearchGoods searchGoods = new SearchGoods(goods);
super.save(searchGoods, SolrCollection.GOODS_CORE);
}
@Override
public SearchGoods searchByGoodsId(String goodsId) {
if(StringUtils.isNotEmpty(goodsId)){
return super.searchById(goodsId, SolrCollection.GOODS_CORE);
}
return null;
}
@Override
public List<SearchGoods> list(GeoSearchRequest request, Byte saleType) throws Exception {
SolrQuery solrQuery = new SolrQuery("*:*");
if(saleType != null && saleType > 0){
solrQuery.addFilterQuery("saleType:" + saleType);
}
if(!StringUtils.isAnyEmpty(request.getLatitude(), request.getLongitude())){
SolrQueryUtil.buildGeoQuery(solrQuery, request);
}
// 按店铺分组
SolrQueryUtil.buildGroupQuery(solrQuery);
// 排序规则
solrQuery.addSort("activePrice", SolrQuery.ORDER.asc);
solrQuery.addSort("salePrice", SolrQuery.ORDER.asc);
solrQuery.addSort("updateTime", SolrQuery.ORDER.desc);
solrQuery.addSort("saleCount", SolrQuery.ORDER.desc);
solrQuery.addSort("id", SolrQuery.ORDER.desc);
// 分页查询
SolrQueryUtil.buildPageQuery(solrQuery, request);
QueryResponse response = solrClient.query(SolrCollection.GOODS_CORE, solrQuery);
List<List<SearchGoods>> goodsGroupList = this.parseGoodsGroupDocumentsByGroup(response);
return GroupList.stream().flatMap(Collection::stream).collect(Collectors.toList());
}
/**
* 分组聚合文档解析
* @param response
* @return
*/
private List<List<SearchGoods>> parseGoodsGroupDocumentsByGroup(QueryResponse response){
List<List<SearchGoods>> goodsList = new ArrayList<>();
// 注意:聚合分组后,此处不能再用response.getResults()接收结果
GroupResponse groupResponse = response.getGroupResponse();
List<GroupCommand> commands = groupResponse.getValues();
if(commands != null) {
for(GroupCommand command : commands) {
for(Group group : command.getValues()) {
SolrDocumentList solrDocuments = group.getResult();
List<SearchGoods> searchGoodsList = this.parseGoodsDocuments(solrDocuments);
if(CollectionUtils.isNotEmpty(searchGoodsList)){
goodsList.add(searchGoodsList);
}
}
}
}
return goodsList;
}
/**
* 搜索文档解析
* @param solrDocuments
* @return
*/
private List<SearchGoods> parseGoodsDocuments(SolrDocumentList solrDocuments){
List<SearchGoods> goodsList = new ArrayList<>();
for(SolrDocument doc : solrDocuments) {
SearchGoods searchGoods = new SearchGoods();
searchGoods.setId((String)doc.getFieldValue("id"));
searchGoods.setName((String)doc.getFieldValue("name"));
searchGoods.setDetail((String)doc.getFieldValue("detail"));
searchGoods.setShopId((Integer) doc.getFieldValue("shopId"));
searchGoods.setSalePrice((Integer) doc.getFieldValue("salePrice"));
searchGoods.setActivePrice((Integer) doc.getFieldValue("activePrice"));
searchGoods.setInventory((Double) doc.getFieldValue("inventory"));
searchGoods.setSaleCount((Double) doc.getFieldValue("saleCount"));
searchGoods.setSaleType((Integer)doc.getFieldValue("saleType"));
searchGoods.setCreateTime((Long) doc.getFieldValue("createTime"));
searchGoods.setUpdateTime((Long) doc.getFieldValue("updateTime"));
goodsList.add(searchGoods);
}
return goodsList;
}
}
3.4 solr query 工具类
public final class SolrQueryUtil {
/**
* 构建基于LBS搜索条件
* @param solrParam
* @param request
* @return
*/
public static SolrQuery buildGeoQuery(SolrQuery solrParam, GeoClassifySearchRequest request){
// 基于LBS搜索
if(StringUtils.isAnyEmpty(request.getLatitude(), request.getLongitude())){
throw new CommonException("未提供当前地理位置经纬度,无法搜索");
}
solrParam.addFilterQuery("{!geofilt}"); // 距离过滤函数
solrParam.set("pt", request.getLatitude() + "," + request.getLongitude()); // 当前纬度,经度
solrParam.set("sfield", "position"); // 经纬度的字段
solrParam.set("d", request.getDistance()); // 就近 d km的所有数据
solrParam.set("score", "distance"); // 距离
solrParam.addSort("geodist()", SolrQuery.ORDER.asc); // 根据距离排序:由近到远
solrParam.set("fl", "*,_dist_:geodist(),score"); // 查询的结果中添加距离和score
return solrParam;
}
/**
* 构建全量聚合搜索条件
* @param solrParam
* @return
*/
public static SolrQuery buildGroupQuery(SolrQuery solrParam){
// 聚合搜索
solrParam.set("group", true); // 是否分组
solrParam.set("group.field", "shopId"); // 分组的域
solrParam.set("group.limit", "1"); // 每组显示的个数,默认为1
solrParam.set("group.ngroups", true); // 是否计算所得分组个数;注意:当每个分组显示数目大于1个时,不能用分组数量来计算总页码
solrParam.addSort("activePrice", SolrQuery.ORDER.asc);
solrParam.addSort("salePrice", SolrQuery.ORDER.asc);
return solrParam;
}
/**
* 构建分页条件
* @param solrParam
* @param request
* @return
*/
public static SolrQuery buildPageQuery(SolrQuery solrParam, PagerRequest request){
solrParam.setStart((request.getPageNum() - 1) * request.getPageSize()); // 起始索引值,默认0
solrParam.setRows(request.getPageSize()); // 显示几条数据
return solrParam;
}
/**
* These characters are part of the query syntax and must be escaped
* @param s
* @return
*/
public static String escapeQueryChars(String s) {
StringBuilder sb = new StringBuilder();
for(int i = 0; i < s.length(); i++){
char c = s.charAt(i);
if(c == '\\' || c == '+' || c == '-' || c == '!' || c == '(' || c == ')' || c == ':'
|| c == '^' || c == '[' || c == ']' || c == '\"' || c == '{' || c == '}' || c == '~'
|| c == '*' || c == '?' || c == '|' || c == '&' || c == ';' || c == '/'
|| Character.isWhitespace(c)){
sb.append('\\');
}
sb.append(c);
}
return sb.toString();
}
}