最近开发一个项目,因为涉及到关键字及 多分类、多标签查询,鉴于日后数据可能会比较多,而多标签分类查询对mysql 来讲必定要全表扫描,所以就请出搜索神器 Elasticsearch ,Elasticsearch 对于PHPer 来讲初次使用可能会觉得很复杂,但是熟悉以后可以解决项目中很多查询问题,建议了解并使用。
1 先安装 Elasticsearch (ES 下文中ES 表示为Elasticsearch)
安装 Elasticsearch 之前请保证你已经安装了 jdk 并设置好了jdk 环境变量。
https://www.elastic.co/downloads/elasticsearch
下载对应系统的版本进行安装,这里我下载了zip 版本。
解压以后 进入 bin 目录(linux 用户 不要用 root 运行 下载存放的目录也不要放在 root 权限下的目录,比如 我放在了 home/wj008/Elasticsearch 目录下面)
2 运行 ES (如果没有权限 先用 chmod 添加执行权限)
./elasticsearch
运行成功以后,我们可以在浏览器中打开地址 :http://127.0.0.1:9200/
如果没有什么问题,将显示如下信息。
因为 我们需要用到 关键字搜索,所以我们需要再安装一个中文分词插件。
这里我使用的分词插件是 ik 分词插件。
https://github.com/medcl/elasticsearch-analysis-ik/
请对应你安装的 ES 安装对应的 ik 插件,我这里是 6.3.0
如果发现你的版本太高 或者太低,你就得重新安装 Elasticsearch
OK ctl+c 先退出 ES,并使用命令行安装插件,进入 bin 目录 执行如下代码
./elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.3.0/elasticsearch-analysis-ik-6.3.0.zip
插件安装完成后 再次启动 ES
到这里 我们的ES 已经准备完成,接下来我们要使用php 开始设置我们的索引了,因为从 ES 6.3 开始 不在区分不同的 文档type(类似表名)所以我们只能使用一个 mappings 全局设置
首先 使用 composer 安装 ES 包(不知道 composer 是什么和怎么使用的 请自行百度了解)。
composer require elasticsearch/elasticsearch
这里我们需要一个在 cli 模式下运行的初始化脚本,当程序运行之前我们需要初始化es 的 mappings 设置。
<?php
//inites.php
require('../vendor/autoload.php');
use Elasticsearch\ClientBuilder;
//只能以命令行模式运行
if (PHP_SAPI != 'cli') {
die('该命令需要在命令行模式下运行');
}
$hosts = ['127.0.0.1:9200'];
$client = ClientBuilder::create()->setHosts($hosts)->build();
//这里的代码适用于实时输出,不使用缓存区
ob_implicit_flush(1);
$params = ['index' => 'my_index'];
//先判断索引是否存在
if ($client->indices()->exists($params)) {
echo '删除索引...' . PHP_EOL;
$response = $client->indices()->delete($params);
echo json_encode($response, JSON_UNESCAPED_UNICODE) . PHP_EOL;
}
echo '创建索引...' . PHP_EOL;
$params = [
'index' => 'my_index', //要创建的索引
'body' => [
'mappings' => [
//文档
'doc' => [
'_source' => [
'enabled' => true
],
//文档属性
'properties' => [
//这个字段 用于存储对应的表名,如果有多个的话,因为 自 ES 6.3 开始 已经不
//支持多个 type(类似数据库表) 设置属性,所以这里不同表 用 tbname 区分
"tbname" => ["type" => "keyword"],
//关键字 字段设置
'keyword' => [
'type' => 'text',
'analyzer' => 'ik_max_word', // 存入时使用 ik 分词
'search_analyzer' => 'ik_max_word', //搜索时使用 ik 分词
],
]
],
]
]
];
$response = $client->indices()->create($params);
echo json_encode($response, JSON_UNESCAPED_UNICODE) . PHP_EOL;
//查看 mappings
echo '查看 mappings...' . PHP_EOL;
$params = [
'index' => 'my_index',
'type' => 'doc'
];
$response = $client->indices()->getMapping($params);
echo json_encode($response, JSON_UNESCAPED_UNICODE) . PHP_EOL;
//如果 你数据已经有数据,那么 你可以接下去 循环插入索引数据,这里就先不插入了....
在命令行中运行 php inites.php 初始化设置。
接下来我写一个例子来演示 ES 的数据插入
<?php
//insertes.php
require('../vendor/autoload.php');
use Elasticsearch\ClientBuilder;
$hosts = ['127.0.0.1:9200'];
$client = ClientBuilder::create()->setHosts($hosts)->build();
/*
数据表:
DROP TABLE IF EXISTS `course`;
CREATE TABLE `course` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(250) DEFAULT NULL COMMENT '课程名称',
`createTime` datetime DEFAULT NULL COMMENT '创建时间',
`allow` tinyint(1) DEFAULT NULL COMMENT '是否启用',
`tags` text COMMENT '选择标签',
`subColumn` text COMMENT '子栏目',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=156 DEFAULT CHARSET=utf8;
INSERT INTO `course` VALUES ('148', '美国本科留学案例', '2018-06-04 03:30:45', '1', '[33,34,32,31]', '[{\"navBid\": 27, \"navMid\": 29, \"columnId\": 1}, {\"navBid\": 1, \"navMid\": 7, \"columnId\": 2}]');
INSERT INTO `course` VALUES ('150', '测试——如风过境', '2018-06-22 17:18:45', '1', '[40]', '[{\"navBid\": 1, \"navMid\": 7, \"columnId\": 2}]');
INSERT INTO `course` VALUES ('151', '斗罗大陆之绝世唐门1', '2018-06-22 18:42:37', '1', '[1,2,3,4,5,6,7,8,9]', '[{\"navBid\": 1, \"navMid\": 4, \"columnId\": 2}, {\"navBid\": 57, \"navMid\": 66, \"columnId\": 3}, {\"navBid\": 68, \"navMid\": 70, \"columnId\": 4}, {\"navBid\": 75, \"navMid\": 77, \"columnId\": 5}]');
INSERT INTO `course` VALUES ('152', '学长分享-考试英语-全部', '2018-06-23 17:09:39', '1', '[41,39,37,24]', '[{\"navBid\": 54, \"navMid\": 58, \"columnId\": 3}, {\"navBid\": 68, \"navMid\": 70, \"columnId\": 4}, {\"navBid\": 75, \"navMid\": 77, \"columnId\": 5}]');
INSERT INTO `course` VALUES ('153', '玉兰花,未来的会计师', '2018-06-23 17:13:54', '1', '[1,2,3,4,5,6,7,8,9]', '[{\"navBid\": 54, \"navMid\": 59, \"columnId\": 3}, {\"navBid\": 68, \"navMid\": 71, \"columnId\": 4}, {\"navBid\": 75, \"navMid\": 77, \"columnId\": 5}, {\"navBid\": 2, \"navMid\": 8, \"columnId\": 1}]');
INSERT INTO `course` VALUES ('154', '水母~海洋生物。', '2018-06-23 17:17:26', '1', '[1,2,3,4,5,6,7,8,9]', '[{\"navBid\": 1, \"navMid\": 7, \"columnId\": 2}, {\"navBid\": 54, \"navMid\": 60, \"columnId\": 3}, {\"navBid\": 68, \"navMid\": 72, \"columnId\": 4}, {\"navBid\": 76, \"navMid\": 79, \"columnId\": 5}]');
*/
$sqli = new mysqli('127.0.0.1', 'root', '123456', 'mytest1', 3306);
$sqli->query("SET NAMES utf8");
$rows = $sqli->query('select * from course')->fetch_all(MYSQLI_ASSOC);
foreach ($rows as $row) {
$row['createTime'] = strtotime($row['createTime']);//日期转为时间戳 以便可以查询区间
$row['tags'] = json_decode($row['tags'], true);//标签转为数组 以便可以查询数组内的值
$row['subColumn'] = json_decode($row['subColumn'], true);
$row['keyword'] = $row['name'];//我们要搜索的关键字,只有 keyword 我们设置了 ik 分词
$row['tbname'] = 'course';// 表名要给上,用于以后区分不同的表查询
$_id = 'course_' . $row['id']; //带上表前缀,以后用于更新的时候区分不同表
//如果存在 就更新 否则插入
if ($client->exists(['index' => 'my_index', 'type' => 'doc', 'id' => $_id])) {
$client->update(['index' => 'my_index', 'type' => 'doc', 'id' => $_id, 'body' => ['doc' => $row]]);
} else {
$client->index(['index' => 'my_index', 'type' => 'doc', 'id' => $_id, 'body' => $row]);
}
}
//显示所有已插入数据
print_r($client->search(['index' => 'my_index', 'type' => 'doc']));
这里我是一次性插入,如果在项目中,每次更新或者插入数据时 需要同时 插入 ES 或者更新 ES.
<?php
//search.php
require('../vendor/autoload.php');
use Elasticsearch\ClientBuilder;
function search($data)
{
$hosts = ['127.0.0.1:9200'];
$client = ClientBuilder::create()->setHosts($hosts)->build();
$must = []; //建一个数组用来装查询条件
$must[] = ['term' => ['allow' => 1]]; //这里查 allow =1 的条件 term 要求全等
//栏目id 需要查询 subColumn 中的 数组的 columnId match为查找匹配
$columnId = intval(empty($data['columnId']) ? '0' : $data['columnId']);
if ($columnId) {
$must[] = ['match' => ['subColumn.columnId' => $columnId]];
}
//导航Bid 需要查询 subColumn 中的 数组的 navBid
$navBid = intval(empty($data['navBid']) ? '0' : $data['navBid']);
if ($navBid) {
$must[] = ['match' => ['subColumn.navBid' => $navBid]];
}
//导航Mid 需要查询 subColumn 中的 数组的 navMid
$navMid = intval(empty($data['navMid']) ? '0' : $data['navMid']);
if ($navMid) {
$must[] = ['match' => ['subColumn.navMid' => $navMid]];
}
//标签id 需要查询 tags 数组中查找tagId
$tagId = intval(empty($data['tagId']) ? '0' : $data['tagId']);
if ($tagId) {
$must[] = ['match' => ['tags' => $tagId]];
}
$keyword = empty($data['keyword']) ? '' : $data['keyword'];
if (!empty($keyword)) {
$must[] = ['match' => ['keyword' => $keyword]];
}
//拼接查询数组
$sort = []; //排序
$sort[] = ['createTime' => ["order" => "desc"]]; //先按时间排序
$sort[] = ['_score' => ["order" => "desc"]]; //按得分排序
//数据分页
$from = 0;
$size = 4;
$params = [
'index' => 'my_index',
'type' => 'doc',
'body' => [
'query' => [
'bool' => [
'must' => $must,
//使用filter 过滤表名,如果不过滤 这会显示其他所有表的数据 tbname 之前设置过滤表的字段
'filter' => [
'match' => [
'tbname' => 'course'
]
]
]
],
'sort' => $sort,
],
'from' => $from,
'size' => $size
];
return $client->search($params);
}
echo '按类搜索=========' . PHP_EOL;
print_r(search([
'columnId' => 3,
'navBid' => 68,
'navMid' => 66,
]));
echo '按tagId搜索=========' . PHP_EOL;
//按tagId搜索
print_r(search([
'tagId' => 3
]));
echo '按关键字搜索=========' . PHP_EOL;
print_r(search([
'keyword' => '玉兰花,未来的会计师 考试英语'
]));
//从 $_GET 获取
echo '按$_GET搜索=========' . PHP_EOL;
print_r(search($_GET));
至此,ES 的PHP 搜索已经完成,如果需要更复杂的搜索 可以参考 官方帮助文档。
这里要注意的情况是,不要把表中所有不需要搜索的数据加入搜索引擎,比如 图片路径 文章内容 等加入到ES 搜索引擎中,这会导致索引相当大,而且索引效率下降,如需要其他字段你可以使用搜索后的 id 在二次查询mysql 数据库。 mysql 使用 id 主键查询是相当快的。
本文为原创文章,未经允许不可转载,请尊重作者劳动成果。