Solr是基于Lucene的全文搜索引擎,服务部署依赖web容器,如tomcat。Solr支持json, xml, csv等数据格式,相比于查询性能更高的ElasticSearch
Solr更适用于传统搜索应用服务。
本篇简述springboot集成solr (单机版,暂不用solrCloud)

准备工作:
搭建springboot脚手架并成功运行,可参考历史分享springboot+mybatis
启动Solr服务(搭建配置Solr及ES,后续会在运维章节另行讲述)
<dependency> <groupId>org.springframework.datagroupId> <artifactId>spring-data-solrartifactId>dependency>
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")@Datapublic 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.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 beans, String solrCore); T searchById(ID id, String solrCore); List<T> searchByIds(List<ID> ids, String solrCore); Page query(Query query, String solrCore); ScoredPage pageQuery(Query query, Pageable pageable, String solrCore); HighlightPage 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, ID extends Serializable> implements SolrService, ID> { @Autowired private SolrTemplate solrTemplate; private Class 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 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 searchByIds(List ids, String solrCore ) { return (List<T>)solrTemplate.getByIds(solrCore, ids, clazz); } @Override public Page query(Query query, String solrCore) { return solrTemplate.query(solrCore, query, clazz); } @Override public ScoredPage pageQuery(Query query, Pageable pageable, String solrCore) { this.buildPageAndSort(query, pageable); return solrTemplate.queryForPage(solrCore, query, clazz); } @Override public HighlightPage 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@Servicepublic class SearchGoodsServiceImpl extends SolrServiceImpl, 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> 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 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(); }}