首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Spring 整合 Solr 實戰:原生 SolrJ API 構建高性能搜索服務

     編輯:Java 相關知識

架構選型:原生 SolrJ vs Spring Data Solr

Spring Data Solr (`SolrTemplate`) 封裝層較薄,實際項目中常遇到靈活性不足、異常處理困擾。本文採用**原生 SolrJ API** + Spring 管理 `SolrServer` Bean,兼顧框架便利與底層控制。

Solr 集群前置條件

  • ZooKeeper 集群(配置 `solr.zkHost`)
  • SolrCloud 模式(集合 `orderInfo`)
  • Tomcat/Jetty 運行 Solr Webapp
  • JDK 1.8+

Maven 依賴


<dependency>
    <groupId>org.apache.solr</groupId>
    <artifactId>solr-solrj</artifactId>
    <version>4.7.2</version>
</dependency>

Spring Bean 配置:SolrCloudServerFactoryBean


<bean id="orderInfoSolrServer" class="com.xxxx.SolrCloudServerFactoryBean">
    <property name="zkHost" value="${solr.zkHost}"/>
    <property name="defaultCollection" value="orderInfo"/>
    <property name="zkClientTimeout" value="6000"/>
</bean>

<bean id="solrService" class="com.xxxx.SolrServiceImpl">
    <property name="solrServer" ref="orderInfoSolrServer"/>
</bean>

核心工廠 Bean 實現


public class SolrCloudServerFactoryBean implements FactoryBean, InitializingBean {
    
    private CloudSolrServer cloudSolrServer;
    private String zkHost;
    private String defaultCollection;
    private int maxConnections = 1000;
    private int maxConnectionsPerHost = 500;
    private int zkClientTimeout = 10000;
    private int zkConnectTimeout = 10000;
    private final Lock lock = new ReentrantLock();

    @Override
    public SolrServer getObject() throws Exception {
        return cloudSolrServer;
    }

    @Override
    public Class getObjectType() {
        return SolrServer.class;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        // HTTP 客戶端優化配置
        ModifiableSolrParams params = new ModifiableSolrParams();
        params.set(HttpClientUtil.PROP_MAX_CONNECTIONS, maxConnections);
        params.set(HttpClientUtil.PROP_MAX_CONNECTIONS_PER_HOST, maxConnectionsPerHost);
        HttpClient client = HttpClientUtil.createClient(params);
        
        // 負載均衡 HTTP SolrServer
        LBHttpSolrServer lbServer = new LBHttpSolrServer(client);
        
        // 雙檢鎖創建 CloudSolrServer
        lock.lock();
        try {
            if (cloudSolrServer == null) {
                cloudSolrServer = new CloudSolrServer(zkHost, lbServer);
            }
        } finally {
            lock.unlock();
        }
        
        cloudSolrServer.setDefaultCollection(defaultCollection);
        cloudSolrServer.setZkClientTimeout(zkClientTimeout);
        cloudSolrServer.setZkConnectTimeout(zkConnectTimeout);
    }
    // setter/getter 省略
}

關鍵點**:

  • `FactoryBean` + `InitializingBean`:Spring 容器管理生命週期,屬性注入後自動調用 `afterPropertiesSet()`
  • `LBHttpSolrServer`:客戶端負載均衡,高可用
  • 連接池優化:`maxConnections`、`maxConnectionsPerHost` 防止連接耗盡
  • 線程安全雙檢鎖:單例模式創建 `CloudSolrServer`

業務 Service 實戰:索引、查詢、聚合、刪除


@Service("orderInfoSolrService")
public class OrderInfoNativeSolrServiceImpl {
    
    @Resource(name = "orderInfoSolrServer")
    private SolrServer solrServer;
    
    // 1. 單條索引
    public void creatIndex(OrderInfo orderInfo) throws IOException, SolrServerException {
        solrServer.addBean(orderInfo);
        solrServer.commit(); // 提交可見
    }
    
    // 2. 批量索引 (高性能關鍵)
    public void createIndexBatch(List list) throws IOException, SolrServerException {
        solrServer.addBeans(list);
        solrServer.commit();
    }
    
    // 3. 條件構建器:精確/模糊/範圍/排序
    public SolrQuery iniFilter(String orderIdFilter, OrderInfo queryObj, 
                               List timeList, Sort... sorts) throws Exception {
        SolrQuery query = new SolrQuery();
        query.setQuery("validTag:1"); // 固定條件放 q (主查詢),利用 filterCache
        
        StringBuilder fq = new StringBuilder();
        if (orderIdFilter != null) {
            fq.append(orderIdFilter);
            queryObj.setOrderId(null);
        }
        
        // 反射遍歷實體字段,自動構建 fq (過濾查詢)
        for (Field field : queryObj.getClass().getDeclaredFields()) {
            if (field.isAnnotationPresent(org.apache.solr.client.solrj.beans.Field.class)) {
                field.setAccessible(true);
                String name = field.getName();
                String value = String.valueOf(field.get(queryObj));
                if (isValid(value)) {
                    // 會員類型、訂單類型模糊查詢
                    if ("memberId".equals(name) || "orderType".equals(name)) {
                        value = "*" + value + "*";
                    }
                    fq.append(name).append(":").append(value).append(" AND ");
                }
            }
        }
        
        // 時間範圍查詢
        if (timeList != null) {
            for (QueryTime qt : timeList) {
                String start = fmt.format(qt.getBeginTime().getTime());
                String end = fmt.format(qt.getEndTime().getTime());
                fq.append(qt.getFieldName()).append(":[").append(start)
                  .append(" TO ").append(end).append("] AND ");
            }
        }
        
        if (fq.length() > 0) fq.delete(fq.length() - 5, fq.length()); // 移除尾部 " AND "
        query.addFilterQuery(fq.toString()); // 動態條件放 fq,利用 filterCache
        
        if (query.toString().isEmpty()) query.setQuery("*:*");
        return query;
    }
    
    // 4. 分頁查詢 + 聚合統計
    public Page query(Map map, OrderInfo queryObj,
                                 List timeList, Pageable page, Sort... sorts) throws Exception {
        SolrQuery query = iniFilter(null, queryObj, timeList);
        
        // 分頁
        if (page != null) {
            query.setStart(page.getPageNumber() * page.getPageSize());
            query.setRows(page.getPageSize());
        }
        
        // 聚合統計 (StatsComponent)
        query.setGetFieldStatistics("orderPrice", "duePrice", "diffPrice");
        
        QueryResponse response = solrServer.query(query);
        SolrDocumentList docs = response.getResults();
        
        // 文檔轉 Bean
        DocumentObjectBinder binder = new DocumentObjectBinder();
        List result = new ArrayList<>();
        for (SolrDocument doc : docs) {
            result.add(binder.getBean(OrderInfo.class, doc));
        }
        
        // 統計結果提取
        Map stats = response.getFieldStatsInfo();
        map.put("orderPriceSum", getSum(stats, "orderPrice"));
        map.put("duePriceSum", getSum(stats, "duePrice"));
        map.put("diffPriceSum", getSum(stats, "diffPrice"));
        
        return new PageImpl<>(result, page, docs.getNumFound());
    }
    
    // 5. 主鍵精確查詢
    public List queryByOrderId(String id) throws Exception {
        SolrQuery query = new SolrQuery("orderId:" + id);
        QueryResponse response = solrServer.query(query);
        return convert(response.getResults());
    }
    
    // 6. 刪除
    public void deleteBySolrQuery(String solrQuery) throws IOException, SolrServerException {
        solrServer.deleteByQuery(solrQuery);
        solrServer.commit();
    }
    
    private String getSum(Map stats, String field) {
        FieldStatsInfo info = stats.get(field);
        return (info != null && info.getSum() != null) ? info.getSum().toString() : null;
    }
    private List convert(SolrDocumentList docs) { /* ... */ }
}

Solr 查詢優化核心原則

原則說明示例
q vs fq 分離固定高選擇性條件放 `q` (主查詢,走 queryResultCache);高頻變化條件放 `fq` (過濾查詢,走 filterCache,不評分、可緩存)`q=validTag:1`、`fq=memberId:*123* AND createTime:[* TO NOW]`
分詞器選型IK 分詞器適合中文語義;Standard 分詞器按單字切分。模糊查詢 `*關鍵詞*` 需分詞器支持IK: "中國人" → "中國人/中國/國人";Standard: "中/國/人"
定期 optimize段合併清理刪除文檔、提升查詢性能。參數建議 `optimize(false, false, 5)` (不阻塞、不等待、最大 5 段)定時任務每天低峰期執行
批量寫入`addBeans()` 批量提交,單條/多條建索引耗時相近`createIndexBatch(list)` 替代循環 `addBean()`

生產環境檢查清單

  1. ZooKeeper 連接超時 `zkClientTimeout` ≥ 10s,防止網絡抖動誤判
  2. HTTP 連接池 `maxConnections` 根據並發壓測調整
  3. Solr `schema.xml` 字段類型、索引/存儲屬性、分詞器配置正確
  4. 關鍵查詢字段建立 `docValues` (排序/聚合) 或 `indexed=true` (檢索)
  5. 監控:QPS、延遲 P99、錯誤率、JVM GC、ZK 會話狀態
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved