最近在开新坑,用的 CI (codeigniter) 写后台, ES (elasticsearch) 当数据库。其实之前用 codeigniter-restserver 写 RESTful API 挺方便的,但是后来把数据库改成 ES 后就各种蛋疼,简单记一下遇到的问题。

JSON

注意需要 php 的 zlib 扩展,否则可能出现 Response 0 或者 JSON 数据显示不全,在 php.ini 里把 zlib.output_compression 改成 on 即可,参考 https://github.com/chriskacerguis/codeigniter-restserver/issues/295

另外若是 Content-type 为 application/json 时,就必须要用 $this->input->raw_input_stream 才能获取到 JSON 数据。如果用到 multipart/form-data 或者 application/x-www-form-urlencoded,要先对 POST 过来的数据 urldecode() 后再操作,否则很可能出现奇怪的问题……

Mappings

ElasticSearch 原生支持 RESTful API ,本来以为用起来会很方便,但实际上手会有很多问题……
最坑爹的是如果不指定 mapping 的话 propertiesfield 的 type 会是自动设定的,而且一旦设定后想改的话非常麻烦。

比如说有个 stop_time 的 field,若是不指定 mapping 而是直接给它一个值 2015-10-22 14:21:22 ,你可能会以为 ES 会把它自动设定为与 MySQL 类似的 DateTime 类型,但其实 ES 会把它设定为 string 类型…… 没有任何报错,但是根本无法正常用 stop_time 进行时间比较。

试了不知道多少次后发现如果直接给 stop_time 的是 UTC 格式的时间,如 2015-11-15T00:12:11+0800, ES 可以自动识别为时间类型 "stop_time":{"type":"date","format":"dateOptionalTime"}

所以要是不想每次都指定 mapping 的话,也可以用模板(templates),详见 indices-templates

后来加了一个新字段,用来保存 UUID,看了下 mapping 里是 "uuid":{"type":"string"},好像没什么问题。众所周知 UUID 一般由数字、字母和分隔符组成,类似 43b21f32-9e5b-11e5-b0b5-5254004b4491,但坑爹的是因为 - 的存在,ES 默认会把它解析(analysed)成五段,即 43b21f32 9e5b 11e5 b0b5 5254004b4491,所以用 term filter 直接查询 UUID 时返回结果为空。

最后 Google 到了类似的问题:Elastic Search Hyphen issue with term filter

回答中提到了两个方法,其中第二个方法是用 text filter,不过看了下最新的文档 text filter 已被弃用了,所以只好用第一种也是最普遍的做法—— 重建索引。

为什么不直接改 mapping 呢,看看官方博客怎么说的吧:

mapping.jpg

至于如何无缝重建索引,请参考官方文章:Changing Mapping with Zero Downtime

DSL

为了图方便用了官方出的 elasticsearch-php,用 Composer 安装后这样加载就好:

<?php

defined('BASEPATH') OR exit('No direct script access allowed');

require 'vendor/autoload.php';
use Elasticsearch\ClientBuilder;

class Test extends CI_Controller {
 
    public function __construct()
    {
        parent::__construct();

        $this->client = ClientBuilder::create()
        ->setHosts('127.0.0.1:9200')->build();

        // 默认返回 JSON 格式数据
        header('Content-type: application/json');
    }
}

不过用这个包的话就必须用 ES 的 DSL 查询语句—— 这玩意可真不好用,而且写起来也麻烦。

官方文档上的例子是这样的:

$params = [
    'index' => 'my_index',
    'type' => 'my_type',
    'body' => [
        'query' => [
            'match' => [
                'testField' => 'abc'
            ]
        ]
    ]
];

其实 body 可以直接用 JSON 语句代替,比如:

$json = '"query" : {
             "match" : {
                 "testField" : "abc"
             }';
$params = [
            'index' => 'my_index',
            'type' => 'my_type',
            'body' => $json
        ];
$response = $this->client->search($params);
echo json_encode($response);

这样写在 DSL 语句很长时可以节省大量时间。

值得吐槽的是官方的 DSL 文档写的好烂…… 想找类似于 MySQL 里的 count 功能时找了半天才在角落里发现:

Deprecated in 2.0.0-beta1.

count does not provide any benefits over query_then_fetch with a size of 0.

于是设个 "size" : 0 就只显示总数了(注意 ES 默认的 size 为 10,即默认只返回 10 条数据)。

Bug

某天突然出现了这种错误,各种折腾无果:

`error: ReduceSearchPhaseException[Failed to execute phase [query], [reduce] ]; nested: ClassCastException[java.lang.Long cannot be cast to org.apache.lucene.util.BytesRef];
status: 503`

我的 ES 版本是 1.7.3,最后找到解决办法,貌似在 2.0 的最新版本中解决了这个 bug,清除缓存就行:
curl -XPOST 'http://localhost:9200/my_index/_cache/clear'