Springboot下的Lucene(详细版)

1.搜索大数据

1.1 为什么要搜索

在当前百万级数据的面前,如果全部放在同一个表或者某几个表中,经常搜索数据库特别模糊搜索会爆吗?

答案是没必,但结果可以预测是很慢很慢!

类似:select * from 表名 where 字段名 like ‘%关键字%’

例如:select * from article where content like ’%here%’

当关键字复杂的话,难道还 like ‘%关键字①%’ or like ‘%关键字②%’ ?

多个表关联就更复杂了......

 

1.2 搜索引擎的必要

百度、google这些搜索引擎可以肯定不是直接搜数据库的,

例如,在百度搜索“spring boot spring的区别”

从结果可以看出,百度搜索具备以下明显特点:

1、即使在相关结果数量接近500万时,也能快速得出结果。

2、搜索的结果不仅仅局限于完整的“spring boot spring”这一短语,而是将此短语拆分成,“spring”,“springboot ”,“的区别”,“spring区别”等关键字。

3、对拆分后的搜索关键字进行标红显示。

4、…

有没有发现这里处理多个单词的搜索,单词在这里是可以分开搜索的,

问题:上述功能,使用大家以前学过的数据库搜索能够方便实现吗?数据库sql可能实现吗?

 

1.3 搜索索引原理

问题的结果,是建立索引,著名的数据库就是oracle,就是建立大量的索引提高搜索速度。

在实际中,我们可以对数据库中原始的数据结构(左图),在业务空闲时事先根据左图内容,创建新的倒排索引结构的数据区域(右图)。

用户有查询需求时,先访问倒排索引数据区域(右图),得出文档id后,通过文档id即可快速,准确的通过左图找到具体的文档内容。

what the mean?

就是文档内容有5个,5个数据

我们把数据的内容拆开一个个单词然后记录起对应的索引id

然后你输入匹配单词的时候,快速找到单词对应的列表id集合返回给你

这一过程,可以通过我们自己写程序来实现,也可以借用已经抽象出来的通用开源技术来实现,例如Lucene,

分布式服务的话solr、elasticsearch。

 

2.Lucene、Solr、Elasticsearch关系

Lucene:底层的API,工具包

Solr:基于Lucene开发的企业级的搜索引擎产品

Elasticsearch:基于Lucene开发的企业级的搜索引擎产品

 

2.1 Lucene

Lucene是一套用于全文检索和搜寻的开源程序库,由Apache软件基金会支持和提供

Lucene提供了一个简单却强大的应用程序接口(API),能够做全文索引和搜寻,在Java开发环境里Lucene是一个成熟的免费开放源代码工具

ps:全文检索意思就是对于文章中的每一个单词都建立索引,然后对这些单词索引进行排序,当查询时根据事先建立的索引进行查找。

 

2.2 关于Elasticsearch 与 Solr 的比较总结

  • Solr 利用 Zookeeper 进行分布式管理,而 Elasticsearch 自身带有分布式协调管理功能;
  • Solr 支持更多格式的数据,而 Elasticsearch 仅支持json文件格式;
  • Solr 官方提供的功能更多,而 Elasticsearch 本身更注重于核心功能,高级功能多有第三方插件提供;
  • Solr 在传统的搜索应用中表现好于 Elasticsearch,但在处理实时搜索应用时效率明显低于 Elasticsearch。
  • Solr 是传统搜索应用的有力解决方案,但 Elasticsearch 更适用于新兴的实时搜索应用。

 

3.创建索引

 

文档Document:数据库中一条具体的记录

字段Field:数据库中的每个字段

目录Directory:物理存储位置

写出器的配置对象:需要分词器Analyer和lucene的版本(lucene的大版本每次更新都有很大的变化

 

3.1 文档Document

其实这个很理解相当于数据库里一条数据。

Id(主键)title(标题)content(内容)author(作者)createTime(创作时间)
1Lucene你好Lucene的探索大数据课堂,跟我喊123456789....Joker2019-9-19 00:00:00

这一个整个id为1的数据就是Document

 

3.2 字段Field

一个Document中可以有很多个不同的字段,每一个字段都是一个Field类的对象。

一个Document中的字段其类型是不确定的,因此Field类就提供了各种不同的子类,来对应这些不同类型的字段。

这些子类有一些不同的特性:

1)DoubleField、FloatField、IntField、LongField、StringField、TextField这些子类一定会被创建索引,但是不会被分词,而且不一定会被存储到文档列表。要通过构造函数中的参数Store来指定:如果Store.YES代表存储,Store.NO代表不存储

2)TextField即创建索引,又会被分词。(多用于内容)

StringField会创建索引,但是不会被分词。(多用于主键)

3)StoreField一定会被存储,但是不一定创建索引(多用于不想再去数据库拿的字段,或者一些重要字段,不想再去找数据库显示的,也有用于记录表名用于知道是哪张表的数据)

如果不分词,会造成整个字段作为一个词条,除非用户完全匹配,否则搜索不到:

// 创建一个存储对象
Document doc = new Document();
// 添加字段
doc.add(new StringField("id", id, Field.Store.YES));
doc.add(new TextField("title", title, Field.Store.YES));
doc.add(new TextField("content", content, Field.Store.YES));

问题1:如何确定一个字段是否需要存储?

如果一个字段要显示到最终的结果中,那么一定要存储,否则就不存储

问题2:如何确定一个字段是否需要创建索引?

如果要根据这个字段进行搜索,那么这个字段就必须创建索引。

问题3:如何确定一个字段是否需要分词?

前提是这个字段首先要创建索引。然后如果这个字段的值是不可分割的,那么就不需要分词。例如:ID

 

3.3 目录Directory

指定索引要存储的位置,就是索引文件放去哪里存放

FSDirectory:文件系统目录,会把索引库指向本地磁盘。

特点:速度略慢,但是比较安全,也方便迁移

RAMDirectory:内存目录,会把索引库保存在内存。

特点:速度快,但是不安全

3.4 Analyzer

lucene提供很多分词算法,可以把文档中的数据按照算法分词

但是这些分词器,并没有合适的中文分词器,因此一般我们会用第三方提供的分词器:

庖丁,IK-Analyzer、mmseg4j(MMSegAnalyzer)等

说一下mmseg4j

<dependency>
	<groupId>com.chenlb.mmseg4j</groupId>
	<artifactId>mmseg4j-core</artifactId>
	<version>1.10.0</version>
</dependency>

这个是支持扩展词典和停用词典的

代码是支持的

词典可以参考源码放在data文件夹下的dic文件

源码:https://github.com/chenlb/mmseg4j-core

可以看到分词的写法其实很简单,我们按需增加就好

3.5 IndexWriterConfig(索引写出器配置类)

设置配置信息

String direct = "D:/test/luceneData";
Directory directory = FSDirectory.open(Paths.get(direct));	
IndexWriterConfig iwConfig = new IndexWriterConfig(analyzer);
// 设置创建索引模式(在原来的索引的基础上创建或新增)
iwConfig.setOpenMode(OpenMode.CREATE_OR_APPEND);

//添加索引,在之前的索引基础上追加 
//iwConfig.setOpenMode(OpenMode.APPEND); 
//创建索引,删除之前的索引
//iwConfig.setOpenMode(OpenMode.CREATE);

3.6 IndexWriter(索引写出器类)

当分词算法弄好,配置也好了,就是要把索引找地方记录保存起来了

/**
	 * 创建索引并进行存储
	 * 
	 * @param title
	 * @param content
	 */
	public static void createIndex(String id,String title, String content) throws IOException {
		Directory directory = FSDirectory.open(Paths.get(direct));
		
		IndexWriterConfig iwConfig = new IndexWriterConfig(analyzer);
		// 设置创建索引模式(在原来的索引的基础上创建或新增)
		iwConfig.setOpenMode(OpenMode.CREATE_OR_APPEND);

		IndexWriter iwriter = new IndexWriter(directory, iwConfig);
		// 创建一个存储对象
		Document doc = new Document();
		// 添加字段
		doc.add(new StringField("id", id, Field.Store.YES));
		doc.add(new TextField("title", title, Field.Store.YES));
		doc.add(new TextField("content", content, Field.Store.YES));
		// 新添加一个doc对象
		iwriter.addDocument(doc);
		// 创建的索引数目
		int numDocs = iwriter.numRamDocs();
		System.out.println("共索引了: " + numDocs + " 个对象");
		// 提交事务
		iwriter.commit();
		// 关闭事务
		iwriter.close();
	}

好了,有人问为什么有事务?

这个跟数据库一样,中间有一个报错了,可以回滚不保存。

 

4.大数据查询(核心)

好了,在这之前我们做了那么多操作,为了就是这个时刻把我们想要的结果给拿出来

对应的

查询解析器:QueryParser(单字段解析器)、MultiFieldQueryParser(多字段的查询解析器)

查询对象Query,要查询的关键词信息:TermQuery(词条查询)、WildcardQuery(通配符查询)、FuzzyQuery(模糊查询)、NumericRangeQuery(数值范围查询)、BooleanQuery(组合查询,这个大数据常用,将前面都可以一起用,想象一下链家房产的搜索数据有多个条件单价啊区域啊学区啊等等)

索引搜索对象IndexSearch(执行搜索功能)

查询结果对象TopDocs(用于分页)

4.1 查询

/**
	 * 查询方法
	 * 
	 * @param text
	 * @return
	 * @throws IOException
	 */
	public static List<Map<String, Object>> search(String text)throws Exception {
 
		// 得到存放索引的位置
		Directory directory = FSDirectory.open(Paths.get(direct));
		DirectoryReader ireader = DirectoryReader.open(directory);
		IndexSearcher searcher = new IndexSearcher(ireader);
		// 在content中进行搜索
		QueryParser parser = new QueryParser("content", analyzer);
		// 搜索含有text的内容
		Query query = parser.parse(text);
		// 搜索标题和显示条数(10)
		TopDocs tds = searcher.search(query, 10);
        // 获取总条数
        System.out.println("本次搜索共找到" + tds.totalHits + "条数据");
		List<Map<String, Object>> list = new ArrayList<Map<String, Object>>();
		Map<String, Object> map = null;
 
		// 在内容中查获找
		for (ScoreDoc sd : tds.scoreDocs) {
			// 获取title
			String id = searcher.doc(sd.doc).get("id");
			// 获取title
			String title = searcher.doc(sd.doc).get("title");
			// 获取content
			String content = searcher.doc(sd.doc).get("content");
			// 内容添加高亮
			QueryParser qp = new QueryParser("content", analyzer);
			// 将匹配到的text添加高亮处理
			Query q = qp.parse(text);
			String HighlightContent = displayHtmlHighlight(q, "content", content);
 
			map = new HashMap<String, Object>();
			map.put("id", id);
			map.put("title", title);
			map.put("content", content);
			map.put("highlight", HighlightContent);
			list.add(map);
		}
 
		return list;
	}
	
	/**
	 * 高亮处理
	 * 
	 * @param query
	 * @param fieldName
	 * @param fieldContent
	 * @return
	 */
	public static String displayHtmlHighlight(Query query, String fieldName, String fieldContent)
			throws IOException, InvalidTokenOffsetsException {
		// 设置高亮标签,可以自定义,这里我用html将其显示为红色
		SimpleHTMLFormatter formatter = new SimpleHTMLFormatter("<font color='red'>", "</font>");
		// 评分
		QueryScorer scorer = new QueryScorer(query);
		// 创建Fragmenter
		Fragmenter fragmenter = new SimpleSpanFragmenter(scorer);
		// 高亮分析器
		Highlighter highlight = new Highlighter(formatter, scorer);
		highlight.setTextFragmenter(fragmenter);
		// 调用高亮方法
		String str = highlight.getBestFragment(analyzer, fieldName, fieldContent);
		return str;
	}

4.2 关于排序

 // 创建排序对象,需要排序字段SortField,参数:字段的名称、字段的类型、是否反转如果是false,升序。true降序
Sort sort = new Sort(new SortField("id", SortField.Type.LONG, true));
// 搜索
TopDocs topDocs = searcher.search(query, 10,sort);

4.3 分页

// 实际上Lucene本身不支持分页。因此我们需要自己进行逻辑分页。我们要准备分页参数:
int pageSize = 2;// 每页条数
int pageNum = 3;// 当前页码
int start = (pageNum - 1) * pageSize;// 当前页的起始条数
int end = start + pageSize;// 当前页的结束条数(不能包含)
// 搜索数据,查询0~end条
TopDocs topDocs = searcher.search(query, end);
System.out.println("本次搜索共" + topDocs.totalHits + "条数据");


ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (int i = start; i < end; i++) {
	ScoreDoc scoreDoc = scoreDocs[i];
	// 获取文档编号
	int docID = scoreDoc.doc;
	Document doc = reader.document(docID);
	System.out.println("id: " + doc.get("id"));
	System.out.println("title: " + doc.get("title"));
}

以上就是lucene的使用

补充一下,这里使用的lucene版本

<lucene.version>7.7.2</lucene.version>

<dependency>
	<groupId>org.apache.lucene</groupId>
	<artifactId>lucene-core</artifactId>
	<version>${lucene.version}</version>
</dependency>
<dependency>
	<groupId>org.apache.lucene</groupId>
	<artifactId>lucene-queryparser</artifactId>
	<version>${lucene.version}</version>
</dependency>
<dependency>
	<groupId>org.apache.lucene</groupId>
	<artifactId>lucene-analyzers-common</artifactId>
	<version>${lucene.version}</version>
</dependency>
<dependency>
	<groupId>org.apache.lucene</groupId>
	<artifactId>lucene-queries</artifactId>
	<version>${lucene.version}</version>
</dependency>
<dependency>
    <groupId>org.apache.lucene</groupId>
	<artifactId>lucene-highlighter</artifactId>
	<version>${lucene.version}</version>
</dependency>

最后,谢谢大家坚持到这里,代码路上共勉之~!~

下篇会说说solr和elasticsearch的使用和选择

  • 3
    点赞
  • 1
    评论
  • 9
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

评论 1 您还未登录,请先 登录 后发表或查看评论
©️2022 CSDN 皮肤主题:编程工作室 设计师:CSDN官方博客 返回首页

打赏作者

Joker_Ye

你的鼓励是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值