首先需要导入 Lucene 的依赖,它的依赖有好几个,如下:
org.apache.lucene
lucene-core
5.3.1
org.apache.lucene
lucene-queryparser
5.3.1
org.apache.lucene
lucene-analyzers-common
5.3.1
org.apache.lucene
lucene-highlighter
5.3.1
org.apache.lucene
lucene-analyzers-smartcn
5.3.1
最后一个依赖是用来支持中文分词的,因为默认是支持英文的。那个高亮的分词依赖是最后我要做一个搜索,然后将搜到的内容高亮显示,模拟当前互联网上的做法,大家可以运用到实际项目中去。
根据上文的分析,全文检索有两个步骤,先建立索引,再检索。所以为了测试这个过程,我新建两个 java 类,一个用来建立索引的,另一个用来检索。
2.2.1 建立索引
我们自己弄几个文件,放到 D:\lucene\data 目录下,新建一个 Indexer 类来实现建立索引功能。首先在构造方法中初始化标准分词器和写索引实例
public class Indexer {
/**
* 写索引实例
*/
private IndexWriter writer;
/**
* 构造方法,实例化IndexWriter
* @param indexDir
* @throws Exception
*/
public Indexer(String indexDir) throws Exception {
Directory dir = FSDirectory.open(Paths.get(indexDir));
//标准分词器,会自动去掉空格啊,is a the等单词
Analyzer analyzer = new StandardAnalyzer();
//将标准分词器配到写索引的配置中
IndexWriterConfig config = new IndexWriterConfig(analyzer);
//实例化写索引对象
writer = new IndexWriter(dir, config);
}
}
在构造放发中传一个存放索引的文件夹路径,然后构建标准分词器(这是英文的),再使用标准分词器来实例化写索引对象。
/**
* 索引指定目录下的所有文件
* @param dataDir
* @return
* @throws Exception
*/
public int indexAll(String dataDir) throws Exception {
// 获取该路径下的所有文件
File[] files = new File(dataDir).listFiles();
if (null != files) {
for (File file : files) {
//调用下面的indexFile方法,对每个文件进行索引
indexFile(file);
}
}
//返回索引的文件数
return writer.numDocs();
}
/**
* 索引指定的文件
* @param file
* @throws Exception
*/
private void indexFile(File file) throws Exception {
System.out.println("索引文件的路径:" + file.getCanonicalPath());
//调用下面的getDocument方法,获取该文件的document
Document doc = getDocument(file);
//将doc添加到索引中
writer.addDocument(doc);
}
/**
* 获取文档,文档里再设置每个字段,就类似于数据库中的一行记录
* @param file
* @return
* @throws Exception
*/
private Document getDocument(File file) throws Exception {
Document doc = new Document();
//开始添加字段
//添加内容
doc.add(new TextField("contents", new FileReader(file)));
//添加文件名,并把这个字段存到索引文件里
doc.add(new TextField("fileName", file.getName(), Field.Store.YES));
//添加文件路径
doc.add(new TextField("fullPath", file.getCanonicalPath(), Field.Store.YES));
return doc;
}
在该类中写一个 main 方法测试一下:
public static void main(String[] args) {
//索引保存到的路径
String indexDir = "D:\\lucene";
//需要索引的文件数据存放的目录
String dataDir = "D:\\lucene\\data";
Indexer indexer = null;
int indexedNum = 0;
//记录索引开始时间
long startTime = System.currentTimeMillis();
try {
// 开始构建索引
indexer = new Indexer(indexDir);
indexedNum = indexer.indexAll(dataDir);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (null != indexer) {
indexer.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
//记录索引结束时间
long endTime = System.currentTimeMillis();
System.out.println("索引耗时" + (endTime - startTime) + "毫秒");
System.out.println("共索引了" + indexedNum + "个文件");
}
两个 tomcat 相关的文件放到 D:\lucene\data
下了,执行完之后,看到控制台输出:
索引文件的路径:D:\lucene\data\catalina.properties
索引文件的路径:D:\lucene\data\logging.properties
索引耗时882毫秒
共索引了2个文件
然后我们去 D:\lucene\
目录下可以看到一些索引文件,这些文件不能删除,删除了就需要重新构建索引,否则没了索引,就无法去检索内容了。
2.2.2 检索内容
上面把这两个文件的索引建立好了,接下来我们就可以写检索程序了,在这两个文件中查找特定的词。
public class Searcher {
public static void search(String indexDir, String q) throws Exception {
//获取要查询的路径,也就是索引所在的位置
Directory dir = FSDirectory.open(Paths.get(indexDir));
IndexReader reader = DirectoryReader.open(dir);
//构建IndexSearcher
IndexSearcher searcher = new IndexSearcher(reader);
//标准分词器,会自动去掉空格啊,is a the等单词
Analyzer analyzer = new StandardAnalyzer();
//查询解析器
QueryParser parser = new QueryParser("contents", analyzer);
//通过解析要查询的String,获取查询对象,q为传进来的待查的字符串
Query query = parser.parse(q);
//记录索引开始时间
long startTime = System.currentTimeMillis();
//开始查询,查询前10条数据,将记录保存在docs中
TopDocs docs = searcher.search(query, 10);
//记录索引结束时间
long endTime = System.currentTimeMillis();
System.out.println("匹配" + q + "共耗时" + (endTime-startTime) + "毫秒");
System.out.println("查询到" + docs.totalHits + "条记录");
//取出每条查询结果
for(ScoreDoc scoreDoc : docs.scoreDocs) {
//scoreDoc.doc相当于docID,根据这个docID来获取文档
Document doc = searcher.doc(scoreDoc.doc);
//fullPath是刚刚建立索引的时候我们定义的一个字段,表示路径。也可以取其他的内容,只要我们在建立索引时有定义即可。
System.out.println(doc.get("fullPath"));
}
reader.close();
}
}
ok,这样我们检索的代码就写完了,每一步解释我写在代码中的注释上了,下面写个 main 方法来测试一下:
public static void main(String[] args) {
String indexDir = "D:\\lucene";
//查询这个字符串
String q = "security";
try {
search(indexDir, q);
} catch (Exception e) {
e.printStackTrace();
}
}
查一下 security
这个字符串,执行一下看控制台打印的结果:
匹配security共耗时23毫秒
查询到1条记录
D:\lucene\data\catalina.properties
可以看出,耗时了23毫秒在两个文件中找到了 security 这个字符串,并输出了文件的名称。上面的代码我写的很详细,这个代码已经比较全了,可以用在生产环境上。
上文已经写了建立索引和检索的代码,但是在实际项目中,我们往往是结合页面做一些查询结果的展示,比如我要查某个关键字,查到了之后,将相关的信息点展示出来,并将查询的关键字高亮等等。这种需求在实际项目中非常常见,而且大多数网站中都会有这种效果。所以这一小节我们就使用 Lucene 来实现这种效果。
2.3.1 中文分词
我们新建一个 ChineseIndexer 类来建立中文索引,建立过程和英文索引一样的,不同的地方在于使用的是中文分词器。除此之外,这里我们不用通过读取文件去建立索引,我们模拟一下用字符串来建立,因为在实际项目中,绝大部分情况是获取到一些文本字符串,然后根据一些关键字去查询相关内容等等。代码如下:
public class ChineseIndexer {
/**
* 存放索引的位置
*/
private Directory dir;
//准备一下用来测试的数据
//用来标识文档
private Integer ids[] = {1, 2, 3};
private String citys[] = {"上海", "南京", "青岛"};
private String descs[] = {
"上海是个繁华的城市。",
"南京是一个文化的城市南京,简称宁,是江苏省会,地处中国东部地区,长江下游,濒江近海。全市下辖11个区,总面积6597平方公里,2013年建成区面积752.83平方公里,常住人口818.78万,其中城镇人口659.1万人。[1-4] “江南佳丽地,金陵帝王州”,南京拥有着6000多年文明史、近2600年建城史和近500年的建都史,是中国四大古都之一,有“六朝古都”、“十朝都会”之称,是中华文明的重要发祥地,历史上曾数次庇佑华夏之正朔,长期是中国南方的政治、经济、文化中心,拥有厚重的文化底蕴和丰富的历史遗存。[5-7] 南京是国家重要的科教中心,自古以来就是一座崇文重教的城市,有“天下文枢”、“东南第一学”的美誉。截至2013年,南京有高等院校75所,其中211高校8所,仅次于北京上海;国家重点实验室25所、国家重点学科169个、两院院士83人,均居中国第三。[8-10] 。",
"青岛是一个美丽的城市。"
};
/**
* 生成索引
* @param indexDir
* @throws Exception
*/
public void index(String indexDir) throws Exception {
dir = FSDirectory.open(Paths.get(indexDir));
// 先调用 getWriter 获取IndexWriter对象
IndexWriter writer = getWriter();
for(int i = 0; i < ids.length; i++) {
Document doc = new Document();
// 把上面的数据都生成索引,分别用id、city和desc来标识
doc.add(new IntField("id", ids[i], Field.Store.YES));
doc.add(new StringField("city", citys[i], Field.Store.YES));
doc.add(new TextField("desc", descs[i], Field.Store.YES));
//添加文档
writer.addDocument(doc);
}
//close了才真正写到文档中
writer.close();
}
/**
* 获取IndexWriter实例
* @return
* @throws Exception
*/
private IndexWriter getWriter() throws Exception {
//使用中文分词器
SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();
//将中文分词器配到写索引的配置中
IndexWriterConfig config = new IndexWriterConfig(analyzer);
//实例化写索引对象
IndexWriter writer = new IndexWriter(dir, config);
return writer;
}
public static void main(String[] args) throws Exception {
new ChineseIndexer().index("D:\\lucene2");
}
}
这里我们用 id、city、desc 分别代表 id、城市名称和城市描述,用他们作为关键字来建立索引,后面我们获取内容的时候,主要来获取城市描述。南京的描述我故意写的长一点,因为下文检索的时候,根据不同的关键字会检索到不同部分的信息,有个权重的概念在里面。
然后执行一下 main 方法,将索引保存到 D:\lucene2\ 中。
2.3.2 中文分词查询
中文分词查询代码逻辑和默认的查询差不多,有一些区别在于,我们需要将查询出来的关键字标红加粗等需要处理,需要计算出一个得分片段,这是什么意思呢?比如我搜索 “南京文化” 跟搜索 “南京文明”,这两个搜索结果应该根据关键字出现的位置,返回的结果不一样才对,这在下文会测试。我们先看一下代码和注释:
public class ChineseSearch {
private static final Logger logger = LoggerFactory.getLogger(ChineseSearch.class);
public static List search(String indexDir, String q) throws Exception {
//获取要查询的路径,也就是索引所在的位置
Directory dir = FSDirectory.open(Paths.get(indexDir));
IndexReader reader = DirectoryReader.open(dir);
IndexSearcher searcher = new IndexSearcher(reader);
//使用中文分词器
SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();
//由中文分词器初始化查询解析器
QueryParser parser = new QueryParser("desc", analyzer);
//通过解析要查询的String,获取查询对象
Query query = parser.parse(q);
//记录索引开始时间
long startTime = System.currentTimeMillis();
//开始查询,查询前10条数据,将记录保存在docs中
TopDocs docs = searcher.search(query, 10);
//记录索引结束时间
long endTime = System.currentTimeMillis();
logger.info("匹配{}共耗时{}毫秒", q, (endTime - startTime));
logger.info("查询到{}条记录", docs.totalHits);
//如果不指定参数的话,默认是加粗,即
SimpleHTMLFormatter simpleHTMLFormatter = new SimpleHTMLFormatter("","");
//根据查询对象计算得分,会初始化一个查询结果最高的得分
QueryScorer scorer = new QueryScorer(query);
//根据这个得分计算出一个片段
Fragmenter fragmenter = new SimpleSpanFragmenter(scorer);
//将这个片段中的关键字用上面初始化好的高亮格式高亮
Highlighter highlighter = new Highlighter(simpleHTMLFormatter, scorer);
//设置一下要显示的片段
highlighter.setTextFragmenter(fragmenter);
//取出每条查询结果
List list = new ArrayList<>();
for(ScoreDoc scoreDoc : docs.scoreDocs) {
//scoreDoc.doc相当于docID,根据这个docID来获取文档
Document doc = searcher.doc(scoreDoc.doc);
logger.info("city:{}", doc.get("city"));
logger.info("desc:{}", doc.get("desc"));
String desc = doc.get("desc");
//显示高亮
if(desc != null) {
TokenStream tokenStream = analyzer.tokenStream("desc", new StringReader(desc));
String summary = highlighter.getBestFragment(tokenStream, desc);
logger.info("高亮后的desc:{}", summary);
list.add(summary);
}
}
reader.close();
return list;
}
}
每一步的注释我写的很详细,在这就不赘述了。接下来我们来测试一下效果。
2.3.3 测试一下
这里我们使用 thymeleaf 来写个简单的页面来展示获取到的数据,并高亮展示。在 controller 中我们指定索引的目录和需要查询的字符串,如下:
@Controller
@RequestMapping("/lucene")
public class IndexController {
@GetMapping("/test")
public String test(Model model) {
// 索引所在的目录
String indexDir = "D:\\lucene2";
// 要查询的字符
// String q = "南京文明";
String q = "南京文化";
try {
List list = ChineseSearch.search(indexDir, q);
model.addAttribute("list", list);
} catch (Exception e) {
e.printStackTrace();
}
return "result";
}
}
直接返回到 result.html 页面,该页面主要来展示一下 model 中的数据即可。
Title