大数据开发工程师-ES+HBase实现仿百度搜索引擎-1


ES+HBase实现仿百度搜索引擎-1

1 企业中快速复杂查询痛点分析

大数据领域海量数据存储现状

1
首先来分析一下目前大数据领域中的一些数据存储系统:HDFS、HBase、Kudu

1
2
3
4
5
-HDFS:是一个分布式文件系统,适合文本类型数据存储,不支持修改删除,适合一次写入,多次读取的场景。借助于Hive可以实现基于SQL的海量数据分析。HDFS在实际工作中是最常见的。

-HBase:是一个NoSQL类型的数据库,支持海量数据的增删改查,基于Rowkey查询效率高,针对普通字段查询效率非常低。HBase不支持传统的SQL语法,不适合做数据分析。在企业中的应用场景有限,仅适用于有修改删除需求的场景。

-Kudu:介于HDFS和HBase之间,专门为了应对快速变化的数据进行快速的分析,既支持增删改查,也支持基于SQL的海量数据分析。它定位于OLAP和少量的OLTP场景,如果有大量的随机查询,还是建议使用HBase最为合适。Kudu没有HDFS批处理速度快,也没有HBase随机读写能力强,所以它属于一个折中的方案,应用场景也是有限的。

大数据领域常见的SQL分析引擎

1
目前大数据领域常见的SQL分析引擎有以下这些:Hive、Impala、Kylin。

image-20230612091102908

1
2
3
4
5
Hive:Hive是一个支持SQL的数据查询引擎,适合通过SQL对HDFS中的海量文本数据进行分析,不适合单条数据的随机查询,因为每一次查询底层都需要执行MapReduce任务,单条数据查询效率比较低。

Impala:Impala是类似于Hive的一个SQL查询引擎,由于Hive底层需要走MapReduce,效率较低,所以Impala自己实现了底层的计算引擎,充分利用了内存的优势,计算性能可以达到Hive的几十倍。虽然性能提高了,但是Impala也不适合快速复杂查询,因为每次查询还是需要加载表中的所有数据,然后过滤出满足条件的数据。

Kylin:kylin是一个可以支持海量数据亚秒级查询的引擎,它最核心的思想就是预聚合,kylin中可以提前创建一些Cube,其实就是一些数据聚合规则,可以在每天凌晨的时候对数据按照指定的规则进行聚合,这样后期在使用的时候,就可以直接查询聚合之后的数据,提高查询效率,所以Kylin适合一些查询条件固定的需求,不适合灵活多变的查询需求。
1
2
3
目前大数据领域中这几个数据分析引擎都是不擅长快速复杂查询的,因为他们侧重的其实都是数据分析,而不是快速复杂查询。

想要实现海量数据的快速复杂查询,其实最常见的解决方案是利用全文检索引擎,因为全文检索引擎工具会对数据分词建立索引,可以支持快速复杂查询。

目前常见的全文检索引擎

1
目前常见的全文检索引擎主要是这几个:Lucene、Solr、Elasticsearch

image-20230612091235678

1
2
3
4
5
lucene:Lucene是Java家族中最为出名的一个开源搜索引擎,缺点是使用起来比较繁琐,并且不支持分布式,无法应用在海量数据场景下。

Solr:Solr是对Lucene进行了封装,并且提供了界面操作,使用起来更加友好,从solr4.0版本开始支持分布式,也就是支持集群架构。

Elasticsearch:Elasticsearch也是对Lucene进行了封装,它从一开始就支持分布式,专门为大数据而生,所以在大数据领域使用的还是比较多的。

海量数据存储+快速复杂查询需求

1
2
3
假设现在遇到一个需求,企业里面有一套爬虫程序,每天都会到互联网上抓取海量的文章数据,针对这些文章数据:
1:首先要实现海量文章数据存储,支持数据更新需求
2:还需要提供针对文章数据的快速复杂查询

image-20230612091639190

1
2
3
4
5
6
7
8
在这里能不能直接使用Elasticsearch实现海量数据存储和快速复杂查询呢?
Elasticsearch最擅长的是快速复杂查询,虽然它支持分布式,也可以存储海量数据,但是这并不是它最擅长的功能,因为数据存储多了之后肯定会影响ES的性能。

所以单纯使用Elasticsearch来实现这个需求是不太合适的。

那针对海量数据存储可以考虑HDFS、HBase、Kudu这几个存储系统,首先要把HDFS排除掉,因为HDFS不支持更新操作,剩下的是HBase和Kudu,这两个都支持更新操作,但是Kudu随机查询性能是不如HBase的,所以针对存储这块考虑使用HBase。

单纯使用HBase也是无法满足需求的,HBase只有根据Rowkey查询效率才高,根据其他字段查询效率是比较差的。

海量数据存储+快速复杂查询的解决方案

1
所以这个需求只使用一个技术组件是无法完美解决的,最好的解决方案是将HBase和Elasticsearch整合到一起,利用HBase适合海量数据存储、基于Rowkey查询效率高的特性,以及Elasticsearch适合快速复杂查询的特性。

image-20230612091806749

2 仿百度搜索引擎项目架构设计

项目概览

1
首先看一下项目最终的界面效果

image-20230612091858736

项目整体架构流程

image-20230612092004096

1
2
3
4
5
6
7
1:项目的数据来源可以是通过爬虫到互联网上采集的数据,也可以是企业数据库中的内部数据
2:根据数据的来源不同,使用不同的程序将数据入库到HBase,实现海量数据存储
3:针对HBase中的数据在ES中建立索引。
4:在数据展现模块中提供仿百度搜索功能。


注意:并不是把HBase中数据的完整内容全部在ES中建立索引,只需要将检索用到的那些字段在ES中建立索引即可。例如:HBase存储的原始数据有20个字段,在ES可能只需要存储5个字段即可,具体的存储细节在后面会详细分析。

ES和HBase数据同步的三种方案

1
2
针对此项目,有一个核心功能点,如何在ES中同步对HBase中的数据建立索引?
大致有下面这几种方案:

image-20230612092710497

1
2
1:方案1,在将原始数据入库HBase的时候,同时在ES中对数据建立索引,此时可以把入库HBase和ES的代码放在一个事务中,保证HBase和ES的数据一致性。
这种方案的优点是操作方便,缺点是入库HBase和ES的代码绑定到一起了,耦合性太高,如果遇到ES出现故障,会导致入库HBase的操作也会失败,或者是ES集群压力过大的时候,会导致数据入库HBase的效率降低。

image-20230612092739473

1
2
3
2:方案2,在将原始数据入库HBase的时候,通过HBase中的协处理器实现数据同步,让协处理器监听HBase中的新增和删除操作,在协处理器内部操作ES,实现对数据建立索引的功能。
HBase中的协处理器其实类似于MySQL中的触发器。
这种方案的优点是通过协处理器可以很方便的获取到HBase中新增和变化的数据,如果入库HBase的程序是之前已经开发好的,此时不需要对之前的代码进行任何改动,影响程度比较低。缺点是过于依赖HBase了,如果后期涉及到HBase集群版本升级,无法保证协处理器功能的可用性。

image-20230612092821245

1
2
3
4
5
6
3:方案3,在将原始数据入库HBase的时候,同时在Redis中使用list数据类型模拟一个队列,存储数据的Rowkey。此时将入库HBase和Redis的操作放在一个事务里面,保证数据的一致性。然后再通过另外一个同步程序,从Redis的list队列中读取Rowkey,根据Rowkey到HBase中获取数据的详细信息,在ES中建立索引,将HBase中数据的Rowkey作为ES中数据的ID。
在这个方案里面是将入库HBase和在ES中建立索引这两个功能解耦了,借助于中间层的Redis实现的。
这种方案的缺点是把入库HBase和Redis的功能耦合在一起了,但是Redis是轻量级的,出现问题的概率是比较低的,对性能损耗也不高,所以是可以接受的。
此时就算ES出现问题,只需要在同步程序内部实现正常的异常处理即可,将建立索引失败数据的Rowkey重新添加到Redis的list列表里面即可,不会导致HBase和ES数据不一致的问题。

其实第2种方案和第3种方案都可以使用,个人推荐使用第3种方案,可控性高一些,在项目中也会使用第3种方案实现。

项目整体执行流程

1
2
接下来分析一下项目底层细节流程
如下图所示

image-20230612093116446

1
2
3
4
5
6
7
1:通过入库程序向HBase中入库数据,同时在Redis中存储数据的Rowkey。
2:从Redis中获取数据的Rowkey,根据Rowkey到HBase中查询数据的详细信息,然后在ES中建立索引。
此时我们的海量数据已经存储到HBase中,并且将需要查询的字段在ES中建立索引了。
3:用户向ES发送查询请求
4:ES返回符合条件的数据的ID,其实就是HBase中数据的Rowkey。在这里也可以根据需求额外再返回一些字段信息都是可以的。
5:当用户想要查看数据完整详细信息的时候,需要根据Rowkey到HBase中查询
6:HBase会给用户返回Rowkey对应数据的详细信息。

3 ES高级特性扩展

1
在具体开发项目之前,先来了解一下ES中的几个特性:

ES中的_source字段

image-20230612093604097

1
2
3
4
5
6
在ES中包含一个特殊的字段:_source
当我们在ES中对数据建立索引的时候,在ES底层其实会存储两份数据,一份是原始文档的内容,还有一份是对原始文档分词产生的倒排索引内容。

其中原始文档内容会存储到_source这个字段里面,咱们前面在学习JavaAPI操作ES的时候,通过search查询的结果数据,最终在解析的时候其实就是从_source字段中解析的。

_source字段中默认会包含原始文档中所有字段的内容。

ES中字段的index和store特性

1
2
3
4
5
6
7
8
我们在向ES中添加数据的时候,ES底层针对每个字段其实还会涉及到index和store这两个属性:

index:表示是否在ES中建立索引,默认为true。
store:表示是否在ES中存储,默认为false。

这里的store属性其实是Lucene中的语法,表示是否存储字段内容,ES对它做了优化,默认会使用_source字段存储原始文档所有字段的内容,这样可以提高数据解析性能,所以这里的store属性其实就不需要设置了,它的默认值就是false,表示不存储。

注意:如果这里的store也设置为true,此时ES会重复存储对应字段的内容。
1
2
3
4
5
6
7
8
下面来看一个例子:
假设我要向ES中添加一批学生数据,包括姓名、年龄、性别、家庭住址这些字段。
针对这批数据,我的需求是需要根据学生的姓名、家庭住址进行查询,最终在返回结果数据的时候,只需要把学生的姓名、年龄、性别返回过来即可,不需要返回家庭住址。

针对这个需求而言
1:需要根据学生的姓名、家庭住址进行查询,所以这两个字段必须在ES中建立索引,否则无法根据姓名和家庭住址进行查询。
2:还需要在返回结果数据的时候把学生的姓名、年龄、性别返回过来,那也就意味着姓名、年龄、性别字段需要在ES中进行存储,否则ES是无法返回这些字段内容的。
所以针对这个需求总结一下:
1
2
3
4
5
		是否建立索引	是否存储
姓名 是 是
年龄 否 是
性别 否 是
家庭住址 是 否
1
2
3
4
解释:

判断一个字段是否需要建立索引,唯一的依据就是是否需要根据这个字段进行查询,如果需要,则建立索引,否则不建立索引。
判断一个字段是否需要存储,唯一的依据就是是否需要从ES中获取这个字段的值,如果需要,则存储,否则不存储。
1
注意:在实际工作中,针对某个字段来说,需要建立索引,但是不需要存储,这种场景也是存在的,因为某一些字段内容如果比较大,并且没有必要从ES中返回,其实就没必要在ES中存储了,否则会额外占用ES的存储空间,也会影响ES的查询效率。
1
2
由于默认情况下_source字段会存储所有字段的内容,所以需要在_source字段中过滤掉不需要存储的字段。
在mapping中设置:"_source":{"excludes":["address"]}
1
2
3
针对这个案例,创建一个索引库:stuinfo

[root@bigdata01 ~] curl -H "Content-Type: application/json" -XPUT 'http://bigdata01:9200/stuinfo' -d'{"mappings":{"_source":{"excludes":["address"]},"properties":{"name":{"type":"text","index":true},"age":{"type":"integer","index":false},"sex":{"type":"text","index":false},"address":{"type":"text","index":true}}}}'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
查询mappings信息。
[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/stuinfo/_mapping?pretty'
{
"stuinfo" : {
"mappings" : {
"_source" : {
"excludes" : [
"address"
]
},
"properties" : {
"address" : {
"type" : "text"
},
"age" : {
"type" : "integer",
"index" : false
},
"name" : {
"type" : "text"
},
"sex" : {
"type" : "text",
"index" : false
}
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
向索引库中添加一条数据
curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/stuinfo/_doc/1' -d '{"name":"zs","age":20,"sex":"man","address":"bj"}'

查看这条数据,验证一下效果。
[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/stuinfo/_search?pretty'
{
......
"_source" : {
"sex" : "man",
"name" : "zs",
"age" : 20
}
}
1
2
3
4
发现返回的_source字段中确实没有address字段的内容了。

但是此时我们还是可以根据address进行查询的,因为倒排索引中有这个字段的内容。
在这里扩展一个知识点,使用RestAPI执行query查询。

image-20230612101521802

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
根据address可以查询到数据,说明address的index属性生效了

[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XGET 'http://bigdata01:9200/stuinfo/_search?pretty' -d'{"query":{"match":{"address":"bj"}}}'
{
"took" : 5,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 0.2876821,
"hits" : [
{
"_index" : "stuinfo",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.2876821,
"_source" : {
"sex" : "man",
"name" : "zs",
"age" : 20
}
}
]
}
}
1
如果我们使用sex字段进行查询,会看到报错信息,因为sex字段没有建立索引是无法作为查询字段的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XGET 'http://bigdata01:9200/stuinfo/_search?pretty' -d'{"query":{"match":{"sex":"man"}}}'    
{
"error" : {
"root_cause" : [
{
"type" : "query_shard_exception",
"reason" : "failed to create query: Cannot search on field [sex] since it is not indexed.",
"index_uuid" : "sPuh_JJNSYW33Oie4KSRcg",
"index" : "stuinfo"
}
],
"type" : "search_phase_execution_exception",
"reason" : "all shards failed",
"phase" : "query",
"grouped" : true,
"failed_shards" : [
{
"shard" : 0,
"index" : "stuinfo",
"node" : "_6ff1IjTTGm3jZ_edi6-KA",
"reason" : {
"type" : "query_shard_exception",
"reason" : "failed to create query: Cannot search on field [sex] since it is not indexed.",
"index_uuid" : "sPuh_JJNSYW33Oie4KSRcg",
"index" : "stuinfo",
"caused_by" : {
"type" : "illegal_argument_exception",
"reason" : "Cannot search on field [sex] since it is not indexed."
}
}
}
]
},
"status" : 400
}

4 开发仿百度搜索引擎项目

1
2
3
4
这个搜索引擎项目主要涉及到数据采集、数据存储、建立索引和数据展现环节。
针对一个搜索引擎项目而言,它的数据基本上都是来源于互联网上的公开数据,想要获取这些数据就需要使用爬虫工具了,目前市面上有一些爬虫产品,但是在使用的时候基本上都需要二次开发,所以企业里面都会有专门的爬虫工程师负责这个工作。
我们在开发这个搜索引擎项目的时候就不再针对爬虫数据采集模块进行扩展了,到时候我会提供一个数据接口,大家通过接口可以直接获取到一些互联网上的公开数据。
数据大致格式是这样的:

image-20230612105000321

1
2
3
4
5
6
我们在开发这个搜索引擎项目的时候会重点实现数据存储、建立索引这两个环节。

大致的开发步骤是这样的:
1:调用接口获取数据导入HBase和Redis(存储Rowkey)。
2:通过ES对HBase中的数据建立索引。
3:对接Web项目,提供页面检索功能。
1
2
3
4
5
6
在具体开发项目之前,我们首先分析一下项目中的数据和具体的查询要求,这样就知道ES中索引库的mapping应该如何设计了。
在这个搜索引擎项目中,我们需要对爬虫采集到的文章数据在ES中建立索引。

由于文章数据的核心内容主要在标题、描述和正文这3个字段中,所以在查询的时候需要用到这3个字段。
那ES在返回满足条件的数据的时候,都需要返回哪些字段呢?
参考一下百度的结果列表界面,可以发现,百度的列表页面会显示标题、描述和作者信息。

image-20230612105219912

1
2
3
4
那针对我们这里的文章数据,还有一个时间字段,其实也可以显示在列表页面中,具体显示哪些字段,可以根据工作中的具体需求来定。
在这里我们希望在列表页面显示的是文章的标题、作者、描述、时间这4个字段。
针对文章的ID,在这里直接作为ES中数据的ID,ES中的ID是必须要存储和建立索引的。
所以最终总结一下是这样的:

image-20230612105345979

1
2
3
4
5
6
文章ID:需要建立索引,并且存储,这是ES中ID字段必须要具备的特性。
标题:因为查询的时候会用到,所以需要建立索引,并且在返回结果列表信息的时候需要直接从ES中返回,所以需要存储。
作者:查询用不到,所以不需要建立索引,但是需要在返回结果列表信息的时候一块返回,所以需要存储。
描述:查询会用到,返回的结果列表信息中也有这个字段内容,所以需要建立索引,并且存储。
正文:因为查询的时候会用到,所以需要建立索引,但是在返回结果列表信息的时候不需要返回这个字段,所以不需要存储。其实还有一点很重要的原因是因为这个字段内容太长了,如果在ES中存储,会额外占用很多的存储空间,最终会影响ES的性能。
时间:查询用不到,所以不需要建立索引,但是需要在返回结果列表信息的时候一块返回,所以需要存储。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
接下来我们来手工指定一下索引库的settings和mapping参数。
由于这里的字段比较多,最好把settings和mapping信息写到一个文件中,使用起来比较方便。
创建一个文件:article.json,内容如下:

{
"settings":{
"number_of_shards":5,
"number_of_replicas":1
},
"mappings":{
"dynamic":"strict",
"_source":{"excludes":["content"]},
"properties":{
"title":{"type":"text","analyzer":"ik_max_word"},
"author":{"type":"text","index":false},
"describe":{"type":"text","analyzer":"ik_max_word"},
"content":{"type":"text","analyzer":"ik_max_word"},
"time":{"type":"date","index":false,"format":"yyyy-MM-dd HH:mm:ss"}
}
}
}
1
2
3
4
5
6
7
解释:
dynamic参数有4个选项值(后期ES中如果用于建立索引的字段多了一些未知字段的场景):

true是默认的,表示开启动态映射(自动识别)
false表示忽略没有定义的字段
strict表示遇到未知字段时抛出异常
runtime表示遇到未知字段时将它作为运行时字段,运行时字段是在ES7.11版本中增加的,运行时字段不会被索引,但是可以从_source中获取运行时字段内容,所以runtime可以适合公共字段已知,并且想兼容未知扩展字段的场景。

image-20230612132118974

1
2
3
4
5
dynamic具体选择哪个参数,就需要根据需求来定了,在这里不希望在ES中保存未知字段,所以使用strict。

将article.json上传到/data/soft/elasticsearch-7.13.4目录下
[root@bigdata01 elasticsearch-7.13.4]# ll article.json
-rw-r--r--. 1 root root 534 Apr 2 17:44 article.json

调用接口获取数据导入HBase和Redis

1
2
3
在Idea中直接打开已有项目:db_fullsearch,里面有一个web_fullsearch子model项目。
接着再创建一个子Model项目:data_manager
在子Model项目data_manager的pom.xml中添加此项目需要用到的依赖。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-client</artifactId>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
</dependencies>
1
2
3
4
5
6
7
在子Model项目data_manager中添加log4j.properties配置文件。
log4j.rootLogger=info,stdout

log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target = System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=[%p] %m%n
1
2
3
4
在子Model项目data_manager中创建package:com.imooc.core和com.imooc.utils
在com.imooc.utils包中引入工具类:HttpUtil、RedisUtil、HBaseUtil(开发)。
接着在com.imooc.core中创建类:DataImport,负责实现数据导入到HBase和Redis。
HBaseUtil代码如下:

com.imooc.utils

HBaseUtil
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
package com.imooc.utils;

import com.sun.org.apache.bcel.internal.generic.TABLESWITCH;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellUtil;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.util.Bytes;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* HBase工具类
* Created by xuwei
*/
public class HBaseUtil {
private HBaseUtil(){}

private static Connection conn = getConn();

private static Connection getConn(){
//获取hbase链接
Configuration conf = new Configuration();
//指定hbase使用的zk地址
//注意:需要在执行hbase hava代码的机器上配置zk和hbase集群的主机名和ip的映射关系
conf.set("hbase.zookeeper.quorum","bigdata01:2181");
//指定hbase在hdfs上的根目录
conf.set("hbase.rootdir","hdfs://bigdata01:9000/hbase");
//创建HBase数据库链接
Connection co = null;
try{
co = ConnectionFactory.createConnection(conf);
}catch (IOException e){
System.out.println("获取链接失败:"+e.getMessage());
}
return co;
}

/**
* 对外提供的方法
* @return
*/
public static Connection getInstance(){
return conn;
}

/**
* 创建表
* @param tableName
* @param cfs
*/
public static void createTable(String tableName,String... cfs) throws Exception {
Admin admin = conn.getAdmin();
ArrayList<ColumnFamilyDescriptor> cfArr = new ArrayList<ColumnFamilyDescriptor>();
for (String cf : cfs) {
ColumnFamilyDescriptor cfDesc = ColumnFamilyDescriptorBuilder
.newBuilder(Bytes.toBytes(cf))
.build();
cfArr.add(cfDesc);
}
TableDescriptor tableDesc = TableDescriptorBuilder
.newBuilder(TableName.valueOf(tableName))
.setColumnFamilies(cfArr)
.build();
admin.createTable(tableDesc);
admin.close();
}

/**
* 添加一个单元格(列)的数据
* @param tableName
* @param rowKey
* @param columnFamily
* @param column
* @param value
* @throws Exception
*/
public static void put2HBaseCell(String tableName,String rowKey,String columnFamily,String column,String value)throws Exception{
Table table = conn.getTable(TableName.valueOf(tableName));
Put put = new Put(Bytes.toBytes(rowKey));
put.addColumn(Bytes.toBytes(columnFamily),Bytes.toBytes(column),Bytes.toBytes(value));
table.put(put);
table.close();
}

/**
* 向hbase中添加一批数据
* @param tableName
* @param list
* @throws Exception
*/
public static void put2HBaseList(String tableName, List<Put> list)throws Exception{
Table table = conn.getTable(TableName.valueOf(tableName));
table.put(list);
table.close();
}
}
1
2
3
把这个构造方法私有化的意义是为了防止外部类创建RedisUtil的实例对象。这样可以保证RedisUtil是一个单例类,只有一个实例存在。这样可以节省内存空间,避免多个实例之间的同步问题,提高性能。😊

已收到消息. 把这个构造方法私有化的意义是为了防止外部类创建RedisUtil的实例对象。这样可以保证RedisUtil是一个单例类,只有一个实例存在。这样可以节省内存空间,避免多个实例之间的同步问题,提高性能。😊
RedisUtil
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.imooc.utils;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
* 基于Redis连接池提取Redis工具类
* Created by xuwei
*/
public class RedisUtil {
//私有化构造函数,禁止new
private RedisUtil(){}

private static JedisPool jedisPool = null;

//获取连接
public static synchronized Jedis getJedis(){
if(jedisPool==null){
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxIdle(10);
poolConfig.setMaxTotal(100);
poolConfig.setMaxWaitMillis(2000);
poolConfig.setTestOnBorrow(true);
jedisPool = new JedisPool(poolConfig, "192.168.182.103", 6379);
}
return jedisPool.getResource();
}

//向连接池返回连接
public static void returnResource(Jedis jedis){
jedis.close();
}
}

com.imooc.core

DataImport
1
DataImport代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package com.imooc.core;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.imooc.utils.HBaseUtil;
import com.imooc.utils.HttpUtil;
import com.imooc.utils.RedisUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;

/**
* 通过接口获取文章数据,入库HBase和Redis(Rowkey)
*
* 注意:HBase建表语句 create 'article','info'
* Created by xuwei
*/
public class DataImport {
private final static Logger logger = LoggerFactory.getLogger(DataImport.class);

public static void main(String[] args) {
//通过接口获取文章数据
String dataUrl = "http://data.xuwei.tech/a1/wz1";
JSONObject paramObj = new JSONObject();
paramObj.put("code","imooc");//校验码
paramObj.put("num",100);//数据条数,默认返回100条,最大支持返回1000条
JSONObject dataObj = HttpUtil.doPost(dataUrl, paramObj);
boolean flag = dataObj.containsKey("error");
if(!flag){
JSONArray resArr = dataObj.getJSONArray("data");
for(int i=0;i<resArr.size();i++){
JSONObject jsonObj = resArr.getJSONObject(i);
//System.out.println(jsonObj.toJSONString());
//文章ID作为HBase的Rowkey和ES的ID
String id = jsonObj.getString("id");
String title = jsonObj.getString("title");
String author = jsonObj.getString("author");
String describe = jsonObj.getString("describe");
String content = jsonObj.getString("content");
String time = jsonObj.getString("time");
Jedis jedis = null;
try{
//将数据入库到HBase
String tableName = "article";
String cf = "info";
HBaseUtil.put2HBaseCell(tableName,id,cf,"title",title);
HBaseUtil.put2HBaseCell(tableName,id,cf,"author",author);
HBaseUtil.put2HBaseCell(tableName,id,cf,"describe",describe);
HBaseUtil.put2HBaseCell(tableName,id,cf,"content",content);
HBaseUtil.put2HBaseCell(tableName,id,cf,"time",time);
//将Rowkey保存到Redis中
jedis = RedisUtil.getJedis();
jedis.lpush("l_article_ids",id);
}catch (Exception e){
//注意:由于hbase的put操作属于幂等操作,多次操作,对最终的结果没有影响,所以不需要额外处理
logger.error("数据添加失败:"+e.getMessage());
}finally {
//向连接池返回连接
if(jedis!=null){
RedisUtil.returnResource(jedis);
}
}

}

}else{
logger.error("获取文章数据失败:"+dataObj.toJSONString());
}
}
}

通过ES对HBase中的数据建立索引

1
2
3
4
5
在com.imooc.core中创建类:DataIndex,负责实现在ES中对数据建立索引。
在开发DataIndex代码的时候,需要向Es中写入数据,所以最好封装一个EsUtil工具类。
同时需要对HBaseUtil工具类进行完善,增加一个getFromHBase方法,负责从HBase中获取数据

EsUtil工具类代码如下:

com.imooc.utils

EsUtil
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package com.imooc.utils;

import org.apache.http.HttpHost;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
import org.apache.http.impl.nio.reactor.IOReactorConfig;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;

import java.io.IOException;
import java.util.Map;

/**
* ES工具类
* Created by xuwei
*/
public class EsUtil {
private EsUtil(){}
private static RestHighLevelClient client;
static{
//获取RestClient连接
//注意:高级别客户端其实是对低级别客户端的代码进行了封装,所以连接池使用的是低级别客户端中的连接池
client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("bigdata01",9200,"http"),
new HttpHost("bigdata01",9200,"http"),
new HttpHost("bigdata01",9200,"http"))
.setHttpClientConfigCallback(new RestClientBuilder.HttpClientConfigCallback() {
public HttpAsyncClientBuilder customizeHttpClient(HttpAsyncClientBuilder httpClientBuilder) {
return httpClientBuilder.setDefaultIOReactorConfig(
IOReactorConfig.custom()
//设置线程池中线程的数量,默认是1个,建议设置为和客户端机器可用CPU数量一致
.setIoThreadCount(1)
.build());
}
}));
}

/**
* 获取客户端
* @return
*/
public static RestHighLevelClient getRestClient(){
return client;
}

/**
* 关闭客户端
* 注意:调用高级别客户单的close方法时,会将低级别客户端创建的连接池整个关闭掉,最终导致client无法继续使用
* 所以正常是用不到这个close方法的,只有在程序结束的时候才需要调用
* @throws IOException
*/
public static void closeRestClient()throws IOException {
client.close();
}

/**
* 建立索引
* @param index
* @param id
* @param map
* @throws IOException
*/
public static void addIndex(String index, String id, Map<String,String> map)throws IOException{
IndexRequest request = new IndexRequest(index);
request.id(id);
request.source(map);
//执行
client.index(request, RequestOptions.DEFAULT);
}
}
HBaseUtil
1
HBaseUtil工具类代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
package com.imooc.utils;

import com.sun.org.apache.bcel.internal.generic.TABLESWITCH;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellUtil;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.util.Bytes;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* HBase工具类
* Created by xuwei
*/
public class HBaseUtil {
private HBaseUtil(){}

private static Connection conn = getConn();

private static Connection getConn(){
//获取hbase链接
Configuration conf = new Configuration();
//指定hbase使用的zk地址
//注意:需要在执行hbase hava代码的机器上配置zk和hbase集群的主机名和ip的映射关系
conf.set("hbase.zookeeper.quorum","bigdata01:2181");
//指定hbase在hdfs上的根目录
conf.set("hbase.rootdir","hdfs://bigdata01:9000/hbase");
//创建HBase数据库链接
Connection co = null;
try{
co = ConnectionFactory.createConnection(conf);
}catch (IOException e){
System.out.println("获取链接失败:"+e.getMessage());
}
return co;
}

/**
* 对外提供的方法
* @return
*/
public static Connection getInstance(){
return conn;
}

/**
* 创建表
* @param tableName
* @param cfs
*/
public static void createTable(String tableName,String... cfs) throws Exception {
Admin admin = conn.getAdmin();
ArrayList<ColumnFamilyDescriptor> cfArr = new ArrayList<ColumnFamilyDescriptor>();
for (String cf : cfs) {
ColumnFamilyDescriptor cfDesc = ColumnFamilyDescriptorBuilder
.newBuilder(Bytes.toBytes(cf))
.build();
cfArr.add(cfDesc);
}
TableDescriptor tableDesc = TableDescriptorBuilder
.newBuilder(TableName.valueOf(tableName))
.setColumnFamilies(cfArr)
.build();
admin.createTable(tableDesc);
admin.close();
}

/**
* 添加一个单元格(列)的数据
* @param tableName
* @param rowKey
* @param columnFamily
* @param column
* @param value
* @throws Exception
*/
public static void put2HBaseCell(String tableName,String rowKey,String columnFamily,String column,String value)throws Exception{
Table table = conn.getTable(TableName.valueOf(tableName));
Put put = new Put(Bytes.toBytes(rowKey));
put.addColumn(Bytes.toBytes(columnFamily),Bytes.toBytes(column),Bytes.toBytes(value));
table.put(put);
table.close();
}

/**
* 向hbase中添加一批数据
* @param tableName
* @param list
* @throws Exception
*/
public static void put2HBaseList(String tableName, List<Put> list)throws Exception{
Table table = conn.getTable(TableName.valueOf(tableName));
table.put(list);
table.close();
}

/**
* 根据Rowkey获取数据
* @param tableName
* @param rowKey
* @return
* @throws IOException
*/
public static Map<String,String> getFromHBase(String tableName,String rowKey)throws IOException{
Table table = conn.getTable(TableName.valueOf(tableName));
Get get = new Get(Bytes.toBytes(rowKey));
Result result = table.get(get);
List<Cell> cells = result.listCells();
HashMap<String, String> resMap = new HashMap<String, String>();
for (Cell cell: cells) {
//列
byte[] column_bytes = CellUtil.cloneQualifier(cell);
//值
byte[] value_bytes = CellUtil.cloneValue(cell);
resMap.put(new String(column_bytes),new String(value_bytes));
}
return resMap;
}
}

com.imooc.core

DataIndex
1
DataIndex代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package com.imooc.core;

import com.imooc.utils.EsUtil;
import com.imooc.utils.HBaseUtil;
import com.imooc.utils.RedisUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;

import java.util.List;
import java.util.Map;

/**
* 在ES中对HBase中的数据建立索引
* Created by xuwei
*/
public class DataIndex {
private final static Logger logger = LoggerFactory.getLogger(DataIndex.class);
public static void main(String[] args) {
List<String> rowKeyList = null;
Jedis jedis = null;
try {
//1:首先从Redis的列表中获取Rowkey
jedis = RedisUtil.getJedis();
//brpop如果获取到了数据,返回的list里面有两列,第一列是key的名称,第二列是具体的数据
rowKeyList = jedis.brpop(3, "l_article_ids");
while (rowKeyList != null) {
String rowKey = rowKeyList.get(1);
//2:根据Rowkey到HBase中获取数据的详细信息
Map<String, String> map = HBaseUtil.getFromHBase("article", rowKey);
//3:在ES中对数据建立索引
EsUtil.addIndex("article",rowKey,map);

//循环从Redis的列表中获取Rowkey
rowKeyList = jedis.brpop(3, "l_article_ids");
}
}catch (Exception e){
logger.error("数据建立索引失败:"+e.getMessage());
//在这里可以考虑把获取出来的rowKey再push到Redis中,这样可以保证数据不丢
if(rowKeyList!=null){
jedis.rpush("l_article_ids",rowKeyList.get(1));
}
}finally {
//向连接池返回连接
if(jedis!=null){
RedisUtil.returnResource(jedis);
}
//注意:确认ES连接不再使用了再关闭连接(当知识运行一次,就可以要下面的代码;如果要一直运行()相当于try外层再加一个while(True),然后后面运行完了sleep一会),就要慎重考虑下面的代码该如何编写),否则会导致client无法继续使用
try{
EsUtil.closeRestClient();
}catch (Exception e){
logger.error("ES连接关闭失败:"+e.getMessage());
}
}
}
}

对接Web项目,提供页面检索功能

1
2
完善web_fullsearch项目中和大数据相关的代码。
最核心的代码就是EsUtil中的search方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/**
* 全文检索功能
* @param key
* @param index
* @param start
* @param row
* @return
* @throws IOException
*/
public static Map<String, Object> search(String key, String index, int start, int row) throws IOException {
SearchRequest searchRequest = new SearchRequest();
//指定索引库,支持指定一个或者多个,也支持通配符
searchRequest.indices(index);

//指定searchType
searchRequest.searchType(SearchType.DFS_QUERY_THEN_FETCH);

//组装查询条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//如果传递了搜索参数,则拼接查询条件
if(StringUtils.isNotBlank(key)){
searchSourceBuilder.query(QueryBuilders.multiMatchQuery(key,"title","describe","content"));
}
//分页
searchSourceBuilder.from(start);
searchSourceBuilder.size(row);

//高亮
//设置高亮字段
HighlightBuilder highlightBuilder = new HighlightBuilder()
.field("title")
.field("describe");//支持多个高亮字段
//设置高亮字段的前缀和后缀内容
highlightBuilder.preTags("<font color='red'>");
highlightBuilder.postTags("</font>");
searchSourceBuilder.highlighter(highlightBuilder);

//指定查询条件
searchRequest.source(searchSourceBuilder);

//执行查询操作
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
//存储返回给页面的数据
Map<String, Object> map = new HashMap<String, Object>();
//获取查询返回的结果
SearchHits hits = searchResponse.getHits();
//获取数据总量
long numHits = hits.getTotalHits().value;
map.put("count",numHits);
ArrayList<Article> arrayList = new ArrayList<>();
//获取具体内容
SearchHit[] searchHits = hits.getHits();
//迭代解析具体内容
for (SearchHit hit: searchHits) {
Map<String, Object> sourceAsMap = hit.getSourceAsMap();
String id = hit.getId();
String title = sourceAsMap.get("title").toString();
String author = sourceAsMap.get("author").toString();
String describe = sourceAsMap.get("describe").toString();
String time = sourceAsMap.get("time").toString();

//获取高亮字段内容
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
//获取title字段的高亮内容
HighlightField highlightField = highlightFields.get("title");
if(highlightField!=null){
Text[] fragments = highlightField.getFragments();
title = "";
for (Text text : fragments) {
title += text;
}
}
//获取describe字段的高亮内容
HighlightField highlightField2 = highlightFields.get("describe");
if(highlightField2!=null){
Text[] fragments = highlightField2.fragments();
describe = "";
for (Text text : fragments) {
describe += text;
}
}
//把文章信息封装到Article对象中
Article article = new Article();
article.setId(id);
article.setTitle(title);
article.setAuthor(author);
article.setDescribe(describe);
article.setTime(time);
//最后再把拼装好的article添加到list对象汇总
arrayList.add(article);
}
map.put("dataList",arrayList);
return map;
}

从0~1运行项目

1
2
3
首先要确保Hadoop、Zookeeper、HBase、Redis、Elasticsearch这些服务都是正常的。
1:首先在HBase中创建表:article
hbase(main):001:0> create 'article','info'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
2:在ES中创建索引库:article,通过article.json文件指定索引库的settings和mapping。
[root@bigdata01 elasticsearch-7.13.4]# curl -H "Content-Type: application/json" -XPUT 'http://bigdata01:9200/article' -d @article.json

确认一下索引库article的mapping信息:
[root@bigdata01 elasticsearch-7.13.4]# curl -XGET 'http://bigdata01:9200/article/_mapping?pretty'
{
"article" : {
"mappings" : {
"dynamic" : "strict",
"_source" : {
"excludes" : [
"content"
]
},
"properties" : {
"author" : {
"type" : "text",
"index" : false
},
"content" : {
"type" : "text",
"analyzer" : "ik_max_word"
},
"describe" : {
"type" : "text",
"analyzer" : "ik_max_word"
},
"time" : {
"type" : "date",
"index" : false,
"format" : "yyyy-MM-dd HH:mm:ss"
},
"title" : {
"type" : "text",
"analyzer" : "ik_max_word"
}
}
}
}
}
1
注意:需要确认ES集群中已经集成了ik分词器,否则这里执行会报错。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
3:在本地执行data_manager项目中的DataImport代码,将数据导入到HBase和Redis。
修改项目中用到的HBaseUtil和RedisUtil工具类中的节点信息。

注意:DataImport代码执行需要消耗一段时间。

到HBase中验证数据:
hbase(main):003:0> count 'article'
100 row(s)
Took 0.2357 seconds
=> 100

到Redis中验证数据:
[root@bigdata04 redis-5.0.9]# redis-cli
127.0.0.1:6379> lrange l_article_ids 0 -1
1) "0e52f3fb-50a7-43e6-ba1a-8f862464436e"
2) "0e0fbb25-421c-4419-9ccc-bbfcd7bc783f"
3) "0e0cbf00-d63c-4c1f-aa2e-9f96a5826029"
4) "0ddaccc1-af4c-47b1-b526-5a32b2d68aa5"
5) "0db74ee1-6243-4c3e-ba01-2eacba76188d"
6) "0d9eae28-b614-4347-9126-ad0ee7519ef1"
7) "0d7bd4de-159e-46ef-a75c-5d88de612def"
8) "0d5aeec8-3aa4-4fa5-b7eb-1ba0d0f2d715"
9) "0d286033-dcad-4514-85e1-2ae052ff531c"
10) "0d239fca-c524-47ab-be9a-05133ea57a4f"
11) "0d20beb1-a5c5-4638-99c5-9f69084b44c8"
12) "0cd0ece6-9d61-4fda-bbcd-a9d9f0d7ff70"
13) "0cc57b16-fbe6-488e-b080-dab0f0a71930"
14) "0cb28936-e83f-46e9-8d36-70ae77018fdf"
15) "0c85c890-57de-40cb-b7f6-8615eb1896fe"
16) "0c30950e-8fb0-4212-ae13-e8e659515ff3"
17) "0c2c9dca-b0db-42bc-aa6c-ad180501169d"
18) "0bfdef9f-8ba9-4c2d-95e4-74284cf07500"
19) "0bf5aef2-9ac4-4550-80f5-be862a04deba"
20) "0be8351d-dd82-4e43-92c7-cb8819421b25"
21) "0bbc79e8-9ab1-44c1-beec-ef2b22c48cfe"
22) "0baf900c-2812-4bdf-8188-962ac9d17f23"
23) "0ba2b80b-0b43-4916-9d04-8d67aa4e95b2"
24) "0b672fc3-0737-467b-b32c-b888de1eaab6"
25) "0b3dc9c2-96b6-4340-8af3-e07c8bac0ba8"
26) "0b0161f5-d1d4-4109-8f12-a4b47dbff0e5"
27) "0a088500-f708-470a-bda1-9e5a714c0b81"
28) "09e8bd66-17e0-4e28-b804-a67df7312f90"
29) "09d6e535-4f39-45ba-9857-c6ab01fe8823"
30) "09a76b9e-030c-465b-8c80-b88fe5bbac16"
31) "09a09479-56c1-4fd3-8893-3e4d307951b9"
32) "09193761-e549-48ee-86fb-89295094f1e4"
33) "090b766d-9f8e-4cb9-9a5f-b8c3373a9eb6"
34) "09089c93-9848-4244-9eff-9034d434865b"
35) "09036b9f-8b67-4275-837a-b884c8945a31"
36) "08d8762e-dc47-49f5-ad14-de52f7fbb04e"
37) "08839fde-1984-4739-bac2-f854989cdc6c"
38) "0872be3a-2545-4a98-ba33-6ec148999130"
39) "0862af37-2047-470d-a8b8-c57740a0df8b"
40) "085f3acc-1f92-4dd2-b516-d0991b660680"
41) "08210f1a-f553-4d3c-a09f-420dc8e6fe8c"
42) "080639ea-ee70-47ff-b7d1-de75eb1e1196"
43) "07ef53b0-9179-416e-bf8b-7720079fc87a"
44) "07d79c1d-ceaa-4cd0-a3be-96186037b0c1"
45) "07b71e20-a4b3-4f9d-b7c6-6db163cbabd1"
46) "07928b77-a31e-4646-aff0-79ab61f8b605"
47) "076077f1-8324-4ef3-9e8f-66c806644db6"
48) "074a36a1-c7b4-4d1a-b8a1-3a01d0c1a2ec"
49) "0734a9d3-4aa6-4206-aa12-9e26e67f0ef2"
50) "072b6883-3258-4cff-8e90-77d4f011acfe"
51) "06d75712-1fac-4a19-927f-f4c13ceba899"
52) "06d498d1-97cd-4384-8ec1-82e9a1e0070b"
53) "06beaece-c734-4eba-bcf7-64a9e43d6c55"
54) "0682b625-f247-4c70-a628-4919946ff588"
55) "0659a1c3-2f34-4342-827b-7c867a3394e3"
56) "0647046c-2129-4c16-83f9-7857e9da6e2c"
57) "0643961e-74ea-46d2-8da8-02309c6cc489"
58) "061776ad-5742-4fcc-b997-9a25f5b6a4e0"
59) "05ec598f-4b02-448f-88b6-f17f3891bd3f"
60) "058ca914-aa70-4cf5-b69f-852e03729b52"
61) "0504f099-995c-475d-97e8-f4d691673cbb"
62) "04fce8dc-e27c-4f1c-bb4c-34597fad1810"
63) "04b00310-c09a-44e2-9895-1fd22cd14148"
64) "04997210-1552-4dcb-b13d-16e79ced1320"
65) "04943196-717d-4594-bc83-a76189187c2e"
66) "04940647-ff85-497d-b2fd-44f43600a569"
67) "04755a1e-32b2-42b7-933e-405c8143eb8f"
68) "047139c9-c351-4275-83be-c96b8105811b"
69) "0451dbce-f8da-4862-b563-bee0bc5abdc6"
70) "042bed42-fbb9-4c51-a424-089f579697e0"
71) "03f3a775-6736-4ffb-bda0-b6e3ae215394"
72) "03dde3c0-300f-4431-9f58-82dc3869020a"
73) "03dd67bf-9044-4c00-a34d-c480d221afdb"
74) "03b79d39-ddcb-48b2-8cdf-bd480041018d"
75) "039b6212-3760-4261-a3ad-66427ca3ece2"
76) "034e7e37-0e9a-49d0-86f7-f74956ad9c1c"
77) "032f6cc9-0ec8-4014-a37c-ee233afad174"
78) "02ce202c-4b37-4e21-b8c6-241323daaf4f"
79) "02c32611-8f4c-44cc-85e6-9aabe50c5deb"
80) "02617a09-af48-4885-9193-e36c73e1cc93"
81) "022ec4b4-9061-489e-9c1f-d702187d9173"
82) "022bfe1c-c04a-4dfd-83d1-545a2fa37d62"
83) "0221116d-1140-4161-976e-780166b409ee"
84) "0211f787-5122-4b95-90b2-36a60da5072c"
85) "01f80b6f-ef70-4cf3-af3c-0c39cc1ca6df"
86) "01ae3dff-19bf-4dfe-a417-f00c5cbee89e"
87) "017b4fc5-a5b4-40f4-a22c-c0585dce1b8c"
88) "0157ba2a-e6ea-4272-8257-be4d07278f41"
89) "01476c80-aa75-46ed-b912-cad708cfb6da"
90) "0125e066-aeb7-4053-ab85-e358076a23b0"
91) "00ded12e-3974-46ff-b425-a1f4202d5380"
92) "00b5546f-04a5-4d98-80b2-dbf2254f8f81"
93) "0087283d-3773-4788-a86e-3a3a425d2240"
94) "005dc64e-0134-43d8-b9bf-b3390e59ab12"
95) "004ecf75-54b9-4aa7-a3ff-7ee2e0907f85"
96) "004a2fb2-1547-4c63-8bfb-abfe812681aa"
97) "0025b235-f4cd-45e4-90c3-46acbb3165fb"
98) "001b046c-f1c9-457d-a8b7-cd157bc5d889"
99) "00119d1d-b587-478f-86c0-ceb4f15c5cf7"
100) "00093f78-11c7-43e1-abed-86c735252155"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
4:在本地执行data_manager项目中的DataIndex代码,在ES中建立索引。

注意:需要修改项目中用到的EsUtil工具类中的节点信息。

到ES中验证数据:
[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/article/_search?pretty'
{
"took" : 393,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 100,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "article",
"_type" : "_doc",
"_id" : "01476c80-aa75-46ed-b912-cad708cfb6da",
"_score" : 1.0,
"_source" : {
"author" : "腾讯新闻",
"describe" : "  浙江在线11月11日讯由于感情纠葛,阿松(化名)的婚外恋女友一怒之下抄起剪刀,几乎剪断了他的整根“命根子”。所幸医生及时抢救,周密施术,才保住了他的“男儿身”。昨天,正在义乌进行康复治疗的阿松,回想起前几天的经历,依旧百感交集。  “小三”怒剪命根只因疑其不忠  三十多岁的阿松是重庆人,在义乌工作,妻子女儿也都在义乌。  阿松还有一个婚外恋女友,是在一家工厂里认识的,两个人相恋已经有一年多了。女友30多岁,也有自己的家庭。  阿松说,最近一段时间,女友屡屡怀疑阿松“不忠”,据说是听到了一些传言。  十几天前,被妒忌之火冲昏头脑的女友,居然想到了断其“命根”的“教训”办法。当时是下午3点多,",
"time" : "2021-10-17 11:13:19",
"title" : "“小三”怀疑男友不忠怒剪其“命根”"
}
},
{
"_index" : "article",
"_type" : "_doc",
"_id" : "01f80b6f-ef70-4cf3-af3c-0c39cc1ca6df",
"_score" : 1.0,
"_source" : {
"author" : "腾讯新闻",
"describe" : "    两岸几十年的隔阂已经使共同的汉语言演变出一些差异。图为在台湾书店里的内地游客。    台湾中华语文研习所董事长何景贤,手捧两种版本的《两岸现代汉语常用词典》。  中新网11月11日电教育部语言文字应用管理司司长王登峰在接受香港《文汇报》访问时表示,两岸合编《中华大辞典》主要将以民间方式进行,届时将推出简体字括注繁体字以及繁体字括注简体字两个版本。另有消息人士透露,预计该项目将于年底启动。  第五届两岸经贸文化论坛共同建议7月份曾就两岸合编《中华大辞典》达成初步意向。王登峰表示,大陆方面已经作好充分的准备工作,将主要以1996年两岸合编的《两岸现代汉语常用词典》为工作基础。两岸有关部门达",
"time" : "2021-10-17 11:13:19",
"title" : "两岸将合编中华大辞典收集新词汇(组图)"
}
}
......
}
]
}
}
1
重点关注_source字段中是否包含content字段,如果包含此字段内容说明前面的mapping配置有问题,如果不包含此字段内容说明是正确的。
1
2
3
4
5
6
7
8
9
10
11
5:对web_fullsearch项目打包并运行。
web_fullsearch项目是一个Javaweb项目,所以需要打war包,最终在Web容器中运行,在这里我们使用Tomcat这个web容器。
打war包。

D:\IdeaProjects\db_fullsearch\web_fullsearch>mvn clean package -DskipTests
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 24.974s
[INFO] Final Memory: 35M/348M
[INFO] ------------------------------------------------------------------------
1
2
3
4
5
6
7
8
9
10
使用之前在学习ES课程的时候在bigdata04上部署的tomcat,将war包上传到tomcat的webapps目录下:
[root@bigdata04 apache-tomcat-8.0.52]# cd webapps/
[root@bigdata04 webapps]# ll
total 84888
drwxr-xr-x. 14 root root 4096 Mar 16 20:37 docs
drwxr-xr-x. 6 root root 83 Mar 16 20:37 examples
drwxr-xr-x. 5 root root 87 Mar 16 20:37 host-manager
drwxr-xr-x. 5 root root 103 Mar 16 20:37 manager
drwxr-xr-x. 3 root root 4096 Mar 16 20:45 ROOT
-rw-r--r--. 1 root root 86913853 Oct 17 2021 web_fullsearch.war
1
2
3
4
5
启动tomcat:
[root@bigdata04 apache-tomcat-8.0.52]# bin/startup.sh

访问项目:
http://bigdata04:8080/web_fullsearch/article

image-20230612151125293

1
2
6:验证搜索功能。
确认分页、高亮、查询文章明细数据等功能是否正常。

image-20230612151144676

image-20230612151156591

5 项目中遇到的典型问题

1
2
3
4
5
6
7
8
9
单索引库查询效率降低的问题

爬虫程序每天都会到互联网上采集新的文章数据,如果项目运行了半年、1年,所有的数据都存储到ES的一个索引库里面,这样会导致查询效率降低。
可以考虑按周或者按月创建索引库,通过索引库别名关联最近半年内的索引库,实现默认查询最近半年内的数据。
索引库的命名可以按照一定的规律,假设是按月建立索引库,则索引库的名称大致是这样的:
article_202201
article_202202
article_202203
......
1
2
3
如果确实需要查询历史以来所有的数据,在查询的时候可以通过索引库通配符实现所有数据查询,使用这个索引库通配符即可:article_*,这样可以查询所有以article_开头的索引库。

类似于百度这样,提供一个高级搜索选项,在里面进行一些个性化高级设置。

image-20230614113918727

image-20230614113936078

1
2
3
4
5
6
自定义词库导致的历史数据查询异常问题

当项目运行了一段时间以后,需要在自定义词库中新增一些词语,但是这个新增的词语只会针对后续新增的索引数据生效,对之前的索引数据是不生效的。
针对之前的历史数据根据新增的词语进行查询,可能还是查不出来结果,这属于正常现象。

如果想要实现新增的自定义词语在历史数据中可以查询出来数据,只有一个办法,就是重建索引,这个工作量就有点大了,所以一般情况下也不需要针对历史数据额外处理,知道有这个现象就行了。
1
2
3
4
ES和HBase数据一致性问题

针对这个问题,可以分析一下ES和HBase数据同步的三种方案,每种方案的优缺点。
详细分析见第2章《ES和HBase数据同步的三种方案》小节。
1
2
3
针对这个项目能不能只用ES?

Elasticsearch最擅长的是快速复杂查询,虽然它支持分布式,也可以存储海量数据,但是这并不是它最擅长的功能,因为数据存储多了之后肯定会影响ES的性能,所以需要引入HBase实现海量数据存储,在ES中主要维护查询需要用到的字段。
1
2
3
4
5
ES和HBase中存储的数据有什么区别?

可以详细分析一下ES中的mapping设计,重点分析文章的正文(content)字段。
正文字段只需要在HBase中存储,不需要在ES中存储,但是需要在ES中建立索引,因为在查询的时候需要根据这个字段进行查询。
详细解释见第4章内容。

image-20230614114230255


本文标题:大数据开发工程师-ES+HBase实现仿百度搜索引擎-1

文章作者:TTYONG

发布时间:2023年06月02日 - 18:06

最后更新:2023年06月18日 - 23:06

原始链接:http://tianyong.fun/%E5%A4%A7%E6%95%B0%E6%8D%AE%E5%BC%80%E5%8F%91%E5%B7%A5%E7%A8%8B%E5%B8%88-ES+HBase%E5%AE%9E%E7%8E%B0%E4%BB%BF%E7%99%BE%E5%BA%A6%E6%90%9C%E7%B4%A2%E5%BC%95%E6%93%8E-1.html

许可协议: 转载请保留原文链接及作者。

多少都是爱
0%