Repository: redis/docs
Pull request #1471: DOC-5150 and DOC-5151 JavaScript and C# vector json examples
Author: login: andy-stark-redis
Created: 2025-04-28T12:57:56Z | Merged: 2025-04-28T14:01:37Z

[DOC-5150](https://redislabs.atlassian.net/browse/DOC-5150) and [DOC-5151](https://redislabs.atlassian.net/browse/DOC-5151)

For the C# examples, I'm not sure if it's clearer to define the `GetEmbedding` and `GetFloatEmbedding` functions the way I have, or if it would be better to have the `GetFloatEmbedding` as it is with another function to convert the result to a binary string. I guess it doesn't make much difference but I'd be happy to hear any thoughts about whether one approach is better than the other.

[DOC-5150]: https://redislabs.atlassian.net/browse/DOC-5150?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[DOC-5151]: https://redislabs.atlassian.net/browse/DOC-5151?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ


Diff:
diff --git a/content/develop/clients/dotnet/vecsearch.md b/content/develop/clients/dotnet/vecsearch.md
index 5b309d6a64..018cdab2ec 100644
--- a/content/develop/clients/dotnet/vecsearch.md
+++ b/content/develop/clients/dotnet/vecsearch.md
@@ -32,7 +32,9 @@ In the example below, we use [Microsoft.ML](https://dotnet.microsoft.com/en-us/a
 to generate the vector embeddings to store and index with Redis Query Engine.
 We also show how to adapt the code to use
 [Azure OpenAI](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/embeddings?tabs=csharp)
-for the embeddings.
+for the embeddings. The code is first demonstrated for hash documents with a
+separate section to explain the
+[differences with JSON documents](#differences-with-json-documents).
 
 ## Initialize
 
@@ -89,7 +91,6 @@ using Azure;
 using Azure.AI.OpenAI;
 ```
 
-
 ## Define a function to obtain the embedding model
 
 {{< note >}}Ignore this step if you are using an Azure OpenAI
@@ -154,7 +155,9 @@ array as a `byte` string. To simplify this, we declare a
 then encodes the returned `float` array as a `byte` string. If you are
 storing your documents as JSON objects instead of hashes, then you should
 use the `float` array for the embedding directly, without first converting
-it to a `byte` string.
+it to a `byte` string (see [Differences with JSON documents](#differences-with-json-documents)
+below).
+
 
 ```csharp
 static byte[] GetEmbedding(
@@ -414,6 +417,156 @@ As you would expect, the result for `doc:1` with the content text
 is the result that is most similar in meaning to the query text
 *"That is a happy person"*.
 
+## Differences with JSON documents
+
+Indexing JSON documents is similar to hash indexing, but there are some
+important differences. JSON allows much richer data modeling with nested fields, so
+you must supply a [path]({{< relref "/develop/data-types/json/path" >}}) in the schema
+to identify each field you want to index. However, you can declare a short alias for each
+of these paths to avoid typing it in full for
+every query. Also, you must specify `IndexType.JSON` with the `On()` option when you
+create the index.
+
+The code below shows these differences, but the index is otherwise very similar to
+the one created previously for hashes:
+
+```cs
+var jsonSchema = new Schema()
+    .AddTextField(new FieldName("$.content", "content"))
+    .AddTagField(new FieldName("$.genre", "genre"))
+    .AddVectorField(
+        new FieldName("$.embedding", "embedding"),
+        VectorField.VectorAlgo.HNSW,
+        new Dictionary<string, object>()
+        {
+            ["TYPE"] = "FLOAT32",
+            ["DIM"] = "150",
+            ["DISTANCE_METRIC"] = "L2"
+        }
+    );
+
+
+db.FT().Create(
+    "vector_json_idx",
+    new FTCreateParams()
+        .On(IndexDataType.JSON)
+        .Prefix("jdoc:"),
+    jsonSchema
+);
+```
+
+An important difference with JSON indexing is that the vectors are
+specified using arrays of `float` instead of binary strings. This requires a modification
+to the `GetEmbedding()` function declared in
+[Define a function to generate an embedding](#define-a-function-to-generate-an-embedding)
+above:
+
+```cs
+static float[] GetFloatEmbedding(
+    PredictionEngine<TextData, TransformedTextData> model, string sentence
+)
+{
+    // Call the prediction API to convert the text into embedding vector.
+    var data = new TextData()
+    {
+        Text = sentence
+    };
+
+    var prediction = model.Predict(data);
+
+    float[] floatArray = Array.ConvertAll(prediction.Features, x => (float)x);
+    return floatArray;
+}
+```
+
+You should make a similar modification to the `GetEmbeddingFromAzure()` function
+if you are using Azure OpenAI with JSON.
+
+Use [`JSON().set()`]({{< relref "/commands/json.set" >}}) to add the data
+instead of [`HashSet()`]({{< relref "/commands/hset" >}}):
+
+```cs
+var jSentence1 = "That is a very happy person";
+
+var jdoc1 = new {
+    content = jSentence1,
+    genre = "persons",
+    embedding = GetFloatEmbedding(predEngine, jSentence1),
+};
+
+db.JSON().Set("jdoc:1", "$", jdoc1);
+
+var jSentence2 = "That is a happy dog";
+
+var jdoc2 = new {
+    content = jSentence2,
+    genre = "pets",
+    embedding = GetFloatEmbedding(predEngine, jSentence2),
+};
+
+db.JSON().Set("jdoc:2", "$", jdoc2);
+
+var jSentence3 = "Today is a sunny day";
+
+var jdoc3 = new {
+    content = jSentence3,
+    genre = "weather",
+    embedding = GetFloatEmbedding(predEngine, jSentence3),
+};
+
+db.JSON().Set("jdoc:3", "$", jdoc3);
+```
+
+The query is almost identical to the one for the hash documents. This
+demonstrates how the right choice of aliases for the JSON paths can
+save you having to write complex queries. The only significant difference is
+that the `FieldName` objects created for the `ReturnFields()` option must
+include the JSON path for the field.
+
+An important thing to notice
+is that the vector parameter for the query is still specified as a
+binary string (using the `GetEmbedding()` method), even though the data for
+the `embedding` field of the JSON was specified as a `float` array.
+
+```cs
+var jRes = db.FT().Search("vector_json_idx",
+    new Query("*=>[KNN 3 @embedding $query_vec AS score]")
+    .AddParam("query_vec", GetEmbedding(predEngine, "That is a happy person"))
+    .ReturnFields(
+        new FieldName("$.content", "content"),
+        new FieldName("$.score", "score")
+    )
+    .SetSortBy("score")
+    .Dialect(2));
+
+foreach (var doc in jRes.Documents) {
+    var props = doc.GetProperties();
+    var propText = string.Join(
+        ", ",
+        props.Select(p => $"{p.Key}: '{p.Value}'")
+    );
+
+    Console.WriteLine(
+        $"ID: {doc.Id}, Properties: [\n  {propText}\n]"
+    );
+}
+```
+
+Apart from the `jdoc:` prefixes for the keys, the result from the JSON
+query is the same as for hash:
+
+```
+ID: jdoc:1, Properties: [
+  score: '4.30777168274', content: 'That is a very happy person'
+]
+ID: jdoc:2, Properties: [
+  score: '25.9752807617', content: 'That is a happy dog'
+]
+ID: jdoc:3, Properties: [
+  score: '68.8638000488', content: 'Today is a sunny day'
+]
+```
+
 ## Learn more
 
 See
diff --git a/content/develop/clients/nodejs/vecsearch.md b/content/develop/clients/nodejs/vecsearch.md
index 874e3e63ba..798d13d21c 100644
--- a/content/develop/clients/nodejs/vecsearch.md
+++ b/content/develop/clients/nodejs/vecsearch.md
@@ -20,53 +20,50 @@ lets you index vector fields in [hash]({{< relref "/develop/data-types/hashes" >
 or [JSON]({{< relref "/develop/data-types/json" >}}) objects (see the
 [Vectors]({{< relref "/develop/interact/search-and-query/advanced-concepts/vectors" >}}) 
 reference page for more information).
-Among other things, vector fields can store *text embeddings*, which are AI-generated vector
-representations of the semantic information in pieces of text. The
+
+Vector fields can store *text embeddings*, which are AI-generated vector
+representations of text content. The
 [vector distance]({{< relref "/develop/interact/search-and-query/advanced-concepts/vectors#distance-metrics" >}})
-between two embeddings indicates how similar they are semantically. By comparing the
-similarity of an embedding generated from some query text with embeddings stored in hash
-or JSON fields, Redis can retrieve documents that closely match the query in terms
-of their meaning.
+between two embeddings measures their semantic similarity. When you compare the
+similarity of a query embedding with stored embeddings, Redis can retrieve documents
+that closely match the query's meaning.
 
 In the example below, we use the
 [`@xenova/transformers`](https://www.npmjs.com/package/@xenova/transformers)
 library to generate vector embeddings to store and index with
-Redis Query Engine.
+Redis Query Engine. The code is first demonstrated for hash documents with a
+separate section to explain the
+[differences with JSON documents](#differences-with-json-documents).
 
 ## Initialize
 
-Install [`node-redis`]({{< relref "/develop/clients/nodejs" >}}) if you
-have not already done so. Also, install `@xenova/transformers` with the
-following command:
+Install the required dependencies:
+
+1. Install [`node-redis`]({{< relref "/develop/clients/nodejs" >}}) if you haven't already.
+2. Install `@xenova/transformers`:
 
 ```bash
 npm install @xenova/transformers
 ```
 
-In a new JavaScript source file, start by importing the required classes:
+In your JavaScript source file, import the required classes:
 
 ```js
 import * as transformers from '@xenova/transformers';
 import {VectorAlgorithms, createClient, SchemaFieldTypes} from 'redis';
 ```
 
-The first of these imports is the `@xenova/transformers` module, which handles
-the embedding models.
-Here, we use an instance of the
+The `@xenova/transformers` module handles embedding models. This example uses the
 [`all-distilroberta-v1`](https://huggingface.co/sentence-transformers/all-distilroberta-v1)
-model for the embeddings. This model generates vectors with 768 dimensions, regardless
-of the length of the input text, but note that the input is truncated to 128
-tokens (see
-[Word piece tokenization](https://huggingface.co/learn/nlp-course/en/chapter6/6)
-at the [Hugging Face](https://huggingface.co/) docs to learn more about the way tokens
-are related to the original text).
-
-The `pipe` value obtained here is a function that we can call to generate the
-embeddings. We also need an object to pass some options for the `pipe()` function
-call. These specify the way the sentence embedding is generated from individual
-token embeddings (see the
+model, which:
+- Generates 768-dimensional vectors
+- Truncates input to 128 tokens
+- Uses word piece tokenization (see [Word piece tokenization](https://huggingface.co/learn/nlp-course/en/chapter6/6)
+  at the [Hugging Face](https://huggingface.co/) docs for details)
+
+The `pipe` function generates embeddings. The `pipeOptions` object specifies how to generate sentence embeddings from token embeddings (see the
 [`all-distilroberta-v1`](https://huggingface.co/sentence-transformers/all-distilroberta-v1)
-docs for more information).
+documentation for details):
 
 ```js
 let pipe = await transformers.pipeline(
@@ -81,30 +78,31 @@ const pipeOptions = {
 
 ## Create the index
 
-Connect to Redis and delete any index previously created with the
-name `vector_idx`. (The `dropIndex()` call throws an exception if
-the index doesn't already exist, which is why you need the
-`try...catch` block.)
+First, connect to Redis and remove any existing index named `vector_idx`:
 
 ```js
 const client = createClient({url: 'redis://localhost:6379'});
-
 await client.connect();
 
-try { await client.ft.dropIndex('vector_idx'); } catch {}
+try { 
+    await client.ft.dropIndex('vector_idx'); 
+} catch (e) {
+    // Index doesn't exist, which is fine
+}
 ```
 
-Next, create the index.
-The schema in the example below specifies hash objects for storage and includes
-three fields: the text content to index, a
-[tag]({{< relref "/develop/interact/search-and-query/advanced-concepts/tags" >}})
-field to represent the "genre" of the text, and the embedding vector generated from
-the original text content. The `embedding` field specifies
-[HNSW]({{< relref "/develop/interact/search-and-query/advanced-concepts/vectors#hnsw-index" >}}) 
-indexing, the
-[L2]({{< relref "/develop/interact/search-and-query/advanced-concepts/vectors#distance-metrics" >}})
-vector distance metric, `Float32` values to represent the vector's components,
-and 768 dimensions, as required by the `all-distilroberta-v1` embedding model.
+Next, create the index with the following schema:
+-   `content`: Text field for the content to index
+-   `genre`: [Tag]({{< relref "/develop/interact/search-and-query/advanced-concepts/tags" >}})
+    field representing the text's genre
+-   `embedding`: [Vector]({{< relref "/develop/interact/search-and-query/advanced-concepts/vectors" >}})
+    field with:
+    -   [HNSW]({{< relref "/develop/interact/search-and-query/advanced-concepts/vectors#hnsw-index" >}})
+        indexing
+    -   [L2]({{< relref "/develop/interact/search-and-query/advanced-concepts/vectors#distance-metrics" >}})
+        distance metric
+    -   Float32 values
+    -   768 dimensions (matching the embedding model)
 
 ```js
 await client.ft.create('vector_idx', {
@@ -112,7 +110,7 @@ await client.ft.create('vector_idx', {
         type: SchemaFieldTypes.TEXT,
     },
     'genre': {
-        type:SchemaFieldTypes.TAG,
+        type: SchemaFieldTypes.TAG,
     },
     'embedding': {
         type: SchemaFieldTypes.VECTOR,
@@ -121,7 +119,7 @@ await client.ft.create('vector_idx', {
         DISTANCE_METRIC: 'L2',
         DIM: 768,
     }
-},{
+}, {
     ON: 'HASH',
     PREFIX: 'doc:'
 });
@@ -129,34 +127,21 @@ await client.ft.create('vector_idx', {
 
 ## Add data
 
-You can now supply the data objects, which will be indexed automatically
-when you add them with [`hSet()`]({{< relref "/commands/hset" >}}), as long as
-you use the `doc:` prefix specified in the index definition.
-
-Use the `pipe()` method and the `pipeOptions` object that we created earlier to
-generate the embedding that represents the `content` field.
-The object returned by `pipe()` includes a `data` attribute, which is a
-[`Float32Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Float32Array)
-that contains the embedding data. If you are indexing hash objects, as
-we are here, then you must also call
-[`Buffer.from()`](https://nodejs.org/api/buffer.html#static-method-bufferfromarraybuffer-byteoffset-length)
-on this array's `buffer` value to convert the `Float32Array`
-to a binary string. If you are indexing JSON objects, you can just
-use the `Float32Array` directly to represent the embedding.
-
-Make the `hSet()` calls within a
-[`Promise.all()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all)
-call to create a Redis [pipeline]({{< relref "/develop/use/pipelining" >}})
-(not to be confused with the `@xenova/transformers` pipeline).
-This combines the commands together into a batch to reduce network
-round trip time.
+Add data objects to the index using `hSet()`. The index automatically processes objects with the `doc:` prefix.
+
+For each document:
+1. Generate an embedding using the `pipe()` function and `pipeOptions`
+2. Convert the embedding to a binary string using `Buffer.from()`
+3. Store the document with `hSet()`
+
+Use `Promise.all()` to batch the commands and reduce network round trips:
 
 ```js
 const sentence1 = 'That is a very happy person';
 const doc1 = {
     'content': sentence1, 
-    'genre':'persons', 
-    'embedding':Buffer.from(
+    'genre': 'persons', 
+    'embedding': Buffer.from(
         (await pipe(sentence1, pipeOptions)).data.buffer
     ),
 };
@@ -164,7 +149,7 @@ const doc1 = {
 const sentence2 = 'That is a happy dog';
 const doc2 = {
     'content': sentence2, 
-    'genre':'pets', 
+    'genre': 'pets', 
     'embedding': Buffer.from(
         (await pipe(sentence2, pipeOptions)).data.buffer
     )
@@ -173,7 +158,7 @@ const doc2 = {
 const sentence3 = 'Today is a sunny day';
 const doc3 = {
     'content': sentence3, 
-    'genre':'weather', 
+    'genre': 'weather', 
     'embedding': Buffer.from(
         (await pipe(sentence3, pipeOptions)).data.buffer
     )
@@ -188,24 +173,14 @@ await Promise.all([
 
 ## Run a query
 
-After you have created the index and added the data, you are ready to run a query.
-To do this, you must create another embedding vector from your chosen query
-text. Redis calculates the vector distance between the query vector and each
-embedding vector in the index and then ranks the results in order of this
-distance value.
-
-The code below creates the query embedding using `pipe()`, as with
-the indexing, and passes it as a parameter during execution
-(see
-[Vector search]({{< relref "/develop/interact/search-and-query/query/vector-search" >}})
-for more information about using query parameters with embeddings).
-
-The query returns an array of objects representing the documents
-that were found (which are hash objects here). The `id` attribute
-contains the document's key. The `value` attribute contains an object
-with a key-value entry corresponding to each index field specified in the
-`RETURN` option of the query.
+To query the index:
+1. Generate an embedding for your query text
+2. Pass the embedding as a parameter to the search
+3. Redis calculates vector distances and ranks results
 
+The query returns an array of document objects. Each object contains:
+- `id`: The document's key
+- `value`: An object with fields specified in the `RETURN` option
 
 ```js
 const similar = await client.ft.search(
@@ -229,9 +204,7 @@ for (const doc of similar.documents) {
 await client.quit();
 ```
 
-The code is now ready to run, but note that it may take a while to download the
-`all-distilroberta-v1` model data the first time you run it. The
-code outputs the following results:
+The first run may take longer as it downloads the model data. The output shows results ordered by score (vector distance), with lower scores indicating greater similarity:
 
 ```
 doc:1: 'That is a very happy person', Score: 0.127055495977
@@ -239,17 +212,101 @@ doc:2: 'That is a happy dog', Score: 0.836842417717
 doc:3: 'Today is a sunny day', Score: 1.50889515877
 ```
 
-The results are ordered according to the value of the `score`
-field, which represents the vector distance here. The lowest distance indicates
-the greatest similarity to the query.
-As you would expect, the result for `doc:1` with the content text
-*"That is a very happy person"*
-is the result that is most similar in meaning to the query text
-*"That is a happy person"*.
+## Differences with JSON documents
+
+JSON documents support richer data modeling with nested fields. Key differences from hash documents:
+
+1. Use paths in the schema to identify fields
+2. Declare aliases for paths using the `AS` option
+3. Set `ON` to `JSON` when creating the index
+4. Use arrays instead of binary strings for vectors
+5. Use `json.set()` instead of `hSet()`
+
+Create the index with path aliases:
+
+```js
+await client.ft.create('vector_json_idx', {
+    '$.content': {
+        type: SchemaFieldTypes.TEXT,
+        AS: 'content',
+    },
+    '$.genre': {
+        type: SchemaFieldTypes.TAG,
+        AS: 'genre',
+    },
+    '$.embedding': {
+        type: SchemaFieldTypes.VECTOR,
+        TYPE: 'FLOAT32',
+        ALGORITHM: VectorAlgorithms.HNSW,
+        DISTANCE_METRIC: 'L2',
+        DIM: 768,
+        AS: 'embedding',
+    }
+}, {
+    ON: 'JSON',
+    PREFIX: 'jdoc:'
+});
+```
+
+Add data using `json.set()`. Convert the `Float32Array` to a standard JavaScript array using the spread operator:
+
+```js
+const jSentence1 = 'That is a very happy person';
+const jdoc1 = {
+    'content': jSentence1,
+    'genre': 'persons',
+    'embedding': [...(await pipe(jSentence1, pipeOptions)).data],
+};
+
+const jSentence2 = 'That is a happy dog';
+const jdoc2 = {
+    'content': jSentence2,
+    'genre': 'pets',
+    'embedding': [...(await pipe(jSentence2, pipeOptions)).data],
+};
+
+const jSentence3 = 'Today is a sunny day';
+const jdoc3 = {
+    'content': jSentence3,
+    'genre': 'weather',
+    'embedding': [...(await pipe(jSentence3, pipeOptions)).data],
+};
+
+await Promise.all([
+    client.json.set('jdoc:1', '$', jdoc1),
+    client.json.set('jdoc:2', '$', jdoc2),
+    client.json.set('jdoc:3', '$', jdoc3)
+]);
+```
+
+Query JSON documents using the same syntax, but note that the vector parameter must still be a binary string:
+
+```js
+const jsons = await client.ft.search(
+    'vector_json_idx',
+    '*=>[KNN 3 @embedding $B AS score]',
+    {
+        "PARAMS": {
+            B: Buffer.from(
+                (await pipe('That is a happy person', pipeOptions)).data.buffer
+            ),
+        },
+        'RETURN': ['score', 'content'],
+        'DIALECT': '2'
+    },
+);
+```
+
+The results are identical to the hash document query, except for the `jdoc:` prefix:
+
+```
+jdoc:1: 'That is a very happy person', Score: 0.127055495977
+jdoc:2: 'That is a happy dog', Score: 0.836842417717
+jdoc:3: 'Today is a sunny day', Score: 1.50889515877
+```
 
 ## Learn more
 
 See
 [Vector search]({{< relref "/develop/interact/search-and-query/query/vector-search" >}})
-for more information about the indexing options, distance metrics, and query format
-for vectors.
+for more information about indexing options, distance metrics, and query format.


Reviews:
user: login: dwdougherty
body: Coupla nits. Otherwise, LGTM.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-28T13:20:00Z
user: login: andy-stark-redis
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T13:55:10Z

Review comments:
diff_hunk: @@ -414,6 +417,156 @@ As you would expect, the result for `doc:1` with the content text
 is the result that is most similar in meaning to the query text
 *"That is a happy person"*.
 
+## Differences with JSON documents
+
+Indexing JSON documents is similar to hash indexing, but there are some
+important differences. JSON allows much richer data modelling with nested fields, so
path: content/develop/clients/dotnet/vecsearch.md
user: login: dwdougherty
body: US English per the SG

```suggestion
important differences. JSON allows much richer data modeling with nested fields, so
```
created_at: 2025-04-28T13:13:48Z
author_association: COLLABORATOR
diff_hunk: @@ -16,57 +16,54 @@ weight: 3
 ---
 
 [Redis Query Engine]({{< relref "/develop/interact/search-and-query" >}})
-lets you index vector fields in [hash]({{< relref "/develop/data-types/hashes" >}})
+enables you to index vector fields in [hash]({{< relref "/develop/data-types/hashes" >}})
path: content/develop/clients/nodejs/vecsearch.md
user: login: dwdougherty
body: Looks like the [SG prefers "lets you"](https://developers.google.com/style/word-list#enable), if I read that correctly. Not a big deal.
created_at: 2025-04-28T13:17:19Z
author_association: COLLABORATOR
diff_hunk: @@ -16,57 +16,54 @@ weight: 3
 ---
 
 [Redis Query Engine]({{< relref "/develop/interact/search-and-query" >}})
-lets you index vector fields in [hash]({{< relref "/develop/data-types/hashes" >}})
+enables you to index vector fields in [hash]({{< relref "/develop/data-types/hashes" >}})
path: content/develop/clients/nodejs/vecsearch.md
user: login: andy-stark-redis
body: @dwdougherty This is what I get for asking Cursor for style advice :-( It actually said that the guide prefers "enables" (which it doesn't, you are correct), and it has rewritten some of the other stuff in a way that I'm not sure is better. It's as well I checked the text again because I've now noticed it has even trashed some quite useful links (I've added them back in now).

I'll be more careful next time I try using AI for this sort of thing!
created_at: 2025-04-28T13:55:10Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-28T12:58:26Z
body: Staging links:<br> https://redis.io/docs/staging/DOC-5150-js-vec-json-examples/develop/clients/dotnet/vecsearch<br>https://redis.io/docs/staging/DOC-5150-js-vec-json-examples/develop/clients/nodejs/vecsearch
author_association: CONTRIBUTOR
user: login: andy-stark-redis
created_at: 2025-04-28T13:57:05Z
body: @dwdougherty Thanks for the review! 
author_association: CONTRIBUTOR