使用ElasticSearch搜索服务器做全文检索

使用ElasticSearch做全文检索

前言

关于ElasticSearch网络上有很多的教程。但是这些教程,讲的都不是很好,大多数都仅讲了,它是一个搜索服务器,一个搜索引擎,这种比较抽象的话,并没有解释他的是一个什么样的结构,这让我在当初学习的时候,有中无从下手的感觉。但如今对他有了一定的了解,于是乎,我打算把这些功能比较形象的写出来。

概念

这个概念比较关键,如果你能理解它的概念,那么你也能理解他的大部分逻辑了。

由来

首先你要明白搜索引擎这个概念。我们平常用的搜索引擎如,百度,谷歌等等。他们是怎么样在这上亿的数据量中快速的搜索到我们需要的内容呢?如果仅仅依靠数据库。,那么,会给数据库造成很大的压力,而且曾经上万的人可能在同一时间使用搜索引擎去搜索内容。数据库怎么可能承担了如此之多的数据量的访问呢?

那么我们根据之前所学的概念。我们可以尝试用的缓存去解决这个问题。对!把需要读取的需要读取的东西放入缓存中。如果需要再从缓存当中去读取。那么就不会给数据库造成很大压力了。但之前的缓存像Redis,它是一种类似于key- value的类型。那么对于千奇百怪的搜索内容,不同编码的字符串,redis是怎么能够能中搜索到呢?就算他能够实现。但是缓存这种东西。一般来说是不会把它设置为永久存在的。缓存一般有他的存活时间,他也没有像数据库那么稳定。所以单单是使用缓存去解决这个问题,明显不是最优的选择。

另外一个比较重要的一点就是,在一个商城中。我们一般都会实现搜索这种功能。但搜索功能是直接去访问数据库的。如果很多人进行搜索,那么海量的请求会给我们数据库造成极大的压力。所以我们也需要把这种东西拆分成两部分。用别的东西去代替搜索,或者说不直接从数据库访问数据。但是仅仅是Redis缓存明显不能够承担这种任务。

于是乎便诞生了ElasticSearch,这种NoSQL类型特殊的数据库型缓存。对,就是数据库类型的缓存。

工作流程

他的工作流程主要是:当用户需要某一个数据时,先从数据库里读取,再把这个数据加入到这个特殊的缓存当中。然后当我们需要某些内容的,他们就能够去这个特殊的缓存中读取数据。这种读取速度的方法跟一般的缓存不同的是,会根据内容关键词去进行搜索,只会的搜索出所需要的内容。

之后如若遇到有相同的关键词,就会去这个搜索服务器中搜索。将它的值或数据类型,返回给用户。而当我们这关键词在这搜索服务器中所显示数据量太少,或者说没有这个数据时,才会去数据库中再次读取数据。再补充到到搜索服务器当中。这个工作流程和Redis缓存非常的类似。但主要的区别在于,它是一个能够搜索和存储的数据库型缓存。

和Redis的主要区别

那这是不是代表了这个搜索服务器完全可以取代Redis这个缓存呢?其实这是不一样的。因为我们的这个搜索服务器,他虽然能对关键是进行搜索,但这搜索的效率相对缓存那样的key-value直接取值而言,并不是特别高,虽然说比数据库要快,但是单单论缓存的速度而言,是远远的比不上Redis的。

所以,当我们需要搜索的时候才会用这个搜索服务器。我们一般仅仅是使用缓存的话还是Redis比较快速。

应用场景

就一个商城项目而言,当我们需要去搜索的时候,就用这个搜索服务器,快速的将关键词拆分进搜索,然后返回出相应的内容,但是这些内容并不包含所有的信息。他只包含了一些比较关键的信息,就例如它的头标题或者是特定的图片等等。

然后等用户点进的这个链接。我们才会去从数据库中另外读取数据,或者说去Redis中读取数据。

实验

接下来我们就来进行一个实验,来尝试的使用这个搜索服务器。

但是这个东西操作有点困难,所以你必须先掌握docker的使用

譬如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#要求熟练掌握
ip addr
service network restart
service docker start
docker pull xxx
docker images
docker ps
docker ps -a
docker run xxx
docker kill xxx
docker rm xxx
docker rename xxx yyy
docker image rm xxx
docker restart xxx

#了解
apt-get update
apt-get install vim

vim xxx

安装

1
2
3
4
5
docker pull elasticsearch:xxx 
#必须带版本号

##启动
docker run -d -e ES_JAVA_POTS="-Xms256m -Xmx256m" -e "discovery.type=single-node" -p 9200:9200 -p 9300:9300 --name es01 f29a1ee41030

注意

如果你的云服务器内存不足1G的话,则使用

1
2
3
4
5
6
find / -name jvm.options
##使用此指令找到es的jvm设置
##使用
vim /var/lib/docker/overlay2/973a5c6a4450ab59e64d6ba7e78ed6622512c142e0a2f8164653b11e8b66baa9/diff/usr/share/elasticsearch/config/jvm.options

#进入,将-xms和-xmx参数,改为256m

否则可能会出现:

配置文件

依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
</dependency>

<dependency>
<groupId>io.searchbox</groupId>
<artifactId>jest</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>

这个依赖还包括其他东西。我们一般是用这个jest方法去使用elasticsearch。

yml
1
2
3
4
5
6
spring:
elasticsearch:
jest:
uris:
- http://192.168.78.128:9200 # ES服务器的地址!
read-timeout: 5000

实体类

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
public class Entity implements Serializable {

private static final long serialVersionUID = -763638353551774166L;

public static final String INDEX_NAME = "index_entity";

public static final String TYPE = "tstype";

private Long id;

private String name;

public Entity() {
super();
}

public Entity(Long id, String name) {
this.id = id;
this.name = name;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}


}

服务层

接口
1
2
3
4
5
6
7
8
public interface TestService {

void saveEntity(Entity entity);

void saveEntity(List<Entity> entityList);

List<Entity> searchEntity(String searchContent);
}
实现
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
@Service
public class TestServiceImpl implements TestService {

private static final Logger LOGGER = LoggerFactory.getLogger(TestServiceImpl.class);

@Autowired
private JestClient jestClient;



@Override
public void saveEntity(Entity entity) {
Index index = new Index.Builder(entity).index(Entity.INDEX_NAME).type(Entity.TYPE).build();
try {
jestClient.execute(index);
LOGGER.info("ES 插入完成");
} catch (IOException e) {
e.printStackTrace();
LOGGER.error(e.getMessage());
}
}


/**
* 批量保存内容到ES
*/
@Override
public void saveEntity(List<Entity> entityList) {
Bulk.Builder bulk = new Bulk.Builder();
for(Entity entity : entityList) {
Index index = new Index.Builder(entity).index(Entity.INDEX_NAME).type(Entity.TYPE).build();
bulk.addAction(index);
}
try {
jestClient.execute(bulk.build());
LOGGER.info("ES 插入完成");
} catch (IOException e) {
e.printStackTrace();
LOGGER.error(e.getMessage());
}
}

/**
* 在ES中搜索内容
*/
@Override
public List<Entity> searchEntity(String searchContent){
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//searchSourceBuilder.query(QueryBuilders.queryStringQuery(searchContent));
//searchSourceBuilder.field("name");
searchSourceBuilder.query(QueryBuilders.matchQuery("name",searchContent));
Search search = new Search.Builder(searchSourceBuilder.toString())
.addIndex(Entity.INDEX_NAME).addType(Entity.TYPE).build();
try {
JestResult result = jestClient.execute(search);
System.out.println(result.toString());
return result.getSourceAsObjectList(Entity.class);
} catch (IOException e) {
LOGGER.error(e.getMessage());
e.printStackTrace();
}
return null;
}
}

接下来来到了重点,详细说一说。

先说saveEntity方法,这里要注意的就是:

1
Index index = new Index.Builder(entity).index(Entity.INDEX_NAME).type(Entity.TYPE).build();

他到底做了什么呢?我们可以从index和type这两个,看到它是在为一个entity类实例建立一个索引名和索引类型,将两者绑定在一起,作为一个索引类型被执行。调用的是:

1
2
3
4
public Builder(Object source) {
this.source = source;
this.id(AbstractAction.getIdFromSource(source));
}

可以看到,它没有对Entity做改动,而仅仅是加上一部分属于他的内容。

我们还可以从中获取到,这个index 类型还附带一个url:uri=index_entity/tstype,method=POST

将这个索引类型发送到我们的ES服务器当中。

接着,我们来看到下面的Search方法:

1
2
3
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();

searchSourceBuilder.query(QueryBuilders.matchQuery("name",searchContent));

这个方法一开始就建立了一个对象,我们可以看到:QueryBuilders.matchQuery(“name”,searchContent)是对要搜索的内容,进行了一定程度的转换,那么转换成什么样子了呢?

我们使用一个例子:entityController/search?name=北京 为例。进行搜索,我们来看看这个searchSourceBuilder:

1
2
3
4
5
6
7
8
9
10
{
"query" : {
"match" : {
"name" : {
"query" : "北京",
"type" : "boolean"
}
}
}
}

变为了一个Json型对象,记住,他将要搜索的内容:北京,变为了一个Json对象。然后通过:

1
2
Search search = new Search.Builder(searchSourceBuilder.toString())
.addIndex(Entity.INDEX_NAME).addType(Entity.TYPE).build();

封装成一个Url,发送到ES服务器。它里面的内容是:

1
uri=index_entity/tstype/_search,method=POST

和添加类似,返回进了服务器当中。

控制层

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
@RestController
@RequestMapping("/entityController")
public class EntityController {


@Autowired
TestService cityESService;

@RequestMapping(value="/save", method=RequestMethod.GET)
public String save(long id, String name) {
System.out.println("save 接口");
if(id>0 && StringUtils.isNotEmpty(name)) {
Entity newEntity = new Entity(id,name);
List<Entity> addList = new ArrayList<Entity>();
addList.add(newEntity);
cityESService.saveEntity(addList);
return "OK";
}else {
return "Bad input value";
}
}

@RequestMapping(value="/search", method=RequestMethod.GET)
public List<Entity> save(String name) {
List<Entity> entityList = null;
if(StringUtils.isNotEmpty(name)) {
entityList = cityESService.searchEntity(name);
}
return entityList;
}

}

控制层比较随意,接收参数,调用接口便可。

测试

我们可以安装ES-Head可视化插件,帮助我们查看里面的内容。

我们打开PostMan,输入 localhost:8888/entityController/save?id=1&name=北京长城,为其添加内容。

想这样子,添加很多内容

之后,可以使用方法进行搜索操作,搜到到的值,会以JSON形式,返回给我们,方便我们绑定对象实例。