lucene join解决父子关系索引
https://yq.aliyun.com/articles/20
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,可以简单将其看做外键。
1
2
3
4
5
6
|
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;
}
|
1
2
3
4
5
6
7
8
|
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;
}
|
1
2
3
4
5
6
|
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。
1
2
3
4
5
6
|
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 );
|
1
2
3
4
|
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); 将这个数组写入。
1
2
3
4
5
6
7
|
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;
}
|
1
2
3
4
5
6
7
|
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;
}
|
1
2
3
4
5
6
7
8
9
10
|
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)查询
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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方式,目前服务运行良好。
转载于:https://www.cnblogs.com/davidwang456/articles/10045715.html
lucene join解决父子关系索引相关推荐
- SQL Server 2005新特性之使用with关键字解决递归父子关系
1. 引言 现实项目中经常遇到需要处理递归父子关系的问题,如果把层次关系分开,放在多个表里通过主外键关系联接,最明显的问题就是扩展起来不方便,对于这种情况,一般我们会创建一个使用自连接的表来存放数据. ...
- elasticsearch的父子_elasticsearch父子关系(官方)实际使用中的一些建议
当文档索引性能远比查询性能重要的时候,父子关系是非常有用的,但是它也是有巨大代价的.其查询速度会比同等的嵌套查询慢5到10倍! 全局序号和延迟 父子关系使用了全局序数来加速文档间的联合.不管父子关系映 ...
- 白话Elasticsearch60-数据建模实战_Join datatype 父子关系数据建模
文章目录 概述 官网 示例 概述 继续跟中华石杉老师学习ES,第60篇 课程地址: https://www.roncoo.com/view/55 白话Elasticsearch58-数据建模实战_基于 ...
- vue-自主研发非父子关系组件之间通信的问题
相信很多人都知道解决组件间通信:vuex,今天的主角不是它. element-ui里解决组件间通信的思路:emitter.js ,但是如果你拿来你会发现它解决的是父子组件之间的通信问题.如果我们通信的 ...
- python画父子关系图_将有父子关系的一维数组转换成树形结构(多维)数据
先来个函数注释 : /** * 将有父子关系的一维数组转换成树形结构(多维)数据 * console.log(JSON.stringify(setTreeData(data), null, 2)); ...
- 信息设计中的“父子关系”
交互设计工作核心在于信息架构和交互细节设计.信息架构包括信息分类以及信息展示逻辑设计:交互细节则多表现为控件的选择,交互效果的定义等.在信息设计中,遇到最棘手的问题就是信息量太多而显得设计结果不尽人意 ...
- java父子表_Java编程:将具有父子关系的数据库表数据转换为树形结构,支持无限层级...
在平时的开发工作中,经常遇到这样一个场景,在数据库中存储了具有父子关系的数据,需要将这些数据以树形结构的形式在界面上进行展示.本文的目的是提供了一个通用的编程模型,解决将具有父子关系的数据转换成树形结 ...
- 【18-业务开发-基础业务-商品模块-分类管理-前后端管理系统的启动-为分类管理表增加数据-Json插件的下载-返回具有层级目录、父子关系结构的数据】
一.知识回顾 [0.三高商城系统的专题专栏都帮你整理好了,请点击这里!] [1-系统架构演进过程] [2-微服务系统架构需求] [3-高性能.高并发.高可用的三高商城系统项目介绍] [4-Linux云 ...
- 3D变形:平移、旋转、缩放、父子关系外间距塌陷
3D变形:平移.旋转.缩放 什么是3d转换 定义元素在三维空间移动.缩放.旋转 3d坐标轴(图示) 3D立体空间的3根轴线 x轴:水平,向右为正,向左为负 y轴:垂直,向下为正,向上为负 z轴:垂直于 ...
最新文章
- 微信公众号网页获取用户信息
- QoS policy-map class-map
- Windows下MongoDB安装及创建用户名和密码
- 三种方法实现Linux系统调用方法分享
- 史上最轻量K8s发行版,赋能边缘计算
- 科普 | 一文完全理解AUC-ROC曲线
- PostgreSQL 8.0 中文手册
- 【蓝鸥Unity开发基础三】课时14 刚体
- 13位数字转日期 oracle_12amp;13. 整数转罗马数字 - 中等amp;简单
- AutoCad入门(一)
- VScode 光标乱跳 光标自动跳动问题
- 用mysql设计一个KTV点歌系统_KTV点歌系统(JAVA+MYSQL)
- Python之网络编程
- 鸿蒙系统转正,鸿蒙首发机型确定 “备胎”鸿蒙系统正式转正
- Office 16 Click-to-Run Extensibility Component 卸载
- 北京2008中国代表团名单
- NX从刷机到更换开机logo
- CDA Day 7-8 Excel 数组学习总结2
- zabbix客户端日志报错no active checks on server [192.168.3.108:10051]: host [192.168.3.108] not found
- 不知道从哪里找数据?这一篇,都解决(持续更新)
热门文章
- java选填,java选择填空 - osc_ug2wy0bi的个人空间 - OSCHINA - 中文开源技术交流社区
- datalist可以放div吗?_炒花生米可以放蜂蜜吗?蜂蜜花生米的正确做法窍门
- JVM、JRE和JDK的概念
- php mysql update 返回_php mysql_query增删改查(update delete insert)返回值类型
- python计算最大公约数和最小公倍数_python怎么求最大公约数和最小公倍数
- java web 购物车_java web开发之实现购物车功能
- php docker开发环境,使用Docker的PHP开发环境
- RecyclerView控件的基本使用(增强版的ListView)
- linux arcgis10.4安装教程,ArcGIS 10.1 for Server安装教程系列—— Linux下的单机安装
- c++的引用是什么意思?怎么回事?