Solr Facet技术的应用与研究
問(wèn)題背景
在《搜索引擎關(guān)鍵字智能提示的一種實(shí)現(xiàn)》一文中介紹過(guò),美團(tuán)的CRM系統(tǒng)負(fù)責(zé)管理銷售人員的門店(POI)和項(xiàng)目(DEAL)信息,提供統(tǒng)一的檢索功能,其索引層采用的是SolrCloud。在用戶搜索時(shí),如果能直觀地給出每個(gè)品類的POI數(shù)目,各個(gè)狀態(tài)的DEAL數(shù)目,可以更好地引導(dǎo)用戶進(jìn)行搜索,進(jìn)而提升搜索體驗(yàn)。
需求分析
例如,下圖是用戶搜索項(xiàng)目(DEAL)的界面,當(dāng)選中一個(gè)人或者組織節(jié)點(diǎn)后,需要實(shí)時(shí)顯示狀態(tài)分組和快捷分組的每個(gè)項(xiàng)的DEAL數(shù)目:
為了實(shí)現(xiàn)上述導(dǎo)航效果,可以采用以下兩個(gè)方案:
方案一, 針對(duì)每個(gè)導(dǎo)航項(xiàng)發(fā)送一個(gè)Ajax請(qǐng)求,去Solr服務(wù)器查詢對(duì)應(yīng)的DEAL數(shù)目。該方案問(wèn)題在于,當(dāng)導(dǎo)航項(xiàng)比較多時(shí),擴(kuò)展性不好。方案二, 應(yīng)用Solr自帶的Facet技術(shù)實(shí)現(xiàn)以導(dǎo)航為目的的搜索,查詢結(jié)果根據(jù)分類添加count信息。DEAL的Solr索引設(shè)計(jì)如下:
schema.xml: <field name="deal_id" type="int" indexed="true" stored="true" /> //deal id <field name="title" type="text_ika" indexed="true" stored="false" /> //標(biāo)題 <field name="bd_id" type="int" indexed="true" stored="false" /> //負(fù)責(zé)人id <field name="begin_time" type="long" indexed="true" stored="false" /> //項(xiàng)目開始時(shí)間 <field name="end_time" type="long" indexed="true" stored="false" /> //項(xiàng)目結(jié)束時(shí)間 <field name="status" type="int" indexed="true" stored="false" /> //項(xiàng)目狀態(tài) <field name="can_buy" type="boolean" indexed="true" stored="false" /> //是否可以購(gòu)買 ...省略 本文的例子中用于facet的字段有status,can_buy,begin_time,end_time注: Facet的字段必須被索引,無(wú)需分詞,無(wú)需存儲(chǔ)。無(wú)需分詞是因?yàn)樵撟侄蔚闹荡砹艘粋€(gè)整體概念,無(wú)需存儲(chǔ)是因?yàn)橐话愣杂脩羲P(guān)心的并不是該字段的具體值,而是作為對(duì)查詢結(jié)果進(jìn)行分組的一種手段,用戶一般會(huì)沿著這個(gè)分組進(jìn)一步深入搜索。
Solr Facet簡(jiǎn)介
Facet是Solr的高級(jí)搜索功能之一,Solr作者給出的定義是導(dǎo)航(Guided Navigation)、參數(shù)化查詢(Paramatic Search)。Facet的主要好處是在搜索的同時(shí),可以按照Facet條件進(jìn)行分組統(tǒng)計(jì),給出導(dǎo)航信息,改善搜索體驗(yàn)。Facet搜索主要分為以下幾類:
1. Field Facet 搜索結(jié)果按照Facet的字段分組并統(tǒng)計(jì),Facet字段通過(guò)在請(qǐng)求中加入”facet.field”參數(shù)加以聲明,如果需要對(duì)多個(gè)字段進(jìn)行Facet查詢,那么將該參數(shù)聲明多次,Facet字段必須被索引。例如,以下表達(dá)式是以DEAL的status和can_buy屬性為facet.field進(jìn)行查詢:
select?q=*:*&facet=true&facet.field=status&facet.field=can_buy&wt=jsonFacet查詢需要在請(qǐng)求參數(shù)中加入”facet=on”或者”facet=true”讓Facet組件起作用,返回結(jié)果:
"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”的值分組,比如狀態(tài)為32的DEAL數(shù)目有96個(gè),能購(gòu)買的DEAL數(shù)目(can_buy=true)是236。
Field Facet主要參數(shù):
facet.field:Facet的字段facet.prefix:Facet字段前綴facet.limit:Facet字段返回條數(shù)facet.offset:開始條數(shù),偏移量,它與facet.limit配合使用可以達(dá)到分頁(yè)的效果facet.mincount:Facet字段最小count,默認(rèn)為0facet.missing:如果為on或true,那么將統(tǒng)計(jì)那些Facet字段值為null的記錄facet.method:取值為enum或fc,默認(rèn)為fc,fc表示Field Cachefacet.enum.cache.minDf:當(dāng)facet.method=enum時(shí),參數(shù)起作用,文檔內(nèi)出現(xiàn)某個(gè)關(guān)鍵字的最少次數(shù)2. Date Facet 日期類型的字段在索引中很常見,如DEAL上線時(shí)間,線下時(shí)間等,某些情況下需要針對(duì)這些字段進(jìn)行Facet。時(shí)間字段的取值有無(wú)限性,用戶往往關(guān)心的不是某個(gè)時(shí)間點(diǎn)而是某個(gè)時(shí)間段內(nèi)的查詢統(tǒng)計(jì)結(jié)果,Solr為日期字段提供了更為方便的查詢統(tǒng)計(jì)方式。字段的類型必須是DateField(或其子類型)。需要注意的是,使用Date Facet時(shí),字段名、起始時(shí)間、結(jié)束時(shí)間、時(shí)間間隔這4個(gè)參數(shù)都必須提供。 與Field Facet類似,Date Facet也可以對(duì)多個(gè)字段進(jìn)行Facet。并且針對(duì)每個(gè)字段都可以單獨(dú)設(shè)置參數(shù)。
3. Facet Query Facet Query利用類似于filter query的語(yǔ)法提供了更為靈活的Facet。通過(guò)facet.query參數(shù),可以對(duì)任意字段進(jìn)行篩選。
基于Solr facet的實(shí)現(xiàn)
本文的例子,需要查詢DEAL的“狀態(tài)”和“快捷選項(xiàng)”導(dǎo)航信息。由于,有的狀態(tài)DEAL數(shù)目不僅與狀態(tài)(status)字段有關(guān),還與開始時(shí)間(begin_time)和(end_time)相關(guān),且各個(gè)快捷選項(xiàng)的DEAL數(shù)目的計(jì)算字段各不相同,要求比較靈活的查詢,所以本文擬采用Facet Query方式實(shí)現(xiàn)。 以下代碼是采用solrJ構(gòu)造facet查詢對(duì)象的過(guò)程:
public SolrQuery buildFacetQuery(Date now) {SolrQuery solrQuery = new SolrQuery();solrQuery.setFacet(true);//設(shè)置facet=onsolrQuery.setFacetLimit(10);//限制facet返回的數(shù)量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; }說(shuō)明: “status:0” 查詢滿足條件的結(jié)果集中status=0的Deal數(shù)目, “status:32 AND “ + “begin_time:[” + nowTime + “ TO ” + maxTime + “ ]”,查詢滿足條件的結(jié)果集中,status=32且begin_time大于現(xiàn)在時(shí)間的Deal數(shù)目, 依次類推
返回結(jié)果:
"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}上述結(jié)果可知,“已上架-待上線”導(dǎo)航項(xiàng)對(duì)應(yīng)的DEAL數(shù)為4個(gè)。
Solr Facet查詢分析
1. Solr HTTP請(qǐng)求分發(fā)
當(dāng)一個(gè)Restful(HTTP)查詢請(qǐng)求到達(dá)SolrCloud服務(wù)器,首先由SolrDispatchFilter(實(shí)現(xiàn)javax.servlet.Filter)處理,該類負(fù)責(zé)分發(fā)請(qǐng)求到相應(yīng)的SolrRequestHandler。具體分發(fā)操作在SolrDispatchFilter的doFilter方法中進(jìn)行:
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 ); //分發(fā)到相應(yīng)的handler.......if( handler != null ) {...... this.execute( req, handler, solrReq, solrRsp ); //處理請(qǐng)求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 ); }接著,調(diào)用solrCore的execute方法:
public void execute(SolrRequestHandler handler, SolrQueryRequest req, SolrQueryResponse rsp) {...... handler.handleRequest(req,rsp); // handler處理請(qǐng)求 postDecorateResponse(handler, req, rsp);...... }從上述代碼邏輯可以看出,請(qǐng)求的實(shí)際處理是由SolrRequestHandler來(lái)完成的。
2. SolrRequestHandler處理過(guò)程
SolrRequestHandler的類繼承結(jié)構(gòu),如下圖所示:
SolrRequestHandler請(qǐng)求處理器的接口,只有兩個(gè)方法,一個(gè)是初始化信息,主要是配置時(shí)的默認(rèn)參數(shù),另一個(gè)就是處理請(qǐng)求的接口。 具體處理邏輯主要由SearchHandler類實(shí)現(xiàn)。
public interface SolrRequestHandler extends SolrInfoMBean {public void init(NamedList args); //初始化信息public void handleRequest(SolrQueryRequest req, SolrQueryResponse rsp); //處理請(qǐng)求 }SearchHandler實(shí)現(xiàn)SolrRequestHandler,SolrCoreAware,在SolrCore初始化的過(guò)程中調(diào)用SolrRequestHandler中的inform(SolrCore core),首先是將solrconfig.xml里配置的各個(gè)處理組件按一定順序組裝起來(lái),先是first-Component,默認(rèn)的component,last-component,這些處理組件會(huì)按照它們的順序來(lái)執(zhí)行。如果沒有配置,則加載默認(rèn)組件,方法如下:
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對(duì)象包含有QueryComponent、FacetComponent、HighlightComponent等,其中QueryComponent主要負(fù)責(zé)查詢部分,FacetComponent處理facet、HighlightComponent負(fù)責(zé)高亮顯示。SearchHandler在請(qǐng)求處理過(guò)程中,由SearchHandler.handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp)方法依次調(diào)用component的prepare、process、distributedProcess方法(分布式搜索本文暫不討論) 。QueryComponent調(diào)用SolrIndexSearcher,SolrIndexSearcher繼承了lucene的IndexSearcher類進(jìn)行搜索,FacetComponent實(shí)現(xiàn)對(duì)Term的層面的統(tǒng)計(jì),下圖是SearchComponent的類圖結(jié)構(gòu):
3. FacetComponent Facet查詢分析
由上述分析可知,Solr的Facet功能實(shí)際上是由FacetComponent組件來(lái)實(shí)現(xiàn)的,具體實(shí)現(xiàn)在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類進(jìn)行處理 NamedList<Object> counts = f.getFacetCounts(); ...... } }首先QueryComponent處理q參數(shù)里的查詢,查詢的結(jié)果的DocID保存在docSet里,這里是一個(gè)無(wú)序的document ID 的集合。然后把docSet封裝在SimpleFacets中,調(diào)用SimpleFacets.getFacetCounts()獲取統(tǒng)計(jì)結(jié)果:
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; }由上可知,返回給客戶端的結(jié)果有四種類型facet_queries、facet_fields、facet_dates、facet_ranges,分別調(diào)用getFacetQueryCounts(),getFacetFieldCounts(),getFacetDateCounts(),getFacetRangeCounts()完成查詢。
4. getFacetQueryCounts統(tǒng)計(jì)count過(guò)程
由于篇幅原因,上述四個(gè)方法不一一展開分析,本文用到的查詢主要是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) { // 循環(huán)統(tǒng)計(jì)每個(gè)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是一個(gè)有序的name/value容器,保存每個(gè)facet query和對(duì)應(yīng)的count值。由代碼可知,在for循環(huán)體中逐個(gè)統(tǒng)計(jì)facet query的count值,其中,parseParams方法中把”key”設(shè)置成本次循環(huán)的facet query變量“q“,由于GroupParams.GROUP_FACET的值是false(group類似與mysql的group by功能,一般不會(huì)打開),所以count值實(shí)際是由searcher.numDocs(qobj, docs)方法負(fù)責(zé)計(jì)算,這里的searcher類型是SolrIndexSearcher。
SolrIndexSearcher的numDocs方法源碼如下:
public int numDocs(Query a, DocSet b) throws IOException {if (filterCache != null) { Query absQ = QueryUtils.getAbs(a); //如果為negative,則返回相應(yīng)的補(bǔ)集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(); }}
參數(shù)a傳入facet query對(duì)象,參數(shù)b傳入經(jīng)過(guò)QueryComponent組件處理后得到DocSet集合。DocSet存儲(chǔ)的是無(wú)序的文檔標(biāo)識(shí)號(hào)(ID),ID并不是我們?cè)趕chema.xml里配置的unique key,而是Solr內(nèi)部的一個(gè)文檔標(biāo)識(shí),其次,DocSet還封裝了集合運(yùn)算的方法,如“求交集”、”求差集”。
由于,我們?cè)趕olrconfig.xml中配置了filterCache:
<filterCache class="solr.FastLRUCache" size="512" initialSize="512" autowarmCount="0”/>于是,numDocs方法中filterCache對(duì)象不為null,運(yùn)行到下面三行代碼:
Query absQ = QueryUtils.getAbs(a); //如果為negative,則返回相應(yīng)的補(bǔ)集 DocSet positiveA = getPositiveDocSet(absQ); //查詢absQ 獲取docSet集合 return a==absQ ? b.intersectionSize(positiveA) : b.andNotSize(positiveA); //集合運(yùn)算首先,通過(guò)QueryUtils.getAbs(a)將查詢對(duì)象a統(tǒng)一轉(zhuǎn)化為一個(gè)“正向查詢對(duì)象”absQ,getPositiveDocSet(absQ)方法查詢absQ對(duì)應(yīng)的DocSet集合:getPositiveDocSet方法首先查詢filterCache中是否存在absQ查詢對(duì)象對(duì)應(yīng)的結(jié)果,存在,則直接返回結(jié)果,否則,從索引中查詢并把結(jié)果保存到filterCache中。
接下來(lái)進(jìn)行集合運(yùn)算,如果Query對(duì)象a和absQ是同一個(gè)對(duì)象,表明本次查詢是“正向查詢”,則進(jìn)行”交集“運(yùn)算b.intersectionSize(positiveA),否則進(jìn)行”差集“運(yùn)算,最終返回結(jié)果集的size。由此可見,facet query對(duì)應(yīng)的count值是集合交集和差集運(yùn)算后的集合的size。
BTW,如果沒有用到filterCache,會(huì)每次都構(gòu)造一個(gè)BooleanQuery查詢對(duì)象到索引中去查詢。
5. FacetComponent Facet排序 Solr的FacetComponet支持兩種排序: count和index。count是按每個(gè)詞出現(xiàn)的次數(shù),index是按詞的字典順序。如果查詢參數(shù)不指定facet.sort,Solr默認(rèn)是按count排序。排序功能是在FacetComponet的finishStage方法中完成的,詳見源碼。
總結(jié)
本文介紹了Solr Facet技術(shù),并在此基礎(chǔ)上實(shí)現(xiàn)了DEAL搜索的導(dǎo)航功能,然后從源碼級(jí)別分析了Solr處理Facet請(qǐng)求的詳細(xì)過(guò)程。
參考資料
- SimpleFacetParameters http://wiki.apache.org/solr/SimpleFacetParameters
- 使用Apache Lucene和Solr 4實(shí)現(xiàn)下一代搜索和分析 http://www.ibm.com/developerworks/cn/java/j-solr-lucene/
- Faceted Search with Solr http://searchhub.org/2009/09/02/faceted-search-with-solr/
總結(jié)
以上是生活随笔為你收集整理的Solr Facet技术的应用与研究的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 活动 Web 页面人机识别验证的探索与实
- 下一篇: 史上最萌最认真的机器学习/深度学习/模式