以商家(Poi)維度來展現各類服務(好比團購(deal)、直連)正變得愈來愈流行(圖1a), 好比目前美食、酒店等品類在移動端將團購信息列表改成POI列表頁展現。 html
圖1 a:商家維度展現信息; b:join示意 java
這給篩選帶來了複雜性。以前的篩選是平面的,如篩選poi列表時僅僅利用到poi的屬性(如評價、品類等),篩選deal列表時也僅僅根據deal的屬性(房態、價格等)。而如今的篩選是具備層次關係的,咱們須要根據deal的屬性來篩選Poi,舉個例子,咱們須要篩選酒店列表,這些酒店必需要有價格在100~200之間的團購。數據庫
這種篩選本質是種join操做,其核心是要將poi與deal關聯起來。從數據庫視角上看(圖1 b),咱們有poi表以及deal表,deal表存儲了外鍵(parentid)用於指示該deal所屬的poi,上述篩選分爲三步:1)先篩選出價格區間在100~200的deal(獲得dealid爲2和3的deal);2)找出deal對應的poi(獲得poiid爲1和1的poi);3)去重,由於可能多個deal對應同一個poi,而咱們須要返回不重複poi。數組
目前咱們使用lucene來提供篩選服務,那麼lucene如何解決這種帶有join的篩選呢? app
在咱們應用中,一個poi存儲爲一個document,一個deal也存儲爲一個document,Join的核心在於將poi以及deal的document進行關聯。lucene提供了兩種join的方式,分別是query time join和index time join,下文將分別展開。ide
query time join是經過相似數據庫「外鍵「方法來創建deal和poi document的關聯關係。post
a)索引性能
分別建立poi的document和deal的document,在創建deal document的時候用一個字段(parentid)將deal與poi關聯起來,本例中建立了parentid這個field,裏面存的是該deal對應的poiid,能夠簡單將其看作外鍵。學習
public static Document createPoiDocument(PoiMsg poiMsg) { Document document = new Document(); document.add(new StringField("poiid", String.valueOf(poiMsg.getId()), Field.Store.YES)); document.add(new StringField("name", poiMsg.getName(), Field.Store.YES)); return document; }
public static Document createDealDocument(DealModel dealModel, PoiMsg poiMsg) { Document document = new Document(); document.add(new StringField("did", String.valueOf(dealModel.getDid()), Field.Store.YES)); document.add(new StringField("name", dealModel.getBrandName(), Field.Store.YES)); document.add(new DoubleField("price", dealModel.getPrice(), Field.Store.YES)); document.add(new StringField("parentid", String.valueOf(poiMsg.getId()), Field.Store.YES)); return document; }
IndexWriter writer = new IndexWriter(directory, config); writer.addDocument(createPoiDocument(poiMsg1)); writer.addDocument(createPoiDocument(poiMsg2)); writer.addDocument(createDealDocument(dealModel1, poiMsg2)); writer.addDocument(createDealDocument(dealModel2, poiMsg1)); writer.addDocument(createDealDocument(dealModel3, poiMsg1));
b)查詢優化
需查詢兩次:首先查詢deal document,以後經過deal中的parentId查詢poi document。
1)第一次查詢發生在JoinUtil.createJoinQuery中。首先建立了TermsCollector這個收集器, 該收集器將知足fromQuery的doc的parentid字段收集起來,以後建立了TermsQuery。
本例執行以後TermsCollector集合裏有兩個terms,分別是」1」和」1」;
2)執行TermsQuery,查詢toField在TermsCollector terms集合中存在的doc,最後找出toField爲「1」的doc。
IndexSearcher indexSearcher = new IndexSearcher(indexReader); String fromFields = "parentid"; Query fromQuery = NumericRangeQuery.newIntRange("price", 100, 200, false, false); String toFields = "poiid"; Query toQuery = JoinUtil.createJoinQuery(fromFields, false, toFields, fromQuery, indexSearcher, ScoreMode.Max); TopDocs results = indexSearcher.search(toQuery, 10);
JoinUtil.createJoinQuery代碼 TermsCollector termsCollector = TermsCollector.create(fromField, multipleValuesPerDocument); fromSearcher.search(fromQuery, termsCollector); return new TermsQuery(toField, fromQuery, termsCollector.getCollectorTerms());
c)優缺點
query time join優勢是很是直觀且靈活;缺點是不能進行打分排序,此外因爲查詢兩遍性能會降低。
query time join經過顯式的在deal document上增長一個「外鍵」來創建關係,找到deal以後須要找出這些deal document的parentid集合,以後再次查詢找出poiId在parentid集合內的poi document。在找到deal以後若是能立刻找到對應的poi document,那將大大提升效率。index time join乾的就是這樣的事情,其經過一種精巧的方法創建了deal document id和poi document id的映射關係。
a)原理
如何經過一個deal document id來找到poi document id?
在lucene中,doc id是自增的,每寫入一個document,doc id加1(簡單起見能夠理解)。 index time join要求寫索引的時候要按前後關係寫入,先寫子document,再寫父document。好比咱們有poi1和poi2兩個poi,其中poi1下有deal2和deal3,而poi2下只有deal1,這時須要先寫入deal二、deal3,再寫入deal2和deal3對應的poi1 document,依次類推,最後造成如圖2所示的結構。
這樣索引創建以後,咱們獲得了父document的id集合(3,5)。當咱們根據deal的屬性查出deal document id時,好比咱們查出知足條件的deal是deal3,其document id=2,這時候只須要到父document id集合裏去查找第一個比2大的id,在本例中立刻就找到3。
圖2
lucene本身實現了BitSet來保存id,lucene內部實現代碼如圖3所示。
圖3 實現原理
b)索引
從上述原理得知咱們須要創建有層次關係的索引。
首先建立document數組,該數組有個特色, 最後一個必須是poi,以前都是deal。而後調用writer.addDocument(documents); 將這個數組寫入。
public static Document createPoiDocument(PoiMsg poiMsg) { Document document = new Document(); document.add(new StringField("poiid", String.valueOf(poiMsg.getId()), Field.Store.YES)); document.add(new StringField("name", poiMsg.getName(), Field.Store.YES)); document.add(new StringField("doctype", "poi", Field.Store.YES)); return document; }
public static Document createDealDocument(DealModel dealModel) { Document document = new Document(); document.add(new StringField("did", String.valueOf(dealModel.getDid()), Field.Store.YES)); document.add(new StringField("name", dealModel.getBrandName(), Field.Store.YES)); document.add(new DoubleField("price", dealModel.getPrice(), Field.Store.YES)); return document; }
IndexWriter writer = new IndexWriter(directory, config); List<Document> documents = new ArrayList<Document>(); documents.add(createDealDocument(dealModel2)); documents.add(createDealDocument(dealModel3)); documents.add(createPoiDocument(poiMsg1)); writer.addDocument(documents); documents.clear(); documents.add(createDealDocument(dealModel1)); documents.add(createPoiDocument(poiMsg2)); writer.addDocument(documents);
c)查詢
Filter poiFilter = new CachingWrapperFilter(new QueryWrapperFilter(new TermQuery(new Term(PoiLuceneField.ATTR_DOCTYPE, "poi")))); //篩選出poi ToParentBlockJoinQuery query = new ToParentBlockJoinQuery(dealQuery, poiFilter, ScoreMode.Max); ToParentBlockJoinCollector collector = new ToParentBlockJoinCollector( sort, // sort (getOffset() + getLimit()), // poi分頁numHits true, // trackScores false // trackMaxScore ); collector = (ToParentBlockJoinCollector) indexSearcher.search(query, collector); Sort childSort = new Sort(new SortField(DealLuceneField.ATTR_PRICE, SortField.Type.DOUBLE)); TopGroups hits = collector.getTopGroups( query.getToParentBlockJoinQuery(), childSort, query.getOffset(), // parent doc offset 100, // maxDocsPerGroup 0, // withinGroupOffset true // fillSortFields );
官方文檔顯示index time join效率更高,比query time join快30%以上。所以咱們在項目中使用了index time join方式,目前服務運行良好。
檢索實踐文章系列: