PHP中使用ElasticSearch(二)(https://www.cnblogs.com/codeAB/p/10288273.html)

首先从ES的支持的字段说起,ES文档中字段有多种类型 官方文档

这几个比较常用:

text,keyword,integer,float,boolean,object,geo_point(地理坐标),geo_shape(描述地理区域),date.

注:不要以为date只能表示 2015-01-01 这种类型,2015/01/01 12:10:30这种类型也一样可以,不像MySQL里面时间还分很多种细分的类型,ES就一个date类型。

注意:这里没有列出array,在ES中,array不是一种单独的类型,但是你可以往ES里面存数组,这个地方有点难以理解,
举个例子: 文档里面我要定义一个字段叫 friends ,用来存储用户的朋友列表,
用 text 类型定义字段:

'friends' => [
    'type' => 'text'
]

看似这仅仅定义了一个text类型的字段,并不是我们想要的数组,重点解释来了,虽然我们的friends是字符串类型,但是
我们在存入数据的时候 往 friends里面存储两个或者三个字符串,他就变成数组了!
其实这句话描述还是不准确,不是从字符串变成数组,而是多个字符串组成了一个数组!

插入数据:

复制代码
$this->putDoc([
    'first_name' => $this->faker->name,
    'last_name' => $this->faker->name,
    'age' => $this->faker->numberBetween(20,80),
    'height' => (float)($this->faker->numberBetween(160,200)/100),
    'friends' => [
        $this->faker->name(),
        $this->faker->name(),
        $this->faker->name(),
        $this->faker->name()
    ]
]);
复制代码

这个putDoc来得有点突然是不是?因为这是延续上一篇文章的续集,请看上一篇文章 使用PHP操作ElasticSearch

注意:faker是用来随机生成数据的,详细信息参考谷歌。

你看,friends明明是 text 类型,但是我在插入的时候插入了多条数据,他变成了一个字符串类型的集合,前面我说他是数组,这个地方
我把他说成是集合,这回更准确了,因为数组在ES中查询是不能保证顺序的,所以集合更准确,官方文档中也表示他更像集合
再说一下object,模板里面这样定义:

复制代码
'info' => [
    'type' => 'object',
    'properties' => [
        'country' => [
            'type' => 'text',
            'analyzer' => 'ik_max_word'
        ],
        'sex' => [
            'type' => 'keyword'
        ]
    ]
]
复制代码

这里定义了一个对象文档,指定了下面两个属性的基本信息,但是不代表这个对象就只能存储两个属性,
比如我还可以在添加文档的时候往里面添加一个skin 肤色的字段,完全没有问题,只不过这里定义的两个
字段我们设置了类型和具体的analyzer,没有在这里定义,但是我们实际上添加了的字段比如skin,ES会
自动设置正确的类型,以及默认的analyzer.

存入数据:

'info' => [
    'country' => ['中国','印度','法国','英国','瑞士','刚果共和国'][random_int(0,5)],
    'sex' => ['男','女'][random_int(0,1)],
    'skin' => ['白','黑','黄'][random_int(0,2)],
]

还有一个keyword,他和text都表示字符串,区别在于 keyword里面的值不会被分词器分词,text里面的值会被分词器智能拆分,
记住这一点,这一点非常重要,后面还会讲到这个区别。

在定义text字段的时候 analyzer和index你需要清楚的地方:

'last_name' => [
    'type' => 'text',
    //'analyzer' => 'standard', // 这个地方不设置analyzer会默认standard
    //'index' => false
]

analyzer不设置analyzer会默认standard
对于老版本的 ES,这里的index允许设置为 analyzed/not_analyzed/no,
大部分网络上的文章都是这样讲的,但是,最新版本已经移除了这些选项,
现在只能是 true或false,所以我建议当你有一点基础后,通读一下官方最新文档,虽然是英文的
如果这里设置为false,这个字段不加入索引,不能在查询条件中出现,默认为true
等一下,这里突然发现有点不对劲,以前可以设置 分析/不分析/不索引,现在只能设置索引和不索引了,
如果想实现索引且不分析,那keyword类型刚好符合,而text字段是为分析而生的。

ES中的搜索分两个概念,匹配和过滤
匹配通常针对的是 text 类型的字段,而过滤针通常对的精确的类型,比如 integer,keyword,date等,
之所以加了通常二字,说明他们之间没有明确的规定,匹配可以去匹配data,integer等类型,过滤也可以去过滤text字段,
通过匹配的方式去找精确类型通常不会出现什么问题,通过过滤去找text类型的数据通常会得到意外的结果。
谨记:如果没有特殊情况,匹配针对text类型,过滤针对其他类型,
但是对于精确类型使用过滤的性能通常比匹配更高,所以能使用过滤的地方都过滤。
注意:这里要区别一下MySQL中的过滤概念,MySQL中的过滤是对查找后的数据进行过滤,而在ES中,过滤和匹配都等同于MySQL中的查找,
匹配适合查找模糊数据,过滤适合查找精确数据而已。
为了简化代码,下面的搜索都基于一下这份代码,更改的部分只是 $query:

复制代码
$params = [
    'index' => $this->index,
    'type' => $this->type,
    'body' => array_merge([
        'from' => $from,
        'size' => $size
    ],$query)
];
复制代码

常用的过滤:
term(精确查找)
查找倪玲为44的数据

复制代码
$query = [
    'query' => [
        'term' => [
            'age' => 44
        ]
    ]
];
复制代码

terms(精确查找多个字段)
查找年龄为 44或55或66的数据

复制代码
$query = [
    'query' => [
        'terms' => [
            'age' => [44,55,66]
        ]
    ]
];
复制代码

range(范围查找),

复制代码
$query = [
    'query' => [
        'range' => [
            'age' => [
                'gt' => 43,
                'lt' => 45
            ]
        ]
    ]
];
复制代码

exists(等同于MySQL中的 is not null),
查找存在age属性的文档

复制代码
$query = [
    'query' => [
        'exists' => [
            'field' => 'age'
        ]
    ]
];
复制代码

missing(等同于 MySQL中的 is null),
注意:这个过滤方法在2.x版本就废弃了,请使用 must_not 嵌套 exists 来实现
bool(用来组合其他过滤条件,包含 must,must_not,should操作)

复制代码
$query = [
    'query' => [
        'bool' => [
            'should' => [
                'range' => [
                    'height' => ['gt' => 1.8]
                ]
            ],
            'must_not' => [
                'term' => [
                    'info.sex' => '女'
                ]
            ],
            'must' => [
                [
                    'term' => [
                        'info.country' => '法国'
                    ]
                ],
                [
                    'term' => [
                        'info.skin' => '白'
                    ]
                ]
            ]
        ]
    ]
];
复制代码

上面这个查询的意思是,身高应该大于1.8,性别不能是女,国家是法国且肤色是黑色。
这里country实际上是text类型,但是我任然通过过滤的方法找到了正确的值,但是这种方式是非常危险的,
这里之所以找到了正确的值,是因为country类型很简单,碰巧
analyzer(这里用的ik,如果是standard就没那么好运了)没有对其进行拆分。

常用的查询:
match(匹配一个字段)

复制代码
$query = [
    'query' => [
        'match' => [
            'height' => '1.8'
        ]
    ]
];
复制代码

match_all(匹配所有文档,相当于没有条件)
等于是 $query = []
multi_match(匹配多个字段)
匹配姓和名里面包含 'Riley Libby Preston' 的数据

复制代码
$query = [
    'query' => [
        'multi_match' => [
            'query' => 'Riley Libby Preston',
            'fields' => ['first_name','last_name']
        ]
    ]
];
复制代码

bool(用来组合其他匹配条件,包含 must,must_not,should操作)

复制代码
$query = [
    'query' => [
        'bool' => [
            'should' => [
                'match' => [
                    'height' => '1.8'
                ]
            ],
            'must_not' => [
                'match' => [
                    'info.sex' => '男'
                ]
            ]
        ]
    ]
];
复制代码

在实际使用中,匹配和过滤都是混合搭配使用的,比如:

复制代码
$query = [
    'query' => [
        'bool' => [
            'should' => [
                'match' => [
                    'height' => '1.8'
                ]
            ],
            'must_not' => [
                'term' => [
                    'info.sex' => '女'
                ]
            ],
            'must' => [
                [
                    'match' => [
                        'info.country' => '法国'
                    ]
                ],
                [
                    'match' => [
                        'info.skin' => '白'
                    ]
                ]
            ]
        ]
    ]
];
复制代码

match时常会出现一些怪异的现象,如果你不清楚你用的analyzer,比如这个例子:

复制代码
$query = [
    'query' => [
        'bool' => [
            'must' => [
                [
                    'match' => [
                        'last_name' => 'Hamill'
                    ]
                ],
                [
                    'match' => [
                        'info.country' => '法国'
                    ]
                ]
            ]
        ]
    ]
];
复制代码

这个查询的需求是选出last_name中匹配到Hamill并且国家匹配到法国的结果,但是查询的结果是这样的,
last_name 的中包含 Hamill,在我们意料之中,但是 country出现了英国,法国等很多国家,这个太意外了,
现在来改造一下这个 $query,很小的改造,只需要把法国改成法,再次查询,这次的结果完美的实现了我们的需求。
原因在于:
文档中的法国二字被analyzer拆分成 (法,国) 存储在索引中,同理,英国被拆分为 (英,国),
现在你搜索法国的时候,你的这个搜索词默认会被拆分成 (法,国),然后拿着这两个词去分别查找,
第一个法可以匹配所有法国,第二个国字可以匹配到英国,美国等所有包含国字的结果。
现在你知道结果的形成原因了。
这个很大程度上上取决于你使用的analyzer,不同的analyzer分词的策略不一样,所以你有必要先搞明白你用的分词器
他的大概分词策略,上面这个例子没有指定analyzer,是ES默认的分词器在起作用,当我指定analyzer为 ik_max_word后,情况
发生了变化,这个时候法国被当成了一个整体,没有被拆分。
可以通过简单的测试来看看具体分词器的分词方式:

复制代码
$params = [
    'body' => [
        'analyzer' => 'ik_max_word', //默认 standard
        'text' => '我在广场吃着炸鸡'
    ]
];
return $this->EsClient->indices()->analyze($params);
复制代码

默认分词器standard会把这句话简单的拆分成单个字,而ik相对就更懂中文一点,拆分出来的词更有语义化,
大部分的analyzer对英文的分词都基于空格拆分

 

 

二、基本概念

2.1 Node 与 Cluster

Elastic 本质上是一个分布式数据库,允许多台服务器协同工作,每台服务器可以运行多个 Elastic 实例。

单个 Elastic 实例称为一个节点(node)。一组节点构成一个集群(cluster)。

2.2 Index

Elastic 会索引所有字段,经过处理后写入一个反向索引(Inverted Index)。查找数据的时候,直接查找该索引。

所以,Elastic 数据管理的顶层单位就叫做 Index(索引)。它是单个数据库的同义词。每个 Index (即数据库)的名字必须是小写。

下面的命令可以查看当前节点的所有 Index。

$ curl -X GET 'http://localhost:9200/_cat/indices?v'


2.3 Document

Index 里面单条的记录称为 Document(文档)。许多条 Document 构成了一个 Index。

Document 使用 JSON 格式表示,下面是一个例子。

{
  "user": "张三",
  "title": "工程师",
  "desc": "数据库管理"
}

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

2.4 Type

Document 可以分组,比如weather这个 Index 里面,可以按城市分组(北京和上海),也可以按气候分组(晴天和雨天)。这种分组就叫做 Type,它是虚拟的逻辑分组,用来过滤 Document。

不同的 Type 应该有相似的结构(schema),举例来说,id字段不能在这个组是字符串,在另一个组是数值。这是与关系型数据库的表的一个区别。性质完全不同的数据(比如products和logs)应该存成两个 Index,而不是一个 Index 里面的两个 Type(虽然可以做到)。

下面的命令可以列出每个 Index 所包含的 Type。

$ curl 'localhost:9200/_mapping?pretty=true'

根据规划,Elastic 6.x 版只允许每个 Index 包含一个 Type,7.x 版将会彻底移除 Type。

三、新建和删除 Index

新建 Index,可以直接向 Elastic 服务器发出 PUT 请求。下面的例子是新建一个名叫weather的 Index。

$ curl -X PUT 'localhost:9200/weather'

服务器返回一个 JSON 对象,里面的acknowledged字段表示操作成功。

{
  "acknowledged":true,
  "shards_acknowledged":true
}

然后,我们发出 DELETE 请求,删除这个 Index。

$ curl -X DELETE 'localhost:9200/weather'

四、中文分词设置

首先,安装中文分词插件。这里使用的是 ik,也可以考虑其他插件(比如 smartcn)。

$ ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v5.5.1/elasticsearch-analysis-ik-5.5.1.zip

上面代码安装的是5.5.1版的插件,与 Elastic 5.5.1 配合使用。
接着,重新启动 Elastic,就会自动加载这个新安装的插件。
然后,新建一个 Index,指定需要分词的字段。这一步根据数据结构而异,下面的命令只针对本文。基本上,凡是需要搜索的中文字段,都要单独设置一下。

$ curl -X PUT 'localhost:9200/accounts' -d '
{
  "mappings": {
    "person": {
      "properties": {
        "user": {
          "type": "text",
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_max_word"
        },
        "title": {
          "type": "text",
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_max_word"
        },
        "desc": {
          "type": "text",
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_max_word"
        }
      }
    }
  }
}'

上面代码中,首先新建一个名称为accounts的 Index,里面有一个名称为person的 Type。person有三个字段。

◾user
◾title
◾desc

这三个字段都是中文,而且类型都是文本(text),所以需要指定中文分词器,不能使用默认的英文分词器。
Elastic 的分词器称为 analyzer。我们对每个字段指定分词器。

"user": {
  "type": "text",
  "analyzer": "ik_max_word",
  "search_analyzer": "ik_max_word"
}

上面代码中,analyzer是字段文本的分词器,search_analyzer是搜索词的分词器。ik_max_word分词器是插件ik提供的,可以对文本进行最大数量的分词。

五、数据操作

5.1 新增记录

向指定的 /Index/Type 发送 PUT 请求,就可以在 Index 里面新增一条记录。比如,向/accounts/person发送请求,就可以新增一条人员记录。

$ curl -X PUT 'localhost:9200/accounts/person/1' -d '
{
  "user": "张三",
  "title": "工程师",
  "desc": "数据库管理"
}' 

服务器返回的 JSON 对象,会给出 Index、Type、Id、Version 等信息。

{
  "_index":"accounts",
  "_type":"person",
  "_id":"1",
  "_version":1,
  "result":"created",
  "_shards":{"total":2,"successful":1,"failed":0},
  "created":true
}

如果你仔细看,会发现请求路径是/accounts/person/1,最后的1是该条记录的 Id。它不一定是数字,任意字符串(比如abc)都可以。

新增记录的时候,也可以不指定 Id,这时要改成 POST 请求。

$ curl -X POST 'localhost:9200/accounts/person' -d '
{
  "user": "李四",
  "title": "工程师",
  "desc": "系统管理"
}'

上面代码中,向/accounts/person发出一个 POST 请求,添加一个记录。这时,服务器返回的 JSON 对象里面,_id字段就是一个随机字符串。


{
  "_index":"accounts",
  "_type":"person",
  "_id":"AV3qGfrC6jMbsbXb6k1p",
  "_version":1,
  "result":"created",
  "_shards":{"total":2,"successful":1,"failed":0},
  "created":true
}

注意,如果没有先创建 Index(这个例子是accounts),直接执行上面的命令,Elastic 也不会报错,而是直接生成指定的 Index。所以,打字的时候要小心,不要写错 Index 的名称。

5.2 查看记录

向/Index/Type/Id发出 GET 请求,就可以查看这条记录。

$ curl 'localhost:9200/accounts/person/1?pretty=true'

上面代码请求查看/accounts/person/1这条记录,URL 的参数pretty=true表示以易读的格式返回。

返回的数据中,found字段表示查询成功,_source字段返回原始记录。


{
  "_index" : "accounts",
  "_type" : "person",
  "_id" : "1",
  "_version" : 1,
  "found" : true,
  "_source" : {
    "user" : "张三",
    "title" : "工程师",
    "desc" : "数据库管理"
  }
}

如果 Id 不正确,就查不到数据,found字段就是false。


$ curl 'localhost:9200/weather/beijing/abc?pretty=true'

{
  "_index" : "accounts",
  "_type" : "person",
  "_id" : "abc",
  "found" : false
}

5.3 删除记录
删除记录就是发出 DELETE 请求。

$ curl -X DELETE 'localhost:9200/accounts/person/1'

这里先不要删除这条记录,后面还要用到。

5.4 更新记录

更新记录就是使用 PUT 请求,重新发送一次数据。


$ curl -X PUT 'localhost:9200/accounts/person/1' -d '
{
    "user" : "张三",
    "title" : "工程师",
    "desc" : "数据库管理,软件开发"
}' 

{
  "_index":"accounts",
  "_type":"person",
  "_id":"1",
  "_version":2,
  "result":"updated",
  "_shards":{"total":2,"successful":1,"failed":0},
  "created":false
}

上面代码中,我们将原始数据从"数据库管理"改成"数据库管理,软件开发"。 返回结果里面,有几个字段发生了变化。

"_version" : 2,
"result" : "updated",
"created" : false

可以看到,记录的 Id 没变,但是版本(version)从1变成2,操作类型(result)从created变成updated,created字段变成false,因为这次不是新建记录。

六、数据查询

6.1 返回所有记录

使用 GET 方法,直接请求/Index/Type/_search,就会返回所有记录。


$ curl 'localhost:9200/accounts/person/_search'

{
  "took":2,
  "timed_out":false,
  "_shards":{"total":5,"successful":5,"failed":0},
  "hits":{
    "total":2,
    "max_score":1.0,
    "hits":[
      {
        "_index":"accounts",
        "_type":"person",
        "_id":"AV3qGfrC6jMbsbXb6k1p",
        "_score":1.0,
        "_source": {
          "user": "李四",
          "title": "工程师",
          "desc": "系统管理"
        }
      },
      {
        "_index":"accounts",
        "_type":"person",
        "_id":"1",
        "_score":1.0,
        "_source": {
          "user" : "张三",
          "title" : "工程师",
          "desc" : "数据库管理,软件开发"
        }
      }
    ]
  }
}


上面代码中,返回结果的 took字段表示该操作的耗时(单位为毫秒),timed_out字段表示是否超时,hits字段表示命中的记录,里面子字段的含义如下。

◾total:返回记录数,本例是2条。
◾max_score:最高的匹配程度,本例是1.0。
◾hits:返回的记录组成的数组。

返回的记录中,每条记录都有一个_score字段,表示匹配的程序,默认是按照这个字段降序排列。

6.2 全文搜索

Elastic 的查询非常特别,使用自己的查询语法,要求 GET 请求带有数据体。


$ curl 'localhost:9200/accounts/person/_search'  -d '
{
  "query" : { "match" : { "desc" : "软件" }}
}'

上面代码使用 Match 查询,指定的匹配条件是desc字段里面包含"软件"这个词。返回结果如下。


{
  "took":3,
  "timed_out":false,
  "_shards":{"total":5,"successful":5,"failed":0},
  "hits":{
    "total":1,
    "max_score":0.28582606,
    "hits":[
      {
        "_index":"accounts",
        "_type":"person",
        "_id":"1",
        "_score":0.28582606,
        "_source": {
          "user" : "张三",
          "title" : "工程师",
          "desc" : "数据库管理,软件开发"
        }
      }
    ]
  }
}

Elastic 默认一次返回10条结果,可以通过size字段改变这个设置。


$ curl 'localhost:9200/accounts/person/_search'  -d '
{
  "query" : { "match" : { "desc" : "管理" }},
  "size": 1
}'

上面代码指定,每次只返回一条结果。

还可以通过from字段,指定位移。


$ curl 'localhost:9200/accounts/person/_search'  -d '
{
  "query" : { "match" : { "desc" : "管理" }},
  "from": 1,
  "size": 1
}'

上面代码指定,从位置1开始(默认是从位置0开始),只返回一条结果。

6.3 逻辑运算

如果有多个搜索关键字, Elastic 认为它们是or关系。


$ curl 'localhost:9200/accounts/person/_search'  -d '
{
  "query" : { "match" : { "desc" : "软件 系统" }}
}'

上面代码搜索的是软件 or 系统。

如果要执行多个关键词的and搜索,必须使用布尔查询。


$ curl 'localhost:9200/accounts/person/_search'  -d '
{
  "query": {
    "bool": {
      "must": [
        { "match": { "desc": "软件" } },
        { "match": { "desc": "系统" } }
      ]
    }
  }
}'

https://www.elastic.co/guide/cn/elasticsearch/php/current/_search_operations.html

https://www.elastic.co/guide/en/elasticsearch/reference/current/full-text-queries.html

Elasticsearch系列---多字段搜索

本篇介绍一下multi_match的best_fields、most_fields和cross_fields三种语法的场景和简单示例。

最佳字段

bool查询采取"more-matches-is-better"匹配越多分越高的方式,所以每条match语句的评分结果会被加在一起,从而为每个文档提供最终的分数_score。能与两条语句同时匹配的文档会比只与一条语句匹配的文档得分要高,但有时这样也会带来一些与期望不符合的情况,我们举个例子:

我们以英文儿歌为案例背景,我们这样搜索:

GET /music/children/_search
{
  "query": {
    "bool": {
      "should": [
        { "match": { "name":  "brush mouth" }},
        { "match": { "content": "you sunshine"   }}
      ]
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

结果响应(有删减)

{
  "hits": {
    "total": 2,
    "max_score": 1.7672573,
    "hits": [
      {
        "_id": "4",
        "_score": 1.7672573,
        "_source": {
          "name": "brush your teeth",
          "content": "When you wake up in the morning it's a quarter to one, and you want to have a little fun You brush your teeth"
        }
      },
      {
        "_id": "3",
        "_score": 0.7911257,
        "_source": {
          "name": "you are my sunshine",
          "content": "you are my sunshine, my only sunshine, you make me happy, when skies are gray"
        }
      }
    ]
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

预期的结果是"you are my sunshine"要排在"brush you teeth"前面,实际结果却相反,为什么呢?

我们按照匹配的方式复原一下_score的评分过程:每个query的分数,乘以匹配的query的数量,除以总query的数量。

我们来看一下匹配情况:
文档4的name字段包含brush,content字段包含you,所以两个match都能得到评分。
文档3的name字段不匹配,但是content字段包含you和sunshine,命中一个match,只能得一项的分。
结果文档4的得分会高一些。

但我们仔细想一想,文档4虽然两个match都匹配了,但每个match只匹配了其中一个关键词,文档3只匹配了一个match,却是同时匹配了两个连续的关键词,按我们的预期,一个field上匹配了两个连续关键词的相关性应该高一些,简单的把多个match的得分加起来,虽然分高一些,但不是我们期望的首位。

我们探寻的是最佳字段匹配,某一个字段匹配到了尽可能多的关键词,让它排在前面;而不是更多的field匹配了关键词,就让它在前面。

我们使用dis_max语法查询,优先将最佳匹配的评分作为查询的评分结果返回,请求如下:

GET /music/children/_search
{
  "query": {
    "dis_max": {
      "queries": [
        { "match": { "name":  "brush mouth" }},
        { "match": { "content": "you sunshine"   }}
      ]
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

结果响应(有删减)

{
  "hits": {
    "total": 2,
    "max_score": 1.0310873,
    "hits": [
      {
        "_id": "4",
        "_score": 1.0310873,
        "_source": {
          "name": "brush your teeth",
          "content": "When you wake up in the morning it's a quarter to one, and you want to have a little fun You brush your teeth"
        }
      },
      {
        "_id": "3",
        "_score": 0.7911257,
        "_source": {
          "name": "you are my sunshine",
          "content": "you are my sunshine, my only sunshine, you make me happy, when skies are gray"
        }
      }
    ]
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

呃,结果排序还是不理想,不过可以看到_id为4的评分由之前的1.7672573降为1.0310873,说明dis_max操作后,能够影响评分,只是案例取得不好,_id为3的记录评分实在太低了,只有0.7911257,仍然不能改变次序。

最佳字段查询调优

上一节的dis_max查询会采用单个最佳匹配字段,而忽略其他的匹配项,这对精准化搜索还是不够合理,我们需要其他匹配项的匹配结果按一定权重参与最后的评分,权重可以自己设置。

我们可以加一个tie_breaker参数,这样就可以把其他匹配项的结果也考虑进去,它的使用规则如下:

  1. tie_breaker的值介于0-1之间,是个小数,建议此值范围0.1-0.4.
  2. dis_max负责获取最佳匹配语句的分数_score,其他匹配语句的_score与tie_breaker相乘。
  3. 对评分求和并归一化处理。

所以说,加上了tie_breaker,会考虑所有的匹配条件,但最佳匹配语句仍然占大头。

请求示例:

GET /music/children/_search
{
  "query": {
    "dis_max": {
      "queries": [
        { "match": { "name":  "brush mouth" }},
        { "match": { "content": "you sunshine"   }}
      ],
      "tie_breaker": 0.3
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

multi_match查询

best_fields

best-fields策略:将某一个field匹配了尽可能多关键词的文档优先返回回来。

如果我们在多个字段上使用相同的搜索字符串进行搜索,请求语法可以冗长一些:

GET /music/children/_search
{
  "query": {
    "dis_max": {
      "queries": [
        {
          "match": {
            "name": {
              "query": "you sunshine",
              "boost": 2,
              "minimum_should_match": "50%"
            }
          }
        },
        {
          "match": {
            "content": "you sunshine"
          }
        }
      ],
      "tie_breaker": 0.3
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

可以用multi_match将搜索请求简化,multi_match支持boost、minimum_should_match、tie_breaker参数的设置:

GET /music/children/_search
{
  "query": {
    "multi_match": {
      "query": "you sunshine",
      "type": "best_fields", 
      "fields": ["name^2","content"],
      "minimum_should_match": "50%",
      "tie_breaker": 0.3
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

而boost、minimum_should_match、tie_breaker参数的一个显著作用就是去长尾,长尾数据比如说我们搜索4个关键词,但很多文档只匹配1个,也显示出来了,这些文档其实不是我们想要的,可以通过这几个参数的设置,将门槛提高,过滤掉长尾数据。

most_fields

most-fields策略:尽可能返回更多field匹配到某个关键词的doc,优先返回回来。

常用方式是我们为同一文本字段,建立多种方式的索引,词干提取分析处理的和原文存储的都做一份,这样能提高匹配的精准度。

我们拿music索引举个例子(摘抄mapping片断信息)。我们做一点小修改:

PUT /music
{
  "mappings": {
      "children": {
        "properties": {
          "name": {
            "type": "text",
            "analyzer": "english"
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          },
          "content": {
            "type": "text",
            "analyzer": "english"
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          }
        }
      }
    }
}
  • 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

比如name和content字段,我们除了有text类型的字段,还有keyword类型的子字段,text会做分词、英文词干处理,keywork则保持原样,搜索内容的时候,我们可以使用name或name.keyword两个字段同时进行搜索,示例:

GET /music/children/_search
{
  "query": {
    "multi_match": {
      "query": "brushed",
      "type": "most_fields", 
      "fields": ["name","name.keyword"]
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

我们搜索name及name.keyword两个字段,由于name字段的分词器是english,搜索字符串brushed经过提取词干后变成brush,是能匹配到结果的,name.keyword则无法匹配,最终还是有文档结果返回。如果只对name.keyword字段搜索,则不会有结果返回。

这个就是most_fields的策略,希望对同一个文本进行多种索引,搜索时各种索引的结果都参与,这样就能尽可能地多返回结果。

与best_fields区别

  1. best_fields,是对多个field进行搜索,挑选某个field匹配度最高的那个分数,同时在多个query最高分相同的情况下,在一定程度上考虑其他query的分数。简单来说,你对多个field进行搜索,就想搜索到某一个field尽可能包含更多关键字的数据
  • 优点:通过best_fields策略,以及综合考虑其他field,还有minimum_should_match支持,可以尽可能精准地将匹配的结果推送到最前面
  • 缺点:除了那些精准匹配的结果,其他差不多大的结果,排序结果不是太均匀,没有什么区分度了

实际的例子:百度之类的搜索引擎,最匹配的到最前面,但是其他的就没什么区分度了

  1. most_fields,综合多个field一起进行搜索,尽可能多地让所有field的query参与到总分数的计算中来,此时就会是个大杂烩,出现类似best_fields案例最开始的那个结果,结果不一定精准,某一个document的一个field包含更多的关键字,但是因为其他document有更多field匹配到了,所以排在了前面;所以需要建立更多类似name.keyword,name.std这样的field,尽可能让某一个field精准匹配query string,贡献更高的分数,将更精准匹配的数据排到前面
  • 优点:将尽可能匹配更多field的结果推送到最前面,整个排序结果是比较均匀的
  • 缺点:可能那些精准匹配的结果,无法推送到最前面

实际的例子:wiki,明显的most_fields策略,搜索结果比较均匀,但是的确要翻好几页才能找到最匹配的结果

cross_fields

有些实体对象在设计中,可能会使用多个字段来标识一个信息,如地址,常见存储方案可以是省、市、区、街道四个字段,分别存储,合起来才是完整的地址信息。再如人名,国外有first name和last name之分。

遇到针对这种字段的搜索,我们叫做跨字段实体搜索,我们要注意哪些问题呢?

我们回顾music索引的author字段,就是设计成了author_first_name和author_last_name的结构,我们试着对它来演示一下跨字段实体搜索。

使用most_fields查询

GET /music/children/_search
{
  "query": {
    "multi_match": {
      "query":       "Peter Raffi",
      "type":        "most_fields",
      "fields":      [ "author_first_name", "author_last_name" ]
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

响应的结果:

{
  "hits": {
    "total": 2,
    "max_score": 1.3862944,
    "hits": [
      {
        "_id": "4",
        "_score": 1.3862944,
        "_source": {
          "id": "55fa74f7-35f3-4313-a678-18c19c918a78",
          "author_first_name": "Peter",
          "author_last_name": "Raffi",
          "author": "Peter Raffi",
          "name": "brush your teeth",
          "content": "When you wake up in the morning it's a quarter to one, and you want to have a little fun You brush your teeth"
        }
      },
      {
        "_id": "1",
        "_score": 0.2876821,
        "_source": {
          "author_first_name": "Peter",
          "author_last_name": "Gymbo",
          "author": "Peter Gymbo",
          "name": "gymbo",
          "content": "I hava a friend who loves smile, gymbo is his name"
        }
      }
    ]
  }
}
  • 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

看起来结果是对的,"Peter Raffi"按预期排在首位,但Peter Gymbo也出来的,这不是我们想要的结果,只是由于数据量太少的原因,长尾数据没有显示出来,most_fields查询引出的问题有如下3个:

  1. 只是找到尽可能多的field匹配的doc,而不是某个field完全匹配的doc
  2. most_fields,没办法用minimum_should_match去掉长尾数据,就是匹配的特别少的结果
  3. TF/IDF算法,比如Peter Raffi和Peter Gymbo,搜索Peter Raffi的时候,由于first_name中很少有Raffi的,所以query在所有document中的频率很低,得到的分数很高,可能会出现非预期的次序。

使用copy_to合并字段

copy_to语法可以将多个字段合并在一起,这样就可以解决跨实体字段的问题,带来的副面影响就是占用更多的存储空间,copy_to的示例如下:

PUT /music/_mapping/children
{
  "properties": {
      "author_first_name": {
          "type":     "text",
          "copy_to":  "author_full_name" 
      },
      "author_last_name": {
          "type":     "text",
          "copy_to":  "author_full_name" 
      },
      "author_full_name": {
          "type":     "text"
      }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

注意这个请求需要在建立索引时执行,局限性比较大。
所以案例设计时,专门有一个author字段,存储完整的名称的。

GET /music/children/_search
{
  "query": {
    "match": {
      "author_full_name": {
        "query": "Peter Raffi",
        "operator": "and"
      }
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

单字段的查询,就可以随心所欲的指定operator或minimum_should_match来控制精度了。

我们看一下前面提到的3个问题能否解决

  1. 匹配问题

解决,最匹配的数据优先返回。

  1. 长尾问题

解决,可以指定operator或minimum_should_match来控制精度。

  1. 评分不准的问题

解决,所有信息在一个字段里,IDF计算时次数是均匀的,不会有极端的误差。

缺点:
需要前期设计时冗余字段,占用的存储会多一些。
copy_to拼接字段时,会遇到顺序问题,如英文名称名前姓后,而地址顺序则不固定,有的从省到街道由大到小,有的是反的,这也是局限性之一。

原生cross_fields语法

multi_match有原生的cross_fields语法解决跨字段实体搜索问题,请求如下:

GET /music/children/_search
{
  "query": {
    "multi_match": {
      "query": "Peter Raffi",
      "type": "cross_fields", 
      "operator": "and",
      "fields": ["author_first_name", "author_last_name"]
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

这次cross_fields的含义是要求:

  • Peter必须在author_first_name或author_last_name中出现
  • Raffi必须在author_first_name或author_last_name中出现

看看上面提及的3个问题解决情况:

  1. 匹配问题

解决,cross_fields要求每个term都必须在任何一个field中出现

  1. 长尾问题

解决,参见上一条,每个term都必须匹配,长尾问题自然迎刃而解。

  1. 评分不准的问题

解决,cross_fields通过混合不同字段逆向索引文档频率的方式解决词频的问题,具体来说,Peter在first_name中频率会高一些,在last_name中频率会低一些,在两个字段得到的IDF值,会取小的那个,Raffi也是同样处理,这样得到的IDF值就比较正常,不会偏高

七、参考链接

ElasticSearch 官方手册

A Practical Introduction to Elasticsearch

posted on 2021-02-02 16:39  yipianchuyun  阅读(330)  评论(0编辑  收藏  举报

导航