惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Cisco Talos Blog
Cisco Talos Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
爱范儿
爱范儿
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Jina AI
Jina AI
雷峰网
雷峰网
The Register - Security
The Register - Security
The Cloudflare Blog
博客园 - 【当耐特】
M
MIT News - Artificial intelligence
I
InfoQ
博客园 - 三生石上(FineUI控件)
H
Help Net Security
博客园 - 司徒正美
Vercel News
Vercel News
WordPress大学
WordPress大学
S
SegmentFault 最新的问题
云风的 BLOG
云风的 BLOG
B
Blog
Google DeepMind News
Google DeepMind News
B
Blog RSS Feed
L
LangChain Blog
人人都是产品经理
人人都是产品经理
GbyAI
GbyAI
T
The Blog of Author Tim Ferriss
T
Tailwind CSS Blog
aimingoo的专栏
aimingoo的专栏
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Recorded Future
Recorded Future
小众软件
小众软件
Martin Fowler
Martin Fowler
罗磊的独立博客
Stack Overflow Blog
Stack Overflow Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
酷 壳 – CoolShell
酷 壳 – CoolShell
腾讯CDC
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Apple Machine Learning Research
Apple Machine Learning Research
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - Franky
Hugging Face - Blog
Hugging Face - Blog
Y
Y Combinator Blog
V
Visual Studio Blog
F
Fortinet All Blogs
Microsoft Azure Blog
Microsoft Azure Blog
大猫的无限游戏
大猫的无限游戏
Engineering at Meta
Engineering at Meta
N
Netflix TechBlog - Medium
V
V2EX
Blog — PlanetScale
Blog — PlanetScale

文章列表 - 她和她的猫

使用小鹤双拼一个月 折腾 Hugo PaperMod 主题 去长鹿旅游休博园 1Password 自动化登录堡垒机 深入 Hyperf:Inject 注解是如何工作的? 译|理解容器镜像层 使用 Rust 实现高性能预写日志(Write Ahead Log) 译|使用 Linux 命名空间隔离系统 深入 Hyperf:HTTP 服务是如何处理请求的? Linux 文件权限学习笔记 译|了解 Bitcask:基于日志结构的 KV 存储引擎 还原 SM2 压缩公钥的几种方法 深入 Hyperf:HTTP 服务启动时发生了什么? 一次 Hyperf 注解失效问题分析 将博客迁移到又拍云 译|PHP 7 新的 Hashtable 实现 浅析 Redlock 分布式锁实现原理 Yar 源码阅读笔记:RPC 服务端 Yar 源码阅读笔记:客户端的并行调用 Yar 源码阅读笔记:客户端的同步调用 Yar 源码阅读笔记:数据传输模块 Yar 源码阅读笔记:消息编码模块 Yar 源码阅读笔记:RPC 通信协议 Yar 源码阅读笔记:开篇 PHP 8 新特性介绍 基于 OpenSSL 实现国密 SM4 加解密 PHP 源码阅读笔记:编译与调试 PHP 什么是惊群问题 大端模式和小端模式 基于 GitHub Actions 定时推送网址到百度站长平台 聊聊五种 I/O 模型 Workerman 源码分析:文件上传 浅入浅出 HTTP 协议 PHP 网络编程:构建 MySQL 蜜罐获取攻击者微信 ID 使用 Workerman 接入 Bilibili 直播弹幕协议 使用 GitHub Actions 自动部署 Hexo 什么是二进制安全 【转载】PHP 程序员进阶之路 优化 Workerman 检查主进程是否存活的逻辑 基于 Redis 实现分布式锁 《PHP 实现 Base64 编码/解码》笔记 PHP 多进程下载必应壁纸 基于 Redis 实现延迟队列 【转载】PHP 实现 Base64 编码/解码 使用 MySQL FIELD() 函数自定义排序规则 Laravel 的 Facades 实现原理 C语言单链表实现约瑟夫环 数据结构学习笔记:顺序栈和链栈(C语言) 使用 QueryList + Redis 下载壁纸 在 Laravel 登录/注册中使用 mews/captcha 扩展包 使用 mews/captcha 扩展包为 Laravel 应用添加图片验证码 给 YOURLS 短网址系统编写插件《批量生成短网址》 给 YOURLS 短网址系统编写插件《Hello World!》 PHP 7.1 使用 json_encode 函数造成浮点类型数据出现精度问题 Laravel 授权策略(Policy)的基本使用 Ubuntu 系统下 WiFi 频繁掉线解决方法 Laravel 应用部署到 Nginx 服务器上的第一个坑 Ubuntu 16.04 LST 安装 Redis 和 Composer PHP 采集逐浪小说章节列表 Nginx 环境 SSL 强制 HTTP 301 跳转到 HTTPS 【工作&生活】第一周 ServiceStack.Redis 与多线程 Count the number of Duplicates C# Base64 加密及解密 ASP.NET MVC 使用动态类型传递数据 Django 使用 order_by() 对数据进行排序操作 ASP.NET + jQuery + AJAX 实现用户登录 ASP.NET 从客户端中检测到有潜在危险的 Request.Form 值 ASP.NET 使用 FormData 实现 AJAX 上传图片 Python 使用 BeautifulSoup 抓取网页 贪心算法之字符串的完美度 一言不和就造轮子之 CookieHelper ASP.NET 上传图片 DBHelper 类 C# 捕获屏幕源码 用 JavaScript 做一些有趣的事情 第一篇文章 好久不见。 如何防止非法调用 PHP 文件 清明小记。 PHP 生成随机字符串 Z-Blog PHP 程序做 301 跳转 PHP 处理数组常用的几个函数 Sublime Text 3 快捷键总结 PHP 使用 file_get_contents() 函数实现采集网页 使用 C# 编写学生信息管理系统 终于到了。 zblog报错: magic_quotes_gpc 和 safe_mode 世事无常。 如何使用 PHP 上传文件 安卓手机编写和运行 PHP 程序的软件 使用 AJAX 刷新、验证 PHP 图片验证码 PHP 生成图片验证码 PHP 之 session 的使用方法 PHP 之 cookie 的使用方法 PHP 实现分页原理详解 Thinkphp 框架中系统常量在 Js 文件中不解析的解决方法
排查 ES 查询问题:深入了解 json_encode() 函数
她和她的猫 · 2022-06-02 · via 文章列表 - 她和她的猫

在使用 Elasticsearch 时,有时候会需要通过某个字段批量查询数据,比如通过用户 ID 批量获取用户信息,DSL 语句如下:

{
  "query": {
    "bool": {
      "must": [
        {
          "terms": {
            "user_id": ["123", "456"]
          }
        }
      ]
    }
  }
}

在 PHP 中,通常会使用数组来构造 DSL 语句,然后调用 json_encode() 函数将数组转成 JSON 字符串。

$user_ids = ['123', '456'];

$body = [
    'query' => [
        'bool' => [
            'must' => [
                [
                    'terms' => [
                        'user_id' => $user_ids,
                    ]
                ]
            ]
        ]
    ]
];

echo json_encode($body);

但是 $user_ids 一般都是由调用方传过来的,数组里面可能会存在重复的用户 ID,所以需要使用 array_unique() 函数对数组进行去重。

$user_ids = ['123', '456', '123', '789'];
$user_ids = array_unique($user_ids);

$body = [
    'query' => [
        'bool' => [
            'must' => [
                [
                    'terms' => [
                        'user_id' => $user_ids,
                    ]
                ]
            ]
        ]
    ]
];

echo json_encode($body);

这时候用生成的 DSL 语句再次请求,就会发现报错了:

{
  "error" : {
    "root_cause" : [
      {
        "type" : "x_content_parse_exception",
        "reason" : "[8:17] [terms_lookup] unknown field [0]"
      }
    ],
    "type" : "x_content_parse_exception",
    "reason" : "[8:22] [bool] failed to parse field [must]",
    "caused_by" : {
      "type" : "x_content_parse_exception",
      "reason" : "[8:17] [terms_lookup] unknown field [0]"
    }
  },
  "status" : 400
}

打印生成的 DSL 语句,会发现这次的 DSL 语句跟我们最初的有些许不一样。

{
  "query": {
    "bool": {
      "must": [
        {
          "terms": {
            "user_id": {
                "0": "123", 
                "1": "456",
                "3": "789"
            }
          }
        }
      ]
    }
  }
}

传入的 user_id 数组变成 JSON 对象了,为什么?

首先可以确定的是,肯定跟 array_unique() 函数的去重操作有关系。

我们知道 array_unique() 函数在对数组去重时,如果数组中有重复的元素,会删除后面出现的那个重复元素,并且不会更新数组其他元素的下标。

举个例子:

$arr = ['aaa', 'bbb', 'aaa', 'ccc'];
$unique_arr = array_unique($arr);

print_r($arr);
print_r($unique_arr);

//   Array
//   (
//       [0] => aaa
//       [1] => bbb
//       [2] => aaa
//       [3] => ccc
//   )
//   Array
//   (
//       [0] => aaa
//       [1] => bbb
//       [3] => ccc
//   )

在上面的例子中,下标为 2 的重复元素被删除了,但是其它元素的下标依然是保持原样的。

如果再分别调用 json_encode() 函数,没有去重操作的数组转成 JSON 字符串后是 JSON 数组,而经过去重操作的数组却变成了 JSON 对象。

arr: ["aaa","bbb","aaa","ccc"]
unique_arr: {"0":"aaa","1":"bbb","3":"ccc"}

通过对比,两个数组除了元素数量不一样以外,另一个区别就是经过去重后的数组的下标不是连续的,所以 PHP 将该数组转为 JSON 字符串时,认为需要将该数组转成 JSON 对象。

为了验证这一想法,我们在 PHP 源码中找到 json_encode() 函数的实现来验证一下,因为调用 json_encode() 函数时传入的是数组类型的变量,所以实际上调用的是 php_json_encode_array() 函数。

static int php_json_encode_array(smart_str *buf, zval *val, int options, php_json_encoder *encoder) /* {{{ */
{
    // 判断变量是不是数组类型
    if (Z_TYPE_P(val) == IS_ARRAY) {
        myht = Z_ARRVAL_P(val);
        prop_ht = NULL;
        // 如果没有强制返回 JSON 对象的话,就调用 php_json_determine_array_type 检查变量要输出
        r = (options & PHP_JSON_FORCE_OBJECT) ? PHP_JSON_OUTPUT_OBJECT : php_json_determine_array_type(val);
    } else {
        // 不是数组的话则返回 JSON_对象
        prop_ht = myht = zend_get_properties_for(val, ZEND_PROP_PURPOSE_JSON);
        r = PHP_JSON_OUTPUT_OBJECT;
    }

    // 根据类型在 JSON 字符串首部添加相应的字符
    if (r == PHP_JSON_OUTPUT_ARRAY) {
        smart_str_appendc(buf, '[');
    } else {
        smart_str_appendc(buf, '{');
    }

    // 这里省略了转换的逻辑

    // 根据类型在 JSON 字符串末尾添加相应的字符
    if (r == PHP_JSON_OUTPUT_ARRAY) {
        smart_str_appendc(buf, ']');
    } else {
        smart_str_appendc(buf, '}');
    }

    zend_release_properties(prop_ht);
    return SUCCESS;
}

通过上面删减过的代码,可以知道我们传入的数组最终转成 JSON 数组还是 JSON 对象,是由 r 这个变量来控制的。当 r 等于 PHP_JSON_OUTPUT_ARRAY 时,将数组转成 JSON 数组,否则转成 JSON 对象。

而变量 r 的值则是由传入的变量的类型来控制的,由于我们传入的是数组类型,并且没有设置强制转为对象的选项,所以最终是由 php_json_determine_array_type() 函数来确定要将数组转换成什么类型。

static int php_json_determine_array_type(zval *val)
{
    int i;
    HashTable *myht = Z_ARRVAL_P(val);

    // 获取数组的长度
    i = myht ? zend_hash_num_elements(myht) : 0;
    if (i > 0) {
        zend_string *key;
        zend_ulong index, idx;

        if (HT_IS_PACKED(myht) && HT_IS_WITHOUT_HOLES(myht)) {
            return PHP_JSON_OUTPUT_ARRAY;
        }

        idx = 0;
        // 遍历数组
        ZEND_HASH_FOREACH_KEY(myht, index, key) {
            if (key) {
                // 如果元素设置了 key,返回 PHP_JSON_OUTPUT_OBJECT
                return PHP_JSON_OUTPUT_OBJECT;
            } else {
                // 重点!当元素的下标不等于随着遍历递增的 idx 时,返回 PHP_JSON_OUTPUT_OBJECT
                if (index != idx) {
                    return PHP_JSON_OUTPUT_OBJECT;
                }
            }
            // 下标递增
            idx++;
        } ZEND_HASH_FOREACH_END();
    }

    // 默认返回 PHP_JSON_OUTPUT_ARRAY
    return PHP_JSON_OUTPUT_ARRAY;
}

该函数的主要逻辑是,通过遍历数组检查每一个元素的 key 和下标。如果数组中的某个元素设置了 key,或者元素的下标不等于随着遍历而递增的索引时,就返回 PHP_JSON_OUTPUT_OBJECT,否则返回 PHP_JSON_OUTPUT_ARRAY。

可以看到 json_encode() 函数的实现跟我们的猜想是一样的。

知道了问题原因,解决的方法就很简单了:在调用 array_unique() 函数之后,再调用一次 array_values() 函数,将数组中的元素依次取出来保存到新数组中,这样新数组的下标就是连续的了。

$arr = ['aaa', 'bbb', 'aaa', 'ccc'];
$unique_arr = array_unique($arr);

print_r($arr);
print_r(array_values($unique_arr));

//   Array
//   (
//       [0] => aaa
//       [1] => bbb
//       [2] => aaa
//       [3] => ccc
//   )
//   Array
//   (
//       [0] => aaa
//       [1] => bbb
//       [2] => ccc
//   )