今天聊聊 Elasticsearch 在PHP项目中的使用。

最近开发一个项目,因为涉及到关键字及 多分类、多标签查询,鉴于日后数据可能会比较多,而多标签分类查询对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 主键查询是相当快的。

本文为原创文章,未经允许不可转载,请尊重作者劳动成果。