【PHP】PHP中使用ElasticSearch
在es中,使用组合条件查询是其作为搜索引擎检索数据的一个强大之处,在前几篇中,简单演示了es的查询语法,但基本的增删改查功能并不能很好的满足复杂的查询场景,比如说我们期望像mysql那样做到拼接复杂的条件进行查询该如何做呢?es中有一种语法叫bool,通过在bool里面拼接es特定的语法可以做到大部分场景下复杂条件的拼接查询,也叫复合查询
首先简单介绍es中常用的组合查询用到的关键词,
filter:过滤,不参与打分
must:如果有多个条件,这些条件都必须满足 and与
should:如果有多个条件,满足一个或多个即可 or或
must_not:和must相反,必须都不满足条件才可以匹配到 !非
{
"bool": {
"must": [],--必须满足的条件--and
"should": [],--可以满足也可以不满足的条件--or
"must_not": []--不能满足的条件--not
}
}
发生 描述
must
该条款(查询)必须出现在匹配的文件,并将有助于得分。
filter
子句(查询)必须出现在匹配的文档中。然而不像 must查询的分数将被忽略。Filter子句在过滤器上下文中执行,这意味着评分被忽略,子句被考虑用于高速缓存。
should
子句(查询)应该出现在匹配的文档中。如果 bool查询位于查询上下文中并且具有mustor filter子句,则bool即使没有should查询匹配,文档也将匹配该查询 。在这种情况下,这些条款仅用于影响分数。如果bool查询是过滤器上下文 或者两者都不存在,must或者filter至少有一个should查询必须与文档相匹配才能与bool查询匹配。这种行为可以通过设置minimum_should_match参数来显式控制 。
must_not
子句(查询)不能出现在匹配的文档中。子句在过滤器上下文中执行,意味着评分被忽略,子句被考虑用于高速缓存。因为计分被忽略,0所有文件的分数被返回。
网上很多关于ES的例子都过时了,版本很久,这篇文章的测试环境是ES6.5
通过composer 安装
composer require 'elasticsearch/elasticsearch'
在代码中引入
require 'vendor/autoload.php';
use Elasticsearch\ClientBuilder;
// 配置
$client = ClientBuilder::create()->setHosts(['172.16.55.53'])->build();
// 配置
$client = ClientBuilder::create()->setHosts([
[
'host' => 'xxx',
'port' => '9200',
'scheme' => 'http',
'user' => 'xxx',
'pass' => 'xxxx'
],
])->build();
下面循序渐进完成一个简单的添加和搜索的功能。
首先要新建一个 index:
index 对应关系型数据(以下简称MySQL)里面的数据库,而不是对应MySQL里面的索引,这点要清楚
$params = [
'index' => 'myindex', #index的名字不能是大写和下划线开头
'body' => [
'settings' => [
'number_of_shards' => 2,
'number_of_replicas' => 0
]
]
];
$client->indices()->create($params);
在MySQL里面,光有了数据库还不行,还需要建立表,ES也是一样的,ES中的type对应MySQL里面的表。
注意:ES6以前,一个index有多个type,就像MySQL中一个数据库有多个表一样自然,但是ES6以后,每个index只允许一个type,在往以后的版本中很可能会取消type。
type不是单独定义的,而是和字段一起定义
$params = [
'index' => 'myindex',
'type' => 'mytype',
'body' => [
'mytype' => [
'_source' => [
'enabled' => true
],
'properties' => [
'id' => [
'type' => 'integer'
],
'first_name' => [
'type' => 'text',
'analyzer' => 'ik_max_word'
],
'last_name' => [
'type' => 'text',
'analyzer' => 'ik_max_word'
],
'age' => [
'type' => 'integer'
]
]
]
]
];
$client->indices()->putMapping($params);
在定义字段的时候,可以看出每个字段可以定义单独的类型,在first_name中还自定义了 分词器 ik,
这个分词器是一个插件,需要单独安装的,参考另一篇文章:ElasticSearch基本尝试
现在 数据库和表都有了,可以往里面插入数据了
概念:这里的 数据 在ES中叫 文档
$params = [
'index' => 'myindex',
'type' => 'mytype',
//'id' => 1, #可以手动指定id,也可以不指定随机生成
'body' => [
'first_name' => '张',
'last_name' => '三',
'age' => 35
]
];
$client->index($params);
多插入一点数据,然后来看看怎么把数据取出来:
通过id取出单条数据:
插曲:如果你之前添加文档的时候没有传入id,ES会随机生成一个id,这个时候怎么通过id查?id是多少都不知道啊。
所以这个插入一个简单的搜索,最简单的,一个搜索条件都不要,返回所有index下所有文档:
$data = $client->search();
现在可以去找一找id了,不过你会发现id可能长这样:zU65WWgBVD80YaV8iVMk,不要惊讶,这是ES随机生成的。
现在可以通过id查找指定文档了:
$params = [
'index' => 'myindex',
'type' => 'mytype',
'id' =>'zU65WWgBVD80YaV8iVMk'
];
$data = $client->get($params);
最后一个稍微麻烦点的功能:
注意:这个例子我不打算在此详细解释,看不懂没关系,这篇文章主要的目的是基本用法,并没有涉及到ES的精髓地方,
ES精髓的地方就在于搜索,后面的文章我会继续深入分析
$query = [
'query' => [
'bool' => [
'must' => [
'match' => [
'first_name' => '张',
]
],
'filter' => [
'range' => [
'age' => ['gt' => 76]
]
]
]
]
];
$params = [
'index' => 'myindex',
// 'index' => 'm*', #index 和 type 是可以模糊匹配的,甚至这两个参数都是可选的
'type' => 'mytype',
'_source' => ['first_name','age'], // 请求指定的字段
'body' => array_merge([
'from' => 0,
'size' => 5
],$query)
];
$data = $this->EsClient->search($params);
上面的是一个简单的使用流程,但是不够完整,只讲了添加文档,没有说怎么删除文档,
下面我贴出完整的测试代码,基于Laravel环境,当然环境只影响运行,不影响理解,包含基本的常用操作:
<?php
use Elasticsearch\ClientBuilder;
use Elasticsearch\Common\Exceptions\Missing404Exception;
use services\CommonService;
use yii\base\UserException;
use yii\db\Exception;
class BaseEs
{
/**
* @var $instance Es类实例
*/
private static $instance;
/**
* @var $debug 设置debug
*/
protected $debug;
/**
* 客户端
* @var \Elasticsearch\Client
*/
private $client;
/**
* @var 索引名称
*/
protected $index;
/**
* @var 索引别名
*/
public $alias;
protected $retries = 2;
// 外网
private $config = [];
// 内网
private $lanConfig = [];
/**
* 查询参数
* @var array $queryParams
*/
protected $queryParams = [];
private function __construct()
{
if (DEBUG) {
$this->config = [
'xxx:9200',
];
$this->lanConfig = [
'xxx:9200',
];
} else {
$this->config = [
[
'host' => 'xxx',
'port' => '9200',
'scheme' => 'http',
'user' => 'xxx',
'pass' => 'xxx',
],
];
$this->lanConfig = [
[
'host' => 'xxx',
'port' => '9200',
'scheme' => 'http',
'user' => 'xxx',
'pass' => 'xxx',
],
];
}
try {
$ip = CommonService::getIp();
if (strpos($ip, '222.222') !== false) {
$config = $this->lanConfig;
} else {
$config = $this->config;
}
if (!$this->client instanceof Elasticsearch\Client) {
$this->client = ClientBuilder::create()
->setHosts($config)
->setRetries($this->retries)
->build();
}
} catch (\Exception $e) {
throw new Exception('Es conn failed');
}
}
public function select($params = '')
{
$this->queryParams['select'] = str_replace(' ', '', trim($params, " ,\t\r\n"));
return $this;
}
public function from($number = 0)
{
$this->queryParams['from'] = $number ? ($number - 1) * 10 : 0;
return $this;
}
public function size($number = 10)
{
$this->queryParams['size'] = $number;
return $this;
}
public function equal($params = [])
{
$this->queryParams['equal'][] = $params;
return $this;
}
public function in($params = [])
{
$this->queryParams['in'][] = $params;
return $this;
}
public function like($params = [])
{
$this->queryParams['like'][] = $params;
return $this;
}
public function orlike($params = [])
{
$this->queryParams['orlike'][] = $params;
return $this;
}
public function order($params = [])
{
$this->queryParams['order'] = $params;
return $this;
}
/**
* 根据条件删除文档
*
* @param array $params
* @return array
*/
public function deleteDoc($params = [])
{
return $this->client->deleteByQuery($params);
}
/**
* 根据id删除文档
*
* @param int $id
* @return array
*/
public function deleteDocById($id = 0)
{
$params = [
'index' => $this->index,
'id' => $id,
];
$response = $this->client->delete($params);
return $response;
}
/**
* 组装查询条件
*
* @param array $params
* @return $this
*/
private function parseQueryParams()
{
$queryParams = [
'index' => $this->index,
'body' => [
'query' => [
'bool' => [
'must' => [],
'filter' => [],
],
],
],
'from' => $this->queryParams['from'] ?? 0,
'size' => $this->queryParams['size'] ?? 10,
];
$filter = $must = [];
if (!empty($this->queryParams['select'])) {
$queryParams['_source'] = explode(',', $this->queryParams['select']);
}
if (!empty($this->queryParams['equal'])) {
foreach ($this->queryParams['equal'] as $key => $row) {
foreach ($row as $filed => $value) {
$filter[] = [
'term' => [$filed => $value],
];
}
}
}
if (!empty($this->queryParams['in'])) {
foreach ($this->queryParams['in'] as $key => $row) {
foreach ($row as $filed => $value) {
$filter[] = [
'terms' => [$filed => array_values(array_unique(array_filter($value)))],
];
}
}
}
if (!empty($this->queryParams['like'])) {
foreach ($this->queryParams['like'] as $key => $row) {
foreach ($row as $filed => $value) {
/*$must[] = [
'wildcard' => [$filed => '*'. $value. '*']
];*/
$must[] = [
// 'match' => [$filed => $value]
'match_phrase' => [$filed => $value],
];
}
}
$queryParams['body']['query']['bool']['must'] = $must;
}
if (!empty($this->queryParams['orlike'])) {
foreach ($this->queryParams['orlike'] as $key => $row) {
foreach ($row as $filed => $value) {
$orlike[] = [
'match_phrase' => [$filed => $value],
];
}
}
$queryParams['body']['query']['bool']['must']['bool']['should'] = $orlike;
}
if (!empty($this->queryParams['order'])) {
$queryParams['body']['sort'] = [
key($this->queryParams['order']) => [
'order' => current($this->queryParams['order']),
],
];
}
$queryParams['body']['query']['bool']['filter'] = $filter;
$this->queryParams = $queryParams;
return $this;
}
/**
* @param bool $isTotal isTotal=true时, 返回总数
* @return array|string
*/
public function query($isTotal = false)
{
try {
$this->parseQueryParams();
if ($this->debug) {
return \GuzzleHttp\json_encode($this->queryParams);
}
if ($isTotal === true) {
unset(
$this->queryParams['from'],
$this->queryParams['size'],
$this->queryParams['_source']
);
$count = $this->client->count($this->queryParams);
return (int)$count['count'];
}
if (!empty($this->queryParams)) {
$result = $this->client->search($this->queryParams);
if (!empty($result['hits']['hits'])) {
$return = [];
foreach ($result['hits']['hits'] as $row) {
$return[] = $row['_source'];
}
return $return;
} else {
return [];
}
}
} catch (\Exception $e) {
$msg = $e->getMessage();
$msg = '服务器开小差了~';
throw new Exception($msg);
}
}
/**
* 创建文档
*
* @param array $body
* @return array|bool
*/
public function createDocument($body = [], $id = null)
{
try {
if (!empty($body)) {
$params = [
'index' => $this->index,
'body' => $body,
];
if ($id) {
$params['id'] = $id;
}
$column = $params['body'];
$indexMappings = $this->mappings();
$diff = array_diff(array_keys($column), array_keys($indexMappings));
if (!empty($diff)) {
throw new Exception('different from the default mappings', $diff);
}
return $this->client->index($params);
}
} catch (\Exception $e) {
throw $e;
}
}
public function getIndex()
{
return $this->index;
}
/**
* @return BaseEs|Es类实例
* @throws Exception
*/
public static function setEs($index = '')
{
$class = get_called_class();
if (!self::$instance || !self::$instance instanceof $class) {
self::$instance = new static();
}
if ($index) {
self::$instance->index = $index;
}
return self::$instance;
}
protected function settings()
{
return [
'number_of_shards' => 1,
'number_of_replicas' => 0,
];
}
/**
* @return bool
* @throws Exception
*/
public function createIndex()
{
try {
$params = [
'index' => $this->index,
];
$check = $this->indices()->exists($params);
if ($check) {
throw new Exception('index: ' . $this->index . ' already exists');
}
$params = [
'index' => $this->index,
'body' => [
'settings' => $this->settings(),
'mappings' => [
'_source' => [
'enabled' => true,
],
'properties' => $this->mappings(),
],
],
];
$result = $this->indices()->create($params);
return $result['acknowledged'] === true;
} catch (\Exception $e) {
throw new Exception($e->getMessage());
}
}
/**
* 删除索引
*
* @return bool
* @throws Missing404Exception
*/
public function deleteIndex()
{
try {
$params = [
'index' => $this->index,
];
$check = $this->indices()->exists($params);
if (!$check) {
return true;
}
$result = $this->indices()->delete([
'index' => $this->index,
]);
return $result['acknowledged'] === true;
} catch (Missing404Exception $e) {
throw new Missing404Exception('no such index ' . $this->index);
}
}
/**
* 批量写入
* type 1.增加/修改 2.删除
* $data = [
* ['id' => 1, 'name' => 'llf', 'age' => 30],
* ['id' => 1, 'name' => 'llf', 'age' => 30],
* ['id' => 1, 'name' => 'llf', 'age' => 30],
* ['id' => 1, 'name' => 'llf', 'age' => 30],
* ];
* @param array $data
*/
public function doBulkDocument($type = 1, $data = [])
{
try {
$params = ['body' => []];
if ($type == 1) {
foreach ($data as $key => $row) {
$params['body'][] = [
'index' => [
'_index' => $this->index,
'_id' => $row['info_id'] . '-' . $row['info_type'],
],
];
$params['body'][] = $row;
if (($key + 1) % 10000 == 0) {
$this->client->bulk($params);
$params = ['body' => []];
}
}
} else {
foreach ($data as $key => $row) {
$params['body'][] = [
'delete' => [
'_index' => $this->index,
'_id' => $row['info_id'] . '-' . $row['info_type'],
],
];
}
}
if (!empty($params['body'])) {
$this->client->bulk($params);
return true;
}
} catch (\Exception $e) {
throw new Exception($e->getMessage());
}
}
/**
* @return array @todo
*/
public function updateIndex()
{
$putParams = [
'index' => $this->index,
//'type' => '_doc',
'body' => [
'properties' => $this->mappings(),
],
];
return $this->indices()->putMapping($putParams);
}
/**
* 方便调试, 直接返回拼接的query
* @return $this
*/
public function setDebug()
{
$this->debug = true;
return $this;
}
private function indices()
{
return $this->client->indices();
}
public function analyze($text = '')
{
$params = [
'index' => $this->index,
'body' => [
'analyzer' => 'ik_smart',
'text' => $text,
],
];
return $this->indices()->analyze($params);
}
public function update($id = 0, $updateParams = [])
{
$params = [
'index' => $this->index,
'id' => $id,
'body' => [
'doc' => $updateParams,
],
];
return $this->client->update($params);
}
}
猜你喜欢
- 【PHP】制作自己的Composer插件并与其他开发者共享
- 如何编写自己的Composer插件并分享给其他开发者在现代的PHP开发领域,Composer已经成为了一个不可或缺的工具。它可以帮助开发者管理项目依赖和自动加载类,大大简化了项目的构建过程。除了使用Composer来安装第三方的扩展包之外,我们也可以使用Composer来编写自己的插件,并将其分享给其他开发者。本文将逐步介绍如何编写自己的Composer插件,并提供具体的代码示例。首先,我们需要创建一个空的Composer插件项目。在命令行中进入项目根目录,然后执行以下命令:compo
- 【PHP】SQL查询优化方法
- 1.对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。2.应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。3.应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:select id from t where num is null可以在num上设置默认值0,确保表
- 【PHP】PHP常见漏洞的防范
- 一、常见PHP网站安全漏洞对于PHP的漏洞,目前常见的漏洞有五种。分别是Session文件漏洞、SQL注入漏洞、脚本命令执行漏洞、全局变量漏洞和文件漏洞。这里分别对这些漏洞进行简要的介绍。1、session文件漏洞Session攻击是黑客最常用到的攻击手段之一。当一个用户访问某一个网站时,为了免客户每进人一个页面都要输人账号和密码,PHP设置了Session和Cookie用于方便用户的使用和访向。2、SQL注入漏洞在进行网站开发的时候,程序员由于对用户输人数据缺乏全面判断或者过滤不严导致服务器执
- 【PHP】PHP8如何通过Sanitize Filters来增强应用程序的安全性
- PHP是一门广泛应用于Web开发的脚本语言,而安全性一直是Web应用程序开发者需要关注的重要问题。PHP8提供了一种称为Sanitize Filters的机制,通过对用户输入进行过滤和清理,可以增强应用程序的安全性。本文将详细介绍PHP8中Sanitize Filters的使用方法,并提供一些具体的代码示例,帮助开发者更好地了解如何应用这一特性。首先,让我们来了解一下Sanitize Filters是什么。Sanitize Filters是一组用于过滤和清理用户输入数据的PHP函数,可以帮助开发
- 【PHP】PHP 字符串编码处理 (附各语言的字符集编码范围)
- PHP中GBK和UTF8编码处理 一、编码范围1. GBK (GB2312/GB18030) x00-xff GBK双字节编码范围 x20-x7f ASCII xa1-xff 中文 x80-xff 中文 2. UTF-8 (Unicode)u4e00-u9fa5 (中文) x3130-x318F (韩文) xAC00-xD7A3 (韩文) u0800-u4e00 (日文) ps: 韩文是大于[u9fa5]的字符 正则例子:preg_replace("/([x80-
- 【PHP】PHP中的ob系列函数
- 在PHP中,ob系列函数,又称输出控制函数,可用于缓冲输出控制。1. ob_start()功能:打开一个输出缓冲区,所有的输出内容不再直接输出到浏览器,而是保存在输出缓冲区里面。返回值:布尔值。2. ob_get_clean()功能:获取当前缓冲区的内容并删除(关闭)当前输出缓冲区。返回值:返回输出缓冲区的内容,并结束输出缓冲区;如果输出缓冲区不是活跃的,返回false。ob_get_clean() 实质上是一起执行了 ob_get_contents() 和 ob_end_clean() 。3.
- 【PHP】用PHP从数据库到后端到前端完整实现一个中秋节祝福语项目
- 文章目录🚀一、前言🚀二、开发环境准备🚀三、功能实现🍁3.3.1 HTML布局🍁3.3.2 JQuery事件处理🍁3.2.1 连接数据库🍁3.2.1 获取祝福语🍁3.2.3 处理请求🍁3.2.4 配置Nginx与FPM🍁3.1.1 创建数据库及表结构🍁3.1.2 准备数据🔎3.1 准备数据库和数据🔎3.2 后端开发🔎3.3 前端开发🚀四、运行和测试🔎4.1 绑定host🔎4.2 开始测试🚀五、总结中秋佳节即将来临!在这特殊的时刻,我们特别举办一场属于程序员的中秋
- 【PHP】微信支付v3的jsapi接口接入thinkphp6完整流程
- 相信,写过微信支付接口的程序员,都会骂一句,什么垃圾文档。惠州网站建设今天给个完整的解决案例。哎,绕来绕去,把你绕坑里。我也是不知道掉了多少坑才写出这个避坑文档。目的是想让自己记住thinkphp6在接入微信支付v3时候jsapi的时候,不要在掉一次坑。因为,官网文档的说明内容真的让人无语。都严重怀疑,他不想让人成功接入他们支付一样。 下面说下我们