上一篇文章中提到,使用lucene包括两个步骤:一是索引;二是检索。索引是基础、是前提,检索是目的。本文讲的是lucene的基本索引。
本文以及后面的文章都以存储在磁盘的文件为背景,进行索引和检索的演示。
对磁盘文件,我们可能有以下的检索需求:
-
- 按照文件名检索(这个经常有)
- 按照文件路径检索(这个。。。)开玩笑,我们没有这样的检索需求,但是我们需要从检索结果中了解这个信息
- 按照文件类型检索
- 按照文件大小检索
- 按照修改时间检索
- 按照文件内容检索
- ……
撇开lucene不谈,想一下我们人会怎么处理这样的需求。
好了,我们可能会拿一张纸、一支笔,然后填写类似下面的表格:
序号 | 文件名 | 文件路径 | 文件类型 | 文件大小 | 修改时间 | 文件内容 | …… |
有了这样的表格,我们就可以“按图索骥”,完成上面的检索任务了。
这个填充表格的过程就是索引的过程,与lucene的对应为:
-
纸:也就是保存索引的地方。在lucene中对应为Directory。lucene中有几种Directory的实现,最常用的是FSDirectory和RAMDirectory。从名称中不难知道:FSDIrectory是将索引保存到磁盘文件中,就相当于本例中的纸;RAMDirectory是将索引保存到内存中,就相当于本例中把内容保存到大脑中 。
-
笔:也就是写索引的工具。在lucene中对应为IndexWriter。有Java基础的人应该可以推测,这个IndexWriter是一个Writer的子类。
- 记录:在lucene中对应为Document。
- 字段:在lucene中对应为Field(IndexableField)。每个Field实例可以设置:字段名、是否建立索引、是否分词、是否存储等几个属性。对于设置为存储的字段,我们可以从Document中直接读取该字段的值,而当试图从Document中取未存储字段的值时,返回null值。经常使用的Field子类有以下几个:
Field的子类 | 是否建立索引 | 是否分词 | 是否存储 |
StringField | 是 | 否 | 可以控制 |
DoubleField FloatField LongField IntField | 是 | 否 | 可以控制 |
TextField | 是 | 是 | 可以控制 |
StoredField | 否 | 否 | 是 |
- 序号:数据库中有自增的Id,在lucene中,也有一个自增的Id,称为docId。这个字段不需要你指定,而是lucene自动生成的。在检索时,lucene可以根据docId,确定唯一的Document。
有了上面的分析,我们需要记录的字段有:
- 序号:这个是lucene自动生成的,不需要处理
- 文件名:StringField,存储。字段名为filename
- 文件路径:StoredField,存储。字段名为pathname
- 文件类型:StringField,存储。字段名为type
- 文件大小:LongField,存储。字段名为size
- 修改时间:LongField,存储。字段名为lastmodified
- 文件内容:TextField,不存储。字段名为content
下面进行程序设计。
伪代码如下:
创建一个对磁盘文件进行索引的类,并提供一个索引的方法,接收两个参数:第一个参数为索引保存路径,第二个参数为磁盘文件(夹)路径(可以是多个)。
这个方法的实现:
1. 获得Directory对象-->获得IndexWriter对象;
2. 对每个文件(夹)进行索引:
如果是文件,则将文件信息转为Document对象,并将Document对象加入IndexWriter对象中;
如果是文件夹,则对于文件夹下的文件或文件夹,重复2过程。
3. 关闭IndexWriter对象和Directory对象。
编程实现:
package cn.lym.lucene.quickstart.index;
import java.io.File;
import java.io.FileReader;
import java.io.Reader;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.document.LongField;
import org.apache.lucene.document.StringField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.util.Version;
import cn.lym.lucene.quickstart.util.FileUtil;
import cn.lym.lucene.quickstart.util.StreamUtil;
/**
* 提供对磁盘文件建立索引的功能
*
* @author liuyimin
*
*/
public class Indexer {
/**
* Logger对象
*/
private static final Logger logger = LogManager.getLogger(Indexer.class);
/**
* 建立索引
*
* @param indexDir
* 索引保存路径
* @param dataDirs
* 数据文件路径
* @throws Exception
*/
public void index(String indexDir, String... dataDirs) throws Exception {
Directory directory = FSDirectory.open(new File(indexDir));
IndexWriterConfig config = new IndexWriterConfig(Version.LATEST, new StandardAnalyzer());
IndexWriter writer = new IndexWriter(directory, config);
for (String dataDir : dataDirs) {
index(writer, new File(dataDir));
writer.commit();
}
// 关闭流
StreamUtil.close(writer, directory);
}
/**
* 对文件(或目录)建立索引
*
* @param writer
* IndexWriter对象
* @param file
* 文件或目录
*/
private void index(IndexWriter writer, File file) {
if (file.isDirectory()) {// 目录,需要递归建立索引
File[] subFiles = file.listFiles();
if (subFiles != null) {
for (File subFile : subFiles) {
index(writer, subFile);
}
}
} else if (file.isFile()) {// 文件,对文件建立索引
if (logger.isDebugEnabled()) {
logger.debug("indexing file: " + file.getAbsolutePath());
}
try {
Document document = file2Document(file);
writer.addDocument(document);
} catch (Exception e) {
logger.error(
"An error occurred while adding a document to indexwriter. File: " + file.getAbsolutePath(), e);
}
}
}
/**
* 将文件转为lucene的{@link Document}类型<br/>
* 其中包括:
* <ul>
* <li>pathname:路径名</li>
* <li>filename:文件名</li>
* <li>size:文件大小(字节)</li>
* <li>type:文件类型</li>
* <li>content:文件内容(只有明文文件有,判断是否是明文文件:{@link FileUtil#isPlainTextFile(File)}
* )</li>
* </ul>
*
* @param file
* @return
*/
private Document file2Document(File file) {
Document document = new Document();
document.add(new StoredField("pathname", file.getAbsolutePath()));
document.add(new StringField("filename", file.getName(), Store.YES));
document.add(new StringField("type", FileUtil.getFileType(file), Store.YES));
document.add(new LongField("size", file.length(), Store.YES));
document.add(new LongField("lastmodified", file.lastModified(), Store.YES));
if (FileUtil.isPlainTextFile(file)) {// 对明文文件的内容建立索引
try {
Reader reader = new FileReader(file);
document.add(new TextField("content", reader));
} catch (Exception e) {
logger.error("An error occurred while indexing " + file.getAbsolutePath(), e);
}
}
return document;
}
}
使用到的两个工具类:FileUtil和StreamUtil。
FileUtil:
package cn.lym.lucene.quickstart.util;
import java.io.File;
/**
* 文件有关的工具类
*
* @author liuyimin
*
*/
public class FileUtil {
/**
* 获得文件类型
*
* @param file
* @return
*/
public static String getFileType(File file) {
String fileName = file.getName();
int index = fileName.lastIndexOf(".");
if (index != -1) {
return fileName.substring(index + 1);
}
return fileName;
}
/**
* 判断文件是否是明文的文件
*
* @param file
* @return
*/
public static boolean isPlainTextFile(File file) {
// 为了简化,这里只将txt文件作为明文文件
String fileType = getFileType(file);
return "txt".equals(fileType);
}
}
StreamUtil:
package cn.lym.lucene.quickstart.util;
import java.io.Closeable;
/**
* 流操作有关的工具类
*
* @author liuyimin
*
*/
public class StreamUtil {
/**
* 关闭流操作
*
* @param closeables
*/
public static void close(Closeable... closeables) {
if (closeables != null) {
for (Closeable closeable : closeables) {
if (closeable != null) {
try {
closeable.close();
} catch (Exception e) {
} finally {
closeable = null;
}
}
}
}
}
}
好了,写一个单元测试测试一下:
package cn.lym.lucene.quickstart.index;
import org.junit.Before;
import org.junit.Test;
public class IndexerTest {
private Indexer indexer;
@Before
public void init() {
this.indexer = new Indexer();
}
@Test
public void testIndex() throws Exception {
String indexDir = "E:\\Documents\\lucene-quickstart\\";
String dataDir = "D:\\";
this.indexer.index(indexDir, dataDir);
}
}
程序正常运行完成之后,在索引存放目录下,应该有如下的文件:
本文的代码可以从 https://git.oschina.net/coding4j/lucene-quickstart 获得。