31 July 2014

桌面搜索器的需求很简单:读取指定目录的文本文件,建立索引,根据关键词返回文件名与路径。真正需要处理的细节,是重复建索引、文件变化、编码和搜索资源的生命周期。

项目源码使用早期 Lucene API。下面按该项目的 Lucene 3.x 思路说明关键片段,不能直接和后续主版本的类混合编译。

路径是比时间戳更合适的标识

用当前毫秒作为文件 ID,同一毫秒处理多个文件可能冲突,也无法在下一次扫描时识别同一文件。应使用规范化路径或明确的业务文件 ID,重复扫描时更新同一条索引记录。

String path = file.getCanonicalPath();
Document doc = new Document();
doc.add(new Field("filepath", path, Field.Store.YES, Field.Index.NOT_ANALYZED));
doc.add(new Field("filename", file.getName(), Field.Store.YES, Field.Index.ANALYZED));
doc.add(new NumericField("inputdate", Field.Store.YES, true)
        .setLongValue(file.lastModified()));
// 此前应读取并添加 content 字段。
indexWriter.updateDocument(new Term("filepath", path), doc);

这里依赖已有 IndexWriter 与对应 Lucene 导入,是索引构建片段。正文读取使用 try-with-resources 或 finally 关闭输入流,并限制可处理的文件大小。目录枚举可能失败,符号链接也可能形成循环,不能假设 listFiles 永远返回有效列表。

分词与搜索保持一致

QueryParser parser = new QueryParser(Version.LUCENE_30, "content",
        new StandardAnalyzer(Version.LUCENE_30));
Query query = parser.parse(keyword);
Sort sort = new Sort(new SortField("inputdate", SortField.LONG, true));
TopDocs hits = searcher.search(query, null, 50, sort);

使用已有 searcher,并确保写入时也采用匹配的分析器。排序字段与建索引字段保持同名;最多需要 50 条时,查询阶段就限制为 50,而不是先请求 Integer.MAX_VALUE 再截断数组。

QueryParser 接收的是查询语言,用户输入中的冒号、括号等可能有特殊含义。如果只接受普通关键词,应按实际查询设计转义或构造 Query,而不直接开放所有语法。

搜索结果与文件变化一起管理

搜索器与 reader 可以复用,但必须明确刷新和关闭时机;写入 commit 后,已有搜索器也未必立即看到变化。文件修改后更新索引,文件删除后删除对应记录,才能保持结果与目录一致。

大文件、无法解码的文本和无权限目录应单独记录,避免一次失败终止全部扫描。只有一条文件从新增、检索到修改、删除都能被正确处理,这个小工具才不只是一次性的全文扫描。

参考:Lucene 3.6 API。





blog comments powered by Disqus
Fork me on GitHub