Lucene 3.5.0学习笔记
1. 整个全文搜索引擎就是建立索引,查找索引的一个过程。
2. 几个重要的概念:
Directory:目录(分为RAMdirectory 和 FSDdirectory ,用于存放文件和索引的目录)
Analyzer:分词器,一般用标准分词器(standardAnalyzer就可以啦)
IndexWriter:写索引
IndexReader:读索引
IndexSearcher:搜索索引
Document:相当于数据库里的一条记录
Field:相当于数据库里的一条记录的字段
3. 建立索引:
public void createIndex(){
IndexWriter writer = null;
try {
//1.创建一个索引目录
//Directory dir = new RAMDirectory(); //建立在内存中
Directory dir = FSDirectory.open(new File("D:\\Documents\\Desktop\\新建文件夹\\myIndex"));//建立在硬盘上
//2.创建一个indexWriter
IndexWriterConfig iwc = new IndexWriterConfig(Version.LUCENE_35, new StandardAnalyzer(Version.LUCENE_35));
writer = new IndexWriter(dir, iwc);
//3.创建一个document
Document document ;
for(File file : new File("D:\\Documents\\Desktop\\新建文件夹\\myFile").listFiles()){
document = new Document();
document.add(new Field("content",new FileReader(file)));
document.add(new Field("path",file.getAbsolutePath(),Field.Store.YES,Field.Index.NOT_ANALYZED));
document.add(new Field("name",file.getName(),Field.Store.YES,Field.Index.NOT_ANALYZED));
//4.利用indexWriter将索引写入到目录中
writer.addDocument(document);
}
} catch (Exception e) {
e.printStackTrace();
} finally{
if(writer != null){
try {
writer.close();
} catch (CorruptIndexException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
4. 查找索引:
public void searchIndex(){
Directory dir;
try {
//1.创建一个目录
dir = FSDirectory.open(new File("D:\\Documents\\Desktop\\新建文件夹\\myIndex"));//建立在硬盘上
//2.创建一个indexReader
IndexReader reader = IndexReader.open(dir);
//3.创建一个indexSearcher
IndexSearcher searcher = new IndexSearcher(reader);
//4.创建一个queryParser(第二个参数为要搜索的域,即条件)
QueryParser parser = new QueryParser(Version.LUCENE_35, "content", new StandardAnalyzer(Version.LUCENE_35));
//5.创建一个query(参数为要搜索的指,即content=“中华”)
Query query = parser.parse("JAVA");
//6.根据searcher搜索并返回一个TopDocs
TopDocs docs = searcher.search(query, 10); //搜索前10条记录
//7.根据TopDocs获取一个ScoreDoc数组对象
ScoreDoc[] sds = docs.scoreDocs;
Document document ;
for(ScoreDoc sd : sds){
//8.创建一个document
document = searcher.doc(sd.doc); //根据编号为sd.doc查找
System.out.println("path:"+document.get("path")+"-------- name:"+document.get("name"));
}
//9.关闭流
reader.close();
} catch (Exception e) {
e.printStackTrace();
}
}
5. 关于域Field的几个选项说明:
Field.Store.YES或者NO(存储域选项)
设置为YES表示或把这个域中的内容完全存储到文件中,方便进行文本的还原
设置为NO表示把这个域的内容不存储到文件中,但是可以被索引,此时内容无法完全 还原(doc.get是为null)
Field.Index(索引选项)
Index.ANALYZED:进行分词和索引,适用于标题、内容等
Index.NOT_ANALYZED:进行索引,但是不进行分词,如果身份证号,姓名,ID等,适 用于精确搜索
Index.ANALYZED_NOT_NORMS:进行分词但是不存储norms信息,这个norms中包括 了创建索引的时间和权值等信息
Index.NOT_ANALYZED_NOT_NORMS:即不进行分词也不存储norms信息
Index.NO:不进行索引
最佳实践:
Field.Index
Field.Store
NOT_ANALYZED_NO_NORMS
YES
标识符(主键,文件名),电话号码,身份证号,姓名,日期
ANALYZED
YES
文档摘要和标题
ANALYZED
NO
文档正文
NO
YES
文档类型,数据库主键(不进行索引)
NOT_ANALYZED
NO
隐藏关键字
6. 索引的删除、更新、优化
1.索引的删除
//使用indexWriter删除
2.索引的恢复删除:
3.强制删除:
4.更新(先删除后添加):
7. 当项目中始终只用到一个indexReader和一个indexWriter的时候,也就是把这两个设计 为单例的时候,就不能随手关闭indexReader和indexWriter,但是多线程同时执行搜索 索引的时候就会出现异常信息,这时候lucene提供了解决方案:
即调用IndexReader的openIfChanged()方法。当然实际项目经常将reader和writer新建 后立即存放到application里保存就可以了。
8. 对数字或日期(当作数字处理)添加索引的时候就不能再简单的使用:
doc.add(new Field("name",names[i],Field.Store.YES,Field.Index.NOT_ANALYZED_NO_NORMS))
了(第二个参数类型是String,不是int),而使用
//存储数字
doc.add(new NumericField("attach",Field.Store.YES,true).setIntValue(attachs[i]));
//存储日期
doc.add(new NumericField("date",Field.Store.YES,true).setLongValue(dates[i].getTime()));
9. 分页:lucene的分页非常撮,没有数据库如hibernate里的limited方法,它是每次加载 所有的数据,然后取出你要显示的那页数据。至此,lucene官网解释是因为我们的检索 速度快得惊人,让你察觉不到,而且这种方法还是比数据库的要快。而到了3.5.0版本, lucene的indexsearcher开始加入了一个叫做searchAfter()的方法,它是从上页的最后一 个元素开始检索的,希望在以后的版本中能出来类似hibernate的limited()方法。下面给 出一个效率稍微高一点的分页方法:
10. 分词器:
1.分词过程:
2. Lucene3.5.0中Tokenizer的类图结构:
以 how are you ,I'm a student为例,whilespaceTokenizer会分词为[how][are][you,I'm][a][student],而LetterTokenizer会分词为[how][are][you][I][am][a][student]。
3. Lucene3.5.0中TokenFilter的类图结构:
4. Lucene3.5.0在对一个readerStream分词时需记录的信息(这里最关键了!!!):
5. 用户自扩展分词器,只需集成Analyzer类,重写里面的方法,为它设定过滤器链 即可。如:
6. 中文分词器:庖丁解牛分词器(不再更新词库)、mmseg4j(使用sugou词库,支 持 用户自定义词库)、IK Analyzer。但是,他们都不支持同义词搜索。
7. 扩展写一个自己的支持同义词搜索的中文分词器是很简单的。首先,利用mmseg4j 进行分词,就可以得到正常的分词tokenStream,然后遍历这个tokenStream,判断 当前的token是否有同义词(当然,有一个map集合来存储同义词),这样就支持 同义词的中文搜索了。
11. 高级搜索
1. 搜索排序:
2. 搜索过滤
3. 自定义评分
A.创建一个Query
B.创建一个FieldScoreQuery
C.写一个自定义类继承CustomScoreQuery,实现里面的构造方法和getCustomScoreProvider(),在getCustomScoreProvider()返回一个自定义的CustomScoreProvider对象,见D
D.写一个自定义类继承CustomScoreProvider,实现里面的构造方法和customScore(),在 customScore()编写自己的评分法则
E.利用A,B中的Query和FieldScoreQuery对象创建自定义的CustomScoreQuery对象,用这个query对象来查询,即传给indexSearcher
F.代码实例:
/**
* 自定义评分
* @author Administrator
*
*/
public class MyScoreQuery {
public void searchByMyScoreQuery(){
try {
IndexReader reader = IndexReader.open(FSDirectory.open(new File("E:\\Project\\MyEclipse9.1\\LuceneLearning\\index")));
IndexSearcher searcher = new IndexSearcher(reader);
Query query = new TermQuery(new Term("content", "like"));
//创建一个评分域
FieldScoreQuery fsq = new FieldScoreQuery("score", Type.INT);
//根据评分域和原有的query创建自定义的query对象
MyCustomScoreQuery myCustomScoreQuery = new MyCustomScoreQuery(query, fsq);
TopDocs docs = searcher.search(myCustomScoreQuery, 10);
System.out.println("一共找到的文件数:"+docs.totalHits);
ScoreDoc[] sds = docs.scoreDocs;
Document doc = null;
for(ScoreDoc sd : sds){
doc = searcher.doc(sd.doc);
System.out.println("ID:"+doc.get("id")+"--"+(Integer.parseInt(doc.get("score")))/(sd.score)+"--NAME:"+doc.get("name")+"--EMAIL:"+doc.get("email")+"--ATTACH:"+doc.get("attach"));
}
searcher.close();
} catch (CorruptIndexException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 写一个类继承CustomScoreQuery
* 实现里面的构造方法和getCustomScoreProvider
* @author Administrator
*
*/
private class MyCustomScoreQuery extends CustomScoreQuery{
/**
*
*/
private static final long serialVersionUID = -479883456177308726L;
public MyCustomScoreQuery(Query subQuery, ValueSourceQuery valSrcQuery) {
super(subQuery, valSrcQuery);
}
/**
* 自己获取一个评分
*/
@Override
protected CustomScoreProvider getCustomScoreProvider(IndexReader reader)
throws IOException {
//默认情况下的评分法则是subQuery*valSrcQuery即(原有的评分)*(传入进来的评分域所获取的评分)
//为了写自己的评分法则需要写一个自己的CustomScoreProvider,即下面的MyCustomScoreProvider类
//return super.getCustomScoreProvider(reader);
return new MyCustomScoreProvider(reader);
}
}
/**
* 写一个类继承CustomScoreProvider
* 实现里面的构造方法和customScore
* @author Administrator
*
*/
private class MyCustomScoreProvider extends CustomScoreProvider{
public MyCustomScoreProvider(IndexReader reader) {
super(reader);
}
@Override
public float customScore(int doc, float subQueryScore, float valSrcScore)
throws IOException {
//return super.customScore(doc, subQueryScore, valSrcScore);
//假设我们的需求是这样的评分
return valSrcScore/subQueryScore;
}
}
}
4. 自定义QueryParser:由于某些QueryParser(如:WildCardQuery、PrefixQuery、 FuzzyQuery)的性能低下,所以考虑将其取消。实现思路:
A. 继承QueryParser类,并且重载里面的相应方法,如getWilfCardQuery()
5. 工具介绍:
1. luke(可以解析lucene索引文件,注意版本必须和你的lucene严格一致, 否则打不开)
2. Tika:用于打开各种类型的文件的apache软件,由于我们可能要对一个二进制 文件而不是文本进行索引(比如word),所以就不能直接打开,这时候可以借助tika 提取出里面的文本再索引。使用tika 的方式很简单,有两种:
/**
* 第一种使用Tika读取文件的方式
* @param fileName
* @return
* @throws TikaException
* @throws SAXException
* @throws IOException
*/
public String fileToTxt(String fileName) throws IOException, SAXException, TikaException{
Parser parser = new AutoDetectParser(); //当不知道文件类型时,就创建一个AutoDetectParser,它会自动匹配
InputStream is = null;
is = new FileInputStream(new File(fileName));
ContentHandler handler = new BodyContentHandler();
Metadata metadata = new Metadata();
ParseContext context = new ParseContext();
context.set(Parser.class, parser);
parser.parse(is, handler, metadata, context);
metadata.set(Metadata.AUTHOR, "王欢");
for(String name:metadata.names()){
System.out.println(name+":"+metadata.get(name));
}
return handler.toString();
}
/**
* 第二种使用Tika读取文件的方式
* @param fileName
* @return
* @throws TikaException
* @throws IOException
*/
public String fileToTxt2(String fileName) throws IOException, TikaException{
return new Tika().parseToString(new File(fileName));
}
而建立文件的索引的时候就应该这样来读了:
doc.add(new Field("content", new Tika().parse(new File(fileName))));
如:
/**
* 利用Tika读取任何类型的文件并生成索引
* @param fileName
*/
public void index(String fileName){
IndexWriter writer = null;
Directory dir;
try {
dir = FSDirectory.open(new File("D:\\Documents\\Desktop\\新建文件夹\\myIndex"));
writer = new IndexWriter(dir, new IndexWriterConfig(Version.LUCENE_35, new MMSegAnalyzer()));//使用中文分词器
writer.deleteAll(); //先删除所有的索引
Document doc = new Document();
doc.add(new Field("content", new Tika().parse(new File(fileName)))); //使用Tika去除掉无用的东西
writer.addDocument(doc);
} catch (Exception e) {
e.printStackTrace();
}finally{
if(writer != null)
try {
writer.close();
} catch (CorruptIndexException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
12. Solr:全文搜索服务器(用于管理索引,版本和lucene要严格一致!)
1. Solr与tomcat整合
A. 建立一个文件夹目录solr->home和solr->server,将下载的apache-solr-3.5.0.zip里的example下的solr文件夹里的内容复制到home里,将webapps下的solr.war解压到server里;
B. 修改solr\home\conf下的solrConfig.xml文件:
<dataDir>${solr.data.dir:G:\study\lucene\solr\home\data}</dataDir>
<queryResponseWriter name="velocity" class="solr.VelocityResponseWriter" enable="${solr.velocity.enabled:false}"/>
C.配置Tomcat 6.0\conf下的server.xml文件,在<HOST>之间增加:
<!--solr-->
<Context path="/solr" docBase="G:\study\lucene\solr\server" reloadable="false">
<Environment name="solr/home" type="java.lang.String" value="G:\study\lucene\solr\home" override="true">
</Environment>
</Context>
D.使solr支持中文分词:
1.导包:将下载的mmseg4j-1.8.5.zip里的mmseg4j-all-1.8.5.jar和mmseg4j-solr-1.8.5.jar拷贝到 solr\server\WEB-INF\lib里;
2.修改solr\home\conf下的schema.xml文件,在<types>之间增加:(这段代码在mmseg4j-1.8.5.zip\readme.txt里)
<!--MMseg Analyzer-->
<fieldType name="textComplex" class="solr.TextField" >
<analyzer>
<tokenizer class="com.chenlb.mmseg4j.solr.MMSegTokenizerFactory" mode="complex" dicPath="dic"/>
</analyzer>
</fieldType>
<fieldType name="textMaxWord" class="solr.TextField" >
<analyzer>
<tokenizer class="com.chenlb.mmseg4j.solr.MMSegTokenizerFactory" mode="max-word" dicPath="dic"/>
</analyzer>
</fieldType>
<fieldType name="textSimple" class="solr.TextField" >
<analyzer>
<tokenizer class="com.chenlb.mmseg4j.solr.MMSegTokenizerFactory" mode="simple" dicPath="n:/OpenSource/apache-solr-1.3.0/example/solr/my_dic"/>
</analyzer>
</fieldType>
3. 拷贝mmseg4j-1.8.5.zip\data下的几个txt文件到solr\home\dic下
2. Solr与Java整合
A. 导包(位于apache-solr-3.5.0\dist\solrj-lib和apache-solr-3.5.0\dist下的):
B. 所有自定义的field都在schema.xml文件里定义,如:
<field name="test_all" type="textComplex" indexed="true" stored="true" multiValued="true"/>
<field name="test_title" type="textComplex" indexed="true" stored="true"/>
<field name="test_content" type="textComplex" indexed="true" stored="true"/>
C.为了加快搜索可定义
<copyField source="test_title" dest="test_all"/>
<copyField source="test_content" dest="test_all"/>
D.需注意的几点:
<uniqueKey>id</uniqueKey> (所以每个doc必须要赋予id,id是主键)
<defaultSearchField>text</defaultSearchField>(默认搜索域是text,可以自己更改)
<solrQueryParser defaultOperator="OR"/>(默认操作符是OR ,也可以自己设定)
E.示范代码:
public class TestSolrUtils {
private static String url = "http://localhost:8080/solr";
/**
* 第一种方式添加document
*/
@Test
public void test01(){
try {
CommonsHttpSolrServer server = new CommonsHttpSolrServer(url);
server.deleteByQuery("*:*");
server.commit();
SolrInputDocument doc = new SolrInputDocument();
doc.addField("id", "1");
doc.addField("test_title", "这是我的第一个solr小程序");
doc.addField("test_content", "有了solr就是爽啊,哈哈哈~!!!");
server.add(doc);
server.commit();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (SolrServerException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 第二种方式添加document
*/
@Test
public void test02(){
CommonsHttpSolrServer server;
try {
server = new CommonsHttpSolrServer(url);
server.deleteByQuery("*:*");
server.commit();
List<Message> msgs = new ArrayList<Message>();
msgs.add(new Message("1", "第一个javabean", "用javabean的方式添加第一个solr的document!"));
msgs.add(new Message("2", "第二个javabean", "用javabean的方式添加第二个solr的document!"));
msgs.add(new Message("3", "第三个javabean", "用javabean的方式添加第三个solr的document!"));
server.addBeans(msgs);
server.commit();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (SolrServerException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 使用solr查询索引
*/
@Test
public void test03(){
CommonsHttpSolrServer server;
try {
server = new CommonsHttpSolrServer(url);
SolrQuery query = new SolrQuery();
query.setQuery("test_title:javabean");
/**solr的分页*/
query.setStart(0);
query.setRows(2);
QueryResponse response = server.query(query);
SolrDocumentList sdl = response.getResults();
for(SolrDocument sd : sdl){
System.out.println("id:"+ sd.getFieldValue("id"));
System.out.println("test_title:"+sd.getFieldValue("test_title"));
System.out.println("test_content:"+sd.getFieldValue("test_content"));
System.out.println("***********************************************");
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (SolrServerException e) {
e.printStackTrace();
}
}
/**
* solr高亮
*/
@Test
public void test04(){
CommonsHttpSolrServer server ;
try {
server = new CommonsHttpSolrServer(url);
SolrQuery query = new SolrQuery();
query.setQuery("test_content:javabean");
query.setHighlight(true).setHighlightSimplePre("<span style='color:red'>")
.setHighlightSimplePost("</span>").setStart(1).setRows(3);
query.setParam("hl.fl", "test_title,test_content");
QueryResponse response = server.query(query);
SolrDocumentList sdl= response.getResults();
for(SolrDocument sd: sdl){
String id = (String)sd.getFieldValue("id");
//采用solr要高亮的部分必须先存储,否则无法高亮,只能采取再查询的方式
System.out.println(response.getHighlighting().get(id).get("test_title"));
System.out.println(response.getHighlighting().get(id).get("test_content"));
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (SolrServerException e) {
e.printStackTrace();
}
}
}