以商家(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方式,目前服务运行良好。
检索实践文章系列: