Spring Data Solr (`SolrTemplate`) 封裝層較薄,實際項目中常遇到靈活性不足、異常處理困擾。本文採用**原生 SolrJ API** + Spring 管理 `SolrServer` Bean,兼顧框架便利與底層控制。
<dependency>
<groupId>org.apache.solr</groupId>
<artifactId>solr-solrj</artifactId>
<version>4.7.2</version>
</dependency>
<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>
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 省略
}
關鍵點**:
@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) { /* ... */ }
}
| 原則 | 說明 | 示例 |
|---|---|---|
| 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()` |