Elasticsearch

什么是 Elasticsearch?它是一个分布式的开源搜索和分析引擎,适用于所有类型的数据,包括文本、数字、地理空间、结构化和非结构化数据。

无论在开源还是专有领域,Lucene可以被认为是迄今为止最先进、性能最好的、功能最全的搜索引擎库。但是,Lucene非常复杂,使用之前需要深入了解检索的相关知识来理解它是如何工作的。Elasticsearch(以下用ES代替)是一个基于Lucene的分布式可扩展的实时分析搜索引擎。它的目的是通过简单的RESTfulAPI来隐藏Lucene的复杂性,从而让全文搜索变得简单。

 

 

基本概念

 

首先,按照江湖惯例,我们先来说一说ES的基本概念。如下图,在ES服务器中,数据是被存放在一个或多个索引(Index)中的,索引里包含着多种类型(Type)的文档(Document),文档是ES中的主要实体,由字段(Feild)构成,ES使用映射(Mapping)来定义文档中每个字段的类型和属性。

 

为了加深理解,再将它与我们熟悉的MySql数据库做下对比:

ESMySql
索引数据库
类型
文档数据行
字段数据列
隐射列定义

另外,需要说明一下,从ES7的版本开始, 类型(Type)这个概念将被完全移除。这一改动,使整个ES的数据结构又轻量了不少,每个索引下的文档只有一种数据结构,在索引数据的管理上也更方便了。由于我参与的项目使用的是ES5.3版本,所以后面的讲解都是基于该版本的。

那么,现在思考一个问题:

在一个ES实例的某个索引中,它的文档数量比较大,甚至超过了硬盘的容量,这时,ES要如何进行存储?

其实,在大数据场景下,我们的ES都是以集群的方式部署的,集群中的每一个节点就是一个ES实例。在这种模式下,我们的索引数据不再集中存储到一个实例中,而是被切分成多个分片,这些分片被分配到不同的节点中进行存储,另外,为了保证高可用,每个分片至少还会有一个副本分片,并与主分片分开进行存储。集群整体结构如下图所示:

在ES7.0之前的版本中,主分片和副本分片的默认数量是5和1,7.0之后,主分片和副本分片的默认数量都为1。主副分片的数量可以在创建索引时手动指定。

 

 

应用场景

 

 

作为一个分布式的搜索引擎,在实际的应用中,ES不仅被用来实现全文检索功能,还被广泛应用于日志分析和数据分析等多种场景,大幅度降低了维护多套专用系统的成本:

  1. 全文检索

    这是ES的核心功能,不必多说。

  2. 日志分析

    ES结合Logstash和Kibana这两大开源软件,组成的ELK系统,打通了日志从采集到清洗再到存储和可视化的整个过程,使得日志搜索、分析和可视化变得不再棘手,是目前日志处理领域的技术首选。

  3. 数据分析

    借助与ES很好的时间序列处理能力、丰富的数据统计功能以及分布式的特点,使得ES逐渐成为了数据分析和可视化的理想工具。

 

ES安装

 

好了,概念就介绍到这,现在让我们开始动手安装ES。

ES是基于java开发的,所以安装之前需要确保本机上已经安装有jdk,ES对java依赖如下:

  • ES5.x以上需要Java8 以上的版本

  • ES6.5以上开始支持Java 11

  • 从ES7.0开始,ES内置了Java环境

下面以ES5.3.3版本为例来讲解ES的安装,其他版本安装与此差别不大。

 

 

下载并解压

 

在官网https://www.elastic.co/cn/downloads/elasticsearch 下载相应版本的压缩包并解压。

 

 

启动ES

进入bin目录,执行elasticsearch脚本既可启动

cd bin
./elasticsearch

 

 

 

可能遇到的问题

问题1 can not run elasticsearch as root

 

当使用root用户启动ES时,会报如下错误

 

所以,需要为ES新建一个运行账号

# 新建一个elasticsearch的用户组
groupadd elasticsearch
# 在elasticsearch用户组下面建立一个elasticsearch的用户
useradd -g elasticsearch elasticsearch

 

将elasticsearch目录的所有者给该账号

chown -R elasticsearch:elasticsearch elasticsearch-5.3.3/

 

接下来切换到elasticsearch用户,并启动ES

su elasticsearch
./elasticsearch

如果你运气好,就可能会遇到另一个问题,就是下面的问题2。

 

 

问题2  

vm.max_map_count [65530] is too low, increase to at least [262144]

 

这个问题也很好解决,我们只需要修改系统的最大虚拟内存区域的数值就行 退回到root账号下,修改配置文件/etc/sysctl.conf

vi /etc/sysctl.conf

 

在文件末尾添加vm.max_map_count的配置,值大于等于262144即可 添加完成后执行如下命令重新加载系统参数

sysctl -p

上面的问题解决后,切换到elasticsearch账号,就可以顺利启动ES了。

 

从文本到索引

现在我们就可以开始操作ES了。首先从建立一个空索引开始。

 

 

建立索引

创建索引很简单,因为ES对各类操作都提供了RESTful API接口,我们可以很方便的使用这些接口来与ES交互。下面是一个最基本的创建索引请求:

PUT /test //put一个创建索引的请求,索引名为test
{
  "settings": {}, //这里可以配置一些索引的参数,如主副分片数量
  "mappings": {  //这里就是定义文档类型和字段的地方啦
    "type1": { //定义文档类型:type1
      "properties": {
        "field1": { //定义字段名:field1
          "type": "text", //字段类型:text
          "analyzer": "standard"
        }
      }
    }
  }
}

这个请求会在ES中创建一个名为test的索引,索引中包含一个名为type1的类型,该类型有一个字段field1,字段类型为text,该字段在索引和搜索时都使用standard分析器。分析器不懂没关系,后面会讲。

 

其实,还有一种更简单的方法,就是不创建索引,直接put一个文档到ES,比如:

PUT test/type1/1 //将数据提交到test索引,文档类型为type1,id为1
{
    "user": "kimchy",
    "post_date": "2009-11-15T14:12:12",
    "message": "trying out Elasticsearch"
}

 

执行这条请求的时候,如果ES发现test索引不存在,便会根据文档的字段和内容,自动创建索引和mapping信息,如下:

"mappings": {
  "type1": {
    "properties": {
      "message": {
        "type": "text", //自动为message字段设置为text类型
        "fields": { //为message创建的子字段
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "post_date": {
        "type": "date" //自动为post_date字段设置为date类型
      },
      "user": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      }
    }
  }
}

可以看到,ES会根据文档的原始信息,为字段映射对应的类型(如将post_date字段类型设为Date),另外,对每个text类型的字段,都会增加一个fields子域,这个子域的作用就是扩展出原文档没有的字段,本例中,最终的存储字段有message,message.keywod,post_date,user,user.keyword这个5个。

但是,通过这种方式创建的索引,结构往往比较简单,对于复杂点的系统,一般是不太能满足业务需要的。所以,比较建议的做法还是预先通过手动定义mapping来创建索引。

 

分析器

接下来,我们来看看ES是怎样将处理我们提交的文本数据并最终存入索引的。当收到一条索引请求时,ES会用一种类似于E TL的方式对文档进行处理和分析。比如判断文本中是否需要进行特殊字符处理、是否需要进行大小写转换、以何种方式分词和是否要去停词等等。这些分析操作被ES封装在一个叫做分析器(Analyzer)的模块中。

 

 

分析器主要由三个子模块组成:字符过滤器:对文本进行粗略的清洗工作,如去除原始文本的HTML标记,把字符“&”转换为单词“and”,或者模式匹配替换等。分词器:将文本分割成一系列被称为词汇单元(token)的独立原子元素,每个token大致能与自然语言中的“单词”对应起来。表征过滤器:对分词后的词汇进行修改词(例如将字符转为小写),去掉词(例如停词“a”,“and”,“也”,“又”等等)或者增加词(例如同义词像“jump”和“leap”等等)等操作。

实际使用时,这一系列的转换操作是根据具体业务需求来决定的,ES已经帮我们预定义了多种常用的分析器,如Keyword Analyzer,Pattern Analyzer和Whitespace Analyzer等,同时也预定义了很多字符过滤器、分词器和表征过滤器,方便我们使用。想了解更多的内建分析器可参考官方文档。

另外,ES也支持用户自定义分析器,甚至联合Lucene的token工具和过滤器创建自定义的分析器。比如,在创建索引时,我们可以使用pattern分析器自定义一个自己的数字分析器digit_analyzer,并在mapping中使用它:

PUT /test
{
  "settings": {
    "analysis": {
      "analyzer": {
        //定义分析器
        "digit_analyzer": {
          "tokenizer": "digit_tokenizer"
        }
      },
      "tokenizer": {
        //定义分词器
        "digit_tokenizer": {
          "type": "pattern", //设置为模式匹配分词器
          "pattern": "[^(0-9)]" //只匹配数字
        }
      }
    }
  },
  "mappings": { //文档的mapping配置
    "_default_": { //文档的类型
      "properties": {
        "userCode": { //字段
          "type": "text", //设置userCode字段的类型
          "analyzer": "digit_analyzer" //使用自定义分析器
        },
        …… //省略其他字段的配置
      }
    }
  }
}

 

分析器不仅仅会被用在索引阶段,在执行某些搜索时,ES也会使用分析器,对查询关键字进行过滤和分词操作。默认情况下,索引和搜索阶段使用的是同一个分析器,即analyzer指定的分析器,若想使用不同的分析器,可以在mappings中设置search_analyzer,如:

"mappings": {
  "_default_": {
    "properties": {
      "user": {
        "type": "text",
        "analyzer": "standard",
        "search_analyzer": "standard" //指定搜索时用的分析器
      }
    }
  }
}

 

 

中文分词

分析器中最重要的一个模块就是分词器(Tokenizer),因为分词的好坏决定了最终搜索的效果。

虽然,ES预定义了很多分词器,但是,对中文的支持却非常有限。在分词时,它们只会将中文分成一个一个的汉字,无法按照词语划分。于是,有人就专门为ES开发了支持中文的分词器插件,常见的中文分词插件有:ik、结巴和THULAC等等。

其中,ik分词器是使用最广泛的,下面就简单介绍一下ik分词器以及在ES中如何使用。

ik提供了两种分词模式,ik_max_word和ik_smart模式:

ik_max_wordik_smart
会将文本做最细粒度的拆分,比如会将“中华人民共和国国歌”拆分为“中华人民共和国,中华人民,中华,华人,人民共和国,人民,人,民,共和国,共和,和,国国,国歌”,会穷尽各种可能的组合。会将文本做最粗粒度的拆分,比如会将“中华人民共和国国歌”拆分为“中华人民共和国,国歌”。

 

索引时,为了提供索引的覆盖范围,通常会采用ik_max_word分析器。比如:

"userName": {
 "type": "text",
 "analyzer": "ik_max_word" //使用ik_max_word模式
}

搜索时,可以根据业务需求的不同,选择两种模式中的任意一种。

 

 

 

倒排索引

读到这里,你或许会有个疑惑:ES为什么要对文本进行分词呢?

答案是:为了建立倒排索引。

索引,我们都知道,是为了加快速数据检索的,那何为倒排索引?从字面上理解,感觉是倒着排序的索引,然而,感觉这东西,有时候其实并不可靠。

倒排索引的英文原名叫Inverted Index,翻译过来应该叫“反向索引”,是相对于“正向索引”(Forward Index)而言的。

正向索引,简单理解就是在文档中找关键词,而反向索引就是通过关键词找文档。

比如,下面三篇文档:

文档ID文档内容
1元数据是数据的数据
2容器就是某种镜像的实例
3设计就是代码,代码就是设计

它们的正向索引结构大致如下:

KeyValue
1元数据,数据
2容器,就是,某种,镜像,实例
3设计,就是,代码,设计

当我们搜索“代码”在那篇文档出现时,就要遍历索引中每个Key下的Value,判断其中是否含有“代码”这个关键词,若有,则返回对应的Key值。当数据量很大时,这样的检索方式将非常消耗资源,相当于每个文档都要遍历一遍。

于是人们就发明了反向索引。还是上面的三篇文档,反向索引的结构大致如下:

KeyValue
元数据1
数据1
容器2
就是2,3
…………
代码3
设计3

同样还是搜索“代码”这个关键字,但是,反向索引只要遍历Key就能立马的找到对应的文档ID,这速度,简直是绿皮换高铁啊。

在 ES中,文档中的每个字段都会有自己的倒排索引。我们可以在 mappings 中去设置index为false来对某些字段不做索引,这样做可以节省存储空间,但同时也会导致这个字段无法搜索了。

"mappings": {
  "_default_": {
    "properties": {
      "userName": {
        "type": "text",
        "analyzer": "standard",
        "index": false
      },
      "profile": {
        "type": "text",
        "index": "false", //指定profile字段不创建索引
      }
    }
  }
}

 

搜索

 

索引有了,数据也存进去了,接下来我们就可以开始进行搜索了。

 

 

常用搜索方式

ES提供了如match匹配、filter过滤器、range范围查询、布尔查询、aggregations聚合等丰富的基于json的搜索方式。

这些搜索方式大致分为两类:基础搜索复合搜索

基础搜索,是对文档中指定的一个或多个字段进行搜索的方式,如term,match或range搜索等:

  • term搜索:

GET /index_name/_search //搜索api
{
  "query": {
    "term": {// term搜索,不对关键字进行分词
    "user": "张三" //搜索user字段为“张三”的文档
    }
  }
}
  • martch搜索:

GET /index_name/_search
{
  "query": {
    "match": { // match搜索,会对关键字进行分词
      // 搜索content字段中含有“搜狐”或“媒体”的文档
      "content": "搜狐媒体" 
    }
  }
}
  • range搜索:

GET /index_name/_search
{
  "query": {
    "range": { // range搜索
      "age": { // 搜索age字段值在10~20之间的文档
        "gte": 10,
        "lte": 20
      }
    }
  }
}

复合搜索,是将多个基础搜索或其他复合搜索组合起来,从而实现更加复杂的逻辑搜索的一种搜索方式,如bool或dis_max搜索等。

  • bool搜索:

POST /index_name/_search
{
  "query": {
    "bool": { // bool搜索
      "must": { // 与
        "term": { "user" : "kimchy" }// term搜索
      }
      "must_not": { // 非
        "range": { // range搜索
          "age" : { "gte" : 10, "lte" : 20 }
        }
      },
      "should" : [ // 或
        { "term" : { "tag" : "wow" } },
        { "match" : { "addr" : "streat" } }// match搜索
      ],
      "minimum_should_match" : 1
    }
  }
}
  • dis_max搜索:

GET /index_name/_search
{
  "query": {
    "dis_max": { // dis_max搜索
      "tie_breaker": 0.7,
      "boost": 1.2,
      "queries": [
        {
          "term": { "age" : 34 } // term搜索
        },
        {
          "term": { "age" : 35 } // term搜索
        }
      ]
    }
  }
}

更多的搜索方式可以参看ES的官方文档Query DSL部分。

 

 

搜索结果字段说明

下面这段json代码就是ES返回的查询结果。

{
  "took": 1,       // 当前搜索花费的毫秒数
  "timed_out": false,     // 当前搜索是否超时
  "_shards": {       // 当前搜索查找的分片情况
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {        // 搜素结果都在这个字段下
    "total": 443,      // 总共命中条件的文档个数
    "max_score": 1,      // 最大的文档评分值
    "hits": [       // 具体的文档数据
      {
        "_index": "test_index",
        "_type": "myDoc",
        "_id": "1380224",
        "_score": 1,
        "_source": {
          "name": "ESTest",
          "desc": "This is a desc test" 
        }
      },
      ......
    ]
  }
}

下面,我们来说说几个重要的字段:

  1. took

    这个字段表示当前搜索总共花费的毫秒数。

  2. time_out

    这个字段告诉我们当前查询是否超时了。当然,一般情况下是不会超时的,因为默认情况下,ES是没有配置超时时间的。我们可以通过修改ES的配置项search.default_search_timeout来设置全局超时时间,或者在搜索请求上通过timeout参数设置当前请求的超时时间,如

    GET /test_index/_search?timeout=10ms
    

    需要注意的是timeout不会停止查询的进行,当查询超时了,在后台,可能依旧在执行查询,尽管超时结果已经被返回。

  3. _shards

    该字段表示参与查询的分片数(total字段),有多少是成功的(successful字段),有多少的是失败的(failed字段)。

  4. hits

    这是所有返回字段中最重要的一个字段,我们需要的文档数据都在这里面。

    hits的第一个字段total告诉我们匹配文档的总数。

    后面的hits数组里,包含着我们需要的文档数据,每个文档都包含_index,_type,_id_score这些基础信息字段和_source源数据字段

    max_score指的是所有匹配文档中_score的最大值,这里涉及到ES文档打分机制的知识点,由于不是本文的重点,这里就不扩展了。

 

分页和排序

 

分页和排序可以说是我们业务开发中最常用的功能了。在ES中使用分页排序查询就像在SQL中写LIMIT和ORDER BY一样简单

想要分页查询,只需要在搜索请求中指定from和size字段即可,如下所示:

GET /_search
{
  "from": 100, //从第100条数据开始
  "size": 10, //查询10条数据
  "query": {
    "term": { "user" : "kimchy" }
  }
}

但是,这种查询方式有一个性能问题,就是,当from很大时,比如5000,ES会查出前5010条数据到内存,然后返回最后10条给我们,这样查询时间将会变长,严重的情况下,内存还有可能会爆掉,oh my god!不过,ES还提供了另一种分页查询方式:scroll查询

POST /_search?scroll=1m
{
  "size": 100,
  "query": {
    "match" : {
      "title" : "elasticsearch"
    }
  }
}

这个查询结果会出现一个scroll_id,就是下面这个,scroll=1m表示这个scroll_id维持的时间是1分钟,如果这1分钟没有继续查询,那么这个id就会失效。

"_scroll_id": "DnF1ZXJ5VGhlbkZldGNoAgAAAAAAEddYFldScjNIUDZKVDBPbk1CeG ZBRmJXVFEAAAAAABHXWRZXUnIzSFA2SlQwT25NQnhmQUZiV1RR"

接下来,我们就可以用这个_scroll_id查询下一页了:

POST /_search/scroll
{
  "scroll": "1m",
  "scroll_id": "DnF1ZXJ5VGhlbkZldGNoAgAAAAAAEddYFldScjNIUDZKVDBPbK1CeGZBRmJXVFEAAAAAABHXWRZXUnIzSFA2SlQwT25NQnhmQUZiV1RR"
}

scroll查询相当于维护了一份当前索引段的快照信息,这个快照信息是你执行这个scroll查询时的快照。在这个查询后的任何新索引进来的数据,都不会在这个快照中查询到。

这种查询方式还有一个弊端,那就是无法指定页数查询,只能按顺序一页一页查询。

排序的话,相对就简单多了,下面是一个带多级排序的查询请求:

GET /_search
{
  "sort" : [
    "name",              // 按name排序
    { "age" : "desc" },  // 按age倒序排序
    "_score"             // 使用ES默认排序
  ],
  "query" : {
    "term" : { "user" : "kimchy" }
  }
}

 

上手实践

光说不练假把式,下面我们以一个简单的用户名查询功能为例,来将上面的部分知识点串起来。

我们知道,用户名一般是由中文,英文和数字组成的。这里,我使用ik中文分词分词器来对用户名进行分词,并analyzer配置为ik_max_word模式,为了将用户名尽量拆分成细粒度的词,增加搜索时的覆盖率。

PUT /user_index
{
  "mappings": {
    "user": {
      "properties": {
        "userName": {
          "type": "text",
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_max_word"
        }
      }
    }
  }
}

 

我们先往索引中put几条数据:

POST /_bulk //批量导入数据接口
{ "index" : { "_index" : "user_index", "_type": "user", "_id" : "1" } }
{ "userName" : "杯具人生" }
{ "index" : { "_index" : "user_index", "_type": "user", "_id" : "2" } }
{ "userName" : "旋转跳跃" }
{ "index" : { "_index" : "user_index", "_type": "user", "_id" : "3" } }
{ "userName" : "一杯茶" }
{ "index" : { "_index" : "user_index", "_type": "user", "_id" : "4" } }
{ "userName" : "茶杯和茶碗" }
{ "index" : { "_index" : "user_index", "_type": "user", "_id" : "5" } }
{ "userName" : "茶禅一味" }

数据添加成功后,我们就可以开始进行搜索了。

我们用match搜索试试。match搜索时,ES会先对关键字进行分词,然后再对每个分词进行检索,比如,搜索用户名中包含“茶杯”的用户:

GET /user_index/user/_search
{
  "query": {
    "match": {
      "userName": "茶杯"
    }
  }
}

 

返回结果中不仅出现了包含“茶杯”的用户,还返回了包含“茶”或“杯”的用户:

"hits": {
  "total": 4,
  "max_score": 2.034024,
  "hits": [
    {
      "_index": "user_index",
      "_type": "user",
      "_id": "4",
      "_score": 2.034024,
      "_source": {
        "userName": "茶杯和茶碗"
      }
    },
    {
      "_index": "user_index",
      "_type": "user",
      "_id": "3",
      "_score": 0.5753642,
      "_source": {
        "userName": "一杯茶"
      }
    },
    {
      "_index": "user_index",
      "_type": "user",
      "_id": "5",
      "_score": 0.2824934,
      "_source": {
        "userName": "茶禅一味"
      }
    },
    {
      "_index": "user_index",
      "_type": "user",
      "_id": "1",
      "_score": 0.25316024,
      "_source": {
        "userName": "杯具人生"
      }
    }
  ]
}

这是因为,ES的match搜索会先对关键字“茶杯”进行分词,分成“茶杯”、“茶”和“杯”三个词,然后再去索引中搜索含有这三个词之一的文本。

如果我们只想搜索包含“茶杯”的用户怎么办呢?解决方法是设置match搜索的operatior参数为“and”,使检索出来结果包含所有的关键字。

GET /user_index/user/_search
{
  "query": {
    "match": {
      "userName": {
          "query": "茶杯",
          "operator": "and"
      }
    }
  }
}

 

现在的返回结果就只有一条了:

"hits": {
  "total": 1,
  "max_score": 2.034024,
  "hits": [
    {
      "_index": "user_index",
      "_type": "user",
      "_id": "4",
      "_score": 2.034024,
      "_source": {
        "userName": "茶杯和茶碗"
      }
    }
  ]
}

 

接下来,我们再插入另一组数据:

POST /_bulk
{ "index" : { "_index" : "user_index", "_type" : "user", "_id" : "6" } }
{ "userName" : "我湖KobeBryant" }
{ "index" : { "_index" : "user_index", "_type" : "user", "_id" : "7" } }
{ "userName" : "Kobe最帅" }
{ "index" : { "_index" : "user_index", "_type" : "user", "_id" : "8" } }
{ "userName" : "彭彭520" }
{ "index" : { "_index" : "user_index", "_type" : "user", "_id" : "9" } }
{ "userName" : "彭于晏小粉" }
{ "index" : { "_index" : "user_index", "_type" : "user", "_id" : "10" } }
{ "userName" : "莫逆于心" }

 

现在,我们来搜索“kobe”试试,看看能不能搜到 我湖KobeBryant 和 Kobe最帅这两条数据:

GET /user_index/user/_search
{
  "query": {
    "match": {
      "userName": {
        "query": "kobe",
        "operator": "and"
      }
    }
  }
}

 

搜索结果如下,ES只返回了Kobe最帅”,却没有 我湖KobeBryant这条数据。

"hits": [
  {
    "_index": "user_index",
    "_type": "user",
    "_id": "7",
    "_score": 0.6099695,
    "_source": {
      "userName": "Kobe最帅"
    }
  }
]

这是为什么呢?

我们已经知道,“我湖KobeBryant”在ES中是被分词后存入倒排索引的,既然没被搜索到,那有没有可能是“KobeBryant”这个词没有被拆分?我们不妨来看看这个用户名的分词情况。

在ES中,提供了专门用来测试分词器的接口:_analyze,我们就用这个接口来看看:

GET /user_index/_analyze
{
  "analyzer": "ik_max_word",
  "text": "我湖KobeBryant"
}

 

返回结果如下:

{
  "tokens": [
    {
      "token": "我",
      "start_offset": 0,
      "end_offset": 1,
      "type": "CN_CHAR",
      "position": 0
    },
    {
      "token": "湖",
      "start_offset": 1,
      "end_offset": 2,
      "type": "CN_WORD",
      "position": 1
    },
    {
      "token": "kobebryant",
      "start_offset": 2,
      "end_offset": 12,
      "type": "ENGLISH",
      "position": 2
    }
  ]
}

可以看到,ik分词器将“我湖KobeBryant”分成了3个词,其中“KobeBryant”被当成了一个词,所以,在倒排索引中,当搜索到key=kobe时,对应的value中是没有“我湖KobeBryant”这条文档的,难怪我们搜索不出来。

找到问题原因了,那就解决它。我们可以改写ik分词器的源码,让它支持驼峰式英文的拆分,但是,这个方法不仅工程量大而且还容易出bug。一个比较简单可行的方法就是在存入ES前对文档进行预处理,比如通过加空格的方式将“KobeBryant”转换成“Kobe Bryant”,因为ik分词器是支持空格分词的。这样的处理方式实现起来不仅简单,而且在后期也方便扩展和维护。

转换以后的分词情况如下:

GET /user_index/_analyze
{
  "analyzer": "ik_max_word",
  "text": "我湖Kobe Bryant"
}

//返回
{
  "tokens": [
    {
      "token": "我",
      "start_offset": 0,
      "end_offset": 1,
      "type": "CN_CHAR",
      "position": 0
    },
    {
      "token": "湖",
      "start_offset": 1,
      "end_offset": 2,
      "type": "CN_WORD",
      "position": 1
    },
    {
      "token": "kobe",
      "start_offset": 2,
      "end_offset": 6,
      "type": "ENGLISH",
      "position": 2
    },
    {
      "token": "bryant",
      "start_offset": 7,
      "end_offset": 13,
      "type": "ENGLISH",
      "position": 3
    }
  ]
}

 

最后,我们就可以将想要用户名给搜索出来啦。

"hits": [
  {
    "_index": "user_index",
    "_type": "user",
    "_id": "6",
    "_score": 1.2336597,
    "_source": {
      "userName": "我湖Kobe Bryant"
    }
},
  {
    "_index": "user_index",
    "_type": "user",
    "_id": "7",
    "_score": 0.6099695,
    "_source": {
      "userName": "Kobe最帅"
    }
  }
]

只不过,这样搜索出来的用户名“我湖Kobe Bryant”是带有空格的,解决办法也简单,我们只要根据查到的用户ID去缓存中拿取正确的用户名就可以了,或者在原文档中增加一个冗余字段(比如:originUserName)来存储原用户名,这样我们就能直接从搜索结果中获取正确的用户名了。

至此,一个用户名查询的核心功能就完成了。

 

总结

ES是一个强大且功能丰富的分布式搜索引擎,配合Spring Boot,我们可以很快的在项目中增加一个搜索功能。本文简单的介绍了一下ES的基础知识,讲解了分析器的功能和组成结构以及常用的搜索接口,最后通过一个简单的示例,将前面的知识串联起来。整体上展示了ES的易用和强大,但这些功能还只是ES的冰山一角,本文只是抛个砖,更多的原理和功能就等着你去探索吧。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值