平时项目开发中,经常会遇到模糊搜索的需求。通常当需要模糊搜索的数据库字段不大,我们可以简单通过 字段名 like '%搜索值%'实现,搜索效率不高,而且就算加索引也无法生效。对于数据库字段很大的,mysql还提供全文索引,开销也很大。

有没有一种专门做搜索的“数据库”呢?不仅可以实现高效的模糊搜索,而且还能像百度、谷歌这类搜索引擎一样,从我输入的一段文字中,自动识别关键词进行搜索。下面介绍的elasticsearch就是这方面的行家。

1. 简介

全文搜索属于最常见的需求,开源的 Elasticsearch是目前全文搜索引擎的首选。它可以快速地储存、搜索和分析海量数据。维基百科、Stack Overflow、Github 都采用它。Elasticsearch 的底层是开源库 Lucene。但是,你没法直接用 Lucene,必须自己写代码去调用它的接口。Elasticsearch 是 Lucene 的封装,提供了 REST API 的操作接口,开箱即用。

index 索引

elasticsearch 数据管理的顶层单位叫做 Index(索引)。它是单个数据库的同义词。每个 Index (即数据库)的名字必须是小写。下面的命令可以查看当前节点的所有 Index。

 curl -X GET 'http://localhost:9200/_cat/indices?v'
document 文档

Index 里面单条的记录称为 Document(文档)。许多条 Document 构成了一个 Index,Document 使用 JSON 格式表示。

同一个 Index 里面的 Document,不要求有相同的结构(scheme),但是最好保持相同,这样有利于提高搜索效率。

type (已移除)

type 是之前存在的概念,elasticsearch 7.x 就不在使用。Document 可以分组,比如weather这个 Index 里面,可以按城市分组(北京和上海),这种分组就叫做 Type,它是虚拟的逻辑分组,用来过滤 Document。

但是不同的分组中,Document 的数据结构应当尽量保持一致,否则会影响搜索效率,type的定位就很鸡肋。因此elasticsearch就直接做强制限制,在6.X 版本中,一个index下只能存在一个type;在 7.X 版本中,直接去除了 type 的概念,就是说 index 不再会有 type。从前的那些写法,可以直接用index下的_doc代替。

倒排索引

什么是倒排索引: 倒排索引也叫反向索引,通俗来讲正向索引是通过key找value,反向索引则是通过value找key。Elasticsearch 使用一种称为倒排索引的结构,它适用于快速的全文搜索。一个倒排索引由文档中所有不重复词的列表构成,对于其中每个词,有一个包含它的文档列表。倒排索引建立的是分词(Term)和文档(Document)之间的映射关系,在倒排索引中,数据是面向词(Term)而不是面向文档的。

假设我们有两个文档,每个文档的content域包含如下内容:
The quick brown fox jumped over the lazy dog
Quick brown foxes leap over lazy dogs in summer

为了创建倒排索引,我们首先将每个文档的content域拆分成单独的词(我们称它为词条或tokens),创建一个包含所有不重复词条的排序列表,然后列出每个词条出现在哪个文档。

现在,如果我们想搜索quick brown,我们只需要查找包含每个词条的文档。两个文档都匹配,但是第一个文档比第二个匹配度更高。如果我们使用仅计算匹配词条数量的简单相似性算法,那么,我们可以说,对于我们查询的相关性来讲,第一个文档比第二个文档更佳。

分词

Elasticsearch在倒排索引时会文本会使用设置的分析器,而输入的检索语句也会首先通过设置的相同分析器,然后在进行查询。

es内置很多分词器,但是对中文分词并不友好,例如使用standard分词器对一句中文话进行分词,会分成一个字一个字的。这时可以使用第三方的Analyzer插件,比如 ik、pinyin等,本文用的是ik。

2. 安装

本次除了安装elasticsearch以外,我们还会安装kibana(可视化展示elasticsearch数据的产品),以及给elasticsearch预装一个ik中文分词器的插件。因为elasticsearch选用的版本是 7.9.3 ,为了保证兼容,因此kibana和ik插件都使用该对应版本。

先查看下面容器化安装脚本:

# 启动 Elasticsearch
docker run -d \
 --name elasticsearch \
 --restart=on-failure:3 \
 -p 9200:9200 \
 -p 9300:9300 \
 -e "discovery.type=single-node" \
 -v /Volumes/elasticsearch/data/:/usr/share/elasticsearch/data/ \
 -v /Volumes/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
 -v /Volumes/elasticsearch/plugins/:/usr/share/elasticsearch/plugins/ \
 elasticsearch:7.9.3

#启动 Kibana
docker run -d \
 --name kibana \
 --link elasticsearch:es \
 -p 5601:5601 \
 -e ELASTICSEARCH_URL=es:9200 \
 kibana:7.9.3
elasticsearch部分

单节点执行,暴露9200和9300端口,值得关注的是有三个挂载卷:

  1. /data: elasticsearch的数据库都在该目录,可以直接挂载该目录。
  2. /config/elasticsearch.yml: 主要是安装后要修改elasticsearch.yml文件,在文件末尾加上下列两行,以解决跨域问题。为了方便,就直接挂载该文件了。

    http.cors.enabled: true
    http.cors.allow-origin: "*"
  3. /plugins: 这是elasticsearch安装插件的目录,默认是空的,通常将插件解压后放入该目录,重启后即可使用。关于分词器的插件,下文再说。
kibana部分

因为kibana的环境变量中是要配置elasticsearch地址的,但是docker不同容器之间默认网络隔离。可以通过配置同一网络环境的方式解决,这里使用的方式,是通过--link 给elasticsearch设置了一个别名,相当于容器内部维护了一个DNS域名映射。

ik分词器插件

ik中文分词器插件的下载地址,最好找elasticsearch对应版本的插件。

通常来说是启动elasticsearch服务后再安装插件的,重启之后生效。我为了简单,先将插件zip下载到本地,elasticsearch-analysis-ik-7.9.3.zip 解压到 /Volumes/elasticsearch/plugins/analysis-ik/ 目录,再对 /Volumes/elasticsearch/plugins/ 目录做挂载,当elasticsearch 容器启动后,插件就已经生效了。

3. API示例

这里我们通过REST API,从零开始做一个完整的示例。我们通过创建一个articles的索引,使用ik中文分词器,创建完数据后,再做查询。本文不会介绍所有的API,但好在是 RESTFul 风格,很多用法都能自己推敲出来。

创建索引

(PUT) http://localhost:9200/articles

更新索引mapping

(POST) http://localhost:9200/articles/_mapping

{
    "properties": {
        "title": {
            "type": "text",
            "analyzer": "ik_max_word",
            "search_analyzer": "ik_max_word"
        },
        "content": {
            "type": "text",
            "analyzer": "ik_max_word",
            "search_analyzer": "ik_max_word"
        }
    }
}
查看索引mapping

(GET) http://localhost:9200/articles/_mapping

新建文档

(POST) http://localhost:9200/articles/_doc

{
    "title": "都江堰",
    "content": "一位年迈的老祖宗,没有成为挂在墙上的画像,没有成为写在书里的回忆..."
}
搜索文档

(GET) http://localhost:9200/articles/_search

{
    "query": {
        "match": {
            "content": "画像回忆"
        }
    }
}

4. spring开发

实际spring结合elasticsearch的开发还是比较复杂的,如果详细的讲这块内容,得单独讲几篇文章。本文的目的主要是简要介绍elasticsearch的应用,因此本章只是做一个很简单的demo,实际开发还得查阅相关文档。

下文使用的是spring-boot-starter-data-elasticsearch,还好目前4.2版本是兼容7.9.x版本elasticsearch的。因为都是属于 spring data jpa 体系的,所以dao层的语法还是容易理解的。另外一些复杂的操作,可以通过注入 ElasticsearchRestTemplate 来实现。

下列代码中的 /articles 接口,和上文中的 http://localhost:9200/articles/_search 一样,都是对文章正文的模糊搜索。

pom.xml
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>
application.yml
spring:
  data:
    elasticsearch:
      client:
        reactive:
          endpoints: localhost:9200
ArticlesEO.java
@Data
@Document(indexName = "articles")
public class ArticlesEO {
    @Id
    private String id;
    @Field(type = FieldType.Text,analyzer = "ik_max_word")
    private String title;
    @Field(type = FieldType.Text,analyzer = "ik_max_word")
    private String content;
}
ArticlesRepository.java
@Repository
public interface ArticlesRepository extends ElasticsearchRepository<ArticlesEO,String> {
    Page<ArticlesEO> findByContent(String content, Pageable pageable);
}
ArticleController.java
@RestController
@RequestMapping
public class ArticleController {
    private Pageable pageable = PageRequest.of(0,10);

    private final ArticlesRepository articlesRepository;
    private final ElasticsearchRestTemplate elasticsearchRestTemplate;

    public ArticleController(ArticlesRepository articlesRepository,ElasticsearchRestTemplate elasticsearchRestTemplate) {
        this.articlesRepository = articlesRepository;
        this.elasticsearchRestTemplate=elasticsearchRestTemplate;
    }

    /**
     * 查询 document
     * @param content
     * @return
     */
    @GetMapping("/articles")
    public Page<ArticlesEO> searchArticle(@RequestParam("content")String content){
        return articlesRepository.findByContent(content,pageable);
    }

}

KerryWu
641 声望159 粉丝

保持饥饿