Repository: redis/redis

Problem statement:
[NEW]Scan for hash fields only
**The problem/use-case that the feature addresses**

Is it possible to scan the hash field only, value is not necessary

**Description of the feature**


Maybe `HKSCAN`, scans the hash fields only, not loading the hash value, the value may be large


**Alternatives you've considered**



**Additional information**




Hints:
Why no use `HKEY` command?
> Why no use `HKEY` command?

There may be too many fields in the hash, the hash fields we needed may be only about 1~10%, so we prefer a pattern-matching way to get the fields, and the key value may be very large, we generally avoid reading unnecessary field value

seems that the existing commands aren't sufficient for your needs, perhaps we can extend `HKEY` command with pattern matching.
like `KEY` command:
```
HKEY key [MATCH pattern]
```

ping @redis/redis-committers, please share your thoughts.
> HKEY key [MATCH pattern]

That's also a great candidate, thanks
It's not good to use HKEYS for very large hashes because it's O(N) just like KEYS.

I prefer HKSCAN. Another possibility is HSCAN with a new argument like NOVALUES.
We may want the same for ZSCAN, to return the elements without their score.
Do we want to avoid adding arguments that affect the return type of a command? I don't remember. @guybe7 do you know?
i think adding a `[NOVALUES]` argument to HSCAN makes sense.
@zuiderkwast yes, that is preferable IMHO
@guybe7 so if we add `[NOVALUE]` to HSCAN, it changes the return structure. If you want to avoid that, I would think you prefer a separate HKSCAN?

That's my only concern. Personally, I don't mind adding this. We already have a lot of WITHSCORE and other arguments affecting the return value of various commands.

Patch:
diff --git a/src/commands.def b/src/commands.def
index deba47feabb..8ede899d0d0 100644
--- a/src/commands.def
+++ b/src/commands.def
@@ -3565,6 +3565,7 @@ struct COMMAND_ARG HSCAN_Args[] = {
 {MAKE_ARG("cursor",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
 {MAKE_ARG("pattern",ARG_TYPE_PATTERN,-1,"MATCH",NULL,NULL,CMD_ARG_OPTIONAL,0,NULL)},
 {MAKE_ARG("count",ARG_TYPE_INTEGER,-1,"COUNT",NULL,NULL,CMD_ARG_OPTIONAL,0,NULL)},
+{MAKE_ARG("novalues",ARG_TYPE_PURE_TOKEN,-1,"NOVALUES",NULL,NULL,CMD_ARG_OPTIONAL,0,NULL)},
 };
 
 /********** HSET ********************/
@@ -10711,7 +10712,7 @@ struct COMMAND_STRUCT redisCommandTable[] = {
 {MAKE_CMD("hmget","Returns the values of all fields in a hash.","O(N) where N is the number of fields being requested.","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HMGET_History,0,HMGET_Tips,0,hmgetCommand,-3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HMGET_Keyspecs,1,NULL,2),.args=HMGET_Args},
 {MAKE_CMD("hmset","Sets the values of multiple fields.","O(N) where N is the number of fields being set.","2.0.0",CMD_DOC_DEPRECATED,"`HSET` with multiple field-value pairs","4.0.0","hash",COMMAND_GROUP_HASH,HMSET_History,0,HMSET_Tips,0,hsetCommand,-4,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HMSET_Keyspecs,1,NULL,2),.args=HMSET_Args},
 {MAKE_CMD("hrandfield","Returns one or more random fields from a hash.","O(N) where N is the number of fields returned","6.2.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HRANDFIELD_History,0,HRANDFIELD_Tips,1,hrandfieldCommand,-2,CMD_READONLY,ACL_CATEGORY_HASH,HRANDFIELD_Keyspecs,1,NULL,2),.args=HRANDFIELD_Args},
-{MAKE_CMD("hscan","Iterates over fields and values of a hash.","O(1) for every call. O(N) for a complete iteration, including enough command calls for the cursor to return back to 0. N is the number of elements inside the collection.","2.8.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSCAN_History,0,HSCAN_Tips,1,hscanCommand,-3,CMD_READONLY,ACL_CATEGORY_HASH,HSCAN_Keyspecs,1,NULL,4),.args=HSCAN_Args},
+{MAKE_CMD("hscan","Iterates over fields and values of a hash.","O(1) for every call. O(N) for a complete iteration, including enough command calls for the cursor to return back to 0. N is the number of elements inside the collection.","2.8.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSCAN_History,0,HSCAN_Tips,1,hscanCommand,-3,CMD_READONLY,ACL_CATEGORY_HASH,HSCAN_Keyspecs,1,NULL,5),.args=HSCAN_Args},
 {MAKE_CMD("hset","Creates or modifies the value of a field in a hash.","O(1) for each field/value pair added, so O(N) to add N field/value pairs when the command is called with multiple field/value pairs.","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSET_History,1,HSET_Tips,0,hsetCommand,-4,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HSET_Keyspecs,1,NULL,2),.args=HSET_Args},
 {MAKE_CMD("hsetnx","Sets the value of a field in a hash only when the field doesn't exist.","O(1)","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSETNX_History,0,HSETNX_Tips,0,hsetnxCommand,4,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HSETNX_Keyspecs,1,NULL,3),.args=HSETNX_Args},
 {MAKE_CMD("hstrlen","Returns the length of the value of a field.","O(1)","3.2.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSTRLEN_History,0,HSTRLEN_Tips,0,hstrlenCommand,3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HSTRLEN_Keyspecs,1,NULL,2),.args=HSTRLEN_Args},
diff --git a/src/commands/hscan.json b/src/commands/hscan.json
index 0888eec9fce..99e916574f6 100644
--- a/src/commands/hscan.json
+++ b/src/commands/hscan.json
@@ -56,6 +56,12 @@
                 "name": "count",
                 "type": "integer",
                 "optional": true
+            },
+            {
+                "token": "NOVALUES",
+                "name": "novalues",
+                "type": "pure-token",
+                "optional": true
             }
         ],
         "reply_schema": {
@@ -69,7 +75,7 @@
                     "type": "string"
                 },
                 {
-                    "description": "list of key/value pairs from the hash where each even element is the key, and each odd element is the value",
+                    "description": "list of key/value pairs from the hash where each even element is the key, and each odd element is the value, or when novalues option is on, a list of keys from the hash",
                     "type": "array",
                     "items": {
                         "type": "string"
diff --git a/src/db.c b/src/db.c
index 8260676d424..1f6115e6106 100644
--- a/src/db.c
+++ b/src/db.c
@@ -1029,6 +1029,7 @@ typedef struct {
     long long type; /* the particular type when scan the db */
     sds pattern;  /* pattern string, NULL means no pattern */
     long sampled; /* cumulative number of keys sampled */
+    int no_values; /* set to 1 means to return keys only */
 } scanData;
 
 /* Helper function to compare key type in scan commands */
@@ -1091,7 +1092,7 @@ void scanCallback(void *privdata, const dictEntry *de) {
     }
 
     listAddNodeTail(keys, key);
-    if (val) listAddNodeTail(keys, val);
+    if (val && !data->no_values) listAddNodeTail(keys, val);
 }
 
 /* Try to parse a SCAN cursor stored at object 'o':
@@ -1164,7 +1165,7 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) {
     sds pat = NULL;
     sds typename = NULL;
     long long type = LLONG_MAX;
-    int patlen = 0, use_pattern = 0;
+    int patlen = 0, use_pattern = 0, no_values = 0;
     dict *ht;
 
     /* Object must be NULL (to iterate keys names), or the type of the object
@@ -1210,6 +1211,13 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) {
                 return; */
             }
             i+= 2;
+        } else if (!strcasecmp(c->argv[i]->ptr, "novalues")) {
+            if (!o || o->type != OBJ_HASH) {
+                addReplyError(c, "NOVALUES option can only be used in HSCAN");
+                return;
+            }
+            no_values = 1;
+            i++;
         } else {
             addReplyErrorObject(c,shared.syntaxerr);
             return;
@@ -1264,17 +1272,20 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) {
          * it is possible to fetch more data in a type-dependent way;
          * 3. data.type: the specified type scan in the db, LLONG_MAX means
          * type matching is no needed;
-         * 4. data.pattern: the pattern string
+         * 4. data.pattern: the pattern string;
          * 5. data.sampled: the maxiteration limit is there in case we're
          * working on an empty dict, one with a lot of empty buckets, and
          * for the buckets are not empty, we need to limit the spampled number
-         * to prevent a long hang time caused by filtering too many keys*/
+         * to prevent a long hang time caused by filtering too many keys;
+         * 6. data.no_values: to control whether values will be returned or 
+         * only keys are returned. */
         scanData data = {
             .keys = keys,
             .o = o,
             .type = type,
             .pattern = use_pattern ? pat : NULL,
             .sampled = 0,
+            .no_values = no_values,
         };
 
         /* A pattern may restrict all matching keys to one cluster slot. */
@@ -1329,8 +1340,10 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) {
             /* add key object */
             listAddNodeTail(keys, sdsnewlen(str, len));
             /* add value object */
-            str = lpGet(p, &len, intbuf);
-            listAddNodeTail(keys, sdsnewlen(str, len));
+            if (!no_values) {
+                str = lpGet(p, &len, intbuf);
+                listAddNodeTail(keys, sdsnewlen(str, len));
+            }
             p = lpNext(o->ptr, p);
         }
         cursor = 0;


Test patch:
diff --git a/tests/unit/scan.tcl b/tests/unit/scan.tcl
index 5e093d90dbb..d980a52adba 100644
--- a/tests/unit/scan.tcl
+++ b/tests/unit/scan.tcl
@@ -272,6 +272,10 @@ proc test_scan {type} {
 
             set keys2 [lsort -unique $keys2]
             assert_equal $count [llength $keys2]
+
+            # Test NOVALUES 
+            set res [r hscan hash 0 count 1000 novalues]
+            assert_equal [lsort $keys2] [lsort [lindex $res 1]]
         }
     }
 
@@ -368,6 +372,13 @@ proc test_scan {type} {
         lsort -unique [lindex $res 1]
     } {1 10 foo foobar}
 
+    test "{$type} HSCAN with NOVALUES" {
+        r del mykey
+        r hmset mykey foo 1 fab 2 fiz 3 foobar 10 1 a 2 b 3 c 4 d
+        set res [r hscan mykey 0 NOVALUES]
+        lsort -unique [lindex $res 1]
+    } {1 2 3 4 fab fiz foo foobar}
+
     test "{$type} ZSCAN with PATTERN" {
         r del mykey
         r zadd mykey 1 foo 2 fab 3 fiz 10 foobar