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(创作时间) |
1 | Lucene你好 | Lucene的探索大数据课堂,跟我喊123456789.... | Joker | 2019-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的使用和选择