From 96378b70fd03b571d1952022ec07f20307bf9fe6 Mon Sep 17 00:00:00 2001 From: michael-grunder Date: Thu, 31 Jul 2025 12:50:29 -0700 Subject: [PATCH] Implement `VEMB` and slightly rework VINFO Unfortunately `VEMB` has a unique `RESP2` reply as far as I can tell, where it sends the embedding mode (int8, bin, fp32) as a simple string. This would cause any of PhpRedis' generic reply handlers to turn that into `true` which isn't useful. For that reason we need a custom reply handler. Additionally slightly rework `VINFO` to short circuit and return failure if we read anything other than a bulk string or an integer reply type. Otherwise we may get out of sync on the socket. See #2543 --- library.c | 86 +++++++++++++++++++++++++++++----- library.h | 5 ++ redis.c | 4 ++ redis.stub.php | 11 +++++ redis_arginfo.h | 10 +++- redis_cluster.c | 4 ++ redis_cluster.stub.php | 5 ++ redis_cluster_arginfo.h | 10 +++- redis_cluster_legacy_arginfo.h | 10 +++- redis_commands.c | 32 +++++++++++++ redis_commands.h | 3 ++ redis_legacy_arginfo.h | 10 +++- tests/RedisTest.php | 19 ++++++++ 13 files changed, 194 insertions(+), 15 deletions(-) diff --git a/library.c b/library.c index 3c7c3b2..d1632b6 100644 --- a/library.c +++ b/library.c @@ -2491,21 +2491,18 @@ redis_read_vinfo_response(RedisSock *redis_sock, zval *z_ret, long long count) { size_t klen, vlen; long lval; - if (count < 0 || count % 2 != 0) { + if (count < 0 || count % 2 != 0 || Z_TYPE_P(z_ret) != IS_ARRAY) { zend_error_noreturn(E_ERROR, "Internal finfo handler error"); } for (long long i = 0; i < count; i += 2) { if (redis_read_reply_type(redis_sock, &type, &lval) < 0 || - type != TYPE_LINE) + type != TYPE_LINE || + redis_sock_gets(redis_sock, kbuf, sizeof(kbuf), &klen) < 0) { return FAILURE; } - if (redis_sock_gets(redis_sock, kbuf, sizeof(kbuf), &klen) < 0) { - return FAILURE; - } - if (redis_read_reply_type(redis_sock, &type, &lval) < 0) { return FAILURE; } @@ -2521,10 +2518,8 @@ redis_read_vinfo_response(RedisSock *redis_sock, zval *z_ret, long long count) { add_assoc_long_ex(z_ret, kbuf, klen, lval); break; default: - add_assoc_null_ex(z_ret, kbuf, klen); - break; - - } + return FAILURE; + } } return SUCCESS; @@ -2558,8 +2553,77 @@ redis_vinfo_reply(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, return SUCCESS; } +/* Unfortunately VEMB decided to use +string\r\n for the encoding when RAW is + * sent which PhpRedis will parse as `(true)` so we need a specific handler for + * it */ PHP_REDIS_API int -redis_xinfo_reply(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx) +redis_read_vemb_response(RedisSock *redis_sock, zval *z_ret, long long count) { + REDIS_REPLY_TYPE type; + char kbuf[256], *str; + size_t klen; + double dval; + long tlen; + + if (count < 0 || Z_TYPE_P(z_ret) != IS_ARRAY) { + zend_error_noreturn(E_ERROR, "Internal vemb handler error"); + } + + for (long long i = 0; i < count; i++) { + if (redis_read_reply_type(redis_sock, &type, &tlen) < 0) { + return FAILURE; + } + + if (type == TYPE_LINE) { + if (redis_sock_gets(redis_sock, kbuf, sizeof(kbuf), &klen) < 0) + return FAILURE; + add_next_index_stringl(z_ret, kbuf, klen); + } else if (type == TYPE_BULK) { + if ((str = redis_sock_read_bulk_reply(redis_sock, tlen)) == NULL) + return FAILURE; + + if (is_numeric_string(str, tlen, NULL, &dval, 0) == IS_DOUBLE) { + add_next_index_double(z_ret, dval); + } else { + add_next_index_stringl(z_ret, str, tlen); + } + + efree(str); + } else { + return FAILURE; + } + } + + return SUCCESS; +} + +PHP_REDIS_API int +redis_vemb_reply(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, + zval *z_tab, void *ctx) +{ + zval z_ret; + int count; + + if (read_mbulk_header(redis_sock, &count) < 0 || count < 1) { + REDIS_RESPONSE_ERROR(redis_sock, z_tab); + return FAILURE; + } + + array_init_size(&z_ret, count); + + if (redis_read_vemb_response(redis_sock, &z_ret, count) != SUCCESS) { + zval_dtor(&z_ret); + REDIS_RESPONSE_ERROR(redis_sock, z_tab); + return FAILURE; + } + + REDIS_RETURN_ZVAL(redis_sock, z_tab, z_ret); + + return SUCCESS; +} + +PHP_REDIS_API int +redis_xinfo_reply(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, + zval *z_tab, void *ctx) { zval z_ret; int elements; diff --git a/library.h b/library.h index 3565f59..809efdf 100644 --- a/library.h +++ b/library.h @@ -128,6 +128,11 @@ PHP_REDIS_API int redis_vinfo_reply(INTERNAL_FUNCTION_PARAMETERS, PHP_REDIS_API int redis_read_vinfo_response(RedisSock *redis_sock, zval *z_ret, long long count); +PHP_REDIS_API int redis_vemb_reply(INTERNAL_FUNCTION_PARAMETERS, + RedisSock *redis_sock, zval *z_tab, void *ctx); +PHP_REDIS_API int redis_read_vemb_response(RedisSock *redis_sock, zval *z_ret, + long long count); + PHP_REDIS_API int redis_pubsub_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx); PHP_REDIS_API int redis_subscribe_response(INTERNAL_FUNCTION_PARAMETERS, diff --git a/redis.c b/redis.c index eb91a32..fa5fdae 100644 --- a/redis.c +++ b/redis.c @@ -3195,6 +3195,10 @@ PHP_METHOD(Redis, vinfo) { REDIS_PROCESS_KW_CMD("VINFO", redis_key_cmd, redis_vinfo_reply); } +PHP_METHOD(Redis, vemb) { + REDIS_PROCESS_CMD(vemb, redis_vemb_reply); +} + /* * Streams */ diff --git a/redis.stub.php b/redis.stub.php index df9572b..7546b78 100644 --- a/redis.stub.php +++ b/redis.stub.php @@ -4270,6 +4270,17 @@ class Redis { */ public function vinfo(string $key): Redis|array|false; + /** + * Get the embeddings for a specific member + * + * @param string $key The vector set to query. + * @param mixed $member The member to query. + * @param bool $raw If set to `true`, the raw embeddings will be returned + * + * @return Redis|array|false An array of embeddings for the member or false on failure. + */ + public function vemb(string $key, mixed $member, bool $raw = false): Redis|array|false; + /** * Truncate a STREAM key in various ways. * diff --git a/redis_arginfo.h b/redis_arginfo.h index 820978a..b42d10d 100644 --- a/redis_arginfo.h +++ b/redis_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: ef50011ff095df387f4b0f043d381e092253c8e8 */ + * Stub hash: 8cbcd94f8610fdfd08566cdf137ce63dbcfbf609 */ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Redis___construct, 0, 0, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") @@ -1082,6 +1082,12 @@ ZEND_END_ARG_INFO() #define arginfo_class_Redis_vinfo arginfo_class_Redis_getWithMeta +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_class_Redis_vemb, 0, 2, Redis, MAY_BE_ARRAY|MAY_BE_FALSE) + ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, member, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, raw, _IS_BOOL, 0, "false") +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_class_Redis_xtrim, 0, 2, Redis, MAY_BE_LONG|MAY_BE_FALSE) ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) ZEND_ARG_TYPE_INFO(0, threshold, IS_STRING, 0) @@ -1498,6 +1504,7 @@ ZEND_METHOD(Redis, vsim); ZEND_METHOD(Redis, vcard); ZEND_METHOD(Redis, vdim); ZEND_METHOD(Redis, vinfo); +ZEND_METHOD(Redis, vemb); ZEND_METHOD(Redis, xtrim); ZEND_METHOD(Redis, zAdd); ZEND_METHOD(Redis, zCard); @@ -1779,6 +1786,7 @@ static const zend_function_entry class_Redis_methods[] = { ZEND_ME(Redis, vcard, arginfo_class_Redis_vcard, ZEND_ACC_PUBLIC) ZEND_ME(Redis, vdim, arginfo_class_Redis_vdim, ZEND_ACC_PUBLIC) ZEND_ME(Redis, vinfo, arginfo_class_Redis_vinfo, ZEND_ACC_PUBLIC) + ZEND_ME(Redis, vemb, arginfo_class_Redis_vemb, ZEND_ACC_PUBLIC) ZEND_ME(Redis, xtrim, arginfo_class_Redis_xtrim, ZEND_ACC_PUBLIC) ZEND_ME(Redis, zAdd, arginfo_class_Redis_zAdd, ZEND_ACC_PUBLIC) ZEND_ME(Redis, zCard, arginfo_class_Redis_zCard, ZEND_ACC_PUBLIC) diff --git a/redis_cluster.c b/redis_cluster.c index 39eb453..f5a3c28 100644 --- a/redis_cluster.c +++ b/redis_cluster.c @@ -3199,6 +3199,10 @@ PHP_METHOD(RedisCluster, vinfo) { CLUSTER_PROCESS_KW_CMD("VINFO", redis_key_cmd, cluster_vinfo_resp, 1); } +PHP_METHOD(RedisCluster, vemb) { + CLUSTER_PROCESS_CMD(vemb, cluster_variant_resp, 1); +} + /* {{{ proto long RedisCluster::xack(string key, string group, array ids) }}} */ PHP_METHOD(RedisCluster, xack) { CLUSTER_PROCESS_CMD(xack, cluster_long_resp, 0); diff --git a/redis_cluster.stub.php b/redis_cluster.stub.php index 1aa4776..6e39d12 100644 --- a/redis_cluster.stub.php +++ b/redis_cluster.stub.php @@ -1088,6 +1088,11 @@ class RedisCluster { */ public function vinfo(string $key): RedisCluster|array|false; + /** + * @see Redis::vemb + */ + public function vemb(string $key, mixed $member, bool $raw = false): RedisCluster|array|false; + /** * @see Redis::xack */ diff --git a/redis_cluster_arginfo.h b/redis_cluster_arginfo.h index 09c2f87..4e46712 100644 --- a/redis_cluster_arginfo.h +++ b/redis_cluster_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 6833953df5e7260f7791abe42c5ae9cd9a0125d2 */ + * Stub hash: 0a9dbef0d65b9c674457ff335ec637544141c4e5 */ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_RedisCluster___construct, 0, 0, 1) ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 1) @@ -898,6 +898,12 @@ ZEND_END_ARG_INFO() #define arginfo_class_RedisCluster_vinfo arginfo_class_RedisCluster_getWithMeta +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_class_RedisCluster_vemb, 0, 2, RedisCluster, MAY_BE_ARRAY|MAY_BE_FALSE) + ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, member, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, raw, _IS_BOOL, 0, "false") +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_class_RedisCluster_xack, 0, 3, RedisCluster, MAY_BE_LONG|MAY_BE_FALSE) ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) ZEND_ARG_TYPE_INFO(0, group, IS_STRING, 0) @@ -1338,6 +1344,7 @@ ZEND_METHOD(RedisCluster, vsim); ZEND_METHOD(RedisCluster, vcard); ZEND_METHOD(RedisCluster, vdim); ZEND_METHOD(RedisCluster, vinfo); +ZEND_METHOD(RedisCluster, vemb); ZEND_METHOD(RedisCluster, xack); ZEND_METHOD(RedisCluster, xadd); ZEND_METHOD(RedisCluster, xclaim); @@ -1587,6 +1594,7 @@ static const zend_function_entry class_RedisCluster_methods[] = { ZEND_ME(RedisCluster, vcard, arginfo_class_RedisCluster_vcard, ZEND_ACC_PUBLIC) ZEND_ME(RedisCluster, vdim, arginfo_class_RedisCluster_vdim, ZEND_ACC_PUBLIC) ZEND_ME(RedisCluster, vinfo, arginfo_class_RedisCluster_vinfo, ZEND_ACC_PUBLIC) + ZEND_ME(RedisCluster, vemb, arginfo_class_RedisCluster_vemb, ZEND_ACC_PUBLIC) ZEND_ME(RedisCluster, xack, arginfo_class_RedisCluster_xack, ZEND_ACC_PUBLIC) ZEND_ME(RedisCluster, xadd, arginfo_class_RedisCluster_xadd, ZEND_ACC_PUBLIC) ZEND_ME(RedisCluster, xclaim, arginfo_class_RedisCluster_xclaim, ZEND_ACC_PUBLIC) diff --git a/redis_cluster_legacy_arginfo.h b/redis_cluster_legacy_arginfo.h index 031365a..8273dcd 100644 --- a/redis_cluster_legacy_arginfo.h +++ b/redis_cluster_legacy_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 6833953df5e7260f7791abe42c5ae9cd9a0125d2 */ + * Stub hash: 0a9dbef0d65b9c674457ff335ec637544141c4e5 */ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_RedisCluster___construct, 0, 0, 1) ZEND_ARG_INFO(0, name) @@ -761,6 +761,12 @@ ZEND_END_ARG_INFO() #define arginfo_class_RedisCluster_vinfo arginfo_class_RedisCluster__prefix +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_RedisCluster_vemb, 0, 0, 2) + ZEND_ARG_INFO(0, key) + ZEND_ARG_INFO(0, member) + ZEND_ARG_INFO(0, raw) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_INFO_EX(arginfo_class_RedisCluster_xack, 0, 0, 3) ZEND_ARG_INFO(0, key) ZEND_ARG_INFO(0, group) @@ -1173,6 +1179,7 @@ ZEND_METHOD(RedisCluster, vsim); ZEND_METHOD(RedisCluster, vcard); ZEND_METHOD(RedisCluster, vdim); ZEND_METHOD(RedisCluster, vinfo); +ZEND_METHOD(RedisCluster, vemb); ZEND_METHOD(RedisCluster, xack); ZEND_METHOD(RedisCluster, xadd); ZEND_METHOD(RedisCluster, xclaim); @@ -1422,6 +1429,7 @@ static const zend_function_entry class_RedisCluster_methods[] = { ZEND_ME(RedisCluster, vcard, arginfo_class_RedisCluster_vcard, ZEND_ACC_PUBLIC) ZEND_ME(RedisCluster, vdim, arginfo_class_RedisCluster_vdim, ZEND_ACC_PUBLIC) ZEND_ME(RedisCluster, vinfo, arginfo_class_RedisCluster_vinfo, ZEND_ACC_PUBLIC) + ZEND_ME(RedisCluster, vemb, arginfo_class_RedisCluster_vemb, ZEND_ACC_PUBLIC) ZEND_ME(RedisCluster, xack, arginfo_class_RedisCluster_xack, ZEND_ACC_PUBLIC) ZEND_ME(RedisCluster, xadd, arginfo_class_RedisCluster_xadd, ZEND_ACC_PUBLIC) ZEND_ME(RedisCluster, xclaim, arginfo_class_RedisCluster_xclaim, ZEND_ACC_PUBLIC) diff --git a/redis_commands.c b/redis_commands.c index 8452b30..ec17f5f 100644 --- a/redis_commands.c +++ b/redis_commands.c @@ -7007,6 +7007,38 @@ redis_vsim_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, return SUCCESS; } + +int +redis_vemb_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, + char **cmd, int *cmd_len, short *slot, void **ctx) +{ + smart_string cmdstr = {0}; + zend_bool raw = 0; + zend_string *key; + zval *member; + + ZEND_PARSE_PARAMETERS_START(2, 3) { + Z_PARAM_STR(key) + Z_PARAM_ZVAL(member); + Z_PARAM_OPTIONAL + Z_PARAM_BOOL(raw) + } ZEND_PARSE_PARAMETERS_END_EX(return FAILURE); + + REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 2 + !!raw, "VEMB"); + redis_cmd_append_sstr_key_zstr(&cmdstr, key, redis_sock, slot); + redis_cmd_append_sstr_zval(&cmdstr, member, redis_sock); + + if (raw) { + REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "RAW"); + } + + *cmd = cmdstr.c; + *cmd_len = cmdstr.len; + + return SUCCESS; +} + + /* * Redis commands that don't deal with the server at all. The RedisSock* * pointer is the only thing retrieved differently, so we just take that diff --git a/redis_commands.h b/redis_commands.h index 2f9ef61..32aac27 100644 --- a/redis_commands.h +++ b/redis_commands.h @@ -352,6 +352,9 @@ int redis_vadd_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, int redis_vsim_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, char **cmd, int *cmd_len, short *slot, void **ctx); +int redis_vemb_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, + char **cmd, int *cmd_len, short *slot, void **ctx); + int redis_xadd_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, char **cmd, int *cmd_len, short *slot, void **ctx); diff --git a/redis_legacy_arginfo.h b/redis_legacy_arginfo.h index e8c7794..b68ed53 100644 --- a/redis_legacy_arginfo.h +++ b/redis_legacy_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: ef50011ff095df387f4b0f043d381e092253c8e8 */ + * Stub hash: 8cbcd94f8610fdfd08566cdf137ce63dbcfbf609 */ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Redis___construct, 0, 0, 0) ZEND_ARG_INFO(0, options) @@ -962,6 +962,12 @@ ZEND_END_ARG_INFO() #define arginfo_class_Redis_vinfo arginfo_class_Redis__prefix +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Redis_vemb, 0, 0, 2) + ZEND_ARG_INFO(0, key) + ZEND_ARG_INFO(0, member) + ZEND_ARG_INFO(0, raw) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Redis_xtrim, 0, 0, 2) ZEND_ARG_INFO(0, key) ZEND_ARG_INFO(0, threshold) @@ -1336,6 +1342,7 @@ ZEND_METHOD(Redis, vsim); ZEND_METHOD(Redis, vcard); ZEND_METHOD(Redis, vdim); ZEND_METHOD(Redis, vinfo); +ZEND_METHOD(Redis, vemb); ZEND_METHOD(Redis, xtrim); ZEND_METHOD(Redis, zAdd); ZEND_METHOD(Redis, zCard); @@ -1617,6 +1624,7 @@ static const zend_function_entry class_Redis_methods[] = { ZEND_ME(Redis, vcard, arginfo_class_Redis_vcard, ZEND_ACC_PUBLIC) ZEND_ME(Redis, vdim, arginfo_class_Redis_vdim, ZEND_ACC_PUBLIC) ZEND_ME(Redis, vinfo, arginfo_class_Redis_vinfo, ZEND_ACC_PUBLIC) + ZEND_ME(Redis, vemb, arginfo_class_Redis_vemb, ZEND_ACC_PUBLIC) ZEND_ME(Redis, xtrim, arginfo_class_Redis_xtrim, ZEND_ACC_PUBLIC) ZEND_ME(Redis, zAdd, arginfo_class_Redis_zAdd, ZEND_ACC_PUBLIC) ZEND_ME(Redis, zCard, arginfo_class_Redis_zCard, ZEND_ACC_PUBLIC) diff --git a/tests/RedisTest.php b/tests/RedisTest.php index c2405fc..a38a44d 100644 --- a/tests/RedisTest.php +++ b/tests/RedisTest.php @@ -7754,6 +7754,25 @@ class Redis_Test extends TestSuite { $this->assertEquals(1, $this->redis->del('v')); } + public function testVEmb() { + if ( ! $this->minVersionCheck('8.0')) + $this->markTestSkipped(); + + $this->assertIsInt($this->redis->del('v')); + + $this->assertEquals(1, $this->redis->vadd('v', [0.5, 1.0], 'e')); + + $res = $this->redis->vemb('v', 'e'); + $this->assertIsArray($res); + $this->assertTrue(filter_var($res[0], FILTER_VALIDATE_FLOAT) !== false); + + $res = $this->redis->vemb('v', 'e', true); + $this->assertIsArray($res); + $this->assertEquals('int8', $res[0]); + + $this->assertEquals(1, $this->redis->del('v')); + } + public function testInvalidAuthArgs() { $client = $this->newInstance();