springboot + solr

Solr是基于Lucene的全文搜索引擎,服务部署依赖web容器,如tomcat。Solr支持json, xml, csv等数据格式,相比于查询性能更高的ElasticSearch

Solr更适用于传统搜索应用服务。

本篇简述springboot集成solr (单机版,暂不用solrCloud)

准备工作:

  1. 搭建springboot脚手架并成功运行,可参考历史分享springboot+mybatis

  2. 启动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")@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. 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@Servicepublic 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();    }}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值