Solr Facet技术的应用与研究
問題背景
在《搜索引擎關鍵字智能提示的一種實現》一文中介紹過,美團的CRM系統負責管理銷售人員的門店(POI)和項目(DEAL)信息,提供統一的檢索功能,其索引層采用的是SolrCloud。在用戶搜索時,如果能直觀地給出每個品類的POI數目,各個狀態的DEAL數目,可以更好地引導用戶進行搜索,進而提升搜索體驗。
需求分析
例如,下圖是用戶搜索項目(DEAL)的界面,當選中一個人或者組織節點后,需要實時顯示狀態分組和快捷分組的每個項的DEAL數目:
為了實現上述導航效果,可以采用以下兩個方案:
方案一, 針對每個導航項發送一個Ajax請求,去Solr服務器查詢對應的DEAL數目。該方案問題在于,當導航項比較多時,擴展性不好。方案二, 應用Solr自帶的Facet技術實現以導航為目的的搜索,查詢結果根據分類添加count信息。DEAL的Solr索引設計如下:
schema.xml: <field name="deal_id" type="int" indexed="true" stored="true" /> //deal id <field name="title" type="text_ika" indexed="true" stored="false" /> //標題 <field name="bd_id" type="int" indexed="true" stored="false" /> //負責人id <field name="begin_time" type="long" indexed="true" stored="false" /> //項目開始時間 <field name="end_time" type="long" indexed="true" stored="false" /> //項目結束時間 <field name="status" type="int" indexed="true" stored="false" /> //項目狀態 <field name="can_buy" type="boolean" indexed="true" stored="false" /> //是否可以購買 ...省略 本文的例子中用于facet的字段有status,can_buy,begin_time,end_time注: Facet的字段必須被索引,無需分詞,無需存儲。無需分詞是因為該字段的值代表了一個整體概念,無需存儲是因為一般而言用戶所關心的并不是該字段的具體值,而是作為對查詢結果進行分組的一種手段,用戶一般會沿著這個分組進一步深入搜索。
Solr Facet簡介
Facet是Solr的高級搜索功能之一,Solr作者給出的定義是導航(Guided Navigation)、參數化查詢(Paramatic Search)。Facet的主要好處是在搜索的同時,可以按照Facet條件進行分組統計,給出導航信息,改善搜索體驗。Facet搜索主要分為以下幾類:
1. Field Facet 搜索結果按照Facet的字段分組并統計,Facet字段通過在請求中加入”facet.field”參數加以聲明,如果需要對多個字段進行Facet查詢,那么將該參數聲明多次,Facet字段必須被索引。例如,以下表達式是以DEAL的status和can_buy屬性為facet.field進行查詢:
select?q=*:*&facet=true&facet.field=status&facet.field=can_buy&wt=jsonFacet查詢需要在請求參數中加入”facet=on”或者”facet=true”讓Facet組件起作用,返回結果:
"facet_counts”: { "facet_queries": {}, "facet_fields": { "status": [ "32", 96, "0", 40, "8", 81, "16", 50, "127", 80, "64", 27 ] ,"can_buy": [ "true", 236, "false", 21 ]}, "facet_dates": {}, "facet_ranges": {} }分組count信息包含在“facet_fields”中,分別按照”status”和“can_buy”的值分組,比如狀態為32的DEAL數目有96個,能購買的DEAL數目(can_buy=true)是236。
Field Facet主要參數:
facet.field:Facet的字段facet.prefix:Facet字段前綴facet.limit:Facet字段返回條數facet.offset:開始條數,偏移量,它與facet.limit配合使用可以達到分頁的效果facet.mincount:Facet字段最小count,默認為0facet.missing:如果為on或true,那么將統計那些Facet字段值為null的記錄facet.method:取值為enum或fc,默認為fc,fc表示Field Cachefacet.enum.cache.minDf:當facet.method=enum時,參數起作用,文檔內出現某個關鍵字的最少次數2. Date Facet 日期類型的字段在索引中很常見,如DEAL上線時間,線下時間等,某些情況下需要針對這些字段進行Facet。時間字段的取值有無限性,用戶往往關心的不是某個時間點而是某個時間段內的查詢統計結果,Solr為日期字段提供了更為方便的查詢統計方式。字段的類型必須是DateField(或其子類型)。需要注意的是,使用Date Facet時,字段名、起始時間、結束時間、時間間隔這4個參數都必須提供。 與Field Facet類似,Date Facet也可以對多個字段進行Facet。并且針對每個字段都可以單獨設置參數。
3. Facet Query Facet Query利用類似于filter query的語法提供了更為靈活的Facet。通過facet.query參數,可以對任意字段進行篩選。
基于Solr facet的實現
本文的例子,需要查詢DEAL的“狀態”和“快捷選項”導航信息。由于,有的狀態DEAL數目不僅與狀態(status)字段有關,還與開始時間(begin_time)和(end_time)相關,且各個快捷選項的DEAL數目的計算字段各不相同,要求比較靈活的查詢,所以本文擬采用Facet Query方式實現。 以下代碼是采用solrJ構造facet查詢對象的過程:
public SolrQuery buildFacetQuery(Date now) {SolrQuery solrQuery = new SolrQuery();solrQuery.setFacet(true);//設置facet=onsolrQuery.setFacetLimit(10);//限制facet返回的數量solrQuery.setQuery("*:*");long nowTime = now.getTime() / 1000;long minTime = minTimeStamp;long maxTime = maxTimeStamp;solrQuery.addFacetQuery("status:0"); //待撰寫solrQuery.addFacetQuery("status:8"); //撰寫中solrQuery.addFacetQuery("status:16"); //已終審solrQuery.addFacetQuery("status:32 AND " + "begin_time:[" + nowTime + " TO " + maxTime + " ]"); //已上架-待上線solrQuery.addFacetQuery("status:32 AND " + "begin_time:[" + minTime + " TO " + nowTime + "] AND " + //已上架-上線中"end_time:[" + nowTime + " TO " + maxTime + " ]");solrQuery.addFacetQuery("status:32 AND " + "end_time:[" + minTime + " TO " + nowTime + "]"); //已上架-已下線return solrQuery; }說明: “status:0” 查詢滿足條件的結果集中status=0的Deal數目, “status:32 AND “ + “begin_time:[” + nowTime + “ TO ” + maxTime + “ ]”,查詢滿足條件的結果集中,status=32且begin_time大于現在時間的Deal數目, 依次類推
返回結果:
"status:0":756, "status:8":28, "status:16":21, "status:32 AND begin_time:[1401869128 TO 1956499199 ]":4, "status:32 AND begin_time:[0 TO 1401869128] AND end_time:[1401869128 TO 1956499199 ]":41, "status:32 AND end_time:[0 TO 1401869128]":10}上述結果可知,“已上架-待上線”導航項對應的DEAL數為4個。
Solr Facet查詢分析
1. Solr HTTP請求分發
當一個Restful(HTTP)查詢請求到達SolrCloud服務器,首先由SolrDispatchFilter(實現javax.servlet.Filter)處理,該類負責分發請求到相應的SolrRequestHandler。具體分發操作在SolrDispatchFilter的doFilter方法中進行:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain, boolean retry) {...... handler = core.getRequestHandler( path );if( handler == null && parser.isHandleSelect() ) {if( "/select".equals( path ) || "/select/".equals( path ) ) {solrReq = parser.parse( core, path, req );String qt = solrReq.getParams().get( CommonParams.QT );handler = core.getRequestHandler( qt ); //分發到相應的handler.......if( handler != null ) {...... this.execute( req, handler, solrReq, solrRsp ); //處理請求HttpCacheHeaderUtil.checkHttpCachingVeto(solrRsp, resp, reqMethod); ...... return; }} }protected void execute( HttpServletRequest req, SolrRequestHandler handler, SolrQueryRequest sreq, SolrQueryResponse rsp) {sreq.getContext().put( "webapp", req.getContextPath() );sreq.getCore().execute( handler, sreq, rsp ); }接著,調用solrCore的execute方法:
public void execute(SolrRequestHandler handler, SolrQueryRequest req, SolrQueryResponse rsp) {...... handler.handleRequest(req,rsp); // handler處理請求 postDecorateResponse(handler, req, rsp);...... }從上述代碼邏輯可以看出,請求的實際處理是由SolrRequestHandler來完成的。
2. SolrRequestHandler處理過程
SolrRequestHandler的類繼承結構,如下圖所示:
SolrRequestHandler請求處理器的接口,只有兩個方法,一個是初始化信息,主要是配置時的默認參數,另一個就是處理請求的接口。 具體處理邏輯主要由SearchHandler類實現。
public interface SolrRequestHandler extends SolrInfoMBean {public void init(NamedList args); //初始化信息public void handleRequest(SolrQueryRequest req, SolrQueryResponse rsp); //處理請求 }SearchHandler實現SolrRequestHandler,SolrCoreAware,在SolrCore初始化的過程中調用SolrRequestHandler中的inform(SolrCore core),首先是將solrconfig.xml里配置的各個處理組件按一定順序組裝起來,先是first-Component,默認的component,last-component,這些處理組件會按照它們的順序來執行。如果沒有配置,則加載默認組件,方法如下:
protected List<String> getDefaultComponents() {ArrayList<String> names = new ArrayList<String>(6);names.add( QueryComponent.COMPONENT_NAME );names.add( FacetComponent.COMPONENT_NAME );names.add( MoreLikeThisComponent.COMPONENT_NAME );names.add( HighlightComponent.COMPONENT_NAME );names.add( StatsComponent.COMPONENT_NAME );names.add( DebugComponent.COMPONENT_NAME );names.add( AnalyticsComponent.COMPONENT_NAME );return names; }SearchHandler中的component對象包含有QueryComponent、FacetComponent、HighlightComponent等,其中QueryComponent主要負責查詢部分,FacetComponent處理facet、HighlightComponent負責高亮顯示。SearchHandler在請求處理過程中,由SearchHandler.handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp)方法依次調用component的prepare、process、distributedProcess方法(分布式搜索本文暫不討論) 。QueryComponent調用SolrIndexSearcher,SolrIndexSearcher繼承了lucene的IndexSearcher類進行搜索,FacetComponent實現對Term的層面的統計,下圖是SearchComponent的類圖結構:
3. FacetComponent Facet查詢分析
由上述分析可知,Solr的Facet功能實際上是由FacetComponent組件來實現的,具體實現在FacetComponent.process方法中:
public void process(ResponseBuilder rb) throws IOException {if (rb.doFacets) {SolrParams params = rb.req.getParams();SimpleFacets f = new SimpleFacets(rb.req, rb.getResults().docSet,params, rb ); //最終facet查詢委托給SimpleFacets類進行處理 NamedList<Object> counts = f.getFacetCounts(); ...... } }首先QueryComponent處理q參數里的查詢,查詢的結果的DocID保存在docSet里,這里是一個無序的document ID 的集合。然后把docSet封裝在SimpleFacets中,調用SimpleFacets.getFacetCounts()獲取統計結果:
public NamedList<Object> getFacetCounts() {......facetResponse = new SimpleOrderedMap<Object>();facetResponse.add("facet_queries", getFacetQueryCounts());facetResponse.add("facet_fields", getFacetFieldCounts());facetResponse.add("facet_dates", getFacetDateCounts());facetResponse.add("facet_ranges", getFacetRangeCounts()); ......return facetResponse; }由上可知,返回給客戶端的結果有四種類型facet_queries、facet_fields、facet_dates、facet_ranges,分別調用getFacetQueryCounts(),getFacetFieldCounts(),getFacetDateCounts(),getFacetRangeCounts()完成查詢。
4. getFacetQueryCounts統計count過程
由于篇幅原因,上述四個方法不一一展開分析,本文用到的查詢主要是Facet Query,下面分析一下getFacetQueryCounts方法源碼:
public NamedList<Integer> getFacetQueryCounts() throws IOException,SyntaxError {NamedList<Integer> res = new SimpleOrderedMap<Integer>();String[] facetQs = params.getParams(FacetParams.FACET_QUERY);if (null != facetQs && 0 != facetQs.length) {for (String q : facetQs) { // 循環統計每個facet query的countparseParams(FacetParams.FACET_QUERY, q);Query qobj = QParser.getParser(q, null, req).getQuery();if (qobj == null) {res.add(key, 0);} else if (params.getBool(GroupParams.GROUP_FACET, false)) {res.add(key, getGroupedFacetQueryCount(qobj));} else {res.add(key, searcher.numDocs(qobj, docs)); //}}}return res; }該方法的返回類型NamedList是一個有序的name/value容器,保存每個facet query和對應的count值。由代碼可知,在for循環體中逐個統計facet query的count值,其中,parseParams方法中把”key”設置成本次循環的facet query變量“q“,由于GroupParams.GROUP_FACET的值是false(group類似與mysql的group by功能,一般不會打開),所以count值實際是由searcher.numDocs(qobj, docs)方法負責計算,這里的searcher類型是SolrIndexSearcher。
SolrIndexSearcher的numDocs方法源碼如下:
public int numDocs(Query a, DocSet b) throws IOException {if (filterCache != null) { Query absQ = QueryUtils.getAbs(a); //如果為negative,則返回相應的補集DocSet positiveA = getPositiveDocSet(absQ); //查詢absQ 獲取docSet集合return a==absQ ? b.intersectionSize(positiveA) : b.andNotSize(positiveA);} else {TotalHitCountCollector collector = new TotalHitCountCollector();BooleanQuery bq = new BooleanQuery();bq.add(QueryUtils.makeQueryable(a), BooleanClause.Occur.MUST);bq.add(new ConstantScoreQuery(b.getTopFilter()), BooleanClause.Occur.MUST);super.search(bq, null, collector);return collector.getTotalHits(); }}
參數a傳入facet query對象,參數b傳入經過QueryComponent組件處理后得到DocSet集合。DocSet存儲的是無序的文檔標識號(ID),ID并不是我們在schema.xml里配置的unique key,而是Solr內部的一個文檔標識,其次,DocSet還封裝了集合運算的方法,如“求交集”、”求差集”。
由于,我們在solrconfig.xml中配置了filterCache:
<filterCache class="solr.FastLRUCache" size="512" initialSize="512" autowarmCount="0”/>于是,numDocs方法中filterCache對象不為null,運行到下面三行代碼:
Query absQ = QueryUtils.getAbs(a); //如果為negative,則返回相應的補集 DocSet positiveA = getPositiveDocSet(absQ); //查詢absQ 獲取docSet集合 return a==absQ ? b.intersectionSize(positiveA) : b.andNotSize(positiveA); //集合運算首先,通過QueryUtils.getAbs(a)將查詢對象a統一轉化為一個“正向查詢對象”absQ,getPositiveDocSet(absQ)方法查詢absQ對應的DocSet集合:getPositiveDocSet方法首先查詢filterCache中是否存在absQ查詢對象對應的結果,存在,則直接返回結果,否則,從索引中查詢并把結果保存到filterCache中。
接下來進行集合運算,如果Query對象a和absQ是同一個對象,表明本次查詢是“正向查詢”,則進行”交集“運算b.intersectionSize(positiveA),否則進行”差集“運算,最終返回結果集的size。由此可見,facet query對應的count值是集合交集和差集運算后的集合的size。
BTW,如果沒有用到filterCache,會每次都構造一個BooleanQuery查詢對象到索引中去查詢。
5. FacetComponent Facet排序 Solr的FacetComponet支持兩種排序: count和index。count是按每個詞出現的次數,index是按詞的字典順序。如果查詢參數不指定facet.sort,Solr默認是按count排序。排序功能是在FacetComponet的finishStage方法中完成的,詳見源碼。
總結
本文介紹了Solr Facet技術,并在此基礎上實現了DEAL搜索的導航功能,然后從源碼級別分析了Solr處理Facet請求的詳細過程。
參考資料
- SimpleFacetParameters http://wiki.apache.org/solr/SimpleFacetParameters
- 使用Apache Lucene和Solr 4實現下一代搜索和分析 http://www.ibm.com/developerworks/cn/java/j-solr-lucene/
- Faceted Search with Solr http://searchhub.org/2009/09/02/faceted-search-with-solr/
總結
以上是生活随笔為你收集整理的Solr Facet技术的应用与研究的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 活动 Web 页面人机识别验证的探索与实
- 下一篇: 美团点评技术年货:一本覆盖各技术领域、1