Elasticsearch 是一个分布式的搜索和分析引擎,可以用于全文检索、结构化检索和分析,并能将这三者结合起来。Elasticsearch 基于 Lucene 开发,是 Lucene 的封装,提供了 REST API 的操作接口,开箱即用。现在是使用最广的开源搜索引擎之一,Wikipedia、Stack Overflow、GitHub 等都基于 Elasticsearch 来构建他们的搜索引擎。

安装

Elasticsearch 安装相对简单,只需安装 JDK[1] 下载解压即可使用。

安装 JDK

yum install java-1.8.0-openjdk # 👈 CentOs

apt-get install openjdk-8-jdk # 👈 Ubuntu

下载完后运行以下命令检查

java -version 
# openjdk version "1.8.0_312"
# OpenJDK Runtime Environment (build 1.8.0_312-b07)
# OpenJDK 64-Bit Server VM (build 25.312-b07, mixed mode)

下载并安装 Elasticsearch

  • 打开网址:https://www.elastic.co/cn/downloads/past-releases#elasticsearch

  • 选择适合的版本点击 Download 按钮,本文选择安装 8.2.3 版本「截止发文 ik 插件最新版为 8.2.3」

    下载流程

  • 选择 LINUX X86_64 右键点击复制链接

    复制链接截图

  • 使用 wget 命令下载并解压到你喜欢的路径

    # 此过程在国内服务器较为缓慢
    wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.2.3-linux-x86_64.tar.gz -O elasticsearch.tar.gz
    tar -zxvf elasticsearch.tar.gz

    # 将 elasticsearch 移动到 elasticsearch 目录
    mv elasticsearch /www/server/elasticsearch

运行

Elasticsearch 不可以使用 root 用户运行,所以在执行以下命令前需要切换至普通用户,并赋予该用户对应命令的执行权限

/www/server/elasticsearch/bin/elasticsearch -d

连接测试

curl 'http://localhost:9200/?pretty'

如果一切正常,将会返回类似如下格式内容

{
"name" : "VM-12-15-centos",
"cluster_name" : "elasticsearch",
"cluster_uuid" : "pYjA-9oFTpGhS295HBxpOg",
"version" : {
"number" : "8.2.3",
"build_flavor" : "default",
"build_type" : "tar",
"build_hash" : "9905bfb62a3f0b044948376b4f607f70a8a151b4",
"build_date" : "2022-06-08T22:21:36.455508792Z",
"build_snapshot" : false,
"lucene_version" : "9.1.0",
"minimum_wire_compatibility_version" : "7.17.0",
"minimum_index_compatibility_version" : "7.0.0"
},
"tagline" : "You Know, for Search"
}

如果遇到报错,是因为 Elasticsearch 8 默认开启了 X-Pack[2],因其不在本文的讨论范围,故现将其关闭,编辑文件 config/elasticsearch.yml 在 98 行左右将 xpack.security.enabled: false 修改为 false,如图

修改配置

基础概念

Elasticsearch 是一个基于文档的 NoSQL 数据库,是一个 分布式RESTful风格的搜索和数据分析引擎,同时也是 Elastic Stack 的核心,集中存储数据。Elasticsearch、Logstash、Kibana 经常被用作日志分析系统,俗称 ELK。

说白了就是一个数据库,既然是数据库,有一些概念是互通的,如下表:

MySQLElasticsearch
表(Table)索引(Index)
记录(Row)文档(Document)
字段(Column)字段(Fields)

基础操作

以下为 Elasticsearch 常用的 API,其中 {index_name} 代表自定义的索引名称,{id} 为文档的 ID「Elasticsearch 的 ID 并非自增,所以需要自行指定」。Elasticsearch 的返回值是 JSON 格式,在对应地址后添加 ?pretty 即可获取格式化的 JSON 内容

请求方式请求路径说明
PUT/{index_name}创建索引
GET/{index_name}查看索引信息
PUT/{index_name}/_mapping修改索引字段
PUT{index_name}/_doc/{id}创建\编辑文档
DELETE{index_name}/_doc/{id}删除文档
GET{index_name}/_doc/{id}读取文档数据
POST{index_name}/_search搜素数据

创建索引

curl -XPUT http://localhost:9200/test_index

查看索引信息

curl http://localhost:9200/test_index

为索引创建类型

curl -H'Content-Type: application/json' -XPUT http://localhost:9200/test_index/_mapping -d'{
"properties": {
"title": { "type": "text", "analyzer": "ik_smart" },
"description": { "type": "text", "analyzer": "ik_smart" },
"price": { "type": "scaled_float", "scaling_factor": 100 }
}
}'
  • properties 表示这个索引中各个字段的定义,其中 key 是字段名称,value 是字段的定义
    • type 定义了字段的数据类型,常用的类型有 text / integer / date / boolean / keyword,可以在 这个连接 查看所有类型
    • analyzer 告诉 Elasticsearch 使用什么方式给这个字段分词,示例中使用了 ik_smart ,这是一个中文分词器,后文会有介绍。

创建文档

curl -H'Content-Type: application/json' -XPUT http://localhost:9200/test_index/_doc/1 -d'{
"title": "iPhone XR",
"description": "全新国产",
"price": 12800
}'

URL 中的 1 是文档的 ID,这点和 Mysql 不太一样,Elasticsearch 的文档 ID 不是自增的,需要我们手动指定。

读取文档

curl http://localhost:9200/test_index/_doc/1

URL 中的 1 即创建文档时的ID

搜索

curl -XPOST -H'Content-Type:application/json' http://localhost:9200/test_index/_search?pretty -d'
{
"query" : { "match" : { "description" : "全新" }}
}'

返回内容

{
"took" : 201,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 0.6931471,
"hits" : [
{
"_index" : "test_index",
"_id" : "1",
"_score" : 0.6931471,
"_source" : {
"title" : "iPhone XR",
"description" : "全新国产",
"price" : 12800
}
}
]
}
}

ik_smart 会把『全新国产』分词成『全新』和『国产』两个词,当我们用 match 来搜索时,Elasticsearch 就会拿搜索词在分词结果中寻找完全匹配的文档。

Elasticsearch 查询

布尔查询

Elasticsearch 的布尔查询(Bool Query)与 SQL 语言中的 and / or 有些类似,可以根据多个条件来筛选文档。

布尔查询下可以有 4 类条件,每个类条件对应的项都是一个数组,数组内的每个项对应一个条件

  • filter 与 SQL 语句中的 and 类似,查询的文档必须同时满足类下的所有条件。
  • mustfilter 相同,区别在于 must 方法会参与 打分,而 filter 不会。
  • should 查询条件不需完全满足,默认情况下只需要满足 should 下的一项即可,可以通过 minimum_should_match 参数来改变需要满足的个数,满足的条件越多对应文档的打分就越高。
  • must_notmust 相反,查询的文档必须不符合此类下的所有条件。

示例如下:

curl -XPOST -H'Content-Type:application/json' http://localhost:9200/test_index/_search?pretty -d'
{
"query": {
"bool": {
"filter": [{
"match": {
"description": "全新"
}
}, {
"match": {
"title": "iPhone"
}
}]
}
}
}'

在上面的示例中,查询条件必须同时满足 title 包含 iPhonedescription 包含 全新

分页查询

分页是数据库查询的一项非常重要的功能,Elasticsearch 提供了 fromsize 两个参数,其含义与 SQL 语句的 limit $offset, $count 语法中的 $offset$count 参数完全一致。

示例如下:

curl -XPOST -H'Content-Type:application/json' http://localhost:9200/test_index/_search?pretty -d'
{
"from": 0,
"size": 10,
"query": {
"bool": {
"filter": [{
"match": {
"description": "全新"
}
}]
}
}
}'

此示例中从第 0 个文档获取,共获取 10 个文档,返回数据中心的 $results['hits']['hits'] 数组包含了此次查询符合条件的文档,$results['hits']['total']['value'] 则代表整个索引中符合查询条件的文档数量。

排序

Elasticsearch 的排序很简单,只需要一个 sort 参数,sort 参数是一个数组,数组下的项可以有多种格式,我们常用的格式是 key value 数组,key 是要排序的字段,value 可以是 desc 或者 asc

示例

curl -XPOST -H'Content-Type:application/json' http://localhost:9200/test_index/_search?pretty -d'
{
"from": 0,
"size": 10,
"sort": [{"price": "desc"}],
"query": {
"bool": {
"filter": [{
"match": {
"description": "全新"
}
}]
}
}
}'

该示例使用 price 对查询结果进行排序。

多字段匹配查询

curl -XPOST -H'Content-Type:application/json' http://localhost:9200/test_index/_search?pretty -d'
{
"from": 0,
"size": 10,
"sort": [{"price": "desc"}],
"query": {
"bool": {
"must": [{
"multi_match": {
"query": "全新",
"fields": ["title^2", "description"]
}
}]
}
}
}'

该示例同时搜索 titledescription 字段,其中 title 字段的权重为 2

中文分词

Elasticsearch 默认提供了一堆的分词器,比如 standardwhitespacelanguage(比如english) 等分词器,但是都对中文分词的效果不太好,为了实现更好的搜索效果,我们需要安装第三方分词器来进行分词,比较常见的就是 ik 分词器。

ik 分词器的安装比较简单,首先前往 github 选择与你的 Elasticsearch 相同的版本下载,下载地址:https://github.com/medcl/elasticsearch-analysis-ik/releases,随后解压至 的 plugins/ik/ 目录下即可。如下下载 8.2.3 版本:

cd /www/server/elasticsearch/plugins
mkdir ik
cd ik
wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v8.2.3/elasticsearch-analysis-ik-8.2.3.zip
unzip elasticsearch-analysis-ik-8.2.3.zip
rm -f elasticsearch-analysis-ik-8.2.3.zip

分面搜索

我们可以在京东上搜索一下『手机』:

京东分面搜索

我们可以看到京东把一些属性聚合在一起并做成了链接,我们可以点击聚合的链接进一步的筛选商品,这个功能就叫做分面搜索,分面搜索是搜索引擎中非常重要的一个功能,可以帮助用户更方便的搜索想要的商品。

想要实现分面搜索就需要用到 Elasticsearch 中的聚合,其与 SQL 语句的 group by 有些类似,但更加灵活和强大

在实现分面搜索之前,我们需要先对索引结构进行调整:

curl -H'Content-Type: application/json' -XPUT http://localhost:9200/test_index/_mapping -d'{
"properties": {
"title": { "type": "text", "analyzer": "ik_smart" },
"description": { "type": "text", "analyzer": "ik_smart" },
"price": { "type": "scaled_float", "scaling_factor": 100 },
"properties": {
"type": "nested",
"properties": {
"name": { "type": "keyword" },
"value": { "type": "keyword" }
}
}
}
}'
curl -H'Content-Type: application/json' -XPUT http://localhost:9200/test_index/_doc/1 -d'{
"title": "iPhone XR",
"description": "全新国产",
"price": 12800,
"properties": [{
"name": "品牌名称",
"value": "苹果"
}, {
"name": "机身内存",
"value": "256G"
}]
}'
curl -H'Content-Type: application/json' -XPUT http://localhost:9200/test_index/_doc/2 -d'{
"title": "VIVO X3",
"description": "全新国产正品",
"price": 3600,
"properties": [{
"name": "品牌名称",
"value": "VIVO"
}, {
"name": "机身内存",
"value": "128G"
}]
}'
curl -H'Content-Type: application/json' -XPUT http://localhost:9200/test_index/_doc/3 -d'{
"title": "iPhone 13 PLUS",
"description": "全新国行",
"price": 9800,
"properties": [{
"name": "品牌名称",
"value": "苹果"
}, {
"name": "机身内存",
"value": "520G"
}]
}'

可以看到我们在 test_index 索引新增了一个 properties 字段用于储存商品属性,并插入了三条测试文档。接下来我们尝试进行搜索:

curl -XPOST -H'Content-Type:application/json' http://localhost:9200/test_index/_search?pretty -d'
{
"from": 0,
"size": 10,
"sort": [{
"price": "desc"
}],
"query": {
"bool": {
"must": [{
"multi_match": {
"query": "全新",
"fields": ["title^2", "description"]
}
}]
}
},
"aggs": {
"properties": {
"nested": {
"path": "properties"
},
"aggs": {
"properties": {
"terms": {
"field": "properties.name"
}
}
}
}
}
}'

以上搜索条件返回如下:

返回示例

以下为对应字段解释

'aggs' => [
// 这里的 properties 是我们给这个聚合操作的命名
// 可以是任意字符串,与商品结构里的 properties 没有必然联系
'properties' => [
// 由于我们要聚合的属性是在 nested 类型字段下的属性,需要在外面套一层 nested 聚合查询
'nested' => [
// 代表我们要查询的 nested 字段名为 properties
'path' => 'properties',
],
// 在 nested 聚合下嵌套聚合
'aggs' => [
// 聚合的名称
'properties' => [
// terms 聚合,用于聚合相同的值
'terms' => [
// 我们要聚合的字段名
'field' => 'properties.name',
],
],
],
]
]

返回信息解释:

// 聚合结果
"aggregations" => [
// 第一层聚合的名称
"properties" => [
// 聚合了 6 个文档,即搜索结果中共有 6 个商品属性
"doc_count" => 6,
// 第二层聚合的名称
"properties" => [
"doc_count_error_upper_bound" => 0,
"sum_other_doc_count" => 0,
// 第二层聚合结果
"buckets" => [
[
// properties.name 为『品牌名称』的属性共有 3 个
"key" => "品牌名称",
"doc_count" => 3,
],
[
"key" => "机身内存",
"doc_count" => 3,
],
],
],
],
],

解决下我们进行第三层聚合,也就是属性值的聚合

curl -XPOST -H'Content-Type:application/json' http://localhost:9200/test_index/_search?pretty -d'
{
"from": 0,
"size": 10,
"sort": [{
"price": "desc"
}],
"query": {
"bool": {
"must": [{
"multi_match": {
"query": "全新",
"fields": ["title^2", "description"]
}
}]
}
},
"aggs": {
"properties": {
"nested": {
"path": "properties"
},
"aggs": {
"properties": {
"terms": {
"field": "properties.name"
},
"aggs": {
"value": {
"terms": {
"field": "properties.value"
}
}
}
}
}
}
}
}'

第三层聚合

此时可以看到几乎已实现类似于京东分面搜索的功能。

同义词搜索

在前面的内容中,我们实现了基本的搜索,某个文档要想在某个关键词搜索结果中出现,就必须在文档内容中出现该关键词,这样就需要给商品配置巨量的关键词才能让商品出现的频率提高, 对运营管理人员不甚友好。为了解决这个问题,我们就需要让搜索引擎支持 同义词搜索,比如用户搜索「苹果手机」,那么包含 「Iphone」 的文档也会出现在搜索结果中。

作为目前最强大的搜索引擎之一,Elasticsearch 是默认支持 同义词搜索 的。

分析器

Elasticsearch 的分析器是由 「字符过滤器」、「分词器」与「字符过滤器」三部分组成,Elasticsearch 内置了一些 「分析器」,同时也允许我们自行定义分析器。

  • 字符过滤器:「字符过滤器」会以字符为单位,根据一定的规则去添加、删除、替换原始字符串,比如将汉字的 「一二三四」替换成阿拉伯数字「1234」,一个「分析器」可以包含 0 个或多个「字符过滤器」。
  • 分词器:「分词器」是根据一定的规则,将原始字符串拆分成一组组的词语,比如前文介绍的 ik_smart 分词器,其可以将「苹果手机」拆分成「苹果」和「手机」两个词语,一个「分析器」有且仅能有一个「分词器」。
  • 词语过滤器:「词语过滤器」会根据「分词器」的分词结果,以词语为单位,根据一定的规则去添加、删除、替换词句,例如同义词过滤器 synonym 可以将「西红柿」替换为 「西红柿」+「番茄」两个词,一个「分析器」可以包含 0 个或多个「词语过滤器」。

自定义分析器

首先我们先创建同义词对应关系的文本文件,格式形如 iPhone,苹果手机 => iPhone,苹果手机,每行一组关键词:

cd /www/server/elasticsearch/config/
mkdir analysis
echo "iPhone,苹果手机 => iPhone,苹果手机" > analysis/synonyms.txt

接下来我们创建一个自定义分析器,我们创建一个新的索引来测试,Elasticsearch 支持在创建索引的同时创建「分析器」:

curl -XPUT -H'Content-Type: application/json' http://localhost:9200/test_synonym?pretty -d' 
{
"settings": {
"index": {
"analysis": {
"filter": {
"synonym_filter": {
"type": "synonym",
"synonyms_path": "analysis/synonyms.txt",
"updateable": true
}
},
"analyzer": {
"ik_smart_synonym": {
"type": "custom",
"tokenizer": "ik_smart",
"filter": ["synonym_filter"]
}
}
}
}
}
}'

在这个请求中我们在 analysis 下的 filter 中定义了一个名为 synonym_filter 的『同义词词语过滤器』,并且指定同义词的字典路径为 analysis/synonyms.txt;同时在 analyzer 下定义了一个名为 ik_smart_synonym 的「自定义分析器」,并指定 ik_smart 作为「分词器」,上面定义的 synonym_filter 作为「词语过滤器」。

测试

我们来测试一下这个分析器的效果:

curl -H'Content-Type: application/json' http://localhost:9200/test_synonym/_analyze?pretty -d '{"text": "苹果手机","analyzer":"ik_smart_synonym"}'

返回内容如下:

{
"tokens" : [
{
"token" : "iphone",
"start_offset" : 0,
"end_offset" : 4,
"type" : "SYNONYM",
"position" : 0
},
{
"token" : "苹果",
"start_offset" : 0,
"end_offset" : 2,
"type" : "SYNONYM",
"position" : 0
},
{
"token" : "手机",
"start_offset" : 2,
"end_offset" : 4,
"type" : "SYNONYM",
"position" : 1
}
]
}

可以看到「分析器」将关键词 苹果手机 拆分成为了 iphone苹果手机 三个词。

在 php 中使用 Elasticsearch

  1. 引入 Composer 包

Elasticsearch 官方提供了 Composer 包,因为不同版本的 Elasticsearch 的 API 略有不同,所以在引入时需要注意要指定版本,例如引入 8.x 版本:

composer require elasticsearch/elasticsearch '^7.0'
  1. 实例化 Elasticsearch 实例
$builder = Elastic\Elasticsearch\ClientBuilder::create()->setHosts(['localhost:9200']);

$client = $builder->build();

// 获取 test_index 索引中 ID 为 1 的文档
$client->get(['index' => 'test_index', 'id' => 1]);

SDK 详细使用说明可参考官方文档:https://www.elastic.co/guide/cn/elasticsearch/php/current/_quickstart.html


  1. 1.JDK 全称 Java Development Kit。它是 Java 语言的软件开发工具包,主要用于移动设备、嵌入式设备上的 java 应用程序。JDK 是整个 java 开发的核心,它包含了 JAVA 的运行环境(JVM+Java 系统类库)和 JAVA 工具。
  2. 2.x-pack 是 elasticsearch 的一个扩展包,集安全,警告,监视,图形和报告功能于一体,可以轻松的启用或者关闭一些功能。