http://www.cnblogs.com/LBSer/p/4417074.html

1 背景

以商家(Poi)维度来展示各种服务(比如团购(deal)、直连)正变得越来越流行(图1a), 比如目前美食、酒店等品类在移动端将团购信息列表改为POI列表页展示。

                 

图1   a:商家维度展示信息; b:join示意

这给筛选带来了复杂性。之前的筛选是平面的,如筛选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的筛选呢?

2 lucene join解决方案

在我们应用中,一个poi存储为一个document,一个deal也存储为一个document,Join的核心在于将poi以及deal的document进行关联。lucene提供了两种join的方式,分别是query time join和index time join,下文将分别展开。

2.1. query time join

query time join是通过类似数据库“外键“方法来建立deal和poi document的关联关系。

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优点是非常直观且灵活;缺点是不能进行打分排序,此外由于查询两遍性能会下降。

2.2. index 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,这时需要先写入deal2、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
);

  

3 实践

官方文档显示index time join效率更高,比query time join快30%以上。因此我们在项目中使用了index time join方式,目前服务运行良好。

检索实践文章系列:

lucene字典实现原理

lucene索引文件大小优化小结

排序学习实践

lucene如何通过docId快速查找field字段以及最近距离等信息?

lucene join解决父子关系索引的更多相关文章

  1. AJ学IOS 之控制器view显示中view的父子关系及controller的父子关系_解决屏幕旋转不能传递事件问题

    AJ分享,必须精品 一:效果 二:项目代码 这个Demo用的几个控制器分别画了不通的xib,随便拖拽了几个空间,主要是几个按钮的切换,主要代码展示下: // // NYViewController.m ...

  2. Lucene 查询原理 传统二级索引方案 倒排链合并 倒排索引 跳表 位图

    提问: 1.倒排索引与传统数据库的索引相比优势? 2.在lucene中如果想做范围查找,根据上面的FST模型可以看出来,需要遍历FST找到包含这个range的一个点然后进入对应的倒排链,然后进行求并集 ...

  3. 读《深入理解Elasticsearch》点滴-对象类型、嵌套文档、父子关系

    一.对象类型 1.mapping定义文件 "title":{ "type":"text" }, "edition":{ ...

  4. [转]NHibernate之旅(9):探索父子关系(一对多关系)

    本节内容 引入 NHibernate中的集合类型 建立父子关系 父子关联映射 结语 引入 通过前几篇文章的介绍,基本上了解了NHibernate,但是在NHibernate中映射关系是NHiberna ...

  5. elasticsearch 基础 —— Jion父子关系

    前言 由于ES6.X版本以后,每个索引下面只支持单一的类型(type),因此不再支持以下形式的父子关系: PUT /company { "mappings": { "br ...

  6. iOS 父子关系

    1.面向对象特征,类的继承 成员变量(实例变量) 子类继承父类所有功能,只能直接(访问)调用父类中的.h中的protect和public成员变量(实例变量)及方法, .h中的私有的成员变量,子类不能直 ...

  7. oracle处理节点之间的父子关系

    通常当与树的结构之间的关系处理,这是一个很复杂的事情,我们可以通过程序代码去逐层遍历父或子节点,这样做的缺点是很明显,效率不高,操作复杂性是比较大的.而当我们使用Oracle当数据库,我们可以有一个简 ...

  8. SQL SERVER 2000 遍历父子关系数据的表(二叉树)获得所有子节点 所有父节点及节点层数函数

    ---SQL SERVER 2000 遍历父子关系數據表(二叉树)获得所有子节点 所有父节点及节点层数函数---Geovin Du 涂聚文--建立測試環境Create Table GeovinDu([ ...

  9. vue-自主研发非父子关系组件之间通信的问题

    相信很多人都知道解决组件间通信:vuex,今天的主角不是它. element-ui里解决组件间通信的思路:emitter.js ,但是如果你拿来你会发现它解决的是父子组件之间的通信问题.如果我们通信的 ...

随机推荐

  1. Spring----->projects----->Spring Boot

    1.概述:(about Spring Boot) Spring boot是在spring framework的基础上搭建起来的一个基本项目,该项目中已经集成了spring框架下的若干模块以及sprin ...

  2. Swift3.0基础语法学习<四>

    协议和扩展: // // ViewController4.swift // SwiftBasicDemo // // Created by 思 彭 on 16/11/16. // Copyright ...

  3. css基本知识框架(转)

    css基本知识框架: 1.css样式表的基本概念 2.样式表基本类型-----1.内嵌样式 2.内联样式3.链入外部样式表4.导入外部?式 3.样式表配置方法 4.字体属性----1.font-fam ...

  4. HDU 3974 Assign the task(dfs建树+线段树)

    题目大意:公司里有一些员工及对应的上级,给出一些员工的关系,分配给某员工任务后,其和其所有下属都会进行这项任务.输入T表示分配新的任务, 输入C表示查询某员工的任务.本题的难度在于建树,一开始百思不得 ...

  5. TortoiseSVN汉化包装了,不管用,仍然是英文菜单

    TortoiseSVN装了后,把对应的汉化包也装了,但不管用,仍然是英文菜单. 想着是因为没有重启的原因,但是重启了再装,仍然看不到中文工菜单. 想了一下,TortoiseSVN汉化包在装的时候,没有 ...

  6. Windows 设置扩展投影鼠标移出方向

    1. 连接数据线,按下 “WINDOWS” + P 按钮,选择“扩展投影”: 2.更改鼠标移出屏幕的方向:桌面右键选择“屏幕分辨率” , 移动“更改显示器外观”中两个图的相对方向即可:

  7. jquery中html()、text()、val()的区别与使用

    .html()用为读取和修改元素的HTML标签 .text()用来读取或修改元素的纯文本内容 .val()用来读取或修改表单元素的value值. .html(),.text(),.val()三种方法都 ...

  8. 基于PHP使用rabbitmq实现消息队列

    1.从github上面获取AMQP基于php的实现扩展 2.创建生产者 send.php   3.创建消费者 receive.php 4.在cli模式下 分别执行 send.php  receive. ...

  9. MySQL下划线特殊字符(Like 语句)

    最近用了MySQL+ibatis 做项目 问题描述: 在SQL里 用到了 a.name like ##%searchName%## searchName=test_1 发现出来的结果: test_1, ...

  10. 关于Plupload结合上传插件jquery.plupload.queue的使用

    之前使用过很多的上传组件,但对各种浏览器的兼容性太差,不得不放弃!! plupload 是款很强大的上传组件,不得不推荐.plupload 前端根据浏览器不同选择使用Html5. Gears, Sil ...