搜索引擎Lucene技术
- Lucene 这个开源项目,使得 Java开发人员可以很方便地得到像搜索引擎google baidu那样的搜索效果。
1.1 Lucene入门
参照连接 https://blog.csdn.net/forfuture1978/article/details/4711308
1.1.1 数据添加到索引
例:
private static Directory createIndex(IKAnalyzer analyzer, List<Search> products) throws IOException
{
//创建内存索引
Directory index = new RAMDirectory();
//根据中文分词器创建配置对象
IndexWriterConfig config = new IndexWriterConfig(analyzer);
//创建索引 writer
IndexWriter writer = new IndexWriter(index, config);
//遍历数据,把他们挨个放进索引里
for (Search search : products) {
addDoc(writer, search.getIntroduce());
}
writer.close();
return index;
}
1.1.2 创建文档对象
文档(Document) : 获取原始内容的目的是为了索引,在索引前需要将原始内容创建成文档(Document),文档中包括一个一个的域(Field),域中存储内容。
private static void addDoc(IndexWriter w, String name) throws IOException
{
//创建文档对象
Document doc = new Document();
//将数据添加到文档中
doc.add(new TextField("name", name, Field.Store.YES));
w.addDocument(doc);
}
1.1.3 结果处理
private static void showSearchResults(IndexSearcher searcher, ScoreDoc[] hits, Query query, IKAnalyzer analyzer) throws Exception
{
System.out.println("找到 " + hits.length + " 个命中.");
System.out.println("序号\t匹配度得分\t结果");
//每一个ScoreDoc[] hits 就是一个搜索结果,首先把他遍历出来
for (int i = 0; i < hits.length; ++i) {
ScoreDoc scoreDoc = hits[i];
//然后获取当前结果的docid, 这个docid相当于就是这个数据在索引中的主键
int docId = scoreDoc.doc;
//再根据主键docid,通过搜索器从索引里把对应的Document取出来
Document d = searcher.doc(docId);
List<IndexableField> fields = d.getFields();
System.out.print((i + 1));
System.out.print("\t" + scoreDoc.score);
for (IndexableField f : fields) {
System.out.print("\t" + d.get(f.name()));
}
System.out.println();
}
}
1.1.4 分词组件(Tokenizer)
分词组件(Tokenizer)会做以下几件事情( 此过程称为Tokenize) :
- 将文档分成一个一个单独的单词。
- 去除标点符号。
- 去除停词(Stop word) 。
所谓停词(Stop word)就是一种语言中最普通的一些单词,由于没有特别的意义,因而大多数情况下不能成为搜索的关键词,因而创建索引时,这种词会被去掉而减少索引的大小。
英语中挺词(Stop word)如:“the”,“a”,“this”等。
对于每一种语言的分词组件(Tokenizer),都有一个停词(stop word)集合。
经过分词(Tokenizer) 后得到的结果称为词元(Token)
1.1. 5 测试
@Test
void testOne() throws Exception
{
//1,准备中文分词组件
IKAnalyzer analyzer = new IKAnalyzer();
//获取数据
List<Search> searches = searchService.queryAllByLimit(0, 2000);
//2,创建内存索引 并将数据添加到索引中
Directory index = createIndex(analyzer, searches);
// 查询关键字
String keyword = "护眼";
//3,查询器
Query query = new QueryParser("name", analyzer).parse(keyword);
//4,创建索引 reader
IndexReader reader = DirectoryReader.open(index);
//基于 reader 创建搜索器
IndexSearcher searcher = new IndexSearcher(reader);
//指定每页显示多少条数据
int numPerPage = 10;
System.out.println("当前一共" + searches.size() + "条数据");
System.out.println("查询关键字:" + keyword);
//执行搜索
ScoreDoc[] hits = searcher.search(query, numPerPage).scoreDocs;
//5,显示结果
showSearchResults(searcher, hits, query, analyzer);
//6,关闭查询
reader.close();
}
1.2 思路
- 首先搜集数据
数据可以是文件系统,数据库,网络上,手工输入的,或者像本例直接写在内存上的 - 通过数据创建索引
- 用户输入关键字
- 通过关键字创建查询器
- 根据查询器到索引里获取数据
- 然后把查询结果展示在用户面前
1.3 和like的区别
- 相关度
通过观察运行结果,可以看到不同相关度的结果都会查询出来,但是使用 like,就做不到这一点 - 性能
数据量小的时候,like 也会有很好的表现,但是数据量一大,like 的表现就差很多。
1.4 分词器概念
分词器指的是搜索引擎如何使用关键字进行匹配,
如:输入搜索的关键字:“护眼带光源”。 如果使用like,那么**“护眼带光源”,匹配出来的结果要么是完全匹配**,要么是完全不匹配
而使用分词器,会把这个关键字分为 “护眼”,“带”,“光源” 3个关键字,这样就可以找到不同相关程度的结果了
例:
@Test
void testTwo() throws Exception
{
IKAnalyzer analyzer = new IKAnalyzer();
TokenStream ts = analyzer.tokenStream("name", "护眼带光源");
ts.reset();
while (ts.incrementToken()) {
System.out.println(ts.reflectAsString(false));
}
/* 输出结果
加载扩展词典:ext.dic
加载扩展停止词典:stopword.dic
term=护眼,bytes=......
term=带,bytes=......
term=光源,bytes=......
*/
}
1.5 分页查询
- 假设要查询第10页,每页10条数据。
- Lucene 分页通常来讲有两种方式:
- 第一种是把100条数据查出来,然后取最后10条。 优点是快,缺点是对内存消耗大。
- 第二种是把第90条查询出来,然后基于这一条,通过searchAfter方法查询10条数据。 优点是内存消耗小,缺点是比第一种更慢
1.5.1 第一种方式
例:
/**
* 分页查询(方式一)
* @param query
* @param searcher
* @param pageNow 第几页
* @param pageSize 每页显示记录数
* @return
* @throws IOException
*/
private static ScoreDoc[] pageSearch1(Query query, IndexSearcher searcher, int pageNow, int pageSize)throws IOException
{
TopDocs topDocs = searcher.search(query, pageNow * pageSize);
System.out.println("查询到的总条数\t" + topDocs.totalHits);
ScoreDoc[] alllScores = topDocs.scoreDocs;
List<ScoreDoc> hitScores = new ArrayList<>();
int start = (pageNow - 1) * pageSize;
int end = pageSize * pageNow;
for (int i = start; i < end; i++) {
hitScores.add(alllScores[i]);
}
ScoreDoc[] hits = hitScores.toArray(new ScoreDoc[]{ });
return hits;
}
1.5.1 第二种方式
例:
/**
* 分页查询(方式二)
* @param query
* @param searcher
* @param pageNow 第几页
* @param pageSize 每页显示的记录数
* @return
* @throws IOException
*/
private static ScoreDoc[] pageSearch2(Query query, IndexSearcher searcher, int pageNow, int pageSize)throws IOException
{
//计算起始记录下标公式
int start = (pageNow - 1) * pageSize;
if (0 == start) {
TopDocs topDocs = searcher.search(query, pageNow * pageSize);
return topDocs.scoreDocs;
}
// 查询数据, 结束页面之前的数据都会查询到,但是只取本页的数据
TopDocs topDocs = searcher.search(query, start);
//获取到上一页最后一条
ScoreDoc preScore = topDocs.scoreDocs[start - 1];
//查询最后一条后的数据的一页数据
topDocs = searcher.searchAfter(preScore, query, pageSize);
return topDocs.scoreDocs;
}
Test测试
/**
* 测试分页查询
*/
@Test
void testThree() throws Exception
{
//1,准备中文分词器
IKAnalyzer analyzer = new IKAnalyzer();
//2,创建索引
List<Search> searches = searchService.queryAllByLimit(0, 2000);
Directory index = createIndex(analyzer, searches);
//3,查询器
String keyword = "护眼";
Query query = new QueryParser("name", analyzer).parse(keyword);
//4,创建索引 reader
IndexReader reader = DirectoryReader.open(index);
//基于 reader 创建搜索器
IndexSearcher searcher = new IndexSearcher(reader);
//指定每页显示多少条数据
int numPerPage = 10;
System.out.println("当前一共" + searches.size() + "条数据");
System.out.println("查询关键字:" + keyword);
// 分页查询方式一
//ScoreDoc[] hits = pageSearch1(query, searcher, 2, 5);
//分页查询方式二
ScoreDoc[] hits = pageSearch2(query, searcher, 3, 5);
//5,显示结果
showSearchResults(searcher, hits, query, analyzer);
//6,关闭查询
reader.close();
}
1.6 索引删除和更新
-
索引里的数据,其实就是一个一个的Document 对象
删除索引API:
DeleteDocuments(Query query) //根据Query条件来删除单个或多个Document
DeleteDocuments(Query[] queries) //根据Query条件来删除单个或多个Document
DeleteDocuments(Term term) //根据Term来删除单个或多个Document
DeleteDocuments(Term[] terms) //根据Term来删除单个或多个Document
DeleteAll() //删除所有的Document
测试:
@Test
void testFour() throws Exception
{
//1,准备中文分词器
IKAnalyzer analyzer = new IKAnalyzer();
//2,创建索引
List<Search> searches = searchService.queryAll();
Directory index = createIndex(analyzer, searches);
//3,删除索引
IndexWriterConfig config = new IndexWriterConfig(analyzer);
IndexWriter indexWriter = new IndexWriter(index, config);
/*
根据Term来删除单个或多个Document
删除id=5205的索引
*/
indexWriter.deleteDocuments(new Term("id", "5205"));
indexWriter.commit();
indexWriter.close();
//删除之后再次查询
String keyword = "光";
// 4,查询
Query query = new QueryParser("name", analyzer).parse(keyword);
//5,创建索引 reader
IndexReader reader = DirectoryReader.open(index);
//基于 reader 创建搜索器
IndexSearcher searcher = new IndexSearcher(reader);
//指定每页显示多少条数据
int numPerPage = 20;
System.out.println("当前一共" + searches.size() + "条数据");
System.out.println("查询关键字:" + keyword);
//执行搜索
ScoreDoc[] hits = searcher.search(query, numPerPage).scoreDocs;
//6,显示结果
showSearchResults(searcher, hits, query, analyzer);
//7,关闭查询
reader.close();
}