由浅到深解析全文检索 Lucene

本文深入探讨了Lucene全文检索框架,介绍了全文检索的概念、特点及其在搜索引擎中的基础作用。详细讲解了Lucene的安装、配置、索引创建、搜索、API使用,包括Directory、Analyzer、索引的添删改、Query及Searcher等关键组件。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.全文检索认识

Lucene是一个全文检索的框架,所以呢我们先来了解一下什么是全文检索。

1.1 现实生活中搜索的案例
1)windows系统中的有搜索功能:打开“我的电脑”,按“F3”就可以使用查找的功能,查找指定的文件或文件夹。搜索的范围是整个电脑中的文件资源

2)clipse中的帮助子系统:点击HelpHelp Contents,可以查找出相关的帮助信息。搜索的范围是Eclipse的所有帮助文件,Eclipse的帮助就是用Lucene做的。
3)在BBS、BLOG、新闻等系统中提供的搜索文章的功能,如这里的贴吧的例子。搜索的范围是系统内的文章数据(都在数据库中)。
4)搜索引擎,如Baidu或Google等,可以查询到互联网中的网页、PDF、DOC、PPT、图片、音乐、视频等。搜索范围是整个互联网的网页.
1.2. 全文数据搜索方式

1.2.1. 顺序扫描法(Serial Scanning)
所谓顺序扫描,比如要找内容包含某一个字符串的文件,就是一个文档一个文档的看,对于每一个文档,从头看到尾,如果此文档包含此字符串,则此文档为我们要找的文件,接着看下一个文件,直到扫描完所有的文件。比如Window自带的搜索,在数据库中扫描不带索引文本字段等。
如何提速?
之前的XP系统中搜索都非常慢,因为他采用上面的顺序扫描法,在WIN 7中为了改善这种搜索的速度,提供了如下功能:
如何提升全文检索的速度?

1.2.2. 全文检索
非结构化数据顺序扫描很慢,对结构化数据的搜索却相对较快(由于结构化数据有一定的结构可以采取一定的搜索算法加快速度),那么把我们的非结构化数据想办法弄得有一定结构不就行了吗?关系数据库中存储的都是结构化数据,因此很检索都比较快。
从非结构化数据中提取出的然后重新组织的信息,我们称之索引。即为文本数据建立类似字典目录,从而提高检索速度。

1.3. 全文检索的特点
相关度最高的排在最前面,官网中相关的网页排在最前面;
对摘要进行了截取;
关键词的高亮。
只关注文本,不考虑语义。
比如在输入框中输入“中国的首都在哪里”,搜索引擎不会以对话的形式告诉你“在北京”,而仅仅是列出包含了搜索关键字的网页。
在搜索引擎中查询关键词或句子时,你在最后加上问号也是没有意义的做法,会直接被过滤掉。

1.4. 全文索引是搜索引擎的基础
各种搜索引擎运行依赖于全文检索,百度谷歌的界面看着很简单,技术主要体现在后台全文检索技术的实现上,他们自己都有基于全文检索做了实现,这些技术肯定不是开源的,不然他们在整个是市场也没啥优势了。我们要学习的Lucene是开源的,目前使用最广的全文检索工具包;

1.5. 全文检索知识小结
全文检索主要就分为索引的建立和索引的搜索,如下图所示:
在这里插入图片描述这里再分别简述下两个过程:
索引的创建:首先需要找到数据库源,即待索引的文件;然后进行词法分析,即分词,去停词,去标点符号,大写变小写等,语言处理主要是针对英文,时态的转变等;这些基础处理完成后就可以按一定顺序合并相同的词后生成倒排词链表;把需要储存的文件内容放入索引库中储存同时为文档生成一个编号;最后把倒排词链表和文档的编号建立关联就OK。
索引的搜索:在获取到用户的查询语句后,同样也需要进行词法分析,语言处理,在全文检索中匹配的最小单位是词,索引索引的建立和搜索时必须使用相同的词法分析器进行分词;语法分析主要是确定符合各个词的结果间采取怎样的取舍规则,交集或并集等;然后经过步骤(d),在索引储存库中获取到每个词所对应的结果集;在搜索索引节点处按照分析规则进行结果的筛选处理;最后把结果进行相关性排序然后返回到用户界面.

2. Lucene入门

2.1. 是什么
Apache Lucene是一个用Java写的高性能、可伸缩的全文检索引擎工具包,它可以方便的嵌入到各种应用中实现针对应用的全文索引/检索功能。Lucene的目标是为各种中小型应用程序加入全文检索功能。
Lucene的核心作者:Doug Cutting是一位资深全文索引/检索专家。
版本发布情况:2000年3月,最初版发布,2001年9月,加入apache;2004年7月,发布1.4正式版;2009年11月,发布2.9.1(jdk1.4)及3.0(jdk1.5)版本;2015年3月,发布4.10.4。2016年2月,发布5.5.0。
2.2. 创建索引
步骤:
1、 把文本内容转换为Document对象
文本是作为Document对象的一个字段而存在
2、准备IndexWriter(索引写入器)
3 、通过IndexWriter,把Document添加到缓冲区并提交
addDocument
commit
close
//创建索引的数据 现在写死,以后根据实际应用场景
String doc1 = “hello world”;
String doc2 = “hello java world”;
String doc3 = “hello lucene world”;
private String path =“F:/eclipse/workspace/lucene/index/
hello”;
@Test
public void testCreate() {
try {
//2、准备IndexWriter(索引写入器)
//索引库的位置 FS fileSystem
Directory d = FSDirectory.open(Paths.get(path ));
//分词器
Analyzer analyzer = new StandardAnalyzer();
//索引写入器的配置对象
IndexWriterConfig conf = new IndexWriterConfig(analyzer);
IndexWriter indexWriter = new IndexWriter(d, conf);
System.out.println(indexWriter);

		//1、 把文本内容转换为Document对象
		//把文本转换为document对象
		Document document1 = new Document();
		//标题字段
		document1.add(new TextField("title", "doc1", Store.YES));
		document1.add(new TextField("content", doc1, Store.YES));
		//添加document到缓冲区
		indexWriter.addDocument(document1);
		Document document2 = new Document();
		//标题字段
		document2.add(new TextField("title", "doc2", Store.YES));
		document2.add(new TextField("content", doc2, Store.YES));
		//添加document到缓冲区
		indexWriter.addDocument(document2);
		Document document3 = new Document();
		//标题字段
		document3.add(new TextField("title", "doc3", Store.YES));
		document3.add(new TextField("content", doc3, Store.YES));
		
		//3 、通过IndexWriter,把Document添加到缓冲区并提交
		//添加document到缓冲区
		indexWriter.addDocument(document3);
		indexWriter.commit();
		indexWriter.close();
		
	} catch (Exception e) {
		e.printStackTrace();
	}

}

OpenMode=create 每次都会重置索引库然后重新添加索引文档
后者覆盖前者(默认是不覆盖累加模式)
conf.setOpenMode(OpenMode.CREATE);
图形界面客户端使用

2.2…2. 搜索索引
1 封装查询提交为查询对象
2 准备IndexSearcher
3 使用IndexSearcher传入查询对象做查询-----查询出来只是文档编号DocID
4 通过IndexSearcher传入DocID获取文档
5 把文档转换为前台需要的对象 Docment----> Article

@Test
	public void testSearch() {
		String keyWord = "lucene";
		try {
			// * 1 封装查询提交为查询对象
		    //通过查询解析器解析一个字符串为查询对象
			String f = "content"; //查询的默认字段名,
			Analyzer a = new StandardAnalyzer();//查询关键字要分词,所有需要分词器
			QueryParser parser = new QueryParser(f, a);
			Query query = parser.parse("content:"+keyWord);
			// * 2 准备IndexSearcher
			Directory d = FSDirectory.open(Paths.get(path ));
			IndexReader r = DirectoryReader.open(d);
			IndexSearcher searcher = new IndexSearcher(r);
			// * 3 使用IndexSearcher传入查询对象做查询-----查询出来只是文档编号DocID
			TopDocs topDocs = searcher.search(query, 1000);//查询ton条记录 前多少条记录
			System.out.println("总命中数:"+topDocs.totalHits);
			ScoreDoc[] scoreDocs = topDocs.scoreDocs;//命中的所有的文档的封装(docId)
			// * 4 通过IndexSearcher传入DocID获取文档
			for (ScoreDoc scoreDoc : scoreDocs) {
				int docId = scoreDoc.doc;
				Document document = searcher.doc(docId);
				// * 5 把文档转换为前台需要的对象 Docment----> Article
				System.out.println("=======================================");
				System.out.println("title:"+document.get("title")
								+",content:"+document.get("content"));
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

3. Lucene API详解

3.1. 索引目录Directory
Directory是一个对索引目录的一个抽象。索引目录用于存放lucene索引文件。直接根据一个文件夹地址来创建索引目录使用SimpleFSDirectory。
MMapDirectory : 针对64系统,它在维护索引库时,会结合“内存”与硬盘同步来处理索引。
SimpleFSDirectory : 传统的文件系统索引库。
RAMDirectory : 内存索引库
3.2. 分词Analyzer(词法分析器)
分词器是Lucene中非常重要的一个知识点,如果你面试时说你用过Lucene面试官一定会问你用的什么分词器。
分词,也称词法分析器(或者叫语言分析器),就是指索引中的内容按什么样的方式来建立,这在全文检索中非常关键,是按英文单词建立索引,还是按中文词意建立索引;这些需要由Analyzer来指定。
对于中文,需要采用字典分词,也叫词库分词;把中文件的词全部放置到一个词库中,按某种算法来维护词库内容;如果匹配到就切分出来成为词语。通常词库分词被认为是最理想的中文分词算法。如:“我们是中国人”,效果为:“我们”、“中国人”。(可以使用SmartChineseAnalyzer,“极易分词” MMAnalyzer ,或者是“庖丁分词”分词器、IKAnalyzer。推荐使用IKAnalyzer )
在这里我们推荐IKAnalyzer。使用时需导入IKAnalyzer.jar,并且拷贝IKAnalyzer.cfg.xml,ext_stopword.dic文件,分词器测试代码如下:

public class AnalyzerTest {
	//创建索引的数据 现在写死,以后根据实际应用场景
	private String en = "oh my lady gaga"; // oh my god
	private String cn = "迅雷不及掩耳盗铃儿响叮当仁不让";
	private String str = "源代码教育FullText Search Lucene框架的学习";
	
		
	/**
	 * 把特定字符串按特定的分词器来分词
	 * @param analyzer
	 * @param str
	 * @throws Exception
	 */
	public void testAnalyzer(Analyzer analyzer,String str) throws Exception {
		TokenStream tokenStream = analyzer.tokenStream("content", new StringReader(str));
		// 在读取词元流后,需要先重置/重加载一次
		tokenStream.reset();
		while(tokenStream.incrementToken()){
			System.out.println(tokenStream);
		}
	}
	
	//标准分词:不支持中文
	@Test
	public void testStandardAnalyzer() throws Exception {
		
		testAnalyzer(new StandardAnalyzer(), cn);
	}
	
	//简单分词:不支持中文
	@Test
	public void testSimpleAnalyzer() throws Exception {
		testAnalyzer(new SimpleAnalyzer(), cn);
	}
	
	//二分分词:两个字是一个词
	@Test
	public void testCJKAnalyzer() throws Exception {
		testAnalyzer(new CJKAnalyzer(), cn);
	}
	
	//词典分词:从词典中查找
	@Test
	public void testSmartChineseAnalyzer() throws Exception {
		testAnalyzer(new SmartChineseAnalyzer(), str);
	}
	
	//IK分词:从词典中查找
	// 简单使用:拷贝两个配置文件,IKAnalyzer.cfg.xml,stopword.dic拷贝一个jar包 
    IKAnalyzer2012_V5.jar
	//       扩展词,停止词
	//  注意:打开方式,不要使用其他的,
//直接使用eclipse的text Editor, 
修改以后要刷新一下让项目重新编译(有时候需要有时候不需要刷新)
	
	@Test
	public void testIKAnalyzer() throws Exception {
		//true 粗密度分词(智能分词)  false 细密度分词
		testAnalyzer(new IKAnalyzer(true), str);
	}
}

3.3. 索引的添删改
经过之前的分析,我们知道对索引的操作统一使用IndexWriter。测试代码如下:
// 数据源
private String doc1 = “hello world”;
private String doc2 = “hello java world”;
private String doc3 = “hello lucene world”;

// 索引库目录
private String indexPath = "F:\\ecworkspace\\lucene\\indexCRUD";
@Test
public void createIndex() throws IOException, ParseException {
	/**
	 * 准备工作
	 */
	// 索引目录
	Directory d = FSDirectory.open(Paths.get(indexPath));
	// 词法分析器
	Analyzer analyzer = new StandardAnalyzer();
	// 写操作核心配置对象
	IndexWriterConfig conf = new IndexWriterConfig(analyzer);
	conf.setOpenMode(OpenMode.CREATE);
	// 写操作核心对象
	IndexWriter indexWriter = new IndexWriter(d, conf);
	System.out.println(indexWriter);

	/**
	 * 操作
	 */
	Document document1 = new Document();
	document1.add(new TextField("id", "1", Store.YES));
	document1.add(new TextField("name", "doc1", Store.YES));
	document1.add(new TextField("content", doc1, Store.YES));
	indexWriter.addDocument(document1);

	Document document2 = new Document();
	document2.add(new TextField("id", "2", Store.YES));
	document2.add(new TextField("name", "doc2", Store.YES));
	document2.add(new TextField("content", doc2, Store.YES));
	indexWriter.addDocument(document2);
	Document document3 = new Document();
	document3.add(new TextField("id", "3", Store.YES));
	document3.add(new TextField("name", "doc3", Store.YES));
	document3.add(new TextField("content", doc3, Store.YES));
	indexWriter.addDocument(document3);
	/**
	 * 收尾
	 */
	indexWriter.commit();
	indexWriter.close();
	
	searchIndex();
}

@Test
public void del() throws IOException, ParseException{
	/**
	 * 准备工作
	 */
	// 索引目录
	Directory d = FSDirectory.open(Paths.get(indexPath));
	// 词法分析器
	Analyzer analyzer = new StandardAnalyzer();
	// 写操作核心配置对象
	IndexWriterConfig conf = new IndexWriterConfig(analyzer);
	// 写操作核心对象
	IndexWriter indexWriter = new IndexWriter(d, conf);
	System.out.println(indexWriter);
	
	
	//删除所有
	//indexWriter.deleteAll();
	//第一种

// QueryParser qpParser = new QueryParser(“id”, analyzer);
// Query query = qpParser.parse(“1”);
// indexWriter.deleteDocuments(query);

	//第二种
	indexWriter.deleteDocuments(new Term("id", "1"));
	
	indexWriter.commit();
	indexWriter.close();
	
	searchIndex();
}

@Test
public void update() throws IOException, ParseException{
	/**
	 * 准备工作
	 */
	// 索引目录
	Directory d = FSDirectory.open(Paths.get(indexPath));
	// 词法分析器
	Analyzer analyzer = new StandardAnalyzer();
	// 写操作核心配置对象
	IndexWriterConfig conf = new IndexWriterConfig(analyzer);
	// 写操作核心对象
	IndexWriter indexWriter = new IndexWriter(d, conf);
	System.out.println(indexWriter);
	
	
	Document doc = new Document();
	doc.add(new TextField("id", "2", Store.YES));
	doc.add(new TextField("name", "doc2", Store.YES));
	doc.add(new TextField("content", "修改后 -的doc2", Store.YES));
	
	indexWriter.updateDocument(new Term("id","2"), doc );
	/*等价于
	 indexWriter.deleteDocuments(new Term("id", "2"));
	 indexWriter.addDocument(doc);
	 */
	indexWriter.commit();
	indexWriter.close();
	
	searchIndex();
}


@Test
public void searchIndex() throws IOException, ParseException {
	// 索引目录
	Directory d = FSDirectory.open(Paths.get(indexPath));
	// 词法分析器
	Analyzer analyzer = new StandardAnalyzer();
	// 创建索引的读写对象
	IndexReader r = DirectoryReader.open(d);
	// 创建核心对象
	IndexSearcher indexSearcher = new IndexSearcher(r);

	// 查询解析器
	// 参数1:默认查询的字段
	// 参数2:分词器
	QueryParser queryParser = new QueryParser("content", analyzer);
	String queryString = "*:*";

	Query query = queryParser.parse(queryString);
	// 调用核心对象的search方法
	// 参数query: 查询对象
	// 参数 n : 前n条
	TopDocs topDocs = indexSearcher.search(query, 50);
	System.out.println("一共查询到的数量:" + topDocs.totalHits);

	// 获得数据集合
	ScoreDoc[] scoreDocs = topDocs.scoreDocs;
	for (ScoreDoc scoreDoc : scoreDocs) {
		// 获取文档ID
		int docId = scoreDoc.doc;
		// 通过docId获取Document
		Document doc = indexSearcher.doc(docId);

		System.out.println("id="+doc.get("id")+",name=" + doc.get("name") + ",content=" + doc.get("content"));
	}
}

3.4. Query及Searcher
搜索是全文检索中最重要的一部分,前面HelloWorld中也发现,Query对象只是一个接口,他有很多子类的实现。在前面直接使用QueryParser的Parse方法来创建Query对象的实例,实际他会根据我们传入的搜索关键字自动解析成需要的查询类型,索引在这里我们也可以直接new一个Query实例来达到不同的搜索效
抽取结构:
// 先做一个准备工作,提供两个search方法
//一个传入搜索关键字进行搜索
public void search(String keyword) throws Exception {
Directory directory = FSDirectory.open(Paths.get(“E:\tools\eclipse\workspace\lucene\helloIndex”));
;
// 索引的和读取对象
IndexReader reader = DirectoryReader.open(directory);
// 搜索文档通过核心搜索类IndexSearcher来查询
IndexSearcher indexSearcher = new IndexSearcher(reader);

	// 先创建一个QueryParse对象
	QueryParser queryParser = new QueryParser("content", new StandardAnalyzer());
	// 通过queryParse对象解析关键字并创建对应的查询对象
	Query query = queryParser.parse(keyword);

	// 通过search方法返回前n个文档的封装对象
	TopDocs topDocs = indexSearcher.search(query, 5);
	// 总共找到的相关的文档数
	int totalHits = topDocs.totalHits;
	System.out.println("总条数:" + totalHits);
	// 获取查询的结果(并不包含文档本身)
	ScoreDoc[] scoreDocs = topDocs.scoreDocs;
	for (ScoreDoc scoreDoc : scoreDocs) {
		int documentId = scoreDoc.doc;
		Document document = indexSearcher.doc(documentId);
		float score = scoreDoc.score;

		// 获取文档的字段值
		String docId = document.get("docId");
		String content = document.get("content");

		System.out.println("ID:" + documentId + ",score:" + score + ",docId:" + docId + ",content:" + content);
	}
}
// 传入一个查询对象
	public static void testSearch(Query q) throws Exception {
        // 索引库地址
		String path = "E:\\work\\eclipse4.7_project\\Luncene-demo\\index";
		System.out.println("对应的查询语句为:" + q);
		// 获取索引库的目录
		Directory d = FSDirectory.open(Paths.get(path));
		// 获取索引读取对象
		IndexReader reader = DirectoryReader.open(d);
		// 创建索引查询器
		IndexSearcher searcher = new IndexSearcher(reader);
		// 执行查询
		TopDocs td = searcher.search(q, 10);
		// 遍历结果
		for (int i = 0; i < td.scoreDocs.length; i++) {
			// 得到符合条件的内部文档对象
			ScoreDoc doc = td.scoreDocs[i];
			// 得到文档对象
			Document d1 = searcher.doc(doc.doc);
			System.out.println("title: " + d1.get("title") + "     content:" + d1.get("content"));
		}
	}

1)单词查询

@Test
	public void testTerm() throws Exception {
		// 简写方式查询
		testSearch("content:hello");
		System.out.println("===================================");
		// 查询对象查询
		Query query = new TermQuery(new Term("content", "hello"));
		testSearch(query);
	}

2)段落搜索, 要想把多个单词当成一个整体进行搜索,使用双引号包裹

@Test
	public void testPhsee() throws Exception {
		// 简写方式查询
		testSearch("\"hello java\"");
	}

3)通配符搜索

 @Test
	public void testwild() throws Exception {
		// 简写方式查询
		testSearch("luce*e");
	}

4)模糊搜索 最多允许 2个错误

// 模糊查询(容错查询) ~2 代表 容错几个 最多只能2个
	@Test
	public void testLike() throws Exception {
		// 简写方式查询
		testSearch("lucexx~2");
	}

5)临近查询,在段落查询的基础上用“~”后面跟一个1到正无穷的正整数。代表段落中,单词与单词之间最大的间隔数

@Test
	public void testLj() throws Exception {
		// 简写方式查询
		testSearch("\"hello world\"~2");
	}

6)组合查询
// + (must) : 对应的单词必须出现
// - (must_not): 不能出现
// 不写 (should): 可能出现
// 关键字之间的逻辑计算是 AND

@Test
	public void testzh() throws Exception {
		// 简写方式查询
		testSearch("java hello");
	}

基本我已经吧Lucence框架的作用和用法都给总结在这里了,希望我的总结能够对你有所帮助

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值