/**
* lucene实现近实时搜索:
* lucene通过NRTManager这个类来实现近实时搜索。所谓近实时搜索即在索引发生改变时,通过线程跟踪,在相对很短的时间反映
* 给用户程序的调用。
* NRTManager通过管理IndexWriter对象,并将IndexWriter的一些方法(增删改)例如addDocument,deleteDocument等方法暴露给
* 客户调用。它的操作全部在内存里面,所以如果你不调用IndexWriter的commit方法,通过以上的操作,用户硬盘里面的索引库是不会
* 变化的,所以你每次更新完索引库请记得commit()掉(注意:一般是开发人员自己控制多长时间提交一次),这样才能将变化的索引
* 一起写到硬盘中,实现索引更新后的同步。
* 用户每次获取最新索引(IndexSearcher),可以通过两种方式:
* 第一种是通过调用NRTManagerReopenThread对象,该线程负责实时跟踪索引内存的变化,每次变化就“自动”调用 maybeReopen()方法,
* 保持最新代索引,打开一个新的IndexSearcher对象,而用户所要的IndexSearcher对象是NRTManager通过调用getSearcherManager()
* 方法获得SearcherManager对象,然后通过SearcherManager对象获取IndexSearcher对象返回给客户使用,用户使用完之 后调用
* SearcherManager的release释放IndexSearcher对象,最后记得关闭NRTManagerReopenThread;
* 第二种方式是不通过NRTManagerReopenThread对象,而是“手动”调用NRTManager的maybeReopen()方法来获取最新的IndexSearcher
* 对象来获取最新索引
*
* NRTManager使用基本流程:
* 1.首先创建并初始化NRTManager对象
* 2.创建并初始化NRTManagerReopenThread线程(用该线程周期性地查看索引是否发生改变及是否需要重新打开索引)
* 3.通过NRTManager的getSearcherManager()方法获取SearcherManager
* 4.通过NRTManager对索引进行增、删、改、查操作
* 5.通过dSearcherManager的acquire()方法获取InexSearcher对象,执行查询操作。查询完毕后,调用SearcherManager的
* release()释放IndexSearcher对象
*/
public class NRTSearch {
private String[] ids = {"1","2","3","4","5","6"};
private String[] emails = {"aa@aa.com","bb@bb.com","cc@aa.com","dd@dd.com","ee@ee.com","ff@aa.com"};
private String[] content = {"My name is aa","My name is bb, aa is my brother's name ","My name is cc","My name is dd"
,"My name is ee","My name is ff, aa is my sister's name"};
private int[] attaches = {1,2,3,4,5,6};
private String[] names = {"aa","bb","cc","dd","ee","ff"};
private Date[] dates = null;
private Directory directory = null;
/*----------------------------------------近实时搜索------------------------------------------*/
/**
* IndexWriter设置成单例
*/
private static IndexWriter indexWriter = null;
//线程安全
private NRTManager nrtManager = null;
private SearcherManager searcherManager = null;
/*----------------------------------------近实时搜索------------------------------------------*/

public NRTSearch(){
initDates();
try {
/**
* 创建索引目录
* FSDirectory.open():根据当前环境,选择最好的打开方式
*/
directory = FSDirectory.open(new File("e:/lucene/index02"));
/*----------------------------------------近实时搜索------------------------------------------*/
indexWriter = new IndexWriter(directory, new IndexWriterConfig(Version.LUCENE_35,
new StandardAnalyzer(Version.LUCENE_35)));
/**
* 初始化NRTManager对象:
* 参数一:IndexWriter
* 参数二:ExecutorService 线程池
* 参数三:SearcherWarmer(一旦重新打开索引,就会调用这个该接口)
* 备注:NRTManager对象封装了IndexWriter对象的大部分方法(增、删、改、查),若要实现近实时搜索,必须调用封装
* 好的方法,不能再通过IndexWriter来调用相关方法
*/
nrtManager = new NRTManager(indexWriter, Executors.newCachedThreadPool(), new SearcherWarmer() {
/**
* 索引一更新就要重新获取searcher,那获取searcher的时候就会调用maybeReopen()方法
* 执行maybeReopen()的时候会执行warm方法,在这里可以对资源等进行控制
* 备注:参见前面说明,有两种方式来调用maybeReopen()
*/
@Override
public void warm(IndexSearcher s) throws IOException {
System.out.println("重新打开索引");
}
});

/**
* 以下是自动获取最新索引
*/
/**
* 启动NRTManager的Reopen线程,NRTManagerReopenThread会每隔25秒去检测一下索引是否更新并判断是否需要重新
* 打开writer(0.025代表25秒)
* NRTManagerReopenThread(NRTManager manager, double targetMaxStaleSec, double targetMinStaleSec)参数解析
* 参数一:NRTManager对象
* 参数二:double targetMaxStaleSec 检测索引是否更新并判断是否需要重新打开writer的最大时间
* 参数三:double targetMinStaleSec 最小时间
*/
NRTManagerReopenThread reopenThread = new NRTManagerReopenThread(nrtManager, 5.0, 0.025);

//设置为守护线程(优先级较低,在没有用户线程可服务时会自动离开)
reopenThread.setDaemon(true);
reopenThread.setName("NrtManager Reopen Thread");
//启动该线程
reopenThread.start();

/**
* 参数:boolean applyAllDeletes
* 为true,若有删除操作,则在下一次重新打开索引之后,会查询不出来已经被删除的索引;
* 为false,被删除的索引依然存在
*/
searcherManager = nrtManager.getSearcherManager(true);
} catch (IOException e) {
e.printStackTrace();
}
/*----------------------------------------近实时搜索------------------------------------------*/
}

private void initDates() {
dates = new Date[ids.length];
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-mm-dd");
try {
dates[0] = sdf.parse("2018-01-01");
dates[1] = sdf.parse("2017-01-01");
dates[2] = sdf.parse("2016-01-01");
dates[3] = sdf.parse("2015-01-01");
dates[4] = sdf.parse("2014-01-01");
dates[5] = sdf.parse("2013-01-01");
} catch (ParseException e) {
e.printStackTrace();
}
}

public void index(){
try {
//每次创建新的索引之前,先将以前的索引删除掉
indexWriter.deleteAll();
//创建Document,并添加域
Document document = null;
for (int i=0;i<ids.length;i++){
document = new Document();
//存储在硬盘,但不分词,不加权
document.add(new Field("id",ids[i],Field.Store.YES,Field.Index.NOT_ANALYZED_NO_NORMS));
//存储在硬盘,但不分词,但加权
document.add(new Field("email",emails[i],Field.Store.YES,Field.Index.NOT_ANALYZED));
//不存储在硬盘,但分词、加权
document.add(new Field("content",content[i],Field.Store.NO,Field.Index.ANALYZED));
//存储在硬盘,但不分词,不加权
document.add(new Field("name",names[i],Field.Store.YES,Field.Index.NOT_ANALYZED_NO_NORMS));

/**
* 为数字和日期建立索引:
* NumericField(String name, int precisionStep, Store store, boolean index)
* 为这两项建立索引有专门的方法,NumericField(),其中第四个参数不再是索引的方式,而是是否进行索引
*/
//为数字建立索引
NumericField numericField = new NumericField("attach", Field.Store.YES, true);
numericField.setIntValue(attaches[i]);
document.add(numericField);
/**
* 为日期建立索引:
* 核心思想,将Date对象转换成数字
*/
long time = dates[i].getTime();//将日期转换成long型
//创建NumericField对象
NumericField dateNumericField = new NumericField("date", Field.Store.YES, true);
//设置索引值
dateNumericField.setLongValue(time);
document.add(dateNumericField);

/*----------------------------------------近实时搜索------------------------------------------*/
//添加索引文档(这里不再用IndexWriter,而是用NRTManager)
nrtManager.addDocument(document);
/*----------------------------------------近实时搜索------------------------------------------*/
}
} catch (IOException e) {
e.printStackTrace();
}
}

/**
* 搜索
*/
public void search(){

/*----------------------------------------近实时搜索------------------------------------------*/
//创建IndexSearcher对象(不是用IndexReader创建IndexSearcher,而是用SearcherManager创建IndexSearcher)
IndexSearcher indexSearcher = searcherManager.acquire();
/*----------------------------------------近实时搜索------------------------------------------*/

//创建TermQuery(精确搜索对象)
TermQuery termQuery = new TermQuery(new Term("content", "name"));
//搜索
try {
TopDocs topDocs = indexSearcher.search(termQuery, 10);
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
Calendar calendar = Calendar.getInstance();
for (ScoreDoc scoreDoc:scoreDocs){
Document document = indexSearcher.doc(scoreDoc.doc);
//将数字转换成日期(因为前面是将日期转换成数字进行索引的)
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-mm-dd");
String time = document.get("date");
calendar.setTimeInMillis(Long.valueOf(time));
String date = sdf.format(calendar.getTime());

System.out.println("("+scoreDoc.doc+")"+document.get("name")+"["+document.get("email")+"]-->"+document.get("id")
+ "," +document.get("attach") + "," +date);
}
} catch (IOException e) {
e.printStackTrace();
}finally {

/*----------------------------------------近实时搜索------------------------------------------*/
try {
//不再是关闭IndexSearcher,而是调用searcherManager.release()释放掉IndexSearcher
searcherManager.release(indexSearcher);
} catch (IOException e) {
e.printStackTrace();
}
/*----------------------------------------近实时搜索------------------------------------------*/
}
}

/**
* 删除索引
*/
public void deleteIndex(){
try {

/*----------------------------------------近实时搜索------------------------------------------*/
nrtManager.deleteDocuments(new Term("id", "1"));
/*----------------------------------------近实时搜索------------------------------------------*/

} catch (IOException e) {
e.printStackTrace();
}
}

/**
* 更新
*/
public void updateIndex(){
try {
/**
*
*/
Document document = new Document();
document.add(new Field("id","8",Field.Store.YES,Field.Index.NOT_ANALYZED_NO_NORMS));
//存储在硬盘,但不分词,但加权
document.add(new Field("email",emails[0],Field.Store.YES,Field.Index.NOT_ANALYZED));
//不存储在硬盘,但分词、加权
document.add(new Field("content",content[0],Field.Store.NO,Field.Index.ANALYZED));
//存储在硬盘,但不分词,不加权
document.add(new Field("name",names[0],Field.Store.YES,Field.Index.NOT_ANALYZED_NO_NORMS));

/*----------------------------------------近实时搜索------------------------------------------*/
nrtManager.updateDocument(new Term("id", "2"), document);
/*----------------------------------------近实时搜索------------------------------------------*/

} catch (IOException e) {
e.printStackTrace();
}
}

/**
* 提交内存中变动的索引到硬盘:
* NRTManager中所有的添加修改删除的操作只在内存中生效,然后使用内存中的索引信息在搜索时能起到效果。
* 但是过一段时间累计到一定程序需要进行writer.commit(),将索引更新到硬盘中,这样下一次重新搜索才是最新的索引。
* NRTManage就是这样的功能,把更新的数据存储在内存中,但是lucene搜索的时候也可以搜索到,需要
* writer进行commit才会把索引更新到硬盘中
*/
public void commit(){
try {
indexWriter.commit();
} catch (IOException e) {
e.printStackTrace();
}
}
}