• Lucene教程


    作者:朱小杰

    前言:本教程用于Lucene3.5,Maven地址为

            <dependency>
                <groupId>org.apache.lucene</groupId>
                <artifactId>lucene-core</artifactId>
                <version>3.5.0</version>
            </dependency>

    一:简单的示例

    我就不介绍Lucene了,想来看这篇博客的人,都知道Lucene是什么。直接给出生成索引,和查询的示例

    1.1:生成索引

    生成索引的代码如下:

        /**
         * 创建索引
         */
        public void index(){
            IndexWriter writer = null;
            try {
                //1、创建Derictory
    //        Directory directory = new RAMDirectory();//这个方法是建立在内存中的索引
                Directory directory = FSDirectory.open(new File("G:\TestLucene\index"));//这个方法是建立在磁盘上面的索引
    //        2、创建IndexWriter,用完后要关闭
                IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_35,new StandardAnalyzer(Version.LUCENE_35));
                writer = new IndexWriter(directory,config);
                //3、创建Document对象
                Document document = null;
                File fl = new File("G:\TestLucene\file");
                //4、为Document添加Field
                for(File file : fl.listFiles()){
                    document = new Document();
          
                     document.add(new Field("content",new FileReader(file)));
                    //把文件名存放到硬盘中,不作分词
                    document.add(new Field("fileName",file.getName(),Field.Store.YES, Field.Index.ANALYZED.NOT_ANALYZED));
                    //把绝对路径放到硬盘中,不作分词
                    document.add(new Field("path", file.getAbsolutePath(), Field.Store.YES, Field.Index.NOT_ANALYZED));
                }
                //5、通过IndexWriter添加文档到索引中
                writer.addDocument(document);
    
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if(null != writer){
                    try {
                        writer.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }

    1.1.1:Field.Store和Field.Index

    这里说明一个Field.Index和Field.Store

                    //Field.Store.YES或者NO部分
                    // 如果为YES,代表着是否要把这个域中的内容完全存储到文件中,方便进行还原
                    //如果为NO,代表着不把这个域的内容存储到文件,但是可以被索引,但是这些内容不可被还原
    
                    //Field.Index.ANALYZED:进行分词和索引,适用于标题,内容等
                    //Field.Index.NOT_ANALYZED:进行索引,但是不进行分词。精准的数据不分词,像id,身份证号,姓名等不分词,用于精确搜索
                    //Field.Index.ANALYZED_NOT_NORMS:进行分词但是不存储norm信息,这个norms中包括了索引的时间和权值等信息
                    //Field.Index.NOT_ANALYZED_NOT_NORMS:既不进行分词,也不存储norms信息
                    //Field.Index.NO:完全不进行索引

    1.1.2:为数字生成索引

    看过Field构造方法的人可能知道,这里面并没有对数字索引添加方法,那么会有人说,把数字转换成字符串?额。数字在索引中处理方式与字符串不同,我们可以使用一个新的对象

      //搜索content中包含有着like的
                TermQuery termQuery = new TermQuery(new Term("content","like"));
                    //给数字加索引要用另一个对象
                    document.add(new NumericField("attachs").setIntValue(attachs[i]));
                    //给数字加索引要用另一个对象
                    //查看源码会发现,这个构造函数默认是不存储,但是会进行索引
                    document.add(new NumericField("attachs").setIntValue(attachs[i]));
                    //通过这个构造方法,可以把其修改为存储,最后的boolean参数代表着是否索引
                    document.add(new NumericField("attachs", Field.Store.YES,true).setIntValue(attachs[i])); 

    这里使用一个新的字段,NumericField

    1.1.3:为索引加权

    大家看到搜索引擎的排序,就肯定能猜到,搜索引擎是按照了一定的要求,对查询的结果进行了排序,这里介绍一个简单的加权排序方法,后面会深入研究

    //加权
    document.setBoost(2.1f);

    注意:权重越大,排序越前

    1.1.4:为日期生成索引

    既然数字有专门的NumericField,那么给日期生成索引,是不是也有DateField呢?其实是没有的,那怎么办?

    但是我们都忽略了一件事,日期其实也是一个long类型的数字

     document.add(new NumericField("attachs", Field.Store.YES,true).setLongValue(new Date().getTime()));

    这不就行了吗?

    1.2:查询

    这里演示根据已生成的索引,来查询

    代码如下:

     /**
         * 搜索
         */
        public void searcher(){
            try {
                //1、创建Directory
                Directory directory = FSDirectory.open(new File("G:\TestLucene\index"));
                //2、创建IndexReader,需要关闭
                IndexReader reader = IndexReader.open(directory);
                //3、根据IndexReader创建IndexSearcher
                IndexSearcher searcher = new IndexSearcher(reader);
                //4、创建索引的Query
                //第二个参数代表着要搜索的域
                QueryParser parser = new QueryParser(Version.LUCENE_35,"content",new StandardAnalyzer(Version.LUCENE_35));
                //表示搜索content中包含java的文档
                Query query = parser.parse("朱小杰");  
                //5、根据searcher搜索并返回TopDocs
    //            表示返回前面10条
                TopDocs topDocs = searcher.search(query,10);
                //6、根据TopDocs获取ScoreDoc对象
                ScoreDoc[] scoreDocs = topDocs.scoreDocs;
                for(ScoreDoc sd : scoreDocs){
                    //7、根据Searcher和ScordDoc对象获取具体的Document对象
                    //获取这个文档的id
                    int doc = sd.doc;
                    Document document = searcher.doc(doc);
                    //8、根据Document对象获取需要的值
                    System.out.println("【找到】" + document.get("fileName") + "    " + document.get("path") + " .." + document.get("content"));
                }
                reader.close();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (ParseException e) {
                e.printStackTrace();
            }
    
        }

    1.2.1:介绍IndexReader

    IndexReader顾名思义,它是用来读取索引的信息的,下面来演示一些它的用法

    (1)获取文档的数量

    //存储的文档数量,也就是document对象的数量,删除索引后,这个数值会减少
    System.out.println("存储的文档数量: " + reader.numDocs());

    (2)获取文档的总量

    //存储过的文档的最大数量,删除索引后,数量不会减少
    //此时删除的文件并不会完全删除,它存在回收站里面
    System.out.println("文档存储的总存储量: " + reader.maxDoc());

     (3)获取已删除文档的数量

    System.out.println("删除文档的数量: " + reader.numDeletedDocs());

    1.3:删除

    下面给出删除的代码

        /**
         * 删除索引
         */
        public void delete(){
            try {
                IndexWriter writer = null;
                writer = new IndexWriter(directory,new IndexWriterConfig(Version.LUCENE_35,new StandardAnalyzer(Version.LUCENE_35)));
    
                //删除全部的索引
                //writer.deleteAll();
    
                //参数可以为一个查询的Query,也可以为一个Term,它是一个精确的值,代表着把id为1的给删除掉
                writer.deleteDocuments(new Term("id","1"));
    
    
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    注意,这里的删除,并不是真的删除。执行完之后,可以在索引的目录里面看到多了一个.del的文件,那是一个类似回收站的文件,在回收站中的文件是可以进行还原的

    1.3.1:还原删除的文档

    之前有说到,删除并没有作真正的删除,而是把这个文件放到了类似回收站的位置中,下面来使用代码来进行还原已删除的文件

     /**
         * 删除索引并不是完全删除,它是有着一个回收站的功能
         * 上面的delete删除了一个索引,这里进行恢复
         */
        public void recovery(){
            try {
                //这一步很重要,因为默认打开的reader是只读的,所以这里要通过构造方法,把它的readonly设置为false,否则会抛出异常
                IndexReader reader = IndexReader.open(directory,false);
                //还原所有已删除的数据
                reader.undeleteAll();
    
                reader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    注意:上面的构造方法和以往不同,后面多了一个boolean值,这个值,如果不写,默认是true,代表着只读,那么如果在这种情况下进行还原,是会抛出异常的。这里将其设置为false,也就是把只读设置为了false,这样就可以还原了。

    1.3.2:清空回收站里面的数据

    上面说完从回收站里面还原数据,那么回收站怎么清空掉呢?下面给出代码:

      /**
         * 清空回收站里面的数据
         */
        public void clearRecovery(){
            try {
                IndexWriter writer = new IndexWriter(directory,new IndexWriterConfig(Version.LUCENE_35,new StandardAnalyzer(Version.LUCENE_35)));
    
                writer.forceMergeDeletes();
                //代表着是否等待当前操作完成后,再清空回收站里面的数据
                writer.forceMergeDeletes(true);
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    这里面是有着两个重载的方法,其中一个是立即删除,一个是等待当前操作完成后,再删除

    1.4:更新

    更新一个索引的代码如下:

        /**
         * 更新数据
         */
        public void update(){
            try {
                //注意,Lucene其实并没有更新的操作,它的实际原理是先删除,再添加
                IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_35,new StandardAnalyzer(Version.LUCENE_35));
                IndexWriter writer = new IndexWriter(directory,config);
    
                Document document = new Document();
                document.add(new Field("id","1", Field.Store.YES, Field.Index.NOT_ANALYZED));
                writer.updateDocument(new Term("id","1"),document);
    
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    值得注意的是,这里的更新,并不是在原有的记录里面更新,而是先把该记录删除,然后增加新的记录,所以在查看已删除的文档数量里面会发出多出一条记录,同样的,在文档总量里面,也会增加一条记录

    二:IndexReader的设计

    2.1:设计单例的IndexReader

    为什么要设计一个单例的IndexReader呢?大家可以试着去想像,假如说一个硬盘上面的索引,随着日期的增加,那么它的索引也就越来越多,当我打开一个IndexReader的时候,肯定是要读取索引里面的信息的,如果索引文件过多的话,那么肯定是会造成创建这个对象的时间及性能上面的消耗,所以IndexReader很有必要设计成单例的。

    2.2:当索引的内容发生改变时,单例的IndexReader对象不会改变的问题

    由上面的单例IndexReader,这里又有着一个新的问题,那就是在一个项目中,存在一个单例的IndexReader的时候,虽然可以大大提升性能,但是也有一个问题。IndexReader对象里面的索引内容,是在这个对象被创建的时候生成的,也只有在那个时候,IndexReader才能读取到索引目录里面的数据。

    问题就是,当索引内容添加,或者删除过后,IndexReader的对象不会发生改变!!

    下面来研究创建IndexReader的方法:

    //根据一个Directory创建一个IndexReader
    IndexReader reader = IndexReader.open(directory);

    上面的这个创建IndexReader的方法,将会读取索引中所有的数据,首先消耗性能是肯定的。

    其实还有一个创建IndexReader的方法,如下:

    //这种创建IndexReader的方法,就是把老的IndexReader对象传进去,然后会判断索引的内容是否会发生改变,如果索引内容发生改变,则会创建一个新的对象,如果索引的内容没有发生改变,则会返回空
    IndexReader ir = IndexReader.openIfChanged(reader);

     这是一个新的方法,通过这个方法,就可以知道是否需要产生新的IndexReader方法,下面来演示一下IndexReader的设计

    public class CustomerIndexReader {
        static {
            try {
               directory = FSDirectory.open(new File("d:/index"));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        private static IndexReader reader = null;
        private static Directory directory = null;
    
        public CustomerIndexReader(){
    
        }    
        public IndexReader getIndexReader(){
            if(directory == null){
                synchronized (this){
                    if(directory == null){
                        try {
                            reader = IndexReader.open(directory);
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }else{
                try {
                    IndexReader ir = IndexReader.openIfChanged(reader);
                    if(ir != null){
                        //如果这个对象不为空,则代表着索引发生了改变
                        reader = ir;
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return reader;
        }
    }

     其实IndexReader也有删除文档的方法。而且它可以保证IndexReader的数据是最新的数据。也就是reader.deleteDocument()

    三:查询的方式

    3.1:精确查询

    何为精确查询,精确查询就相当于数据库的=号,也就是查询的字符,与索引中字符必须完全一致,才能匹配到

        public void searchers(){
            try {
                Directory directory = FSDirectory.open(new File("d:/index"));
                IndexReader reader = IndexReader.open(directory);
                IndexSearcher searcher = new IndexSearcher(reader);
                //这个是精确查询
                Query query = new TermQuery(new Term("name","大牛"));
    
                reader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    如上面的代码,会在name域里面找名字为“大牛”的结果,但是如果搜索“大”,或者“牛”,就找不到结果,因为TermQuery是精确查询

     3.2:字符串的范围搜索

    说完精确搜索,下面介绍一下范围搜索。范围搜索,也就是指在一定区间范围内查询,下面给出代码的示例。

       public void searcher1(){
            try {
                Directory directory = FSDirectory.open(new File("d:/index"));
                IndexReader reader = IndexReader.open(directory);
                IndexSearcher searcher = new IndexSearcher(reader);
                //这是珍上范围搜索,意思是搜索id域中,最低为1,最高为10,后面的两个boolean的参数分别代表着,是否包好最低值与最高值
                //但是数字类型是查不出来的,也就是NumericField来存储field的类型,使用TermRangeQuery是查不出来的,需要使用NumericRangeQuery
                Query query = new TermRangeQuery("id","1","10",true,true);
    
                //查询名字以a 开头,到以f开头的
              //  Query query = new TermRangeQuery("name","a","f",true,true);
          reader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    注意:TermRangeQuery无法查询数字的结果,也就是使用NumericField来存储的索引,但是可以查询"1","2"字符串类型的数字。

     3.3:数字的范围搜索

    上面说了字符串的范围搜索,而且还特意强掉了,数字不能用TermRangeQuery,那么如果数字的范围搜索,要怎么做呢?可以使用NumericRangeQuery,下面给出代码:

       public void searcher2(){
            try {
                Directory directory = FSDirectory.open(new File("d:/index"));
                IndexReader reader = IndexReader.open(directory);
                IndexSearcher searcher = new IndexSearcher(reader);
    
                //这里是int整数的查询方法,其实,还有float,long,double等方式,也都是通过NumericRangeQuery这个类
               // NumericRangeQuery.newDoubleRange(..);  这是Double类型的
                //NumericRangeQuery.newFloatRange(...);  这是Float类型的
                //NumericRangeQuery.newLongRange(...);   这是Long类型的
            //这里的意思是查询age域中,1岁到100岁的,其中,包含1岁和100岁的
    
                Query query = NumericRangeQuery.newIntRange("age",1,100,true,true);
    
                searcher.close();
                reader.close;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

     3.4:前缀搜索

    前缀搜索,就是对于一个域中的前缀进行匹配,当然,它也会匹配分词后的前缀

    Query query = new PrefixQuery(new Term("name","刘"));

    上面的代码,会找出所有name中姓刘的数据。

    注意:如果内容中进行了分词,那将会查找每一个分词中的以此字符开头的数据。

     3.5:通配符搜索

    通配符大家应该听说过的,那就是*代表任何字符,?代表一个字符

    Query query = new WildcardQuery(new Term("content","*a?"));

     3.6:连接多个条件的查询

    有的时候,查询一个复杂的数据,一个搜索条件,可能不满足结果,那么就可以使用BooleanQuery

               //这个query下面可以add任何多个查询条件
                BooleanQuery query = new BooleanQuery();
                //名字一定是张三
                query.add(new TermQuery(new Term("name","张三")),     
                BooleanClause.Occur.MUST);
                //名族一定不是汉族
                query.add(new TermQuery(new Term("nation","汉")), 
                BooleanClause.Occur.MUST_NOT);
          //可以出现,也可以不出现
                query.add(new WildcardQuery(new Term("content","a")), 
                BooleanClause.Occur.SHOULD);

    BooleanQuery就是Query的扩展类,这个类可以增加任意多个查询条件,并且通过Occur枚举过定义,查询条件的必要性

    3.7:短语间隔搜索

    就是查询一定区间的字符。可能这句话说不明白,我们用代码来说明:

    假如我有下面的一段字符

    I love lucene very much

    那么我现在的目的是,我忘了中间的单词是什么了,我只记得开头为I,结尾为much,那要怎么做呢?

                PhraseQuery query = new PhraseQuery();
                //第一个结果,注意I会变成小写
                query.add(new Term("content","i"));
                //代表着中间相隔3个单词
                query.setSlop(3);
                //第二个结果
                query.add(new Term("content","nuch"));
    

      注意:大写的开头,会被转换成小写哦,但是这种方法开销很大,尽量少用

    3.8:模糊查询

    这里要先说明一下,模糊查询与通配符查询是有区别的。模糊查询是代表着允许有着一定的错别字

    这里来进行说明一下,假如我有这样的一些name属性

    jane  mike  kangkang 

    当我写出下面的代码的时候

                //通过这个,肯定是可以找到mkie的结果的
                FuzzyQuery query1 = new FuzzyQuery(new Term("name","mike"));
                //这里我把i写成了a,但是也是可以查到mike的
                FuzzyQuery query2 = new FuzzyQuery(new Term("name","make"));

    上面的代码代表着,FuzzyQuery,允许有着一定的错别字

    那么可以控制查询字符的错别字吗?

    答案是可以的,如下面的代码:

    //通过第2个float参数调整相似度,值越低,代表相似度越低,容错率越高
    FuzzyQuery query3 = new FuzzyQuery(new Term("name","make"),0.5f,0);

    它会有着一定的容错率

    3.9:QueryParser的使用

    在刚开始的示例中,就使用过QueryParser的这个对象,现在就来重点的说明一下。

    QueryParser它支持一定的查询表达式,什么是查询表达式呢?下面用代码来演示一下

                //创建一个默认搜索域为content的parser
                QueryParser parser = new QueryParser(Version.LUCENE_35,"content",new StandardAnalyzer(Version.LUCENE_35));
                //改变字符串的默认操作符,下面改成AND
                //parser.setDefaultOperator(QueryParser.Operator.AND);
                //开启第一个字符的通配符的匹配,lucene默认是关闭的,因为效率太低
                parser.setAllowLeadingWildcard(true);
    
                //搜索content中包含like的
                Query query = parser.parse("like");
    
                //搜索有dog或者cat的,空格默认就是OR
                query = parser.parse("dog cat");
    
                //改变搜索域为name,搜索其中的jie
                query = parser.parse("name:jie");
    
                //使用通配符*和?来进行匹配
                query = parser.parse("name:j*");
                //通配符默认是不能放在首位的,因为其效率太低,lucene默认关闭了,上面已经开始,所以不会抛异常
                query = parser.parse("name:*e");
    
                //搜索name中没有dog,默认域content中有eat的条件
                query = parser.parse("- name:dog + eat");
    
                //匹配一个区间,TO必须是大写,这个区间是开区间,这个是字符的1,数字的不能
                query = parser.parse("id:[1 TO 3]");
                //这个是闭区间,只会匹配到2,这个是字符的1,数字的不能
                query = parser.parse("id:{1 TO 3}");
    
                //默认域中是dog或者cat,但是age是11的
                query = parser.parse("(dog OR cat) AND age:11");
    
                //匹配两个相连的字符串,这里不会被分割,代表着默认域中,这两个字符串相连的才会被搜索出来
                query = parser.parse(""hello world"");
  • 相关阅读:
    【分享】你敢来挑战?程序员等级
    [css]我要用css画幅画(二)
    [css]我要用css画幅画(一)
    关于学习javascript的一些建议
    孩子们眼中的世界
    全职妈妈再就业辅助计划
    程序员如何打扫卫生并向老婆汇报
    《夏洛特烦恼》观后感
    软件开发之: 做“工程”还是做“艺术品”
    2014年年度总结
  • 原文地址:https://www.cnblogs.com/lxl57610/p/7410806.html
Copyright © 2020-2023  润新知