Broadleaf Commerce 是一個開源的Java電子商務平臺,基於Spring框架開發,提供一個可靠、可擴展的架構,可進行深度的定製和快速開發。
關於Solr
Broadleaf項目中關於商品的搜索使用了嵌入式的Solr服務器,這個從配置文件中可以看出來。
- 項目主頁: http://www.broadleafcommerce.com/
- 示例網站: http://demo.broadleafcommerce.org/
- 示例網站源代碼: https://github.com/BroadleafCommerce/DemoSite
從示例網站源代碼的applicationContext.xml文件中可以看到關於solr的配置:
<bean id="solrEmbedded" class="java.lang.String">
<constructor-arg value="solrhome"/>
</bean>
<bean id="blSearchService" class="org.broadleafcommerce.core.search.service.solr.SolrSearchServiceImpl">
<constructor-arg name="solrServer" ref="${solr.source}" />
<constructor-arg name="reindexServer" ref="${solr.source.reindex}" />
</bean>
<bean id="rebuildIndexJobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="blSearchService" />
<property name="targetMethod" value="rebuildIndex" />
</bean>
<bean id="rebuildIndexTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
<property name="jobDetail" ref="rebuildIndexJobDetail" />
<property name="startDelay" value="${solr.index.start.delay}" />
<property name="repeatInterval" value="${solr.index.repeat.interval}" />
</bean>
<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="rebuildIndexTrigger" />
<!--<ref bean="purgeCartTrigger" />-->
<!--<ref bean="purgeCustomerTrigger" />-->
</list>
</property>
</bean>
資源配置文件在common.properties:
web.defaultPageSize=15
web.maxPageSize=100
solr.source=solrEmbedded
solr.source.reindex=solrEmbedded
solr.index.start.delay=5000
solr.index.repeat.interval=3600000
從上可以看出使用的Solr是嵌入式服務,Solr配置文件(schema.xml和solrconfig.xml)在 https://github.com/BroadleafCommerce/DemoSite/tree/master/site/src/main/resources目錄下。
從源代碼SolrSearchServiceImpl.java中可以看出,一共啓動了兩個Solr服務,分別對應primary和reindex兩個solrcore,primary用於查詢,reindex用於重建索引。
改進搜索引擎
改進目標
本篇文章只是將嵌入式Solr服務換成獨立運行的Solr服務,你還可以更進一步換成SolrCloud集羣。
- 單獨搭建搜素引擎服務器
- 支持增量更新索引
- 支持手動重建索引
設計思路
- 1.修改原系統中的嵌入式搜索引擎爲獨立部署的搜索引擎,安裝方法見下文。
- 2.擴展原系統中的SolrSearchServiceImpl類,添加增加索引的方法。
- 3.修改原來系統中商品的service類(我這裏調用的是LLCatalogServiceImpl,該類是新添加的),在saveProduct方法中添加往搜索引擎添加索引的方法。
- 4.修改原系統中solr相關的配置文件。
- 5.修改原系統中的重建索引的定時任務,以支持手動重建索引。
實現方法
1、搭建獨立運行的solr服務器
你可以參考:Apache Solr介紹及安裝
關鍵在於solr/home的定義,以及在該目錄下創建兩個目錄,分別爲primary和reindex,兩個目錄下的配置文件都一樣,solr/home目錄結構如下:
➜ solrhome-solr tree -L 3
.
├── primary
│ └── conf
│ ├── schema.xml
│ └── solrconfig.xml
├── reindex
│ └── conf
│ ├── schema.xml
│ └── solrconfig.xml
└── solr.xml
4 directories, 5 files
solr.xml文件內容如下:
<?xml version="1.0" encoding="UTF-8" ?>
<solr persistent="true">
<cores defaultCoreName="primary" adminPath="/admin/cores">
<core instanceDir="reindex" name="reindex"/>
<core instanceDir="primary" name="primary"/>
</cores>
</solr>
schema.xml內容和原來的基本一樣,只是添加了一行:
<field name="_version_" type="long" indexed="true" stored="true" multiValued="false"/>
solrconfig.xml內容如下:
<?xml version="1.0" encoding="UTF-8" ?>
<config>
<luceneMatchVersion>4.4</luceneMatchVersion>
<directoryFactory name="DirectoryFactory" class="${solr.directoryFactory:solr.StandardDirectoryFactory}"/>
<schemaFactory class="ClassicIndexSchemaFactory"/>
<updateHandler class="solr.DirectUpdateHandler2">
<autoCommit>
<maxDocs>2</maxDocs>
<maxTime>3000</maxTime>
</autoCommit>
</updateHandler>
<requestHandler name="/get" class="solr.RealTimeGetHandler">
<lst name="defaults">
<str name="omitHeader">true</str>
</lst>
</requestHandler>
<requestHandler name="/replication" class="solr.ReplicationHandler" startup="lazy" />
<requestDispatcher handleSelect="true" >
<requestParsers enableRemoteStreaming="false" multipartUploadLimitInKB="2048" formdataUploadLimitInKB="2048" />
<httpCaching never304="true" />
</requestDispatcher>
<requestHandler name="standard" class="solr.StandardRequestHandler" default="true" />
<requestHandler name="/analysis/field" startup="lazy" class="solr.FieldAnalysisRequestHandler" />
<requestHandler name="/update" class="solr.UpdateRequestHandler" />
<requestHandler name="/update/csv" class="solr.CSVRequestHandler" startup="lazy" />
<requestHandler name="/update/json" class="solr.JsonUpdateRequestHandler" startup="lazy" />
<requestHandler name="/admin/" class="org.apache.solr.handler.admin.AdminHandlers" />
<requestHandler name="/admin/ping" class="solr.PingRequestHandler">
<lst name="invariants">
<str name="q">solrpingquery</str>
</lst>
<lst name="defaults">
<str name="echoParams">all</str>
</lst>
</requestHandler>
<queryResponseWriter name="json" class="solr.JSONResponseWriter">
<str name="content-type">text/plain; charset=UTF-8</str>
</queryResponseWriter>
<!-- config for the admin interface -->
<admin>
<defaultQuery>solr</defaultQuery>
</admin>
</config>
2、擴展SolrSearchServiceImpl類
在core模塊創建org.broadleafcommerce.core.search.service.solr.ExtSolrSearchService接口,該接口定義如下:
package org.broadleafcommerce.core.search.service.solr;
import java.io.IOException;
import org.broadleafcommerce.common.exception.ServiceException;
import org.broadleafcommerce.core.catalog.domain.Product;
import org.broadleafcommerce.core.search.service.SearchService;
public interface ExtSolrSearchService extends SearchService {
public void addProductIndex(Product product) throws ServiceException,
IOException;
}
然後,創建其實現類org.broadleafcommerce.core.search.service.solr.ExtSolrSearchServiceImpl,該實現類定義如下:
package org.broadleafcommerce.core.search.service.solr;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import javax.annotation.Resource;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.solr.client.solrj.SolrServer;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrInputDocument;
import org.broadleafcommerce.common.exception.ServiceException;
import org.broadleafcommerce.common.locale.domain.Locale;
import org.broadleafcommerce.common.util.StopWatch;
import org.broadleafcommerce.common.util.TransactionUtils;
import org.broadleafcommerce.core.catalog.domain.Product;
import org.broadleafcommerce.core.search.domain.Field;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.xml.sax.SAXException;
public class ExtSolrSearchServiceImpl extends SolrSearchServiceImpl implements
ExtSolrSearchService {
private static final Log LOG = LogFactory
.getLog(ExtSolrSearchServiceImpl.class);
@Resource(name = "blSolrIndexService")
protected SolrIndexServiceImpl solrIndexServiceImpl;
public ExtSolrSearchServiceImpl(SolrServer solrServer,
SolrServer reindexServer) {
super(solrServer, reindexServer);
}
public ExtSolrSearchServiceImpl(SolrServer solrServer) {
super(solrServer);
}
public ExtSolrSearchServiceImpl(String solrServer, String reindexServer)
throws IOException, ParserConfigurationException, SAXException {
super(solrServer, reindexServer);
}
public ExtSolrSearchServiceImpl(String solrServer) throws IOException,
ParserConfigurationException, SAXException {
super(solrServer);
}
public void addProductIndex(Product product) throws ServiceException,
IOException {
TransactionStatus status = TransactionUtils.createTransaction(
"saveProduct", TransactionDefinition.PROPAGATION_REQUIRED,
solrIndexServiceImpl.transactionManager, true);
StopWatch s = new StopWatch();
try {
List<Field> fields = fieldDao.readAllProductFields();
List<Locale> locales = solrIndexServiceImpl.getAllLocales();
SolrInputDocument document = solrIndexServiceImpl.buildDocument(
product, fields, locales);
if (LOG.isTraceEnabled()) {
LOG.trace(document);
}
SolrContext.getServer().add(document);
SolrContext.getServer().commit();
TransactionUtils.finalizeTransaction(status,
solrIndexServiceImpl.transactionManager, false);
} catch (SolrServerException e) {
TransactionUtils.finalizeTransaction(status,
solrIndexServiceImpl.transactionManager, true);
throw new ServiceException("Could not rebuild index", e);
} catch (IOException e) {
TransactionUtils.finalizeTransaction(status,
solrIndexServiceImpl.transactionManager, true);
throw new ServiceException("Could not rebuild index", e);
} catch (RuntimeException e) {
TransactionUtils.finalizeTransaction(status,
solrIndexServiceImpl.transactionManager, true);
throw e;
}
LOG.info(String.format("Finished adding index in %s", s.toLapString()));
}
protected List<Product> getProducts(QueryResponse response) {
final List<Long> productIds = new ArrayList<Long>();
SolrDocumentList docs = response.getResults();
for (SolrDocument doc : docs) {
productIds
.add((Long) doc.getFieldValue(shs.getProductIdFieldName()));
}
/**
* TODO 請添加緩存相關代碼
*/
List<Product> products = productDao.readProductsByIds(productIds);
// We have to sort the products list by the order of the productIds list
// to maintain sortability in the UI
if (products != null) {
Collections.sort(products, new Comparator<Product>() {
public int compare(Product o1, Product o2) {
return new Integer(productIds.indexOf(o1.getId()))
.compareTo(productIds.indexOf(o2.getId()));
}
});
}
return products;
}
}
3、 修改solr相關配置文件
a. 刪除web模塊中/web/src/main/webapp/WEB-INF/applicationContext.xml的以下代碼:
<bean id="blSearchService" class="org.broadleafcommerce.core.search.service.solr.SolrSearchServiceImpl">
<constructor-arg name="solrServer" ref="${solr.source}" />
<constructor-arg name="reindexServer" ref="${solr.source.reindex}" />
</bean>
b.刪除web模塊中web/src/main/resources/runtime-properties/common.properties的以下代碼:
solr.source=solrEmbedded
solr.source.reindex=solrEmbedded
c. 在core模塊中core/src/main/resources/applicationContext.xml添加如下代碼:
<bean id="solrServer" class="org.apache.solr.client.solrj.impl.HttpSolrServer">
<constructor-arg value="${solr.url}" />
</bean>
<bean id="solrReindexServer" class="org.apache.solr.client.solrj.impl.HttpSolrServer">
<constructor-arg value="${solr.url.reindex}" />
</bean>
<bean id="blSearchService"
class="org.broadleafcommerce.core.search.service.solr.ExtSolrSearchServiceImpl">
<constructor-arg name="solrServer" ref="${solr.source}" />
<constructor-arg name="reindexServer" ref="${solr.source.reindex}" />
</bean>
d. 在core模塊中core/src/main/resources/runtime-properties/common-shared.properties添加如下代碼:
solr.url=http://localhost:8080/solr
solr.url.reindex=http://localhost:8080/solr/reindex
solr.source=solrServer
solr.source.reindex=solrReindexServer
4、 修改LLCatalogServiceImpl類
添加如下代碼:
@Resource(name = "blSearchService")
private ExtSolrSearchService extSolrSearchService;
修改該類的saveProduct方法如下:
@Override
@Transactional("blTransactionManager")
public Product saveProduct(Product product) {
Product dbProduct = catalogService.saveProduct(product);
try {
extSolrSearchService.addProductIndex(dbProduct);
} catch (ServiceException e) {
e.printStackTrace();
throw new RuntimeException(e);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
return dbProduct;
}
5、 修改定時任務
a. web系統啓動時候,會查詢數據庫中商品,然後重建索引。該功能在applicationContext.xml中已經定義了定時任務,建議取消該定時任務。去掉以下代碼:
<bean id="rebuildIndexJobDetail"
class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="blSearchService" />
<property name="targetMethod" value="rebuildIndex" />
</bean>
<bean id="rebuildIndexTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean">
<property name="jobDetail" ref="rebuildIndexJobDetail" />
<property name="startDelay" value="${solr.index.start.delay}" />
<property name="repeatInterval" value="${solr.index.repeat.interval}" />
</bean>
<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="rebuildIndexTrigger" />
</list>
</property>
</bean>
b. 編寫main方法,打成jar包,然後編寫shell腳本,用於手動重建索引或者設置定時任務。該類需要獲取一個名稱爲blSearchService的bean,然後調用該bean的rebuildIndex方法,主要代碼如下。
@Resource(name = "blSearchService")
private SearchService extSolrSearchService;
public void doRebuild(){
extSolrSearchService.rebuildIndex();
}