Lucene
是 Apache Jakarta
家族中的一个开源项目,它不是一个完整的搜索应用程序,但可为我们的应用程序提供索引和搜索功能。Lucene
也是目前流行的基于 Java
的开源全文检索工具包。
目前已有很多应用程序基于 Lucene
实现了搜索功能,比如 Eclipse 帮助系统的搜索功能。Lucene
能为文本类型的数据建立索引,我们只要能把需要建立索引的数据转化为文本格式,Lucene
就能对该文档建立索引并实现搜索。
所需要的pom
依赖
<dependency>
<groupId>com.github.magesegroupId>
<artifactId>ik-analyzerartifactId>
<version>8.5.0version>
dependency>
<dependency>
<groupId>org.apache.lucenegroupId>
<artifactId>lucene-analyzers-commonartifactId>
<version>8.9.0version>
dependency>
<dependency>
<groupId>org.apache.lucenegroupId>
<artifactId>lucene-coreartifactId>
<version>9.0.0version>
dependency>
<dependency>
<groupId>org.apache.lucenegroupId>
<artifactId>lucene-highlighterartifactId>
<version>9.0.0version>
dependency>
<dependency>
<groupId>org.apache.lucenegroupId>
<artifactId>lucene-memoryartifactId>
<version>9.0.0version>
dependency>
<dependency>
<groupId>org.apache.lucenegroupId>
<artifactId>lucene-queryparserartifactId>
<version>9.0.0version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.78version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.24version>
dependency>
<dependency>
<groupId>commons-iogroupId>
<artifactId>commons-ioartifactId>
<version>2.8.0version>
dependency>
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.*;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.wltea.analyzer.lucene.IKAnalyzer;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class TestLucene {
public static void main(String[] args) throws Exception{
// 1. 准备中文分词器
IKAnalyzer analyzer = new IKAnalyzer();
// 2. 索引
List<String> productNames = new ArrayList<>();
productNames.add("飞利浦led灯泡e27螺口暖白球泡灯家用照明超亮节能灯泡转色温灯泡");
productNames.add("飞利浦led灯泡e14螺口蜡烛灯泡3W尖泡拉尾节能灯泡暖黄光源Lamp");
productNames.add("雷士照明 LED灯泡 e27大螺口节能灯3W球泡灯 Lamp led节能灯泡");
productNames.add("飞利浦 led灯泡 e27螺口家用3w暖白球泡灯节能灯5W灯泡LED单灯7w");
productNames.add("飞利浦led小球泡e14螺口4.5w透明款led节能灯泡照明光源lamp单灯");
productNames.add("飞利浦蒲公英护眼台灯工作学习阅读节能灯具30508带光源");
productNames.add("欧普照明led灯泡蜡烛节能灯泡e14螺口球泡灯超亮照明单灯光源");
productNames.add("欧普照明led灯泡节能灯泡超亮光源e14e27螺旋螺口小球泡暖黄家用");
productNames.add("聚欧普照明led灯泡节能灯泡e27螺口球泡家用led照明单灯超亮光源");
Directory index = createIndex(analyzer, productNames);
// 3. 查询器
String keyword = "护眼带光源";
Query query = new QueryParser("name", analyzer).parse(keyword);
// 4. 搜索
IndexReader reader = DirectoryReader.open(index);
IndexSearcher searcher = new IndexSearcher(reader);
int numberPerPage = 1000;
System.out.printf("当前一共有%d条数据%n",productNames.size());
System.out.printf("查询关键字是:\"%s\"%n",keyword);
ScoreDoc[] hits = searcher.search(query, numberPerPage).scoreDocs;
// 5. 显示查询结果
showSearchResults(searcher, hits);
// 6. 关闭查询
reader.close();
}
private static void showSearchResults(IndexSearcher searcher, ScoreDoc[] hits)
throws Exception {
System.out.println("找到 " + hits.length + " 个命中.");
System.out.println("序号\t匹配度得分\t结果");
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();
}
}
private static Directory createIndex(IKAnalyzer analyzer, List<String> products) throws IOException {
Directory index = new ByteBuffersDirectory();
IndexWriterConfig config = new IndexWriterConfig(analyzer);
IndexWriter writer = new IndexWriter(index, config);
for (String name : products) {
addDoc(writer, name);
}
writer.close();
return index;
}
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);
}
}
分词器
指的是搜索引擎如何使用关键字进行匹配,如 入门 中的关键字:护眼带光源。 如果使用 like
,那么 %护眼带光源%
,匹配出来的结果就是要么全匹配,要不都不匹配。而使用分词器,就会把这个关键字分为 护眼,带,光源 3个关键字,这样就可以找到不同相关程度的结果了
如果想要增加自定义的分词词语,可以在 resources
目录下增加两个文件:
ext.dic
:扩展匹配的词,比如上面的护眼带光源,可以拆分为:护眼,带,光源 3个关键字,假如添加了 护眼带,就可以拆分为:护眼带,护眼,带,光源 4个关键字stopword.dic
:阻止可以扩展的词,比如上面的 护眼带光源,本来可以拆分为:护眼,带,光源 3个关键字,假如添加了 光源,就只能拆分为:护眼,带 2个关键字import org.apache.lucene.analysis.TokenStream;
import org.wltea.analyzer.lucene.IKAnalyzer;
import java.io.IOException;
public class TestAnalyzer {
public static void main(String[] args) throws IOException {
IKAnalyzer analyzer = new IKAnalyzer();
TokenStream ts = analyzer.tokenStream("name","护眼带光源");
ts.reset();
while (ts.incrementToken()){
System.out.println(ts.reflectAsString(false));
}
}
}
代码基本和入门Demo一样,只是在展示结果这里有点不一样,只列出这部分即可
public class TestHighlight {
private static void showSearchResults(IndexSearcher searcher, ScoreDoc[] hits, Query query, IKAnalyzer analyzer)
throws Exception {
System.out.println("找到 " + hits.length + " 个命中.");
System.out.println("序号\t匹配度得分\t结果");
SimpleHTMLFormatter shf = new SimpleHTMLFormatter("", "");
Highlighter highlighter = new Highlighter(shf, new QueryScorer(query));
for (int i = 0; i < hits.length; ++i) {
ScoreDoc scoreDoc= hits[i];
int docId = scoreDoc.doc;
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) {
TokenStream tokenStream = analyzer.tokenStream(f.name(), new StringReader(d.get(f.name())));
String fileContent = highlighter.getBestFragment(tokenStream, d.get(f.name()));
System.out.print("\t" + fileContent);
}
System.out.println("
");
}
}
}
运行结果是 html
代码,为了正常显示,复制到一个html
文件里,打开就可以看到效果了
数据量太大,只贴出 部分测试数据
10001,房屋卫士自流平美缝剂瓷砖地砖专用双组份真瓷胶防水填缝剂镏金色,品质建材,398.00,上海,540785126782
10002,艾瑞泽手工大号小号调温热熔胶枪玻璃胶枪硅胶条热溶胶棒20W-100W,品质建材,21.80,山东青岛,24727352473
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Data
public class Product {
private String id;
private String name;
private String category;
private String price;
private String place;
private String code;
}
public class ProductUtil {
public static void main(String[] args) throws Exception{
//假如 此处有 一个txt文件 里面 有14多万条的数据
String fileName = "D:\\Users\\admin\\Desktop\\140k_products.txt";
List<Product> productList = file2List(fileName);
System.out.println(productList.size());
}
public static List<Product> file2List(String fileName)throws Exception{
File file = new File(fileName);
List<String> lines = FileUtils.readLines(file, "utf-8");
List<Product> produceList = new ArrayList<>();
for(String line:lines){
String[] split = line.split(",");
Product product = new Product(split[0], split[1], split[2], split[3], split[4], split[5]);
produceList.add(product);
}
return produceList;
}
}
public class TestLucene {
public static void main(String[] args) throws Exception {
IKAnalyzer analyzer = new IKAnalyzer();
Directory index = createIndex(analyzer);
// 3. 查询器
Scanner scanner = new Scanner(System.in);
while (true){
System.out.print("请输入查询关键字:");
String keyword = scanner.nextLine();
System.out.println("当前关键字是:"+keyword);
Query query = new QueryParser( "name", analyzer).parse(keyword);
// 4. 搜索
IndexReader reader = DirectoryReader.open(index);
IndexSearcher searcher = new IndexSearcher(reader);
int numberPerPage = 10;
System.out.printf("查询关键字是:\"%s\"%n",keyword);
ScoreDoc[] hits = searcher.search(query, numberPerPage).scoreDocs;
// 5. 显示查询结果
showSearchResults(searcher, hits, query, analyzer);
// 6. 关闭查询
reader.close();
}
}
private static void showSearchResults(IndexSearcher searcher, ScoreDoc[] hits, Query query, IKAnalyzer analyzer)
throws Exception {
System.out.println("找到 " + hits.length + " 个命中.");
System.out.println("序号\t匹配度得分\t结果");
SimpleHTMLFormatter shf = new SimpleHTMLFormatter("", "");
Highlighter highlighter = new Highlighter(shf, new QueryScorer(query));
for (int i = 0; i < hits.length; ++i) {
ScoreDoc scoreDoc= hits[i];
int docId = scoreDoc.doc;
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) {
if("name".equals(f.name())){
TokenStream tokenStream = analyzer.tokenStream(f.name(), new StringReader(d.get(f.name())));
String fileContent = highlighter.getBestFragment(tokenStream, d.get(f.name()));
System.out.print("\t" + fileContent);
}else{
System.out.println("\t"+d.get(f.name()));
}
}
System.out.println("
");
}
}
private static Directory createIndex(IKAnalyzer analyzer) throws Exception {
Directory index = new ByteBuffersDirectory();
IndexWriterConfig config = new IndexWriterConfig(analyzer);
IndexWriter writer = new IndexWriter(index, config);
String fileName = "D:\\Users\\admin\\Desktop\\140k_products.txt";
List<Product> productList = ProductUtil.file2List(fileName);
int total = productList.size();
int count=0;
int per=0;
int oldPer = 0;
for (Product product : productList) {
addDoc(writer, product);
count++;
per=count*100/total;
if(per!=oldPer){
oldPer=per;
System.out.printf("索引中,总共要添加 %d 条记录,当前添加进度是: %d%% %n",total,per);
}
}
writer.close();
return index;
}
private static void addDoc(IndexWriter w, Product p) throws IOException {
Document doc = new Document();
doc.add(new TextField("id", p.getId(), Field.Store.YES));
doc.add(new TextField("name", p.getName(), Field.Store.YES));
doc.add(new TextField("category", p.getCategory(), Field.Store.YES));
doc.add(new TextField("price", p.getPrice(), Field.Store.YES));
doc.add(new TextField("place", p.getPlace(), Field.Store.YES));
doc.add(new TextField("code", p.getCode(), Field.Store.YES));
w.addDocument(doc);
}
}
分页查询是很常见的需求,比如要查询第10页,每页10条数据。
Lucene
分页通常来讲有两种方式:
searchAfter
方法查询10条数据。 优点是内存消耗小,缺点是比第一种更慢main方法示例
public static void main(String[] args) throws Exception {
// 1. 准备中文分词器
IKAnalyzer analyzer = new IKAnalyzer();
// 2. 索引
Directory index = createIndex(analyzer);
// 3. 查询器
String keyword = "手机";
System.out.println("当前关键字是:"+keyword);
Query query = new QueryParser( "name", analyzer).parse(keyword);
// 4. 搜索
IndexReader reader = DirectoryReader.open(index);
IndexSearcher searcher=new IndexSearcher(reader);
int pageNow = 1;
int pageSize = 20;
ScoreDoc[] hits = pageSearch1(query, searcher, pageNow, pageSize);
// 5. 显示查询结果
showSearchResults(searcher, hits,query,analyzer);
// 6. 关闭查询
reader.close();
}
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;
}
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;
}
索引建立好了之后,还是需要维护的,比如新增,删除和维护。 新增就是建立索引的过程,这里就不表了
索引里的数据,其实就是一个一个的 Document
对象
main方法中调用
public static void main(String[] args) throws Exception {
// 1. 准备中文分词器
IKAnalyzer analyzer = new IKAnalyzer();
// 2. 索引
Directory index = createIndex(analyzer);
// 3. 查询器
//删除id=10046
IndexWriterConfig config = new IndexWriterConfig(analyzer);
try(IndexWriter indexWriter = new IndexWriter(index, config);){
indexWriter.deleteDocuments(new Term("id", "10046"));
indexWriter.commit();
}
String keyword = "手机";
System.out.println("当前关键字是:"+keyword);
Query query = new QueryParser( "name", analyzer).parse(keyword);
// 4. 搜索
IndexReader reader = DirectoryReader.open(index);
IndexSearcher searcher=new IndexSearcher(reader);
int pageNow = 1;
int pageSize = 20;
ScoreDoc[] hits = pageSearch1(query, searcher, pageNow, pageSize);
// 5. 显示查询结果
showSearchResults(searcher, hits,query,analyzer);
// 6. 关闭查询
reader.close();
}
public static void main(String[] args) throws Exception {
// 1. 准备中文分词器
IKAnalyzer analyzer = new IKAnalyzer();
// 2. 索引
Directory index = createIndex(analyzer);
// 3. 查询器
//更新索引
IndexWriterConfig config = new IndexWriterConfig(analyzer);
try(IndexWriter indexWriter = new IndexWriter(index, config);){
Document doc = new Document();
doc.add(new TextField("id","10046",Field.Store.YES));
doc.add(new TextField("name", "手机,神了", Field.Store.YES));
doc.add(new TextField("category", "道具", Field.Store.YES));
doc.add(new TextField("price", "998", Field.Store.YES));
doc.add(new TextField("place", "南海群岛", Field.Store.YES));
doc.add(new TextField("code", "888888", Field.Store.YES));
indexWriter.updateDocument(new Term("id", "10046"), doc);
indexWriter.commit();
}
String keyword = "手机";
System.out.println("当前关键字是:"+keyword);
Query query = new QueryParser( "name", analyzer).parse(keyword);
// 4. 搜索
IndexReader reader = DirectoryReader.open(index);
IndexSearcher searcher=new IndexSearcher(reader);
int pageNow = 1;
int pageSize = 20;
ScoreDoc[] hits = pageSearch1(query, searcher, pageNow, pageSize);
// 5. 显示查询结果
showSearchResults(searcher, hits,query,analyzer);
// 6. 关闭查询
reader.close();
}
更新索引后,就不再是原来的查询结果:1 6.1540966 10046 赛鲸 懒人手机支架 床头支架多功能通用创意手机夹子桌面懒人神器 潮酷数码会场 29.90 上海 40399176130
,而是一个新的索引查询结果:1 6.769873 10046 手机,神了 道具 998 南海群岛 888888