关于烂代码的那些事 - 为什么每个团队存在大量烂代码

秦迪,微博研发中心技术专家,2013 年加入微博,负责微博平台通讯系统的设计和研发、微博平台基础工具的开发和维护,并负责微博平台的架构改进工作,在工作中擅长排查复杂系统的各类疑难杂症。爱折腾,喜欢研究从内核到前端的所有方向,近几年重点关注大规模系统的架构设计和性能优化,重度代码洁癖:以 code review 为己任,重度工具控:有现成工具的问题就用工具解决,没有工具能解决的问题就写个工具解决。业余时间喜欢偶尔换个语言写代码放松一下。

“一个人工作了几年、做过很多项目、带过团队、发了一些文章,不一定能代表他代码写的好;反之,一个人代码写的好,其它方面的能力一般不会太差。” —— 秦迪

最近写了不少代码,review 了不少代码,也做了不少重构,总之是对着烂代码工作了几周。为了抒发一下这几周里好几次到达崩溃边缘的情绪,我决定写一篇文章谈一谈烂代码的那些事。这里是上篇,谈一谈烂代码产生的原因和现象。

1、写烂代码很容易

刚入程序员这行的时候经常听到一个观点:你要把精力放在 ABCD(需求文档/ 功能设计/架构设计/理解原理)上,写代码只是把想法翻译成编程语言而已,是一个没什么技术含量的事情。

当时的我在听到这种观点时会有一种近似于高冷的不屑:你们就是一群傻 X,根本不懂代码质量的重要性,这么下去迟早有一天会踩坑,呸。

可是几个月之后,他们似乎也没怎么踩坑。而随着编程技术一直在不断发展,带来了更多的我以前认为是傻 X 的人加入到程序员这个行业中来。

语言越来越高级、封装越来越完善,各种技术都在帮助程序员提高生产代码的效率,依靠层层封装,程序员真的不需要了解一丁点技术细节,只要把需求里的内容逐行翻译出来就可以了。

很多程序员不知道要怎么组织代码、怎么提升运行效率、底层是基于什么原理,他们写出来的是在我心目中一堆垃圾代码。但是那一坨垃圾代码竟然能正常工作。

即使我认为他们写的代码是垃圾,但是从不接触代码的人的视角来看(比如说你的 boss),代码编译过了,测试过了,上线运行了一个月都没出问题,你还想要奢求什么?

所以,即使不情愿,也必须承认,时至今日,写代码这件事本身没有那么难了。

2、烂代码终究是烂代码

但是偶尔有那么几次,写烂代码的人离职了之后,事情似乎又变得不一样了。

想要修改功能时却发现程序里充斥着各种无法理解的逻辑、改完之后莫名其妙的 bug 一个接一个,接手这个项目的人开始漫无目的的加班,并且原本一个挺乐观开朗的人渐渐的开始无法接受了。

我总结了几类经常被鄙视到的烂代码:

2.1 意义不明

能力差的程序员容易写出意义不明的代码,他们不知道自己究竟在做什么。

就像这样:

public void save() {
    for(int i=0;i<100;i++) {
        //防止保存失败,重试100次
        document.save(); 
    }
}

对于这类程序员,建议他们尽早转行。

2.2 不说人话

不说人话是新手最经常出现的问题,直接的表现就是写了一段很简单的代码,其他人却看不懂。

比如下面这段:

public boolean getUrl(Long id) {
    UserProfile up = us.getUser(ms.get(id).getMessage().aid);
    if (up == null) {
        return false;
    }
    if (up.type == 4 || ((up.id >> 2) & 1) == 1) {
        return false;
    } 
    if(Util.getUrl(up.description)) {
        return true;
    } else {
        return false;
    }
}

还有很多程序员喜欢复杂,各种宏定义、位运算之类写的天花乱坠,生怕代码让别人一下子看懂了会显得自己水平不够。
很多程序员喜欢简单的东西:简单的函数名、简单的变量名、代码里翻来覆去只用那么几个单词命名;能缩写就缩写、能省略就省略、能合并就合并。这类人写出来的代码里充斥着各种 g/s/gos/of/mss 之类的全世界没人懂的缩写,或者一长串不知道在做什么的连续调用。

简单的说,他们的代码是写给机器的,不是给人看的。

2.3 不恰当的组织

不恰当的组织是高级一些的烂代码,程序员在写过一些代码之后,有了基本的代码风格,但是对于规模大一些的工程的掌控能力不够,不知道代码应该如何解耦、分层和组织。

这种反模式的现象是经常会看到一段代码在工程里拷来拷去;某个文件里放了一大坨堆砌起来的代码;一个函数堆了几百上千行;或者一个简单的功能七拐八绕的调了几十个函数,在某个难以发现的猥琐的小角落里默默的调用了某些关键逻辑。

这类代码大多复杂度高,难以修改,经常一改就崩;而另一方面,创造了这些代码的人倾向于修改代码,畏惧创造代码,他们宁愿让原本复杂的代码一步步变得更复杂,也不愿意重新组织代码。当你面对一个几千行的类,问为什么不把某某逻辑提取出来的时候,他们会说:

“但是,那样就多了一个类了呀。”

2.4.假设和缺少抽象

相对于前面的例子,假设这种反模式出现的场景更频繁,花样更多,始作俑者也更难以自己意识到问题。比如:

public String loadString() {
    File file = new File("c:/config.txt");
    // read something
}

文件路径变更的时候,会把代码改成这样:

public String loadString(String name) {
    File file = new File(name);
    // read something
}

需要加载的内容更丰富的时候,会再变成这样:

public String loadString(String name) {
    File file = new File(name);
    // read something
}
public Integer loadInt(String name) {
    File file = new File(name);
    // read something
}

之后可能会再变成这样:


public String loadString(String name) {
    File file = new File(name);
    // read something
}
public String loadStringUtf8(String name) {
    File file = new File(name);
    // read something
}
public Integer loadInt(String name) {
    File file = new File(name);
    // read something
}
public String loadStringFromNet(String url) {
    HttpClient ...
}
public Integer loadIntFromNet(String url) {
    HttpClient ...
}
public String loadString(String name) {
    File file = new File(name);
    // read something
}
public String loadStringUtf8(String name) {
    File file = new File(name);
    // read something
}
public Integer loadInt(String name) {
    File file = new File(name);
    // read something
}
public String loadStringFromNet(String url) {
    HttpClient ...
}
public Integer loadIntFromNet(String url) {
    HttpClient ...
}

这类程序员往往是项目组里开发效率比较高的人,但是大量的业务开发工作导致他们不会做多余的思考,他们的口头禅是:“我每天要做 XX 个需求”或者“先做完需求再考虑其他的吧”。

这种反模式表现出来的后果往往是代码很难复用,面对 deadline 的时候,程序员迫切的想要把需求落实成代码,而这往往也会是个循环:写代码的时候来不及考虑复用,代码难复用导致之后的需求还要继续写大量的代码。

一点点积累起来的大量的代码又带来了组织和风格一致性等问题,最后形成了一个新功能基本靠拷的遗留系统。

2.5 还有吗

烂代码还有很多种类型,沿着功能-性能-可读-可测试-可扩展这条路线走下去,还能看到很多匪夷所思的例子。

那么什么是烂代码?个人认为,烂代码包含了几个层次:

  • 如果只是一个人维护的代码,满足功能和性能要求倒也足够了。
  • 如果在一个团队里工作,那就必须易于理解和测试,让其它人员有能力修改各自的代码。
  • 同时,越是处于系统底层的代码,扩展性也越重要。

所以,当一个团队里的底层代码难以阅读、耦合了上层的逻辑导致难以测试、或者对使用场景做了过多的假设导致难以复用时,虽然完成了功能,它依然是垃圾代码。

2.6 够用的代码

而相对的,如果一个工程的代码难以阅读,能不能说这个是烂代码?很难下定义,可能算不上好,但是能说它烂吗?如果这个工程自始至终只有一个人维护,那个人也维护的很好,那它似乎就成了“够用的代码”。

很多工程刚开始可能只是一个人负责的小项目,大家关心的重点只是代码能不能顺利的实现功能、按时完工。

过上一段时间,其他人参与时才发现代码写的有问题,看不懂,不敢动。需求方又开始催着上线了,怎么办?只好小心翼翼的只改逻辑而不动结构,然后在注释里写上这么实现很 ugly,以后明白内部逻辑了再重构。

再过上一段时间,有个相似的需求,想要复用里面的逻辑,这时才意识到代码里做了各种特定场景的专用逻辑,复用非常麻烦。为了赶进度只好拷代码然后改一改。问题解决了,问题也加倍了。

几乎所有的烂代码都是从“够用的代码”演化来的,代码没变,使用代码的场景发生变了,原本够用的代码不符合新的场景,那么它就成了烂代码。

3、重构不是万能药

程序员最喜欢跟程序员说的谎话之一就是:现在进度比较紧,等 X 个月之后项目进度宽松一些再去做重构。

不能否认在某些(极其有限的)场景下重构是解决问题的手段之一,但是写了不少代码之后发现,重构往往是程序开发过程中最复杂的工作。花一个月写的烂代码,要花更长的时间、更高的风险去重构。

曾经经历过几次忍无可忍的大规模重构,每一次重构之前都是找齐了组里的高手,开了无数次分析会,把组内需求全部暂停之后才敢开工,而重构过程中往往哀嚎遍野,几乎每天都会出上很多意料之外的问题,上线时也几乎必然会出几个问题。

从技术上来说,重构复杂代码时,要做三件事:理解旧代码、分解旧代码、构建新代码。而待重构的旧代码往往难以理解;模块之间过度耦合导致牵一发而动全身,不易控制影响范围;旧代码不易测试导致无法保证新代码的正确性。

这里还有一个核心问题,重构的复杂度跟代码的复杂度不是线性相关的。比如有 1000 行烂代码,重构要花 1 个小时,那么 5000 行烂代码的重构可能要花 2、3 天。要对一个失去控制的工程做重构,往往还不如重写更有效率。

而抛开具体的重构方式,从受益上来说,重构也是一件很麻烦的事情:它很难带来直接受益,也很难量化。这里有个很有意思的现象,基本关于重构的书籍无一例外的都会有独立的章节介绍“如何向 boss 说明重构的必要性”。

重构之后能提升多少效率?能降低多少风险?很难答上来,烂代码本身就不是一个可以简单的标准化的东西。

举个例子,一个工程的代码可读性很差,那么它会影响多少开发效率?

你可以说:之前改一个模块要 3 天,重构之后 1 天就可以了。但是怎么应对“不就是做个数据库操作吗为什么要 3 天”这类问题?烂代码“烂”的因素有不确定性、开发效率也因人而异,想要证明这个东西“确实”会增加两天开发时间,往往反而会变成“我看了 3 天才看懂这个函数是做什么的”或者“我做这么简单的修改要花 3 天”这种神经病才会去证明的命题。

而另一面,许多技术负责人也意识到了代码质量和重构的必要性,“那就重构嘛”,或者“如果看到问题了,那就重构”。上一个问题解决了,但实际上关于重构的代价和收益仍然是一笔糊涂账,在没有分配给你更多资源、没有明确的目标、没有具体方法的情况下,很难想象除了有代码洁癖的人还有谁会去执行这种莫名 其妙的任务。

于是往往就会形成这种局面:

  • 不写代码的人认为应该重构,重构很简单,无论新人还是老人都有责任做重构。
  • 写代码老手认为应该迟早应该重构,重构很难,现在凑合用,这事别落在我头上。
  • 写代码的新手认为不出 bug 就谢天谢地了,我也不知道怎么重构。

4、写好代码很难

与写出烂代码不同的是,想写出好代码有很多前提:

  • 理解要开发的功能需求。
  • 了解程序的运行原理。
  • 做出合理的抽象。
  • 组织复杂的逻辑。
  • 对自己开发效率的正确估算。
  • 持续不断的练习。

写出好代码的方法论很多,但我认为写出好代码的核心反而是听起来非常 low 的“持续不断的练习”。这里就不展开了,留到下篇再说。

很多程序员在写了几年代码之后并没有什么长进,代码仍然烂的让人不忍直视,原因有两个主要方面:

  • 环境是很重要的因素之一,在烂代码的熏陶下很难理解什么是好代码,知道的人大部分也会选择随波逐流。
  • 还有个人性格之类的说不清道不明的主观因素,写出烂代码的程序员反而都是一些很好相处的人,他们往往热爱公司团结同事平易近人工作任劳任怨–只是代码很烂而已。

而工作几年之后的人很难再说服他们去提高代码质量,你只会反复不断的听到:“那又有什么用呢?”或者“以前就是这么做的啊?”之类的说法。

那么从源头入手,提高招人时对代码的质量的要求怎么样?

前一阵面试的时候增加了白板编程、最近又增加了上机编程的题目。发现了一个现象:一个人工作了几年、做过很多项目、带过团队、发了一些文章,不一定能代表他代码写的好反之,一个人代码写的好,其它方面的能力一般不会太差

举个例子,最近喜欢用“写一个代码行数统计工具”作为面试的上机编程题目。很多人看到题目之后第一反映是,这道题太简单了,这不就是写写代码嘛。

从实际效果来看,这道题识别度却还不错。

首先,题目足够简单,即使没有看过《面试宝典》之类书的人也不会吃亏。而题目的扩展性很好,即使提前知道题目,配合不同的条件,可以变成不同的题目。比如要求按文件类型统计行数、或者要求提高统计效率、或者统计的同时输出某些单词出现的次数,等等。

从考察点来看,首先是基本的树的遍历算法;其次有一定代码量,可以看出程序员对代码的组织能力、对问题的抽象能力;上机编码可以很简单的看出应聘者是不是很久没写程序了;还包括对于程序易用性和性能的理解。

最重要的是,最后的结果是一个完整的程序,我可以按照日常工作的标准去评价程序员的能力,而不是从十几行的函数里意淫这个人在日常工作中大概会有什么表现。

但即使这样,也很难拍着胸脯说,这个人写的代码质量没问题。毕竟面试只是代表他有写出好代码的能力,而不是他将来会写出好代码。

5、悲观的结语

说了那么多,结论其实只有两条,作为程序员:

  • 不要奢望其他人会写出高质量的代码
  • 不要以为自己写出来的是高质量的代码

如果你看到了这里还没有丧失希望,那么可以期待一下这篇文章的第二部分,关于如何提高代码质量的一些建议和方法。

转载请注明出处:代码说 » 关于烂代码的那些事 - 为什么每个团队存在大量烂代码

IK分词器原理与源码分析

引言

做搜索技术的不可能不接触分词器。个人认为为什么搜索引擎无法被数据库所替代的原因主要有两点,一个是在数据量比较大的时候,搜索引擎的查询速度快,第二点在于,搜索引擎能做到比数据库更理解用户。第一点好理解,每当数据库的单个表大了,就是一件头疼的事,还有在较大数据量级的情况下,你让数据库去做模糊查询,那也是一件比较吃力的事(当然前缀匹配会好得多),设计上就应当避免。关于第二点,搜索引擎如何理解用户,肯定不是简单的靠匹配,这里面可以加入很多的处理,甚至加入各种自然语言处理的高级技术,而比较通用且基本的方法就是靠分词器来完成,而且这是一种比较简单而且高效的处理方法。

分词技术是搜索技术里面的一块基石。很多人用过,如果你只是为了简单快速地搭一个搜索引擎,你确实不用了解太深。但一旦涉及效果问题,分词器上就可以做很多文章。例如, 在实我们际用作电商领域的搜索的工作中,类目预判的实现就极须依赖分词,至少需要做到可以对分词器动态加规则。再一个简单的例子,如果你的优化方法就是对不同的词分权重,提高一些重点词的权重的话,你就需要依赖并理解分词器。本文将根据ik分配器的原码对其实现做一定分析。其中的重点,主要3点,1、词典树的构建,即将现在的词典加载到一个内存结构中去, 2、词的匹配查找,也就相当生成对一个句话中词的切分方式,3、歧义判断,即对不同切分方式的判定,哪种应是更合理的。 代码原网址为:[https://code.google.com/p/ik-analyzer/](https://code.google.com/p/ik-analyzer/) 已上传github,可访问:[https://github.com/quentinxxz/Search/tree/master/IKAnalyzer2012FF_hf1_source/](https://github.com/quentinxxz/Search/tree/master/IKAnalyzer2012FF_hf1_source/)

词典

做后台数据相关操作,一切工作的源头都是数据来源了。IK分词器为我们词供了三类词表分别是:1、主词表 main2012.dic 2、量词表quantifier.dic 3、停用词stopword.dic。
Dictionary为字典管理类中,分别加载了这个词典到内存结构中。具体的字典代码,位于org.wltea.analyzer.dic.DictSegment。 这个类实现了一个分词器的一个核心数据结构,即Tire Tree。

Tire Tree(字典树)是一种结构相当简单的树型结构,用于构建词典,通过前缀字符逐一比较对方式,快速查找词,所以有时也称为前缀树。具体的例子如下。
tireTree.jpg

图1
从左来看,abc,abcd,abd,b,bcd…..这些词就是存在树中的单词。当然中文字符也可以一样处理,但中文字符的数目远多于26个,不应该以位置代表字符(英文的话,可以每节点包完一个长度为26的数组),如此的话,这棵tire tree会变得相当扩散,并占用内存,因而有一个tire Tree的变种,三叉字典树(Ternary Tree),保证占用较小的内存。Ternary Tree不在ik分词器中使用,所以不在此详述,请参考文章http://www.cnblogs.com/rush/archive/2012/12/30/2839996.html
IK中采用的是一种比方简单的实现。先看一下,DictSegment类的成员:

class DictSegment implements Comparable<DictSegment>{  
  
    //公用字典表,存储汉字  
    private static final Map<Character , Character> charMap = new HashMap<Character , Character>(16 , 0.95f);  
    //数组大小上限  
    private static final int ARRAY_LENGTH_LIMIT = 3;  
  
      
    //Map存储结构  
    private Map<Character , DictSegment> childrenMap;  
    //数组方式存储结构  
    private DictSegment[] childrenArray;  
  
  
    //当前节点上存储的字符  
    private Character nodeChar;  
    //当前节点存储的Segment数目  
    //storeSize <=ARRAY_LENGTH_LIMIT ,使用数组存储, storeSize >ARRAY_LENGTH_LIMIT ,则使用Map存储  
    private int storeSize = 0;  
    //当前DictSegment状态 ,默认 0 , 1表示从根节点到当前节点的路径表示一个词  
    private int nodeState = 0;    
    ……  

这里有两种方式去存储,根据ARRAY_LENGTH_LIMIT作为阈值来决定,如果当子节点数,不太于阈值时,采用数组的方式childrenArray来存储,当子节点数大于阈值时,采用Map的方式childrenMap来存储,childrenMap是采用HashMap实现的。这样做好处在于,节省内存空间。因为HashMap的方式的方式,肯定是需要预先分配内存的,就可能会存在浪费的现象,但如果全都采用数组去存组(后续采用二分的方式查找),你就无法获得O(1)的算法复杂度。所以这里采用了两者方式,当子节点数很少时,用数组存储,当子结点数较多时候,则全部迁至hashMap中去。在构建过程中,会将每个词一步步地加入到字典树中,这是一个递归的过程:

/** 
 * 加载填充词典片段 
 * @param charArray 
 * @param begin 
 * @param length 
 * @param enabled 
 */  
private synchronized void fillSegment(char[] charArray , int begin , int length , int enabled){  

     ……       
    //搜索当前节点的存储,查询对应keyChar的keyChar,如果没有则创建  
    DictSegment ds = lookforSegment(keyChar , enabled);  
    if(ds != null){  
        //处理keyChar对应的segment  
        if(length > 1){  
            //词元还没有完全加入词典树  
            ds.fillSegment(charArray, begin + 1, length - 1 , enabled);  
        }else if (length == 1){  
            //已经是词元的最后一个char,设置当前节点状态为enabled,  
            //enabled=1表明一个完整的词,enabled=0表示从词典中屏蔽当前词  
            ds.nodeState = enabled;  
        }  
    }  

}  

其中lookforSegment,就会在所在子树的子节点中查找,如果是少于ARRAY_LENGTH_LIMIT阈值,则是为数组存储,采用二分查找;如果大于ARRAY_LENGTH_LIMIT阈值,则为HashMap存储,直接查找。

词语切分

IK分词器,基本可分为两种模式,一种为smart模式,一种为非smart模式。例如原文:
张三说的确实在理
smart模式的下分词结果为:
张三 | 说的 | 确实 | 在理
而非smart模式下的分词结果为:
张三 | 三 | 说的 | 的确 | 的 | 确实 | 实在 | 在理
可见非smart模式所做的就是将能够分出来的词全部输出;smart模式下,IK分词器则会根据内在方法输出一个认为最合理的分词结果,这就涉及到了歧义判断。

首来看一下最基本的一些元素结构类:

public class Lexeme implements Comparable<Lexeme>{  
    ……  
  
    //词元的起始位移  
    private int offset;  
    //词元的相对起始位置  
    private int begin;  
    //词元的长度  
    private int length;  
    //词元文本  
    private String lexemeText;  
    //词元类型  
    private int lexemeType;  
     ……  

这里的Lexeme(词元),就可以理解为是一个词语或个单词。其中的begin,是指其在输入文本中的位置。注意,它是实现Comparable的,起始位置靠前的优先,长度较长的优先,这可以用来决定一个词在一条分词结果的词元链中的位置,可以用于得到上面例子中分词结果中的各个词的顺序。

/* 
* 词元在排序集合中的比较算法 
* @see java.lang.Comparable#compareTo(java.lang.Object) 
*/  
public int compareTo(Lexeme other) {  
//起始位置优先  
    if(this.begin < other.getBegin()){  
        return -1;  
    }else if(this.begin == other.getBegin()){  
     //词元长度优先  
     if(this.length > other.getLength()){  
         return -1;  
     }else if(this.length == other.getLength()){  
         return 0;  
     }else {//this.length < other.getLength()  
         return 1;  
     }  
       
    }else{//this.begin > other.getBegin()  
     return 1;  
    }  
}  

还有一个重要的结构就是词元链,声明如下

/** 
 * Lexeme链(路径) 
 */  
class LexemePath extends QuickSortSet implements Comparable<LexemePath> 

一条LexmePath,你就可以认为是上述分词的一种结果,根据前后顺序组成一个链式结构。可以看到它实现了QuickSortSet,所以它本身在加入词元的时候,就在内部完成排序,形成了一个有序的链,而排序规则就是上面Lexeme的compareTo方法所实现的。你也会注意到,LexemePath也是实现Comparable接口的,这就是用于后面的歧义分析用的,下一节介绍。
另一个重要的结构是AnalyzeContext,这里面就主要存储了输入信息 的文本,切分出来的lemexePah ,分词结果等一些相关的上下文信息。
IK中默认用到三个子分词器,分别是LetterSegmenter(字母分词器),CN_QuantifierSegment(量词分词器),CJKSegmenter(中日韩分词器)。分词是会先后经过这三个分词器,我们这里重点根据CJKSegment分析。其核心是一个analyzer方法。

public void analyze(AnalyzeContext context) {  
    …….  
          
        //优先处理tmpHits中的hit  
        if(!this.tmpHits.isEmpty()){  
            //处理词段队列  
            Hit[] tmpArray = this.tmpHits.toArray(new Hit[this.tmpHits.size()]);  
            for(Hit hit : tmpArray){  
                hit = Dictionary.getSingleton().matchWithHit(context.getSegmentBuff(), context.getCursor() , hit);  
                if(hit.isMatch()){  
                    //输出当前的词  
                    Lexeme newLexeme = new Lexeme(context.getBufferOffset() , hit.getBegin() , context.getCursor() - hit.getBegin() + 1 , Lexeme.TYPE_CNWORD);  
                    context.addLexeme(newLexeme);  
                      
                    if(!hit.isPrefix()){//不是词前缀,hit不需要继续匹配,移除  
                        this.tmpHits.remove(hit);  
                    }  
                      
                }else if(hit.isUnmatch()){  
                    //hit不是词,移除  
                    this.tmpHits.remove(hit);  
                }                     
            }  
        }             
          
        //*********************************  
        //再对当前指针位置的字符进行单字匹配  
        Hit singleCharHit = Dictionary.getSingleton().matchInMainDict(context.getSegmentBuff(), context.getCursor(), 1);  
        if(singleCharHit.isMatch()){//首字成词  
            //输出当前的词  
            Lexeme newLexeme = new Lexeme(context.getBufferOffset() , context.getCursor() , 1 , Lexeme.TYPE_CNWORD);  
            context.addLexeme(newLexeme);  

            //同时也是词前缀  
            if(singleCharHit.isPrefix()){  
                //前缀匹配则放入hit列表  
                this.tmpHits.add(singleCharHit);  
            }  
        }else if(singleCharHit.isPrefix()){//首字为词前缀  
            //前缀匹配则放入hit列表  
            this.tmpHits.add(singleCharHit);  
        }  
   ……  
}  

从下半截代码看起,这里的matchInMain就是用于匹配主题表内的词的方法。这里的主词表已经加载至一个字典树之内,所以整个过程也就是一个从树根层层往下走的一个层层递归的方式,但这里只处理单字,不会去递归。而匹配的结果一共三种UNMATCH(未匹配),MATCH(匹配), PREFIX(前缀匹配),Match指完全匹配已经到达叶子节点,而PREFIX是指当前对上所经过的匹配路径存在,但未到达到叶子节点。此外一个词也可以既是MATCH也可以是PREFIX,例如图1中的abc。前缀匹配的都被存入了tempHit中去。而完整匹配的都存入context中保存。
继续看上半截代码,前缀匹配的词不应该就直接结束,因为有可能还能往后继续匹配更长的词,所以上半截代码所做的就是对这些词继续匹配。matchWithHit,就是在当前的hit的结果下继续做匹配。如果得到MATCH的结果,便可以在context中加入新的词元。
通过这样不段匹配,循环补充的方式,我们就可以得到所有的词,至少能够满足非smart模式下的需求。

歧义判断

IKArbitrator(歧义分析裁决器)是处理歧义的主要类。
如果觉着我这说不清,也可以参考的博客:http://fay19880111-yeah-net.iteye.com/blog/1523740

在上一节中,我们提到LexemePath是实现compareble接口的。

public int compareTo(LexemePath o) {  
    //比较有效文本长度  
    if(this.payloadLength > o.payloadLength){  
        return -1;  
    }else if(this.payloadLength < o.payloadLength){  
        return 1;  
    }else{  
        //比较词元个数,越少越好  
        if(this.size() < o.size()){  
            return -1;  
        }else if (this.size() > o.size()){  
            return 1;  
        }else{  
            //路径跨度越大越好  
            if(this.getPathLength() >  o.getPathLength()){  
                return -1;  
            }else if(this.getPathLength() <  o.getPathLength()){  
                return 1;  
            }else {  
                //根据统计学结论,逆向切分概率高于正向切分,因此位置越靠后的优先  
                if(this.pathEnd > o.pathEnd){  
                    return -1;  
                }else if(pathEnd < o.pathEnd){  
                    return 1;  
                }else{  
                    //词长越平均越好  
                    if(this.getXWeight() > o.getXWeight()){  
                        return -1;  
                    }else if(this.getXWeight() < o.getXWeight()){  
                        return 1;  
                    }else {  
                        //词元位置权重比较  
                        if(this.getPWeight() > o.getPWeight()){  
                            return -1;  
                        }else if(this.getPWeight() < o.getPWeight()){  
                            return 1;  
                        }  
                          
                    }  
                }  
            }  
        }  
    }  
    return 0;  
}  
显然作者在这里定死了一些排序的规则,依次比较有效文本长度、词元个数、路径跨度…..

IKArbitrator有一个judge方法,对不同路径做了比较。

private LexemePath judge(QuickSortSet.Cell lexemeCell , int fullTextLength){  
    //候选路径集合  
    TreeSet<LexemePath> pathOptions = new TreeSet<LexemePath>();  
    //候选结果路径  
    LexemePath option = new LexemePath();  
      
    //对crossPath进行一次遍历,同时返回本次遍历中有冲突的Lexeme栈  
    Stack<QuickSortSet.Cell> lexemeStack = this.forwardPath(lexemeCell , option);  
      
    //当前词元链并非最理想的,加入候选路径集合  
    pathOptions.add(option.copy());  
      
    //存在歧义词,处理  
    QuickSortSet.Cell c = null;  
    while(!lexemeStack.isEmpty()){  
        c = lexemeStack.pop();  
        //回滚词元链  
        this.backPath(c.getLexeme() , option);  
        //从歧义词位置开始,递归,生成可选方案  
        this.forwardPath(c , option);  
        pathOptions.add(option.copy());  
    }  
      
    //返回集合中的最优方案  
    return pathOptions.first();  
}  

其核心处理思想是从第一个词元开始,遍历各种路径,然后加入至一个TreeSet中,实现了排序,取第一个即可。

其它说明

1、stopWord(停用词),会在最后输出结果的阶段(AnalyzeContext. getNextLexeme)被移除,不会在分析的过程中移除,否则也会存在风险。
2、可以从LexemePath的compareTo方法中看出,Ik的排序方法特别粗略,如果比较发现path1的词个数,比path2的个数少,就直接判定path1更优。其实这样的规则,并未完整的参考各个分出来的词的实际情况,我们可能想加入每个词经统计出现的频率等信息,做更全面的打分,这样IK原有的比较方法就是不可行的。
关于如何修改的思路可以参考另一篇博客,其中介绍了一种通过最短路径思路去处理的方法:http://www.hankcs.com/nlp/segment/n-shortest-path-to-the-java-implementation-and-application-segmentation.html

3、未匹配的单字,不论是否在smart模式下,最后都会输出,其处理时机在最后输出结果阶段,具体代码位于在AnalyzeContext. outputToResult方法中。

转载请注明出处:代码说 » IK分词器原理与源码分析

Joyent CTO谈容器在2016年亟需改变的问题

为了在云时代取得领先地位,Joyent 将赌注压在容器上。本文由 Joyent CTO Bryan Cantrill 接受 TechTarget 采访,深度介绍关于容器技术 2015 年的现状和亟需改变的问题。

Joyent 在过去的十年中一直在生产环境中实践 OS 容器技术,当 Docker 横空出世之时,Joyent 是最早投身这种技术的公司之一。去年年底,Joyent 把自己重新打造成了“原生容器架构”,从那时起,他们发布了多种产品,包括 Triton——用于管理 Docker 容器的容器基础设施;以及 Containerbuddy,可以把传统服务容器化,使它们可以在任何地方运行的。

Joyent 同时还是“原生云计算基金会”的创始成员之一,该基金会于 7 月成立,目的在于创造原生云应用和容器的参考架构。 Joyent 的 CTO Bryan Cantrill 是该基金会技术指导委员会成员。他最近和 SearchCloudComputing 谈到了容器技术的现状,他对未来一年期待,以及亟需改变的问题。

With containers there’s more opportunity and arguably more peril because they allow you to change the way you think about the problems.

Bryan Cantrill

CTO, Joyent Inc.,

问:容器在 2015 年持续受到很多关注,但是仍然存在不少问题需要解决,这方面你怎么看?

每个人都把容器看做未来,因此也引起了大量的关注。在十几年前的虚拟机革命过程中,人们大多时候不需要改变思维习惯,只需要把运行环境分成物理层及虚拟层来看。但对于容器来说,一方面机会更多,而同时存在着不少陷阱,因为容器改变之前十几年那种固化的思维习惯。

问:有些人会认为容器之所以能够受欢迎是因为它们有些类似于虚拟机?

没错,但是他们会发现容器只是和容器所在的环境水平相当。你有了能在生产中盛东西的容器并不代表你就有了服务发现。当我们把宠物转变成牲畜时,容器的意义可远远不止于改变基底。容器的真正意义在于更快速和简单地构建大规模系统,但是同时意味你需要解决如何有效地把现有系统转变为微服务的问题。

问:现在容器市场的成熟度如何?人们对容器技术的理解水平如何?

KubeCon(最近谷歌的 Kubernetes 社区举行的大会)提出的问题之一就是:“容器领域的状态接近峰值混乱了吗?” 有趣的是,每个和我交谈的人——无论他们是开发、运维还是云厂商—— 都认为我们没有进入峰值混乱。他们仍然期待成长,甚至有些人期待着加速成长。

我不知道我们应该期待容器技术在近期达到多大程度的稳定性,因为所有的技术都是开源的。容器领域存在着各种各样的框架和设计哲学,因此也存在较多混乱以及和简洁相悖的气质。直白一点说,这里有很多竞争白热化地带,在这种情况下,如果你故意想要进入尽量多的领域,那么区分每个区域的边界就会很难。

问:容器即服务(CaaS)是我们从许多云厂商那里看到的最新趋势,你是否担心某些厂商和容器带来的私有概念会让服务的跨平台可移植性大打折扣?

人们确实对华而不实的容器即服务抱有成见。他们想要自己使用容器来构建东西。我们的信念就是,我们需要原生容器基础设施—— 并不是在虚拟机内部供应,而是在纯粹的容器上供应。在这种情况下,如果容器有一个 IP 地址的话某些事情就会简单得多,而你也不需要完成任何愚蠢的映射特技了。

我的确认为有些容器服务,特别是那些期望将容器运行在 VM 内部的服务,只是现有的虚拟机专家想要在容器领域“到此一游”而已。他们并不理解容器革命背后隐含的经济学原理。

问:容器市场接下来需要发生什么样的变化?

概念的扩张需要放缓一些,或者我们需要沉淀一些东西,让这些技术在生产环境中做出有意义的东西来。但是这方面我们还一筹莫展……

你可以真正在生产环境部署 Triton。你可以用 Docker Compose 来支持适应性比较强的服务,但是除了 Triton 以外,很多技术感觉上还很稚嫩和原始,而这样的技术也招来了一些恶名。人们不应该再吹嘘自己收到多少代码提交,拥有多少贡献者以及下载量了。对于我来说,这样做从某种程度上代表着混淆视听。如果你面临着一两万个问题,你怎么能忍受项目上的任何东西以这样的速度迅速扩张呢?

到 2017 年,我们至少会在心里巩固某些概念,并不是说未来将会产生某些概念一统天下的情况,而是说我们将会更好地了解某些概念在哪些方面更合适,在哪些方面不合适。进一步的技术突变也很有可能会发生,尽管如此,我仍然相信我们将会看到更多对稳健性的增强,以及更少的不间断扩张。

更多「架构」容器文章

英文原文: http://searchcloudcomputing.techtarget.com/news/4500258178/Joyent-CTO-talks-Docker-containers-and-the-work-ahead-in-2016

转载请注明出处:代码说 » Joyent CTO谈容器在2016年亟需改变的问题

微博基于Docker的混合云平台设计与实践

王关胜,微博研发中心运维架构师。2011 年初加入新浪,一直负责微博平台&大数据等业务线的运维保障工作,包括产品稳定性,运维基础设施建设,工具建设等。致力于推进 Docker 在微博的应用,参与建设微博混合云平台 DCP。擅长大规模分布式系统集群的管理与运维,疑难问题分析,故障定位与处理等。对运维工具平台建设、监控、应用性能跟踪及分析、数据化运维等方面有深入的研究。

2011 年初,新浪微博进入快速发展期,同时也开启平台化的进程,服务器设备,及人力成本大量增加,业务的快速发展,促使运维团队建立了一套完整的运维平台。虽然已稳定运行了 3 年,但随着公有云的逐渐成熟,Docker Container 技术的兴起,一时间各大型企业纷纷开始升级内部运维系统,提供自动化能力的同时,更注重弹性调度。我们也于 2014 年底构建了第一版基于 Docker 的运维平台,并在元旦,春节,红包飞等大型活动中得到了考验。但是要想更好的应对微博的这种业务场景,系统局限性还很多,比如设备申请慢,业务负载饱和度不一,扩缩容流程繁琐且时间长,基于此出发点,2015 年技术团队设计与实现了一套基于 Docker 的混合云平台 DCP。

微博的业务场景及混合云背景

微博目前有 8 亿注册用户,单日活跃用户数达 1 亿多。微博总体分为端和后端平台,端上主要是 PC 端,移动端和第三方开发者,后端平台主要是 Java 编写的各种接口层,服务层及存储层。就前端来说,每日超过 600 亿次的 API 调用,超过万亿的 RPC 调用,产生的日志就达百 T+。对于这么大体量的业务系统,对于运维的要求也很严格,就接口层来说,SLA 必须达到 4 个 9,且接口平均响应时间不能高于 50ms。因此技术团队会面临下述各种各样的挑战。

  • 产品功能迭代快,代码变更频繁
  • 业务模块多,且依赖关系复杂
  • 突发的热点事件,如典型的#周一见# #马航370# #刘翔摔倒# #明星丑闻#
  • 大型活动及春节、元旦保障,如红包飞

下面具体看下春晚时的业务模型:


每年的元旦,春晚,红包飞会带来巨大的流量挑战,这些业务场景的主要特点是: 瞬间峰值高,互动时间短。基本上一次峰值事件,互动时间都会 3 小时以内,而明星突发新闻事件,红包飞这种业务,经常会遇到高达多倍的瞬间峰值。传统的应对手段,主要是靠提前申请足够的设备,保证冗余;降级非核心及周边的业务;生扛这三种手段。这么做,除了成本高外,对系统进行水平扩容时,耗费的时间也久。

除了来自业务的挑战,在应对峰值事件时,对于运维的挑战也蛮大的。遇到难点较多的环节包括:设备申请太麻烦,时间长;扩缩容流程繁琐。

要完成一次具体的扩容,首先基础运维从采购拿到新机器,录入 CMDB,再根据业务运维提的需求,进行上架到相应的 IDC,机架,操作系统安装,网络配置,最后分给相应的业务运维,就是一台完整的可以登录的机器了。其次,业务运维拿到机器,需要对机器进行初始化配置,并继续服务部署。服务部署好后,经过 check,再挂到负载均衡上,引入流量。设备坏了自动报修,过期下架或替换。具体可看下图

这种扩容的方式,除了流程繁琐外,还经常遇到各服务器间环境差异,无法充分利用服务器硬件资源,即使有多余的服务器也无法灵活调度。若采取新申请设备,在大一点的公司申请流程通常较长。一般设备申请是业务方及业务运维发起采购提案,然后相关方进行架构评审,评审通过,则由 IT 管委会评审,再由决策及成本部门评审,评审通过,进入采购流程,再上架,还经常遇到机房机架位不足,这些都导致交付周期变得很长。

除了业务扩容的繁琐,公司内设备利用率也不均衡,主要表现在三个地方:

  1. 各个业务组的服务器利用率各不相同,大家对利用率的理解不一致,导致有些设备未能得到充分利用,这也会导致更大的成本压力。
  2. 各个业务模型不同,比如有的业务高峰是在晚 22 – 23 点,有的业务则在中午。
  3. 在线业务与离线业务完全分离,导致成本也高,对于离线业务,可在低峰继续跑在线业务

正因为这些挑战,怎么能更好的解决,技术团队想到基于 Docker,及公有云来构建一套具有弹性伸缩的混合云系统。利用过去的私有云加公有云,以及公司内闲置的运算资源,混合云,兼具安全性与弹性扩展能力。其特点如下图

有了这套混合云系统后,不仅能很好的整合内部运算资源,解决内部的弹性需求外,当系统面临流量剧增的峰值事件时,也可以将过多的流量切入外部公有云,减轻内部系统的压力。

两种云内的设备如何使用呢?内部私有云设备安全性高,可控度也高,只要对硬件资源进行优化配置,用它来处理固定的工作负载。而公有云的设备则具有标准化,自动化的特性,可以快速因临时需求,在流量暴涨时,可以快速创建大量 ECS,扩容业务工作负载的能力。而对于公司,可以利用公有云这种按需付费的机制,减低硬件的运营成本。因此采用混合云架构,就可以兼具私有云及公有云的优点,可以同时拥有安全与弹性扩容能力,使业务工作负载可以在业务间进行漂移,低负载的业务就应该使用更少的设备,反之亦然。而基于 Docker 来做,对于上述情况来说,复杂度降低很多。

三大基础设施助力微博混合云

微博混合云系统不单只是一般的混合云,而是应用了 Docker,透过 Docker Container 快速部署的特性,解决海量峰值事件对微博系统带来的压力。过去公司在面对峰值事件,一般采取的作法是,首先评估需要多少额外设备,并向外部公有云申请机器分摊流量。然而,除了可能低估应付峰值事件所需的设备外,即使事先準备了足够的 VM,其部署时间成本也远高于 Docker,无法即时帮助公司分摊过多外部请求。

而微博 Docker Container 平台的混合云核心设计思想,主要是借鉴银行的运作机制:民众可以把钱存在银行,而需要使用金钱的时候,只需要提领一部分,剩余的存款银行可以拿去进行投资。而微博则借鉴银行的这一套运作模式,在内部设立一个运算资源共享池外,还导入了外部公有云。

而要微博实现高弹性调度资源的混合云架构,其中实现的关键则是 Docker。刚开始我们思考该要使用裸机还是 VM 架构,作为 Docker Container 的基础设施。后来,考量如果要采用 VM,内部机房架构要进行许多改造。所以,目前微博的内部私有云使用裸机部署,而外部公有云则采用阿里云弹性计算服务(ECS)的 VM 架构。等平台建设成熟后,还可以应用其他厂商公有云,如 AWS 等。

构建 Docker Container 平台基础架构的 3 大关键,包含 Docker 在裸机上的部署架构、改进版的 Docker Registry 以及负载均衡组件 Nginx Upsync 模块

第一是 Docker Container 的裸机部署方案,透过 IP 位址以及 Port 定义一个唯一的 Container 服务,而每台主机上则可以开启多个 Container,各个具有不同的功能。

每一个 Container 服务所产生的行为日志,会经由一个名为 Scribe 的 Container 集中收集。而集中后的数据则可进行用户行为分析。对于容器类监控数据,则是透过建立 CAdvisor Container,将 Container 运行产生的资料,传送至 ELK(Elasticsearch、Logstash 及 Kibana)开源监控软体,进行分析。对于业务数据,通过 Graphite,监控业务系统的运作状况。

第二则是 Docker Registry,我们使用 Docker 官方提供的 Docker Registry,构建了私有的 Registry Hub,并且透过这个私有调度 Docker Container 需要的镜像。

今年基于 V2 版的 Registry Hub,将存储引擎改为使用分布式开源存储平台 Ceph,并且在 Docker Registry 前端结合 Nginx,实作负载平衡功能。这个过程中,比较麻烦的是,在 Docker 版本升级过程中必须让系统能够兼容新旧版本的 Registry Hub,而前端 Nginx 可以分析系统需求,辨別要从新版本或是旧版本 Docker 仓库下载镜像。而外部公有云,则是透过镜像缓存 mirror,不必像私有云一样,部署完整的镜像仓库。

对于镜像服务,总共包含 3 层架构。包含最底层操作系统、中间层的运行环境如 Java、Tomcat,及最上层的 Container。而在调度 Container 时,透过使用 dockerignore 指令,减少不必要的文件、目录,借此减低映像档的容量。除此之外,在镜像标签命名上,我们则禁止使用「Latest」做为镜像标签。这是由于不同使用者对于标签的理解不一,可能会误以为是代表映像档最新的版本。

而第三则是独立研发的 nginx upsync 模块,去年刚开始使用 container 时,将container 挂到 nginx 后,必须通过重启或 reload 指令,使流量生效。而这个过过程中,nginx 对于特別大的并发流量会发生运行不稳定的情形,因此后来开发了 nginx upsync 模块,不通过 reload 指令重启,也可以保持系统稳定运作。针对两种模块进行比较,发现流量大时,用了 nginx upsync 模块,不做 reload,单机扛量多了 10% 的请求,且性能并不会降低。

Nginx upsync 的核心功能主要是:

  1. Fix Nginx reload时带来的服务抖动
  2. Fix Web Container的服务发现

项目已开源至 github 上:https://github.com/weibocom/nginx-upsync-module

其架构如下

微博混合云 DCP 系统设计核心:自动化,弹性调度

目前开发的 Docker Container 混合云平台,包含 4 层架构:主机层、调度层及业务编排层,最上层则是各业务方系统。底层的混合云基础架构则架构了专线,打通微博内部资料中心以及阿里云。

主要思想:来源于官方三驾马车(Machine + Swarm +Compose)

DCP 混合云系统的设计理念,总共包含 4 个核心概念:弹性伸缩、自动化、业务导向等。

DCP 系统,最核心 3 层架构:主机层、调度适配层及编排层。

主机层的核心是要调度运算资源。目前设计了资源共享池、Buffer 资源池,配额管理,及多租户管理机制,借此实现弹性调度资源。

而调度层则通过 API,把调度工具用 API 进行包装,而微博 4 种常用的调度工具组合包含:Docker、Swarm、Mesos 及自主开发的 Dispatch。

而最上层的则是负责业务编排及服务发现。编排层也包括了大数据工具Hadoop,进行大数据分析的业务场景。

对于调度来说,最重要的就是资源管理,Mesos 处理这个是最合适不过了,很多公司就专用其做资源管理,比如 Netflix 写了一个 Titan 调度服务,其底层资源管理则是通过 Mesos。在调度组件中,使用最多的还是 swarm,dispatch。

可以看下面这个场景:这也是私有云内部资源流转的最佳实践

当业务 A 多余的运算资源导入混合云共享池时,此时流量暴涨的业务 C,可从共享池调度业务 A 的运算资源,而峰值事件后,便可以把多余的运算资源归还至共享池。

DCP 对于物理主机的流转,基于资源共享池、Buffer 资源池,配额管理,及多租户管理机制,其实非常复杂,详情可以去看我在台湾 iThome 举办 Container summit 2015 技术大会上分享的内容。这里可以看一下一台物理主机的生命周期(状态流转图)

引入阿里云做为第三机房,实现弹性调度架构

起初,对于是否采用混合云的架构,技术团队也是做了一番考虑的。目前微博机房的部署架构总共分为 3 层,依次是最上层域名以及负载均衡层。中间则是Web 层,包含各种前端框架,RPC 层以及中间件(Middleware)。而最下层则包含 MySQL、Redis、HBase 等资源架构。

微博核心服务主要分布在 2 个机房,两者互相做为灾难备份用途,而第 3 个机房则采用外部阿里云。混合云架构总共有 2 种部署方案。

第 1 种部署方案,将阿里云视为数据中心,底层架构与微博内部机房一样部署。内部机房采用 nginx 做为负载均衡层,但外部公有云则 SLB 引入流量。其他阿里云的中间 Web 层则与内部机房架构一致。不过,出于存储数据需要考虑的因素较多,初期把阿里云做为应付大量峰值时的方案更为简单,存储永久性数据的MySQL、HBase暂不部署于阿里云。

而第 2 种部署方案则把应付峰值事件需要的弹性计算能力迁移到阿里云。第 2 种部署方案困难之处,需要把微博的内部业务进行改造,让微博中间 Web 层,直接对阿里云机房进行 RPC 调用。此种方案部署结构相较比较简单,也让混合云架构具有实现可行性。

而这 2 种方案都会依赖 VPC 网路,如果有没有专线,想实现公有云的弹性计算能力几乎是不可能。因为公网调度资源的延迟时间太高,无法应付微博大量的业务。因此,技术团队与跟阿里云合作,在两边建立了内部专线,让阿里云机房与微博的机房互通。

大规模集群操作自动化:设备申请,初始化,服务上线

微博 Docker Container 混合云 DCP 设计思想,核心目标就是透过自动化操作大规模集群,进行弹性调度资源的任务,要完成此任务,必须经过 3 个流程:设备申请、设备初始化及服务上线。

弹性扩容任务所需要的设备来源是内部的集群以及外部的阿里云。而申请到足够设备后,也必须对设备进行初始化、部署环境及配置管理。最后一步则是将服务上线,开始执行 Container 调度以及弹性扩容的任务。

第 1 步骤则是申请设备,而设备申请借鉴于银行机制的概念。私有云的设备来源,主要来自离线集群、低负载集群以及错峰时间空出的多余设备。具体来说:离线集群并不是时时刻刻都在执行运算,即使稍微减少计算资源,也不会对业务产生重大影响;而工作负载较低的集群,如果有多余的资源也可以进行调度。此外,微博有上百个业务线,每个业务线峰值出现的时间点都不一样,而在此种状况下,各业务也可以互相支援,共享多余的计算资源。

而不同业务的集群则各自独立,并且具有独立的资源池。集群内可以自由调度资源,例如,缩小 A 服务的规模,将多余的运算资源分派给 B 服务。若集群要调度外部资源,也有设计配额制度,控制每个集群分配到的资源。在集群外,必须看 Buffer 池是否有足够的资源,如果足够的话,可以直接将 Buffer 池内的设备初始化,进行使用。

反之,如果 Buffer 池内的资源不足,也必须查看是否有足够的配额,可以直接申请机器。当设备申请完成,直接分配给 Buffer 池后,随即被纳入 DCP 使用,所有可调度的主机只能通过集群自己的 Buffer 池。

第 2 步骤则是设备初始化;通过自己开发的工具系统,可以达到操作系统升级自动化、系统操作 API 化。而服务器所依赖的基础环境、需要的软体等环境配置,透过配置管理工具 Puppet,将需要的配置写成模块,并使用 RPM 机制打包,进行安装。

在初始化流程中,必须要支持软件反安装模式,例如,当A业务要调度设备时,其他业务在交付设备前,必须要先透过反安装程序,将要设备恢复为最原始的状态。

而目前业界中的一些做法也可以免去初始化的流程。例如导入为 Container 而生的容器操作系统,像 CoreOS、RancherOS 或 Red Hat Atomic。不过,虽然这样可以,但是此种做法的可维护度高,但是缺点在于迁移设备的成本较高。

第 3 步骤则是利用完成初始化的设备,开始进行服务上线。目前一线的运维人员,只要在系统页面上输入服务池名称、服务类型、Container 类型、需要 Container 数量以及镜像地址等参数,即可在 5 分钟内完成扩容,扩容完成后,系统也会产出完整的报告,列出扩容程序中,执行的步骤,以及每件任务的成功、失败状况。

总体来说:一键扩容流程如下

应用微博Docker混合云,不怕峰值事件

在用微博混合云 DCP 时,优先调度内部的共享池资源的运算资源。但是在红包飞、春晚时,则必须调度外部的运算资源。平日的正常状况下,阿里云只需提供几百个运算节点,并且在 5 到 10 分钟内完成部署任务。但是,面对春晚、红包飞的峰值流量,则要提供数千个节点。这对阿里云的机房也是有较高要求的。

目前 Docker 混合云 DCP 平台,去年 10 月完成第一版,Container 数量千级。截止发稿,基本上手机微博,微博平台,红包飞,在今年的春晚,都会基于此系统进行峰值应对。上线以来,达成了微博所设定每次水平扩容时间低于 5 分钟的目标。「有这样的弹性调度能力时,系统面对大型活动的峰值压力就小很多。」

转载请注明出处:代码说 » 微博基于Docker的混合云平台设计与实践

深入理解同步/异步与阻塞/非阻塞区别

「那谁」,codedump.info 博主,多年从事互联网服务器后台开发工作。

几年前曾写过一篇描写同步/异步以及阻塞/非阻塞的文章,最近再回头看,还存在一些理解和认知误区,于是重新整理一下相关的概念,希望对网络编程的同行能有所启发。

同步与异步

首先来解释同步和异步的概念,这两个概念与消息的通知机制有关。

举个例子,比如一个用户去银行办理业务,他可以自己去排队办理,也可以叫人代办,办完之后再告知用户结果。对于要办理这个银行业务的人而言,自己去办理是同步方式,而别人代办完毕再告知则是异步方式。

两者的区别在于,同步的方式下,操作者主动完成了这件事情;而异步方式下,调用指令发出后,操作马上就返回了,操作者并不能马上知道结果,而是等待所调用的异步过程(在这个例子中是帮忙代办的人)处理完毕之后,再通过通知手段(在代码中通常是回调函数)来告诉操作者结果。

 

在上图的异步 IO 模型中,应用程序调用完 aio_read 之后,不论是否有数据可读,这个操作都会马上返回,这个过程相当于这个例子中委托另一个人去帮忙代办银行业务的过程,当数据读完拷贝到用户内存之后,会发一个信号通知原进程告诉读数据操作已经完成(而不仅仅是有数据可读)。

阻塞与非阻塞

接着解释阻塞与非阻塞的概念。这两个概念与程序处理事务时的状态有关。

同样用前面的例子,当真正执行办理业务的人去办理银行业务时,前面可能已经有人排队等候。如果这个人一直从排队到办理完毕,中间都没有做过其他的事情,那么这个过程就是阻塞的,这个人当前只在做这么一件事情。

在上图中,应用程序发起 recvfrom 操作之后,要等待数据拷贝成功才能返回。整个过程中,不能做其它的操作,这个就是典型的阻塞 IO。

反之,如果这个人发现前面排队的人不少,于是选择了出去逛逛,过了一会再回来看看有没有轮到他的号被叫到,如果没有又继续出去逛,过一阵再回来看看……如此以往,这个过程就是非阻塞的。因为处理这个事情的人,在这整个过程中,并没有做到除了这件事之外不做别的事情,他的做法是反复的过来检测,如果还没有完成就下一次再次尝试完成这件事情。

上图与前面阻塞 IO 图的区别在于,当没有数据可读时,同样的 recvfrom 操作返回了错误码,表示当前没有可读数据。换言之,即使没有数据也不会一直让这个应用阻塞在这个调用上,这就是非阻塞 IO。

到了这里,可以先简单的小结一下这两组概念

  • 阻塞与非阻塞:区别在于完成一件事情时,当事情还没有完成时,处理这件事情的人除此之外不能再做别的事情;
  • 同步与异步:是自己去做这件事情,还是等别人做好了来通知有结果,然后再自己去拿结果。注意这里说的是拿结果,如果只是别人告诉你可以做某事,然后自己去操作,这种情况下也是同步的操作,在后面多路复用I/O中会进行阐述。

可见,两组概念不是一个维度的概念。我们把需要办理银行业务的人称为 A,把代办理的人称为 B,那么在 A 委托 B 办理业务的情况下,假设 A 在交代 B 帮忙办事之后,A 就去做别的事情,那么 A 并不存在针对办理银行业务这件事情而言是阻塞还是非阻塞,办理事务时阻塞与否,是针对真正需要办理这件事情的人,也就是这个例子里的 B。

与多路复用 I/O 的联系

前几年写这篇文章时,将多路复用 I/O 类的 select/poll 等和异步操作混为一谈,在这里特别做一些补充说明。

在以前笔者包括不少同行的理解中,就这个例子而言,列举下面情况,当去办理业务的人,需要排队时通常都会先去取号,拿到一个纸条的号码,然后等待银行叫号。在那个例子里面,曾经将银行叫号理解成 select 操作,把纸条比作向 select 注册的回调函数,一旦可以进行操作的条件满足,就会根据这个回调函数来通知办理人,然后办理人再去完成工作,因此 select 等多路复用操作是异步的行为。

但上面这种理解最大的错误在于,同步与异步的区别在于是不是要求办理者自己来完成,所有需要自己完成的操作都是同步操作,不管是注册了一个回调(这里的叫号小纸条)等待回调你,还是自己一直阻塞等待。

在上例中,虽然对需要办理业务的人而言,通过叫号小纸条,他可以等待银行的办理通知。等待的同时可以去别的事情,比如浏览手机上网,但只要可以办理该业务的条件满足,真正叫到号可以办理业务时,办理者是需要自己去完成办理的。

换言之,在完成一件事情时,需要区分处理两种状态:一是这个事情是不是可以做了(条件满足的消息,如 select 告诉你某个文件描述符可读),另外一个是否完成了这件事情(如调用 read/write 完成 IO 操作)。多路复用 IO 记录下来有哪些人在等待消息的通知,在条件满足时负责通知办理者,而完成这件事情还是需要办理者自己去完成。只要是自己去完成的操作,都是同步的操作

UNP 的 6.2 节中,最后对异步与同步做的总结非常准确。

POSIX defines these two terms as follows:

A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes.

An asynchronous I/O operation does not cause the requesting process to be blocked.

Using these definitions, the first four I/O models—blocking, nonblocking, I/O multiplexing, and signal-driven I/O—are all synchronous because the actual I/O operation (recvfrom) blocks the process. Only the asynchronous I/O model matches the asynchronous I/O definition.

转载请注明出处:代码说 » 深入理解同步/异步与阻塞/非阻塞区别

大数据盘点之Spark篇

谭政,Hulu 网大数据基础平台研发。曾在新浪微博平台工作过。专注于大数据存储和处理,对 Hadoop、HBase 以及 Spark 等等均有深入的了解。

Spark 最新的特性以及功能

2015 年中 Spark 版本从 1.2.1 升级到当前最新的 1.5.2,1.6.0 版本也马上要进行发布,每个版本都包含了许多的新特性以及重要的性能改进,我会按照时间顺序列举部分改进出来,希望大家对 Spark 版本的演化有一个稍微直观的认识。

由于篇幅关系,这次不能给大家一一讲解其中每一项改进,因此挑选了一些我认为比较重要的特性来给大家讲解。如有遗漏和错误,还请帮忙指正。

Spark 版本演化

首先还是先来看一下 Spark 对应版本的变化:

先来一个整体的介绍:1.2 版本主要集中于 Shuffle 优化, 1.3 版本主要的贡献是 DataFrame API, 1.4 版本引入 R API 并启动 Tungsten 项目阶段,1.5 版本完成了 Tungsten 项目的第一阶段,1.6 版本将会继续进行 Tungsten 项目的第二个阶段。而我下面则重点介绍 DataFrame API 以及 Tungsten 项目。

DataFrame 介绍

DataFrame API 是在 1.3.0 中引入的,其目的是为了统一 Spark 中对结构化数据的处理。在引入 DataFrame 之前,Spark 之有上针对结构化数据的 SQL 查询以及 Hive 查询。

这些查询的处理流程基本类似:查询串先需要经过解析器生成逻辑查询计划,然后经过优化器生成物理查询计划,最终被执行器调度执行。

而不同的查询引擎有不同的优化器和执行器实现,并且使用了不同的中间数据结构,这就导致很难将不同的引擎的优化合并到一起,新增一个查询语言也是非常艰难。

为了解决这个问题,Spark 对结构化数据表示进行了高层抽象,产生了 DataFrame API。简单来说 DataFrame 可以看做是带有 Schema 的 RDD(在1.3之前DataFrame 就叫做 SchemaRDD,受到 R 以及 Python 的启发改为 DataFrame这个名字)。

在 DataFrame 上可以应用一系列的表达式,最终生成一个树形的逻辑计划。这个逻辑计划将会经历 Analysis, Logical Optimization, Physical Planning 以及 Code Generation 阶段最终变成可执行的 RDD,如下图所示:

在上图中,除了最开始解析 SQL/HQL 查询串不一样之外,剩下的部分都是同一套执行流程,在这套流程上 Spark 实现了对上层 Spark SQL, Hive SQL, DataFrame 以及 R 语言的支持。

下面我们来看看这些语言的简单示例:
Spark SQL : val count = sqlContext.sql(“SELECT COUNT(*) FROM records”).collect().head.getLong(0)

各个语言的使用方式都很类似。除了类 SQL 的表达式操作之外,DataFrame 也提供普通的类似于 RDD 的转换,例如可以写如下代码:

另外还值得一提的是,和 DataFrame API 紧密相关的 API — DataSource API。如果说 DataFrame API 提供的是对结构化数据的高层抽象,那么 DataSource API 提供的则是对于结构化数据统一的读写接口。

DataSource API 支持从 JSON, JDBC, ORC, parquet 中加载结构化数据 (SQLContext 类中的诸多读取方法,均会返回一个 DataFrame 对象),也同时支持将 DataFrame 的数据写入到上述数据源中 (DataFrame 中的 save 系列方法 )。

这两个 API 再加上层多种语言的支持,使得 Spark 对结构化数据拥有强大的处理能力,极大简化了用户编程工作。

Tungsten 项目介绍

在官方介绍中 Tungsten 将会是对 Spark 执行引擎所做的最大的修改,其主要目标是改进 Spark 内存和 CPU 的使用效率,尽可能发挥出机器硬件的最大性能。

之所以将优化的重点集中在内存和 CPU 而不是 IO 之上是社区实践发现现在很多的大数据应用的瓶颈在 CPU 。例如目前很多网络 IO 链路的速度达到 10Gbps,SSD 硬盘和 Striped HDD 阵列的使用也使得磁盘 IO 也有较大提升。而 CPU 的主频却没有多少提升,CPU 核数的增长也不如前两者迅速。

此外在 Spark 已经对 IO 做过很多的优化(如列存储以及 IO 剪枝可以减少 IO的数据量,优化的 Shuffle 改善了 IO 和网络的传输效率),再继续进行优化提升空间并不大。

而随着序列化以及 Hash 的广泛使用,现在 CPU 反而成为了一个瓶颈。

内存方面,使用 Java 原生的堆内存管理方式很容易产生 OOM 问题,并伴随着较大的 GC 负担,进一步降低了 CPU 的利用率。

基于上述观察 Spark 在 1.4 中启动了 Tungsten 项目,并在 1.5 中完成第一阶段的优化

这些优化包括下面三个方面:

  • 内存管理和二进制格式处理
  • 缓存友好的计算
  • 代码生成

内存管理和二进制格式处理
避免以原生格式存储 Java 对象(使用二进制的存储格式),减少 GC 负担
压缩内存数据格式,减少内存占用以及可能的溢写。使用更准确的内存的统计而不是依赖启发规则管理内存。

对于那些已知数据格式运算( DataFrame 和 SQL ),直接使用二进制的运算,避免序列化和反序列化开销。

缓存友好的计算
更加快的排序以及 Hash,优化 Aggregation, Join 以及 Shuffle 操作。

代码生成
更快的表达式计算以及 DataFrame/SQL 运算(这是代码生成的主要应用场景,主要是为了降低进行表达式评估中 JVM 的各种开销,如虚函数调用,分支预测,原始类型的对象装箱开销以及内存消耗)更快的序列化。

相关的每个版本所做的优化如下:

Tungsten 项目并不是完全是一个通用的优化技术,其中很多优化利用了 DataFrame 模型所提供的丰富的语义信息(因此 DataFrame 和 Spark SQL 查询能够享受该项目所来的大量的好处),同样未来也会改进 RDD API 来为底层优化提供更多的信息支持。

Spark 在 Hulu 的实践

Hulu 是一家在线付费视频网站,每天都有大量的用户观看行为数据产生,这些数据会由 Hulu 的大数据平台进行存储以及处理。推荐团队需要从这些数据中挖掘出单个用户感兴趣的内容并推荐给对应的观众,广告团队需要根据用户的观看记录以及行为给其推荐的最合适广告,而数据团队则需要分析所有数据的各个维度并为公司的策略制定提供有效支持。

他们的所有工作都是在 Hulu 的大数据平台上完成的,该平台由 HDFS/Yarn, HBase, Hive, Cassandera 以及的 Presto,Spark 等组成。Spark 是运行在 Yarn上,由 Yarn 来管理资源并进行任务调度。

Spark 则主要有两类应用:Streaming 应用以及短时 Job。

Streaming 应用中各个设备前端将用户的行为日志输入到 Kafka 中,然后由 Spark Streaming 来进行处理,输出结果到 Cassandera, HBase 以及 HDFS 中。短时 Job 并不像 Streaming 应用一样一直运行,而是由用户或者定时脚本触发,一般运行时间从几分钟到十几个小时不等。

此外为了方便 PM 类型的用户更便捷的使用 Spark,我们也搭建了 Apache Zeppelin 这种交互式可视化执行环境。对于非 Python/Scala/Java/R 用户(例如某些用户想在 NodeJS 中提交 Spark 任务),我们也提供 REST 的 Spark-JobServer 来方便用户提交作业。

Hulu 从 0.9 版本就开始将 Spark 应用于线上作业,内部经历了 1.1.1, 1.2.0, 1.4.0 等诸多版本,目前内部使用的最新版本是基于社区 1.5.1 进行改造的。

在之前的版本中我们遇到的很多的问题也添加了不少新功能,大部分修改都已经包含在最新版本里面,我就不再这里赘述了。这节里我主要想讲的是社区里所没有的,但是我们认为还比较重要的一些修改。

较多的迭代触发 StackOverflow 的问题

在某些机器学习算法里面需要进行比较多轮的迭代,当迭代的次数超过一定次数时候应用程序就会发生 StackOverflow 而崩溃。这个次数限制并不会很大,几百次迭代就可能发生栈溢出。大家可以利用一小段代码来进行一个简单的测试:

产生上述错误的原因在于 Driver 将 RDD 任务发送给 Executor 执行的时候需要将 RDD 的信息序列化后广播到对应的 Executor 上。而 RDD 在序列的时候需要递归将其依赖的 RDD 序列化,这样在出现长 lineage 的 RDD 的时候就可能因为线程的栈帧内存不够,抛出 StackOverflow 异常。

解决方法也比较直接,就是将递归改为迭代,把原来需要递归保存在线程栈帧的序列化 RDD 挪到堆区进行保存。具体的做法是将 RDD 的依赖关系分离出来,变成两个映射表: rddId->List of depId 以及depId -> Dependency。Driver 端然后将 RDD 以及这些映射序列化为字节数组广播出去,Executor 端接收到广播消息后重新将映射组装成为原始的依赖。

这个过程中要改动 RDD 核心 Task 接口,需要经过严格的测试。但是在做这种优化之后,迭代个一两千次都没有什么问题。

Streaming 延迟数据接收机制( Receive-Base )

在 Receive-Base 的 Spark Streaming 的架构中, 主要有两个角色 Driver 和 Executor。

在 Executor 中运行着 Receiver, Receiver 的主要作用是从外部接收数据并缓存到本地内存中,同时 Receiver 回向 Driver 汇报自己所接收的数据块,Driver 定期产生新的任务并分发到各个 Executor 去处理这些数据。

在应用启动的时候,Driver 会首先将 Receiver 处理程序调度到各个 Executor 上让其初始化。一旦 Receiver 初始化完毕,它就开始源源不断的接收数据,并且需要 Driver 定期调度任务来消耗这些数据。

但是在某些场景下, Executor 处理端还并没有准备好,无法开始处理数据。

这时候在 Receiver 端就会发生内存积压,随着积压的数据越来越多,大部分数据都会撑过新生代回收年龄进入老年代,进一步给 GC 带来严重的压力,这个时候也就离应用程序崩溃不远了。

在 Hulu 的 Spark Streaming 处理中,需要加载并初始化很多机器学习的模型,这些模型的初始化非常费时间,长的可能需要半个小时才能初始化完毕。在此期间 Receiver 不能接收数据,否者内存将会被消耗殆尽。

Hulu 中的解决方法是在每个 Executor 接收任何任务之前先进行执行一个用户定义的初始化任务,初始化任务中可以执行一些独立的用户代码。我们在新增了一个接口,让用户可以设置自定义的初始化任务。

如下代码所示:

实现上需要更改 Spark 的任务调度器,先将每个 Executor 设置为未初始化状态,除了初始化任务之外调度器不会给未初始化状态的 Executor 分配其他任务。等 Executor 运行完初始化任务,调度器更新 Executor 的状态为已初始化,这样的 Executor 就可以分配给其他正常任务了,包括初始化 Receiver 的任务。

其他注意事项

Spark 允许用户设置 spark.executor.userClassPathFirst,这可以部分缓解用户代码库和 Spark 系统代码库冲突的问题。

但是在实践过程中我们发现,大并发情况加载相同的类有可能发生死锁的情况(我们的一个场景下有 1/10 几率复现该问题)。

其问题在于 Spark 所新增加的 ChildFirstURLClassLoader 的实现引入了并发死锁的问题。

Java 7 中的 ClassLoader 本身提供细粒度的类加载并发锁,可以做到为每个 classname 设置一个锁,但是使用该细粒度的类加载锁有一个条件,用户自己实现的 ClassLoader 必须在自身静态初始化方法中将自己注册到 ClassLoader 中。

然而在 Scala 语言中并没有类的静态初始化方法,只有一个伴生对象的初始化方法。但是伴生对象和类对象的类型并不完全一致。

因此 Scala 在 ChildFirstURLClassLoader 中模仿 Java 的 ClassLoader 实现了自己的细粒度的类加载锁,然而这段代码却无法达到预期目的,最终还是会降级到 ClassLoader 级别的锁,并且在某些场景下还会触发死锁,解决方法是去除对应的细粒度锁代码。

Spark 未来的发展趋势

Spark 1.6 即将发布,其中最重要的特性有两个 [SPARK-10000] 统一内存管理以及 [SPARK-9999] DataSet API。当然还有很多其他的改进,由于篇幅关系,下面主要介绍上两个。

统一内存管理

在 1.5 以及之前存在两个独立的内存管理:执行时内存管理以及存储内存管理,前者是在对 Shuffle, Join, Sort, Aggregation 等计算的时候所用到的内存,后者是缓存以及广播变量时用的内存。

可以通过 spark.storage.memoryFraction 来指定两部分的大小,默认存储占据 60% 的堆内存。这种方式分配的内存都是静态的,需要手动调优以避免 spill,且没有一个合理的默认值可以覆盖到所有的应用场景。

在 1.6 中这两个部分内存管理被统一起来了,当执行时内存超过给自己分配的大小时可以临时向存储时内存借用空间,临时借用的内存可以在任何时候被回收,反之亦然。更进一步可以设置存储内存的最低量,系统保证这部分量不会被剔除。

DataSet API

RDD API 存储的是原始的 JVM 对象,提供丰富的函数式运算符,使用起来很灵活,但是由于缺乏类型信息很难对它的执行过程优化。DataFrame API 存储的是 Row 对象,提供了基于表达式的运算以及逻辑查询计划,很方便做优化,并且执行起来速度很快,但是却不如 RDD API 灵活。

DataSet API 则充分结合了二者的优势,既允许用户很方便的操作领域对象又拥有 SQL 执行引擎的高性能表现。

本质上来说 DataSet API 相当于 RDD + Encoder, Encoder 可以将原始的 JVM对象高效的转化为二进制格式,使得可以后续对其进行更多的处理。目前是实现为 Catalyst 的逻辑计划,这样就能够充分利用现有的 Tungsten 的优化成果。

DataSet API 需要达到如下几点目标:

  • 快速:Encoder 需要至少和现有的 Kryo 或者 Java 序列一样快。
  • 类型安全:在操作这些对象的时候需要尽可能提供编译时的类型安全,如果编译期无法知晓类型,在发生 Schema 不匹配的时候需要快速失败。
  • 对象模型支持:默认需要提供原子类型,case 类,tuple, POJOs, JavaBeans 的 Encoder。
  • Java 兼容性:需要提供一个简单的 API 来兼容 Scala 和 Java,尽可能使用这些 API,如果实在不能使用这些API也需要提供重载的版本。
  • DataFrame 的互操作:用户需要能够无缝的在 DataSet API 和 DataFrame API之间做转换。

目前 DataSet API 和 DataFrame API 还是独立的两个 API,未来 DataFrame 有可能继承自 DataSet[Row]。

最后再来看一下整体的架构:

参考文章

Hadoop年度回顾与2016发展趋势

Apache HBase 2015年发展回顾与未来展望

Q & A

1、在 hulu, streaming 跑在多少个节点上?Zeppelin 和 sparknotebook.io 各有什么优劣、是如何选型的?

hulu 的 Spark Streaming 运行在 YARN 上,规模是几百个节点。我们当前主要用的是 Zeppelin,sparknotebook.ion 目前还没有试用

2、我们用的是 hive on spark 模式,因为 hive 是统一入口,上面已经有 mr 和 tez,请问对比 spark sql 各自优缺点?还有就到对比一下 spark shuffle 和 yarn自带 shuffle(on yarn 模式)的优缺点?

底层的存储引擎不一样,相比于性能方面 spark 和 tez 不相上下,但是稳定性方面 spark 更胜一筹。spark shuffle 提供了三种实现,分别是 hash-based,sort-based 和 tungsten-sort, 而 mapreduce shuffle 知识 sort-based,在灵活度上,spark 更高,且个别之处,spark 有深度优化。

3、能否简单说说 spark 在图片计算方面的应用?

是指图像处理方面吗,这方面 Spark 并没有专门的组件来处理。图片方面的应用比较少,至少在 hulu 没有。

4、Tungsten 项目目前成熟吗?或者说贵司有线上应用没?

Tungsten 项目还处于开发阶段(阶段二),不建议在线上使用。

5、请问使用 Spark streaming 在 YARN 上和其他任务共同运行,稳定性如何?YARN 有没有做 CPU 级别的隔离?我们在 YARN 上运行的任务,运行几天就会挂掉,通常都是 OOM,但是从程序看,并没有使用过多内存。

如果 YARN 上还会混合运行 mapreduce 和 tez 等应用,则会对 Spark streaming 存在资源竞争,造成性能不稳定,可以使用 label-based scheduling 对一些节点打标签,专门运行 Spark streaming。总体上说,spark streaming 在 YARN 上运行比较稳定。YARN 对 CPU 有隔离,使用的 cgroups。 如果是 OOM 挂掉,可能程序存在内存泄露,不知道你们用的什么版本,建议使用 jprofile 定位一下内存效率之处。

6、能否简单对比下 Storm 和 Spark 的优劣?如何技术选型?

Storm 是实时流式数据处理,面向行处理,单条延时比较低。Spark 是近实时流式处理,面向 vp 处理,吞吐量比较高。如果应用对实时性要求比较高建议试用 Storm, 否则大家可以考虑利用 Spark 的丰富的数据操作能力。

转载请注明出处:代码说 » 大数据盘点之Spark篇

Lucene5学习之QueryParser-Query解析器

Lucene已经给我们提供了很多Query查询器,如PhraseQuery,SpanQuery,那为什么还要提供QueryParser呢?或者说设计QueryParser的目的是什么?QueryParser的目的就是让你从众多的Query实现类中脱离出来,因为Query实现类太多了,你有时候会茫然了,我到底该使用哪个Query实现类来完成我的查询需求呢,所以Lucene制定了一套Query语法,根据你传入的Query语法字符串帮你把它转换成Query对象,你不用关心底层是使用什么Query实现类。

上面既然说到PhraseQuery和SpanQuery,那我就随带扯一扯这两个Query的区别吧,我估计这是很多初学Lucene者比较困惑的问题,两个Query都能根据多个Term进行查询,但PhraseQuery只能按照查询短语在文档中出现的顺序进行匹配,而不能颠倒过来匹配,比如你查询quick lazy,而索引中出现的是xxxxxxxxlazy qucikxxxxxxx,那PhraseQuery就没法匹配到了,这时候你就只能使用SpanQuery了,SpanQuery的inorder参数允许你设置是否按照查询短语在文档中出现的顺序进行匹配,以及是否允许有重叠,什么叫是否允许重叠?举个例子说明,假如域的值是这样的:“jumps over extremely very lazy broxn dog”,而你的查询短语是“dog over”,因为索引中dog在over后面,而你提供的查询短语中dog却在over前面,这与它在索引文档中出现的顺序是颠倒的,这时候你就不能使用PhraseQuery,PhraseQuery只能按出现顺序匹配,这种颠倒顺序匹配无法用PhraseQuery实现,把SpanQuery的inOrder设为false,就可以无视顺序了,即只要你能按slop规定的步数内匹配到dog over或者 over dog都算匹配成功。而如果inOrder设为true,意思就是你只能在规定步数内匹配到dog over,而匹配到over dog不算,并且匹配过程中不能有重叠。什么叫重叠?要得到dog over,那只能把over往右移动6步,可是它跨过了dog了,即dog重叠了,意思就是你只能在两者之间移动,不能跨越两者的边界进行匹配。我解释的不知道你们能看的明白不?注意两者的slop都是最多需要移动几步的意思即在规定步数内达到你想要的情况。

跑题扯了半天PhraseQuery和SpanQuery的区别,回归正题,接着说说QueryParser的查询语法,关于QueryParser的语法,我们还是去看Lucene的官方Wiki吧,

https://lucene.apache.org/core/2_9_4/queryparsersyntax.html

概述里提到QueryParser是通过JavaCC把用户输入的String转换成Query对象,那什么是JavaCC?JavaCC就是一个非常流行的用Java写的解析器生成器。

Before choosing to use the provided Query Parser, please consider the following:
意思就是提醒我们在选择使用QueryParser之前请先仔细考虑下面3个问题。

1.QueryParser是为用户输入文本而设计的而不是你应用程序生成的文本而设计的,什么意思?意思就是你要考虑最恶劣的情况,因为用户输入的文本是无法预知的,你不能试图去规范用户输入什么样格式的查询字符串,如果你正在准备这么做,请你还是去使用Query api 构建你的Query实现类吧。

2.没有分词的域请直接使用Query API来构建你的Query实现类,因为QueryParser会使用分词器对用户输入的文本进行分词得到N个Term,然后再根据匹配的,这点你必须清楚。

3.第3点里提示你在设计查询表单时,对应普通的文本框可以直接使用QueryParser,但像日期范围啊搜索关键字啊下拉框里选定某个值或多个值进行限定值时,请使用Query API去做。

Term:
Term直接用一个单词表示,如“hello” ,多个Term用空格分割,如“hello java”,

Field:
可以添加上域,域和Term字符串用冒号隔开,如title:”The Right Way”,查询多个域用or或者and连接,

如title:”The Right Way” AND text:go

Term字符串你还可以使用通配符进行模糊匹配,如title: ja*a title:ja?a title:ja*等等

你还可以使用~字符开启FuzzyQuery,如title:roam~ or title:roam~0.8
相似度阀值取值范围是0-1之间,默认值是0.5,

QueryParser语法表达式还支持开启PhraseQuery短语查询,如title:”jakarta apache”~10

表示查询title域中包含jakarta和apache字符且jakarta在apache前面且jakarta与apache之间间隔距离在10个间距之内(即<=10)。

当然也支持范围查询,title:[java to php],age[18 to 28]

你也可以单独为某个Term设置权重,如title:java^4,默认权重都为1.

Boolean Operators即boolean操作符即or和and,用来链接多个Term的,如果两个Term仅仅用空格隔开,则默认为or链接的,如title:java^5 and content:lucen*

当然还有+ -字符,表示必须符合和必须不符合即排除的意思,如+jakarta lucene,但注意只有一个Term的时候,不能用NOT,比如NOT “jakarta apache”是不合法的。

而这样就可以,”jakarta apache” -“Apache Lucene”表示必须包含jakarta apache,但不能包含Apache Lucene.

当or and条件很复杂时,需要限制优先级时可以用()小括号对Term条件进行分组,如(jakarta OR apache) AND website

当对某个域的限定值有多个可以用or/and进行链接,也可以用()写在一起,如title:(+return +”pink panther”),当然你也可以用and拆成title:return and title:”pink panther”

Lucene中需要进行转义的特殊字符包括

+ – && || ! ( ) { } [ ] ^ ” ~ * ? : \

QueryParser使用示例如下:

QueryParser parser = new QueryParser(fieldName, new IKAnalyzer());
Query query = parser.parse(queryString);

queryString即为上面解释的那些queryParser查询表达式。

但QueryParser并不能完全代替Query API,它并不能实现所有Query实现类的功能,比如它不支持SpanQuery.

上面说的都是在单个域中查询,当然要在多个域中查询你可以使用or/and进行拼接,如果要在多个域中进行查询,你除了用or/and拼接以外,多了另一种选择,它就是MultiFieldQueryParser.我想Google大家都用过,Google的搜索界面就为我们提供了一个搜索输入框,用户只需要输入搜索关键字即可,而不用关心我输入的搜索关键字接下来会在哪些域(Field)里去查找,可能底层我们的索引会建立title,content,category等各种域,会依次从这几个域中去匹配是否有符合用户输入的查询关键字,但这些对用户都是透明的,用户也没必要去了解这些,MultiFieldQueryParser就是用来解决这种多域查询问题的。

public MultiFieldQueryParser(String[] fields, Analyzer analyzer, Map boosts) {
    this(fields, analyzer);
    this.boosts = boosts;
  }

这是MultiFieldQueryParser的构造函数,首先fields毫无疑问就是提供一个域名称数组即你需要在哪些域中进行查询,analyzer即分词器对象,用户输入的搜索关键字我们需要对其分词,为什么要分词?因为用户输入的搜索关键字可能是一句话,比如:我女朋友要跟我分手,我该怎么办,分词后可能得到的只有两个关键字就是女朋友和分手,其他都是停用词被剔除了。最后一个boosts参数是一个map对象,是用来设置每个域的权重加权值的,map的key就是域名称,value就是加权值。boosts参数可以不传入,你传入一个null也行,不传入即表示不进行特殊加权,则默认权重加权值都是1.

转载请注明出处:代码说 » Lucene5学习之QueryParser-Query解析器

Lucene5学习之排序

这回我们来学习Lucene的排序。机智的少年应该已经发现了,IndexSearcher类的search方法有好几个重载:

 /** Finds the top <code>n</code>
 * hits for <code>query</code>.
 *
 * @throws BooleanQuery.TooManyClauses If a query would exceed 
 * {@link BooleanQuery#getMaxClauseCount()} clauses.
 */
 public TopDocs search(Query query, int n)
 throws IOException {
 return search(query, null, n);
 }


 /** Finds the top <code>n</code>
 * hits for <code>query</code>, applying <code>filter</code> if non-null.
 *
 * @throws BooleanQuery.TooManyClauses If a query would exceed 
 * {@link BooleanQuery#getMaxClauseCount()} clauses.
 */
 public TopDocs search(Query query, Filter filter, int n)
 throws IOException {
 return search(createNormalizedWeight(wrapFilter(query, filter)), null, n);
 }

 /** Lower-level search API.
 *
 * <p>{@link LeafCollector#collect(int)} is called for every matching
 * document.
 *
 * @param query to match documents
 * @param filter if non-null, used to permit documents to be collected.
 * @param results to receive hits
 * @throws BooleanQuery.TooManyClauses If a query would exceed 
 * {@link BooleanQuery#getMaxClauseCount()} clauses.
 */
 public void search(Query query, Filter filter, Collector results)
 throws IOException {
 search(leafContexts, createNormalizedWeight(wrapFilter(query, filter)), results);
 }

 /** Lower-level search API.
 *
 * <p>{@link LeafCollector#collect(int)} is called for every matching document.
 *
 * @throws BooleanQuery.TooManyClauses If a query would exceed 
 * {@link BooleanQuery#getMaxClauseCount()} clauses.
 */
 public void search(Query query, Collector results)
 throws IOException {
 search(leafContexts, createNormalizedWeight(query), results);
 }
 
 /** Search implementation with arbitrary sorting. Finds
 * the top <code>n</code> hits for <code>query</code>, applying
 * <code>filter</code> if non-null, and sorting the hits by the criteria in
 * <code>sort</code>.
 * 
 * <p>NOTE: this does not compute scores by default; use
 * {@link IndexSearcher#search(Query,Filter,int,Sort,boolean,boolean)} to
 * control scoring.
 *
 * @throws BooleanQuery.TooManyClauses If a query would exceed 
 * {@link BooleanQuery#getMaxClauseCount()} clauses.
 */
 public TopFieldDocs search(Query query, Filter filter, int n,
 Sort sort) throws IOException {
 return search(createNormalizedWeight(wrapFilter(query, filter)), n, sort, false, false);
 }

 /** Search implementation with arbitrary sorting, plus
 * control over whether hit scores and max score
 * should be computed. Finds
 * the top <code>n</code> hits for <code>query</code>, applying
 * <code>filter</code> if non-null, and sorting the hits by the criteria in
 * <code>sort</code>. If <code>doDocScores</code> is <code>true</code>
 * then the score of each hit will be computed and
 * returned. If <code>doMaxScore</code> is
 * <code>true</code> then the maximum score over all
 * collected hits will be computed.
 * 
 * @throws BooleanQuery.TooManyClauses If a query would exceed 
 * {@link BooleanQuery#getMaxClauseCount()} clauses.
 */
 public TopFieldDocs search(Query query, Filter filter, int n,
 Sort sort, boolean doDocScores, boolean doMaxScore) throws IOException {
 return search(createNormalizedWeight(wrapFilter(query, filter)), n, sort, doDocScores, doMaxScore);
 }

query参数就不用解释了,filter用来再次过滤的,int n表示只返回Top N,Sort表示排序对象, doDocScores这个参数是重点,表示是否对文档进行相关性打分,如果你设为false,那你索引文档的score值就是NAN, doMaxScore表示啥意思呢,举个例子说明吧,假如你有两个Query(QueryA和QueryB),两个条件是通过BooleanQuery连接起来的,假如QueryA条件匹配到某个索引文档,而QueryB条件也同样匹配到该文档,如果doMaxScore设为true,表示该文档的评分计算规则为取两个Query(当然你可能会有N个Query链接,那就是N个Query中取最大值)之中的最大值,否则就是取两个Query查询评分的相加求和。默认为false. 注意:在Lucene4.x时代,doDocScores和doMaxScore这两个参数可以通过indexSearcher类来设置,比如这样:

 searcher.setDefaultFieldSortScoring(true, false);

而在Lucene5.x时代,你只能在调用search方法时传入这两个参数,比如这样:

 searcher.search(query, filter, n, sort, doDocScores, doMaxScore);

看方法声明我们知道,我们如果需要改变默认的按评分降序排序行为,则必须传入一个Sort对象,那我们来观摩下Sort类源码:

 package org.apache.lucene.search;

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.io.IOException;
import java.util.Arrays;


/**
 * Encapsulates sort criteria for returned hits.
 *
 * <p>The fields used to determine sort order must be carefully chosen.
 * Documents must contain a single term in such a field,
 * and the value of the term should indicate the document's relative position in
 * a given sort order. The field must be indexed, but should not be tokenized,
 * and does not need to be stored (unless you happen to want it back with the
 * rest of your document data). In other words:
 *
 * <p><code>document.add (new Field ("byNumber", Integer.toString(x), Field.Store.NO, Field.Index.NOT_ANALYZED));</code></p>
 * 
 *
 * <p><h3>Valid Types of Values</h3>
 *
 * <p>There are four possible kinds of term values which may be put into
 * sorting fields: Integers, Longs, Floats, or Strings. Unless
 * {@link SortField SortField} objects are specified, the type of value
 * in the field is determined by parsing the first term in the field.
 *
 * <p>Integer term values should contain only digits and an optional
 * preceding negative sign. Values must be base 10 and in the range
 * <code>Integer.MIN_VALUE</code> and <code>Integer.MAX_VALUE</code> inclusive.
 * Documents which should appear first in the sort
 * should have low value integers, later documents high values
 * (i.e. the documents should be numbered <code>1..n</code> where
 * <code>1</code> is the first and <code>n</code> the last).
 *
 * <p>Long term values should contain only digits and an optional
 * preceding negative sign. Values must be base 10 and in the range
 * <code>Long.MIN_VALUE</code> and <code>Long.MAX_VALUE</code> inclusive.
 * Documents which should appear first in the sort
 * should have low value integers, later documents high values.
 * 
 * <p>Float term values should conform to values accepted by
 * {@link Float Float.valueOf(String)} (except that <code>NaN</code>
 * and <code>Infinity</code> are not supported).
 * Documents which should appear first in the sort
 * should have low values, later documents high values.
 *
 * <p>String term values can contain any valid String, but should
 * not be tokenized. The values are sorted according to their
 * {@link Comparable natural order}. Note that using this type
 * of term value has higher memory requirements than the other
 * two types.
 *
 * <p><h3>Object Reuse</h3>
 *
 * <p>One of these objects can be
 * used multiple times and the sort order changed between usages.
 *
 * <p>This class is thread safe.
 *
 * <p><h3>Memory Usage</h3>
 *
 * <p>Sorting uses of caches of term values maintained by the
 * internal HitQueue(s). The cache is static and contains an integer
 * or float array of length <code>IndexReader.maxDoc()</code> for each field
 * name for which a sort is performed. In other words, the size of the
 * cache in bytes is:
 *
 * <p><code>4 * IndexReader.maxDoc() * (# of different fields actually used to sort)</code>
 *
 * <p>For String fields, the cache is larger: in addition to the
 * above array, the value of every term in the field is kept in memory.
 * If there are many unique terms in the field, this could
 * be quite large.
 *
 * <p>Note that the size of the cache is not affected by how many
 * fields are in the index and <i>might</i> be used to sort - only by
 * the ones actually used to sort a result set.
 *
 * <p>Created: Feb 12, 2004 10:53:57 AM
 *
 * @since lucene 1.4
 */
public class Sort {

 /**
 * Represents sorting by computed relevance. Using this sort criteria returns
 * the same results as calling
 * {@link IndexSearcher#search(Query,int) IndexSearcher#search()}without a sort criteria,
 * only with slightly more overhead.
 */
 public static final Sort RELEVANCE = new Sort();

 /** Represents sorting by index order. */
 public static final Sort INDEXORDER = new Sort(SortField.FIELD_DOC);

 // internal representation of the sort criteria
 SortField[] fields;

 /**
 * Sorts by computed relevance. This is the same sort criteria as calling
 * {@link IndexSearcher#search(Query,int) IndexSearcher#search()}without a sort criteria,
 * only with slightly more overhead.
 */
 public Sort() {
 this(SortField.FIELD_SCORE);
 }

 /** Sorts by the criteria in the given SortField. */
 public Sort(SortField field) {
 setSort(field);
 }

 /** Sets the sort to the given criteria in succession: the
 * first SortField is checked first, but if it produces a
 * tie, then the second SortField is used to break the tie,
 * etc. Finally, if there is still a tie after all SortFields
 * are checked, the internal Lucene docid is used to break it. */
 public Sort(SortField... fields) {
 setSort(fields);
 }

 /** Sets the sort to the given criteria. */
 public void setSort(SortField field) {
 this.fields = new SortField[] { field };
 }

 /** Sets the sort to the given criteria in succession: the
 * first SortField is checked first, but if it produces a
 * tie, then the second SortField is used to break the tie,
 * etc. Finally, if there is still a tie after all SortFields
 * are checked, the internal Lucene docid is used to break it. */
 public void setSort(SortField... fields) {
 this.fields = fields;
 }
 
 /**
 * Representation of the sort criteria.
 * @return Array of SortField objects used in this sort criteria
 */
 public SortField[] getSort() {
 return fields;
 }

 /**
 * Rewrites the SortFields in this Sort, returning a new Sort if any of the fields
 * changes during their rewriting.
 *
 * @param searcher IndexSearcher to use in the rewriting
 * @return {@code this} if the Sort/Fields have not changed, or a new Sort if there
 * is a change
 * @throws IOException Can be thrown by the rewriting
 * @lucene.experimental
 */
 public Sort rewrite(IndexSearcher searcher) throws IOException {
 boolean changed = false;
 
 SortField[] rewrittenSortFields = new SortField[fields.length];
 for (int i = 0; i < fields.length; i++) {
 rewrittenSortFields[i] = fields[i].rewrite(searcher);
 if (fields[i] != rewrittenSortFields[i]) {
 changed = true;
 }
 }

 return (changed) ? new Sort(rewrittenSortFields) : this;
 }

 @Override
 public String toString() {
 StringBuilder buffer = new StringBuilder();

 for (int i = 0; i < fields.length; i++) {
 buffer.append(fields[i].toString());
 if ((i+1) < fields.length)
 buffer.append(',');
 }

 return buffer.toString();
 }

 /** Returns true if <code>o</code> is equal to this. */
 @Override
 public boolean equals(Object o) {
 if (this == o) return true;
 if (!(o instanceof Sort)) return false;
 final Sort other = (Sort)o;
 return Arrays.equals(this.fields, other.fields);
 }

 /** Returns a hash code value for this object. */
 @Override
 public int hashCode() {
 return 0x45aaf665 + Arrays.hashCode(fields);
 }

 /** Returns true if the relevance score is needed to sort documents. */
 public boolean needsScores() {
 for (SortField sortField : fields) {
 if (sortField.needsScores()) {
 return true;
 }
 }
 return false;
 }

}

首先定义了两个静态常量:

public static final Sort RELEVANCE = new Sort();

public static final Sort INDEXORDER = new Sort(SortField.FIELD_DOC);

RELEVANCE 表示按评分排序,

INDEXORDER 表示按文档索引排序,什么叫按文档索引排序?意思是按照索引文档的docId排序,我们在创建索引文档的时候,Lucene默认会帮我们自动加一个Field(docId),如果你没有修改默认的排序行为,默认是先按照索引文档相关性评分降序排序(如果你开启了对索引文档打分功能的话),然后如果两个文档评分相同,再按照索引文档id升序排列。

然后就是Sort的构造函数,你需要提供一个SortField对象,其中有一个构造函数要引起你们的注意:

 public Sort(SortField... fields) {
 setSort(fields);
 }

SortField… fields写法是JDK7引入的新语法,类似于以前的SortField[] fields写法,但它又于以前的这种写法有点不同,它支持field1,field2,field3,field4,field5,………fieldN这种方式传参,当然你也可以传入一个数组也是可以的。其实我是想说Sort支持传入多个SortField即表示Sort是支持按多个域进行排序,就像SQL里的order by age,id,哦-哦,TM又扯远了,那接着去观摩下SoreField的源码,看看究竟:

 public class SortField {

 /**
 * Specifies the type of the terms to be sorted, or special types such as CUSTOM
 */
 public static enum Type {

 /** Sort by document score (relevance). Sort values are Float and higher
 * values are at the front. */
 SCORE,

 /** Sort by document number (index order). Sort values are Integer and lower
 * values are at the front. */
 DOC,

 /** Sort using term values as Strings. Sort values are String and lower
 * values are at the front. */
 STRING,

 /** Sort using term values as encoded Integers. Sort values are Integer and
 * lower values are at the front. */
 INT,

 /** Sort using term values as encoded Floats. Sort values are Float and
 * lower values are at the front. */
 FLOAT,

 /** Sort using term values as encoded Longs. Sort values are Long and
 * lower values are at the front. */
 LONG,

 /** Sort using term values as encoded Doubles. Sort values are Double and
 * lower values are at the front. */
 DOUBLE,

 /** Sort using a custom Comparator. Sort values are any Comparable and
 * sorting is done according to natural order. */
 CUSTOM,

 /** Sort using term values as Strings, but comparing by
 * value (using String.compareTo) for all comparisons.
 * This is typically slower than {@link #STRING}, which
 * uses ordinals to do the sorting. */
 STRING_VAL,

 /** Sort use byte[] index values. */
 BYTES,

 /** Force rewriting of SortField using {@link SortField#rewrite(IndexSearcher)}
 * before it can be used for sorting */
 REWRITEABLE
 }

 /** Represents sorting by document score (relevance). */
 public static final SortField FIELD_SCORE = new SortField(null, Type.SCORE);

 /** Represents sorting by document number (index order). */
 public static final SortField FIELD_DOC = new SortField(null, Type.DOC);

 private String field;
 private Type type; // defaults to determining type dynamically
 boolean reverse = false; // defaults to natural order

 // Used for CUSTOM sort
 private FieldComparatorSource comparatorSource;

首先你看到的里面定义了一个排序规则类型的枚举Type,

SCORE:表示按评分排序,默认是降序

DOC:按文档ID排序,除了评分默认是降序以外,其他默认都是升序

STRING:表示把域的值转成字符串进行排序,

STRING_VAL也是把域的值转成字符串进行排序,不过比较的时候是调用String.compareTo来比较的

STRING_VAL性能比STRING要差,STRING是怎么比较的,源码里没有说明。

相应的还有INT,FLOAT,DOUBLE,LONG就不多说了,

CUSTOM:表示自定义排序,这个是要结合下面的成员变量

private FieldComparatorSource comparatorSource;一起使用,即指定一个自己的自定义的比较器,通过自己的比较器来决定排序顺序。

SortField还有3个比较重要的成员变量,除了刚才的说自定义比较器外:

  private String field;
 private Type type; // defaults to determining type dynamically
 boolean reverse = false; // defaults to natural order

毫无疑问,Field表示你要对哪个域进行排序,即排序域名称

Type即上面解释过的排序规则即按什么来排序,评分 or docID 等等

reverse表示是否反转默认的排序行为,即升序变降序,降序就变升序,比如默认评分是降序的,reverse设为true,则默认评分就按升序排序了,而其他域就按升序排序了。默认reverse为false.

OK,了解以上内容,我想大家已经对如何实现自己对索引文档的自定义排序已经了然于胸了。下面我把我写的测试demo代码贴出来供大家参考:

首先创建用于测试的索引文档:

package com.yida.framework.lucene5.sort;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Paths;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Properties;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.DateTools;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.IntField;
import org.apache.lucene.document.NumericDocValuesField;
import org.apache.lucene.document.SortedDocValuesField;
import org.apache.lucene.document.SortedNumericDocValuesField;
import org.apache.lucene.document.StringField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.IndexWriterConfig.OpenMode;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.util.BytesRef;
/**
 * 创建测试索引
 * @author Lanxiaowei
 *
 */
public class CreateTestIndex {
 public static void main(String[] args) throws IOException {
 String dataDir = "C:/data";
 String indexDir = "C:/lucenedir";

 Directory dir = FSDirectory.open(Paths.get(indexDir));
 Analyzer analyzer = new StandardAnalyzer();
 IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);
 indexWriterConfig.setOpenMode(OpenMode.CREATE_OR_APPEND);
 IndexWriter writer = new IndexWriter(dir, indexWriterConfig);

 List<File> results = new ArrayList<File>();
 findFiles(results, new File(dataDir));
 System.out.println(results.size() + " books to index");

 for (File file : results) {
 Document doc = getDocument(dataDir, file);
 writer.addDocument(doc);
 }
 writer.close();
 dir.close();

 }

 /**
 * 查找指定目录下的所有properties文件
 * 
 * @param result
 * @param dir
 */
 private static void findFiles(List<File> result, File dir) {
 for (File file : dir.listFiles()) {
 if (file.getName().endsWith(".properties")) {
 result.add(file);
 } else if (file.isDirectory()) {
 findFiles(result, file);
 }
 }
 }

 /**
 * 读取properties文件生成Document
 * 
 * @param rootDir
 * @param file
 * @return
 * @throws IOException
 */
 public static Document getDocument(String rootDir, File file)
 throws IOException {
 Properties props = new Properties();
 props.load(new FileInputStream(file));

 Document doc = new Document();

 String category = file.getParent().substring(rootDir.length());
 category = category.replace(File.separatorChar, '/');

 String isbn = props.getProperty("isbn");
 String title = props.getProperty("title");
 String author = props.getProperty("author");
 String url = props.getProperty("url");
 String subject = props.getProperty("subject");

 String pubmonth = props.getProperty("pubmonth");

 System.out.println("title:" + title + "\n" + "author:" + author + "\n" + "subject:" + subject + "\n"
 + "pubmonth:" + pubmonth + "\n" + "category:" + category + "\n---------");

 doc.add(new StringField("isbn", isbn, Field.Store.YES));
 doc.add(new StringField("category", category, Field.Store.YES));
 doc.add(new SortedDocValuesField("category", new BytesRef(category)));
 doc.add(new TextField("title", title, Field.Store.YES));
 doc.add(new Field("title2", title.toLowerCase(), Field.Store.YES,
 Field.Index.NOT_ANALYZED_NO_NORMS,
 Field.TermVector.WITH_POSITIONS_OFFSETS));

 String[] authors = author.split(",");
 for (String a : authors) {
 doc.add(new Field("author", a, Field.Store.YES,
 Field.Index.NOT_ANALYZED,
 Field.TermVector.WITH_POSITIONS_OFFSETS));
 }

 doc.add(new Field("url", url, Field.Store.YES,
 Field.Index.NOT_ANALYZED_NO_NORMS));
 doc.add(new Field("subject", subject, Field.Store.YES,
 Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS));

 doc.add(new IntField("pubmonth", Integer.parseInt(pubmonth),
 Field.Store.YES));
 doc.add(new NumericDocValuesField("pubmonth", Integer.parseInt(pubmonth)));
 Date d = null;
 try {
 d = DateTools.stringToDate(pubmonth);
 } catch (ParseException pe) {
 throw new RuntimeException(pe);
 }
 doc.add(new IntField("pubmonthAsDay",
 (int) (d.getTime() / (1000 * 3600 * 24)), Field.Store.YES));
 
 for (String text : new String[] { title, subject, author, category }) {
 doc.add(new Field("contents", text, Field.Store.NO,
 Field.Index.ANALYZED,
 Field.TermVector.WITH_POSITIONS_OFFSETS));
 }
 return doc;
 }

}

不要问我为什么上面创建索引还要用已经提示快要被废弃了的Field类呢,我会告诉你:我任性!!!不要在意那些细节,我只是想变着花样玩玩。其实就是读取data文件夹下的所有properties文件然后读取文件里的数据写入索引。我待会儿会在底下附件里上传测试用的properties数据文件。

然后就是编写测试类进行测试:

 package com.yida.framework.lucene5.sort;

import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.Paths;
import java.text.DecimalFormat;

import org.apache.commons.lang.StringUtils;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.apache.lucene.search.SortField.Type;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;

public class SortingExample {
 private Directory directory;

 public SortingExample(Directory directory) {
 this.directory = directory;
 }
 
 public void displayResults(Query query, Sort sort)
 throws IOException {
 IndexReader reader = DirectoryReader.open(directory);
 IndexSearcher searcher = new IndexSearcher(reader);

 //searcher.setDefaultFieldSortScoring(true, false);
 
 //Lucene5.x把是否评分的两个参数放到方法入参里来进行设置
 //searcher.search(query, filter, n, sort, doDocScores, doMaxScore);
 TopDocs results = searcher.search(query, null, 
 20, sort,true,false); 

 System.out.println("\nResults for: " + 
 query.toString() + " sorted by " + sort);

 System.out
 .println(StringUtils.rightPad("Title", 30)
 + StringUtils.rightPad("pubmonth", 10)
 + StringUtils.center("id", 4)
 + StringUtils.center("score", 15));
 PrintStream out = new PrintStream(System.out, true, "UTF-8");

 DecimalFormat scoreFormatter = new DecimalFormat("0.######");
 for (ScoreDoc sd : results.scoreDocs) {
 int docID = sd.doc;
 float score = sd.score;
 Document doc = searcher.doc(docID);
 out.println(StringUtils.rightPad( 
 StringUtils.abbreviate(doc.get("title"), 29), 30) + 
 StringUtils.rightPad(doc.get("pubmonth"), 10) + 
 StringUtils.center("" + docID, 4) + 
 StringUtils.leftPad( 
 scoreFormatter.format(score), 12)); 
 out.println(" " + doc.get("category"));
 // out.println(searcher.explain(query, docID)); 
 }
 System.out.println("\n**************************************\n");
 reader.close();
 }

 public static void main(String[] args) throws Exception {
 String indexdir = "C:/lucenedir";
 Query allBooks = new MatchAllDocsQuery();

 QueryParser parser = new QueryParser("contents",new StandardAnalyzer()); 
 BooleanQuery query = new BooleanQuery(); 
 query.add(allBooks, BooleanClause.Occur.SHOULD); 
 query.add(parser.parse("java OR action"), BooleanClause.Occur.SHOULD); 

 Directory directory = FSDirectory.open(Paths.get(indexdir));
 SortingExample example = new SortingExample(directory); 

 example.displayResults(query, Sort.RELEVANCE);

 example.displayResults(query, Sort.INDEXORDER);

 example.displayResults(query, new Sort(new SortField("category",
 Type.STRING)));

 example.displayResults(query, new Sort(new SortField("pubmonth",
 Type.INT, true)));

 example.displayResults(query, new Sort(new SortField("category",
 Type.STRING), SortField.FIELD_SCORE, new SortField(
 "pubmonth", Type.INT, true)));

 example.displayResults(query, new Sort(new SortField[] {
 SortField.FIELD_SCORE,
 new SortField("category", Type.STRING) }));
 directory.close();
 }
}

理解清楚了我上面说的那些知识点,我想这些测试代码你们应该看得懂,不过我还是要提醒一点,在new Sort对象时,可以传入多个SortField来支持多域排序,比如:

 new Sort(new SortField("category",
 Type.STRING), SortField.FIELD_SCORE, new SortField(
 "pubmonth", Type.INT, true))

表示先按category域按字符串升序排,再按评分降序排,接着按pubmonth域进行数字比较后降序排,一句话,域的排序顺序跟你StoreField定义的先后顺序保持一致。注意Sort的默认排序行为。

下面是运行后的打印结果,你们请对照这打印结构和代码多理解酝酿下吧:

 Results for: *:* (contents:java contents:action) sorted by <score>
Title pubmonth id score 
Ant in Action 200707 6 1.052735
 /technology/computers/programming
Lucene in Action, Second E... 201005 9 1.052735
 /technology/computers/programming
Tapestry in Action 200403 11 0.447534
 /technology/computers/programming
JUnit in Action, Second Ed... 201005 8 0.429442
 /technology/computers/programming
A Modern Art of Education 200403 0 0.151398
 /education/pedagogy
Imperial Secrets of Health... 199903 1 0.151398
 /health/alternative/chinese
Lipitor, Thief of Memory 200611 2 0.151398
 /health
Nudge: Improving Decisions... 200804 3 0.151398
 /health
Tao Te Ching 道德經 200609 4 0.151398
 /philosophy/eastern
Gödel, Escher, Bach: an Et... 199905 5 0.151398
 /technology/computers/ai
Mindstorms: Children, Comp... 199307 7 0.151398
 /technology/computers/programming/education
Extreme Programming Explained 200411 10 0.151398
 /technology/computers/programming/methodology
The Pragmatic Programmer 199910 12 0.151398
 /technology/computers/programming

**************************************


Results for: *:* (contents:java contents:action) sorted by <doc>
Title pubmonth id score 
A Modern Art of Education 200403 0 0.151398
 /education/pedagogy
Imperial Secrets of Health... 199903 1 0.151398
 /health/alternative/chinese
Lipitor, Thief of Memory 200611 2 0.151398
 /health
Nudge: Improving Decisions... 200804 3 0.151398
 /health
Tao Te Ching 道德經 200609 4 0.151398
 /philosophy/eastern
Gödel, Escher, Bach: an Et... 199905 5 0.151398
 /technology/computers/ai
Ant in Action 200707 6 1.052735
 /technology/computers/programming
Mindstorms: Children, Comp... 199307 7 0.151398
 /technology/computers/programming/education
JUnit in Action, Second Ed... 201005 8 0.429442
 /technology/computers/programming
Lucene in Action, Second E... 201005 9 1.052735
 /technology/computers/programming
Extreme Programming Explained 200411 10 0.151398
 /technology/computers/programming/methodology
Tapestry in Action 200403 11 0.447534
 /technology/computers/programming
The Pragmatic Programmer 199910 12 0.151398
 /technology/computers/programming

**************************************


Results for: *:* (contents:java contents:action) sorted by <string: "category">
Title pubmonth id score 
A Modern Art of Education 200403 0 0.151398
 /education/pedagogy
Lipitor, Thief of Memory 200611 2 0.151398
 /health
Nudge: Improving Decisions... 200804 3 0.151398
 /health
Imperial Secrets of Health... 199903 1 0.151398
 /health/alternative/chinese
Tao Te Ching 道德經 200609 4 0.151398
 /philosophy/eastern
Gödel, Escher, Bach: an Et... 199905 5 0.151398
 /technology/computers/ai
Ant in Action 200707 6 1.052735
 /technology/computers/programming
JUnit in Action, Second Ed... 201005 8 0.429442
 /technology/computers/programming
Lucene in Action, Second E... 201005 9 1.052735
 /technology/computers/programming
Tapestry in Action 200403 11 0.447534
 /technology/computers/programming
The Pragmatic Programmer 199910 12 0.151398
 /technology/computers/programming
Mindstorms: Children, Comp... 199307 7 0.151398
 /technology/computers/programming/education
Extreme Programming Explained 200411 10 0.151398
 /technology/computers/programming/methodology

**************************************


Results for: *:* (contents:java contents:action) sorted by <int: "pubmonth">!
Title pubmonth id score 
JUnit in Action, Second Ed... 201005 8 0.429442
 /technology/computers/programming
Lucene in Action, Second E... 201005 9 1.052735
 /technology/computers/programming
Nudge: Improving Decisions... 200804 3 0.151398
 /health
Ant in Action 200707 6 1.052735
 /technology/computers/programming
Lipitor, Thief of Memory 200611 2 0.151398
 /health
Tao Te Ching 道德經 200609 4 0.151398
 /philosophy/eastern
Extreme Programming Explained 200411 10 0.151398
 /technology/computers/programming/methodology
A Modern Art of Education 200403 0 0.151398
 /education/pedagogy
Tapestry in Action 200403 11 0.447534
 /technology/computers/programming
The Pragmatic Programmer 199910 12 0.151398
 /technology/computers/programming
Gödel, Escher, Bach: an Et... 199905 5 0.151398
 /technology/computers/ai
Imperial Secrets of Health... 199903 1 0.151398
 /health/alternative/chinese
Mindstorms: Children, Comp... 199307 7 0.151398
 /technology/computers/programming/education

**************************************


Results for: *:* (contents:java contents:action) sorted by <string: "category">,<score>,<int: "pubmonth">!
Title pubmonth id score 
A Modern Art of Education 200403 0 0.151398
 /education/pedagogy
Nudge: Improving Decisions... 200804 3 0.151398
 /health
Lipitor, Thief of Memory 200611 2 0.151398
 /health
Imperial Secrets of Health... 199903 1 0.151398
 /health/alternative/chinese
Tao Te Ching 道德經 200609 4 0.151398
 /philosophy/eastern
Gödel, Escher, Bach: an Et... 199905 5 0.151398
 /technology/computers/ai
Lucene in Action, Second E... 201005 9 1.052735
 /technology/computers/programming
Ant in Action 200707 6 1.052735
 /technology/computers/programming
Tapestry in Action 200403 11 0.447534
 /technology/computers/programming
JUnit in Action, Second Ed... 201005 8 0.429442
 /technology/computers/programming
The Pragmatic Programmer 199910 12 0.151398
 /technology/computers/programming
Mindstorms: Children, Comp... 199307 7 0.151398
 /technology/computers/programming/education
Extreme Programming Explained 200411 10 0.151398
 /technology/computers/programming/methodology

**************************************


Results for: *:* (contents:java contents:action) sorted by <score>,<string: "category">
Title pubmonth id score 
Ant in Action 200707 6 1.052735
 /technology/computers/programming
Lucene in Action, Second E... 201005 9 1.052735
 /technology/computers/programming
Tapestry in Action 200403 11 0.447534
 /technology/computers/programming
JUnit in Action, Second Ed... 201005 8 0.429442
 /technology/computers/programming
A Modern Art of Education 200403 0 0.151398
 /education/pedagogy
Lipitor, Thief of Memory 200611 2 0.151398
 /health
Nudge: Improving Decisions... 200804 3 0.151398
 /health
Imperial Secrets of Health... 199903 1 0.151398
 /health/alternative/chinese
Tao Te Ching 道德經 200609 4 0.151398
 /philosophy/eastern
Gödel, Escher, Bach: an Et... 199905 5 0.151398
 /technology/computers/ai
The Pragmatic Programmer 199910 12 0.151398
 /technology/computers/programming
Mindstorms: Children, Comp... 199307 7 0.151398
 /technology/computers/programming/education
Extreme Programming Explained 200411 10 0.151398
 /technology/computers/programming/methodology

**************************************

写的比较匆忙,如果有哪里没有说清楚或说的不对的,请尽情的喷我,谢谢!

demo源码我也会上传到底下的附件里,你们运行测试类的时候,记得把测试用的数据文件copy到C盘下,如图:

lucene5-sort-demo.rar

data.rar

转载请注明出处:代码说 » Lucene5学习之排序

Lucene5学习之自定义同义词分词器简单示例

同义词功能在全文搜索时的意义,大家应该都懂的。今天中文我就试着写了一个同义词分词的示例demo,其实主要代码还是参考Lucene in Action 这本英文版书籍的随书代码,只不过Lucenen in Action书里的示例代码目前最新版只支持到Lucene4.x,对于Lucene5.x,代码需要稍作修改,下面是基于Lucene5.x的自定义同义词分词器demo:

12717355_10156599378770341_5246585988498113837_n

package com.yida.framework.lucene5.analyzer.synonym;

import java.io.IOException;
/**
 * 同义词提取引擎
 * @author Lanxiaowei
 *
 */
public interface SynonymEngine {
    String[] getSynonyms(String s) throws IOException;
}
package com.yida.framework.lucene5.analyzer.synonym;

import java.io.IOException;
import java.util.HashMap;

public class BaseSynonymEngine implements SynonymEngine {
    private static HashMap<String, String[]> map = new HashMap<String, String[]>();
    
    {
        map.put("quick", new String[] {"fast","speedy"});
        map.put("jumps", new String[] {"leaps","hops"});
        map.put("over", new String[] {"above"});
        map.put("lazy", new String[] {"apathetic","slugish"});
        map.put("dog", new String[] {"canine","pooch"});
    }

    public String[] getSynonyms(String s) throws IOException {
        return map.get(s);
    }
}
package com.yida.framework.lucene5.analyzer.synonym;

import java.io.IOException;
import java.util.Stack;

import org.apache.lucene.analysis.TokenFilter;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute;
import org.apache.lucene.util.AttributeSource;

/**
 * 自定义同义词过滤器
 * 
 * @author Lanxiaowei
 * 
 */
public class SynonymFilter extends TokenFilter {
    public static final String TOKEN_TYPE_SYNONYM = "SYNONYM";

    private Stack synonymStack;
    private SynonymEngine engine;
    private AttributeSource.State current;

    private final CharTermAttribute termAtt;
    private final PositionIncrementAttribute posIncrAtt;

    public SynonymFilter(TokenStream in, SynonymEngine engine) {
        super(in);
        synonymStack = new Stack(); // #1
        this.engine = engine;

        this.termAtt = addAttribute(CharTermAttribute.class);
        this.posIncrAtt = addAttribute(PositionIncrementAttribute.class);
    }

    public boolean incrementToken() throws IOException {
        if (synonymStack.size() > 0) { // #2
            String syn = synonymStack.pop(); // #2
            restoreState(current); // #2
            // 这里Lucene4.x的写法
            // termAtt.setTermBuffer(syn);

            // 这是Lucene5.x的写法
            termAtt.copyBuffer(syn.toCharArray(), 0, syn.length());
            posIncrAtt.setPositionIncrement(0); // #3
            return true;
        }

        if (!input.incrementToken()) // #4
            return false;

        if (addAliasesToStack()) { // #5
            current = captureState(); // #6
        }

        return true; // #7
    }

    private boolean addAliasesToStack() throws IOException {
        // 这里Lucene4.x的写法
        // String[] synonyms = engine.getSynonyms(termAtt.term()); //#8

        // 这里Lucene5.x的写法
        String[] synonyms = engine.getSynonyms(termAtt.toString()); // #8

        if (synonyms == null) {
            return false;
        }
        for (String synonym : synonyms) { // #9
            synonymStack.push(synonym);
        }
        return true;
    }
}

/*
#1 Define synonym buffer
#2 Pop buffered synonyms
#3 Set position increment to 0
#4 Read next token
#5 Push synonyms onto stack
#6 Save current token
#7 Return current token
#8 Retrieve synonyms
#9 Push synonyms onto stack
*/

package com.yida.framework.lucene5.analyzer.synonym;

import java.io.BufferedReader;
import java.io.Reader;
import java.io.StringReader;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.Tokenizer;
import org.apache.lucene.analysis.Analyzer.TokenStreamComponents;
import org.apache.lucene.analysis.core.LetterTokenizer;
import org.apache.lucene.analysis.core.LowerCaseFilter;
import org.apache.lucene.analysis.core.StopAnalyzer;
import org.apache.lucene.analysis.core.StopFilter;
import org.apache.lucene.analysis.standard.StandardFilter;
import org.apache.lucene.analysis.standard.StandardTokenizer;

import com.yida.framework.lucene5.util.analyzer.codec.MetaphoneReplacementFilter;

/**
 * 自定义同义词分词器
 * 
 * @author Lanxiaowei
 * @createTime 2015-03-31 10:15:23
 */
public class SynonymAnalyzer extends Analyzer {

    private SynonymEngine engine;

    public SynonymAnalyzer(SynonymEngine engine) {
        this.engine = engine;
    }

    @Override
    protected TokenStreamComponents createComponents(String text) {
        Tokenizer tokenizer = new StandardTokenizer();
        TokenStream tokenStream = new SynonymFilter(tokenizer, engine);
        tokenStream = new LowerCaseFilter(tokenStream);
        tokenStream = new StopFilter(tokenStream,StopAnalyzer.ENGLISH_STOP_WORDS_SET);
        return new TokenStreamComponents(tokenizer, tokenStream);
    }
}

package com.yida.framework.lucene5.analyzer.synonym;

import java.io.IOException;

import org.apache.lucene.analysis.Analyzer;

import com.yida.framework.lucene5.util.AnalyzerUtils;

public class SynonymAnalyzerTest {
    public static void main(String[] args) throws IOException {
        String text = "The quick brown fox jumps over the lazy dog";
        Analyzer analyzer = new SynonymAnalyzer(new BaseSynonymEngine());
        AnalyzerUtils.displayTokens(analyzer, text);
    }
}
package com.yida.framework.lucene5.util;

import java.io.IOException;

import junit.framework.Assert;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.apache.lucene.analysis.tokenattributes.OffsetAttribute;
import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute;
import org.apache.lucene.analysis.tokenattributes.TypeAttribute;

/**
 * 用于分词器测试的一个简单工具类(用于打印分词情况,包括Term的起始位置和结束位置(即所谓的偏 * 移量),位置增量,Term字符串,Term字符串类型(字符串/阿拉伯数字之类的))
 * @author Lanxiaowei
 *
 */
public class AnalyzerUtils {
    public static void displayTokens(Analyzer analyzer,String text) throws IOException {
        TokenStream tokenStream = analyzer.tokenStream("text", text);
        displayTokens(tokenStream);
    }
    
    public static void displayTokens(TokenStream tokenStream) throws IOException {
        OffsetAttribute offsetAttribute = tokenStream.addAttribute(OffsetAttribute.class);
        PositionIncrementAttribute positionIncrementAttribute = tokenStream.addAttribute(PositionIncrementAttribute.class);
        CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);
        TypeAttribute typeAttribute = tokenStream.addAttribute(TypeAttribute.class);
        
        tokenStream.reset();
        int position = 0;
        while (tokenStream.incrementToken()) {
            int increment = positionIncrementAttribute.getPositionIncrement();
            if(increment > 0) {
                position = position + increment;
                System.out.print(position + ":");
            }
            int startOffset = offsetAttribute.startOffset();
            int endOffset = offsetAttribute.endOffset();
            String term = charTermAttribute.toString();
            System.out.println("[" + term + "]" + ":(" + startOffset + "-->" + endOffset + "):" + typeAttribute.type());
        }
    }
    
    /**
     * 断言分词结果
     * @param analyzer
     * @param text        源字符串
     * @param expecteds   期望分词后结果
     * @throws IOException 
     */
    public static void assertAnalyzerTo(Analyzer analyzer,String text,String[] expecteds) throws IOException {
        TokenStream tokenStream = analyzer.tokenStream("text", text);
        CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);
        for(String expected : expecteds) {
            Assert.assertTrue(tokenStream.incrementToken());
            Assert.assertEquals(expected, charTermAttribute.toString());
        }
        Assert.assertFalse(tokenStream.incrementToken());
        tokenStream.close();
    }
}

以上代码都是Lucene in Action这本书里面的示例代码,我只不过是基于Lucene5.x把它重写并调试成功了,特此分享,希望对正在学习Lucene5的童鞋们有所帮助。

转载请注明出处:代码说 » Lucene5学习之自定义同义词分词器简单示例

Lucene5学习之多索引目录查询以及多线程查询

上一篇中我们使用多线程创建了索引,下面我们来试着采用不把多个索引目录里的数据合并到一个新的索引目录的方式去查询索引数据,当然你也可以合并(合并到一个索引目录查询就很简单了),其实很多情况我们都是不合并到一个索引目录的,那多索引目录该如何查询呢,在Lucene5中使用的MultiReader类,在Lucene4时代,使用的是MultiSearcher类。至于Lucene多线程查询,只需要在构建IndexSearcher对象时传入一个ExecutorService线程池管理对象即可,具体请看下面贴出的示例代码:

http://7xqzo3.com1.z0.glb.clouddn.com/sundown.jpg

package com.yida.framework.lucene5.index;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import org.apache.lucene.document.Document;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.MultiReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.store.Directory;

import com.yida.framework.lucene5.util.LuceneUtils;

/**
 * 多线程多索引目录查询测试
 * @author Lanxiaowei
 *
 */
public class MultiThreadSearchTest {
    public static void main(String[] args) throws InterruptedException, ExecutionException, IOException {
        //每个线程都从5个索引目录中查询,所以最终5个线程的查询结果都一样
        //multiThreadAndMultiReaderSearch();
        
        //多索引目录查询(把多个索引目录当作一个索引目录)
        multiReaderSearch();
    }
    
    /**
     * 多索引目录查询
     * @throws InterruptedException
     * @throws ExecutionException
     * @throws IOException
     */
    public static void multiReaderSearch()  throws InterruptedException, ExecutionException, IOException {
        Directory directory1 = LuceneUtils.openFSDirectory("C:/lucenedir1");
        Directory directory2 = LuceneUtils.openFSDirectory("C:/lucenedir2");
        Directory directory3 = LuceneUtils.openFSDirectory("C:/lucenedir3");
        Directory directory4 = LuceneUtils.openFSDirectory("C:/lucenedir4");
        Directory directory5 = LuceneUtils.openFSDirectory("C:/lucenedir5");
        IndexReader reader1 = DirectoryReader.open(directory1);
        IndexReader reader2 = DirectoryReader.open(directory2);
        IndexReader reader3 = DirectoryReader.open(directory3);
        IndexReader reader4 = DirectoryReader.open(directory4);
        IndexReader reader5 = DirectoryReader.open(directory5);
        MultiReader multiReader = new MultiReader(reader1,reader2,reader3,reader4,reader5);
        
        IndexSearcher indexSearcher = LuceneUtils.getIndexSearcher(multiReader);
        Query query = new TermQuery(new Term("contents","volatile"));
        List list = LuceneUtils.query(indexSearcher, query);
        if(null == list || list.size() <= 0) {
            System.out.println("No results.");
            return;
        }
        for(Document doc : list) {
            String path = doc.get("path");
            //String content = doc.get("contents");
            System.out.println("path:" + path);
            //System.out.println("contents:" + content);
        }
    }
    
    /**
     * 多索引目录且多线程查询,异步收集查询结果
     * @throws InterruptedException
     * @throws ExecutionException
     * @throws IOException
     */
    public static void multiThreadAndMultiReaderSearch()  throws InterruptedException, ExecutionException, IOException {
        int count = 5;
        ExecutorService pool = Executors.newFixedThreadPool(count);
        
        Directory directory1 = LuceneUtils.openFSDirectory("C:/lucenedir1");
        Directory directory2 = LuceneUtils.openFSDirectory("C:/lucenedir2");
        Directory directory3 = LuceneUtils.openFSDirectory("C:/lucenedir3");
        Directory directory4 = LuceneUtils.openFSDirectory("C:/lucenedir4");
        Directory directory5 = LuceneUtils.openFSDirectory("C:/lucenedir5");
        IndexReader reader1 = DirectoryReader.open(directory1);
        IndexReader reader2 = DirectoryReader.open(directory2);
        IndexReader reader3 = DirectoryReader.open(directory3);
        IndexReader reader4 = DirectoryReader.open(directory4);
        IndexReader reader5 = DirectoryReader.open(directory5);
        MultiReader multiReader = new MultiReader(reader1,reader2,reader3,reader4,reader5);
        
        final IndexSearcher indexSearcher = LuceneUtils.getIndexSearcher(multiReader, pool);
        final Query query = new TermQuery(new Term("contents","volatile"));
        List<Future<List>> futures = new ArrayList<Future<List>>(count);
        for (int i = 0; i < count; i++) {
            futures.add(pool.submit(new Callable<List>() {
                public List call() throws Exception {
                    return LuceneUtils.query(indexSearcher, query);
                }
            }));
        }
        
        int t = 0;
        //通过Future异步获取线程执行后返回的结果
        for (Future<List> future : futures) {
            List list = future.get();
            if(null == list || list.size() <= 0) {
                t++;
                continue;
            }
            for(Document doc : list) {
                String path = doc.get("path");
                //String content = doc.get("contents");
                System.out.println("path:" + path);
                //System.out.println("contents:" + content);
            }
            System.out.println("");
        }
        //释放线程池资源
        pool.shutdown();
        
        if(t == count) {
            System.out.println("No results.");
        }
    }
}

当然你也可以把上面的代码改造成每个线程查询一个索引目录,我上面是每个线程都从5个索引目录中查询,所以结果会打印5次,看到运行结果请不要感到奇怪。

转载请注明出处:代码说 » Lucene5学习之多索引目录查询以及多线程查询