Document

搜索结果的处理和显示《第六篇》

一、提取搜索结果

  1、TopDocs对象的说明

  • MaxScore:最高得分;
  • TotalHits:匹配到的结果总数;
  • ScoreDocs:匹配到的文档数组(内部可以获得文档Id与分数);

  下面来看看获得的结果信息:

复制代码
    Console.WriteLine(docs.MaxScore);       //输出最高得分
    Console.WriteLine(docs.TotalHits);      //输出搜索结果数量

    foreach (ScoreDoc d in docs.ScoreDocs)  //输出文档得分与文档ID
    {
        Console.WriteLine(d.Score);
        Console.WriteLine(d.Doc);
    }
复制代码

  2、搜索性能说明

  为了快速搜索,可以先将文件索引读入内存,建立起内存中的索引,然后再进行搜索。

    DirectoryInfo dir = new DirectoryInfo(@"D:\3");
    Lucene.Net.Store.Directory directory1 = new SimpleFSDirectory(dir);
    //将索引文件读入内存
    Lucene.Net.Store.Directory rd = new RAMDirectory(directory1);
    //再从内存索引查找
    using (IndexSearcher searcher = new IndexSearcher(rd)){}

二、过滤搜索结果

  当搜索结果中含有的记录只有一部分满足要求时吗,可以通过过滤的方法将无用的记录滤去,从而得到更好的搜索结果。

  过滤搜索结果的方式有这样两种:一种是在搜索结果提取出来以后过滤,另一种是把过滤条件加在搜索条件中实现过滤。

  1、搜索结果提取出来以后过滤

复制代码
    for (int i = 0; i < docs.TotalHits; i++)
    {
        Document doc = MySearcher.Doc(docs.ScoreDocs[i].Doc); //输出 张飞打架 或关系
        string name = doc.Get("title");
        if (name.Contains("张飞"))
        {
            continue;
        }
        Console.WriteLine(name);
    }
复制代码

  2、利用Lucene的Filter过滤

  Filter用于从搜索结果中筛选出满足特定条件的记录。

  1、PrefixFilter 

  PrefixFilter用于从搜索结果中得到具有某一前缀要求的记录。

复制代码
    //搜索包含“打”的结果
    Term t1 = new Term("title", "打");
    Query query = new TermQuery(t1);

    //筛选出以关开始的结果
    Term t2 = new Term("title", "关");
    PrefixFilter filter = new PrefixFilter(t2);

    TopDocs docs = MySearcher.Search(query, filter, 10);
复制代码

   2、NumericRangeFilter

  过滤掉不在某范围内的数据,在搜索结果中过滤掉大于18岁的数据。

  NumericRangeFilter<int> NumFilter = NumericRangeFilter.NewIntRange("Age", 0, 18, true, true);

  过滤掉不在NumericRangeFilter范围内的数据。

  3、CachingWrapperFilter

  CachingWrapperFilter被开发出来用于包装其他的Filter,从而使之具有缓存功能。

  其构造方法如下:

  CachingWrapperFilter(Filter filter);

  使用方法如下:

    NumericRangeFilter<int> NumFilter = NumericRangeFilter.NewIntRange("Age", 0, 18, true, true);
    CachingWrapperFilter c = new CachingWrapperFilter(NumFilter);

  4、FilteredQuery

  FilterQuery是一种带有过滤器的特殊Query类,它是一种包装器,将原始的Query对象和某个过滤器结合起来,从而达到过滤的目的。

  由于有了这个方法的存在,我们有了两种方式来过滤搜索结果:

  1. 使用Query加Filter,得到Filter,然后使用Search(Query对象,Filter对象)方法执行搜索。
  2. 使用Query加Filter,得到Query,然后使用Search(Query对象方法)执行搜索。

   示例:

    NumericRangeQuery<int> query = NumericRangeQuery.NewIntRange("Age", 0, 30, true, true);
    NumericRangeFilter<int> NumFilter = NumericRangeFilter.NewIntRange("Age", 0, 18, true, true);
    FilteredQuery fq = new FilteredQuery(query, NumFilter);
    TopDocs docs = searcher.Search(fq, null, 10);

  5、建议

  1. 使用过滤器消耗资源、慎用。
  2. 如果一定要使用过滤器,应使用带缓存的过滤器,如CachingWrapperFilter进行包装。

三、搜索结果的排序

  Lucene的搜索结果默认是按照得分进行排序的,我们可以根据需要使其按照其他规则进行排序。

  1、按照文档得分排序

  1、默认的排序规则

  Lucene在建立索引时,对每个文档进行评分(计算相关度),也就是确定其权重。这个得分是相对于搜索关键词的,根据不同的关键词去搜索得到的相同的文档,其得分往往不同。

  搜索结果默认是按照得分进行排序的,而且是倒序,即按照得分从高到低进行排序。如果两个文档的得分相同,则按照对文档建立索引的顺序排列。

  如:

  

  

  我们,看到当关键字改变了,得分也变了。

  2、修改默认得分

  Lucene通过一个公式来计算每个文档的得分,这个很复杂,不懂。每个文档的得分受到多个因素的影响,我们可以通过这些因素来改变某个文档的得分。

  最常用的修改得分方式是通过Document的Boost属性:

  document1.Boost = 1.0f;

  Boost是影响得分的主要因素之一。如果不考虑其他因素,boost值越大,得分越大,搜索结果的排名越靠前。但是,由于Lucene搜索结果的排名包括很多因素,所以单纯地增加boost并不总能提高排名顺序。

  Document和Field类都具有这个方法。Document类的Boost属性用来设定其所含有的所有Field的Boost值,Field类的Boost用来设定该Field的Boost值。对于Boost值的设定是建立索引的过程中进行的,也就是对某个文档调用AddDocument方法之前。

    Field f1 = new Field("title", "张飞很能打", Field.Store.YES, Field.Index.ANALYZED);
    f1.Boost = 1.0f;

  Boost设置的值只代表我们对文档排名的期待,这个值要经过进一步的计算才能转化为文档真正的Boost值。我们不需要关心其计算方法,我们需要的是相对排名,而不是绝对数值。

  3、查看得分计算过程

  Lucene提供了一个方法,可以用来查看得分的计算过程。这个方法就是Searcher类的Explain方法。

  该方法调用方式如下:

  searcher.Explain(query, docs.ScoreDocs[i].Doc);

  第一个参数是被解释的Query对象,第二个对象是索引中的文档序号。返回值是一个Explanation对象,这个对象包括GetDetails()、IsMatch(),ToHtml(),ToString()等方法;

复制代码
    for (int i = 0; i < docs.TotalHits; i++)
    {
        Explanation exp = searcher.Explain(query, docs.ScoreDocs[i].Doc);
        Console.WriteLine(exp.IsMatch);
        Console.WriteLine(exp.GetDetails());
        Console.WriteLine(exp.ToString());
    }
复制代码

  其输出结果如下:

  

  执行Explain方法是要付出代价的,而且代价不小。这代价相当于执行对整个索引的查询。所以,除非必要,否则不要轻易使用。

  2、自定义排序算法

  有时候,我们需要修改默认的排序规则。例如,按照日期排序,或者按照多个字段组合排序等。

  假设我们做一个新闻网搜索引擎,如果搜索按照文档得分排序。那么,往往会导致旧的文档排在新文档前面。得分高的文档永远排在前面。

  要解决这个问题,有如下方法:

  1. 修改评分规则,使得新发布的新闻得分永远高于旧新闻。
  2. 在搜索结果显示的过程中做过滤,使得旧新闻在显示的时候排在新发布的新闻后面。
  3. 建立新的排序规则,使搜索结果按照新规则排序。例如,使搜索结果按照日期排序。或者利用评分规则,得分相同的文档按照日期的先后排序。

本处主要讲解第三种方法。
需要自定义排序规则,需要使用Sort类。
1、Sort()
  按照系统自己的排序规则,不施加人工干预。这个构造方法没什么用。
2、Sort(String field)
  按照指定的某个field进行排序,参数是该field的名称。
3、Sort(String[] fields)
  按照指定的多个field进行排序,参数是这些field的名称。
4、Sort(SortField field)
  按照指定的某个field进行排序,注意:这个field不是原始的字段名称,而是SortField对象
5、Sort(SortField[] fields)
  按照指定的多个field进行排序,注意:这些field不是原始的字段名称,而是SortField对象
6、Sort(String field,boolean reverse)
  按照指定的某个field进行排序,第一个参数是该field的名称,第二个参数用来指定是降序(从大到小)还是升序(从小到大),若为false,是升序,这也是默认值。
Sort类除了具有以上方法之外,还具有两个静态属性。

(1)public static final Sort RELEVANCE

    使用RELEVANCE属性相当于没有使用Sort方法,它将排序的规则交给系统去决定了。

  (2)public static final Sort INDEXORDER

    使用INDEXORDER属性相当于按照索引内部的文档编号去进行排序。

  在构造好Sort对象以后,就可以通过Search(Query对象,Sort对象)来执行搜索了。

  注意:

  1. 被指定作为排序依据的字段必须被索引并且不能被分词,这是因为,如果一个字段被分词了,它往往会失去本来的意思。这样一来,排序就无从红丝线。如果我们试图对一个做了分词的字段作排序,Lucene会抛出异常。
  2. 被指定作为排序依据的字段可以存储也可以不存储。
  3. 被指定作为排序依据的字段的值类型必须是可排序的整型、浮点型或者字符型。系统会根据字段的第一个值自动判断字段类型。整型和浮点型排序效率高,系统消耗少。

  3、让系统决定如何排序

  要让系统决定如何排序,可以使用Sort类的RELEVANCE。

  TopDocs docs = searcher.Search(query, null, 1000, Sort.RELEVANCE);

  4、按照索引中的文档编号排序

  按照索引中的文档编号排序,要使用Sort类的INDEXORDER属性。

  TopDocs docs = searcher.Search(query, null, 1000, Sort.INDEXORDER);

  5、按照文档字段,指定类型排序

  我们使用Sort(String field)和Sort(String field,boolean reverse)两个方法来演示按照某个字段进行排序的方法。
  下面的示例,我们队常见的文本字段进行排序。注意:我们的文本字段没有作分词。

复制代码
    //建立索引
    using (IndexWriter writer = new IndexWriter(directory1, analyzer, maxFieldLength))
    {
        Document document1 = new Document();
        document1.Add(new Field("title", "大神", Field.Store.YES, Field.Index.NOT_ANALYZED));
        document1.Add(new Field("time", "2010-01-01", Field.Store.YES, Field.Index.NOT_ANALYZED));
        writer.AddDocument(document1);

        Document document2 = new Document();
        document2.Add(new Field("title", "大神", Field.Store.YES, Field.Index.NOT_ANALYZED));
        document2.Add(new Field("time", "2011-01-01", Field.Store.YES, Field.Index.NOT_ANALYZED));
        writer.AddDocument(document2);
    }
    //查找
    using (IndexSearcher searcher = new IndexSearcher(directory1))
    {
        Term t = new Term("title", "大神");
        TermQuery query = new TermQuery(t);
        SortField sf = new SortField("time", SortField.STRING, true);   //true false 决定顺序Or倒序
        Sort sort = new Sort(sf);
        TopDocs docs = searcher.Search(query, null, 10, sort);
        for (int i = 0; i < docs.TotalHits; i++)
        {
            Document doc = searcher.Doc(docs.ScoreDocs[i].Doc);
            string name = doc.Get("time");
            Console.WriteLine(name);
        }
    }
复制代码

  这里,我们指定了按照time字段进行排序。并且time字段的数据类型为String。所以Lucene.net把他们当做字符串进行排序。如果我们在排序的时候没有指定将某个字段看做某种类型的话,那么Lucene会首先尝试将这个字符串转换成int类型,如果转换失败,会继续尝试将其转换为float类型,如果还是转换失败,这时就把它当做字符串来处理。

  要良好地控制排序规则,我们必须自己去指定Lucene把某个字段当成某种类型。

  其中,SortField支持的类型如下:

  • public const int BYTE = 10;
  • public const int CUSTOM = 9;
  • public const int DOC = 1;
  • public const int DOUBLE = 7;
  • public const int FLOAT = 5;
  • public const int INT = 4;
  • public const int LONG = 6;
  • public const int SCORE = 0;
  • public const int SHORT = 8;
  • public const int STRING = 3;
  • public const int STRING_VAL = 11;

  6、按多个字段排序

  同时按照多个字段排序,只需将作为排序依据的字段合起来,以数组形式传递给Sort方法即可。

  如,下面的示例中,每个文档含有id,age和text三个字段,其中id和age两个字段是可以转换成数字类型的。我们按照id和age两个字段进行组合排序。

    SortField sf1 = new SortField("id",SortField.INT, false);    //按id升序
    SortField sf2 = new SortField("age", SortField.INT, true);    //按age降序
    Sort sort = new Sort(sf1,sf2);
    TopDocs docs = searcher.Search(query, null, 10, sort);

  我们有时需要让排序所依据的多个字段按照不同的升降顺序去排列,这时就需要建立SortField对象,在SortField对象中指定字段的排序规则,然后将SortField对象数组传入Sort类的构造方法。

  7、综述

  排序是消耗资源的事情,越复杂消耗越大。

  通常,按照得分去排序就足够了。

  另外,Lucene提供了对于复杂自定义类型字段的排序接口,例如希望将三维坐标值以字符串形式保存在索引中,然后在提取出来的时候进行排序。但是,要实现这种排序消耗资源太多。

  实际的工厂项目所要求的是执行效率,而不是概念。我们完全可以通过编码解码的方法来解决复杂自定义类型的存储、搜索和排序问题。我们甚至可以为某个不方便排序的字段专门建立一个辅助字段,将复杂数据类型转换成可以表示其大小的数值存储在辅助字段中,这样,搜索的时候就非常容易排序了。

四、搜索结果的高亮显示

  1、关于高亮显示

  当搜索引擎显示出搜索结果之后,最好将搜索结果中的关键词以某种醒目的方式显示出来,从而使用户知道他所搜索的关键词在文档中的位置。搜索引擎完成这项操作就叫做高亮显示。高亮显示的方法就是给文档中的关键词设置特殊的显示格式。例如,加上背景色,或者将关键词的显示颜色改变、加粗、变成斜体等。

  在Web搜索中,通常使用修改关键词文本背景色、加粗显示和修改文本颜色的方法来实现高亮。

  2、Lucene高亮处理的基本方式

  实现高亮的原理是:

  1. 建立索引时,在文档的相关字段(需要高亮处理的字段,被搜索的字段)中记录词条(Term)的位置。
  2. 搜索时,利用字段中记录的词条的位置信息,将修饰符号添加进去,从而改变了搜索关键词的显示格式,达到了突出显示的目的。

  (1)建立索引时,在文档的相关字段中记录词条的位置

  document.Add(new Field("text", "大神", Field.Store.YES, Field.Index.NOT_ANALYZED, Field.TermVector.WITH_OFFSETS));

  最后一个参数表示,在构造Field对象时,就可以将词条位置信息记录进去。

  (2)在搜索时修改词条文本的显示格式

  要实现这一步,首先要构造Highlighter对象,Highlighter类的最基本构造方法是(使用此类要引用Lucene.Net.Contrib.Highlighter.dll):

  Highlighter(IScorer fragmentScorer)

  其参数类型IScorer是一个接口,它的一个常用实现类是QueryScorer,这个类可以有QueryScorer(Query query)方法来构造。

    TermQuery query = new TermQuery(t);
    QueryScorer qs = new QueryScorer(query);
    Highlighter hl = new Highlighter(qs);

  Highlighter类通过TextFragmenter属性来设置文本的分块,其参数是Fragmenter类型,Fragmenter是一个接口,它最常用的子类是SimpleFragmenter,可以通过SimpleFragmenter()或SimpleFragmenter(int fragmentSize)方法来构造。其他第二个构造方法的参数表示分块的大小,也就是显示给用户的含有关键词的文本块的大小。

复制代码
    class Program
    {
        static void Main(string[] args)
        {
            Analyzer analyzer = new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_30);
            IndexWriter.MaxFieldLength maxFieldLength = new IndexWriter.MaxFieldLength(10000);
            Lucene.Net.Store.Directory directory1 = new RAMDirectory();

            //建立索引
            using (IndexWriter writer = new IndexWriter(directory1, analyzer, maxFieldLength))
            {
                Document document1 = new Document();
                document1.Add(new Field("id", "1", Field.Store.YES, Field.Index.NOT_ANALYZED));
                document1.Add(new Field("age", "30", Field.Store.YES, Field.Index.NOT_ANALYZED));
                document1.Add(new Field("text", "刘备", Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_OFFSETS));
                writer.AddDocument(document1);

                Document document2 = new Document();
                document2.Add(new Field("id", "2", Field.Store.YES, Field.Index.NOT_ANALYZED));
                document2.Add(new Field("age", "25", Field.Store.YES, Field.Index.NOT_ANALYZED));
                document2.Add(new Field("text", "关羽", Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_OFFSETS));
                writer.AddDocument(document2);
            }
            //查找
            using (IndexSearcher searcher = new IndexSearcher(directory1))
            {
                Term t = new Term("text", "关");
                TermQuery query = new TermQuery(t);
                QueryScorer qs = new QueryScorer(query);
                Highlighter hl = new Highlighter(qs);
                SimpleFragmenter sf = new SimpleFragmenter(100);
                hl.TextFragmenter = sf;
                //获得高亮处理后的结果
                TopDocs docs = searcher.Search(query, null, 10);
                for (int i = 0; i < docs.TotalHits; i++)
                {
                    Document doc = searcher.Doc(docs.ScoreDocs[i].Doc);string[] StrArr = hl.GetBestFragments(analyzer, "text", doc.Get("text"), 3);
                    Console.WriteLine(StrArr[0]);
                }
            }
            Console.ReadKey();
        }
    }
复制代码

  输出如下:

  

  3、高亮处理的基本设置

  (1)、如果一篇文档中包含许多个被搜索的关键词,那么可以限定显示出来的关键词数量。通过GetBestFragments()方法重载中的maxNumFragments来设定。
  (2)、高亮处理后的结果是一段文本,可以在SimpleFragmenter的构造方法中指定显示给用户的文本长度。
  (3)、通常,我们设置含有关键词的文本块之间使用"..."符号来连接。通过GetBestFragments()方法中的separator参数来设定。
  (4)、默认情况下,高亮处理是使用"<b></b>"来修饰关键词的。我们可以自定义新的修饰格式。

  下面,再举个例子,实现前三个设置。将maxNumFragments设置为2,设定高亮处理后的显示文本长度为60。

复制代码
    using (IndexSearcher searcher = new IndexSearcher(directory1))
    {
        Term t = new Term("text", "关");
        TermQuery query = new TermQuery(t);
        QueryScorer qs = new QueryScorer(query);
        Highlighter hl = new Highlighter(qs);
        SimpleFragmenter sf = new SimpleFragmenter(60);  //设定一个关键词显示周围的内容长度为60个字符
        hl.TextFragmenter = sf;
        TopDocs docs = searcher.Search(query, null, 10);

        for (int i = 0; i < docs.TotalHits; i++)
        {
            Document doc = searcher.Doc(docs.ScoreDocs[i].Doc);
            Lucene.Net.Analysis.TokenStream tokenStream = analyzer.TokenStream("text", new System.IO.StringReader(doc.Get("text")));
            string Str = hl.GetBestFragments(tokenStream, doc.Get("text"), 2, "!!!");  //显示关键词数为2,分隔符为"!!!"
            Console.WriteLine(Str);
        }
    }
复制代码

  4、为高亮显示设置新的格式

  默认情况下,高亮处理是使用"<b></b>"来格式化关键词的。我们可以自定义新的修饰格式。这主要会用到SimpleHTMLFormatter类。

  Highlighter(IFormatter formatter, IScorer fragmentScorer);

  这个构造方法的第一个参数是IFormatter,也就是“格式器”,即用来格式化高亮文本的工具类。SimpleHTMLFormatter类很适合作为格式器。

  (1)SimpleHTMLFormatter()

  构造的格式奇使用"<B>"和"</B>"标记来格式化高亮文本。

  (2)SimpleHTMLFormatter(string preTag,String postTag)

  构造的格式器使用自定义的标记来格式化高亮文本,第一个参数是前标记,第二个参数的后标记。

    SimpleHTMLFormatter stf = new SimpleHTMLFormatter("<font style=\"color:red\">","</font>");
    Highlighter hl = new Highlighter(stf,qs);

  输出效果如下:

  

posted @ 2016-05-05 15:01  从未被超越  阅读(301)  评论(0编辑  收藏  举报