首页 > 代码库 > Lucene 时间排序

Lucene 时间排序

在Lucene4.4中,想要实现搜索结果按照时间倒序的效果:如果两个文档得分相同,那么就按照发布时间倒序排列;否则就按照分数排列。这种效果在Lucene4.6中实现起来极其简单,直接利用search接口的Sort参数即可达成,完全不需要像某些人说的重写Similarity那么麻烦。三两行代码的事情,体现了Make it simple, stupid的精髓。

首先来看看测试例子,这个例子中我建立了四个文档,按照内容-发布日期来表示分别是:

2004年光棍节攻略 , 20041111

2005年光棍节攻略 , 20051111

2006年光棍节攻略 , 20061111

游戏攻略 ,20141111

统一使用“光棍节攻略”来搜索它们,用户希望最新的光棍节攻略排在第一。

如果不做排序处理的话,用户体验非常糟糕:

  1. package com.hankcs.test;
  2.  
  3. import org.apache.lucene.analysis.Analyzer;
  4. import org.apache.lucene.document.*;
  5. import org.apache.lucene.index.*;
  6. import org.apache.lucene.queries.CustomScoreQuery;
  7. import org.apache.lucene.queries.function.FunctionQuery;
  8. import org.apache.lucene.queryparser.classic.ParseException;
  9. import org.apache.lucene.queryparser.classic.QueryParser;
  10. import org.apache.lucene.search.*;
  11. import org.apache.lucene.store.Directory;
  12. import org.apache.lucene.store.LockObtainFailedException;
  13. import org.apache.lucene.store.RAMDirectory;
  14. import org.apache.lucene.util.Version;
  15. import org.wltea.analyzer.lucene.IKAnalyzer;
  16.  
  17. import java.io.IOException;
  18.  
  19. /**
  20.  * @author hankcs
  21.  */
  22. public class TestSortByTime
  23. {
  24.     public static void main(String[] args)
  25.     {
  26.         // Lucene Document的主要域名
  27.         String fieldName = "text";
  28.  
  29.         // 实例化IKAnalyzer分词器
  30.         Analyzer analyzer = new IKAnalyzer();
  31.  
  32.         Directory directory = null;
  33.         IndexWriter iwriter;
  34.         IndexReader ireader = null;
  35.         IndexSearcher isearcher;
  36.         try
  37.         {
  38.             //索引过程**********************************
  39.             //建立内存索引对象
  40.             directory = new RAMDirectory();
  41.  
  42.             //配置IndexWriterConfig
  43.             IndexWriterConfig iwConfig = new IndexWriterConfig(Version.LUCENE_46, analyzer);
  44.             iwConfig.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND);
  45.             iwriter = new IndexWriter(directory, iwConfig);
  46.             //写入索引
  47.             for (int i = 0; i < 3; ++i)
  48.             {
  49.                 int year = 2004 + i;
  50.                 Document doc = new Document();
  51.                 doc.add(new TextField(fieldName, year + "年光棍节攻略", Field.Store.YES));
  52.                 doc.add(new IntField("date",  year * 10000 + 1111, Field.Store.YES));
  53.                 iwriter.addDocument(doc);
  54.             }
  55.             // 加入一个干扰文档
  56.             Document doc = new Document();
  57.             doc.add(new TextField(fieldName, "游戏攻略", Field.Store.YES));
  58.             doc.add(new IntField("date",  20141111, Field.Store.YES));
  59.             iwriter.addDocument(doc);
  60.             iwriter.close();
  61.  
  62.             //搜索过程**********************************
  63.             //实例化搜索器
  64.             ireader = DirectoryReader.open(directory);
  65.             isearcher = new IndexSearcher(ireader);
  66.  
  67.             String keyword = "光棍节攻略";
  68.             //使用QueryParser查询分析器构造Query对象
  69.             QueryParser qp = new QueryParser(Version.LUCENE_46, fieldName, analyzer);
  70.             Query query = qp.parse(keyword);
  71.             System.out.println("Query = " + query);
  72.  
  73.             //搜索相似度最高的5条记录
  74.             TopDocs topDocs = isearcher.search(query, 5);
  75.             System.out.println("命中:" + topDocs.totalHits);
  76.             //输出结果
  77.             ScoreDoc[] scoreDocs = topDocs.scoreDocs;
  78.             for (int i = 0; i < Math.min(5, scoreDocs.length); i++)
  79.             {
  80.                 Document targetDoc = isearcher.doc(scoreDocs[i].doc);
  81.                 System.out.print(targetDoc.getField(fieldName).stringValue());
  82.                 System.out.print(" , " + targetDoc.getField("date").numericValue());
  83.                 System.out.println(" , " + scoreDocs[i].score);
  84.             }
  85.  
  86.         } catch (CorruptIndexException e)
  87.         {
  88.             e.printStackTrace();
  89.         } catch (LockObtainFailedException e)
  90.         {
  91.             e.printStackTrace();
  92.         } catch (IOException e)
  93.         {
  94.             e.printStackTrace();
  95.         } catch (ParseException e)
  96.         {
  97.             e.printStackTrace();
  98.         } finally
  99.         {
  100.             if (ireader != null)
  101.             {
  102.                 try
  103.                 {
  104.                     ireader.close();
  105.                 } catch (IOException e)
  106.                 {
  107.                     e.printStackTrace();
  108.                 }
  109.             }
  110.             if (directory != null)
  111.             {
  112.                 try
  113.                 {
  114.                     directory.close();
  115.                 } catch (IOException e)
  116.                 {
  117.                     e.printStackTrace();
  118.                 }
  119.             }
  120.         }
  121.     }
  122. }

输出:

2004年光棍节攻略 , 20041111 , 0.71185887

2005年光棍节攻略 , 20051111 , 0.71185887

2006年光棍节攻略 , 20061111 , 0.71185887

游戏攻略 , 20141111 , 0.049675122

可以看到文档是严格按照分数排序的,如果分数相同,则按照索引顺序排序,导致最新的文章反而排在最下面。

使用search接口的Sort参数优化搜索结果:

  1. package com.hankcs.test;
  2.  
  3. import org.apache.lucene.analysis.Analyzer;
  4. import org.apache.lucene.document.*;
  5. import org.apache.lucene.index.*;
  6. import org.apache.lucene.queries.CustomScoreQuery;
  7. import org.apache.lucene.queries.function.FunctionQuery;
  8. import org.apache.lucene.queryparser.classic.ParseException;
  9. import org.apache.lucene.queryparser.classic.QueryParser;
  10. import org.apache.lucene.search.*;
  11. import org.apache.lucene.store.Directory;
  12. import org.apache.lucene.store.LockObtainFailedException;
  13. import org.apache.lucene.store.RAMDirectory;
  14. import org.apache.lucene.util.Version;
  15. import org.wltea.analyzer.lucene.IKAnalyzer;
  16.  
  17. import java.io.IOException;
  18.  
  19. /**
  20.  * @author hankcs
  21.  */
  22. public class TestSortByTime
  23. {
  24.     public static void main(String[] args)
  25.     {
  26.         // Lucene Document的主要域名
  27.         String fieldName = "text";
  28.  
  29.         // 实例化IKAnalyzer分词器
  30.         Analyzer analyzer = new IKAnalyzer();
  31.  
  32.         Directory directory = null;
  33.         IndexWriter iwriter;
  34.         IndexReader ireader = null;
  35.         IndexSearcher isearcher;
  36.         try
  37.         {
  38.             //索引过程**********************************
  39.             //建立内存索引对象
  40.             directory = new RAMDirectory();
  41.  
  42.             //配置IndexWriterConfig
  43.             IndexWriterConfig iwConfig = new IndexWriterConfig(Version.LUCENE_46, analyzer);
  44.             iwConfig.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND);
  45.             iwriter = new IndexWriter(directory, iwConfig);
  46.             //写入索引
  47.             for (int i = 0; i < 3; ++i)
  48.             {
  49.                 int year = 2004 + i;
  50.                 Document doc = new Document();
  51.                 doc.add(new TextField(fieldName, year + "年光棍节攻略", Field.Store.YES));
  52.                 doc.add(new IntField("date",  year * 10000 + 1111, Field.Store.YES));
  53.                 iwriter.addDocument(doc);
  54.             }
  55.             // 加入一个干扰文档
  56.             Document doc = new Document();
  57.             doc.add(new TextField(fieldName, "游戏攻略", Field.Store.YES));
  58.             doc.add(new IntField("date",  20141111, Field.Store.YES));
  59.             iwriter.addDocument(doc);
  60.             iwriter.close();
  61.  
  62.             //搜索过程**********************************
  63.             //实例化搜索器
  64.             ireader = DirectoryReader.open(directory);
  65.             isearcher = new IndexSearcher(ireader);
  66.  
  67.             String keyword = "光棍节攻略";
  68.             //使用QueryParser查询分析器构造Query对象
  69.             QueryParser qp = new QueryParser(Version.LUCENE_46, fieldName, analyzer);
  70.             Query query = qp.parse(keyword);
  71.             System.out.println("Query = " + query);
  72.  
  73.             //搜索相似度最高的5条记录
  74.             Sort sort = new Sort(new SortField("text", SortField.Type.SCORE), new SortField("date", SortField.Type.INT, true));
  75.             TopDocs topDocs = isearcher.search(query, 5, sort);
  76.             System.out.println("命中:" + topDocs.totalHits);
  77.             //输出结果
  78.             ScoreDoc[] scoreDocs = topDocs.scoreDocs;
  79.             for (int i = 0; i < Math.min(5, scoreDocs.length); i++)
  80.             {
  81.                 Document targetDoc = isearcher.doc(scoreDocs[i].doc);
  82.                 System.out.print(targetDoc.getField(fieldName).stringValue());
  83.                 System.out.print(" , " + targetDoc.getField("date").numericValue());
  84.                 System.out.println(" , " + scoreDocs[i].score);
  85.             }
  86.  
  87.         } catch (CorruptIndexException e)
  88.         {
  89.             e.printStackTrace();
  90.         } catch (LockObtainFailedException e)
  91.         {
  92.             e.printStackTrace();
  93.         } catch (IOException e)
  94.         {
  95.             e.printStackTrace();
  96.         } catch (ParseException e)
  97.         {
  98.             e.printStackTrace();
  99.         } finally
  100.         {
  101.             if (ireader != null)
  102.             {
  103.                 try
  104.                 {
  105.                     ireader.close();
  106.                 } catch (IOException e)
  107.                 {
  108.                     e.printStackTrace();
  109.                 }
  110.             }
  111.             if (directory != null)
  112.             {
  113.                 try
  114.                 {
  115.                     directory.close();
  116.                 } catch (IOException e)
  117.                 {
  118.                     e.printStackTrace();
  119.                 }
  120.             }
  121.         }
  122.     }
  123. }

输出结果:

命中:4

2006年光棍节攻略 , 20061111 , NaN

2005年光棍节攻略 , 20051111 , NaN

2004年光棍节攻略 , 20041111 , NaN

游戏攻略 , 20141111 , NaN

我们看到“2006年光棍节攻略”因为时间比较新,并且相关性高,就排在了第一。“2005年光棍节攻略”相关度相同,因为时间旧就排在后面一点,而干扰文档“游戏攻略”即使时间最新,因为不相关的原因排在最后面。这种效果正好是我想要的,极大提升了用户体验。

Lucene 时间排序