# 优化 Lucene 搜索速度的一点建议

> Lucene 是一个全文检索的框架，具体干嘛的自己 Google，在公司项目中使用到了 Lucene 记录用户的操作日志来做用户的行为分析（好像 Lucene 本来不是干这个用的。。），开始的时候索引的数量少，搜索速度很快，随着日志的数量级增大（目前已到千万级），索引文件的大小已攀升到5个多G，搜索的速度已经明显变慢，后来在 apache 官网找到了相关文章介绍优化 Lecene 搜索速度的文章，记录一下此次优化的过程

没有代码谈什么优化，先上一段示例代码，传入一个完整的用户列表，根据用户在时间段内是否登录过 APP 来切分列表

```java
public Map<String, List> cutListByLoginApp(List<Map> userList, String startDate, String endDate) {
    Map<String, List> tMap = new HashMap<>();
    try {
        // 登陆过APP
        List loginAppUserList = new ArrayList<>();
        // 未登录过APP
        List unLoginAppuserList = new ArrayList<>();

        IndexReader reader = DirectoryReader.open(FSDirectory.open(Paths.get(App.getIndexPath())));
        IndexSearcher searcher = new IndexSearcher(reader);

        // 限制时间范围
        Term startTime = new Term("dt", DataAnalyzerUtil.dateToTime(startDate));
        Term endTime = new Term("dt", DataAnalyzerUtil.dateToTime(endDate));
        TermRangeQuery timeQuery = new TermRangeQuery("dt", startTime.bytes(), endTime.bytes(), true, false);

        QueryParser userIdQueryParser = new QueryParser("userid", new StandardAnalyzer());

        for (Map userInfo : userList) {
            BooleanQuery.Builder builder = new BooleanQuery.Builder();
            builder.add(timeQuery, Occur.MUST);
            Query userIdQuery = userIdQueryParser.parse(userInfo.get("userId").toString());
            builder.add(userIdQuery, Occur.MUST);
            // 搜索
            TopDocs results = searcher.search(builder.build(), searcher.getIndexReader().maxDoc());
            if (results.totalHits != 0) {
                // 有访问记录
                loginAppUserList.add(userInfo);
            } else {
                // 没有访问记录
                unLoginAppuserList.add(userInfo);
            }
        }
        tMap.put("loginAppList", loginAppUserList);
        tMap.put("unLoginAppList", unLoginAppuserList);
    catch (Exception e) {
        e.printStackTrace();
    }
    return tMap;
}
```

### 官方文章中一些建议

> 文章链接: <https://wiki.apache.org/lucene-java/ImproveSearchingSpeed>

* **确定你真的需要去优化 Lucene 搜索的速度**

大意就是你的应用可能比较复杂，请确定程序慢是 Lucene 慢导致的，此文就是为了优化 Lucene 的搜索速度，这一点略过

* **确定你使用的是最新版本的 Lucene**

我用的版本是 5.5.3，截止至此文发布的最新版本是 6.3.0，为了项目稳定，能不升级就不升级，这一点我没有去实践，所以不发表意见

* **使用本地的文件系统**

意思是将索引文件放在本地，不要放在远程服务器上，如果一定要放在远程服务器，那就将远程的文件系统挂载设为只读模式，可能会提升一点搜索速度，我的索引文件就在本地，还没有做分布式，这一步也没有进行尝试，欢迎尝试过的朋友和大家分享一下心得

* **用更好的硬件，更快的系统**

硬件不是我想换，想换就能换。。。系统不是我想装，想装就能装。。。

* **优化你的操作系统**

。。。。。。以后再说吧

* **把 IndexReader 设为只读模式**

据官方文档说这在多线程共享同一个 IndexReader 的应用中效果很明显，因为这样会消除线程之间相互争抢某些资源，但是我还没有找到在哪里可以设置，以后找到了再来更新文章。。。

* **在非 Windows 平台，使用 NIOFSDirectory 代替 FSDirectory**

终于有一点可以进行优化了，公司项目部署在 Linux 环境下，果断将代码 `IndexReader reader = DirectoryReader.open(FSDirectory.open(Paths.get(App.getIndexPath())));` 改为 `IndexReader reader = DirectoryReader.open(NIOFSDirectory.open(Paths.get(App.getIndexPath())));` ，实际测试效果不是很明显，但是效果都是累积出来的，按照文档改。。

* **增加的你的内存，提高 JVM 的堆大小**

索引文件越大，搜索时会使用更多的内存，如果你没有足够的内存或者 JVM 的堆大小不够，那么搜索的速度就会很慢，这一点同样目前无法实践

* **使用单例的 IndexSearcher**

在你的程序中使用一个单例的 IndexSearcher 对象在线程之间共享，这一点没有去做，原因是我的索引是实时更新的，如果只使用一个单例对象的话，无法实时的获取最新的索引

* **测试性能的时候，忽略第一个 Query**

第一个 Query 在搜索的时候要初始化缓存，由尤其是要以一个字段进行排序的时候

* **只有在必要的时候才重新打开 IndexSearcher**

只有当要搜索最新的索引的时候再重新打开 IndexSearcher

* **减少合并因子（mergeFactor）**

更小的 mergeFactor 意味着更少的 segments，并且会使搜索速度更快，但是它会降低建立索引的速度，所以你应该尝试不同的值，在索引和搜索之间找到一个平衡点，现在重建索引基本不可能，所以没有并没有实践

* **限制存储字段和 term vectors 的使用**

Lucene 在建立索引的时候，每个字段都有两个属性，一个是 index（索引），另一个是 stored（存储），将不需要存储的字段的 stored属性设为 flase，也是大大提高搜索的速度，同样也是无法重建索引，没有实践

* **使用 FieldSelector**

在检索的时候，FieldSelector 会去选择加载哪些字段以及如何去加载他们

* **如果非必要，不要去迭代结果集**

迭代所有的结果集会很慢，有两个原因：一是当你需要100个以上的结果的时候，search() 方法会在返回的 Hits 对象中再次执行搜索，解决方法是使用 HitController 去替代；二是搜索的结果集会分布在上，这会增加 IO 开销，除非它很小，可以被加载到内存中，如果你不需要一个完整的 document，你可以从 FieldCache 中快速的访问一个字段

* **使用模糊查询的时候使用长度最小的前缀**

模糊查询会让 CPU 执行很密集的字符串比较方法，更短的前缀会提高模糊搜索的速度

* **考虑使用过滤器**

使用过滤器比用一条查询语句可以更高效的限制检索的结果，尤其是在索引数量很大的时候，查询和过滤器的区别是，查询对分数有影响，但是过滤器不会

* **找到程序的瓶颈在哪里**

复杂的查询分析和结果集的再处理是 Lucene 搜索的隐藏瓶颈所在，可以使用 VisualVM 等工具来找到问题的所在

### 开始优化

看完了官方的建议之后，发现有用的建议其实并不太多（我水平太低，能用上的不多。。），但是有一点可以着重去做一下优化，就是在非必要的时候，不要去迭代结果集，在示例代码中可以看到这样一句

```java
TopDocs results = searcher.search(builder.build(), searcher.getIndexReader().maxDoc());
```

这句开始执行搜索操作，返回一个结果集，通过 `results.totalHits` 的值可以判断用户是否有访问记录，但是可以看到 `searcher.getIndexReader().maxDoc()` 这个值是随着索引文件的增大而不断变大的，之前取这个值是因为我要遍历所有的结果集，但是现在的需求是我只要知道用户有过访问记录就可以，而不需要知道他具体访问了哪些页面，按照文档的建议，使用 HitController 替代，于是改为以下代码

```java
TotalHitCountCollector totalHitCountCollector = new TotalHitCountCollector();
searcher.search(builder.build(), totalHitCountCollector);
if (totalHitCountCollector.getTotalHits() != 0) {
    // 有访问记录
    loginAppUserList.add(object);
} else {
    // 没有访问记录
    unLoginAppuserList.add(object);
}
```

经过测试，速度有了很大幅度的提升，基本不超过 1ms，而之前没优化之前查询一次要两秒左右，对两万多个用户进行遍历的话，时间简直不敢想象。。。这次优化先到这里结束，之后如果再次优化会更新此文章，由于笔者菜鸟一枚，文中难免会有错误之处，欢迎大家读阅斧正。
