diff --git a/build/check_tryit_payloads.py b/build/check_tryit_payloads.py new file mode 100644 index 0000000000..0c3357a54b --- /dev/null +++ b/build/check_tryit_payloads.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +""" +Check interactive "Try it" payload sizes across the docs. + +The external "Try it" button on a `clients-example` block opens +`redis.io/cli?commands=&autorun=true`, where is the URL-safe +base64 of a JSON array of the block's CLI commands. The server rejects the +request with "Request Line is too large (N > 4094)" once the **HTTP request +line** exceeds ~4094 bytes. The request line is `GET /cli?commands=& +autorun=true HTTP/1.1`, i.e. the base64 payload plus ~40 bytes of scaffolding, +so the base64 must be ≲ 4054 bytes. Two subtleties this checker accounts for: + + * The cap is on the request line / URL, not the raw typed command text. + Sizes are computed to match the browser exactly: base64 of + JSON.stringify(cmds) (compact separators, real UTF-8) — see _payload_b64_len. + * A block with `needs_prereq="true"` has its set's `prereq="true"` block + prepended by the button, so it inherits the prereq's payload size. + +A block is only subject to the limit if its external button is actually shown: +`try_it != "false"` and `runnable != "false"`. Blocks over the limit should get +`try_it="false"` (the on-page terminal still runs them — it does not go through +the URL). See the DOC-6823 work on content/develop/get-started/document-database.md. + +Usage: + python build/check_tryit_payloads.py [--content-dir content] [--limit 4094] + [--overhead 40] [--warn-frac 0.85] [--quiet] + +Exit status: 1 if any interactive block's request line exceeds the limit, else 0 +(so it can gate CI). Stdlib only; run from the repo root. +""" + +import argparse +import base64 +import glob +import json +import os +import re +import sys + +# Match a shortcode attribute value. The negative lookbehind on a word char +# stops `prereq="..."` from also matching inside `needs_prereq="..."`. +def _attr(tag, name): + m = re.search(r'(?\}\}', re.S) +_CLOSE = '{{< /clients-example' + + +def _parse_blocks(text): + """Yield (open_tag, body_between_tags) for each clients-example block.""" + parts = re.split(r'(' + _OPEN_TAG.pattern + r')', text, flags=re.S) + i = 1 + while i < len(parts): + tag = parts[i] + body = parts[i + 1] if i + 1 < len(parts) else "" + end = body.find(_CLOSE) + if end >= 0: + body = body[:end] + yield tag, body + i += 2 + + +def _cli_cmds(body): + """Extract the redis-cli command lines (the `> ` / `redis> ` prefixed lines).""" + cmds = [] + for line in body.split("\n"): + s = line.strip() + if s.startswith("redis> "): + cmds.append(s[len("redis> "):]) + elif s.startswith("> "): + cmds.append(s[2:]) + return cmds + + +def _payload_b64_len(cmds): + """Bytes of the URL-safe base64 payload the button would send (no padding). + + Mirrors the browser's btoa(unescape(encodeURIComponent(JSON.stringify(cmds)))): + JSON.stringify emits compact JSON (no spaces after ',' / ':') and keeps real + Unicode, so use compact separators and ensure_ascii=False + UTF-8 bytes. + Default json.dumps (spaces, \\uXXXX escapes) would overstate the size. + """ + js = json.dumps(cmds, separators=(",", ":"), ensure_ascii=False) + b64 = base64.b64encode(js.encode("utf-8")).decode().rstrip("=") + return len(b64) + + +def scan(content_dir): + """Return (blocks, set_prereq) for all clients-example blocks under content_dir.""" + blocks = [] + set_prereq = {} # (file, set) -> prereq command list + for f in sorted(glob.glob(os.path.join(content_dir, "**", "*.md"), recursive=True)): + try: + text = open(f, encoding="utf-8").read() + except (OSError, UnicodeDecodeError): + continue + if "clients-example" not in text: + continue + for tag, body in _parse_blocks(text): + b = { + "file": f, + "set": _attr(tag, "set"), + "step": _attr(tag, "step"), + "prereq": _attr(tag, "prereq") == "true", + "needs_prereq": _attr(tag, "needs_prereq") == "true", + "runnable": _attr(tag, "runnable"), + "try_it": _attr(tag, "try_it"), + "cmds": _cli_cmds(body), + } + blocks.append(b) + if b["prereq"]: + set_prereq[(b["file"], b["set"])] = b["cmds"] + return blocks, set_prereq + + +def payload_for(b, set_prereq): + """The full command list the button would send for block b.""" + cmds = [] + if b["needs_prereq"]: + cmds += set_prereq.get((b["file"], b["set"]), []) + cmds += b["cmds"] + return cmds + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--content-dir", default="content") + ap.add_argument("--limit", type=int, default=4094, + help="max HTTP request-line bytes for redis.io/cli " + "(observed server cap: 4094)") + ap.add_argument("--overhead", type=int, default=40, + help="bytes added to the base64 payload to form the request " + "line: 'GET /cli?commands=' + '&autorun=true HTTP/1.1' " + "(~40; observed ~24, so 40 is conservative)") + ap.add_argument("--warn-frac", type=float, default=0.85, + help="warn when payload exceeds this fraction of the limit") + ap.add_argument("--quiet", action="store_true", + help="only print offenders, not the summary") + args = ap.parse_args() + + blocks, set_prereq = scan(args.content_dir) + + over, near = [], [] + for b in blocks: + # Only blocks whose external button is actually shown are subject to the limit. + if b["try_it"] == "false" or b["runnable"] == "false" or not b["cmds"]: + continue + b64 = _payload_b64_len(payload_for(b, set_prereq)) + size = b64 + args.overhead # estimated HTTP request-line length + if size > args.limit: + over.append((size, b64, b)) + elif size > args.limit * args.warn_frac: + near.append((size, b64, b)) + + for size, b64, b in sorted(near, key=lambda x: x[0], reverse=True): + print(f"WARN req-line ~{size} B ({100*size/args.limit:.0f}% of " + f"{args.limit}, b64={b64}) {b['set']}/{b['step']} ({b['file']})") + for size, b64, b in sorted(over, key=lambda x: x[0], reverse=True): + print(f"OVER req-line ~{size} B (> {args.limit}, b64={b64}) " + f"{b['set']}/{b['step']} ({b['file']}) -- set try_it=\"false\" " + f"or trim to b64 <= {args.limit - args.overhead}") + + if not args.quiet: + interactive = sum(1 for b in blocks + if b["try_it"] != "false" and b["runnable"] != "false" + and b["cmds"]) + print(f"\nScanned {len(blocks)} blocks ({interactive} interactive) under " + f"{args.content_dir}/. Request-line limit={args.limit} B " + f"(base64 payload + {args.overhead} B scaffolding). " + f"{len(over)} over, {len(near)} near.") + + return 1 if over else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/build/example-test-harness/.gitignore b/build/example-test-harness/.gitignore new file mode 100644 index 0000000000..1e8d39635c --- /dev/null +++ b/build/example-test-harness/.gitignore @@ -0,0 +1,3 @@ +# transient: cached per-language projects/deps and run logs +work/ +results/ diff --git a/build/example-test-harness/README.md b/build/example-test-harness/README.md new file mode 100644 index 0000000000..ec70e54ca1 --- /dev/null +++ b/build/example-test-harness/README.md @@ -0,0 +1,52 @@ +# TCE example test harness + +Runs a docs example set's per-client source files (from `local_examples/`) against a +**live throwaway Redis on `localhost:6379`**, using each library's real in-file +assertions. Built for the DOC-6823 work (making `try_it="false"` examples self-contained), +but reusable for any example set. + +## Usage + +```bash +./run.sh [client ...] +# all clients: +./run.sh ss_tutorial +# one/some: +./run.sh set_tutorial rust-sync dotnet +``` + +`example_set` ∈ { `ss_tutorial`, `set_tutorial` } today. Results print as a matrix; +per-run logs land in `results/_.log`. + +⚠️ Several examples call `FLUSHALL`/`FLUSHDB`, and the harness flushes before each run. +Point it only at a scratch Redis. + +## Clients & how each is run + +| Client | Toolchain | Notes | +|---|---|---| +| python | venv + `redis` | run script directly (`assert`) | +| node | `npm i redis`, ESM | run as `.mjs` | +| go | module + `go-redis` | `go test`; needs a sibling `package example_commands` stub | +| jedis | Maven + `jedis:5.2.0` | surefire include `**/*Example.java` (classes aren't `*Test`) | +| lettuce-async / -reactive | Maven + `lettuce-core:6.5.5.RELEASE` | same surefire include | +| ruby | `redis` gem | run script (`raise`/local `assert_equal`) | +| rust-sync | Cargo + `redis = "1.3"` | file is `#[cfg(test)]` → dropped in `src/lib.rs`, `cargo test` | +| rust-async | Cargo + `redis` (tokio-comp) + `tokio` | `#[tokio::test]` | +| php | Composer + `predis/predis` | `bootstrap.php` stubs `PredisTestCase` asserts; reflection finds the `test*` method | +| dotnet | `dotnet test` + xunit + `StackExchange.Redis` | `dotnet/stubs.cs` stands in for NRedisStack's `AbstractNRedisStackTest`/`EndpointsFixture`/`[SkippableFact]`/`DocsTests` collection | + +## Gotchas learned + +- **Version pins matter.** `redis-rs` is now **1.x** (`1.3`), not `0.27` — the old pin failed + to compile `flushall` in a REMOVE block. Jedis 5.2, Lettuce 6.5.5, StackExchange.Redis 2.8.x. +- **Surefire only runs `*Test`/`*Tests` by default** — these classes are `*Example`, so the + POMs add an explicit ``. First run looked green with **zero tests** without it. +- The test scaffolding lives in `REMOVE_START` blocks; for py/ruby/node/go/rust/jedis/lettuce + it's self-contained (stdlib asserts / JUnit), but **C# and PHP reference their repo's own + test base classes**, which is why they need the stubs above. + +## Adding a new example set + +Add one block of `case "$set:$client"` → source-path lines in `src_path()` in `run.sh`. +Nothing else changes; deps are cached under `work/`. diff --git a/build/example-test-harness/dotnet/GlobalUsings.cs b/build/example-test-harness/dotnet/GlobalUsings.cs new file mode 100644 index 0000000000..c802f4480b --- /dev/null +++ b/build/example-test-harness/dotnet/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/build/example-test-harness/dotnet/harness.csproj b/build/example-test-harness/dotnet/harness.csproj new file mode 100644 index 0000000000..883b5dd0d1 --- /dev/null +++ b/build/example-test-harness/dotnet/harness.csproj @@ -0,0 +1,16 @@ + + + net9.0 + latest + disable + enable + false + + + + + + + + + diff --git a/build/example-test-harness/dotnet/stubs.cs b/build/example-test-harness/dotnet/stubs.cs new file mode 100644 index 0000000000..7901bd88ba --- /dev/null +++ b/build/example-test-harness/dotnet/stubs.cs @@ -0,0 +1,33 @@ +// Minimal stand-ins for NRedisStack's own test fixtures, so a docs example +// file (which extends AbstractNRedisStackTest and uses [SkippableFact]) runs +// under plain xunit + StackExchange.Redis. The example creates its own +// ConnectionMultiplexer, so the fixture helpers can be no-ops. +using StackExchange.Redis; + +namespace NRedisStack.Tests +{ + public class EndpointsFixture + { + public enum Env { Standalone } + } + + public class AbstractNRedisStackTest : IDisposable + { + private ConnectionMultiplexer? _muxer; + protected AbstractNRedisStackTest(EndpointsFixture fixture) { } + protected void SkipIfTargetConnectionDoesNotExist(EndpointsFixture.Env env) { } + protected IDatabase GetCleanDatabase(EndpointsFixture.Env env) + { + _muxer ??= ConnectionMultiplexer.Connect("localhost:6379"); + return _muxer.GetDatabase(); + } + public void Dispose() { _muxer?.Dispose(); } + } + + // The examples annotate the test method [SkippableFact]; make it a plain Fact. + public class SkippableFactAttribute : FactAttribute { } +} + +// Satisfy [Collection("DocsTests")] + constructor injection of EndpointsFixture. +[CollectionDefinition("DocsTests")] +public class DocsTestsCollection : ICollectionFixture { } diff --git a/build/example-test-harness/pom-jedis.xml b/build/example-test-harness/pom-jedis.xml new file mode 100644 index 0000000000..d442649664 --- /dev/null +++ b/build/example-test-harness/pom-jedis.xml @@ -0,0 +1,15 @@ + + 4.0.0 + io.redisjedis1.0 + 17UTF-8 + + redis.clientsjedis7.5.3 + org.junit.jupiterjunit-jupiter5.10.2test + org.assertjassertj-core3.25.3test + + + org.apache.maven.pluginsmaven-surefire-plugin3.2.5 + **/*Example.java + + diff --git a/build/example-test-harness/pom-lettuce-async.xml b/build/example-test-harness/pom-lettuce-async.xml new file mode 100644 index 0000000000..b488bebbf5 --- /dev/null +++ b/build/example-test-harness/pom-lettuce-async.xml @@ -0,0 +1,15 @@ + + 4.0.0 + io.redislettuce-async1.0 + 17UTF-8 + + io.lettucelettuce-core6.5.5.RELEASE + org.junit.jupiterjunit-jupiter5.10.2test + org.assertjassertj-core3.25.3test + + + org.apache.maven.pluginsmaven-surefire-plugin3.2.5 + **/*Example.java + + diff --git a/build/example-test-harness/pom-lettuce-reactive.xml b/build/example-test-harness/pom-lettuce-reactive.xml new file mode 100644 index 0000000000..e3f67e7892 --- /dev/null +++ b/build/example-test-harness/pom-lettuce-reactive.xml @@ -0,0 +1,15 @@ + + 4.0.0 + io.redislettuce-reactive1.0 + 17UTF-8 + + io.lettucelettuce-core6.5.5.RELEASE + org.junit.jupiterjunit-jupiter5.10.2test + org.assertjassertj-core3.25.3test + + + org.apache.maven.pluginsmaven-surefire-plugin3.2.5 + **/*Example.java + + diff --git a/build/example-test-harness/run.sh b/build/example-test-harness/run.sh new file mode 100755 index 0000000000..406c41a0de --- /dev/null +++ b/build/example-test-harness/run.sh @@ -0,0 +1,206 @@ +#!/usr/bin/env bash +# Reusable TCE example test harness. +# Runs a docs example set's client source files against a live (throwaway) Redis +# on localhost:6379, using each library's real in-file assertions. +# +# Usage: ./run.sh [client ...] +# example_set : ss_tutorial | set_tutorial (add more in src_path()) +# client : one or more of the CLIENTS below; default = all +# +# Assumes a SCRATCH Redis on 6379 — several examples FLUSH the db. +set -uo pipefail + +REPO="$(cd "$(dirname "$0")/../.." && pwd)" +HARNESS="$(cd "$(dirname "$0")" && pwd)" +WORK="$HARNESS/work"; mkdir -p "$WORK" +SET="${1:?usage: run.sh [client...]}"; shift || true + +CLIENTS_ALL=(python node go jedis ruby rust-sync rust-async lettuce-async lettuce-reactive php dotnet) +CLIENTS=("$@"); [ ${#CLIENTS[@]} -eq 0 ] && CLIENTS=("${CLIENTS_ALL[@]}") + +# --- example_set + client -> repo-relative source path ----------------------- +src_path() { + local set="$1" client="$2" + case "$set:$client" in + ss_tutorial:python) echo local_examples/tmp/datatypes/sorted-sets/dt_ss.py ;; + ss_tutorial:node) echo local_examples/tmp/datatypes/sorted-sets/dt-ss.js ;; + ss_tutorial:go) echo local_examples/tmp/datatypes/sorted-sets/ss_tutorial_test.go ;; + ss_tutorial:jedis) echo local_examples/tmp/datatypes/sorted-sets/SortedSetsExample.java ;; + ss_tutorial:ruby) echo local_examples/ruby/dt_sorted_sets.rb ;; + ss_tutorial:rust-sync) echo local_examples/rust-sync/dt-sorted-sets.rs ;; + ss_tutorial:rust-async) echo local_examples/rust-async/dt-sorted-sets.rs ;; + ss_tutorial:lettuce-async) echo local_examples/client-specific/lettuce-async/SortedSetExample.java ;; + ss_tutorial:lettuce-reactive)echo local_examples/client-specific/lettuce-reactive/SortedSetExample.java ;; + ss_tutorial:php) echo local_examples/php/DtSortedSetsTest.php ;; + ss_tutorial:dotnet) echo local_examples/tmp/datatypes/sorted-sets/SortedSetExample.cs ;; + set_tutorial:python) echo local_examples/tmp/datatypes/strings/dt_string.py ;; + set_tutorial:node) echo local_examples/tmp/datatypes/strings/dt-string.js ;; + set_tutorial:go) echo local_examples/tmp/datatypes/strings/string_example_test.go ;; + set_tutorial:jedis) echo local_examples/tmp/datatypes/strings/StringExample.java ;; + set_tutorial:ruby) echo local_examples/ruby/dt_string.rb ;; + set_tutorial:rust-sync) echo local_examples/rust-sync/dt-string.rs ;; + set_tutorial:rust-async) echo local_examples/rust-async/dt-string.rs ;; + set_tutorial:lettuce-async) echo local_examples/client-specific/lettuce-async/StringExample.java ;; + set_tutorial:lettuce-reactive)echo local_examples/client-specific/lettuce-reactive/StringExample.java ;; + set_tutorial:php) echo local_examples/php/DtStringTest.php ;; + set_tutorial:dotnet) echo local_examples/tmp/datatypes/strings/StringSnippets.cs ;; + hash_tutorial:python) echo local_examples/tmp/datatypes/hashes/dt_hash.py ;; + hash_tutorial:node) echo local_examples/tmp/datatypes/hashes/dt-hash.js ;; + hash_tutorial:go) echo local_examples/tmp/datatypes/hashes/hash_tutorial_test.go ;; + hash_tutorial:jedis) echo local_examples/tmp/datatypes/hashes/HashExample.java ;; + hash_tutorial:ruby) echo local_examples/ruby/dt_hash.rb ;; + hash_tutorial:rust-sync) echo local_examples/rust-sync/dt-hash.rs ;; + hash_tutorial:rust-async) echo local_examples/rust-async/dt-hash.rs ;; + hash_tutorial:lettuce-async) echo local_examples/tmp/lettuce-async/HashExample.java ;; + hash_tutorial:lettuce-reactive)echo local_examples/tmp/lettuce-reactive/HashExample.java ;; + hash_tutorial:php) echo local_examples/php/DtHashTest.php ;; + hash_tutorial:dotnet) echo local_examples/tmp/datatypes/hashes/HashExample.cs ;; + sets_tutorial:python) echo local_examples/tmp/datatypes/sets/dt_set.py ;; + sets_tutorial:node) echo local_examples/tmp/datatypes/sets/dt-set.js ;; + sets_tutorial:go) echo local_examples/tmp/datatypes/sets/sets_example_test.go ;; + sets_tutorial:jedis) echo local_examples/tmp/datatypes/sets/SetsExample.java ;; + sets_tutorial:ruby) echo local_examples/ruby/dt_sets.rb ;; + sets_tutorial:rust-sync) echo local_examples/rust-sync/dt-sets.rs ;; + sets_tutorial:rust-async) echo local_examples/rust-async/dt-sets.rs ;; + sets_tutorial:lettuce-async) echo local_examples/tmp/lettuce-async/SetExample.java ;; + sets_tutorial:lettuce-reactive)echo local_examples/tmp/lettuce-reactive/SetExample.java ;; + sets_tutorial:php) echo local_examples/php/DtSetsTest.php ;; + sets_tutorial:dotnet) echo local_examples/tmp/datatypes/sets/SetsTutorial.cs ;; + time_series_tutorial:python) echo local_examples/time_series_tutorial/redis-py/dt_time_series.py ;; + time_series_tutorial:go) echo local_examples/time_series_tutorial/go-redis/timeseries_tut_test.go ;; + time_series_tutorial:jedis) echo local_examples/time_series_tutorial/jedis/TimeSeriesTutorialExample.java ;; + time_series_tutorial:node) echo local_examples/time_series_tutorial/node-redis/dt-time-series.js ;; + time_series_tutorial:dotnet) echo local_examples/time_series_tutorial/nredisstack/TimeSeriesTutorial.cs ;; + # search_quickstart / geoindex: all mapped clients PASS (pom-jedis.xml is 7.5.3, + # which has both the RedisClient API these examples use and the older UnifiedJedis + # API; the node wildcard assert now checks result.total, not doc order). + search_quickstart:python) echo local_examples/search_quickstart/redis-py/search_quickstart.py ;; + search_quickstart:node) echo local_examples/search_quickstart/node-redis/search-quickstart.js ;; + search_quickstart:go) echo local_examples/search_quickstart/go-redis/search_quickstart_test.go ;; + search_quickstart:jedis) echo local_examples/search_quickstart/jedis/SearchQuickstartExample.java ;; + search_quickstart:dotnet) echo local_examples/search_quickstart/nredisstack/SearchQuickstartExample.cs ;; + geoindex:python) echo local_examples/geoindex/redis-py/geo_index.py ;; + geoindex:go) echo local_examples/geoindex/go-redis/geo_index_test.go ;; + geoindex:jedis) echo local_examples/geoindex/jedis/GeoIndexExample.java ;; + geoindex:dotnet) echo local_examples/geoindex/nredisstack/GeoIndexExample.cs ;; + *) echo "" ;; + esac +} + +log() { printf '%s\n' "$*"; } +SUMMARY=() # entries: "clientstatus" (bash 3.2 compatible) + +# --- per-language runners: set $rc (0=pass) and leave detail in $LOG ---------- +run_python() { + [ -d "$WORK/py/venv" ] || { python3 -m venv "$WORK/py/venv"; "$WORK/py/venv/bin/pip" -q install redis; } + "$WORK/py/venv/bin/python" "$1" >"$LOG" 2>&1; rc=$? +} +run_ruby() { + gem list -i redis >/dev/null 2>&1 || gem install --silent redis >/dev/null 2>&1 + ruby "$1" >"$LOG" 2>&1; rc=$? +} +run_node() { + local d="$WORK/node"; mkdir -p "$d" + [ -d "$d/node_modules/redis" ] || { printf '{"type":"module"}\n' >"$d/package.json"; (cd "$d" && npm i -s redis >/dev/null 2>&1); } + cp "$1" "$d/example.mjs"; (cd "$d" && node example.mjs) >"$LOG" 2>&1; rc=$? +} +run_go() { + local d="$WORK/go"; mkdir -p "$d" + if [ ! -f "$d/go.mod" ]; then + (cd "$d" && go mod init tce.local >/dev/null 2>&1 && printf 'package example_commands\n' >lib.go \ + && go get github.com/redis/go-redis/v9 >/dev/null 2>&1) + fi + cp "$1" "$d/ex_test.go"; (cd "$d" && go test ./... ) >"$LOG" 2>&1; rc=$? +} +run_rust_sync() { rust_run "$WORK/rust-sync" "$1" 'redis = "1.3"' ; } +run_rust_async(){ rust_run "$WORK/rust-async" "$1" 'redis = { version = "1.3", features = ["tokio-comp"] } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] }' ; } +rust_run() { + local d="$1" src="$2" deps="$3"; mkdir -p "$d/src" + if [ ! -f "$d/Cargo.toml" ]; then + cat >"$d/Cargo.toml" <"$LOG" 2>&1; rc=$? +} +run_maven_java() { # $1=src $2=workdir $3=package-relpath + local d="$2"; mkdir -p "$d/src/test/java/$3" + [ -f "$d/pom.xml" ] || cp "$HARNESS/pom-$(basename "$d").xml" "$d/pom.xml" + rm -f "$d/src/test/java/$3"/*.java + cp "$1" "$d/src/test/java/$3/" + (cd "$d" && mvn -q -B test) >"$LOG" 2>&1; rc=$? +} +run_jedis() { run_maven_java "$1" "$WORK/jedis" io/redis/examples ; } +run_lettuce_async() { run_maven_java "$1" "$WORK/lettuce-async" io/redis/examples/async ; } +run_lettuce_reactive() { run_maven_java "$1" "$WORK/lettuce-reactive" io/redis/examples/reactive ; } +run_dotnet() { # stubs NRedisStack.Tests fixtures so the file runs under plain xunit + local d="$WORK/dotnet"; mkdir -p "$d" + cp "$HARNESS/dotnet/harness.csproj" "$d/harness.csproj" + cp "$HARNESS/dotnet/stubs.cs" "$d/stubs.cs" + cp "$HARNESS/dotnet/GlobalUsings.cs" "$d/GlobalUsings.cs" + rm -f "$d"/Example_*.cs; cp "$1" "$d/Example_src.cs" + (cd "$d" && dotnet test --nologo) >"$LOG" 2>&1; rc=$? +} +run_php() { + local d="$WORK/php"; mkdir -p "$d" + [ -d "$d/vendor/predis" ] || { printf '{}\n' >"$d/composer.json"; (cd "$d" && composer -q require predis/predis >/dev/null 2>&1); } + cat >"$d/bootstrap.php" <<'PHP' +$m(); } } + fwrite(STDERR,"OK\n"); + ') >"$LOG" 2>&1; rc=$? +} + +# --- drive -------------------------------------------------------------------- +log "=== TCE sweep: $SET (redis @ localhost:6379) ===" +# Fail fast if the scratch Redis is unreachable — every run FLUSHes it and relies +# on a clean db, so silently proceeding would give misleading pass/fail results. +if ! redis-cli ping >/dev/null 2>&1; then + log "ERROR: no Redis responding on localhost:6379 — start a throwaway Redis first." + exit 1 +fi +mkdir -p "$HARNESS/results" +for c in "${CLIENTS[@]}"; do + rel="$(src_path "$SET" "$c")" + if [ -z "$rel" ] || [ ! -f "$REPO/$rel" ]; then SUMMARY+=("$c SKIP (no source)"); log ">> $c: SKIP"; continue; fi + LOG="$HARNESS/results/${SET}_${c}.log"; rc=1 + # A failed flush means stale keys leak into the next example -> unreliable + # results, so abort loudly rather than test against leftover state. + if ! redis-cli flushall >/dev/null 2>&1; then + log "ERROR: 'redis-cli flushall' failed before $c — aborting to avoid testing against stale keys." + exit 1 + fi + log ">> $c: running..." + "run_${c//-/_}" "$REPO/$rel" + if [ "${rc:-1}" -eq 0 ]; then SUMMARY+=("$c PASS"); log ">> $c: PASS" + else SUMMARY+=("$c FAIL (results/${SET}_${c}.log)"); log ">> $c: FAIL"; fi +done + +log ""; log "=== RESULTS: $SET ===" +for e in "${SUMMARY[@]}"; do printf ' %-18s %s\n' "${e%% *}" "${e#* }"; done + +# Exit non-zero if any client FAILed, so the harness can gate CI. +for e in "${SUMMARY[@]}"; do case "$e" in *FAIL*) exit 1;; esac; done +exit 0 diff --git a/content/develop/ai/search-and-query/indexing/geoindex.md b/content/develop/ai/search-and-query/indexing/geoindex.md index 10aa6bfed0..05e0436932 100644 --- a/content/develop/ai/search-and-query/indexing/geoindex.md +++ b/content/develop/ai/search-and-query/indexing/geoindex.md @@ -34,29 +34,21 @@ reference page for a full description of both types. ## `GEO` -The following command creates a `GEO` index for JSON objects that contain -the geospatial data in a field called `location`: - -{{< clients-example set="geoindex" step="create_geo_idx" description="Foundational: Create an index for geospatial point data using the GEO field type when you need to store and query longitude-latitude coordinates" difficulty="beginner" >}} +The following example creates a `GEO` index for JSON objects that store +geospatial data in a field called `location`, adds two product documents (any +JSON object with the `product:` prefix and a `location` field is indexed +automatically), and then queries for products within a 100 mile radius of +Colorado Springs (Longitude=-104.800644, Latitude=38.846127). This returns only +the location in Denver, but a radius of 200 miles would also include the +location in Fort Collins: + +{{< clients-example set="geoindex" step="create_geo_idx" description="GEO index: create a GEO index, add geospatial point documents, then run a radius query to find nearby locations" difficulty="beginner" >}} > FT.CREATE productidx ON JSON PREFIX 1 product: SCHEMA $.location AS location GEO OK -{{< /clients-example >}} - -If you now add JSON objects with the `product:` prefix and a `location` field, -they will be added to the index automatically: - -{{< clients-example set="geoindex" step="add_geo_json" description="Foundational: Add JSON documents with geospatial point data to an indexed field when you need to populate the index with location information" difficulty="beginner" >}} > JSON.SET product:46885 $ '{"description": "Navy Blue Slippers","price": 45.99,"city": "Denver","location": "-104.991531, 39.742043"}' OK > JSON.SET product:46886 $ '{"description": "Bright Green Socks","price": 25.50,"city": "Fort Collins","location": "-105.0618814,40.5150098"}' OK -{{< /clients-example >}} - -The query below finds products within a 100 mile radius of Colorado Springs -(Longitude=-104.800644, Latitude=38.846127). This returns only the location in -Denver, but a radius of 200 miles would also include the location in Fort Collins: - -{{< clients-example set="geoindex" step="geo_query" description="GEO radius query: Query geospatial points within a radius using center coordinates and distance when you need to find nearby locations" difficulty="beginner" try_it="false" >}} > FT.SEARCH productidx '@location:[-104.800644 38.846127 100 mi]' 1) "1" 2) "product:46885" @@ -69,20 +61,18 @@ for more information about the available options. ## `GEOSHAPE` -The following command creates an index for JSON objects that include +The following example creates an index for JSON objects that include geospatial data in a field called `geom`. The `FLAT` option at the end of the field definition specifies Cartesian coordinates instead of -the default spherical geographical coordinates. Use `SPHERICAL` in -place of `FLAT` to choose the coordinate space explicitly. +the default spherical geographical coordinates (use `SPHERICAL` in +place of `FLAT` to choose the coordinate space explicitly). It then adds +several shapes using the `shape:` prefix and runs a query that returns any +shapes within the boundary of the green square but omits the green square +itself: -{{< clients-example set="geoindex" step="create_gshape_idx" description="Foundational: Create an index for geometric shapes using the GEOSHAPE field type when you need to store and query polygons and points" difficulty="intermediate" >}} +{{< clients-example set="geoindex" step="create_gshape_idx" description="GEOSHAPE index: create a GEOSHAPE index, add polygon and point documents, then run a WITHIN query to test shape containment" difficulty="intermediate" >}} > FT.CREATE geomidx ON JSON PREFIX 1 shape: SCHEMA $.name AS name TEXT $.geom AS geom GEOSHAPE FLAT OK -{{< /clients-example >}} - -Use the `shape:` prefix for the JSON objects to add them to the index: - -{{< clients-example set="geoindex" step="add_gshape_json" description="Foundational: Add JSON documents with geometric shape data to an indexed field when you need to populate the index with polygon and point information" difficulty="intermediate" >}} > JSON.SET shape:1 $ '{"name": "Green Square", "geom": "POLYGON ((1 1, 1 3, 3 3, 3 1, 1 1))"}' OK > JSON.SET shape:2 $ '{"name": "Red Rectangle", "geom": "POLYGON ((2 2.5, 2 3.5, 3.5 3.5, 3.5 2.5, 2 2.5))"}' @@ -91,13 +81,6 @@ OK OK > JSON.SET shape:4 $ '{"name": "Purple Point", "geom": "POINT (2 2)"}' OK -{{< /clients-example >}} - -You can now run various geospatial queries against the index. For -example, the query below returns any shapes within the boundary -of the green square but omits the green square itself: - -{{< clients-example set="geoindex" step="gshape_query" description="GEOSHAPE query: Query geometric shapes using spatial operators like WITHIN to find shapes with specific geometric relationships when you need to test shape containment" difficulty="intermediate" try_it="false" >}} > FT.SEARCH geomidx "(-@name:(Green Square) @geom:[WITHIN $qshape])" PARAMS 2 qshape "POLYGON ((1 1, 1 3, 3 3, 3 1, 1 1))" RETURN 1 name DIALECT 2 1) (integer) 1 diff --git a/content/develop/data-types/geospatial.md b/content/develop/data-types/geospatial.md index 9cca4ba060..2463aa6642 100644 --- a/content/develop/data-types/geospatial.md +++ b/content/develop/data-types/geospatial.md @@ -38,7 +38,7 @@ available in Redis Search. Suppose you're building a mobile app that lets you find all of the bike rental stations closest to your current location. Add several locations to a geospatial index: -{{< clients-example set="geo_tutorial" step="geoadd" description="Foundational: Add geographic coordinates to a geospatial index using GEOADD to store location data" >}} +{{< clients-example set="geo_tutorial" step="geoadd" description="Foundational: Add geographic coordinates to a geospatial index using GEOADD to store location data" prereq="true" >}} > GEOADD bikes:rentable -122.27652 37.805186 station:1 (integer) 1 > GEOADD bikes:rentable -122.2674626 37.8062344 station:2 @@ -48,7 +48,7 @@ Add several locations to a geospatial index: {{< /clients-example >}} Find all locations within a 5 kilometer radius of a given location, and return the distance to each location: -{{< clients-example set="geo_tutorial" step="geosearch" description="Proximity search: Use GEOSEARCH to find locations within a radius or bounding box when you need to discover nearby points" difficulty="intermediate" buildsUpon="geoadd" try_it="false" >}} +{{< clients-example set="geo_tutorial" step="geosearch" description="Proximity search: Use GEOSEARCH to find locations within a radius or bounding box when you need to discover nearby points" difficulty="intermediate" buildsUpon="geoadd" needs_prereq="true" >}} > GEOSEARCH bikes:rentable FROMLONLAT -122.2612767 37.7936847 BYRADIUS 5 km WITHDIST 1) 1) "station:1" 2) "1.8523" diff --git a/content/develop/data-types/hashes.md b/content/develop/data-types/hashes.md index 3d01966ffc..7258d7467a 100644 --- a/content/develop/data-types/hashes.md +++ b/content/develop/data-types/hashes.md @@ -52,7 +52,12 @@ hashes in many different ways inside your application. The command [`HSET`]({{< relref "/commands/hset" >}}) sets multiple fields of the hash, while [`HGET`]({{< relref "/commands/hget" >}}) retrieves a single field. [`HMGET`]({{< relref "/commands/hmget" >}}) is similar to [`HGET`]({{< relref "/commands/hget" >}}) but returns an array of values: -{{< clients-example set="hash_tutorial" step="hmget" description="Retrieve multiple field values from a hash using HMGET when you need to reduce round trips to the server" buildsUpon="set_get_all" try_it="false" >}} +{{< clients-example set="hash_tutorial" step="hmget" description="Retrieve multiple field values from a hash using HMGET when you need to reduce round trips to the server" buildsUpon="set_get_all" >}} +# Recreate the bike:1 hash so this example runs on its own. +> DEL bike:1 +(integer) 1 +> HSET bike:1 model Deimos brand Ergonom type 'Enduro bikes' price 4972 +(integer) 4 > HMGET bike:1 model price no-such-field 1) "Deimos" 2) "4972" @@ -62,7 +67,12 @@ a single field. [`HMGET`]({{< relref "/commands/hmget" >}}) is similar to [`HGET There are commands that are able to perform operations on individual fields as well, like [`HINCRBY`]({{< relref "/commands/hincrby" >}}): -{{< clients-example set="hash_tutorial" step="hincrby" description="Increment hash field values for counters using HINCRBY (creates field if missing, initializes to 0)" buildsUpon="set_get_all" try_it="false" >}} +{{< clients-example set="hash_tutorial" step="hincrby" description="Increment hash field values for counters using HINCRBY (creates field if missing, initializes to 0)" buildsUpon="set_get_all" >}} +# Recreate the bike:1 hash so this example runs on its own. +> DEL bike:1 +(integer) 1 +> HSET bike:1 model Deimos brand Ergonom type 'Enduro bikes' price 4972 +(integer) 4 > HINCRBY bike:1 price 100 (integer) 5072 > HINCRBY bike:1 price -100 diff --git a/content/develop/data-types/sets.md b/content/develop/data-types/sets.md index 7f5e0b12f7..a16fb506f3 100644 --- a/content/develop/data-types/sets.md +++ b/content/develop/data-types/sets.md @@ -89,7 +89,12 @@ elements in any order at every call. Redis has commands to test for set membership. These commands can be used on single as well as multiple items: -{{< clients-example set="sets_tutorial" step="smismember" description="Batch membership checks: Test multiple items at once using SMISMEMBER when you need to reduce round trips to the server" difficulty="intermediate" buildsUpon="sismember" try_it="false" >}} +{{< clients-example set="sets_tutorial" step="smismember" description="Batch membership checks: Test multiple items at once using SMISMEMBER when you need to reduce round trips to the server" difficulty="intermediate" buildsUpon="sismember" >}} +# Recreate the France racing set so this example runs on its own. +> DEL bikes:racing:france +(integer) 1 +> SADD bikes:racing:france bike:1 bike:2 bike:3 +(integer) 3 > SISMEMBER bikes:racing:france bike:1 (integer) 1 > SMISMEMBER bikes:racing:france bike:2 bike:3 bike:4 diff --git a/content/develop/data-types/sorted-sets.md b/content/develop/data-types/sorted-sets.md index 51e3e67724..f8a0d88c90 100644 --- a/content/develop/data-types/sorted-sets.md +++ b/content/develop/data-types/sorted-sets.md @@ -151,7 +151,12 @@ position of an element in the set of ordered elements. The [`ZREVRANK`]({{< relref "/commands/zrevrank" >}}) command is also available in order to get the rank, considering the elements sorted in a descending way. -{{< clients-example set="ss_tutorial" step="zrank" description="Get member position: Use ZRANK and ZREVRANK to find a member's position in the sorted set (useful for leaderboards)" difficulty="intermediate" buildsUpon="zadd" try_it="false" >}} +{{< clients-example set="ss_tutorial" step="zrank" description="Get member position: Use ZRANK and ZREVRANK to find a member's position in the sorted set (useful for leaderboards)" difficulty="intermediate" buildsUpon="zadd" >}} +# Recreate the three remaining racers so this example runs on its own. +> DEL racer_scores +(integer) 1 +> ZADD racer_scores 10 "Norem" 10 "Royce" 14 "Prickett" +(integer) 3 > ZRANK racer_scores "Norem" (integer) 0 > ZREVRANK racer_scores "Norem" @@ -172,7 +177,7 @@ The main commands to operate with lexicographical ranges are [`ZRANGEBYLEX`]({{< For example, let's add again our list of famous racers, but this time using a score of zero for all the elements. We'll see that because of the sorted sets ordering rules, they are already sorted lexicographically. Using [`ZRANGEBYLEX`]({{< relref "/commands/zrangebylex" >}}) we can ask for lexicographical ranges: -{{< clients-example set="ss_tutorial" step="zadd_lex" description="Lexicographical queries: Add members with identical scores and use ZRANGEBYLEX to query by string range (enables generic indexing)" difficulty="intermediate" buildsUpon="zadd" try_it="false" >}} +{{< clients-example set="ss_tutorial" step="zadd_lex" description="Lexicographical queries: Add members with identical scores and use ZRANGEBYLEX to query by string range (enables generic indexing)" difficulty="intermediate" buildsUpon="zadd" >}} > ZADD racer_scores 0 "Norem" 0 "Sam-Bodden" 0 "Royce" 0 "Castilla" 0 "Prickett" 0 "Ford" (integer) 3 > ZRANGE racer_scores 0 -1 diff --git a/content/develop/data-types/strings/_index.md b/content/develop/data-types/strings/_index.md index b67f681ef3..b237efccd9 100644 --- a/content/develop/data-types/strings/_index.md +++ b/content/develop/data-types/strings/_index.md @@ -47,7 +47,10 @@ The [`SET`]({{< relref "/commands/set" >}}) command has interesting options that arguments. For example, I may ask [`SET`]({{< relref "/commands/set" >}}) to fail if the key already exists, or the opposite, that it only succeed if the key already exists: -{{< clients-example set="set_tutorial" step="setnx_xx" description="Conditional SET operations: Use NX and XX options to control key existence when you need atomic compare-and-set behavior" difficulty="intermediate" buildsUpon="set_get" try_it="false" >}} +{{< clients-example set="set_tutorial" step="setnx_xx" description="Conditional SET operations: Use NX and XX options to control key existence when you need atomic compare-and-set behavior" difficulty="intermediate" buildsUpon="set_get" >}} +# Recreate the bike:1 key so this example runs on its own. +> SET bike:1 Deimos +OK > SET bike:1 bike NX (nil) > SET bike:1 bike XX diff --git a/content/develop/data-types/timeseries/_index.md b/content/develop/data-types/timeseries/_index.md index 80c2c8c290..f8d34f9827 100644 --- a/content/develop/data-types/timeseries/_index.md +++ b/content/develop/data-types/timeseries/_index.md @@ -129,7 +129,14 @@ is an array containing the number of samples in each time series after the opera If you use the `*` character as the timestamp, Redis will record the current Unix time, as reported by the server's clock. -{{< clients-example set="time_series_tutorial" step="madd" description="Batch operations: Add multiple data points to one or more time series using TS.MADD when you need to reduce round trips to the server" difficulty="beginner" buildsUpon="create" try_it="false" >}} +{{< clients-example set="time_series_tutorial" step="madd" description="Batch operations: Add multiple data points to one or more time series using TS.MADD when you need to reduce round trips to the server" difficulty="beginner" buildsUpon="create" >}} +# Recreate the thermometer series so this example runs on its own. +> DEL thermometer:1 thermometer:2 +(integer) 2 +> TS.CREATE thermometer:1 +OK +> TS.ADD thermometer:2 1 10.8 +(integer) 1 > TS.MADD thermometer:1 1 9.2 thermometer:1 2 9.9 thermometer:2 2 10.3 1) (integer) 1 2) (integer) 2 @@ -141,7 +148,14 @@ Unix time, as reported by the server's clock. Use [`TS.GET`]({{< relref "commands/ts.get/" >}}) to retrieve the data point with the highest timestamp in a time series. This returns both the timestamp and the value. -{{< clients-example set="time_series_tutorial" step="get" description="Foundational: Use TS.GET to get the latest value and timestamp" difficulty="beginner" buildsUpon="madd" try_it="false" >}} +{{< clients-example set="time_series_tutorial" step="get" description="Foundational: Use TS.GET to get the latest value and timestamp" difficulty="beginner" buildsUpon="madd" >}} +# Recreate the thermometer:2 series so this example runs on its own. +> DEL thermometer:2 +(integer) 1 +> TS.ADD thermometer:2 1 10.8 +(integer) 1 +> TS.ADD thermometer:2 2 10.3 +(integer) 2 # The last recorded temperature for thermometer:2 # was 10.3 at time 2ms. > TS.GET thermometer:2 @@ -224,7 +238,18 @@ use this option). Specify a minimum and maximum value to include only samples within that range. The value range is inclusive and you can use the same value for the minimum and maximum to filter for a single value. -{{< clients-example set="time_series_tutorial" step="range_filter" description="Filtering results: Use FILTER_BY_TS and FILTER_BY_VALUE options with range queries when you need to select specific timestamps or value ranges" difficulty="intermediate" buildsUpon="range" try_it="false" >}} +{{< clients-example set="time_series_tutorial" step="range_filter" description="Filtering results: Use FILTER_BY_TS and FILTER_BY_VALUE options with range queries when you need to select specific timestamps or value ranges" difficulty="intermediate" buildsUpon="range" >}} +# Recreate the rg:1 series so this example runs on its own. +> DEL rg:1 +(integer) 1 +> TS.CREATE rg:1 +OK +> TS.MADD rg:1 0 18 rg:1 1 14 rg:1 2 22 rg:1 3 18 rg:1 4 24 +1) (integer) 0 +2) (integer) 1 +3) (integer) 2 +4) (integer) 3 +5) (integer) 4 > TS.RANGE rg:1 - + FILTER_BY_TS 0 2 4 1) 1) (integer) 0 2) 18 @@ -398,7 +423,18 @@ For example, the example below shows an aggregation with the `avg` aggregator ov five data points in the `rg:2` time series. The bucket size is 2ms, so there are three aggregated values with only one value used to calculate the average for the last bucket. -{{< clients-example set="time_series_tutorial" step="agg" description="Aggregation: Use AGGREGATION option with range queries to compute statistics such as avg, sum, min, and max over time buckets when you need to reduce large datasets" difficulty="intermediate" buildsUpon="madd" try_it="false" >}} +{{< clients-example set="time_series_tutorial" step="agg" description="Aggregation: Use AGGREGATION option with range queries to compute statistics such as avg, sum, min, and max over time buckets when you need to reduce large datasets" difficulty="intermediate" buildsUpon="madd" >}} +# Recreate the rg:2 series so this example runs on its own. +> DEL rg:2 +(integer) 1 +> TS.CREATE rg:2 +OK +> TS.MADD rg:2 0 1.8 rg:2 1 2.1 rg:2 2 2.3 rg:2 3 1.9 rg:2 4 1.78 +1) (integer) 0 +2) (integer) 1 +3) (integer) 2 +4) (integer) 3 +5) (integer) 4 > TS.RANGE rg:2 - + AGGREGATION avg 2 1) 1) (integer) 0 2) 1.9500000000000002 @@ -457,7 +493,20 @@ Bucket(25ms): |_________________________||_________________________||___________ You can also align the buckets to the start or end of the query range. For example, the following command aligns the buckets to the start of the query range at time 10. -{{< clients-example set="time_series_tutorial" step="agg_align" description="Custom alignment: Use ALIGN option with aggregations to align buckets to query range start/end when you need aggregations relative to specific time boundaries" difficulty="advanced" buildsUpon="agg_bucket" try_it="false" >}} +{{< clients-example set="time_series_tutorial" step="agg_align" description="Custom alignment: Use ALIGN option with aggregations to align buckets to query range start/end when you need aggregations relative to specific time boundaries" difficulty="advanced" buildsUpon="agg_bucket" >}} +# Recreate the sensor3 series so this example runs on its own. +> DEL sensor3 +(integer) 1 +> TS.CREATE sensor3 +OK +> TS.MADD sensor3 10 1000 sensor3 20 2000 sensor3 30 3000 sensor3 40 4000 sensor3 50 5000 sensor3 60 6000 sensor3 70 7000 +1) (integer) 10 +2) (integer) 20 +3) (integer) 30 +4) (integer) 40 +5) (integer) 50 +6) (integer) 60 +7) (integer) 70 > TS.RANGE sensor3 10 70 AGGREGATION min 25 ALIGN start 1) 1) (integer) 10 2) 1000 @@ -671,7 +720,16 @@ produce any data in the compacted series. However, when you add data for time 4 (in the second bucket), the compaction rule computes the minimum value for the first bucket and adds it to the compacted series. -{{< clients-example set="time_series_tutorial" step="comp_add" description="Compaction behavior: Understand how compaction rules process data incrementally, computing aggregates for completed buckets when new data arrives" difficulty="intermediate" buildsUpon="create_compaction" try_it="false" >}} +{{< clients-example set="time_series_tutorial" step="comp_add" description="Compaction behavior: Understand how compaction rules process data incrementally, computing aggregates for completed buckets when new data arrives" difficulty="intermediate" buildsUpon="create_compaction" >}} +# Recreate the compaction rule so this example runs on its own. +> DEL hyg:1 hyg:compacted +(integer) 2 +> TS.CREATE hyg:1 +OK +> TS.CREATE hyg:compacted +OK +> TS.CREATERULE hyg:1 hyg:compacted AGGREGATION min 3 +OK > TS.MADD hyg:1 0 75 hyg:1 1 77 hyg:1 2 78 1) (integer) 0 2) (integer) 1 @@ -702,7 +760,14 @@ that fall within a given timestamp range. The range is inclusive, meaning that samples whose timestamp equals the start or end of the range are deleted. If you want to delete a single timestamp, use it as both the start and end of the range. -{{< clients-example set="time_series_tutorial" step="del" description="Deleting data: Use TS.DEL to remove data points within a timestamp range when you need to clean up or correct historical data" difficulty="beginner" buildsUpon="create" try_it="false" >}} +{{< clients-example set="time_series_tutorial" step="del" description="Deleting data: Use TS.DEL to remove data points within a timestamp range when you need to clean up or correct historical data" difficulty="beginner" buildsUpon="create" >}} +# Recreate the thermometer:1 series so this example runs on its own. +> DEL thermometer:1 +(integer) 1 +> TS.ADD thermometer:1 1 9.2 +(integer) 1 +> TS.ADD thermometer:1 2 9.9 +(integer) 2 > TS.INFO thermometer:1 1) totalSamples 2) (integer) 2 diff --git a/content/develop/get-started/document-database.md b/content/develop/get-started/document-database.md index 4124f77bc7..9abdb219e1 100644 --- a/content/develop/get-started/document-database.md +++ b/content/develop/get-started/document-database.md @@ -65,25 +65,22 @@ You can copy and paste the connection details from the Redis Cloud database conf {{% /alert %}} -## Create an index +## Create an index and add documents As explained in the [in-memory data store]({{< relref "/develop/get-started/data-store" >}}) quick start guide, Redis allows you to access an item directly via its key. You also learned how to scan the keyspace. Whereby you can use other data structures (e.g., hashes and sorted sets) as secondary indexes, your application would need to maintain those indexes manually. Redis is a document database that allows you to declare which fields are auto-indexed. Redis currently supports secondary index creation on the [hashes]({{< relref "/develop/data-types/hashes" >}}) and [JSON]({{< relref "/develop/data-types/json" >}}) documents. -The following example shows an [FT.CREATE]({{< relref "commands/ft.create" >}}) command that creates an index with some text fields, a numeric field (price), and a tag field (condition). The text fields have a weight of 1.0, meaning they have the same relevancy in the context of full-text searches. The field names follow the [JSONPath]({{< relref "/develop/data-types/json/path" >}}) notion. Each such index field maps to a property within the JSON document. +The following example uses an [FT.CREATE]({{< relref "commands/ft.create" >}}) command to create an index with some text fields, a numeric field (price), and a tag field (condition). The text fields have a weight of 1.0, meaning they have the same relevancy in the context of full-text searches. The field names follow the [JSONPath]({{< relref "/develop/data-types/json/path" >}}) notion. Each such index field maps to a property within the JSON document. The example then uses the [JSON.SET]({{< relref "commands/json.set" >}}) command to add the JSON documents that the index tracks. +Any pre-existing JSON documents with a key prefix `bicycle:` are automatically added to the index. Additionally, any JSON documents with that prefix created or modified after index creation are added or re-added to the index. -{{< clients-example set="search_quickstart" step="create_index" description="Foundational: Create an index on JSON documents using FT.CREATE with text, numeric, and tag fields" difficulty="beginner" >}} + +{{< clients-example set="search_quickstart" step="create_index" description="Foundational: Create an index on JSON documents with FT.CREATE, then add the documents with JSON.SET" difficulty="beginner" max_lines="2" prereq="true" try_it="false" >}} > FT.CREATE idx:bicycle ON JSON PREFIX 1 bicycle: SCORE 1.0 SCHEMA $.brand AS brand TEXT WEIGHT 1.0 $.model AS model TEXT WEIGHT 1.0 $.description AS description TEXT WEIGHT 1.0 $.price AS price NUMERIC $.condition AS condition TAG SEPARATOR , OK -{{< /clients-example >}} - -Any pre-existing JSON documents with a key prefix `bicycle:` are automatically added to the index. Additionally, any JSON documents with that prefix created or modified after index creation are added or re-added to the index. - -## Add JSON documents - -The example below shows you how to use the [JSON.SET]({{< relref "commands/json.set" >}}) command to create new JSON documents: - -{{< clients-example set="search_quickstart" step="add_documents" description="Foundational: Add JSON documents to Redis using JSON.SET" difficulty="beginner" max_lines="2" try_it="false" >}} > JSON.SET "bicycle:0" "." "{\"brand\": \"Velorim\", \"model\": \"Jigger\", \"price\": 270, \"description\": \"Small and powerful, the Jigger is the best ride for the smallest of tikes! This is the tiniest kids\\u2019 pedal bike on the market available without a coaster brake, the Jigger is the vehicle of choice for the rare tenacious little rider raring to go.\", \"condition\": \"new\"}" OK > JSON.SET "bicycle:1" "." "{\"brand\": \"Bicyk\", \"model\": \"Hillcraft\", \"price\": 1200, \"description\": \"Kids want to ride with as little weight as possible. Especially on an incline! They may be at the age when a 27.5\\\" wheel bike is just too clumsy coming off a 24\\\" bike. The Hillcraft 26 is just the solution they need!\", \"condition\": \"used\"}" @@ -96,9 +93,9 @@ OK OK > JSON.SET "bicycle:5" "." "{\"brand\": \"Breakout\", \"model\": \"XBN 2.1 Alloy\", \"price\": 810, \"description\": \"The XBN 2.1 Alloy is our entry-level road bike \\u2013 but that\\u2019s not to say that it\\u2019s a basic machine. With an internal weld aluminium frame, a full carbon fork, and the slick-shifting Claris gears from Shimano\\u2019s, this is a bike which doesn\\u2019t break the bank and delivers craved performance.\", \"condition\": \"new\"}" OK -> JSON.SET "bicycle:6" "." "{\"brand\": \"ScramBikes\", \"model\": \"WattBike\", \"price\": 2300, \"description\": \"The WattBike is the best e-bike for people who still feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one charge. It\\u2019s great for tackling hilly terrain or if you just fancy a more leisurely ride. With three working modes, you can choose between E-bike, assisted bicycle, and normal bike modes.\", \"condition\": \"new\"}" +> JSON.SET "bicycle:6" "." "{\"brand\": \"ScramBikes\", \"model\": \"WattBike\", \"price\": 2300, \"description\": \"An e-bike with a 1000W mid-drive and a 48V battery, offering over 60 miles per charge and three riding modes.\", \"condition\": \"new\"}" OK -> JSON.SET "bicycle:7" "." "{\"brand\": \"Peaknetic\", \"model\": \"Secto\", \"price\": 430, \"description\": \"If you struggle with stiff fingers or a kinked neck or back after a few minutes on the road, this lightweight, aluminum bike alleviates those issues and allows you to enjoy the ride. From the ergonomic grips to the lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. The rear-inclined seat tube facilitates stability by allowing you to put a foot on the ground to balance at a stop, and the low step-over frame makes it accessible for all ability and mobility levels. The saddle is very soft, with a wide back to support your hip joints and a cutout in the center to redistribute that pressure. Rim brakes deliver satisfactory braking control, and the wide tires provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts facilitate setting up the Roll Low-Entry as your preferred commuter, and the BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.\", \"condition\": \"new\"}" +> JSON.SET "bicycle:7" "." "{\"brand\": \"Peaknetic\", \"model\": \"Secto\", \"price\": 430, \"description\": \"A lightweight aluminum commuter bike with ergonomic grips, a lumbar-supporting seat, and a low step-over frame for comfort and easy mounting.\", \"condition\": \"new\"}" OK > JSON.SET "bicycle:8" "." "{\"brand\": \"nHill\", \"model\": \"Summit\", \"price\": 1200, \"description\": \"This budget mountain bike from nHill performs well both on bike paths and on the trail. The fork with 100mm of travel absorbs rough terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. The Shimano Tourney drivetrain offered enough gears for finding a comfortable pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. Whether you want an affordable bike that you can take to work, but also take trail in mountains on the weekends or you\\u2019re just after a stable, comfortable ride for the bike path, the Summit gives a good value for money.\", \"condition\": \"new\"}" OK @@ -132,10 +129,10 @@ You can retrieve all indexed documents using the [FT.SEARCH]({{< relref "command 2) "{\"brand\":\"Velorim\",\"model\":\"Jigger\",\"price\":270,\"description\":\"Small and powerful, the Jigger is the best ride for the smallest of tikes! This is the tiniest kids\xe2\x80\x99 pedal bike on the market available without a coaster brake, the Jigger is the vehicle of choice for the rare tenacious little rider raring to go.\",\"condition\":\"new\"}" 12) "bicycle:6" 13) 1) "$" - 2) "{\"brand\":\"ScramBikes\",\"model\":\"WattBike\",\"price\":2300,\"description\":\"The WattBike is the best e-bike for people who still feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one charge. It\xe2\x80\x99s great for tackling hilly terrain or if you just fancy a more leisurely ride. With three working modes, you can choose between E-bike, assisted bicycle, and normal bike modes.\",\"condition\":\"new\"}" + 2) "{\"brand\":\"ScramBikes\",\"model\":\"WattBike\",\"price\":2300,\"description\":\"An e-bike with a 1000W mid-drive and a 48V battery, offering over 60 miles per charge and three riding modes.\",\"condition\":\"new\"}" 14) "bicycle:7" 15) 1) "$" - 2) "{\"brand\":\"Peaknetic\",\"model\":\"Secto\",\"price\":430,\"description\":\"If you struggle with stiff fingers or a kinked neck or back after a few minutes on the road, this lightweight, aluminum bike alleviates those issues and allows you to enjoy the ride. From the ergonomic grips to the lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. The rear-inclined seat tube facilitates stability by allowing you to put a foot on the ground to balance at a stop, and the low step-over frame makes it accessible for all ability and mobility levels. The saddle is very soft, with a wide back to support your hip joints and a cutout in the center to redistribute that pressure. Rim brakes deliver satisfactory braking control, and the wide tires provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts facilitate setting up the Roll Low-Entry as your preferred commuter, and the BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.\",\"condition\":\"new\"}" + 2) "{\"brand\":\"Peaknetic\",\"model\":\"Secto\",\"price\":430,\"description\":\"A lightweight aluminum commuter bike with ergonomic grips, a lumbar-supporting seat, and a low step-over frame for comfort and easy mounting.\",\"condition\":\"new\"}" 16) "bicycle:9" 17) 1) "$" 2) "{\"model\":\"ThrillCycle\",\"brand\":\"BikeShind\",\"price\":815,\"description\":\"An artsy, retro-inspired bicycle that\xe2\x80\x99s as functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. A 9-speed drivetrain has enough gears for coasting in the city, but we wouldn\xe2\x80\x99t suggest taking it to the mountains. Fenders protect you from mud, and a rear basket lets you transport groceries, flowers and books. The ThrillCycle comes with a limited lifetime warranty, so this little guy will last you long past graduation.\",\"condition\":\"refurbished\"}" @@ -151,7 +148,7 @@ You can retrieve all indexed documents using the [FT.SEARCH]({{< relref "command The following command shows a simple single-term query for finding all bicycles with a specific model: -{{< clients-example set="search_quickstart" step="query_single_term" description="Foundational: Perform a single-term full-text query using FT.SEARCH to find documents matching a specific field value" difficulty="beginner" try_it="false" >}} +{{< clients-example set="search_quickstart" step="query_single_term" description="Foundational: Perform a single-term full-text query using FT.SEARCH to find documents matching a specific field value" difficulty="beginner" needs_prereq="true" try_it="false" >}} > FT.SEARCH "idx:bicycle" "@model:Jigger" LIMIT 0 10 1) (integer) 1 2) "bicycle:0" @@ -163,7 +160,7 @@ The following command shows a simple single-term query for finding all bicycles Below is a command to perform an exact match query that finds all bicycles with the brand name `Noka Bikes`. You must use double quotes around the search term when constructing an exact match query on a text field. -{{< clients-example set="search_quickstart" step="query_exact_matching" description="Foundational: Perform an exact match query using FT.SEARCH with double quotes to find documents with precise field values" difficulty="beginner" try_it="false" >}} +{{< clients-example set="search_quickstart" step="query_exact_matching" description="Foundational: Perform an exact match query using FT.SEARCH with double quotes to find documents with precise field values" difficulty="beginner" needs_prereq="true" try_it="false" >}} > FT.SEARCH "idx:bicycle" "@brand:\"Noka Bikes\"" LIMIT 0 10 1) (integer) 1 2) "bicycle:4" diff --git a/local_examples/client-specific/lettuce-async/SortedSetExample.java b/local_examples/client-specific/lettuce-async/SortedSetExample.java new file mode 100644 index 0000000000..15ea3cd50c --- /dev/null +++ b/local_examples/client-specific/lettuce-async/SortedSetExample.java @@ -0,0 +1,244 @@ +// EXAMPLE: ss_tutorial +package io.redis.examples.async; + +import io.lettuce.core.*; +import io.lettuce.core.api.async.RedisAsyncCommands; +import io.lettuce.core.api.StatefulRedisConnection; + +// REMOVE_START +import org.junit.jupiter.api.Test; +// REMOVE_END +import java.util.*; +import java.util.concurrent.CompletableFuture; +// REMOVE_START +import static org.assertj.core.api.Assertions.assertThat; +// REMOVE_END + +public class SortedSetExample { + + @Test + public void run() { + RedisClient redisClient = RedisClient.create("redis://localhost:6379"); + + try (StatefulRedisConnection connection = redisClient.connect()) { + RedisAsyncCommands asyncCommands = connection.async(); + // REMOVE_START + asyncCommands.del("racer_scores").toCompletableFuture().join(); + // REMOVE_END + + // STEP_START zadd + CompletableFuture zadd = asyncCommands.zadd("racer_scores", ScoredValue.just(10d, "Norem")) + .thenCompose(res1 -> { + System.out.println(res1); // >>> 1 + // REMOVE_START + assertThat(res1).isEqualTo(1); + // REMOVE_END + + return asyncCommands.zadd("racer_scores", ScoredValue.just(12d, "Castilla")); + }).thenCompose(res2 -> { + System.out.println(res2); // >>> 1 + // REMOVE_START + assertThat(res2).isEqualTo(1); + // REMOVE_END + + return asyncCommands.zadd("racer_scores", ScoredValue.just(8d, "Sam-Bodden"), + ScoredValue.just(10d, "Royce"), ScoredValue.just(6d, "Ford"), + ScoredValue.just(14d, "Prickett")); + }) + // REMOVE_START + .thenApply(res -> { + assertThat(res).isEqualTo(4); + return res; + }) + // REMOVE_END + .thenAccept(System.out::println) // >>> 4 + .toCompletableFuture(); + // STEP_END + zadd.join(); + + // STEP_START zrange + CompletableFuture zrange = asyncCommands.zrange("racer_scores", 0, -1).thenCompose(res3 -> { + System.out.println(res3); + // >>> [Ford, Sam-Bodden, Norem, Royce, Castilla, Prickett] + // REMOVE_START + assertThat(res3.toString()).isEqualTo("[Ford, Sam-Bodden, Norem, Royce, Castilla, Prickett]"); + // REMOVE_END + + return asyncCommands.zrevrange("racer_scores", 0, -1); + }) + // REMOVE_START + .thenApply((List res) -> { + assertThat(res.toString()).isEqualTo("[Prickett, Castilla, Royce, Norem, Sam-Bodden, Ford]"); + return res; + }) + // REMOVE_END + .thenAccept(System.out::println) + // >>> [Prickett, Castilla, Royce, Norem, Sam-Bodden, Ford] + .toCompletableFuture(); + // STEP_END + zrange.join(); + + // STEP_START zrange_withscores + CompletableFuture zrangeWithScores = asyncCommands.zrangeWithScores("racer_scores", 0, -1) + // REMOVE_START + .thenApply((List> res) -> { + assertThat(res.toString()).isEqualTo("[ScoredValue[6.000000, Ford], ScoredValue[8.000000, Sam-Bodden]," + + " ScoredValue[10.000000, Norem], ScoredValue[10.000000, Royce]," + + " ScoredValue[12.000000, Castilla], ScoredValue[14.000000, Prickett]]"); + return res; + }) + // REMOVE_END + .thenAccept(System.out::println) + // >>> [ScoredValue[6.000000, Ford], ScoredValue[8.000000, Sam-Bodden]... + .toCompletableFuture(); + // STEP_END + zrangeWithScores.join(); + + // STEP_START zrangebyscore + CompletableFuture zrangebyscore = asyncCommands + .zrangebyscore("racer_scores", Range.create(Double.NEGATIVE_INFINITY, 10)) + // REMOVE_START + .thenApply(res -> { + assertThat(res.toString()).isEqualTo("[Ford, Sam-Bodden, Norem, Royce]"); + return res; + }) + // REMOVE_END + .thenAccept(System.out::println) + // >>> [Ford, Sam-Bodden, Norem, Royce] + .toCompletableFuture(); + // STEP_END + zrangebyscore.join(); + + // STEP_START zremrangebyscore + CompletableFuture zremrangebyscore = asyncCommands.zrem("racer_scores", "Castilla").thenCompose(res4 -> { + System.out.println(res4); // >>> 1 + // REMOVE_START + assertThat(res4).isEqualTo(1); + // REMOVE_END + + return asyncCommands.zremrangebyscore("racer_scores", Range.create(Double.NEGATIVE_INFINITY, 9)); + }).thenCompose(res5 -> { + System.out.println(res5); // >>> 2 + // REMOVE_START + assertThat(res5).isEqualTo(2); + // REMOVE_END + + return asyncCommands.zrange("racer_scores", 0, -1); + }) + // REMOVE_START + .thenApply(res -> { + assertThat(res.toString()).isEqualTo("[Norem, Royce, Prickett]"); + return res; + }) + // REMOVE_END + .thenAccept(System.out::println) + // >>> [Norem, Royce, Prickett] + .toCompletableFuture(); + // STEP_END + zremrangebyscore.join(); + + // STEP_START zrank + // Recreate the three remaining racers so this example runs on its own. + CompletableFuture zrank = asyncCommands.del("racer_scores") + .thenCompose(delRes -> asyncCommands.zadd("racer_scores", ScoredValue.just(10d, "Norem"), + ScoredValue.just(10d, "Royce"), ScoredValue.just(14d, "Prickett"))) + .thenCompose(addRes -> asyncCommands.zrank("racer_scores", "Norem")) + .thenCompose(res6 -> { + System.out.println(res6); // >>> 0 + // REMOVE_START + assertThat(res6).isZero(); + // REMOVE_END + + return asyncCommands.zrevrank("racer_scores", "Norem"); + }) + // REMOVE_START + .thenApply(res -> { + assertThat(res).isEqualTo(2); + return res; + }) + // REMOVE_END + .thenAccept(System.out::println) // >>> 2 + .toCompletableFuture(); + // STEP_END + zrank.join(); + + // STEP_START zadd_lex + CompletableFuture zaddLex = asyncCommands.zadd("racer_scores", ScoredValue.just(0d, "Norem"), + ScoredValue.just(0d, "Sam-Bodden"), ScoredValue.just(0d, "Royce"), ScoredValue.just(0d, "Castilla"), + ScoredValue.just(0d, "Prickett"), ScoredValue.just(0d, "Ford")).thenCompose(res7 -> { + System.out.println(res7); // >>> 3 + // REMOVE_START + assertThat(res7).isEqualTo(3); + // REMOVE_END + + return asyncCommands.zrange("racer_scores", 0, -1); + }).thenCompose(res8 -> { + System.out.println(res8); + // >>> [Castilla, Ford, Norem, Prickett, Royce, Sam-Bodden] + // REMOVE_START + assertThat(res8.toString()).isEqualTo("[Castilla, Ford, Norem, Prickett, Royce, Sam-Bodden]"); + // REMOVE_END + + return asyncCommands.zrangebylex("racer_scores", Range.create("A", "L")); + }) + // REMOVE_START + .thenApply(res -> { + assertThat(res.toString()).isEqualTo("[Castilla, Ford]"); + return res; + }) + // REMOVE_END + .thenAccept(System.out::println) + // >>> [Castilla, Ford] + .toCompletableFuture(); + // STEP_END + zaddLex.join(); + + // STEP_START leaderboard + CompletableFuture leaderboard = asyncCommands.zadd("racer_scores", ScoredValue.just(100, "Wood")) + .thenCompose(res9 -> { + System.out.println(res9); // >>> 1 + // REMOVE_START + assertThat(res9).isEqualTo(1); + // REMOVE_END + + return asyncCommands.zadd("racer_scores", ScoredValue.just(100, "Henshaw")); + }).thenCompose(res10 -> { + System.out.println(res10); // >>> 1 + // REMOVE_START + assertThat(res10).isEqualTo(1); + // REMOVE_END + + return asyncCommands.zadd("racer_scores", ScoredValue.just(150, "Henshaw")); + }).thenCompose(res11 -> { + System.out.println(res11); // >>> 0 + // REMOVE_START + assertThat(res11).isZero(); + // REMOVE_END + + return asyncCommands.zincrby("racer_scores", 50, "Wood"); + }).thenCompose(res12 -> { + System.out.println(res12); // >>> 150 + // REMOVE_START + assertThat(res12).isEqualTo(150); + // REMOVE_END + + return asyncCommands.zincrby("racer_scores", 50, "Henshaw"); + }) + // REMOVE_START + .thenApply(res -> { + assertThat(res).isEqualTo(200); + return res; + }) + // REMOVE_END + .thenAccept(System.out::println) // >>> 200 + .toCompletableFuture(); + // STEP_END + leaderboard.join(); + // HIDE_START + } finally { + redisClient.shutdown(); + } + } + +} +// HIDE_END diff --git a/local_examples/client-specific/lettuce-async/StringExample.java b/local_examples/client-specific/lettuce-async/StringExample.java new file mode 100644 index 0000000000..767848868f --- /dev/null +++ b/local_examples/client-specific/lettuce-async/StringExample.java @@ -0,0 +1,139 @@ +// EXAMPLE: set_tutorial +package io.redis.examples.async; + +import io.lettuce.core.*; +import io.lettuce.core.api.async.RedisAsyncCommands; +import io.lettuce.core.api.StatefulRedisConnection; + +// REMOVE_START +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +// REMOVE_END + +import java.util.*; +import java.util.concurrent.CompletableFuture; + +public class StringExample { + + // REMOVE_START + @Test + // REMOVE_END + public void run() { + RedisClient redisClient = RedisClient.create("redis://localhost:6379"); + + try (StatefulRedisConnection connection = redisClient.connect()) { + RedisAsyncCommands asyncCommands = connection.async(); + + // STEP_START set_get + CompletableFuture setAndGet = asyncCommands.set("bike:1", "Deimos").thenCompose(v -> { + System.out.println(v); // >>> OK + // REMOVE_START + assertThat(v).isEqualTo("OK"); + // REMOVE_END + return asyncCommands.get("bike:1"); + }) + // REMOVE_START + .thenApply(res -> { + assertThat(res).isEqualTo("Deimos"); + return res; + }) + // REMOVE_END + .thenAccept(System.out::println) // >>> Deimos + .toCompletableFuture(); + // STEP_END + // HIDE_START + setAndGet.join(); + // HIDE_END + + // STEP_START setnx_xx + // Recreate the bike:1 key so this example runs on its own. + CompletableFuture setnx = asyncCommands.set("bike:1", "Deimos") + .thenCompose(setup -> asyncCommands.setnx("bike:1", "bike")).thenCompose(v -> { + System.out.println(v); // >>> false (because key already exists) + // REMOVE_START + assertThat(v).isFalse(); + // REMOVE_END + return asyncCommands.get("bike:1"); + }) + // REMOVE_START + .thenApply(res -> { + assertThat(res).isEqualTo("Deimos"); + return res; + }) + // REMOVE_END + .thenAccept(System.out::println) // >>> Deimos (value is unchanged) + .toCompletableFuture(); + // HIDE_START + setnx.join(); + // HIDE_END + + // set the value to "bike" if it already exists + CompletableFuture setxx = asyncCommands.set("bike:1", "bike", SetArgs.Builder.xx()) + // REMOVE_START + .thenApply(res -> { + assertThat(res).isEqualTo("OK"); + return res; + }) + // REMOVE_END + .thenAccept(System.out::println) // >>> OK + .toCompletableFuture(); + // HIDE_START + setxx.join(); + // HIDE_END + // STEP_END + + // STEP_START mset + Map bikeMap = new HashMap<>(); + bikeMap.put("bike:1", "Deimos"); + bikeMap.put("bike:2", "Ares"); + bikeMap.put("bike:3", "Vanth"); + + CompletableFuture mset = asyncCommands.mset(bikeMap).thenCompose(v -> { + System.out.println(v); // >>> OK + return asyncCommands.mget("bike:1", "bike:2", "bike:3"); + }) + // REMOVE_START + .thenApply(res -> { + List> expected = new ArrayList<>( + Arrays.asList(KeyValue.just("bike:1", "Deimos"), KeyValue.just("bike:2", "Ares"), + KeyValue.just("bike:3", "Vanth"))); + assertThat(res).isEqualTo(expected); + return res; + }) + // REMOVE_END + .thenAccept(System.out::println) + // >>> [KeyValue[bike:1, Deimos], KeyValue[bike:2, Ares], KeyValue[bike:3, + // Vanth]] + .toCompletableFuture(); + // STEP_END + // HIDE_START + mset.join(); + // HIDE_END + + // STEP_START incr + CompletableFuture incrby = asyncCommands.set("total_crashes", "0") + .thenCompose(v -> asyncCommands.incr("total_crashes")).thenCompose(v -> { + System.out.println(v); // >>> 1 + // REMOVE_START + assertThat(v).isEqualTo(1L); + // REMOVE_END + return asyncCommands.incrby("total_crashes", 10); + }) + // REMOVE_START + .thenApply(res -> { + assertThat(res).isEqualTo(11L); + return res; + }) + // REMOVE_END + .thenAccept(System.out::println) // >>> 11 + .toCompletableFuture(); + // STEP_END + // HIDE_START + incrby.join(); + // HIDE_END + } finally { + redisClient.shutdown(); + } + } + +} diff --git a/local_examples/client-specific/lettuce-reactive/SortedSetExample.java b/local_examples/client-specific/lettuce-reactive/SortedSetExample.java new file mode 100644 index 0000000000..8439afb2fc --- /dev/null +++ b/local_examples/client-specific/lettuce-reactive/SortedSetExample.java @@ -0,0 +1,200 @@ +// EXAMPLE: ss_tutorial +package io.redis.examples.reactive; + +import io.lettuce.core.*; +import io.lettuce.core.api.reactive.RedisReactiveCommands; +import io.lettuce.core.api.StatefulRedisConnection; +// REMOVE_START +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +// REMOVE_END + +import reactor.core.publisher.Mono; + +import java.util.*; + +public class SortedSetExample { + + // REMOVE_START + @Test + // REMOVE_END + public void run() { + RedisClient redisClient = RedisClient.create("redis://localhost:6379"); + + try (StatefulRedisConnection connection = redisClient.connect()) { + RedisReactiveCommands reactiveCommands = connection.reactive(); + // REMOVE_START + reactiveCommands.del("racer_scores").block(); + // REMOVE_END + + // STEP_START zadd + Mono zadd = reactiveCommands.zadd("racer_scores", ScoredValue.just(10d, "Norem")).doOnNext(result -> { + System.out.println(result); // >>> 1 + // REMOVE_START + assertThat(result).isEqualTo(1); + // REMOVE_END + }).flatMap(v -> reactiveCommands.zadd("racer_scores", ScoredValue.just(12d, "Castilla"))).doOnNext(result -> { + System.out.println(result); // >>> 1 + // REMOVE_START + assertThat(result).isEqualTo(1); + // REMOVE_END + }).flatMap(v -> reactiveCommands.zadd("racer_scores", ScoredValue.just(8d, "Sam-Bodden"), + ScoredValue.just(10d, "Royce"), ScoredValue.just(6d, "Ford"), + ScoredValue.just(14d, "Prickett"))).doOnNext(result -> { + System.out.println(result); // >>> 4 + // REMOVE_START + assertThat(result).isEqualTo(4); + // REMOVE_END + }).then(); + // STEP_END + zadd.block(); + + // STEP_START zrange + Mono zrange = reactiveCommands.zrange("racer_scores", 0, -1).collectList().doOnNext(result -> { + System.out.println(result); + // >>> [Ford, Sam-Bodden, Norem, Royce, Castilla, Prickett] + // REMOVE_START + assertThat(result.toString()).isEqualTo("[Ford, Sam-Bodden, Norem, Royce, Castilla, Prickett]"); + // REMOVE_END + }).flatMap(v -> reactiveCommands.zrevrange("racer_scores", 0, -1).collectList()).doOnNext(result -> { + System.out.println(result); + // >>> [Prickett, Castilla, Royce, Norem, Sam-Bodden, Ford] + // REMOVE_START + assertThat(result.toString()).isEqualTo("[Prickett, Castilla, Royce, Norem, Sam-Bodden, Ford]"); + // REMOVE_END + }).then(); + // STEP_END + zrange.block(); + + // STEP_START zrange_withscores + Mono zrangeWithScores = reactiveCommands.zrangeWithScores("racer_scores", 0, -1).collectList() + .doOnNext(result -> { + System.out.println(result); + // >>> [ScoredValue[6.000000, Ford], ScoredValue[8.000000, Sam-Bodden]... + // REMOVE_START + assertThat(result.toString()) + .isEqualTo("[ScoredValue[6.000000, Ford], ScoredValue[8.000000, Sam-Bodden]," + + " ScoredValue[10.000000, Norem], ScoredValue[10.000000, Royce]," + + " ScoredValue[12.000000, Castilla], ScoredValue[14.000000, Prickett]]"); + // REMOVE_END + }).then(); + // STEP_END + zrangeWithScores.block(); + + // STEP_START zrangebyscore + Mono zrangebyscore = reactiveCommands + .zrangebyscore("racer_scores", Range.create(Double.NEGATIVE_INFINITY, 10)).collectList().doOnNext(result -> { + System.out.println(result); // >>> [Ford, Sam-Bodden, Norem, Royce] + // REMOVE_START + assertThat(result.toString()).isEqualTo("[Ford, Sam-Bodden, Norem, Royce]"); + // REMOVE_END + }).then(); + // STEP_END + zrangebyscore.block(); + + // STEP_START zremrangebyscore + Mono zremrangebyscore = reactiveCommands.zrem("racer_scores", "Castilla").doOnNext(result -> { + System.out.println(result); // >>> 1 + // REMOVE_START + assertThat(result).isEqualTo(1); + // REMOVE_END + }).flatMap(v -> reactiveCommands.zremrangebyscore("racer_scores", Range.create(Double.NEGATIVE_INFINITY, 9))) + .doOnNext(result -> { + System.out.println(result); // >>> 2 + // REMOVE_START + assertThat(result).isEqualTo(2); + // REMOVE_END + }).flatMap(v -> reactiveCommands.zrange("racer_scores", 0, -1).collectList()).doOnNext(result -> { + System.out.println(result); // >>> [Norem, Royce, Prickett] + // REMOVE_START + assertThat(result.toString()).isEqualTo("[Norem, Royce, Prickett]"); + // REMOVE_END + }).then(); + // STEP_END + zremrangebyscore.block(); + + // STEP_START zrank + // Recreate the three remaining racers so this example runs on its own. + Mono zrank = reactiveCommands.del("racer_scores") + .flatMap(v -> reactiveCommands.zadd("racer_scores", ScoredValue.just(10d, "Norem"), + ScoredValue.just(10d, "Royce"), ScoredValue.just(14d, "Prickett"))) + .flatMap(v -> reactiveCommands.zrank("racer_scores", "Norem")).doOnNext(result -> { + System.out.println(result); // >>> 0 + // REMOVE_START + assertThat(result).isZero(); + // REMOVE_END + }).flatMap(v -> reactiveCommands.zrevrank("racer_scores", "Norem")).doOnNext(result -> { + System.out.println(result); // >>> 2 + // REMOVE_START + assertThat(result).isEqualTo(2); + // REMOVE_END + }).then(); + // STEP_END + zrank.block(); + + // STEP_START zadd_lex + Mono zaddLex = reactiveCommands + .zadd("racer_scores", ScoredValue.just(0d, "Norem"), ScoredValue.just(0d, "Sam-Bodden"), + ScoredValue.just(0d, "Royce"), ScoredValue.just(0d, "Castilla"), + ScoredValue.just(0d, "Prickett"), ScoredValue.just(0d, "Ford")) + .doOnNext(result -> { + System.out.println(result); // >>> 3 + // REMOVE_START + assertThat(result).isEqualTo(3); + // REMOVE_END + }).flatMap(v -> reactiveCommands.zrange("racer_scores", 0, -1).collectList()).doOnNext(result -> { + System.out.println(result); + // >>> [Castilla, Ford, Norem, Prickett, Royce, Sam-Bodden] + // REMOVE_START + assertThat(result.toString()).isEqualTo("[Castilla, Ford, Norem, Prickett, Royce, Sam-Bodden]"); + // REMOVE_END + }).flatMap(v -> reactiveCommands.zrangebylex("racer_scores", Range.create("A", "L")).collectList()) + .doOnNext(result -> { + System.out.println(result); // >>> [Castilla, Ford] + // REMOVE_START + assertThat(result.toString()).isEqualTo("[Castilla, Ford]"); + // REMOVE_END + }).then(); + // STEP_END + zaddLex.block(); + + // STEP_START leaderboard + Mono leaderboard = reactiveCommands.zadd("racer_scores", ScoredValue.just(100, "Wood")) + .doOnNext(result -> { + System.out.println(result); // >>> 1 + // REMOVE_START + assertThat(result).isEqualTo(1); + // REMOVE_END + }).flatMap(v -> reactiveCommands.zadd("racer_scores", ScoredValue.just(100, "Henshaw"))) + .doOnNext(result -> { + System.out.println(result); // >>> 1 + // REMOVE_START + assertThat(result).isEqualTo(1); + // REMOVE_END + }).flatMap(v -> reactiveCommands.zadd("racer_scores", ScoredValue.just(150, "Henshaw"))) + .doOnNext(result -> { + System.out.println(result); // >>> 0 + // REMOVE_START + assertThat(result).isZero(); + // REMOVE_END + }).flatMap(v -> reactiveCommands.zincrby("racer_scores", 50, "Wood")).doOnNext(result -> { + System.out.println(result); // >>> 150 + // REMOVE_START + assertThat(result).isEqualTo(150); + // REMOVE_END + }).flatMap(v -> reactiveCommands.zincrby("racer_scores", 50, "Henshaw")).doOnNext(result -> { + System.out.println(result); // >>> 200 + // REMOVE_START + assertThat(result).isEqualTo(200); + // REMOVE_END + }).then(); + // STEP_END + leaderboard.block(); + // HIDE_START + } finally { + redisClient.shutdown(); + } + } + +} +// HIDE_END diff --git a/local_examples/client-specific/lettuce-reactive/StringExample.java b/local_examples/client-specific/lettuce-reactive/StringExample.java new file mode 100644 index 0000000000..ea7f1f164c --- /dev/null +++ b/local_examples/client-specific/lettuce-reactive/StringExample.java @@ -0,0 +1,109 @@ +// EXAMPLE: set_tutorial +package io.redis.examples.reactive; + +import io.lettuce.core.*; +import io.lettuce.core.api.reactive.RedisReactiveCommands; +import io.lettuce.core.api.StatefulRedisConnection; +// REMOVE_START +import org.junit.jupiter.api.Test; +// REMOVE_END +import reactor.core.publisher.Mono; + +import java.util.*; + +// REMOVE_START +import static org.assertj.core.api.Assertions.assertThat; +// REMOVE_END + +public class StringExample { + + // REMOVE_START + @Test + // REMOVE_END + public void run() { + RedisClient redisClient = RedisClient.create("redis://localhost:6379"); + + try (StatefulRedisConnection connection = redisClient.connect()) { + RedisReactiveCommands reactiveCommands = connection.reactive(); + + // STEP_START set_get + Mono setAndGet = reactiveCommands.set("bike:1", "Deimos").doOnNext(v -> { + System.out.println(v); // OK + // REMOVE_START + assertThat(v).isEqualTo("OK"); + // REMOVE_END + }).flatMap(v -> reactiveCommands.get("bike:1")).doOnNext(res -> { + // REMOVE_START + assertThat(res).isEqualTo("Deimos"); + // REMOVE_END + System.out.println(res); // Deimos + }).then(); + // STEP_END + + // STEP_START setnx_xx + // Recreate the bike:1 key so this example runs on its own. + Mono setnx = reactiveCommands.set("bike:1", "Deimos") + .flatMap(setup -> reactiveCommands.setnx("bike:1", "bike")).doOnNext(v -> { + System.out.println(v); // false (because key already exists) + // REMOVE_START + assertThat(v).isFalse(); + // REMOVE_END + }).flatMap(v -> reactiveCommands.get("bike:1")).doOnNext(res -> { + // REMOVE_START + assertThat(res).isEqualTo("Deimos"); + // REMOVE_END + System.out.println(res); // Deimos (value is unchanged) + }).then(); + + Mono setxx = reactiveCommands.set("bike:1", "bike", SetArgs.Builder.xx()).doOnNext(res -> { + // REMOVE_START + assertThat(res).isEqualTo("OK"); + // REMOVE_END + System.out.println(res); // OK + }).then(); + // STEP_END + + // STEP_START mset + Map bikeMap = new HashMap<>(); + bikeMap.put("bike:1", "Deimos"); + bikeMap.put("bike:2", "Ares"); + bikeMap.put("bike:3", "Vanth"); + + Mono mset = reactiveCommands.mset(bikeMap).doOnNext(System.out::println) // OK + .flatMap(v -> reactiveCommands.mget("bike:1", "bike:2", "bike:3").collectList()).doOnNext(res -> { + // REMOVE_START + List> expected = new ArrayList<>( + Arrays.asList(KeyValue.just("bike:1", "Deimos"), KeyValue.just("bike:2", "Ares"), + KeyValue.just("bike:3", "Vanth"))); + + assertThat(res).isEqualTo(expected); + // REMOVE_END + System.out.println(res); // [KeyValue[bike:1, Deimos], KeyValue[bike:2, Ares], KeyValue[bike:3, Vanth]] + }).then(); + // STEP_END + + // STEP_START incr + Mono incrby = reactiveCommands.set("total_crashes", "0").flatMap(v -> reactiveCommands.incr("total_crashes")) + .doOnNext(v -> { + System.out.println(v); // 1 + // REMOVE_START + assertThat(v).isEqualTo(1L); + // REMOVE_END + }).flatMap(v -> reactiveCommands.incrby("total_crashes", 10)).doOnNext(res -> { + // REMOVE_START + assertThat(res).isEqualTo(11L); + // REMOVE_END + System.out.println(res); // 11 + }).then(); + // STEP_END + + // Run the steps sequentially: several of them mutate bike:1, so a + // shared Mono.when() would race them against each other's reads. + setAndGet.then(setnx).then(setxx).then(mset).then(incrby).block(); + + } finally { + redisClient.shutdown(); + } + } + +} diff --git a/local_examples/geoindex/go-redis/geo_index_test.go b/local_examples/geoindex/go-redis/geo_index_test.go new file mode 100644 index 0000000000..b503451b0c --- /dev/null +++ b/local_examples/geoindex/go-redis/geo_index_test.go @@ -0,0 +1,201 @@ +// EXAMPLE: geoindex +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_geoindex() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + Protocol: 2, + }) + + // REMOVE_START + // make sure we are working with fresh database + rdb.FlushDB(ctx) + rdb.FTDropIndex(ctx, "productidx") + rdb.FTDropIndex(ctx, "geomidx") + rdb.Del(ctx, "product:46885", "product:46886", "shape:1", "shape:2", "shape:3", "shape:4") + // REMOVE_END + + // STEP_START create_geo_idx + geoCreateResult, err := rdb.FTCreate(ctx, + "productidx", + &redis.FTCreateOptions{ + OnJSON: true, + Prefix: []interface{}{"product:"}, + }, + &redis.FieldSchema{ + FieldName: "$.location", + As: "location", + FieldType: redis.SearchFieldTypeGeo, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(geoCreateResult) // >>> OK + + prd46885 := map[string]interface{}{ + "description": "Navy Blue Slippers", + "price": 45.99, + "city": "Denver", + "location": "-104.991531, 39.742043", + } + + gjResult1, err := rdb.JSONSet(ctx, "product:46885", "$", prd46885).Result() + + if err != nil { + panic(err) + } + + fmt.Println(gjResult1) // >>> OK + + prd46886 := map[string]interface{}{ + "description": "Bright Green Socks", + "price": 25.50, + "city": "Fort Collins", + "location": "-105.0618814,40.5150098", + } + + gjResult2, err := rdb.JSONSet(ctx, "product:46886", "$", prd46886).Result() + + if err != nil { + panic(err) + } + + fmt.Println(gjResult2) // >>> OK + + geoQueryResult, err := rdb.FTSearch(ctx, "productidx", + "@location:[-104.800644 38.846127 100 mi]", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(geoQueryResult) + // >>> {1 [{product:46885... + // STEP_END + + // STEP_START create_gshape_idx + geomCreateResult, err := rdb.FTCreate(ctx, "geomidx", + &redis.FTCreateOptions{ + OnJSON: true, + Prefix: []interface{}{"shape:"}, + }, + &redis.FieldSchema{ + FieldName: "$.name", + As: "name", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.geom", + As: "geom", + FieldType: redis.SearchFieldTypeGeoShape, + GeoShapeFieldType: "FLAT", + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(geomCreateResult) // >>> OK + + shape1 := map[string]interface{}{ + "name": "Green Square", + "geom": "POLYGON ((1 1, 1 3, 3 3, 3 1, 1 1))", + } + + gmjResult1, err := rdb.JSONSet(ctx, "shape:1", "$", shape1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(gmjResult1) // >>> OK + + shape2 := map[string]interface{}{ + "name": "Red Rectangle", + "geom": "POLYGON ((2 2.5, 2 3.5, 3.5 3.5, 3.5 2.5, 2 2.5))", + } + + gmjResult2, err := rdb.JSONSet(ctx, "shape:2", "$", shape2).Result() + + if err != nil { + panic(err) + } + + fmt.Println(gmjResult2) // >>> OK + + shape3 := map[string]interface{}{ + "name": "Blue Triangle", + "geom": "POLYGON ((3.5 1, 3.75 2, 4 1, 3.5 1))", + } + + gmjResult3, err := rdb.JSONSet(ctx, "shape:3", "$", shape3).Result() + + if err != nil { + panic(err) + } + + fmt.Println(gmjResult3) // >>> OK + + shape4 := map[string]interface{}{ + "name": "Purple Point", + "geom": "POINT (2 2)", + } + + gmjResult4, err := rdb.JSONSet(ctx, "shape:4", "$", shape4).Result() + + if err != nil { + panic(err) + } + + fmt.Println(gmjResult4) // >>> OK + + geomQueryResult, err := rdb.FTSearchWithArgs(ctx, "geomidx", + "(-@name:(Green Square) @geom:[WITHIN $qshape])", + &redis.FTSearchOptions{ + Params: map[string]interface{}{ + "qshape": "POLYGON ((1 1, 1 3, 3 3, 3 1, 1 1))", + }, + DialectVersion: 4, + Limit: 1, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(geomQueryResult) + // >>> {1 [{shape:4... + // STEP_END + + // Output: + // OK + // OK + // OK + // {1 [{product:46885 map[$:{"city":"Denver","description":"Navy Blue Slippers","location":"-104.991531, 39.742043","price":45.99}] }] []} + // OK + // OK + // OK + // OK + // OK + // {1 [{shape:4 map[$:[{"geom":"POINT (2 2)","name":"Purple Point"}]] }] []} +} diff --git a/local_examples/geoindex/jedis/GeoIndexExample.java b/local_examples/geoindex/jedis/GeoIndexExample.java new file mode 100644 index 0000000000..1f6d06817d --- /dev/null +++ b/local_examples/geoindex/jedis/GeoIndexExample.java @@ -0,0 +1,173 @@ +// EXAMPLE: geoindex +// REMOVE_START +package io.redis.examples; + +import org.junit.jupiter.api.Test; +// REMOVE_END + +// HIDE_START +import org.json.JSONObject; + +import redis.clients.jedis.RedisClient; +import redis.clients.jedis.json.Path2; +import redis.clients.jedis.search.Document; +import redis.clients.jedis.search.FTCreateParams; +import redis.clients.jedis.search.FTSearchParams; +import redis.clients.jedis.search.IndexDataType; +import redis.clients.jedis.search.schemafields.*; +import redis.clients.jedis.search.schemafields.GeoShapeField.CoordinateSystem; +import redis.clients.jedis.search.SearchResult; +import redis.clients.jedis.exceptions.JedisDataException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +// HIDE_END + +// HIDE_START +public class GeoIndexExample { + + @Test + public void run() { + RedisClient jedis = RedisClient.create("redis://localhost:6379"); + //REMOVE_START + // Clear any keys here before using them in tests. + try { + jedis.ftDropIndex("productidx"); + } catch (JedisDataException j) {} + + try { + jedis.ftDropIndex("geomidx"); + } catch (JedisDataException j) {} + + jedis.del("product:46885", "product:46886", "shape:1", "shape:2", "shape:3", "shape:4"); + //REMOVE_END +// HIDE_END + + // STEP_START create_geo_idx + SchemaField[] geoSchema = { + GeoField.of("$.location").as("location") + }; + + String geoIdxCreateResult = jedis.ftCreate("productidx", + FTCreateParams.createParams() + .on(IndexDataType.JSON) + .addPrefix("product:"), + geoSchema + ); + // REMOVE_START + assertEquals("OK", geoIdxCreateResult); + // REMOVE_END + + JSONObject prd46885 = new JSONObject() + .put("description", "Navy Blue Slippers") + .put("price", 45.99) + .put("city", "Denver") + .put("location", "-104.991531, 39.742043"); + + String jsonAddResult1 = jedis.jsonSet("product:46885", new Path2("$"), prd46885); + System.out.println(jsonAddResult1); // >>> OK + + JSONObject prd46886 = new JSONObject() + .put("description", "Bright Green Socks") + .put("price", 25.50) + .put("city", "Fort Collins") + .put("location", "-105.0618814,40.5150098"); + + String jsonAddResult2 = jedis.jsonSet("product:46886", new Path2("$"), prd46886); + System.out.println(jsonAddResult2); // >>> OK + // REMOVE_START + assertEquals("OK", jsonAddResult1); + assertEquals("OK", jsonAddResult2); + // REMOVE_END + + SearchResult geoResult = jedis.ftSearch("productidx", + "@location:[-104.800644 38.846127 100 mi]" + ); + + System.out.println(geoResult.getTotalResults()); // >>> 1 + + for (Document doc: geoResult.getDocuments()) { + System.out.println(doc.getId()); + } + // >>> product:46885 + // STEP_END + // REMOVE_START + assertEquals("OK", jsonAddResult1); + assertEquals("OK", jsonAddResult2); + assertEquals("product:46885", geoResult.getDocuments().get(0).getId()); + // REMOVE_END + + // STEP_START create_gshape_idx + SchemaField[] geomSchema = { + TextField.of("$.name").as("name"), + GeoShapeField.of("$.geom", CoordinateSystem.FLAT).as("geom") + }; + + String geomIndexCreateResult = jedis.ftCreate("geomidx", + FTCreateParams.createParams() + .on(IndexDataType.JSON) + .addPrefix("shape:"), + geomSchema + ); + System.out.println(geomIndexCreateResult); // >>> OK + // REMOVE_START + assertEquals("OK", geomIndexCreateResult); + // REMOVE_END + + JSONObject shape1 = new JSONObject() + .put("name", "Green Square") + .put("geom", "POLYGON ((1 1, 1 3, 3 3, 3 1, 1 1))"); + + String gmJsonRes1 = jedis.jsonSet("shape:1", new Path2("$"), shape1); + System.out.println(gmJsonRes1); // >>> OK + + JSONObject shape2 = new JSONObject() + .put("name", "Red Rectangle") + .put("geom", "POLYGON ((2 2.5, 2 3.5, 3.5 3.5, 3.5 2.5, 2 2.5))"); + + String gmJsonRes2 = jedis.jsonSet("shape:2", new Path2("$"), shape2); + System.out.println(gmJsonRes2); // >>> OK + + JSONObject shape3 = new JSONObject() + .put("name", "Blue Triangle") + .put("geom", "POLYGON ((3.5 1, 3.75 2, 4 1, 3.5 1))"); + + String gmJsonRes3 = jedis.jsonSet("shape:3", new Path2("$"), shape3); + System.out.println(gmJsonRes3); // >>> OK + + JSONObject shape4 = new JSONObject() + .put("name", "Purple Point") + .put("geom", "POINT (2 2)"); + + String gmJsonRes4 = jedis.jsonSet("shape:4", new Path2("$"), shape4); + System.out.println(gmJsonRes4); // >>> OK + // REMOVE_START + assertEquals("OK", gmJsonRes1); + assertEquals("OK", gmJsonRes2); + assertEquals("OK", gmJsonRes3); + assertEquals("OK", gmJsonRes4); + // REMOVE_END + + SearchResult geomResult = jedis.ftSearch("geomidx", + "(-@name:(Green Square) @geom:[WITHIN $qshape])", + FTSearchParams.searchParams() + .addParam("qshape", "POLYGON ((1 1, 1 3, 3 3, 3 1, 1 1))") + .dialect(4) + .limit(0, 1) + ); + System.out.println(geomResult.getTotalResults()); // >>> 1 + + for (Document doc: geomResult.getDocuments()) { + System.out.println(doc.getId()); + } + // shape:4 + // STEP_END + // REMOVE_START + assertEquals(1, geomResult.getTotalResults()); + assertEquals("shape:4", geomResult.getDocuments().get(0).getId()); + // REMOVE_END + // HIDE_START + + jedis.close(); + } +} +// HIDE_END diff --git a/local_examples/geoindex/nredisstack/GeoIndexExample.cs b/local_examples/geoindex/nredisstack/GeoIndexExample.cs new file mode 100644 index 0000000000..58f9d2ffbc --- /dev/null +++ b/local_examples/geoindex/nredisstack/GeoIndexExample.cs @@ -0,0 +1,203 @@ +// EXAMPLE: geoindex + +// STEP_START import +using NRedisStack.RedisStackCommands; +using NRedisStack.Search; +using NRedisStack.Search.Literals.Enums; +using StackExchange.Redis; +// STEP_END + +// REMOVE_START +using NRedisStack.Tests; + +namespace Doc; + +[Collection("DocsTests")] +// REMOVE_END + +// HIDE_START +public class GeoIndexExample +// REMOVE_START +: AbstractNRedisStackTest, IDisposable +// REMOVE_END +{ + // REMOVE_START + public GeoIndexExample(EndpointsFixture fixture) : base(fixture) { } + + [Fact] + // REMOVE_END + public void run() + { + //REMOVE_START + // This is needed because we're constructing ConfigurationOptions in the test before calling GetConnection + SkipIfTargetConnectionDoesNotExist(EndpointsFixture.Env.Standalone); + var _ = GetCleanDatabase(EndpointsFixture.Env.Standalone); + //REMOVE_END + var muxer = ConnectionMultiplexer.Connect("localhost:6379"); + var db = muxer.GetDatabase(); + //REMOVE_START + // Clear any keys here before using them in tests. + db.KeyDelete(new RedisKey[] { "product:46885", "product:46886", "shape:1", "shape:2", "shape:3", "shape:4" }); + try { db.FT().DropIndex("productidx"); } catch { } + try { db.FT().DropIndex("geomidx"); } catch { } + //REMOVE_END + // HIDE_END + + // STEP_START create_geo_idx + Schema geoSchema = new Schema() + .AddGeoField(new FieldName("$.location", "location")); + + bool geoCreateResult = db.FT().Create( + "productidx", + new FTCreateParams() + .On(IndexDataType.JSON) + .Prefix("product:"), + geoSchema + ); + Console.WriteLine(geoCreateResult); // >>> True + // REMOVE_START + Assert.True(geoCreateResult); + // REMOVE_END + + var product46885 = new + { + description = "Navy Blue Slippers", + price = 45.99, + city = "Denver", + location = "-104.991531, 39.742043" + }; + + bool gjAddResult1 = db.JSON().Set("product:46885", "$", product46885); + Console.WriteLine(gjAddResult1); // >>> True + + var product46886 = new + { + description = "Bright Green Socks", + price = 25.50, + city = "Fort Collins", + location = "-105.0618814,40.5150098" + }; + + bool gjAddResult2 = db.JSON().Set("product:46886", "$", product46886); + Console.WriteLine(gjAddResult2); // >>> True + // REMOVE_START + Assert.True(gjAddResult1); + Assert.True(gjAddResult2); + // REMOVE_END + + SearchResult geoQueryResult = db.FT().Search( + "productidx", + new Query("@location:[-104.800644 38.846127 100 mi]") + ); + Console.WriteLine(geoQueryResult.Documents.Count); // >>> 1 + + Console.WriteLine( + string.Join(", ", geoQueryResult.Documents.Select(x => x["json"])) + ); + // >>> {"description":"Navy Blue Slippers","price":45.99,"city":"Denver"... + // STEP_END + // REMOVE_START + Assert.Single(geoQueryResult.Documents); + Assert.Equal( + "{\"description\":\"Navy Blue Slippers\",\"price\":45.99,\"city\":\"Denver\",\"location\":\"-104.991531, 39.742043\"}", + string.Join(", ", geoQueryResult.Documents.Select(x => x["json"])) + ); + // REMOVE_END + + // STEP_START create_gshape_idx + Version version = muxer.GetServer("localhost:6379").Version; + if (version.Major >= 7) + { + Schema geomSchema = new Schema() + .AddGeoShapeField( + new FieldName("$.geom", "geom"), + Schema.GeoShapeField.CoordinateSystem.FLAT + ) + .AddTextField(new FieldName("$.name", "name")); + + bool geomCreateResult = db.FT().Create( + "geomidx", + new FTCreateParams() + .On(IndexDataType.JSON) + .Prefix("shape:"), + geomSchema + ); + // REMOVE_START + Assert.True(geomCreateResult); + // REMOVE_END + Console.WriteLine(geomCreateResult); // >>> True + } + + + var shape1 = new + { + name = "Green Square", + geom = "POLYGON ((1 1, 1 3, 3 3, 3 1, 1 1))" + }; + + bool gmJsonRes1 = db.JSON().Set("shape:1", "$", shape1); + Console.WriteLine(gmJsonRes1); // >>> True + + var shape2 = new + { + name = "Red Rectangle", + geom = "POLYGON ((2 2.5, 2 3.5, 3.5 3.5, 3.5 2.5, 2 2.5))" + }; + + bool gmJsonRes2 = db.JSON().Set("shape:2", "$", shape2); + Console.WriteLine(gmJsonRes2); // >>> True + + var shape3 = new + { + name = "Blue Triangle", + geom = "POLYGON ((3.5 1, 3.75 2, 4 1, 3.5 1))" + }; + + bool gmJsonRes3 = db.JSON().Set("shape:3", "$", shape3); + Console.WriteLine(gmJsonRes3); // >>> True + + var shape4 = new + { + name = "Purple Point", + geom = "POINT (2 2)" + }; + + bool gmJsonRes4 = db.JSON().Set("shape:4", "$", shape4); + Console.WriteLine(gmJsonRes4); // >>> True + // REMOVE_START + Assert.True(gmJsonRes1); + Assert.True(gmJsonRes2); + Assert.True(gmJsonRes3); + Assert.True(gmJsonRes4); + // REMOVE_END + + if (version.Major >= 7) + { + SearchResult geomQueryResult = db.FT().Search( + "geomidx", + new Query("(-@name:(Green Square) @geom:[WITHIN $qshape])") + .AddParam("qshape", "POLYGON ((1 1, 1 3, 3 3, 3 1, 1 1))") + .Limit(0, 1) + .Dialect(2) + ); + + Console.WriteLine(geomQueryResult.Documents.Count); // >>> 1 + var res = string.Join(", ", geomQueryResult.Documents.Select(x => x["json"])); + + Console.WriteLine( + string.Join(", ", geomQueryResult.Documents.Select(x => x["json"])) + ); + // >>> {"name":"Purple Point","geom":"POINT (2 2)"} + // REMOVE_START + Assert.Single(geomQueryResult.Documents); + Assert.Equal( + "{\"name\":\"Purple Point\",\"geom\":\"POINT (2 2)\"}", + string.Join(", ", geomQueryResult.Documents.Select(x => x["json"])) + ); + // REMOVE_END + } + // STEP_END + // HIDE_START + } +} +// HIDE_END diff --git a/local_examples/geoindex/redis-py/geo_index.py b/local_examples/geoindex/redis-py/geo_index.py new file mode 100644 index 0000000000..3f5359be7a --- /dev/null +++ b/local_examples/geoindex/redis-py/geo_index.py @@ -0,0 +1,145 @@ +# EXAMPLE: geoindex +import redis +from redis.commands.json.path import Path +from redis.commands.search.field import TextField, GeoField, GeoShapeField +from redis.commands.search.index_definition import IndexDefinition, IndexType +from redis.commands.search.query import Query + +r = redis.Redis() +# REMOVE_START +try: + r.ft("productidx").dropindex(True) +except redis.exceptions.ResponseError: + pass + +try: + r.ft("geomidx").dropindex(True) +except redis.exceptions.ResponseError: + pass + +r.delete("product:46885", "product:46886", "shape:1", "shape:2", "shape:3", "shape:4") +# REMOVE_END + +# STEP_START create_geo_idx +geo_schema = ( + GeoField("$.location", as_name="location") +) + +geo_index_create_result = r.ft("productidx").create_index( + geo_schema, + definition=IndexDefinition( + prefix=["product:"], index_type=IndexType.JSON + ) +) +print(geo_index_create_result) # >>> True +# REMOVE_START +assert geo_index_create_result +# REMOVE_END + +prd46885 = { + "description": "Navy Blue Slippers", + "price": 45.99, + "city": "Denver", + "location": "-104.991531, 39.742043" +} + +json_add_result_1 = r.json().set("product:46885", Path.root_path(), prd46885) +print(json_add_result_1) # >>> True + +prd46886 = { + "description": "Bright Green Socks", + "price": 25.50, + "city": "Fort Collins", + "location": "-105.0618814,40.5150098" +} + +json_add_result_2 = r.json().set("product:46886", Path.root_path(), prd46886) +print(json_add_result_2) # >>> True +# REMOVE_START +assert json_add_result_1 +assert json_add_result_2 +# REMOVE_END + +geo_result = r.ft("productidx").search( + "@location:[-104.800644 38.846127 100 mi]" +) +print(geo_result) +# >>> Result{1 total, docs: [Document {'id': 'product:46885'... +# STEP_END +# REMOVE_START +assert len(geo_result.docs) == 1 +assert geo_result.docs[0]["id"] == "product:46885" +# REMOVE_END + +# STEP_START create_gshape_idx +geom_schema = ( + TextField("$.name", as_name="name"), + GeoShapeField( + "$.geom", as_name="geom", coord_system=GeoShapeField.FLAT + ) +) + +geom_index_create_result = r.ft("geomidx").create_index( + geom_schema, + definition=IndexDefinition( + prefix=["shape:"], index_type=IndexType.JSON + ) +) +print(geom_index_create_result) # True +# REMOVE_START +assert geom_index_create_result +# REMOVE_END + +shape1 = { + "name": "Green Square", + "geom": "POLYGON ((1 1, 1 3, 3 3, 3 1, 1 1))" +} + +gm_json_res_1 = r.json().set("shape:1", Path.root_path(), shape1) +print(gm_json_res_1) # >>> True + +shape2 = { + "name": "Red Rectangle", + "geom": "POLYGON ((2 2.5, 2 3.5, 3.5 3.5, 3.5 2.5, 2 2.5))" +} + +gm_json_res_2 = r.json().set("shape:2", Path.root_path(), shape2) +print(gm_json_res_2) # >>> True + +shape3 = { + "name": "Blue Triangle", + "geom": "POLYGON ((3.5 1, 3.75 2, 4 1, 3.5 1))" +} + +gm_json_res_3 = r.json().set("shape:3", Path.root_path(), shape3) +print(gm_json_res_3) # >>> True + +shape4 = { + "name": "Purple Point", + "geom": "POINT (2 2)" +} + +gm_json_res_4 = r.json().set("shape:4", Path.root_path(), shape4) +print(gm_json_res_4) # >>> True +# REMOVE_START +assert gm_json_res_1 +assert gm_json_res_2 +assert gm_json_res_3 +assert gm_json_res_4 +# REMOVE_END + +geom_result = r.ft("geomidx").search( + Query( + "(-@name:(Green Square) @geom:[WITHIN $qshape])" + ).dialect(4).paging(0, 1), + query_params={ + "qshape": "POLYGON ((1 1, 1 3, 3 3, 3 1, 1 1))" + } +) +print(geom_result) +# >>> Result{1 total, docs: [Document {'id': 'shape:4'... +# STEP_END +# REMOVE_START +assert len(geom_result.docs) == 1 +assert geom_result.docs[0]["id"] == "shape:4" +# REMOVE_END diff --git a/local_examples/php/DtHashTest.php b/local_examples/php/DtHashTest.php index eadeefb1c8..b5eded71fb 100644 --- a/local_examples/php/DtHashTest.php +++ b/local_examples/php/DtHashTest.php @@ -57,6 +57,15 @@ public function testDtHash() { // REMOVE_END // STEP_START hmget + // Recreate the bike:1 hash so this example runs on its own. + $r->del('bike:1'); + $r->hset('bike:1', [ + 'model' => 'Deimos', + 'brand' => 'Ergonom', + 'type' => 'Enduro bikes', + 'price' => 4972, + ]); + $res5 = $r->hmget('bike:1', ['model', 'price']); echo json_encode($res5) . PHP_EOL; // >>> ["Deimos","4972"] @@ -66,6 +75,15 @@ public function testDtHash() { // REMOVE_END // STEP_START hincrby + // Recreate the bike:1 hash so this example runs on its own. + $r->del('bike:1'); + $r->hset('bike:1', [ + 'model' => 'Deimos', + 'brand' => 'Ergonom', + 'type' => 'Enduro bikes', + 'price' => 4972, + ]); + $res6 = $r->hincrby('bike:1', 'price', 100); echo $res6 . PHP_EOL; // >>> 5072 diff --git a/local_examples/php/DtSetsTest.php b/local_examples/php/DtSetsTest.php index 5444c84eb3..cdfa1aa26f 100644 --- a/local_examples/php/DtSetsTest.php +++ b/local_examples/php/DtSetsTest.php @@ -98,6 +98,10 @@ public function testDtSet() { // REMOVE_END // STEP_START smismember + // Recreate the set so this example runs on its own. + $r->del('bikes:racing:france'); + $r->sadd('bikes:racing:france', 'bike:1', 'bike:2', 'bike:3'); + $res11 = $r->sismember('bikes:racing:france', 'bike:1'); echo $res11 . PHP_EOL; // >>> 1 diff --git a/local_examples/php/DtSortedSetsTest.php b/local_examples/php/DtSortedSetsTest.php index e7c596e90b..18e7d95c8a 100644 --- a/local_examples/php/DtSortedSetsTest.php +++ b/local_examples/php/DtSortedSetsTest.php @@ -105,6 +105,14 @@ public function testDtSortedSet() { // REMOVE_END // STEP_START zrank + // Recreate the three remaining racers so this example runs on its own. + $r->del('racer_scores'); + $r->zadd('racer_scores', [ + 'Norem' => 10, + 'Royce' => 10, + 'Prickett' => 14, + ]); + $res11 = $r->zrank('racer_scores', 'Norem'); echo $res11 . PHP_EOL; // >>> 0 diff --git a/local_examples/php/DtStringTest.php b/local_examples/php/DtStringTest.php index 6ea1420a7e..f423d7eceb 100644 --- a/local_examples/php/DtStringTest.php +++ b/local_examples/php/DtStringTest.php @@ -38,6 +38,9 @@ public function testDtString() { // REMOVE_END // STEP_START setnx_xx + // Recreate the bike:1 key so this example runs on its own. + $r->set('bike:1', 'Deimos'); + $res3 = $r->set('bike:1', 'bike', 'nx'); echo "$res3" . PHP_EOL; // >>> (null) diff --git a/local_examples/ruby/dt_hash.rb b/local_examples/ruby/dt_hash.rb index da31462727..898044de23 100644 --- a/local_examples/ruby/dt_hash.rb +++ b/local_examples/ruby/dt_hash.rb @@ -46,6 +46,15 @@ def assert_equal(expected, actual) # REMOVE_END # STEP_START hmget +# Recreate the bike:1 hash so this example runs on its own. +r.del('bike:1') +r.hset('bike:1', { + 'model' => 'Deimos', + 'brand' => 'Ergonom', + 'type' => 'Enduro bikes', + 'price' => 4972 +}) + res5 = r.hmget('bike:1', 'model', 'price', 'no-such-field') puts res5.inspect # ["Deimos", "4972", nil] # STEP_END @@ -55,6 +64,15 @@ def assert_equal(expected, actual) # REMOVE_END # STEP_START hincrby +# Recreate the bike:1 hash so this example runs on its own. +r.del('bike:1') +r.hset('bike:1', { + 'model' => 'Deimos', + 'brand' => 'Ergonom', + 'type' => 'Enduro bikes', + 'price' => 4972 +}) + res6 = r.hincrby('bike:1', 'price', 100) puts res6 # 5072 diff --git a/local_examples/ruby/dt_sets.rb b/local_examples/ruby/dt_sets.rb index 238c5f754f..fb8b8bb118 100644 --- a/local_examples/ruby/dt_sets.rb +++ b/local_examples/ruby/dt_sets.rb @@ -99,6 +99,10 @@ def assert_same_members(expected, actual) # REMOVE_END # STEP_START smismember +# Recreate the set so this example runs on its own. +r.del('bikes:racing:france') +r.sadd('bikes:racing:france', ['bike:1', 'bike:2', 'bike:3']) + res11 = r.sismember('bikes:racing:france', 'bike:1') puts res11 # true diff --git a/local_examples/ruby/dt_sorted_sets.rb b/local_examples/ruby/dt_sorted_sets.rb index 7392e7ccfe..8da3f9bfe4 100644 --- a/local_examples/ruby/dt_sorted_sets.rb +++ b/local_examples/ruby/dt_sorted_sets.rb @@ -95,6 +95,10 @@ def assert_equal(expected, actual) # REMOVE_END # STEP_START zrank +# Recreate the three remaining racers so this example runs on its own. +r.del('racer_scores') +r.zadd('racer_scores', [[10, 'Norem'], [10, 'Royce'], [14, 'Prickett']]) + res11 = r.zrank('racer_scores', 'Norem') puts res11 # 0 diff --git a/local_examples/ruby/dt_string.rb b/local_examples/ruby/dt_string.rb index c7adaf0aaa..98898897a7 100644 --- a/local_examples/ruby/dt_string.rb +++ b/local_examples/ruby/dt_string.rb @@ -19,6 +19,9 @@ # REMOVE_END # STEP_START setnx_xx +# Recreate the bike:1 key so this example runs on its own. +r.set('bike:1', 'Deimos') + res3 = r.set('bike:1', 'bike', nx: true) puts res3 # false diff --git a/local_examples/rust-async/dt-hash.rs b/local_examples/rust-async/dt-hash.rs index 0ed0dc981b..59bf7319d0 100644 --- a/local_examples/rust-async/dt-hash.rs +++ b/local_examples/rust-async/dt-hash.rs @@ -93,6 +93,13 @@ mod tests { // STEP_END // STEP_START hmget + // Recreate the bike:1 hash so this example runs on its own. + let _: () = r.del("bike:1").await.expect("Failed to del"); + let _: () = r.hset_multiple( + "bike:1", + &[("model", "Deimos"), ("brand", "Ergonom"), ("type", "Enduro bikes"), ("price", "4972")], + ).await.expect("Failed to hset"); + match r.hmget("bike:1", &["model", "price"]).await { Ok(res) => { let res: Vec = res; @@ -111,6 +118,13 @@ mod tests { // STEP_END // STEP_START hincrby + // Recreate the bike:1 hash so this example runs on its own. + let _: () = r.del("bike:1").await.expect("Failed to del"); + let _: () = r.hset_multiple( + "bike:1", + &[("model", "Deimos"), ("brand", "Ergonom"), ("type", "Enduro bikes"), ("price", "4972")], + ).await.expect("Failed to hset"); + if let Ok(res) = r.hincr("bike:1", "price", 100).await { let res: i32 = res; println!("{res}"); // >>> 5072 diff --git a/local_examples/rust-async/dt-sets.rs b/local_examples/rust-async/dt-sets.rs index 20903285e6..75381d191c 100644 --- a/local_examples/rust-async/dt-sets.rs +++ b/local_examples/rust-async/dt-sets.rs @@ -133,6 +133,13 @@ mod tests { // STEP_END // STEP_START smismember + // Recreate the set so this example runs on its own. + let _: usize = r.del("bikes:racing:france").await.unwrap_or(0); + let _: usize = r + .sadd("bikes:racing:france", &["bike:1", "bike:2", "bike:3"]) + .await + .unwrap_or(0); + if let Ok(res) = r.sismember("bikes:racing:france", "bike:1").await { let res: bool = res; println!("{}", i32::from(res)); // >>> 1 diff --git a/local_examples/rust-async/dt-sorted-sets.rs b/local_examples/rust-async/dt-sorted-sets.rs index d20a482ba4..68a4d2ed61 100644 --- a/local_examples/rust-async/dt-sorted-sets.rs +++ b/local_examples/rust-async/dt-sorted-sets.rs @@ -155,6 +155,16 @@ mod tests { // STEP_END // STEP_START zrank + // Recreate the three remaining racers so this example runs on its own. + let _: () = r.del("racer_scores").await.expect("Failed to del"); + let _: usize = r + .zadd_multiple( + "racer_scores", + &[(10, "Norem"), (10, "Royce"), (14, "Prickett")], + ) + .await + .expect("Failed to zadd"); + if let Ok(res) = r.zrank("racer_scores", "Norem").await { let res: Option = res; if let Some(res) = res { diff --git a/local_examples/rust-async/dt-string.rs b/local_examples/rust-async/dt-string.rs index 40535b3399..bb64639851 100644 --- a/local_examples/rust-async/dt-string.rs +++ b/local_examples/rust-async/dt-string.rs @@ -46,6 +46,9 @@ mod tests { // STEP_END // STEP_START setnx_xx + // Recreate the bike:1 key so this example runs on its own. + let _: () = r.set("bike:1", "Deimos").await.expect("Failed to set"); + if let Ok(res) = r.set_options("bike:1", "bike", redis::SetOptions::default().conditional_set(ExistenceCheck::NX)).await { let res: bool = res; println!("{res}"); // >>> false diff --git a/local_examples/rust-sync/dt-hash.rs b/local_examples/rust-sync/dt-hash.rs index a052b67250..ff47ceb3c9 100644 --- a/local_examples/rust-sync/dt-hash.rs +++ b/local_examples/rust-sync/dt-hash.rs @@ -93,6 +93,13 @@ mod hash_tests { // STEP_END // STEP_START hmget + // Recreate the bike:1 hash so this example runs on its own. + let _: () = r.del("bike:1").expect("Failed to del"); + let _: () = r.hset_multiple( + "bike:1", + &[("model", "Deimos"), ("brand", "Ergonom"), ("type", "Enduro bikes"), ("price", "4972")], + ).expect("Failed to hset"); + match r.hmget("bike:1", &["model", "price"]) { Ok(res) => { let res: Vec = res; @@ -111,6 +118,13 @@ mod hash_tests { // STEP_END // STEP_START hincrby + // Recreate the bike:1 hash so this example runs on its own. + let _: () = r.del("bike:1").expect("Failed to del"); + let _: () = r.hset_multiple( + "bike:1", + &[("model", "Deimos"), ("brand", "Ergonom"), ("type", "Enduro bikes"), ("price", "4972")], + ).expect("Failed to hset"); + if let Ok(res) = r.hincr("bike:1", "price", 100) { let res: i32 = res; println!("{res}"); // >>> 5072 diff --git a/local_examples/rust-sync/dt-sets.rs b/local_examples/rust-sync/dt-sets.rs index cb6f3fa7f3..19cb9a9932 100644 --- a/local_examples/rust-sync/dt-sets.rs +++ b/local_examples/rust-sync/dt-sets.rs @@ -133,6 +133,12 @@ mod sets_tests { // STEP_END // STEP_START smismember + // Recreate the set so this example runs on its own. + let _: usize = r.del("bikes:racing:france").unwrap_or(0); + let _: usize = r + .sadd("bikes:racing:france", &["bike:1", "bike:2", "bike:3"]) + .unwrap_or(0); + if let Ok(res) = r.sismember("bikes:racing:france", "bike:1") { let res: bool = res; println!("{}", i32::from(res)); // >>> 1 diff --git a/local_examples/rust-sync/dt-sorted-sets.rs b/local_examples/rust-sync/dt-sorted-sets.rs index f4dd649986..708557fa90 100644 --- a/local_examples/rust-sync/dt-sorted-sets.rs +++ b/local_examples/rust-sync/dt-sorted-sets.rs @@ -152,6 +152,15 @@ mod sorted_set_tests { // STEP_END // STEP_START zrank + // Recreate the three remaining racers so this example runs on its own. + let _: () = r.del("racer_scores").expect("Failed to del"); + let _: usize = r + .zadd_multiple( + "racer_scores", + &[(10, "Norem"), (10, "Royce"), (14, "Prickett")], + ) + .expect("Failed to zadd"); + if let Ok(res) = r.zrank("racer_scores", "Norem") { let res: Option = res; if let Some(res) = res { diff --git a/local_examples/rust-sync/dt-string.rs b/local_examples/rust-sync/dt-string.rs index 46f4ac8da7..74fc654c8f 100644 --- a/local_examples/rust-sync/dt-string.rs +++ b/local_examples/rust-sync/dt-string.rs @@ -48,6 +48,9 @@ mod strings_tests { // STEP_END // STEP_START setnx_xx + // Recreate the bike:1 key so this example runs on its own. + let _: () = r.set("bike:1", "Deimos").expect("Failed to set"); + if let Ok(res) = r.set_options("bike:1", "bike", redis::SetOptions::default().conditional_set(ExistenceCheck::NX)) { let res: bool = res; println!("{res}"); // >>> false diff --git a/local_examples/search_quickstart/go-redis/search_quickstart_test.go b/local_examples/search_quickstart/go-redis/search_quickstart_test.go new file mode 100644 index 0000000000..b0b362bde4 --- /dev/null +++ b/local_examples/search_quickstart/go-redis/search_quickstart_test.go @@ -0,0 +1,250 @@ +// EXAMPLE: search_quickstart +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +var bicycles = []interface{}{ + map[string]interface{}{ + "brand": "Velorim", + "model": "Jigger", + "price": 270, + "description": "Small and powerful, the Jigger is the best ride " + + "for the smallest of tikes! This is the tiniest " + + "kids’ pedal bike on the market available without" + + " a coaster brake, the Jigger is the vehicle of " + + "choice for the rare tenacious little rider " + + "raring to go.", + "condition": "new", + }, + map[string]interface{}{ + "brand": "Bicyk", + "model": "Hillcraft", + "price": 1200, + "description": "Kids want to ride with as little weight as possible." + + " Especially on an incline! They may be at the age " + + "when a 27.5\" wheel bike is just too clumsy coming " + + "off a 24\" bike. The Hillcraft 26 is just the solution" + + " they need!", + "condition": "used", + }, + map[string]interface{}{ + "brand": "Nord", + "model": "Chook air 5", + "price": 815, + "description": "The Chook Air 5 gives kids aged six years and older " + + "a durable and uberlight mountain bike for their first" + + " experience on tracks and easy cruising through forests" + + " and fields. The lower top tube makes it easy to mount" + + " and dismount in any situation, giving your kids greater" + + " safety on the trails.", + "condition": "used", + }, + map[string]interface{}{ + "brand": "Eva", + "model": "Eva 291", + "price": 3400, + "description": "The sister company to Nord, Eva launched in 2005 as the" + + " first and only women-dedicated bicycle brand. Designed" + + " by women for women, allEva bikes are optimized for the" + + " feminine physique using analytics from a body metrics" + + " database. If you like 29ers, try the Eva 291. It’s a " + + "brand new bike for 2022.. This full-suspension, " + + "cross-country ride has been designed for velocity. The" + + " 291 has 100mm of front and rear travel, a superlight " + + "aluminum frame and fast-rolling 29-inch wheels. Yippee!", + "condition": "used", + }, + map[string]interface{}{ + "brand": "Noka Bikes", + "model": "Kahuna", + "price": 3200, + "description": "Whether you want to try your hand at XC racing or are " + + "looking for a lively trail bike that's just as inspiring" + + " on the climbs as it is over rougher ground, the Wilder" + + " is one heck of a bike built specifically for short women." + + " Both the frames and components have been tweaked to " + + "include a women’s saddle, different bars and unique " + + "colourway.", + "condition": "used", + }, + map[string]interface{}{ + "brand": "Breakout", + "model": "XBN 2.1 Alloy", + "price": 810, + "description": "The XBN 2.1 Alloy is our entry-level road bike – but that’s" + + " not to say that it’s a basic machine. With an internal " + + "weld aluminium frame, a full carbon fork, and the slick-shifting" + + " Claris gears from Shimano’s, this is a bike which doesn’t" + + " break the bank and delivers craved performance.", + "condition": "new", + }, + map[string]interface{}{ + "brand": "ScramBikes", + "model": "WattBike", + "price": 2300, + "description": "An e-bike with a 1000W mid-drive and a 48V battery, offering over 60 miles per charge and three riding modes.", + "condition": "new", + }, + map[string]interface{}{ + "brand": "Peaknetic", + "model": "Secto", + "price": 430, + "description": "A lightweight aluminum commuter bike with ergonomic grips, a lumbar-supporting seat, and a low step-over frame for comfort and easy mounting.", + "condition": "new", + }, + map[string]interface{}{ + "brand": "nHill", + "model": "Summit", + "price": 1200, + "description": "This budget mountain bike from nHill performs well both on bike" + + " paths and on the trail. The fork with 100mm of travel absorbs" + + " rough terrain. Fat Kenda Booster tires give you grip in corners" + + " and on wet trails. The Shimano Tourney drivetrain offered enough" + + " gears for finding a comfortable pace to ride uphill, and the" + + " Tektro hydraulic disc brakes break smoothly. Whether you want an" + + " affordable bike that you can take to work, but also take trail in" + + " mountains on the weekends or you’re just after a stable," + + " comfortable ride for the bike path, the Summit gives a good value" + + " for money.", + "condition": "new", + }, + map[string]interface{}{ + "model": "ThrillCycle", + "brand": "BikeShind", + "price": 815, + "description": "An artsy, retro-inspired bicycle that’s as functional as it is" + + " pretty: The ThrillCycle steel frame offers a smooth ride. A" + + " 9-speed drivetrain has enough gears for coasting in the city, but" + + " we wouldn’t suggest taking it to the mountains. Fenders protect" + + " you from mud, and a rear basket lets you transport groceries," + + " flowers and books. The ThrillCycle comes with a limited lifetime" + + " warranty, so this little guy will last you long past graduation.", + "condition": "refurbished", + }, +} + +func ExampleClient_search_qs() { + // STEP_START connect + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + Protocol: 2, + }) + // STEP_END + + // REMOVE_START + rdb.FTDropIndex(ctx, "idx:bicycle") + // REMOVE_END + + // STEP_START create_index + schema := []*redis.FieldSchema{ + { + FieldName: "$.brand", + As: "brand", + FieldType: redis.SearchFieldTypeText, + }, + { + FieldName: "$.model", + As: "model", + FieldType: redis.SearchFieldTypeText, + }, + { + FieldName: "$.description", + As: "description", + FieldType: redis.SearchFieldTypeText, + }, + { + FieldName: "$.price", + As: "price", + FieldType: redis.SearchFieldTypeNumeric, + }, + { + FieldName: "$.condition", + As: "condition", + FieldType: redis.SearchFieldTypeTag, + }, + } + + _, err := rdb.FTCreate(ctx, "idx:bicycle", + &redis.FTCreateOptions{ + Prefix: []interface{}{"bicycle:"}, + OnJSON: true, + }, + schema..., + ).Result() + + if err != nil { + panic(err) + } + + for i, bicycle := range bicycles { + _, err := rdb.JSONSet( + ctx, + fmt.Sprintf("bicycle:%v", i), + "$", + bicycle, + ).Result() + + if err != nil { + panic(err) + } + } + // STEP_END + + // STEP_START wildcard_query + wCardResult, err := rdb.FTSearch(ctx, "idx:bicycle", "*").Result() + + if err != nil { + panic(err) + } + + fmt.Printf("Documents found: %v\n", wCardResult.Total) + // >>> Documents found: 10 + // STEP_END + + // STEP_START query_single_term + stResult, err := rdb.FTSearch( + ctx, + "idx:bicycle", + "@model:Jigger", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(stResult) + // >>> {1 [{bicycle:0 map[$:{"brand":"Velorim", ... + // STEP_END + + // STEP_START query_exact_matching + exactMatchResult, err := rdb.FTSearch( + ctx, + "idx:bicycle", + "@brand:\"Noka Bikes\"", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(exactMatchResult) + // >>> {1 [{bicycle:4 map[$:{"brand":"Noka Bikes"... + // STEP_END + + // Output: + // Documents found: 10 + // {1 [{bicycle:0 map[$:{"brand":"Velorim","condition":"new","description":"Small and powerful, the Jigger is the best ride for the smallest of tikes! This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger is the vehicle of choice for the rare tenacious little rider raring to go.","model":"Jigger","price":270}] }] []} + // {1 [{bicycle:4 map[$:{"brand":"Noka Bikes","condition":"used","description":"Whether you want to try your hand at XC racing or are looking for a lively trail bike that's just as inspiring on the climbs as it is over rougher ground, the Wilder is one heck of a bike built specifically for short women. Both the frames and components have been tweaked to include a women’s saddle, different bars and unique colourway.","model":"Kahuna","price":3200}] }] []} +} diff --git a/local_examples/search_quickstart/jedis/SearchQuickstartExample.java b/local_examples/search_quickstart/jedis/SearchQuickstartExample.java new file mode 100644 index 0000000000..07d34de399 --- /dev/null +++ b/local_examples/search_quickstart/jedis/SearchQuickstartExample.java @@ -0,0 +1,262 @@ +// EXAMPLE: search_quickstart +package io.redis.examples; + +import java.math.BigDecimal; +import java.util.*; + +import redis.clients.jedis.*; +import redis.clients.jedis.exceptions.*; +import redis.clients.jedis.search.*; +import redis.clients.jedis.search.aggr.*; +import redis.clients.jedis.search.schemafields.*; +// REMOVE_START +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +// REMOVE_END + +class Bicycle { + public String brand; + public String model; + public BigDecimal price; + public String description; + public String condition; + + public Bicycle(String brand, String model, BigDecimal price, String condition, String description) { + this.brand = brand; + this.model = model; + this.price = price; + this.condition = condition; + this.description = description; + } +} + +public class SearchQuickstartExample { + + @Test + public void run() { + // STEP_START connect + RedisClient jedis = RedisClient.create("localhost", 6379); + // STEP_END + // REMOVE_START + try { + jedis.ftDropIndex("idx:bicycle"); + } catch (JedisDataException e) { + System.out.println("Can't connect to Redis: " + e.getMessage()); + } + // REMOVE_END + + // STEP_START create_index + SchemaField[] schema = { + TextField.of("$.brand").as("brand"), + TextField.of("$.model").as("model"), + TextField.of("$.description").as("description"), + NumericField.of("$.price").as("price"), + TagField.of("$.condition").as("condition") + }; + + jedis.ftCreate("idx:bicycle", + FTCreateParams.createParams() + .on(IndexDataType.JSON) + .addPrefix("bicycle:"), + schema + ); + + Bicycle[] bicycles = { + new Bicycle( + "Velorim", + "Jigger", + new BigDecimal(270), + "new", + "Small and powerful, the Jigger is the best ride " + + "for the smallest of tikes! This is the tiniest " + + "kids’ pedal bike on the market available without" + + " a coaster brake, the Jigger is the vehicle of " + + "choice for the rare tenacious little rider " + + "raring to go." + ), + new Bicycle( + "Bicyk", + "Hillcraft", + new BigDecimal(1200), + "used", + "Kids want to ride with as little weight as possible." + + " Especially on an incline! They may be at the age " + + "when a 27.5 inch wheel bike is just too clumsy coming " + + "off a 24 inch bike. The Hillcraft 26 is just the solution" + + " they need!" + ), + new Bicycle( + "Nord", + "Chook air 5", + new BigDecimal(815), + "used", + "The Chook Air 5 gives kids aged six years and older " + + "a durable and uberlight mountain bike for their first" + + " experience on tracks and easy cruising through forests" + + " and fields. The lower top tube makes it easy to mount" + + " and dismount in any situation, giving your kids greater" + + " safety on the trails." + ), + new Bicycle( + "Eva", + "Eva 291", + new BigDecimal(3400), + "used", + "The sister company to Nord, Eva launched in 2005 as the" + + " first and only women-dedicated bicycle brand. Designed" + + " by women for women, allEva bikes are optimized for the" + + " feminine physique using analytics from a body metrics" + + " database. If you like 29ers, try the Eva 291. It's a " + + "brand new bike for 2022.. This full-suspension, " + + "cross-country ride has been designed for velocity. The" + + " 291 has 100mm of front and rear travel, a superlight " + + "aluminum frame and fast-rolling 29-inch wheels. Yippee!" + ), + new Bicycle( + "Noka Bikes", + "Kahuna", + new BigDecimal(3200), + "used", + "Whether you want to try your hand at XC racing or are " + + "looking for a lively trail bike that's just as inspiring" + + " on the climbs as it is over rougher ground, the Wilder" + + " is one heck of a bike built specifically for short women." + + " Both the frames and components have been tweaked to " + + "include a women’s saddle, different bars and unique " + + "colourway." + ), + new Bicycle( + "Breakout", + "XBN 2.1 Alloy", + new BigDecimal(810), + "new", + "The XBN 2.1 Alloy is our entry-level road bike – but that’s" + + " not to say that it’s a basic machine. With an internal " + + "weld aluminium frame, a full carbon fork, and the slick-shifting" + + " Claris gears from Shimano’s, this is a bike which doesn’t" + + " break the bank and delivers craved performance." + ), + new Bicycle( + "ScramBikes", + "WattBike", + new BigDecimal(2300), + "new", + "An e-bike with a 1000W mid-drive and a 48V battery, offering over 60 miles per charge and three riding modes." + ), + new Bicycle( + "Peaknetic", + "Secto", + new BigDecimal(430), + "new", + "A lightweight aluminum commuter bike with ergonomic grips, a lumbar-supporting seat, and a low step-over frame for comfort and easy mounting." + ), + new Bicycle( + "nHill", + "Summit", + new BigDecimal(1200), + "new", + "This budget mountain bike from nHill performs well both on bike" + + " paths and on the trail. The fork with 100mm of travel absorbs" + + " rough terrain. Fat Kenda Booster tires give you grip in corners" + + " and on wet trails. The Shimano Tourney drivetrain offered enough" + + " gears for finding a comfortable pace to ride uphill, and the" + + " Tektro hydraulic disc brakes break smoothly. Whether you want an" + + " affordable bike that you can take to work, but also take trail in" + + " mountains on the weekends or you’re just after a stable," + + " comfortable ride for the bike path, the Summit gives a good value" + + " for money." + ), + new Bicycle( + "BikeShind", + "ThrillCycle", + new BigDecimal(815), + "refurbished", + "An artsy, retro-inspired bicycle that’s as functional as it is" + + " pretty: The ThrillCycle steel frame offers a smooth ride. A" + + " 9-speed drivetrain has enough gears for coasting in the city, but" + + " we wouldn’t suggest taking it to the mountains. Fenders protect" + + " you from mud, and a rear basket lets you transport groceries," + + " flowers and books. The ThrillCycle comes with a limited lifetime" + + " warranty, so this little guy will last you long past graduation." + ), + }; + + for (int i = 0; i < bicycles.length; i++) { + jedis.jsonSetWithEscape(String.format("bicycle:%d", i), bicycles[i]); + } + // STEP_END + + // STEP_START wildcard_query + Query query1 = new Query("*"); + List result1 = jedis.ftSearch("idx:bicycle", query1).getDocuments(); + System.out.println("Documents found:" + result1.size()); + // Prints: Documents found: 10 + // STEP_END + // REMOVE_START + assertEquals(10, result1.size(), "Validate total results"); + // REMOVE_END + + // STEP_START query_single_term + Query query2 = new Query("@model:Jigger"); + List result2 = jedis.ftSearch("idx:bicycle", query2).getDocuments(); + System.out.println(result2); + // Prints: [id:bicycle:0, score: 1.0, payload:null, + // properties:[$={"brand":"Velorim","model":"Jigger","price":270,"description":"Small and powerful, the Jigger is the best ride for the smallest of tikes! This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger is the vehicle of choice for the rare tenacious little rider raring to go.","condition":"new"}]] + // STEP_END + // REMOVE_START + assertEquals("bicycle:0", result2.get(0).getId(), "Validate bike id"); + // REMOVE_END + + // STEP_START query_single_term_limit_fields + Query query3 = new Query("@model:Jigger").returnFields("price"); + List result3 = jedis.ftSearch("idx:bicycle", query3).getDocuments(); + System.out.println(result3); + // Prints: [id:bicycle:0, score: 1.0, payload:null, properties:[price=270]] + // STEP_END + // REMOVE_START + assertEquals("bicycle:0", result3.get(0).getId(),"Validate cargo bike id"); + // REMOVE_END + + // STEP_START query_single_term_and_num_range + Query query4 = new Query("basic @price:[500 1000]"); + List result4 = jedis.ftSearch("idx:bicycle", query4).getDocuments(); + System.out.println(result4); + // Prints: [id:bicycle:5, score: 1.0, payload:null, + // properties:[$={"brand":"Breakout","model":"XBN 2.1 Alloy","price":810,"description":"The XBN 2.1 Alloy is our entry-level road bike – but that’s not to say that it’s a basic machine. With an internal weld aluminium frame, a full carbon fork, and the slick-shifting Claris gears from Shimano’s, this is a bike which doesn’t break the bank and delivers craved performance.","condition":"new"}]] + // STEP_END + // REMOVE_START + assertEquals("bicycle:5", result4.get(0).getId(), "Validate bike id"); + // REMOVE_END + + // STEP_START query_exact_matching + Query query5 = new Query("@brand:\"Noka Bikes\""); + List result5 = jedis.ftSearch("idx:bicycle", query5).getDocuments(); + System.out.println(result5); + // Prints: [id:bicycle:4, score: 1.0, payload:null, + // properties:[$={"brand":"Noka Bikes","model":"Kahuna","price":3200,"description":"Whether you want to try your hand at XC racing or are looking for a lively trail bike that's just as inspiring on the climbs as it is over rougher ground, the Wilder is one heck of a bike built specifically for short women. Both the frames and components have been tweaked to include a women’s saddle, different bars and unique colourway.","condition":"used"}]] + // STEP_END + // REMOVE_START + assertEquals("bicycle:4", result5.get(0).getId(), "Validate bike id"); + // REMOVE_END + + // STEP_START simple_aggregation + AggregationBuilder ab = new AggregationBuilder("*").groupBy("@condition", + Reducers.count().as("count")); + AggregationResult ar = jedis.ftAggregate("idx:bicycle", ab); + for (int i = 0; i < ar.getTotalResults(); i++) { + System.out.println(ar.getRow(i).getString("condition") + " - " + + ar.getRow(i).getString("count")); + } + // Prints: + // refurbished - 1 + // used - 4 + // new - 5 + // REMOVE_START + assertEquals(3, ar.getTotalResults(), "Validate aggregation results"); + // REMOVE_END + // STEP_END + + jedis.close(); + } +} \ No newline at end of file diff --git a/local_examples/search_quickstart/node-redis/search-quickstart.js b/local_examples/search_quickstart/node-redis/search-quickstart.js new file mode 100644 index 0000000000..e1d38ef71c --- /dev/null +++ b/local_examples/search_quickstart/node-redis/search-quickstart.js @@ -0,0 +1,230 @@ +// EXAMPLE: search_quickstart +// REMOVE_START +import assert from 'assert'; +// REMOVE_END +// HIDE_START +import { createClient, SCHEMA_FIELD_TYPE } from 'redis'; +// HIDE_END +// STEP_START connect +const client = createClient(); +client.on('error', err => console.log('Redis Client Error', err)); + +await client.connect(); +// STEP_END + +// STEP_START data_sample +const bicycle1 = { + brand: 'Velorim', + model: 'Jigger', + price: 270, + description: + 'Small and powerful, the Jigger is the best ' + + 'ride for the smallest of tikes! This is the tiniest kids\u2019 ' + + 'pedal bike on the market available without a coaster brake, the ' + + 'Jigger is the vehicle of choice for the rare tenacious little' + + 'rider raring to go.', + condition: 'new' +}; +// STEP_END +const bicycles = [ + bicycle1, + { + brand: 'Bicyk', + model: 'Hillcraft', + price: 1200, + description: 'Kids want to ride with as little weight as possible. Especially on an incline! They may be at the age when a 27.5\" wheel bike is just too clumsy coming off a 24\" bike. The Hillcraft 26 is just the solution they need!', + condition: 'used' + }, + { + brand: 'Nord', + model: 'Chook air 5', + price: 815, + description: 'The Chook Air 5 gives kids aged six years and older a durable and uberlight mountain bike for their first experience on tracks and easy cruising through forests and fields. The lower top tube makes it easy to mount and dismount in any situation, giving your kids greater safety on the trails.', + condition: 'used' + }, + { + brand: 'Eva', + model: 'Eva 291', + price: 3400, + description: 'The sister company to Nord, Eva launched in 2005 as the first and only women-dedicated bicycle brand. Designed by women for women, allEva bikes are optimized for the feminine physique using analytics from a body metrics database. If you like 29ers, try the Eva 291. It\u2019s a brand new bike for 2022.. This full-suspension, cross-country ride has been designed for velocity. The 291 has 100mm of front and rear travel, a superlight aluminum frame and fast-rolling 29-inch wheels. Yippee!', + condition: 'used' + }, + { + brand: 'Noka Bikes', + model: 'Kahuna', + price: 3200, + description: 'Whether you want to try your hand at XC racing or are looking for a lively trail bike that\'s just as inspiring on the climbs as it is over rougher ground, the Wilder is one heck of a bike built specifically for short women. Both the frames and components have been tweaked to include a women\u2019s saddle, different bars and unique colourway.', + condition: 'used' + }, + { + brand: 'Breakout', + model: 'XBN 2.1 Alloy', + price: 810, + description: 'The XBN 2.1 Alloy is our entry-level road bike \u2013 but that\u2019s not to say that it\u2019s a basic machine. With an internal weld aluminium frame, a full carbon fork, and the slick-shifting Claris gears from Shimano\u2019s, this is a bike which doesn\u2019t break the bank and delivers craved performance.', + condition: 'new' + }, + { + brand: 'ScramBikes', + model: 'WattBike', + price: 2300, + description: 'An e-bike with a 1000W mid-drive and a 48V battery, offering over 60 miles per charge and three riding modes.', + condition: 'new' + }, + { + brand: 'Peaknetic', + model: 'Secto', + price: 430, + description: 'A lightweight aluminum commuter bike with ergonomic grips, a lumbar-supporting seat, and a low step-over frame for comfort and easy mounting.', + condition: 'new' + }, + { + brand: 'nHill', + model: 'Summit', + price: 1200, + description: 'This budget mountain bike from nHill performs well both on bike paths and on the trail. The fork with 100mm of travel absorbs rough terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. The Shimano Tourney drivetrain offered enough gears for finding a comfortable pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. Whether you want an affordable bike that you can take to work, but also take trail in mountains on the weekends or you\u2019re just after a stable, comfortable ride for the bike path, the Summit gives a good value for money.', + condition: 'new' + }, + { + model: 'ThrillCycle', + brand: 'BikeShind', + price: 815, + description: 'An artsy, retro-inspired bicycle that\u2019s as functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. A 9-speed drivetrain has enough gears for coasting in the city, but we wouldn\u2019t suggest taking it to the mountains. Fenders protect you from mud, and a rear basket lets you transport groceries, flowers and books. The ThrillCycle comes with a limited lifetime warranty, so this little guy will last you long past graduation.', + condition: 'refurbished' + } +]; +// STEP_START create_index +const schema = { + '$.brand': { + type: SCHEMA_FIELD_TYPE.TEXT, + SORTABLE: true, + AS: 'brand' + }, + '$.model': { + type: SCHEMA_FIELD_TYPE.TEXT, + AS: 'model' + }, + '$.description': { + type: SCHEMA_FIELD_TYPE.TEXT, + AS: 'description' + }, + '$.price': { + type: SCHEMA_FIELD_TYPE.NUMERIC, + AS: 'price' + }, + '$.condition': { + type: SCHEMA_FIELD_TYPE.TAG, + AS: 'condition' + } +}; + +try { + await client.ft.create('idx:bicycle', schema, { + ON: 'JSON', + PREFIX: 'bicycle:' + }); +} catch (e) { + if (e.message === 'Index already exists') { + console.log('Index exists already, skipped creation.'); + } else { + // Something went wrong, perhaps RediSearch isn't installed... + console.error(e); + process.exit(1); + } +} + +await Promise.all( + bicycles.map((bicycle, i) => client.json.set(`bicycle:${i}`, '$', bicycle)) +); +// STEP_END + +// STEP_START wildcard_query +let result = await client.ft.search('idx:bicycle', '*', { + LIMIT: { + from: 0, + size: 10 + } +}); + +console.log(JSON.stringify(result, null, 2)); + +/* +{ + "total": 10, + "documents": ... +} +*/ +// STEP_END + +// REMOVE_START +// FT.SEARCH "*" order is not guaranteed; assert the full result count instead. +assert.equal(result.total, 10); +// REMOVE_END + +// STEP_START query_single_term +result = await client.ft.search( + 'idx:bicycle', + '@model:Jigger', + { + LIMIT: { + from: 0, + size: 10 + } +}); + +console.log(JSON.stringify(result, null, 2)); +/* +{ + "total": 1, + "documents": [{ + "id": "bicycle:0", + "value": { + "brand": "Velorim", + "model": "Jigger", + "price": 270, + "description": "Small and powerful, the Jigger is the best ride for the smallest of tikes! This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger is the vehicle of choice for the rare tenacious little rider raring to go.", + "condition": "new" + } + }] +} + */ +// STEP_END +// REMOVE_START +assert.equal(result.documents[0].id, 'bicycle:0'); +// REMOVE_END + +// STEP_START query_exact_matching +result = await client.ft.search( + 'idx:bicycle', + '@brand:"Noka Bikes"', + { + LIMIT: { + from: 0, + size: 10 + } + } +); + +console.log(JSON.stringify(result, null, 2)); + +/* +{ + "total": 1, + "documents": [{ + "id": "bicycle:4", + "value": { + "brand": "Noka Bikes", + "model": "Kahuna", + "price": 3200, + "description": "Whether you want to try your hand at XC racing or are looking for a lively trail bike that's just as inspiring on the climbs as it is over rougher ground, the Wilder is one heck of a bike built specifically for short women. Both the frames and components have been tweaked to include a women’s saddle, different bars and unique colourway.", + "condition": "used" + } + }] +} +*/ +// STEP_END + +// REMOVE_START +assert.equal(result.documents[0].id, 'bicycle:4'); +// REMOVE_END + +await client.close(); diff --git a/local_examples/search_quickstart/nredisstack/SearchQuickstartExample.cs b/local_examples/search_quickstart/nredisstack/SearchQuickstartExample.cs new file mode 100644 index 0000000000..73e4504a05 --- /dev/null +++ b/local_examples/search_quickstart/nredisstack/SearchQuickstartExample.cs @@ -0,0 +1,287 @@ +// EXAMPLE: search_quickstart + +using NRedisStack.RedisStackCommands; +using NRedisStack.Search; +using NRedisStack.Search.Aggregation; +using NRedisStack.Search.Literals.Enums; +using NRedisStack.Tests; +using StackExchange.Redis; + +// REMOVE_START +namespace Doc; + +[Collection("DocsTests")] +// REMOVE_END +public class SearchQuickstartExample +// REMOVE_START +: AbstractNRedisStackTest, IDisposable +// REMOVE_END +{ + // REMOVE_START + public SearchQuickstartExample(EndpointsFixture fixture) : base(fixture) { } + + [Fact] + // REMOVE_END + public void Run() + { + //REMOVE_START + // This is needed because we're constructing ConfigurationOptions in the test before calling GetConnection + SkipIfTargetConnectionDoesNotExist(EndpointsFixture.Env.Standalone); + var _ = GetCleanDatabase(EndpointsFixture.Env.Standalone); + //REMOVE_END + // STEP_START connect + var muxer = ConnectionMultiplexer.Connect("localhost:6379"); + var db = muxer.GetDatabase(); + var ft = db.FT(); + var json = db.JSON(); + // STEP_END + + // REMOVE_START + try + { + ft.DropIndex("idx:bicycle"); + } + catch + { + // ignored + } + // REMOVE_END + + // STEP_START data_sample + var bike1 = new + { + brand = "Velorim", + model = "Jigger", + price = 270M, + description = "Small and powerful, the Jigger is the best ride " + + "for the smallest of tikes! This is the tiniest " + + "kids’ pedal bike on the market available without" + + " a coaster brake, the Jigger is the vehicle of " + + "choice for the rare tenacious little rider " + + "raring to go.", + condition = "new" + }; + // STEP_END + + var bicycles = new object[] + { + bike1, + new + { + brand = "Bicyk", + model = "Hillcraft", + price = 1200M, + description = "Kids want to ride with as little weight as possible." + + " Especially on an incline! They may be at the age " + + "when a 27.5 inch wheel bike is just too clumsy coming " + + "off a 24 inch bike. The Hillcraft 26 is just the solution" + + " they need!", + condition = "used", + }, + new + { + brand = "Nord", + model = "Chook air 5", + price = 815M, + description = "The Chook Air 5 gives kids aged six years and older " + + "a durable and uberlight mountain bike for their first" + + " experience on tracks and easy cruising through forests" + + " and fields. The lower top tube makes it easy to mount" + + " and dismount in any situation, giving your kids greater" + + " safety on the trails.", + condition = "used", + }, + new + { + brand = "Eva", + model = "Eva 291", + price = 3400M, + description = "The sister company to Nord, Eva launched in 2005 as the" + + " first and only women-dedicated bicycle brand. Designed" + + " by women for women, allEva bikes are optimized for the" + + " feminine physique using analytics from a body metrics" + + " database. If you like 29ers, try the Eva 291. It’s a " + + "brand new bike for 2022.. This full-suspension, " + + "cross-country ride has been designed for velocity. The" + + " 291 has 100mm of front and rear travel, a superlight " + + "aluminum frame and fast-rolling 29-inch wheels. Yippee!", + condition = "used", + }, + new + { + brand = "Noka Bikes", + model = "Kahuna", + price = 3200M, + description = "Whether you want to try your hand at XC racing or are " + + "looking for a lively trail bike that's just as inspiring" + + " on the climbs as it is over rougher ground, the Wilder" + + " is one heck of a bike built specifically for short women." + + " Both the frames and components have been tweaked to " + + "include a women’s saddle, different bars and unique " + + "colourway.", + condition = "used", + }, + new + { + brand = "Breakout", + model = "XBN 2.1 Alloy", + price = 810M, + description = "The XBN 2.1 Alloy is our entry-level road bike – but that’s" + + " not to say that it’s a basic machine. With an internal " + + "weld aluminium frame, a full carbon fork, and the slick-shifting" + + " Claris gears from Shimano’s, this is a bike which doesn’t" + + " break the bank and delivers craved performance.", + condition = "new", + }, + new + { + brand = "ScramBikes", + model = "WattBike", + price = 2300M, + description = "An e-bike with a 1000W mid-drive and a 48V battery, offering over 60 miles per charge and three riding modes.", + condition = "new", + }, + new + { + brand = "Peaknetic", + model = "Secto", + price = 430M, + description = "A lightweight aluminum commuter bike with ergonomic grips, a lumbar-supporting seat, and a low step-over frame for comfort and easy mounting.", + condition = "new", + }, + new + { + brand = "nHill", + model = "Summit", + price = 1200M, + description = "This budget mountain bike from nHill performs well both on bike" + + " paths and on the trail. The fork with 100mm of travel absorbs" + + " rough terrain. Fat Kenda Booster tires give you grip in corners" + + " and on wet trails. The Shimano Tourney drivetrain offered enough" + + " gears for finding a comfortable pace to ride uphill, and the" + + " Tektro hydraulic disc brakes break smoothly. Whether you want an" + + " affordable bike that you can take to work, but also take trail in" + + " mountains on the weekends or you’re just after a stable," + + " comfortable ride for the bike path, the Summit gives a good value" + + " for money.", + condition = "new", + }, + new + { + model = "ThrillCycle", + brand = "BikeShind", + price = 815M, + description = "An artsy, retro-inspired bicycle that’s as functional as it is" + + " pretty: The ThrillCycle steel frame offers a smooth ride. A" + + " 9-speed drivetrain has enough gears for coasting in the city, but" + + " we wouldn’t suggest taking it to the mountains. Fenders protect" + + " you from mud, and a rear basket lets you transport groceries," + + " flowers and books. The ThrillCycle comes with a limited lifetime" + + " warranty, so this little guy will last you long past graduation.", + condition = "refurbished", + }, + }; + + // STEP_START create_index + var schema = new Schema() + .AddTextField(new FieldName("$.brand", "brand")) + .AddTextField(new FieldName("$.model", "model")) + .AddTextField(new FieldName("$.description", "description")) + .AddNumericField(new FieldName("$.price", "price")) + .AddTagField(new FieldName("$.condition", "condition")); + + ft.Create( + "idx:bicycle", + new FTCreateParams().On(IndexDataType.JSON).Prefix("bicycle:"), + schema); + + for (int i = 0; i < bicycles.Length; i++) + { + json.Set($"bicycle:{i}", "$", bicycles[i]); + } + // STEP_END + + // STEP_START wildcard_query + var query1 = new Query("*"); + var res1 = ft.Search("idx:bicycle", query1).Documents; + Console.WriteLine(string.Join("\n", res1.Count())); + // Prints: Documents found: 10 + // STEP_END + // REMOVE_START + Assert.Equal(10, res1.Count()); + // REMOVE_END + + // STEP_START query_single_term + var query2 = new Query("@model:Jigger"); + var res2 = ft.Search("idx:bicycle", query2).Documents; + Console.WriteLine(string.Join("\n", res2.Select(x => x["json"]))); + // Prints: {"brand":"Moore PLC","model":"Award Race","price":3790.76, + // "description":"This olive folding bike features a carbon frame + // and 27.5 inch wheels. This folding bike is perfect for compact + // storage and transportation.","condition":"new"} + // STEP_END + // REMOVE_START + Assert.Single(res2); + Assert.Equal("bicycle:0", res2[0].Id); + // REMOVE_END + + // STEP_START query_single_term_and_num_range + var query3 = new Query("basic @price:[500 1000]"); + var res3 = ft.Search("idx:bicycle", query3).Documents; + Console.WriteLine(string.Join("\n", res3.Select(x => x["json"]))); + // Prints: {"brand":"Moore PLC","model":"Award Race","price":3790.76, + // "description":"This olive folding bike features a carbon frame + // and 27.5 inch wheels. This folding bike is perfect for compact + // storage and transportation.","condition":"new"} + // STEP_END + // REMOVE_START + Assert.Single(res3); + Assert.Equal("bicycle:5", res3[0].Id); + // REMOVE_END + + // STEP_START query_exact_matching + var query4 = new Query("@brand:\"Noka Bikes\""); + var res4 = ft.Search("idx:bicycle", query4).Documents; + Console.WriteLine(string.Join("\n", res4.Select(x => x["json"]))); + // Prints: {"brand":"Moore PLC","model":"Award Race","price":3790.76, + // "description":"This olive folding bike features a carbon frame + // and 27.5 inch wheels. This folding bike is perfect for compact + // storage and transportation.","condition":"new"} + // STEP_END + // REMOVE_START + Assert.Single(res4); + Assert.Equal("bicycle:4", res4[0].Id); + // REMOVE_END + + // STEP_START query_single_term_limit_fields + var query5 = new Query("@model:Jigger").ReturnFields("price"); + var res5 = ft.Search("idx:bicycle", query5).Documents; + Console.WriteLine(res5.First()["price"]); + // Prints: 270 + // STEP_END + // REMOVE_START + Assert.Single(res5); + Assert.Equal("bicycle:0", res5[0].Id); + // REMOVE_END + + // STEP_START simple_aggregation + var request = new AggregationRequest("*").GroupBy( + "@condition", Reducers.Count().As("Count")); + var result = ft.Aggregate("idx:bicycle", request); + + for (var i = 0; i < result.TotalResults; i++) + { + var row = result.GetRow(i); + Console.WriteLine($"{row["condition"]} - {row["Count"]}"); + } + + // Prints: + // refurbished - 1 + // used - 4 + // new - 5 + // STEP_END + // REMOVE_START + Assert.Equal(3, result.TotalResults); + // REMOVE_END + } +} \ No newline at end of file diff --git a/local_examples/search_quickstart/redis-py/search_quickstart.py b/local_examples/search_quickstart/redis-py/search_quickstart.py new file mode 100644 index 0000000000..dd1b3022d7 --- /dev/null +++ b/local_examples/search_quickstart/redis-py/search_quickstart.py @@ -0,0 +1,403 @@ +# EXAMPLE: search_quickstart +# HIDE_START +""" +Code samples for document database quickstart pages: + https://redis.io/docs/latest/develop/get-started/document-database/ +""" + +import redis +import redis.commands.search.aggregation as aggregations +import redis.commands.search.reducers as reducers +from redis.commands.json.path import Path +from redis.commands.search.field import NumericField, TagField, TextField +from redis.commands.search.index_definition import IndexDefinition, IndexType +from redis.commands.search.query import Query + +# HIDE_END + +# STEP_START connect +r = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True) +# STEP_END +# REMOVE_START +try: + r.ft("idx:bicycle").dropindex() +except Exception: + pass +# REMOVE_END +# STEP_START data_sample +bicycle = { + "brand": "Velorim", + "model": "Jigger", + "price": 270, + "description": ( + "Small and powerful, the Jigger is the best ride " + "for the smallest of tikes! This is the tiniest " + "kids’ pedal bike on the market available without" + " a coaster brake, the Jigger is the vehicle of " + "choice for the rare tenacious little rider " + "raring to go." + ), + "condition": "new", +} +# STEP_END + +bicycles = [ + bicycle, + { + "brand": "Bicyk", + "model": "Hillcraft", + "price": 1200, + "description": ( + "Kids want to ride with as little weight as possible." + " Especially on an incline! They may be at the age " + 'when a 27.5" wheel bike is just too clumsy coming ' + 'off a 24" bike. The Hillcraft 26 is just the solution' + " they need!" + ), + "condition": "used", + }, + { + "brand": "Nord", + "model": "Chook air 5", + "price": 815, + "description": ( + "The Chook Air 5 gives kids aged six years and older " + "a durable and uberlight mountain bike for their first" + " experience on tracks and easy cruising through forests" + " and fields. The lower top tube makes it easy to mount" + " and dismount in any situation, giving your kids greater" + " safety on the trails." + ), + "condition": "used", + }, + { + "brand": "Eva", + "model": "Eva 291", + "price": 3400, + "description": ( + "The sister company to Nord, Eva launched in 2005 as the" + " first and only women-dedicated bicycle brand. Designed" + " by women for women, allEva bikes are optimized for the" + " feminine physique using analytics from a body metrics" + " database. If you like 29ers, try the Eva 291. It’s a " + "brand new bike for 2022.. This full-suspension, " + "cross-country ride has been designed for velocity. The" + " 291 has 100mm of front and rear travel, a superlight " + "aluminum frame and fast-rolling 29-inch wheels. Yippee!" + ), + "condition": "used", + }, + { + "brand": "Noka Bikes", + "model": "Kahuna", + "price": 3200, + "description": ( + "Whether you want to try your hand at XC racing or are " + "looking for a lively trail bike that's just as inspiring" + " on the climbs as it is over rougher ground, the Wilder" + " is one heck of a bike built specifically for short women." + " Both the frames and components have been tweaked to " + "include a women’s saddle, different bars and unique " + "colourway." + ), + "condition": "used", + }, + { + "brand": "Breakout", + "model": "XBN 2.1 Alloy", + "price": 810, + "description": ( + "The XBN 2.1 Alloy is our entry-level road bike – but that’s" + " not to say that it’s a basic machine. With an internal " + "weld aluminium frame, a full carbon fork, and the slick-shifting" + " Claris gears from Shimano’s, this is a bike which doesn’t" + " break the bank and delivers craved performance." + ), + "condition": "new", + }, + { + "brand": "ScramBikes", + "model": "WattBike", + "price": 2300, + "description": ( + "An e-bike with a 1000W mid-drive and a 48V battery, offering over 60 miles per charge and three riding modes." + ), + "condition": "new", + }, + { + "brand": "Peaknetic", + "model": "Secto", + "price": 430, + "description": ( + "A lightweight aluminum commuter bike with ergonomic grips, a lumbar-supporting seat, and a low step-over frame for comfort and easy mounting." + ), + "condition": "new", + }, + { + "brand": "nHill", + "model": "Summit", + "price": 1200, + "description": ( + "This budget mountain bike from nHill performs well both on bike" + " paths and on the trail. The fork with 100mm of travel absorbs" + " rough terrain. Fat Kenda Booster tires give you grip in corners" + " and on wet trails. The Shimano Tourney drivetrain offered enough" + " gears for finding a comfortable pace to ride uphill, and the" + " Tektro hydraulic disc brakes break smoothly. Whether you want an" + " affordable bike that you can take to work, but also take trail in" + " mountains on the weekends or you’re just after a stable," + " comfortable ride for the bike path, the Summit gives a good value" + " for money." + ), + "condition": "new", + }, + { + "model": "ThrillCycle", + "brand": "BikeShind", + "price": 815, + "description": ( + "An artsy, retro-inspired bicycle that’s as functional as it is" + " pretty: The ThrillCycle steel frame offers a smooth ride. A" + " 9-speed drivetrain has enough gears for coasting in the city, but" + " we wouldn’t suggest taking it to the mountains. Fenders protect" + " you from mud, and a rear basket lets you transport groceries," + " flowers and books. The ThrillCycle comes with a limited lifetime" + " warranty, so this little guy will last you long past graduation." + ), + "condition": "refurbished", + }, +] + +# STEP_START create_index +schema = ( + TextField("$.brand", as_name="brand"), + TextField("$.model", as_name="model"), + TextField("$.description", as_name="description"), + NumericField("$.price", as_name="price"), + TagField("$.condition", as_name="condition"), +) + +index = r.ft("idx:bicycle") +index.create_index( + schema, + definition=IndexDefinition(prefix=["bicycle:"], index_type=IndexType.JSON), +) +for bid, bicycle in enumerate(bicycles): + r.json().set(f"bicycle:{bid}", Path.root_path(), bicycle) +# STEP_END + + +# STEP_START wildcard_query +res = index.search(Query("*")) +print("Documents found:", res.total) +# >>> Documents found: 10 +# STEP_END +# REMOVE_START +assert res.total == 10 +# REMOVE_END + +# STEP_START query_single_term +res = index.search(Query("@model:Jigger")) +print(res) +# >>> Result{1 total, docs: [ +# Document { +# 'id': 'bicycle:0', +# 'payload': None, +# 'json': '{ +# "brand":"Velorim", +# "model":"Jigger", +# "price":270, +# ... +# "condition":"new" +# }' +# }]} +# STEP_END +# REMOVE_START +assert res.docs[0].id == "bicycle:0" +# REMOVE_END + +# STEP_START query_single_term_limit_fields +res = index.search(Query("@model:Jigger").return_field("$.price", as_field="price")) +print(res) +# >>> [Document {'id': 'bicycle:0', 'payload': None, 'price': '270'}] +# STEP_END +# REMOVE_START +assert res.docs[0].id == "bicycle:0" +# REMOVE_END + +# STEP_START query_single_term_and_num_range +res = index.search(Query("basic @price:[500 1000]")) +print(res) +# >>> Result{1 total, docs: [ +# Document { +# 'id': 'bicycle:5', +# 'payload': None, +# 'json': '{ +# "brand":"Breakout", +# "model":"XBN 2.1 Alloy", +# "price":810, +# ... +# "condition":"new" +# }' +# }]} +# STEP_END +# REMOVE_START +assert res.docs[0].id == "bicycle:5" +# REMOVE_END + +# STEP_START query_exact_matching +res = index.search(Query('@brand:"Noka Bikes"')) +print(res) +# >>> Result{1 total, docs: [ +# Document { +# 'id': 'bicycle:4', +# 'payload': None, +# 'json': '{ +# "brand":"Noka Bikes", +# "model":"Kahuna", +# "price":3200, +# ... +# "condition":"used" +# }' +# }]} +# STEP_END +# REMOVE_START +assert res.docs[0].id == "bicycle:4" +# REMOVE_END + +# STEP_START query_fuzzy_matching +res = index.search( + Query("@description:%analitics%").dialect( # Note the typo in the word "analytics" + 2 + ) +) +print(res) +# >>> Result{1 total, docs: [ +# Document { +# 'id': 'bicycle:3', +# 'payload': None, +# 'json': '{ +# "brand":"Eva", +# "model":"Eva 291", +# "price":3400, +# "description":"...using analytics from a body metrics database...", +# "condition":"used" +# }' +# }]} +# STEP_END +# REMOVE_START +assert res.docs[0].id == "bicycle:3" +# REMOVE_END + +# STEP_START query_fuzzy_matching_level2 +res = index.search( + Query("@description:%%analitycs%%").dialect( # Note 2 typos in the word "analytics" + 2 + ) +) +print(res) +# >>> Result{1 total, docs: [ +# Document { +# 'id': 'bicycle:3', +# 'payload': None, +# 'json': '{ +# "brand":"Eva", +# "model":"Eva 291", +# "price":3400, +# "description":"...using analytics from a body metrics database...", +# "condition":"used" +# }' +# }]} +# STEP_END +# REMOVE_START +assert res.docs[0].id == "bicycle:3" +# REMOVE_END + +# STEP_START query_prefix_matching +res = index.search(Query("@model:hill*")) +print(res) +# >>> Result{1 total, docs: [ +# Document { +# 'id': 'bicycle:1', +# 'payload': None, +# 'json': '{ +# "brand":"Bicyk", +# "model":"Hillcraft", +# "price":1200, +# ... +# "condition":"used" +# }' +# }]} +# STEP_END +# REMOVE_START +assert res.docs[0].id == "bicycle:1" +# REMOVE_END + +# STEP_START query_suffix_matching +res = index.search(Query("@model:*bike")) +print(res) +# >>> Result{1 total, docs: [ +# Document { +# 'id': 'bicycle:6', +# 'payload': None, +# 'json': '{ +# "brand":"ScramBikes", +# "model":"WattBike", +# "price":2300, +# ... +# "condition":"new" +# }' +# }]} +# STEP_END +# REMOVE_START +assert res.docs[0].id == "bicycle:6" +# REMOVE_END + +# STEP_START query_wildcard_matching +res = index.search(Query("w'H?*craft'").dialect(2)) +print(res.docs[0].json) +# >>> { +# "brand":"Bicyk", +# "model":"Hillcraft", +# "price":1200, +# ... +# "condition":"used" +# } +# STEP_END +# REMOVE_START +assert res.docs[0].id == "bicycle:1" +# REMOVE_END + + +# STEP_START query_with_default_scorer +res = index.search(Query("mountain").with_scores()) +for sr in res.docs: + print(f"{sr.id}: score={sr.score}") +# STEP_END +# REMOVE_START +assert res.total == 3 +# REMOVE_END + +# STEP_START query_with_bm25_scorer +res = index.search(Query("mountain").with_scores().scorer("BM25")) +for sr in res.docs: + print(f"{sr.id}: score={sr.score}") +# STEP_END +# REMOVE_START +assert res.total == 3 +assert res.docs[0].score == res.docs[1].score +# REMOVE_END + +# STEP_START simple_aggregation +req = aggregations.AggregateRequest("*").group_by( + "@condition", reducers.count().alias("count") +) +res = index.aggregate(req).rows +print(res) +# >>> [['condition', 'refurbished', 'count', '1'], +# ['condition', 'used', 'count', '4'], +# ['condition', 'new', 'count', '5']] +# STEP_END +# REMOVE_START +assert len(res) == 3 +# REMOVE_END diff --git a/local_examples/time_series_tutorial/go-redis/timeseries_tut_test.go b/local_examples/time_series_tutorial/go-redis/timeseries_tut_test.go index 1045264e8c..317721bb54 100644 --- a/local_examples/time_series_tutorial/go-redis/timeseries_tut_test.go +++ b/local_examples/time_series_tutorial/go-redis/timeseries_tut_test.go @@ -133,6 +133,11 @@ func ExampleClient_timeseries_add() { // REMOVE_END // STEP_START madd + // Recreate the thermometer series so this example runs on its own. + rdb.Del(ctx, "thermometer:1", "thermometer:2") + rdb.TSCreate(ctx, "thermometer:1") + rdb.TSAdd(ctx, "thermometer:2", 1, 10.8) + res1, err := rdb.TSMAdd(ctx, [][]interface{}{ {"thermometer:1", 1, 9.2}, {"thermometer:1", 2, 9.9}, @@ -146,6 +151,10 @@ func ExampleClient_timeseries_add() { // STEP_END // STEP_START get + // Recreate the thermometer:2 series so this example runs on its own. + rdb.Del(ctx, "thermometer:2") + rdb.TSAdd(ctx, "thermometer:2", 1, 10.8) + rdb.TSAdd(ctx, "thermometer:2", 2, 10.3) // The last recorded temperature for thermometer:2 // was 10.3 at time 2. res2, err := rdb.TSGet(ctx, "thermometer:2").Result() @@ -249,6 +258,17 @@ func ExampleClient_timeseries_range() { // STEP_END // STEP_START range_filter + // Recreate the rg:1 series so this example runs on its own. + rdb.Del(ctx, "rg:1") + rdb.TSCreate(ctx, "rg:1") + rdb.TSMAdd(ctx, [][]interface{}{ + {"rg:1", 0, 18}, + {"rg:1", 1, 14}, + {"rg:1", 2, 22}, + {"rg:1", 3, 18}, + {"rg:1", 4, 24}, + }) + res8, err := rdb.TSRangeWithArgs( ctx, "rg:1", @@ -651,6 +671,17 @@ func ExampleClient_timeseries_aggregation() { } // STEP_START agg + // Recreate the rg:2 series so this example runs on its own. + rdb.Del(ctx, "rg:2") + rdb.TSCreate(ctx, "rg:2") + rdb.TSMAdd(ctx, [][]interface{}{ + {"rg:2", 0, 1.8}, + {"rg:2", 1, 2.1}, + {"rg:2", 2, 2.3}, + {"rg:2", 3, 1.9}, + {"rg:2", 4, 1.78}, + }) + res32, err := rdb.TSRangeWithArgs( ctx, "rg:2", @@ -749,6 +780,19 @@ func ExampleClient_timeseries_agg_bucket() { // STEP_END // STEP_START agg_align + // Recreate the sensor3 series so this example runs on its own. + rdb.Del(ctx, "sensor3") + rdb.TSCreate(ctx, "sensor3") + rdb.TSMAdd(ctx, [][]interface{}{ + {"sensor3", 10, 1000}, + {"sensor3", 20, 2000}, + {"sensor3", 30, 3000}, + {"sensor3", 40, 4000}, + {"sensor3", 50, 5000}, + {"sensor3", 60, 6000}, + {"sensor3", 70, 7000}, + }) + res4, err := rdb.TSRangeWithArgs( ctx, "sensor3", @@ -1040,6 +1084,12 @@ func ExampleClient_timeseries_compaction() { // STEP_END // STEP_START comp_add + // Recreate the compaction rule so this example runs on its own. + rdb.Del(ctx, "hyg:1", "hyg:compacted") + rdb.TSCreate(ctx, "hyg:1") + rdb.TSCreate(ctx, "hyg:compacted") + rdb.TSCreateRule(ctx, "hyg:1", "hyg:compacted", redis.Min, 3) + res50, err := rdb.TSMAdd(ctx, [][]interface{}{ {"hyg:1", 0, 75}, {"hyg:1", 1, 77}, @@ -1111,6 +1161,11 @@ func ExampleClient_timeseries_delete() { // REMOVE_END // STEP_START del + // Recreate the thermometer:1 series so this example runs on its own. + rdb.Del(ctx, "thermometer:1") + rdb.TSAdd(ctx, "thermometer:1", 1, 9.2) + rdb.TSAdd(ctx, "thermometer:1", 2, 9.9) + res54, err := rdb.TSInfo(ctx, "thermometer:1").Result() if err != nil { panic(err) diff --git a/local_examples/time_series_tutorial/jedis/TimeSeriesTutorialExample.java b/local_examples/time_series_tutorial/jedis/TimeSeriesTutorialExample.java new file mode 100644 index 0000000000..6349154166 --- /dev/null +++ b/local_examples/time_series_tutorial/jedis/TimeSeriesTutorialExample.java @@ -0,0 +1,717 @@ +// EXAMPLE: time_series_tutorial +// REMOVE_START +package io.redis.examples; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +// REMOVE_END +import redis.clients.jedis.RedisClient; +import redis.clients.jedis.timeseries.*; +import redis.clients.jedis.timeseries.TSElement; + +import java.util.*; + +public class TimeSeriesTutorialExample { + + @Test + public void run() { + RedisClient jedis = RedisClient.create("redis://localhost:6379"); + // REMOVE_START + // Clear any keys before using them in tests + jedis.del( + "thermometer:1", "thermometer:2", "thermometer:3", + "rg:1", "rg:2", "rg:3", "rg:4", + "sensor3", + "wind:1", "wind:2", "wind:3", "wind:4", + "hyg:1", "hyg:compacted" + ); + // REMOVE_END + + // STEP_START create + String res1 = jedis.tsCreate("thermometer:1"); + System.out.println(res1); // >>> OK + + String res2 = jedis.type("thermometer:1"); + System.out.println(res2); // >>> TSDB-TYPE + + TSInfo res3 = jedis.tsInfo("thermometer:1"); + System.out.println(res3.getProperty("totalSamples")); // >>> 0 + // STEP_END + // REMOVE_START + assertEquals("OK", res1); + assertEquals("TSDB-TYPE", res2); + assertEquals((Long) 0L, res3.getProperty("totalSamples")); + // REMOVE_END + + // STEP_START create_retention + long res4 = jedis.tsAdd("thermometer:2", 1L, 10.8, + TSCreateParams.createParams().retention(100)); + System.out.println(res4); // >>> 1 + + TSInfo res5 = jedis.tsInfo("thermometer:2"); + System.out.println(res5.getProperty("retentionTime")); // >>> 100 + // STEP_END + // REMOVE_START + assertEquals(1L, res4); + assertEquals((Long) 100L, res5.getProperty("retentionTime")); + // REMOVE_END + + // STEP_START create_labels + Map labels = new HashMap<>(); + labels.put("location", "UK"); + labels.put("type", "Mercury"); + + long res6 = jedis.tsAdd("thermometer:3", 1L, 10.4, + TSCreateParams.createParams().labels(labels)); + System.out.println(res6); // >>> 1 + + TSInfo res7 = jedis.tsInfo("thermometer:3"); + System.out.println("Labels: " + res7.getLabels()); + // >>> Labels: {location=UK, type=Mercury} + // STEP_END + // REMOVE_START + assertEquals(1L, res6); + assertEquals(labels, res7.getLabels()); + // REMOVE_END + + // STEP_START madd + // Recreate the thermometer series so this example runs on its own. + jedis.del("thermometer:1", "thermometer:2"); + jedis.tsCreate("thermometer:1"); + jedis.tsAdd("thermometer:2", 1L, 10.8); + + List res8 = jedis.tsMAdd( + new AbstractMap.SimpleEntry<>("thermometer:1", new TSElement(1L, 9.2)), + new AbstractMap.SimpleEntry<>("thermometer:1", new TSElement(2L, 9.9)), + new AbstractMap.SimpleEntry<>("thermometer:2", new TSElement(2L, 10.3)) + ); + System.out.println(res8); // >>> [1, 2, 2] + // STEP_END + // REMOVE_START + assertEquals(Arrays.asList(1L, 2L, 2L), res8); + // REMOVE_END + + // STEP_START get + // Recreate the thermometer:2 series so this example runs on its own. + jedis.del("thermometer:2"); + jedis.tsAdd("thermometer:2", 1L, 10.8); + jedis.tsAdd("thermometer:2", 2L, 10.3); + // The last recorded temperature for thermometer:2 + // was 10.3 at time 2. + TSElement res9 = jedis.tsGet("thermometer:2"); + System.out.println("(" + res9.getTimestamp() + ", " + res9.getValue() + ")"); + // >>> (2, 10.3) + // STEP_END + // REMOVE_START + assertEquals(2L, res9.getTimestamp()); + assertEquals(10.3, res9.getValue(), 0.001); + // REMOVE_END + + // STEP_START range + // Add 5 data points to a time series named "rg:1" + String res10 = jedis.tsCreate("rg:1"); + System.out.println(res10); // >>> OK + + List res11 = jedis.tsMAdd( + new AbstractMap.SimpleEntry<>("rg:1", new TSElement(0L, 18.0)), + new AbstractMap.SimpleEntry<>("rg:1", new TSElement(1L, 14.0)), + new AbstractMap.SimpleEntry<>("rg:1", new TSElement(2L, 22.0)), + new AbstractMap.SimpleEntry<>("rg:1", new TSElement(3L, 18.0)), + new AbstractMap.SimpleEntry<>("rg:1", new TSElement(4L, 24.0)) + ); + System.out.println(res11); // >>> [0, 1, 2, 3, 4] + + // Retrieve all the data points in ascending order + List res12 = jedis.tsRange("rg:1", 0L, 4L); + System.out.println(res12); + // >>> [(0:18.0), (1:14.0), (2:22.0), (3:18.0), (4:24.0)] + + // Retrieve data points up to time 1 (inclusive) + List res13 = jedis.tsRange("rg:1", 0L, 1L); + System.out.println(res13); + // >>> [(0:18.0), (1:14.0)] + + // Retrieve data points from time 3 onwards + List res14 = jedis.tsRange("rg:1", 3L, 4L); + System.out.println(res14); + // >>> [(3:18.0), (4:24.0)] + + // Retrieve all the data points in descending order + List res15 = jedis.tsRevRange("rg:1", 0L, 4L); + System.out.println(res15); + // >>> [(4:24.0), (3:18.0), (2:22.0), (1:14.0), (0:18.0)] + + // Retrieve data points up to time 1 (inclusive), in descending order + List res16 = jedis.tsRevRange("rg:1", 0L, 1L); + System.out.println(res16); + // >>> [(1:14.0), (0:18.0)] + // STEP_END + // REMOVE_START + assertEquals("OK", res10); + assertEquals(Arrays.asList(0L, 1L, 2L, 3L, 4L), res11); + assertEquals(Arrays.asList( + new TSElement(0L, 18.0), new TSElement(1L, 14.0), new TSElement(2L, 22.0), + new TSElement(3L, 18.0), new TSElement(4L, 24.0)), res12); + assertEquals(Arrays.asList(new TSElement(0L, 18.0), new TSElement(1L, 14.0)), res13); + assertEquals(Arrays.asList(new TSElement(3L, 18.0), new TSElement(4L, 24.0)), res14); + assertEquals(Arrays.asList( + new TSElement(4L, 24.0), new TSElement(3L, 18.0), new TSElement(2L, 22.0), + new TSElement(1L, 14.0), new TSElement(0L, 18.0)), res15); + assertEquals(Arrays.asList(new TSElement(1L, 14.0), new TSElement(0L, 18.0)), res16); + // REMOVE_END + + // STEP_START range_filter + // Recreate the rg:1 series so this example runs on its own. + jedis.del("rg:1"); + jedis.tsCreate("rg:1"); + jedis.tsMAdd( + new AbstractMap.SimpleEntry<>("rg:1", new TSElement(0L, 18.0)), + new AbstractMap.SimpleEntry<>("rg:1", new TSElement(1L, 14.0)), + new AbstractMap.SimpleEntry<>("rg:1", new TSElement(2L, 22.0)), + new AbstractMap.SimpleEntry<>("rg:1", new TSElement(3L, 18.0)), + new AbstractMap.SimpleEntry<>("rg:1", new TSElement(4L, 24.0)) + ); + + List res17 = jedis.tsRange("rg:1", + TSRangeParams.rangeParams() + .fromTimestamp(0L) + .toTimestamp(4L) + .filterByTS(0L, 2L, 4L) + ); + System.out.println(res17); + // >>> [(0:18.0), (2:22.0), (4:24.0)] + + List res18 = jedis.tsRevRange("rg:1", + TSRangeParams.rangeParams() + .fromTimestamp(0L) + .toTimestamp(4L) + .filterByTS(0L, 2L, 4L) + .filterByValues(20.0, 25.0) + ); + System.out.println(res18); + // >>> [(4:24.0), (2:22.0)] + + List res19 = jedis.tsRevRange("rg:1", + TSRangeParams.rangeParams() + .fromTimestamp(0L) + .toTimestamp(4L) + .filterByTS(0L, 2L, 4L) + .filterByValues(22.0, 22.0) + .count(1) + ); + System.out.println(res19); + // >>> [(2:22.0)] + // STEP_END + // REMOVE_START + assertEquals(Arrays.asList( + new TSElement(0L, 18.0), new TSElement(2L, 22.0), new TSElement(4L, 24.0)), res17); + assertEquals(Arrays.asList(new TSElement(4L, 24.0), new TSElement(2L, 22.0)), res18); + assertEquals(Arrays.asList(new TSElement(2L, 22.0)), res19); + // REMOVE_END + + // STEP_START query_multi + // Create three new "rg:" time series (two in the US + // and one in the UK, with different units) and add some + // data points. + Map usLabels1 = new HashMap<>(); + usLabels1.put("location", "us"); + usLabels1.put("unit", "cm"); + + Map usLabels2 = new HashMap<>(); + usLabels2.put("location", "us"); + usLabels2.put("unit", "in"); + + Map ukLabels = new HashMap<>(); + ukLabels.put("location", "uk"); + ukLabels.put("unit", "mm"); + + String res20 = jedis.tsCreate("rg:2", + TSCreateParams.createParams().labels(usLabels1)); + System.out.println(res20); // >>> OK + + String res21 = jedis.tsCreate("rg:3", + TSCreateParams.createParams().labels(usLabels2)); + System.out.println(res21); // >>> OK + + String res22 = jedis.tsCreate("rg:4", + TSCreateParams.createParams().labels(ukLabels)); + System.out.println(res22); // >>> OK + + List res23 = jedis.tsMAdd( + new AbstractMap.SimpleEntry<>("rg:2", new TSElement(0L, 1.8)), + new AbstractMap.SimpleEntry<>("rg:3", new TSElement(0L, 0.9)), + new AbstractMap.SimpleEntry<>("rg:4", new TSElement(0L, 25.0)) + ); + System.out.println(res23); // >>> [0, 0, 0] + + List res24 = jedis.tsMAdd( + new AbstractMap.SimpleEntry<>("rg:2", new TSElement(1L, 2.1)), + new AbstractMap.SimpleEntry<>("rg:3", new TSElement(1L, 0.77)), + new AbstractMap.SimpleEntry<>("rg:4", new TSElement(1L, 18.0)) + ); + System.out.println(res24); // >>> [1, 1, 1] + + List res25 = jedis.tsMAdd( + new AbstractMap.SimpleEntry<>("rg:2", new TSElement(2L, 2.3)), + new AbstractMap.SimpleEntry<>("rg:3", new TSElement(2L, 1.1)), + new AbstractMap.SimpleEntry<>("rg:4", new TSElement(2L, 21.0)) + ); + System.out.println(res25); // >>> [2, 2, 2] + + List res26 = jedis.tsMAdd( + new AbstractMap.SimpleEntry<>("rg:2", new TSElement(3L, 1.9)), + new AbstractMap.SimpleEntry<>("rg:3", new TSElement(3L, 0.81)), + new AbstractMap.SimpleEntry<>("rg:4", new TSElement(3L, 19.0)) + ); + System.out.println(res26); // >>> [3, 3, 3] + + List res27 = jedis.tsMAdd( + new AbstractMap.SimpleEntry<>("rg:2", new TSElement(4L, 1.78)), + new AbstractMap.SimpleEntry<>("rg:3", new TSElement(4L, 0.74)), + new AbstractMap.SimpleEntry<>("rg:4", new TSElement(4L, 23.0)) + ); + System.out.println(res27); // >>> [4, 4, 4] + + // Retrieve the last data point from each US time series. + Map res28 = jedis.tsMGet( + TSMGetParams.multiGetParams().latest(), + "location=us" + ); + System.out.println(res28); + // >>> {rg:2=TSMGetElement{key=rg:2, labels={}, element=(4:1.78)}... + + // Retrieve the same data points, but include the `unit` + // label in the results. + Map res29 = jedis.tsMGet( + TSMGetParams.multiGetParams().selectedLabels("unit"), + "location=us" + ); + System.out.println(res29); + // >>> {rg:2=TSMGetElement{key=rg:2, labels={unit=cm}, element=(4:1.78)}... + + // Retrieve data points up to time 2 (inclusive) from all + // time series that use millimeters as the unit. Include all + // labels in the results. + Map res30 = jedis.tsMRange( + TSMRangeParams.multiRangeParams(0L, 2L) + .withLabels() + .filter("unit=mm") + ); + System.out.println(res30); + // >>> {rg:4=TSMRangeElements{key=rg:4, labels={location=uk, unit=mm}, value=[(0:25.0), (1:18.0), (2:21.0)]}} + + // Retrieve data points from time 1 to time 3 (inclusive) from + // all time series that use centimeters or millimeters as the unit, + // but only return the `location` label. Return the results + // in descending order of timestamp. + Map res31 = jedis.tsMRevRange( + TSMRangeParams.multiRangeParams(1L, 3L) + .selectedLabels("location") + .filter("unit=(cm,mm)") + ); + System.out.println(res31); + // >>> {rg:2=TSMRangeElements{key=rg:2, labels={location=us, unit=cm}, value=[(1:2.1)... + // STEP_END + // REMOVE_START + assertEquals("OK", res20); + assertEquals("OK", res21); + assertEquals("OK", res22); + assertEquals(Arrays.asList(0L, 0L, 0L), res23); + assertEquals(Arrays.asList(1L, 1L, 1L), res24); + assertEquals(Arrays.asList(2L, 2L, 2L), res25); + assertEquals(Arrays.asList(3L, 3L, 3L), res26); + assertEquals(Arrays.asList(4L, 4L, 4L), res27); + assertEquals(2, res28.size()); + + assertTrue(res28.containsKey("rg:2")); + TSMGetElement res28rg2 = res28.get("rg:2"); + assertEquals("rg:2", res28rg2.getKey()); + assertEquals(0, res28rg2.getLabels().size()); + assertEquals(4L, res28rg2.getElement().getTimestamp()); + assertEquals(1.78, res28rg2.getElement().getValue(), 0.001); + + assertTrue(res28.containsKey("rg:3")); + TSMGetElement res28rg3 = res28.get("rg:3"); + assertEquals("rg:3", res28rg3.getKey()); + assertEquals(0, res28rg3.getLabels().size()); + assertEquals(4L, res28rg3.getElement().getTimestamp()); + assertEquals(0.74, res28rg3.getElement().getValue(), 0.001); + + assertEquals(2, res29.size()); + assertTrue(res29.containsKey("rg:2")); + TSMGetElement res29rg2 = res29.get("rg:2"); + assertEquals("rg:2", res29rg2.getKey()); + assertEquals(1, res29rg2.getLabels().size()); + assertEquals("cm", res29rg2.getLabels().get("unit")); + assertEquals(4L, res29rg2.getElement().getTimestamp()); + assertEquals(1.78, res29rg2.getElement().getValue(), 0.001); + + assertEquals(1, res30.size()); + assertTrue(res30.containsKey("rg:4")); + TSMRangeElements res30rg4 = res30.get("rg:4"); + assertEquals("rg:4", res30rg4.getKey()); + assertEquals(2, res30rg4.getLabels().size()); + assertEquals("uk", res30rg4.getLabels().get("location")); + assertEquals("mm", res30rg4.getLabels().get("unit")); + assertEquals(3, res30rg4.getElements().size()); + assertEquals(0L, res30rg4.getElements().get(0).getTimestamp()); + assertEquals(25.0, res30rg4.getElements().get(0).getValue(), 0.001); + assertEquals(1L, res30rg4.getElements().get(1).getTimestamp()); + assertEquals(18.0, res30rg4.getElements().get(1).getValue(), 0.001); + assertEquals(2L, res30rg4.getElements().get(2).getTimestamp()); + assertEquals(21.0, res30rg4.getElements().get(2).getValue(), 0.001); + + assertEquals(2, res31.size()); + assertTrue(res31.containsKey("rg:2")); + TSMRangeElements res31rg2 = res31.get("rg:2"); + assertEquals("rg:2", res31rg2.getKey()); + assertEquals(1, res31rg2.getLabels().size()); + assertEquals("us", res31rg2.getLabels().get("location")); + assertEquals(3, res31rg2.getElements().size()); + assertEquals(3L, res31rg2.getElements().get(0).getTimestamp()); + assertEquals(1.9, res31rg2.getElements().get(0).getValue(), 0.001); + assertEquals(2L, res31rg2.getElements().get(1).getTimestamp()); + assertEquals(2.3, res31rg2.getElements().get(1).getValue(), 0.001); + assertEquals(1L, res31rg2.getElements().get(2).getTimestamp()); + assertEquals(2.1, res31rg2.getElements().get(2).getValue(), 0.001); + + // REMOVE_END + + // STEP_START agg + // Recreate the rg:2 series so this example runs on its own. + jedis.del("rg:2"); + jedis.tsCreate("rg:2"); + jedis.tsMAdd( + new AbstractMap.SimpleEntry<>("rg:2", new TSElement(0L, 1.8)), + new AbstractMap.SimpleEntry<>("rg:2", new TSElement(1L, 2.1)), + new AbstractMap.SimpleEntry<>("rg:2", new TSElement(2L, 2.3)), + new AbstractMap.SimpleEntry<>("rg:2", new TSElement(3L, 1.9)), + new AbstractMap.SimpleEntry<>("rg:2", new TSElement(4L, 1.78)) + ); + + List res32 = jedis.tsRange("rg:2", + TSRangeParams.rangeParams() + .fromTimestamp(0L) + .toTimestamp(4L) + .aggregation(AggregationType.AVG, 2) + ); + System.out.println(res32); + // >>> [(0:1.9500000000000002), (2:2.0999999999999996), (4:1.78)] + // STEP_END + // REMOVE_START + assertEquals( + Arrays.asList( + new TSElement(0L, 1.9500000000000002), + new TSElement(2L, 2.0999999999999996), + new TSElement(4L, 1.78) + ), + res32 + ); + // REMOVE_END + + // STEP_START agg_bucket + String res33 = jedis.tsCreate("sensor3"); + System.out.println(res33); // >>> OK + + List res34 = jedis.tsMAdd( + new AbstractMap.SimpleEntry<>("sensor3", new TSElement(10L, 1000.0)), + new AbstractMap.SimpleEntry<>("sensor3", new TSElement(20L, 2000.0)), + new AbstractMap.SimpleEntry<>("sensor3", new TSElement(30L, 3000.0)), + new AbstractMap.SimpleEntry<>("sensor3", new TSElement(40L, 4000.0)), + new AbstractMap.SimpleEntry<>("sensor3", new TSElement(50L, 5000.0)), + new AbstractMap.SimpleEntry<>("sensor3", new TSElement(60L, 6000.0)), + new AbstractMap.SimpleEntry<>("sensor3", new TSElement(70L, 7000.0)) + ); + System.out.println(res34); // >>> [10, 20, 30, 40, 50, 60, 70] + + List res35 = jedis.tsRange("sensor3", + TSRangeParams.rangeParams() + .fromTimestamp(10L) + .toTimestamp(70L) + .aggregation(AggregationType.MIN, 25) + ); + System.out.println(res35); + // >>> [(0:1000.0), (25:3000.0), (50:5000.0)] + // STEP_END + // REMOVE_START + assertEquals("OK", res33); + assertEquals(Arrays.asList(10L, 20L, 30L, 40L, 50L, 60L, 70L), res34); + assertEquals( + Arrays.asList( + new TSElement(0L, 1000.0), + new TSElement(25L, 3000.0), + new TSElement(50L, 5000.0) + ), + res35 + ); + // REMOVE_END + + // STEP_START agg_align + // Recreate the sensor3 series so this example runs on its own. + jedis.del("sensor3"); + jedis.tsCreate("sensor3"); + jedis.tsMAdd( + new AbstractMap.SimpleEntry<>("sensor3", new TSElement(10L, 1000.0)), + new AbstractMap.SimpleEntry<>("sensor3", new TSElement(20L, 2000.0)), + new AbstractMap.SimpleEntry<>("sensor3", new TSElement(30L, 3000.0)), + new AbstractMap.SimpleEntry<>("sensor3", new TSElement(40L, 4000.0)), + new AbstractMap.SimpleEntry<>("sensor3", new TSElement(50L, 5000.0)), + new AbstractMap.SimpleEntry<>("sensor3", new TSElement(60L, 6000.0)), + new AbstractMap.SimpleEntry<>("sensor3", new TSElement(70L, 7000.0)) + ); + + List res36 = jedis.tsRange("sensor3", + TSRangeParams.rangeParams() + .fromTimestamp(10L) + .toTimestamp(70L) + .aggregation(AggregationType.MIN, 25) + .alignStart() + ); + System.out.println(res36); + // >>> [(10:1000.0), (35:4000.0), (60:6000.0)] + // STEP_END + // REMOVE_START + assertEquals( + Arrays.asList( + new TSElement(10L, 1000.0), + new TSElement(35L, 4000.0), + new TSElement(60L, 6000.0) + ), + res36 + ); + // REMOVE_END + + // STEP_START agg_multi + Map ukCountry = new HashMap<>(); + ukCountry.put("country", "uk"); + + Map usCountry = new HashMap<>(); + usCountry.put("country", "us"); + + jedis.tsCreate("wind:1", TSCreateParams.createParams().labels(ukCountry)); + jedis.tsCreate("wind:2", TSCreateParams.createParams().labels(ukCountry)); + jedis.tsCreate("wind:3", TSCreateParams.createParams().labels(usCountry)); + jedis.tsCreate("wind:4", TSCreateParams.createParams().labels(usCountry)); + + jedis.tsMAdd( + new AbstractMap.SimpleEntry<>("wind:1", new TSElement(0L, 10.0)), + new AbstractMap.SimpleEntry<>("wind:2", new TSElement(0L, 12.0)), + new AbstractMap.SimpleEntry<>("wind:3", new TSElement(0L, 8.0)), + new AbstractMap.SimpleEntry<>("wind:4", new TSElement(0L, 15.0)) + ); + + jedis.tsMAdd( + new AbstractMap.SimpleEntry<>("wind:1", new TSElement(1L, 11.0)), + new AbstractMap.SimpleEntry<>("wind:2", new TSElement(1L, 13.0)), + new AbstractMap.SimpleEntry<>("wind:3", new TSElement(1L, 9.0)), + new AbstractMap.SimpleEntry<>("wind:4", new TSElement(1L, 16.0)) + ); + + jedis.tsMAdd( + new AbstractMap.SimpleEntry<>("wind:1", new TSElement(2L, 9.0)), + new AbstractMap.SimpleEntry<>("wind:2", new TSElement(2L, 11.0)), + new AbstractMap.SimpleEntry<>("wind:3", new TSElement(2L, 7.0)), + new AbstractMap.SimpleEntry<>("wind:4", new TSElement(2L, 14.0)) + ); + + jedis.tsMAdd( + new AbstractMap.SimpleEntry<>("wind:1", new TSElement(3L, 12.0)), + new AbstractMap.SimpleEntry<>("wind:2", new TSElement(3L, 14.0)), + new AbstractMap.SimpleEntry<>("wind:3", new TSElement(3L, 10.0)), + new AbstractMap.SimpleEntry<>("wind:4", new TSElement(3L, 17.0)) + ); + + jedis.tsMAdd( + new AbstractMap.SimpleEntry<>("wind:1", new TSElement(4L, 8.0)), + new AbstractMap.SimpleEntry<>("wind:2", new TSElement(4L, 10.0)), + new AbstractMap.SimpleEntry<>("wind:3", new TSElement(4L, 6.0)), + new AbstractMap.SimpleEntry<>("wind:4", new TSElement(4L, 13.0)) + ); + + // Group by country with max reduction + Map res44 = jedis.tsMRange( + TSMRangeParams.multiRangeParams(0L, 4L) + .filter("country=(us,uk)") + .groupBy("country", "max")); + System.out.println(res44); + // >>> {country=uk=TSMRangeElements{key=country=uk, labels={}, value=[(0:12.0)... + + // Group by country with avg reduction + Map res45 = jedis.tsMRange( + TSMRangeParams.multiRangeParams(0L, 4L) + .filter("country=(us,uk)") + .groupBy("country", "avg")); + System.out.println(res45); + // >>> {country=uk=TSMRangeElements{key=country=uk, labels={}, value=[(0:11.0)... + // STEP_END + // REMOVE_START + assertEquals(2, res44.size()); + assertTrue(res44.containsKey("country=uk")); + TSMRangeElements res44uk = res44.get("country=uk"); + assertEquals("country=uk", res44uk.getKey()); + assertEquals(0, res44uk.getLabels().size()); + assertEquals(5, res44uk.getElements().size()); + assertEquals(0L, res44uk.getElements().get(0).getTimestamp()); + assertEquals(12.0, res44uk.getElements().get(0).getValue(), 0.001); + assertEquals(1L, res44uk.getElements().get(1).getTimestamp()); + assertEquals(13.0, res44uk.getElements().get(1).getValue(), 0.001); + assertEquals(2L, res44uk.getElements().get(2).getTimestamp()); + assertEquals(11.0, res44uk.getElements().get(2).getValue(), 0.001); + assertEquals(3L, res44uk.getElements().get(3).getTimestamp()); + assertEquals(14.0, res44uk.getElements().get(3).getValue(), 0.001); + assertEquals(4L, res44uk.getElements().get(4).getTimestamp()); + assertEquals(10.0, res44uk.getElements().get(4).getValue(), 0.001); + + assertTrue(res44.containsKey("country=us")); + TSMRangeElements res44us = res44.get("country=us"); + assertEquals("country=us", res44us.getKey()); + assertEquals(0, res44us.getLabels().size()); + assertEquals(5, res44us.getElements().size()); + assertEquals(0L, res44us.getElements().get(0).getTimestamp()); + assertEquals(15.0, res44us.getElements().get(0).getValue(), 0.001); + assertEquals(1L, res44us.getElements().get(1).getTimestamp()); + assertEquals(16.0, res44us.getElements().get(1).getValue(), 0.001); + assertEquals(2L, res44us.getElements().get(2).getTimestamp()); + assertEquals(14.0, res44us.getElements().get(2).getValue(), 0.001); + assertEquals(3L, res44us.getElements().get(3).getTimestamp()); + assertEquals(17.0, res44us.getElements().get(3).getValue(), 0.001); + assertEquals(4L, res44us.getElements().get(4).getTimestamp()); + assertEquals(13.0, res44us.getElements().get(4).getValue(), 0.001); + + assertEquals(2, res45.size()); + assertTrue(res45.containsKey("country=uk")); + TSMRangeElements res45uk = res45.get("country=uk"); + assertEquals("country=uk", res45uk.getKey()); + assertEquals(0, res45uk.getLabels().size()); + assertEquals(5, res45uk.getElements().size()); + assertEquals(0L, res45uk.getElements().get(0).getTimestamp()); + assertEquals(11.0, res45uk.getElements().get(0).getValue(), 0.001); + assertEquals(1L, res45uk.getElements().get(1).getTimestamp()); + assertEquals(12.0, res45uk.getElements().get(1).getValue(), 0.001); + assertEquals(2L, res45uk.getElements().get(2).getTimestamp()); + assertEquals(10.0, res45uk.getElements().get(2).getValue(), 0.001); + assertEquals(3L, res45uk.getElements().get(3).getTimestamp()); + assertEquals(13.0, res45uk.getElements().get(3).getValue(), 0.001); + assertEquals(4L, res45uk.getElements().get(4).getTimestamp()); + assertEquals(9.0, res45uk.getElements().get(4).getValue(), 0.001); + + assertTrue(res45.containsKey("country=us")); + TSMRangeElements res45us = res45.get("country=us"); + assertEquals("country=us", res45us.getKey()); + assertEquals(0, res45us.getLabels().size()); + assertEquals(5, res45us.getElements().size()); + assertEquals(0L, res45us.getElements().get(0).getTimestamp()); + assertEquals(11.5, res45us.getElements().get(0).getValue(), 0.001); + assertEquals(1L, res45us.getElements().get(1).getTimestamp()); + assertEquals(12.5, res45us.getElements().get(1).getValue(), 0.001); + assertEquals(2L, res45us.getElements().get(2).getTimestamp()); + assertEquals(10.5, res45us.getElements().get(2).getValue(), 0.001); + assertEquals(3L, res45us.getElements().get(3).getTimestamp()); + assertEquals(13.5, res45us.getElements().get(3).getValue(), 0.001); + assertEquals(4L, res45us.getElements().get(4).getTimestamp()); + assertEquals(9.5, res45us.getElements().get(4).getValue(), 0.001); + // REMOVE_END + + // STEP_START create_compaction + String res46 = jedis.tsCreate("hyg:1"); + System.out.println(res46); // >>> OK + + String res47 = jedis.tsCreate("hyg:compacted"); + System.out.println(res47); // >>> OK + + String res48 = jedis.tsCreateRule("hyg:1", "hyg:compacted", AggregationType.MIN, 3); + System.out.println(res48); // >>> OK + + TSInfo res49 = jedis.tsInfo("hyg:1"); + System.out.println("Rules: " + res49.getProperty("rules")); + // >>> Rules: [{compactionKey=hyg:compacted, bucketDuration=3, aggregationType=MIN, alignmentTimestamp=0}] + + TSInfo res50 = jedis.tsInfo("hyg:compacted"); + System.out.println("Source key: " + res50.getProperty("sourceKey")); + // >>> Source key: hyg:1 + // STEP_END + // REMOVE_START + assertEquals("OK", res46); + assertEquals("OK", res47); + assertEquals("OK", res48); + assertEquals("hyg:1", res50.getProperty("sourceKey")); + // REMOVE_END + + // STEP_START comp_add + // Recreate the compaction rule so this example runs on its own. + jedis.del("hyg:1", "hyg:compacted"); + jedis.tsCreate("hyg:1"); + jedis.tsCreate("hyg:compacted"); + jedis.tsCreateRule("hyg:1", "hyg:compacted", AggregationType.MIN, 3); + + List res51 = jedis.tsMAdd( + new AbstractMap.SimpleEntry<>("hyg:1", new TSElement(0L, 75.0)), + new AbstractMap.SimpleEntry<>("hyg:1", new TSElement(1L, 77.0)), + new AbstractMap.SimpleEntry<>("hyg:1", new TSElement(2L, 78.0)) + ); + System.out.println(res51); // >>> [0, 1, 2] + + List res52 = jedis.tsRange("hyg:compacted", 0L, 10L); + System.out.println(res52); // >>> [] + + long res53 = jedis.tsAdd("hyg:1", 3L, 79.0); + System.out.println(res53); // >>> 3 + + List res54 = jedis.tsRange("hyg:compacted", 0L, 10L); + System.out.println(res54); // >>> [(0:75.0)] + // STEP_END + // REMOVE_START + assertEquals(Arrays.asList(0L, 1L, 2L), res51); + assertEquals(Arrays.asList(), res52); + assertEquals(3L, res53); + assertEquals(Arrays.asList(new TSElement(0L, 75.0)), res54); + // REMOVE_END + + // STEP_START del + // Recreate the thermometer:1 series so this example runs on its own. + jedis.del("thermometer:1"); + jedis.tsAdd("thermometer:1", 1L, 9.2); + jedis.tsAdd("thermometer:1", 2L, 9.9); + + TSInfo res55 = jedis.tsInfo("thermometer:1"); + System.out.println(res55.getProperty("totalSamples")); // >>> 2 + System.out.println(res55.getProperty("firstTimestamp")); // >>> 1 + System.out.println(res55.getProperty("lastTimestamp")); // >>> 2 + + long res56 = jedis.tsAdd("thermometer:1", 3L, 9.7); + System.out.println(res56); // >>> 3 + + TSInfo res57 = jedis.tsInfo("thermometer:1"); + System.out.println(res57.getProperty("totalSamples")); // >>> 3 + + long res58 = jedis.tsDel("thermometer:1", 1L, 2L); + System.out.println(res58); // >>> 2 + + TSInfo res59 = jedis.tsInfo("thermometer:1"); + System.out.println(res59.getProperty("totalSamples")); // >>> 1 + + long res60 = jedis.tsDel("thermometer:1", 3L, 3L); + System.out.println(res60); // >>> 1 + + TSInfo res61 = jedis.tsInfo("thermometer:1"); + System.out.println(res61.getProperty("totalSamples")); // >>> 0 + // STEP_END + // REMOVE_START + assertEquals((Long) 2L, res55.getProperty("totalSamples")); + assertEquals((Long) 1L, res55.getProperty("firstTimestamp")); + assertEquals((Long) 2L, res55.getProperty("lastTimestamp")); + assertEquals(3L, res56); + assertEquals((Long) 3L, res57.getProperty("totalSamples")); + assertEquals(2L, res58); + assertEquals((Long) 1L, res59.getProperty("totalSamples")); + assertEquals(1L, res60); + assertEquals((Long) 0L, res61.getProperty("totalSamples")); + // REMOVE_END + + //HIDE_START + jedis.close(); + //HIDE_END + } +} diff --git a/local_examples/time_series_tutorial/node-redis/dt-time-series.js b/local_examples/time_series_tutorial/node-redis/dt-time-series.js new file mode 100644 index 0000000000..1d5267d8df --- /dev/null +++ b/local_examples/time_series_tutorial/node-redis/dt-time-series.js @@ -0,0 +1,690 @@ +// EXAMPLE: time_series_tutorial +// HIDE_START +import assert from 'assert'; +import { createClient } from 'redis'; +import { TIME_SERIES_AGGREGATION_TYPE, TIME_SERIES_REDUCERS } from '@redis/time-series'; + +const client = createClient(); +await client.connect(); +// HIDE_END + +// REMOVE_START +await client.del([ + 'thermometer:1', 'thermometer:2', 'thermometer:3', + 'rg:1', 'rg:2', 'rg:3', 'rg:4', + 'sensor3', + 'wind:1', 'wind:2', 'wind:3', 'wind:4', + 'hyg:1', 'hyg:compacted' +]); +// REMOVE_END + +// STEP_START create +const res1 = await client.ts.create('thermometer:1'); +console.log(res1); // >>> OK + +const res2 = await client.type('thermometer:1'); +console.log(res2); // >>> TSDB-TYPE + +const res3 = await client.ts.info('thermometer:1'); +console.log(res3); +// >>> { rules: [], ... totalSamples: 0, ... +// STEP_END +// REMOVE_START +assert.equal(res1, 'OK'); +assert.equal(res2, 'TSDB-TYPE'); +assert.equal(res3.totalSamples, 0); +// REMOVE_END + +// STEP_START create_retention +const res4 = await client.ts.add('thermometer:2', 1, 10.8, { RETENTION: 100 }); +console.log(res4); // >>> 1 + +const res5 = await client.ts.info('thermometer:2'); +console.log(res5); +// >>> { rules: [], ... retentionTime: 100, ... +// STEP_END +// REMOVE_START +assert.equal(res4, 1); +assert.equal(res5.retentionTime, 100); +// REMOVE_END + +// STEP_START create_labels +const res6 = await client.ts.add('thermometer:3', 1, 10.4, { + LABELS: { location: 'UK', type: 'Mercury' } +}); +console.log(res6); // >>> 1 + +const res7 = await client.ts.info('thermometer:3'); +console.log(res7); +// >>> { labels: [{ name: 'location', value: 'UK' }, { name: 'type', value: 'Mercury' }], ... } +// STEP_END +// REMOVE_START +assert.equal(res6, 1); +assert.deepEqual(res7.labels, [ + { name: 'location', value: 'UK' }, + { name: 'type', value: 'Mercury' }, +]); +// REMOVE_END + +// STEP_START madd +// Recreate the thermometer series so this example runs on its own. +await client.del(['thermometer:1', 'thermometer:2']); +await client.ts.create('thermometer:1'); +await client.ts.add('thermometer:2', 1, 10.8); + +const res8 = await client.ts.mAdd([ + { key: 'thermometer:1', timestamp: 1, value: 9.2 }, + { key: 'thermometer:1', timestamp: 2, value: 9.9 }, + { key: 'thermometer:2', timestamp: 2, value: 10.3 } +]); +console.log(res8); // >>> [1, 2, 2] +// STEP_END +// REMOVE_START +assert.deepEqual(res8, [1, 2, 2]); +// REMOVE_END + +// STEP_START get +// Recreate the thermometer:2 series so this example runs on its own. +await client.del('thermometer:2'); +await client.ts.add('thermometer:2', 1, 10.8); +await client.ts.add('thermometer:2', 2, 10.3); +// The last recorded temperature for thermometer:2 +// was 10.3 at time 2. +const res9 = await client.ts.get('thermometer:2'); +console.log(res9); // >>> { timestamp: 2, value: 10.3 } +// STEP_END +// REMOVE_START +assert.equal(res9.timestamp, 2); +assert.equal(res9.value, 10.3); +// REMOVE_END + +// STEP_START range +// Add 5 data points to a time series named "rg:1". +const res10 = await client.ts.create('rg:1'); +console.log(res10); // >>> OK + +const res11 = await client.ts.mAdd([ + { key: 'rg:1', timestamp: 0, value: 18 }, + { key: 'rg:1', timestamp: 1, value: 14 }, + { key: 'rg:1', timestamp: 2, value: 22 }, + { key: 'rg:1', timestamp: 3, value: 18 }, + { key: 'rg:1', timestamp: 4, value: 24 } +]); +console.log(res11); // >>> [0, 1, 2, 3, 4] + +// Retrieve all the data points in ascending order. +const res12 = await client.ts.range('rg:1', '-', '+'); +console.log(res12); +// >>> [{ timestamp: 0, value: 18 }, { timestamp: 1, value: 14 }, ...] + +// Retrieve data points up to time 1 (inclusive). +const res13 = await client.ts.range('rg:1', '-', 1); +console.log(res13); +// >>> [{ timestamp: 0, value: 18 }, { timestamp: 1, value: 14 }] + +// Retrieve data points from time 3 onwards. +const res14 = await client.ts.range('rg:1', 3, '+'); +console.log(res14); +// >>> [{ timestamp: 3, value: 18 }, { timestamp: 4, value: 24 }] + +// Retrieve all the data points in descending order. +const res15 = await client.ts.revRange('rg:1', '-', '+'); +console.log(res15); +// >>> [{ timestamp: 4, value: 24 }, { timestamp: 3, value: 18 }, ...] + +// Retrieve data points up to time 1 (inclusive), but return them +// in descending order. +const res16 = await client.ts.revRange('rg:1', '-', 1); +console.log(res16); +// >>> [{ timestamp: 1, value: 14 }, { timestamp: 0, value: 18 }] +// STEP_END +// REMOVE_START +assert.equal(res10, 'OK'); +assert.deepEqual(res11, [0, 1, 2, 3, 4]); + +assert.deepEqual(res12, [ + { timestamp: 0, value: 18 }, + { timestamp: 1, value: 14 }, + { timestamp: 2, value: 22 }, + { timestamp: 3, value: 18 }, + { timestamp: 4, value: 24 } +]); +assert.deepEqual(res13, [ + { timestamp: 0, value: 18 }, + { timestamp: 1, value: 14 } +]); +assert.deepEqual(res14, [ + { timestamp: 3, value: 18 }, + { timestamp: 4, value: 24 } +]); +assert.deepEqual(res15, [ + { timestamp: 4, value: 24 }, + { timestamp: 3, value: 18 }, + { timestamp: 2, value: 22 }, + { timestamp: 1, value: 14 }, + { timestamp: 0, value: 18 } +]); +assert.deepEqual(res16, [ + { timestamp: 1, value: 14 }, + { timestamp: 0, value: 18 } +]); +// REMOVE_END + +// STEP_START range_filter +// Recreate the rg:1 series so this example runs on its own. +await client.del('rg:1'); +await client.ts.create('rg:1'); +await client.ts.mAdd([ + { key: 'rg:1', timestamp: 0, value: 18 }, + { key: 'rg:1', timestamp: 1, value: 14 }, + { key: 'rg:1', timestamp: 2, value: 22 }, + { key: 'rg:1', timestamp: 3, value: 18 }, + { key: 'rg:1', timestamp: 4, value: 24 } +]); + +const res17 = await client.ts.range('rg:1', '-', '+', { + FILTER_BY_TS: [0, 2, 4] +}); +console.log(res17); +// >>> [{ timestamp: 0, value: 18 }, { timestamp: 2, value: 22 }, { timestamp: 4, value: 24 }] + +const res18 = await client.ts.revRange('rg:1', '-', '+', { + FILTER_BY_TS: [0, 2, 4], + FILTER_BY_VALUE: { min: 20, max: 25 } +}); +console.log(res18); +// >>> [{ timestamp: 4, value: 24 }, { timestamp: 2, value: 22 }] + +const res19 = await client.ts.revRange('rg:1', '-', '+', { + FILTER_BY_TS: [0, 2, 4], + FILTER_BY_VALUE: { min: 22, max: 22 }, + COUNT: 1 +}); +console.log(res19); +// >>> [{ timestamp: 2, value: 22 }] +// STEP_END +// REMOVE_START +assert.deepEqual(res17, [ + { timestamp: 0, value: 18 }, + { timestamp: 2, value: 22 }, + { timestamp: 4, value: 24 } +]); +assert.deepEqual(res18, [ + { timestamp: 4, value: 24 }, + { timestamp: 2, value: 22 } +]); +assert.deepEqual(res19, [ + { timestamp: 2, value: 22 } +]); +// REMOVE_END + +// STEP_START query_multi +// Create three new "rg:" time series (two in the US +// and one in the UK, with different units) and add some +// data points. +const res20 = await client.ts.create('rg:2', { + LABELS: { location: 'us', unit: 'cm' } +}); +console.log(res20); // >>> OK + +const res21 = await client.ts.create('rg:3', { + LABELS: { location: 'us', unit: 'in' } +}); +console.log(res21); // >>> OK + +const res22 = await client.ts.create('rg:4', { + LABELS: { location: 'uk', unit: 'mm' } +}); +console.log(res22); // >>> OK + +const res23 = await client.ts.mAdd([ + { key: 'rg:2', timestamp: 0, value: 1.8 }, + { key: 'rg:3', timestamp: 0, value: 0.9 }, + { key: 'rg:4', timestamp: 0, value: 25 } +]); +console.log(res23); // >>> [0, 0, 0] + +const res24 = await client.ts.mAdd([ + { key: 'rg:2', timestamp: 1, value: 2.1 }, + { key: 'rg:3', timestamp: 1, value: 0.77 }, + { key: 'rg:4', timestamp: 1, value: 18 } +]); +console.log(res24); // >>> [1, 1, 1] + +const res25 = await client.ts.mAdd([ + { key: 'rg:2', timestamp: 2, value: 2.3 }, + { key: 'rg:3', timestamp: 2, value: 1.1 }, + { key: 'rg:4', timestamp: 2, value: 21 } +]); +console.log(res25); // >>> [2, 2, 2] + +const res26 = await client.ts.mAdd([ + { key: 'rg:2', timestamp: 3, value: 1.9 }, + { key: 'rg:3', timestamp: 3, value: 0.81 }, + { key: 'rg:4', timestamp: 3, value: 19 } +]); +console.log(res26); // >>> [3, 3, 3] + +const res27 = await client.ts.mAdd([ + { key: 'rg:2', timestamp: 4, value: 1.78 }, + { key: 'rg:3', timestamp: 4, value: 0.74 }, + { key: 'rg:4', timestamp: 4, value: 23 } +]); +console.log(res27); // >>> [4, 4, 4] + +// Retrieve the last data point from each US time series. +const res28 = await client.ts.mGet(['location=us']); +console.log(res28); +// >>> { "rg:2": { sample: { timestamp: 4, value: 1.78 } }, "rg:3": { sample: { timestamp: 4, value: 0.74 } } } + +// Retrieve the same data points, but include the `unit` +// label in the results. +const res29 = await client.ts.mGetSelectedLabels(['location=us'], ['unit']); +console.log(res29); +// >>> { "rg:2": { labels: { unit: 'cm' }, sample: { timestamp: 4, value: 1.78 } }, "rg:3": { labels: { unit: 'in' }, sample: { timestamp: 4, value: 0.74 } } } + +// Retrieve data points up to time 2 (inclusive) from all +// time series that use millimeters as the unit. Include all +// labels in the results. +const res30 = await client.ts.mRangeWithLabels('-', 2, 'unit=mm'); +console.log(res30); +// >>> { "rg:4": { labels: { location: 'uk', unit: 'mm' }, samples: [ +// { timestamp: 0, value: 25 }, +// { timestamp: 1, value: 18 }, +// { timestamp: 2, value: 21 } +// ] } } + +// Retrieve data points from time 1 to time 3 (inclusive) from +// all time series that use centimeters or millimeters as the unit, +// but only return the `location` label. Return the results +// in descending order of timestamp. +const res31 = await client.ts.mRevRangeSelectedLabels( + 1, 3, + ['location'], + ['unit=(cm,mm)'] +); +console.log(res31); +// >>> { "rg:2": { labels: { location: 'us' }, samples: [ +// { timestamp: 3, value: 1.9 }, +// { timestamp: 2, value: 2.3 }, +// { timestamp: 1, value: 2.1 } +// ] }, "rg:4": { labels: { location: 'uk' }, samples: [ +// { timestamp: 3, value: 19 }, +// { timestamp: 2, value: 21 }, +// { timestamp: 1, value: 18 } +// ] } } +// STEP_END +// REMOVE_START +assert.equal(res20, 'OK'); +assert.equal(res21, 'OK'); +assert.equal(res22, 'OK'); +assert.deepEqual(res23, [0, 0, 0]); +assert.deepEqual(res24, [1, 1, 1]); +assert.deepEqual(res25, [2, 2, 2]); +assert.deepEqual(res26, [3, 3, 3]); +assert.deepEqual(res27, [4, 4, 4]); + +assert.deepEqual(res28, { + "rg:2": { sample: { timestamp: 4, value: 1.78 } }, + "rg:3": { sample: { timestamp: 4, value: 0.74 } } +}); +assert.deepEqual(res29, { + "rg:2": { labels: { unit: 'cm' }, sample: { timestamp: 4, value: 1.78 } }, + "rg:3": { labels: { unit: 'in' }, sample: { timestamp: 4, value: 0.74 } } +}); + +assert.deepEqual(res30, { + "rg:4": { + labels: { location: 'uk', unit: 'mm' }, + samples: [ + { timestamp: 0, value: 25 }, + { timestamp: 1, value: 18 }, + { timestamp: 2, value: 21 } + ] + } +}); +assert.deepEqual(res31, { + "rg:2": { + labels: { location: 'us' }, + samples: [ + { timestamp: 3, value: 1.9 }, + { timestamp: 2, value: 2.3 }, + { timestamp: 1, value: 2.1 } + ] + }, + "rg:4": { + labels: { location: 'uk' }, + samples: [ + { timestamp: 3, value: 19 }, + { timestamp: 2, value: 21 }, + { timestamp: 1, value: 18 } + ] + } +}); +// REMOVE_END + +// STEP_START agg +// Recreate the rg:2 series so this example runs on its own. +await client.del('rg:2'); +await client.ts.create('rg:2'); +await client.ts.mAdd([ + { key: 'rg:2', timestamp: 0, value: 1.8 }, + { key: 'rg:2', timestamp: 1, value: 2.1 }, + { key: 'rg:2', timestamp: 2, value: 2.3 }, + { key: 'rg:2', timestamp: 3, value: 1.9 }, + { key: 'rg:2', timestamp: 4, value: 1.78 } +]); + +const res32 = await client.ts.range('rg:2', '-', '+', { + AGGREGATION: { + type: TIME_SERIES_AGGREGATION_TYPE.AVG, + timeBucket: 2 + } +}); +console.log(res32); +// >>> [{ timestamp: 0, value: 1.9500000000000002 },{ timestamp: 2, value: 2.0999999999999996 }, { timestamp: 4, value: 1.78 }] +// STEP_END +// REMOVE_START +assert.deepEqual(res32, [ + { timestamp: 0, value: 1.9500000000000002 }, + { timestamp: 2, value: 2.0999999999999996 }, + { timestamp: 4, value: 1.78 } +]); +// REMOVE_END + +// STEP_START agg_bucket +const res33 = await client.ts.create('sensor3'); +console.log(res33); // >>> OK + +const res34 = await client.ts.mAdd([ + { key: 'sensor3', timestamp: 10, value: 1000 }, + { key: 'sensor3', timestamp: 20, value: 2000 }, + { key: 'sensor3', timestamp: 30, value: 3000 }, + { key: 'sensor3', timestamp: 40, value: 4000 }, + { key: 'sensor3', timestamp: 50, value: 5000 }, + { key: 'sensor3', timestamp: 60, value: 6000 }, + { key: 'sensor3', timestamp: 70, value: 7000 } +]); +console.log(res34); // >>> [10, 20, 30, 40, 50, 60, 70] + +const res35 = await client.ts.range('sensor3', 10, 70, { + AGGREGATION: { + type: TIME_SERIES_AGGREGATION_TYPE.MIN, + timeBucket: 25 + } +}); +console.log(res35); +// >>> [{ timestamp: 0, value: 1000 }, { timestamp: 25, value: 3000 }, { timestamp: 50, value: 5000 }] +// STEP_END +// REMOVE_START +assert.equal(res33, 'OK'); +assert.deepEqual(res34, [10, 20, 30, 40, 50, 60, 70]); +assert.deepEqual(res35, [ + { timestamp: 0, value: 1000 }, + { timestamp: 25, value: 3000 }, + { timestamp: 50, value: 5000 } +]); +// REMOVE_END + +// STEP_START agg_align +// Recreate the sensor3 series so this example runs on its own. +await client.del('sensor3'); +await client.ts.create('sensor3'); +await client.ts.mAdd([ + { key: 'sensor3', timestamp: 10, value: 1000 }, + { key: 'sensor3', timestamp: 20, value: 2000 }, + { key: 'sensor3', timestamp: 30, value: 3000 }, + { key: 'sensor3', timestamp: 40, value: 4000 }, + { key: 'sensor3', timestamp: 50, value: 5000 }, + { key: 'sensor3', timestamp: 60, value: 6000 }, + { key: 'sensor3', timestamp: 70, value: 7000 } +]); + +const res36 = await client.ts.range('sensor3', 10, 70, { + AGGREGATION: { + type: TIME_SERIES_AGGREGATION_TYPE.MIN, + timeBucket: 25 + }, + ALIGN: 'START' +}); +console.log(res36); +// >>> [{ timestamp: 10, value: 1000 }, { timestamp: 35, value: 4000 }, { timestamp: 60, value: 6000 }] +// STEP_END +// REMOVE_START +assert.deepEqual(res36, [ + { timestamp: 10, value: 1000 }, + { timestamp: 35, value: 4000 }, + { timestamp: 60, value: 6000 } +]); +// REMOVE_END + +// STEP_START agg_multi +const res37 = await client.ts.create('wind:1', { + LABELS: { country: 'uk' } +}); +console.log(res37); // >>> OK + +const res38 = await client.ts.create('wind:2', { + LABELS: { country: 'uk' } +}); +console.log(res38); // >>> OK + +const res39 = await client.ts.create('wind:3', { + LABELS: { country: 'us' } +}); +console.log(res39); // >>> OK + +const res40 = await client.ts.create('wind:4', { + LABELS: { country: 'us' } +}); +console.log(res40); // >>> OK + +const res41 = await client.ts.mAdd([ + { key: 'wind:1', timestamp: 1, value: 12 }, + { key: 'wind:2', timestamp: 1, value: 18 }, + { key: 'wind:3', timestamp: 1, value: 5 }, + { key: 'wind:4', timestamp: 1, value: 20 } +]); +console.log(res41); // >>> [1, 1, 1, 1] + +const res42 = await client.ts.mAdd([ + { key: 'wind:1', timestamp: 2, value: 14 }, + { key: 'wind:2', timestamp: 2, value: 21 }, + { key: 'wind:3', timestamp: 2, value: 4 }, + { key: 'wind:4', timestamp: 2, value: 25 } +]); +console.log(res42); // >>> [2, 2, 2, 2] + +const res43 = await client.ts.mAdd([ + { key: 'wind:1', timestamp: 3, value: 10 }, + { key: 'wind:2', timestamp: 3, value: 24 }, + { key: 'wind:3', timestamp: 3, value: 8 }, + { key: 'wind:4', timestamp: 3, value: 18 } +]); +console.log(res43); // >>> [3, 3, 3, 3] + +// The result pairs contain the timestamp and the maximum sample value +// for the country at that timestamp. +const res44 = await client.ts.mRangeGroupBy( + '-', '+', ['country=(us,uk)'], + {label: 'country', REDUCE: TIME_SERIES_REDUCERS.MAX} +); +console.log(res44); +// >>> { "country=uk": { samples: [ +// { timestamp: 1, value: 18 }, +// { timestamp: 2, value: 21 }, +// { timestamp: 3, value: 24 } +// ] }, "country=us": { samples: [ +// { timestamp: 1, value: 20 }, +// { timestamp: 2, value: 25 }, +// { timestamp: 3, value: 18 } +// ] } } + +// The result pairs contain the timestamp and the average sample value +// for the country at that timestamp. +const res45 = await client.ts.mRangeGroupBy( + '-', '+', ['country=(us,uk)'], + { label: 'country', REDUCE: TIME_SERIES_REDUCERS.AVG} +); +console.log(res45); +// >>> { +// "country=uk": { +// samples: [{ timestamp: 1, value: 15 }, { timestamp: 2, value: 17.5 }, { timestamp: 3, value: 17 }] +// }, +// "country=us": { +// samples: [{ timestamp: 1, value: 12.5 }, { timestamp: 2, value: 14.5 }, { timestamp: 3, value: 13 }] +// } +// } +// STEP_END +// REMOVE_START +assert.equal(res37, 'OK'); +assert.equal(res38, 'OK'); +assert.equal(res39, 'OK'); +assert.equal(res40, 'OK'); +assert.deepEqual(res41, [1, 1, 1, 1]); +assert.deepEqual(res42, [2, 2, 2, 2]); +assert.deepEqual(res43, [3, 3, 3, 3]); + +assert.deepEqual(res44, { + "country=uk": { + samples: [ + { timestamp: 1, value: 18 }, + { timestamp: 2, value: 21 }, + { timestamp: 3, value: 24 } + ] + }, + "country=us": { + samples: [ + { timestamp: 1, value: 20 }, + { timestamp: 2, value: 25 }, + { timestamp: 3, value: 18 } + ] + } +}); +assert.deepEqual(res45, { + "country=uk": { + samples: [ + { timestamp: 1, value: 15 }, + { timestamp: 2, value: 17.5 }, + { timestamp: 3, value: 17 } + ] + }, + "country=us": { + samples: [ + { timestamp: 1, value: 12.5 }, + { timestamp: 2, value: 14.5 }, + { timestamp: 3, value: 13 } + ] + } +}); +// REMOVE_END + +// STEP_START create_compaction +const res46 = await client.ts.create('hyg:1'); +console.log(res46); // >>> OK + +const res47 = await client.ts.create('hyg:compacted'); +console.log(res47); // >>> OK + +const res48 = await client.ts.createRule('hyg:1', 'hyg:compacted', TIME_SERIES_AGGREGATION_TYPE.MIN, 3); +console.log(res48); // >>> OK + +const res49 = await client.ts.info('hyg:1'); +console.log(res49.rules); +// >>> [{ aggregationType: 'MIN', key: 'hyg:compacted', timeBucket: 3}] + +const res50 = await client.ts.info('hyg:compacted'); +console.log(res50.sourceKey); // >>> 'hyg:1' +// STEP_END +// REMOVE_START +assert.equal(res46, 'OK'); +assert.equal(res47, 'OK'); +assert.equal(res48, 'OK'); +assert.deepEqual(res49.rules, [ + { aggregationType: 'MIN', key: 'hyg:compacted', timeBucket: 3} +]); +assert.equal(res50.sourceKey, 'hyg:1'); +// REMOVE_END + +// STEP_START comp_add +// Recreate the compaction rule so this example runs on its own. +await client.del(['hyg:1', 'hyg:compacted']); +await client.ts.create('hyg:1'); +await client.ts.create('hyg:compacted'); +await client.ts.createRule('hyg:1', 'hyg:compacted', TIME_SERIES_AGGREGATION_TYPE.MIN, 3); + +const res51 = await client.ts.mAdd([ + { key: 'hyg:1', timestamp: 0, value: 75 }, + { key: 'hyg:1', timestamp: 1, value: 77 }, + { key: 'hyg:1', timestamp: 2, value: 78 } +]); +console.log(res51); // >>> [0, 1, 2] + +const res52 = await client.ts.range('hyg:compacted', '-', '+'); +console.log(res52); // >>> [] + +const res53 = await client.ts.add('hyg:1', 3, 79); +console.log(res53); // >>> 3 + +const res54 = await client.ts.range('hyg:compacted', '-', '+'); +console.log(res54); // >>> [{ timestamp: 0, value: 75 }] +// STEP_END +// REMOVE_START +assert.deepEqual(res51, [0, 1, 2]); +assert.deepEqual(res52, []); +assert.equal(res53, 3); +assert.deepEqual(res54, [{ timestamp: 0, value: 75 }]); +// REMOVE_END + +// STEP_START del +// Recreate the thermometer:1 series so this example runs on its own. +await client.del('thermometer:1'); +await client.ts.add('thermometer:1', 1, 9.2); +await client.ts.add('thermometer:1', 2, 9.9); + +const res55 = await client.ts.info('thermometer:1'); +console.log(res55.totalSamples); // >>> 2 +console.log(res55.firstTimestamp); // >>> 1 +console.log(res55.lastTimestamp); // >>> 2 + +const res56 = await client.ts.add('thermometer:1', 3, 9.7); +console.log(res56); // >>> 3 + +const res57 = await client.ts.info('thermometer:1'); +console.log(res57.totalSamples); // >>> 3 +console.log(res57.firstTimestamp); // >>> 1 +console.log(res57.lastTimestamp); // >>> 3 + +const res58 = await client.ts.del('thermometer:1', 1, 2); +console.log(res58); // >>> 2 + +const res59 = await client.ts.info('thermometer:1'); +console.log(res59.totalSamples); // >>> 1 +console.log(res59.firstTimestamp); // >>> 3 +console.log(res59.lastTimestamp); // >>> 3 + +const res60 = await client.ts.del('thermometer:1', 3, 3); +console.log(res60); // >>> 1 + +const res61 = await client.ts.info('thermometer:1'); +console.log(res61.totalSamples); // >>> 0 +// STEP_END +// REMOVE_START +assert.equal(res55.totalSamples, 2); +assert.equal(res55.firstTimestamp, 1); +assert.equal(res55.lastTimestamp, 2); +assert.equal(res56, 3); +assert.equal(res57.totalSamples, 3); +assert.equal(res57.firstTimestamp, 1); +assert.equal(res57.lastTimestamp, 3); +assert.equal(res58, 2); +assert.equal(res59.totalSamples, 1); +assert.equal(res59.firstTimestamp, 3); +assert.equal(res59.lastTimestamp, 3); +assert.equal(res60, 1); +assert.equal(res61.totalSamples, 0); +// REMOVE_END + +// HIDE_START +await client.quit(); +// HIDE_END \ No newline at end of file diff --git a/local_examples/time_series_tutorial/nredisstack/TimeSeriesTutorial.cs b/local_examples/time_series_tutorial/nredisstack/TimeSeriesTutorial.cs new file mode 100644 index 0000000000..aa89896399 --- /dev/null +++ b/local_examples/time_series_tutorial/nredisstack/TimeSeriesTutorial.cs @@ -0,0 +1,833 @@ +// EXAMPLE: time_series_tutorial +// HIDE_START +/* +Code samples for time series page: + https://redis.io/docs/latest/develop/data-types/timeseries/ +*/ +using NRedisStack.DataTypes; +using NRedisStack.Literals.Enums; +using StackExchange.Redis; +// HIDE_END + +// REMOVE_START +using NRedisStack.Tests; +using NRedisStack.RedisStackCommands; +using NRedisStack; + +namespace Doc; + +[Collection("DocsTests")] +// REMOVE_END + +public class TimeSeriesTutorial +// REMOVE_START +: AbstractNRedisStackTest, IDisposable +// REMOVE_END +{ + // REMOVE_START + public TimeSeriesTutorial(EndpointsFixture fixture) : base(fixture) { } + + [Fact] + // REMOVE_END + public void Run() + { + // REMOVE_START + // This is needed because we're constructing ConfigurationOptions in the test before calling GetConnection + SkipIfTargetConnectionDoesNotExist(EndpointsFixture.Env.Standalone); + var _ = GetCleanDatabase(EndpointsFixture.Env.Standalone); + // REMOVE_END + // HIDE_START + var muxer = ConnectionMultiplexer.Connect("localhost:6379"); + var db = muxer.GetDatabase(); + // HIDE_END + + // REMOVE_START + // Clear any keys here before using them in tests. + db.KeyDelete([ + "thermometer:1", "thermometer:2", "thermometer:3", + "rg:1", "rg:2", "rg:3", "rg:4", + "sensor3", + "wind:1", "wind:2", "wind:3", "wind:4", + "hyg:1", "hyg:compacted" + ]); + // REMOVE_END + + // STEP_START create + bool res1 = db.TS().Create( + "thermometer:1", + new TsCreateParamsBuilder().build() + ); + Console.WriteLine(res1); // >>> True + + RedisType res2 = db.KeyType("thermometer:1"); + Console.WriteLine(res2); // >>> TSDB-TYPE + + TimeSeriesInformation res3 = db.TS().Info("thermometer:1"); + Console.WriteLine(res3.TotalSamples); // >>> 0 + // STEP_END + // REMOVE_START + Assert.True(res1); + Assert.Equal(RedisType.Unknown, res2); + Assert.Equal(0, res3.TotalSamples); + // REMOVE_END + + // STEP_START create_retention + long res4 = db.TS().Add( + "thermometer:2", + new TsAddParamsBuilder() + .AddTimestamp(1) + .AddValue(10.8) + .AddRetentionTime(100) + .build() + ); + Console.WriteLine(res4); // >>> 1 + + TimeSeriesInformation res5 = db.TS().Info("thermometer:2"); + Console.WriteLine(res5.RetentionTime); + // >>> 100 + // STEP_END + // REMOVE_START + Assert.Equal(1, res4); + Assert.Equal(100, res5.RetentionTime); + // REMOVE_END + + // STEP_START create_labels + var labels = new List + { + new("location", "UK"), + new("type", "Mercury") + }; + long res6 = db.TS().Add( + "thermometer:3", + new TsAddParamsBuilder() + .AddTimestamp(1) + .AddValue(10.4) + .AddLabels(labels) + .build() + ); + Console.WriteLine(res6); // >>> 1 + + TimeSeriesInformation res7 = db.TS().Info("thermometer:3"); + Console.WriteLine( + $"Labels: {string.Join(", ", res7.Labels!.Select(l => $"{l.Key}={l.Value}"))}" + ); + // >>> Labels: location=UK, type=Mercury + // STEP_END + // REMOVE_START + Assert.Equal(1, res6); + Assert.Equal(2, res7.Labels!.Count); + Assert.Contains(res7.Labels, l => l.Key == "location" && l.Value == "UK"); + Assert.Contains(res7.Labels, l => l.Key == "type" && l.Value == "Mercury"); + // REMOVE_END + + // STEP_START madd + // Recreate the thermometer series so this example runs on its own. + db.KeyDelete(new RedisKey[] { "thermometer:1", "thermometer:2" }); + db.TS().Create("thermometer:1", new TsCreateParamsBuilder().build()); + db.TS().Add("thermometer:2", 1, 10.8); + + var sequence = new List<(string, TimeStamp, double)> + { + ("thermometer:1", 1, 9.2), + ("thermometer:1", 2, 9.9), + ("thermometer:2", 2, 10.3) + }; + IReadOnlyList res8 = db.TS().MAdd(sequence); + Console.WriteLine($"[{string.Join(", ", res8.Select(t => t.Value))}]"); + // >>> [1, 2, 2] + // STEP_END + // REMOVE_START + Assert.Equal(3, res8.Count); + Assert.Equal(1, (long)res8[0]); + Assert.Equal(2, (long)res8[1]); + Assert.Equal(2, (long)res8[2]); + // REMOVE_END + + // STEP_START get + // Recreate the thermometer:2 series so this example runs on its own. + db.KeyDelete("thermometer:2"); + db.TS().Add("thermometer:2", 1, 10.8); + db.TS().Add("thermometer:2", 2, 10.3); + // The last recorded temperature for thermometer:2 + // was 10.3 at time 2. + TimeSeriesTuple? res9 = db.TS().Get("thermometer:2"); + Console.WriteLine($"({res9!.Time.Value}, {res9.Val})"); + // >>> (2, 10.3) + // STEP_END + // REMOVE_START + Assert.NotNull(res9); + Assert.Equal(2, (long)res9.Time); + Assert.Equal(10.3, res9.Val); + // REMOVE_END + + // STEP_START range + // Add 5 data points to a time series named "rg:1". + bool res10 = db.TS().Create( + "rg:1", + new TsCreateParamsBuilder().build() + ); + Console.WriteLine(res10); // >>> True + + var sequence2 = new List<(string, TimeStamp, double)> + { + ("rg:1", 0, 18), + ("rg:1", 1, 14), + ("rg:1", 2, 22), + ("rg:1", 3, 18), + ("rg:1", 4, 24) + }; + IReadOnlyList res11 = db.TS().MAdd(sequence2); + Console.WriteLine( + $"[{string.Join(", ", res11.Select(t => t.Value))}]" + ); + // >>> [0, 1, 2, 3, 4] + + // Retrieve all the data points in ascending order. + IReadOnlyList res12 = db.TS().Range("rg:1", "-", "+"); + Console.WriteLine( + $"[{string.Join(", ", res12.Select(t => $"({t.Time.Value}, {t.Val})"))}]" + ); + // >>> [(0, 18), (1, 14), (2, 22), (3, 18), (4, 24)] + + // Retrieve data points up to time 1 (inclusive). + IReadOnlyList res13 = db.TS().Range("rg:1", "-", 1); + Console.WriteLine( + $"[{string.Join(", ", res13.Select(t => $"({t.Time.Value}, {t.Val})"))}]" + ); + // >>> [(0, 18), (1, 14)] + + // Retrieve data points from time 3 onwards. + IReadOnlyList res14 = db.TS().Range("rg:1", 3, "+"); + Console.WriteLine( + $"[{string.Join(", ", res14.Select(t => $"({t.Time.Value}, {t.Val})"))}]" + ); + // >>> [(3, 18), (4, 24)] + + // Retrieve all the data points in descending order. + IReadOnlyList res15 = db.TS().RevRange("rg:1", "-", "+"); + Console.WriteLine( + $"[{string.Join(", ", res15.Select(t => $"({t.Time.Value}, {t.Val})"))}]" + ); + // >>> [(4, 24), (3, 18), (2, 22), (1, 14), (0, 18)] + + // Retrieve data points up to time 1 (inclusive), but return them + // in descending order. + IReadOnlyList res16 = db.TS().RevRange("rg:1", "-", 1); + Console.WriteLine( + $"[{string.Join(", ", res16.Select(t => $"({t.Time.Value}, {t.Val})"))}]" + ); + // >>> [(1, 14), (0, 18)] + // STEP_END + // REMOVE_START + Assert.True(res10); + Assert.Equal(5, res11.Count); + Assert.Equal(5, res12.Count); + Assert.Equal(2, res13.Count); + Assert.Equal(2, res14.Count); + Assert.Equal(5, res15.Count); + Assert.Equal(2, res16.Count); + // REMOVE_END + + // STEP_START range_filter + // Recreate the rg:1 series so this example runs on its own. + db.KeyDelete("rg:1"); + db.TS().Create("rg:1", new TsCreateParamsBuilder().build()); + db.TS().MAdd(new List<(string, TimeStamp, double)> + { + ("rg:1", 0, 18), + ("rg:1", 1, 14), + ("rg:1", 2, 22), + ("rg:1", 3, 18), + ("rg:1", 4, 24) + }); + + var filterByTs = new List { 0, 2, 4 }; + IReadOnlyList res17 = db.TS().Range( + "rg:1", "-", "+", filterByTs: filterByTs + ); + Console.WriteLine( + $"[{string.Join(", ", res17.Select(t => $"({t.Time.Value}, {t.Val})"))}]" + ); + // >>> [(0, 18), (2, 22), (4, 24)] + + IReadOnlyList res18 = db.TS().RevRange( + "rg:1", "-", "+", + filterByTs: filterByTs, + filterByValue: (20, 25) + ); + Console.WriteLine( + $"[{string.Join(", ", res18.Select(t => $"({t.Time.Value}, {t.Val})"))}]" + ); + // >>> [(4, 24), (2, 22)] + + IReadOnlyList res19 = db.TS().RevRange( + "rg:1", "-", "+", + filterByTs: filterByTs, + filterByValue: (22, 22), + count: 1 + ); + Console.WriteLine( + $"[{string.Join(", ", res19.Select(t => $"({t.Time.Value}, {t.Val})"))}]" + ); + // >>> [(2, 22)] + // STEP_END + // REMOVE_START + Assert.Equal(3, res17.Count); + Assert.Equal(2, res18.Count); + Assert.Single(res19); + // REMOVE_END + + // STEP_START query_multi + // Create three new "rg:" time series (two in the US + // and one in the UK, with different units) and add some + // data points. + var labels2 = new List + { + new("location", "us"), + new("unit", "cm") + }; + bool res20 = db.TS().Create( + "rg:2", + new TsCreateParamsBuilder() + .AddLabels(labels2) + .build() + ); + Console.WriteLine(res20); // >>> True + + var labels3 = new List + { + new("location", "us"), + new("unit", "in") + }; + + bool res21 = db.TS().Create( + "rg:3", + new TsCreateParamsBuilder() + .AddLabels(labels3) + .build() + ); + Console.WriteLine(res21); // >>> True + + var labels4 = new List + { + new("location", "uk"), + new("unit", "mm") + }; + bool res22 = db.TS().Create( + "rg:4", + new TsCreateParamsBuilder() + .AddLabels(labels4) + .build() + ); + Console.WriteLine(res22); // >>> True + + var sequence3 = new List<(string, TimeStamp, double)> + { + ("rg:2", 0, 1.8), + ("rg:3", 0, 0.9), + ("rg:4", 0, 25) + }; + IReadOnlyList res23 = db.TS().MAdd(sequence3); + Console.WriteLine( + $"[{string.Join(", ", res23.Select(t => t.Value))}]" + ); + // >>> [0, 0, 0] + + var sequence4 = new List<(string, TimeStamp, double)> + { + ("rg:2", 1, 2.1), + ("rg:3", 1, 0.77), + ("rg:4", 1, 18) + }; + + IReadOnlyList res24 = db.TS().MAdd(sequence4); + Console.WriteLine( + $"[{string.Join(", ", res24.Select(t => t.Value))}]" + ); + // >>> [1, 1, 1] + + var sequence5 = new List<(string, TimeStamp, double)> + { + ("rg:2", 2, 2.3), + ("rg:3", 2, 1.1), + ("rg:4", 2, 21) + }; + + IReadOnlyList res25 = db.TS().MAdd(sequence5); + Console.WriteLine( + $"[{string.Join(", ", res25.Select(t => t.Value))}]" + ); + // >>> [2, 2, 2] + + var sequence6 = new List<(string, TimeStamp, double)> + { + ("rg:2", 3, 1.9), + ("rg:3", 3, 0.81), + ("rg:4", 3, 19) + }; + + IReadOnlyList res26 = db.TS().MAdd(sequence6); + Console.WriteLine( + $"[{string.Join(", ", res26.Select(t => t.Value))}]" + ); + // >>> [3, 3, 3] + + var sequence7 = new List<(string, TimeStamp, double)> + { + ("rg:2", 4, 1.78), + ("rg:3", 4, 0.74), + ("rg:4", 4, 23) + }; + IReadOnlyList res27 = db.TS().MAdd(sequence7); + Console.WriteLine( + $"[{string.Join(", ", res27.Select(t => t.Value))}]" + ); + // >>> [4, 4, 4] + + // Retrieve the last data point from each US time series. If + // you don't specify any labels, an empty array is returned + // for the labels. + var filters = new List { "location=us" }; + var res28 = db.TS().MGet(filters); + Console.WriteLine(res28.Count); // >>> 2 + + foreach (var (key, labelsResult, value) in res28) + { + Console.WriteLine($"{key}: ({value.Time.Value}, {value.Val})"); + } + // >>> rg:2: (4, 1.78) + // >>> rg:3: (4, 0.74) + + // Retrieve the same data points, but include the `unit` + // label in the results. + var selectUnitLabel = new List { "unit" }; + + var res29 = db.TS().MGet( + filters, + selectedLabels: selectUnitLabel + ); + Console.WriteLine(res29.Count); // >>> 2 + + foreach (var (key, labelsResult, value) in res29) + { + var unitLabel = labelsResult.FirstOrDefault(l => l.Key == "unit"); + Console.WriteLine($"{key} (unit: {unitLabel?.Value}): ({value.Time.Value}, {value.Val})"); + } + // >>> rg:2 (unit: cm): (4, 1.78) + // >>> rg:3 (unit: in): (4, 0.74) + + // Retrieve data points up to time 2 (inclusive) from all + // time series that use millimeters as the unit. Include all + // labels in the results. + var mmFilters = new List { "unit=mm" }; + + IReadOnlyList< + (string, IReadOnlyList, IReadOnlyList) + > res30 = db.TS().MRange( + "-", 2, mmFilters, withLabels: true + ); + Console.WriteLine(res30.Count); // >>> 1 + + foreach (var (key, labelsResult, values) in res30) + { + Console.WriteLine($"{key}:"); + Console.WriteLine($" Labels: ({string.Join(", ", labelsResult.Select(l => $"{l.Key}={l.Value}"))})"); + Console.WriteLine($" Values: [{string.Join(", ", values.Select(t => $"({t.Time.Value}, {t.Val})"))}]"); + } + // >>> rg:4: + // >>> Labels:location=uk,unit=mm + // >>> Values: [(1, 23), (2, 21), (3, 19)] + + // Retrieve data points from time 1 to time 3 (inclusive) from + // all time series that use centimeters or millimeters as the unit, + // but only return the `location` label. Return the results + // in descending order of timestamp. + var cmMmFilters = new List { "unit=(cm,mm)" }; + var locationLabels = new List { "location" }; + IReadOnlyList< + (string, IReadOnlyList, IReadOnlyList) + > res31 = db.TS().MRevRange( + 1, 3, cmMmFilters, selectLabels: locationLabels + ); + Console.WriteLine(res31.Count); // >>> 2 + + foreach (var (key, labelsResult, values) in res31) + { + var locationLabel = labelsResult.FirstOrDefault(l => l.Key == "location"); + Console.WriteLine($"{key} (location: {locationLabel?.Value})"); + Console.WriteLine($" Values: [{string.Join(", ", values.Select(t => $"({t.Time.Value}, {t.Val})"))}]"); + } + // >>> rg:4 (location: uk) + // >>> Values: [(3, 19), (2, 21), (1, 23)] + // >>> rg:2 (location: us) + // >>> Values: [(3, 2.3), (2, 2.1), (1, 1.8)] + // STEP_END + // REMOVE_START + Assert.True(res20); + Assert.True(res21); + Assert.True(res22); + Assert.Equal(3, res23.Count); + Assert.Equal(3, res24.Count); + Assert.Equal(3, res25.Count); + Assert.Equal(3, res26.Count); + Assert.Equal(3, res27.Count); + Assert.Equal(2, res28.Count); + Assert.Equal(2, res29.Count); + Assert.Single(res30); + Assert.Equal(2, res31.Count); + // REMOVE_END + + // STEP_START agg + // Recreate the rg:2 series so this example runs on its own. + db.KeyDelete("rg:2"); + db.TS().Create("rg:2", new TsCreateParamsBuilder().build()); + db.TS().MAdd(new List<(string, TimeStamp, double)> + { + ("rg:2", 0, 1.8), + ("rg:2", 1, 2.1), + ("rg:2", 2, 2.3), + ("rg:2", 3, 1.9), + ("rg:2", 4, 1.78) + }); + + IReadOnlyList res32 = db.TS().Range( + "rg:2", "-", "+", + aggregation: TsAggregation.Avg, + timeBucket: 2 + ); + Console.WriteLine($"[{string.Join(", ", res32.Select(t => $"({t.Time.Value}, {t.Val})"))}]"); + // >>> [(0, 1.95), (2, 2.1), (4, 1.78)] + // STEP_END + // REMOVE_START + Assert.Equal(3, res32.Count); + // REMOVE_END + + // STEP_START agg_bucket + bool res33 = db.TS().Create( + "sensor3", + new TsCreateParamsBuilder() + .build() + ); + Console.WriteLine(res33); // >>> True + + var sensorSequence = new List<(string, TimeStamp, double)> + { + ("sensor3", 10, 1000), + ("sensor3", 20, 2000), + ("sensor3", 30, 3000), + ("sensor3", 40, 4000), + ("sensor3", 50, 5000), + ("sensor3", 60, 6000), + ("sensor3", 70, 7000) + }; + IReadOnlyList res34 = db.TS().MAdd(sensorSequence); + Console.WriteLine($"[{string.Join(", ", res34.Select(t => t.Value))}]"); + // >>> [10, 20, 30, 40, 50, 60, 70] + + IReadOnlyList res35 = db.TS().Range( + "sensor3", 10, 70, + aggregation: TsAggregation.Min, + timeBucket: 25 + ); + Console.WriteLine($"[{string.Join(", ", res35.Select(t => $"({t.Time.Value}, {t.Val})"))}]"); + // >>> [(0, 1000), (25, 3000), (50, 5000)] + // STEP_END + // REMOVE_START + Assert.True(res33); + Assert.Equal(7, res34.Count); + Assert.Equal(3, res35.Count); + // REMOVE_END + + // STEP_START agg_align + // Recreate the sensor3 series so this example runs on its own. + db.KeyDelete("sensor3"); + db.TS().Create("sensor3", new TsCreateParamsBuilder().build()); + db.TS().MAdd(new List<(string, TimeStamp, double)> + { + ("sensor3", 10, 1000), + ("sensor3", 20, 2000), + ("sensor3", 30, 3000), + ("sensor3", 40, 4000), + ("sensor3", 50, 5000), + ("sensor3", 60, 6000), + ("sensor3", 70, 7000) + }); + + IReadOnlyList res36 = db.TS().Range( + "sensor3", 10, 70, + aggregation: TsAggregation.Min, + timeBucket: 25, + align: "-" + ); + Console.WriteLine( + $"[{string.Join(", ", res36.Select(t => $"({t.Time.Value}, {t.Val})"))}]" + ); + // >>> [(10, 1000), (35, 4000), (60, 6000)] + // STEP_END + // REMOVE_START + Assert.Equal(3, res36.Count); + // REMOVE_END + + // STEP_START agg_multi + var ukLabels = new List { new("country", "uk") }; + + bool res37 = db.TS().Create( + "wind:1", + new TsCreateParamsBuilder() + .AddLabels(ukLabels) + .build() + ); + Console.WriteLine(res37); // >>> True + + bool res38 = db.TS().Create( + "wind:2", + new TsCreateParamsBuilder() + .AddLabels(ukLabels) + .build() + ); + Console.WriteLine(res38); // >>> True + + var usLabels = new List { new("country", "us") }; + bool res39 = db.TS().Create( + "wind:3", + new TsCreateParamsBuilder() + .AddLabels(usLabels) + .build() + ); + Console.WriteLine(res39); // >>> True + + bool res40 = db.TS().Create( + "wind:4", + new TsCreateParamsBuilder() + .AddLabels(usLabels) + .build() + ); + Console.WriteLine(res40); // >>> True + + var windSequence1 = new List<(string, TimeStamp, double)> + { + ("wind:1", 1, 12), + ("wind:2", 1, 18), + ("wind:3", 1, 5), + ("wind:4", 1, 20) + }; + IReadOnlyList res41 = db.TS().MAdd(windSequence1); + Console.WriteLine( + $"[{string.Join(", ", res41.Select(t => t.Value))}]" + ); + // >>> [1, 1, 1, 1] + + var windSequence2 = new List<(string, TimeStamp, double)> + { + ("wind:1", 2, 14), + ("wind:2", 2, 21), + ("wind:3", 2, 4), + ("wind:4", 2, 25) + }; + IReadOnlyList res42 = db.TS().MAdd(windSequence2); + Console.WriteLine( + $"[{string.Join(", ", res42.Select(t => t.Value))}]" + ); + // >>> [2, 2, 2, 2] + + var windSequence3 = new List<(string, TimeStamp, double)> + { + ("wind:1", 3, 10), + ("wind:2", 3, 24), + ("wind:3", 3, 8), + ("wind:4", 3, 18) + }; + IReadOnlyList res43 = db.TS().MAdd(windSequence3); + Console.WriteLine( + $"[{string.Join(", ", res43.Select(t => t.Value))}]" + ); + // >>> [3, 3, 3, 3] + + // The result pairs contain the timestamp and the maximum sample value + // for the country at that timestamp. + var countryFilters = new List { "country=(us,uk)" }; + IReadOnlyList< + (string, IReadOnlyList, IReadOnlyList) + > res44 = db.TS().MRange( + "-", "+", + countryFilters, + groupbyTuple: ("country", TsReduce.Max) + ); + Console.WriteLine(res44.Count); // >>> 2 + + foreach (var (key, labelsResult, values) in res44) + { + Console.WriteLine($"{key}:"); + Console.WriteLine($" Values: [{string.Join(", ", values.Select(t => $"({t.Time.Value}, {t.Val})"))}]"); + } + // >>> country=uk + // >>> Values: [(1, 18), (2, 21), (3, 24)] + // >>> country=us + // >>> Values: [(1, 20), (2, 25), (3, 18)] + + // The result pairs contain the timestamp and the average sample value + // for the country at that timestamp. + IReadOnlyList< + (string, IReadOnlyList, IReadOnlyList) + > res45 = db.TS().MRange( + "-", "+", + countryFilters, + groupbyTuple: ("country", TsReduce.Avg) + ); + Console.WriteLine(res45.Count); // >>> 2 + + foreach (var (key, labelsResult, values) in res45) + { + Console.WriteLine($"{key}:"); + Console.WriteLine($" Values: [{string.Join(", ", values.Select(t => $"({t.Time.Value}, {t.Val})"))}]"); + } + // >>> country=uk + // >>> Values: [(1, 14), (2, 18), (3, 10)] + // >>> country=us + // >>> Values: [(1, 16), (2, 22), (3, 14)] + // STEP_END + // REMOVE_START + Assert.True(res37); + Assert.True(res38); + Assert.True(res39); + Assert.True(res40); + Assert.Equal(4, res41.Count); + Assert.Equal(4, res42.Count); + Assert.Equal(4, res43.Count); + Assert.Equal(2, res44.Count); + Assert.Equal(2, res45.Count); + // REMOVE_END + + // STEP_START create_compaction + bool res46 = db.TS().Create( + "hyg:1", + new TsCreateParamsBuilder().build() + ); + Console.WriteLine(res46); // >>> True + + bool res47 = db.TS().Create( + "hyg:compacted", + new TsCreateParamsBuilder().build() + ); + Console.WriteLine(res47); // >>> True + + var compactionRule = new TimeSeriesRule("hyg:compacted", 3, TsAggregation.Min); + bool res48 = db.TS().CreateRule("hyg:1", compactionRule); + Console.WriteLine(res48); // >>> True + + TimeSeriesInformation res49 = db.TS().Info("hyg:1"); + Console.WriteLine(res49.Rules!.Count); + // >>> 1 + + TimeSeriesInformation res50 = db.TS().Info("hyg:compacted"); + Console.WriteLine(res50.SourceKey); + // >>> hyg:1 + // STEP_END + // REMOVE_START + Assert.True(res46); + Assert.True(res47); + Assert.True(res48); + Assert.Single(res49.Rules!); + Assert.Equal("hyg:1", res50.SourceKey); + // REMOVE_END + + // STEP_START comp_add + // Recreate the compaction rule so this example runs on its own. + db.KeyDelete(new RedisKey[] { "hyg:1", "hyg:compacted" }); + db.TS().Create("hyg:1", new TsCreateParamsBuilder().build()); + db.TS().Create("hyg:compacted", new TsCreateParamsBuilder().build()); + db.TS().CreateRule("hyg:1", new TimeSeriesRule("hyg:compacted", 3, TsAggregation.Min)); + + var hygSequence1 = new List<(string, TimeStamp, double)> + { + ("hyg:1", 0, 75), + ("hyg:1", 1, 77), + ("hyg:1", 2, 78) + }; + IReadOnlyList res51 = db.TS().MAdd(hygSequence1); + Console.WriteLine($"[{string.Join(", ", res51.Select(t => t.Value))}]"); + // >>> [0, 1, 2] + + IReadOnlyList res52 = db.TS().Range("hyg:compacted", "-", "+"); + Console.WriteLine(res52.Count); // >>> 0 + + TimeStamp res53 = db.TS().Add( + "hyg:1", + new TsAddParamsBuilder() + .AddTimestamp(3) + .AddValue(79) + .build() + ); + Console.WriteLine(res53.Value); // >>> 3 + + IReadOnlyList res54 = db.TS().Range("hyg:compacted", "-", "+"); + Console.WriteLine( + $"[{string.Join(", ", res54.Select(t => $"({t.Time.Value}, {t.Val})"))}]" + ); + // >>> [(0, 75)] + // STEP_END + // REMOVE_START + Assert.Equal(3, res51.Count); + Assert.Empty(res52); + Assert.Equal(3, (long)res53); + Assert.Single(res54); + // REMOVE_END + + // STEP_START del + // Recreate the thermometer:1 series so this example runs on its own. + db.KeyDelete("thermometer:1"); + db.TS().Add("thermometer:1", 1, 9.2); + db.TS().Add("thermometer:1", 2, 9.9); + + TimeSeriesInformation res55 = db.TS().Info("thermometer:1"); + Console.WriteLine(res55.TotalSamples); // >>> 2 + Console.WriteLine(res55.FirstTimeStamp!); // >>> 1 + Console.WriteLine(res55.LastTimeStamp!); // >>> 2 + + TimeStamp res56 = db.TS().Add( + "thermometer:1", + new TsAddParamsBuilder() + .AddTimestamp(3) + .AddValue(9.7) + .build() + ); + Console.WriteLine(res56.Value); // >>> 3 + + TimeSeriesInformation res57 = db.TS().Info("thermometer:1"); + Console.WriteLine(res57.TotalSamples); // >>> 3 + Console.WriteLine(res57.FirstTimeStamp!); // >>> 1 + Console.WriteLine(res57.LastTimeStamp!); // >>> 3 + + long res58 = db.TS().Del("thermometer:1", 1, 2); + Console.WriteLine(res58); // >>> 2 + + TimeSeriesInformation res59 = db.TS().Info("thermometer:1"); + Console.WriteLine(res59.TotalSamples); // >>> 1 + Console.WriteLine(res59.FirstTimeStamp!); // >>> 3 + Console.WriteLine(res59.LastTimeStamp!); // >>> 3 + + long res60 = db.TS().Del("thermometer:1", 3, 3); + Console.WriteLine(res60); // >>> 1 + + TimeSeriesInformation res61 = db.TS().Info("thermometer:1"); + Console.WriteLine(res61.TotalSamples); // >>> 0 + // STEP_END + // REMOVE_START + Assert.Equal(2, res55.TotalSamples); + Assert.Equal(1, (long)res55.FirstTimeStamp!); + Assert.Equal(2, (long)res55.LastTimeStamp!); + Assert.Equal(3, (long)res56); + Assert.Equal(3, res57.TotalSamples); + Assert.Equal(1, (long)res57.FirstTimeStamp!); + Assert.Equal(3, (long)res57.LastTimeStamp!); + Assert.Equal(2, res58); + Assert.Equal(1, res59.TotalSamples); + Assert.Equal(3, (long)res59.FirstTimeStamp!); + Assert.Equal(3, (long)res59.LastTimeStamp!); + Assert.Equal(1, res60); + Assert.Equal(0, res61.TotalSamples); + // REMOVE_END + // HIDE_START + } +} +// HIDE_END diff --git a/local_examples/time_series_tutorial/redis-py/dt_time_series.py b/local_examples/time_series_tutorial/redis-py/dt_time_series.py index 4eed2a19b3..1db7705e9a 100644 --- a/local_examples/time_series_tutorial/redis-py/dt_time_series.py +++ b/local_examples/time_series_tutorial/redis-py/dt_time_series.py @@ -68,6 +68,11 @@ # REMOVE_END # STEP_START madd +# Recreate the thermometer series so this example runs on its own. +r.delete("thermometer:1", "thermometer:2") +r.ts().create("thermometer:1") +r.ts().add("thermometer:2", 1, 10.8) + res8 = r.ts().madd([ ("thermometer:1", 1, 9.2), ("thermometer:1", 2, 9.9), @@ -80,6 +85,10 @@ # REMOVE_END # STEP_START get +# Recreate the thermometer:2 series so this example runs on its own. +r.delete("thermometer:2") +r.ts().add("thermometer:2", 1, 10.8) +r.ts().add("thermometer:2", 2, 10.3) # The last recorded temperature for thermometer:2 # was 10.3 at time 2. res9 = r.ts().get("thermometer:2") @@ -135,6 +144,17 @@ # REMOVE_END # STEP_START range_filter +# Recreate the rg:1 series so this example runs on its own. +r.delete("rg:1") +r.ts().create("rg:1") +r.ts().madd([ + ("rg:1", 0, 18), + ("rg:1", 1, 14), + ("rg:1", 2, 22), + ("rg:1", 3, 18), + ("rg:1", 4, 24), +]) + res17 = r.ts().range("rg:1", "-", "+", filter_by_ts=[0, 2, 4]) print(res17) # >>> [(0, 18.0), (2, 22.0), (4, 24.0)] @@ -278,6 +298,17 @@ # REMOVE_END # STEP_START agg +# Recreate the rg:2 series so this example runs on its own. +r.delete("rg:2") +r.ts().create("rg:2") +r.ts().madd([ + ("rg:2", 0, 1.8), + ("rg:2", 1, 2.1), + ("rg:2", 2, 2.3), + ("rg:2", 3, 1.9), + ("rg:2", 4, 1.78), +]) + res32 = r.ts().range( "rg:2", "-", "+", aggregation_type="avg", @@ -323,6 +354,19 @@ # REMOVE_END # STEP_START agg_align +# Recreate the sensor3 series so this example runs on its own. +r.delete("sensor3") +r.ts().create("sensor3") +r.ts().madd([ + ("sensor3", 10, 1000), + ("sensor3", 20, 2000), + ("sensor3", 30, 3000), + ("sensor3", 40, 4000), + ("sensor3", 50, 5000), + ("sensor3", 60, 6000), + ("sensor3", 70, 7000), +]) + res36 = r.ts().range( "sensor3", 10, 70, aggregation_type="min", @@ -451,6 +495,12 @@ # REMOVE_END # STEP_START comp_add +# Recreate the compaction rule so this example runs on its own. +r.delete("hyg:1", "hyg:compacted") +r.ts().create("hyg:1") +r.ts().create("hyg:compacted") +r.ts().createrule("hyg:1", "hyg:compacted", "min", 3) + res50 = r.ts().madd([ ("hyg:1", 0, 75), ("hyg:1", 1, 77), @@ -475,6 +525,11 @@ # REMOVE_END # STEP_START del +# Recreate the thermometer:1 series so this example runs on its own. +r.delete("thermometer:1") +r.ts().add("thermometer:1", 1, 9.2) +r.ts().add("thermometer:1", 2, 9.9) + res54 = r.ts().info("thermometer:1") print(res54.total_samples) # >>> 2 print(res54.first_timestamp) # >>> 1 diff --git a/local_examples/tmp/datatypes/hashes/HashExample.cs b/local_examples/tmp/datatypes/hashes/HashExample.cs index cd8eb03ff2..acd90d75da 100644 --- a/local_examples/tmp/datatypes/hashes/HashExample.cs +++ b/local_examples/tmp/datatypes/hashes/HashExample.cs @@ -66,6 +66,15 @@ public void Run() //REMOVE_END //STEP_START hmget + // Recreate the bike:1 hash so this example runs on its own. + db.KeyDelete("bike:1"); + db.HashSet("bike:1", [ + new("model", "Deimos"), + new("brand", "Ergonom"), + new("type", "Enduro bikes"), + new("price", 4972) + ]); + var values = db.HashGet("bike:1", ["model", "price"]); Console.WriteLine(string.Join(" ", values)); // Deimos 4972 @@ -76,6 +85,15 @@ public void Run() //STEP_END //STEP_START hincrby + // Recreate the bike:1 hash so this example runs on its own. + db.KeyDelete("bike:1"); + db.HashSet("bike:1", [ + new("model", "Deimos"), + new("brand", "Ergonom"), + new("type", "Enduro bikes"), + new("price", 4972) + ]); + var newPrice = db.HashIncrement("bike:1", "price", 100); Console.WriteLine($"New price: {newPrice}"); //REMOVE_START diff --git a/local_examples/tmp/datatypes/hashes/HashExample.java b/local_examples/tmp/datatypes/hashes/HashExample.java index 8274dd173d..f4aee681a4 100644 --- a/local_examples/tmp/datatypes/hashes/HashExample.java +++ b/local_examples/tmp/datatypes/hashes/HashExample.java @@ -53,6 +53,10 @@ public void run() { // REMOVE_END // STEP_START hmget + // Recreate the bike:1 hash so this example runs on its own. + jedis.del("bike:1"); + jedis.hset("bike:1", bike1); + List res5 = jedis.hmget("bike:1", "model", "price"); System.out.println(res5); // [Deimos, 4972] // STEP_END @@ -62,6 +66,10 @@ public void run() { // REMOVE_END // STEP_START hincrby + // Recreate the bike:1 hash so this example runs on its own. + jedis.del("bike:1"); + jedis.hset("bike:1", bike1); + Long res6 = jedis.hincrBy("bike:1", "price", 100); System.out.println(res6); // 5072 Long res7 = jedis.hincrBy("bike:1", "price", -100); diff --git a/local_examples/tmp/datatypes/hashes/dt-hash.js b/local_examples/tmp/datatypes/hashes/dt-hash.js index 3496620e0e..fb4315e85f 100644 --- a/local_examples/tmp/datatypes/hashes/dt-hash.js +++ b/local_examples/tmp/datatypes/hashes/dt-hash.js @@ -50,6 +50,18 @@ assert.deepEqual(res4, { // REMOVE_END // STEP_START hmGet +// Recreate the bike:1 hash so this example runs on its own. +await client.del('bike:1') +await client.hSet( + 'bike:1', + { + 'model': 'Deimos', + 'brand': 'Ergonom', + 'type': 'Enduro bikes', + 'price': 4972, + } +) + const res5 = await client.hmGet('bike:1', ['model', 'price']) console.log(res5) // ['Deimos', '4972'] // STEP_END @@ -59,6 +71,18 @@ assert.deepEqual(Object.values(res5), ['Deimos', '4972']) // REMOVE_END // STEP_START hIncrBy +// Recreate the bike:1 hash so this example runs on its own. +await client.del('bike:1') +await client.hSet( + 'bike:1', + { + 'model': 'Deimos', + 'brand': 'Ergonom', + 'type': 'Enduro bikes', + 'price': 4972, + } +) + const res6 = await client.hIncrBy('bike:1', 'price', 100) console.log(res6) // 5072 const res7 = await client.hIncrBy('bike:1', 'price', -100) diff --git a/local_examples/tmp/datatypes/hashes/dt_hash.py b/local_examples/tmp/datatypes/hashes/dt_hash.py index 0ffdc793d9..f0445a8909 100644 --- a/local_examples/tmp/datatypes/hashes/dt_hash.py +++ b/local_examples/tmp/datatypes/hashes/dt_hash.py @@ -49,6 +49,18 @@ # REMOVE_END # STEP_START hmget +# Recreate the bike:1 hash so this example runs on its own. +r.delete("bike:1") +r.hset( + "bike:1", + mapping={ + "model": "Deimos", + "brand": "Ergonom", + "type": "Enduro bikes", + "price": 4972, + }, +) + res5 = r.hmget("bike:1", ["model", "price"]) print(res5) # >>> ['Deimos', '4972'] @@ -59,6 +71,18 @@ # REMOVE_END # STEP_START hincrby +# Recreate the bike:1 hash so this example runs on its own. +r.delete("bike:1") +r.hset( + "bike:1", + mapping={ + "model": "Deimos", + "brand": "Ergonom", + "type": "Enduro bikes", + "price": 4972, + }, +) + res6 = r.hincrby("bike:1", "price", 100) print(res6) # >>> 5072 diff --git a/local_examples/tmp/datatypes/hashes/hash_tutorial_test.go b/local_examples/tmp/datatypes/hashes/hash_tutorial_test.go index 359e9ec668..04b1546293 100644 --- a/local_examples/tmp/datatypes/hashes/hash_tutorial_test.go +++ b/local_examples/tmp/datatypes/hashes/hash_tutorial_test.go @@ -107,23 +107,19 @@ func ExampleClient_hmget() { // REMOVE_START // start with fresh database rdb.FlushDB(ctx) - rdb.Del(ctx, "bike:1") // REMOVE_END + // STEP_START hmget + // Recreate the bike:1 hash so this example runs on its own. + rdb.Del(ctx, "bike:1") hashFields := []string{ "model", "Deimos", "brand", "Ergonom", "type", "Enduro bikes", "price", "4972", } + rdb.HSet(ctx, "bike:1", hashFields) - _, err := rdb.HSet(ctx, "bike:1", hashFields).Result() - - if err != nil { - panic(err) - } - - // STEP_START hmget cmdReturn := rdb.HMGet(ctx, "bike:1", "model", "price") res5, err := cmdReturn.Result() @@ -167,23 +163,19 @@ func ExampleClient_hincrby() { // REMOVE_START // start with fresh database rdb.FlushDB(ctx) - rdb.Del(ctx, "bike:1") // REMOVE_END + // STEP_START hincrby + // Recreate the bike:1 hash so this example runs on its own. + rdb.Del(ctx, "bike:1") hashFields := []string{ "model", "Deimos", "brand", "Ergonom", "type", "Enduro bikes", "price", "4972", } + rdb.HSet(ctx, "bike:1", hashFields) - _, err := rdb.HSet(ctx, "bike:1", hashFields).Result() - - if err != nil { - panic(err) - } - - // STEP_START hincrby res6, err := rdb.HIncrBy(ctx, "bike:1", "price", 100).Result() if err != nil { diff --git a/local_examples/tmp/datatypes/sets/SetsExample.java b/local_examples/tmp/datatypes/sets/SetsExample.java index a4802ee5ef..8d8ac859bb 100644 --- a/local_examples/tmp/datatypes/sets/SetsExample.java +++ b/local_examples/tmp/datatypes/sets/SetsExample.java @@ -106,6 +106,10 @@ public void run() { // REMOVE_END // STEP_START smismember + // Recreate the set so this example runs on its own. + jedis.del("bikes:racing:france"); + jedis.sadd("bikes:racing:france", "bike:1", "bike:2", "bike:3"); + boolean res11 = jedis.sismember("bikes:racing:france", "bike:1"); System.out.println(res11); // >>> true diff --git a/local_examples/tmp/datatypes/sets/SetsTutorial.cs b/local_examples/tmp/datatypes/sets/SetsTutorial.cs index e5a57a5ead..b220c8a74a 100644 --- a/local_examples/tmp/datatypes/sets/SetsTutorial.cs +++ b/local_examples/tmp/datatypes/sets/SetsTutorial.cs @@ -125,6 +125,10 @@ public void Run() // STEP_START smismember + // Recreate the set so this example runs on its own. + db.KeyDelete("bikes:racing:france"); + db.SetAdd("bikes:racing:france", ["bike:1", "bike:2", "bike:3"]); + bool res14 = db.SetContains("bikes:racing:france", "bike:1"); Console.WriteLine(res14); // >>> true diff --git a/local_examples/tmp/datatypes/sets/dt-set.js b/local_examples/tmp/datatypes/sets/dt-set.js index 959e2081fa..194854f49e 100644 --- a/local_examples/tmp/datatypes/sets/dt-set.js +++ b/local_examples/tmp/datatypes/sets/dt-set.js @@ -97,6 +97,10 @@ assert.deepEqual(res10.sort(), ['bike:1', 'bike:2', 'bike:3']) // REMOVE_END // STEP_START smIsMember +// Recreate the set so this example runs on its own. +await client.del('bikes:racing:france') +await client.sAdd('bikes:racing:france', ['bike:1', 'bike:2', 'bike:3']) + const res11 = await client.sIsMember('bikes:racing:france', 'bike:1') console.log(res11) // >>> 1 diff --git a/local_examples/tmp/datatypes/sets/dt_set.py b/local_examples/tmp/datatypes/sets/dt_set.py index 974261c3e3..89d5a0bd74 100644 --- a/local_examples/tmp/datatypes/sets/dt_set.py +++ b/local_examples/tmp/datatypes/sets/dt_set.py @@ -94,6 +94,10 @@ # REMOVE_END # STEP_START smismember +# Recreate the set so this example runs on its own. +r.delete("bikes:racing:france") +r.sadd("bikes:racing:france", "bike:1", "bike:2", "bike:3") + res11 = r.sismember("bikes:racing:france", "bike:1") print(res11) # >>> 1 diff --git a/local_examples/tmp/datatypes/sorted-sets/SortedSetExample.cs b/local_examples/tmp/datatypes/sorted-sets/SortedSetExample.cs index 3755cc9db5..7f15deee1e 100644 --- a/local_examples/tmp/datatypes/sorted-sets/SortedSetExample.cs +++ b/local_examples/tmp/datatypes/sorted-sets/SortedSetExample.cs @@ -120,6 +120,14 @@ public void Run() //REMOVE_END //STEP_START zrank + // Recreate the three remaining racers so this example runs on its own. + db.KeyDelete("racer_scores"); + db.SortedSetAdd("racer_scores", [ + new("Norem", 10), + new("Royce", 10), + new("Prickett", 14) + ]); + long? res11 = db.SortedSetRank("racer_scores", "Norem"); Console.WriteLine(res11); // >>> 0 //REMOVE_START diff --git a/local_examples/tmp/datatypes/sorted-sets/SortedSetsExample.java b/local_examples/tmp/datatypes/sorted-sets/SortedSetsExample.java index 6d1b801933..28792bf6f9 100644 --- a/local_examples/tmp/datatypes/sorted-sets/SortedSetsExample.java +++ b/local_examples/tmp/datatypes/sorted-sets/SortedSetsExample.java @@ -58,7 +58,7 @@ public void run() { //STEP_END //STEP_START zrangebyscore - List res7 = jedis.zrangeByScore("racer_scores", Double.MIN_VALUE, 10d); + List res7 = jedis.zrangeByScore("racer_scores", Double.NEGATIVE_INFINITY, 10d); System.out.println(res7); // >>> [Ford, Sam-Bodden, Norem, Royce] //STEP_END @@ -66,7 +66,7 @@ public void run() { long res8 = jedis.zrem("racer_scores", "Castilla"); System.out.println(res8); // >>> 1 - long res9 = jedis.zremrangeByScore("racer_scores", Double.MIN_VALUE, 9d); + long res9 = jedis.zremrangeByScore("racer_scores", Double.NEGATIVE_INFINITY, 9d); System.out.println(res9); // >>> 2 List res10 = jedis.zrange("racer_scores", 0, -1); @@ -78,6 +78,14 @@ public void run() { //REMOVE_END //STEP_START zrank + // Recreate the three remaining racers so this example runs on its own. + jedis.del("racer_scores"); + jedis.zadd("racer_scores", new HashMap() {{ + put("Norem", 10d); + put("Royce", 10d); + put("Prickett", 14d); + }}); + long res11 = jedis.zrank("racer_scores", "Norem"); System.out.println(res11); // >>> 0 diff --git a/local_examples/tmp/datatypes/sorted-sets/dt-ss.js b/local_examples/tmp/datatypes/sorted-sets/dt-ss.js index b3ed163bbd..4f5b362ac8 100644 --- a/local_examples/tmp/datatypes/sorted-sets/dt-ss.js +++ b/local_examples/tmp/datatypes/sorted-sets/dt-ss.js @@ -99,6 +99,14 @@ console.assert(count2 === 3); // REMOVE_END // STEP_START zrank +// Recreate the three remaining racers so this example runs on its own. +await client.del('racer_scores'); +await client.zAdd('racer_scores', [ + { score: 10, value: 'Norem' }, + { score: 10, value: 'Royce' }, + { score: 14, value: 'Prickett' } +]); + const res11 = await client.zRank('racer_scores', 'Norem'); console.log(res11); // >>> 0 diff --git a/local_examples/tmp/datatypes/sorted-sets/dt_ss.py b/local_examples/tmp/datatypes/sorted-sets/dt_ss.py index 7392856bc3..f900b170f6 100644 --- a/local_examples/tmp/datatypes/sorted-sets/dt_ss.py +++ b/local_examples/tmp/datatypes/sorted-sets/dt_ss.py @@ -73,6 +73,13 @@ # REMOVE_END # STEP_START zrank +# Recreate the three remaining racers so this example runs on its own. +r.delete("racer_scores") +r.zadd( + "racer_scores", + {"Norem": 10, "Royce": 10, "Prickett": 14}, +) + res11 = r.zrank("racer_scores", "Norem") print(res11) # >>> 0 diff --git a/local_examples/tmp/datatypes/sorted-sets/ss_tutorial_test.go b/local_examples/tmp/datatypes/sorted-sets/ss_tutorial_test.go index f692e597ac..50656c4d4a 100644 --- a/local_examples/tmp/datatypes/sorted-sets/ss_tutorial_test.go +++ b/local_examples/tmp/datatypes/sorted-sets/ss_tutorial_test.go @@ -283,20 +283,17 @@ func ExampleClient_zrank() { // REMOVE_START // start with fresh database rdb.FlushDB(ctx) - rdb.Del(ctx, "racer_scores") // REMOVE_END - _, err := rdb.ZAdd(ctx, "racer_scores", + // STEP_START zrank + // Recreate the three remaining racers so this example runs on its own. + rdb.Del(ctx, "racer_scores") + rdb.ZAdd(ctx, "racer_scores", redis.Z{Member: "Norem", Score: 10}, redis.Z{Member: "Royce", Score: 10}, redis.Z{Member: "Prickett", Score: 14}, - ).Result() + ) - if err != nil { - panic(err) - } - - // STEP_START zrank res11, err := rdb.ZRank(ctx, "racer_scores", "Norem").Result() if err != nil { diff --git a/local_examples/tmp/datatypes/strings/StringExample.java b/local_examples/tmp/datatypes/strings/StringExample.java index 1cd2982255..9038cc31ee 100644 --- a/local_examples/tmp/datatypes/strings/StringExample.java +++ b/local_examples/tmp/datatypes/strings/StringExample.java @@ -34,6 +34,9 @@ public void run() { // REMOVE_END // STEP_START setnx_xx + // Recreate the bike:1 key so this example runs on its own. + jedis.set("bike:1", "Deimos"); + Long res3 = jedis.setnx("bike:1", "bike"); System.out.println(res3); // 0 (because key already exists) System.out.println(jedis.get("bike:1")); // Deimos (value is unchanged) diff --git a/local_examples/tmp/datatypes/strings/StringSnippets.cs b/local_examples/tmp/datatypes/strings/StringSnippets.cs index 2e3d79ab7a..6d52b61d6b 100644 --- a/local_examples/tmp/datatypes/strings/StringSnippets.cs +++ b/local_examples/tmp/datatypes/strings/StringSnippets.cs @@ -49,6 +49,9 @@ public void Run() //REMOVE_END //STEP_START setnx_xx + // Recreate the bike:1 key so this example runs on its own. + db.StringSet("bike:1", "Deimos"); + var res3 = db.StringSet("bike:1", "bike", when: When.NotExists); Console.WriteLine(res3); // false Console.WriteLine(db.StringGet("bike:1")); diff --git a/local_examples/tmp/datatypes/strings/dt-string.js b/local_examples/tmp/datatypes/strings/dt-string.js index 3d9ab300ec..3f96ce2d33 100644 --- a/local_examples/tmp/datatypes/strings/dt-string.js +++ b/local_examples/tmp/datatypes/strings/dt-string.js @@ -24,6 +24,9 @@ assert.equal(res2, 'Deimos'); // REMOVE_END // STEP_START setnx_xx +// Recreate the bike:1 key so this example runs on its own. +await client.set("bike:1", "Deimos"); + const res3 = await client.set("bike:1", "bike", {'NX': true}); console.log(res3); // null console.log(await client.get("bike:1")); // Deimos diff --git a/local_examples/tmp/datatypes/strings/dt_string.py b/local_examples/tmp/datatypes/strings/dt_string.py index 7ca21fda5b..cbb2092d69 100644 --- a/local_examples/tmp/datatypes/strings/dt_string.py +++ b/local_examples/tmp/datatypes/strings/dt_string.py @@ -24,6 +24,9 @@ # REMOVE_END # STEP_START setnx_xx +# Recreate the bike:1 key so this example runs on its own. +r.set("bike:1", "Deimos") + res3 = r.set("bike:1", "bike", nx=True) print(res3) # None print(r.get("bike:1")) # Deimos diff --git a/local_examples/tmp/datatypes/strings/string_example_test.go b/local_examples/tmp/datatypes/strings/string_example_test.go index b3765478a3..5df48bdd63 100644 --- a/local_examples/tmp/datatypes/strings/string_example_test.go +++ b/local_examples/tmp/datatypes/strings/string_example_test.go @@ -61,10 +61,12 @@ func ExampleClient_setnx_xx() { // REMOVE_START // start with fresh database rdb.FlushDB(ctx) - rdb.Set(ctx, "bike:1", "Deimos", 0) // REMOVE_END // STEP_START setnx_xx + // Recreate the bike:1 key so this example runs on its own. + rdb.Set(ctx, "bike:1", "Deimos", 0) + res3, err := rdb.SetNX(ctx, "bike:1", "bike", 0).Result() if err != nil { diff --git a/local_examples/tmp/lettuce-async/HashExample.java b/local_examples/tmp/lettuce-async/HashExample.java new file mode 100644 index 0000000000..a1eafbe2f1 --- /dev/null +++ b/local_examples/tmp/lettuce-async/HashExample.java @@ -0,0 +1,171 @@ +// EXAMPLE: hash_tutorial +package io.redis.examples.async; + +import io.lettuce.core.*; +import io.lettuce.core.api.async.RedisAsyncCommands; +import io.lettuce.core.api.StatefulRedisConnection; + +// REMOVE_START +import org.junit.jupiter.api.Test; +// REMOVE_END +import java.util.*; +import java.util.concurrent.CompletableFuture; +// REMOVE_START +import static org.assertj.core.api.Assertions.assertThat; +// REMOVE_END + +public class HashExample { + + @Test + public void run() { + RedisClient redisClient = RedisClient.create("redis://localhost:6379"); + + try (StatefulRedisConnection connection = redisClient.connect()) { + RedisAsyncCommands asyncCommands = connection.async(); + // REMOVE_START + CompletableFuture delResult = asyncCommands.del("bike:1", "bike:1:stats").toCompletableFuture(); + + // REMOVE_END + + // STEP_START set_get_all + Map bike1 = new HashMap<>(); + bike1.put("model", "Deimos"); + bike1.put("brand", "Ergonom"); + bike1.put("type", "Enduro bikes"); + bike1.put("price", "4972"); + + CompletableFuture setGetAll = asyncCommands.hset("bike:1", bike1).thenCompose(res1 -> { + System.out.println(res1); // >>> 4 + // REMOVE_START + assertThat(res1).isEqualTo(4); + // REMOVE_END + return asyncCommands.hget("bike:1", "model"); + }).thenCompose(res2 -> { + System.out.println(res2); // >>> Deimos + // REMOVE_START + assertThat(res2).isEqualTo("Deimos"); + // REMOVE_END + return asyncCommands.hget("bike:1", "price"); + }).thenCompose(res3 -> { + System.out.println(res3); // >>> 4972 + // REMOVE_START + assertThat(res3).isEqualTo("4972"); + // REMOVE_END + return asyncCommands.hgetall("bike:1"); + }) + // REMOVE_START + .thenApply(res -> { + assertThat(res.get("type")).isEqualTo("Enduro bikes"); + assertThat(res.get("brand")).isEqualTo("Ergonom"); + assertThat(res.get("price")).isEqualTo("4972"); + assertThat(res.get("model")).isEqualTo("Deimos"); + + return res; + }) + // REMOVE_END + .thenAccept(System.out::println) + // >>> {type=Enduro bikes, brand=Ergonom, price=4972, model=Deimos} + .toCompletableFuture(); + // STEP_END + + // STEP_START hmget + // Recreate the bike:1 hash so this example runs on its own. + CompletableFuture hmGet = setGetAll + .thenCompose(res4 -> asyncCommands.del("bike:1")) + .thenCompose(delRes -> asyncCommands.hset("bike:1", bike1)) + .thenCompose(hsetRes -> asyncCommands.hmget("bike:1", "model", "price")) + // REMOVE_START + .thenApply(res -> { + assertThat(res.toString()).isEqualTo("[KeyValue[model, Deimos], KeyValue[price, 4972]]"); + return res; + }) + // REMOVE_END + .thenAccept(System.out::println) + // [KeyValue[model, Deimos], KeyValue[price, 4972]] + .toCompletableFuture(); + // STEP_END + + // STEP_START hincrby + // Recreate the bike:1 hash so this example runs on its own. + CompletableFuture hIncrBy = hmGet + .thenCompose(r -> asyncCommands.del("bike:1")) + .thenCompose(delRes -> asyncCommands.hset("bike:1", bike1)) + .thenCompose(hsetRes -> asyncCommands.hincrby("bike:1", "price", 100)) + .thenCompose(res6 -> { + System.out.println(res6); // >>> 5072 + // REMOVE_START + assertThat(res6).isEqualTo(5072L); + // REMOVE_END + return asyncCommands.hincrby("bike:1", "price", -100); + }) + // REMOVE_START + .thenApply(res -> { + assertThat(res).isEqualTo(4972L); + return res; + }) + // REMOVE_END + .thenAccept(System.out::println) + // >>> 4972 + .toCompletableFuture(); + // STEP_END + + // STEP_START incrby_get_mget + CompletableFuture incrByGetMget = asyncCommands.hincrby("bike:1:stats", "rides", 1).thenCompose(res7 -> { + System.out.println(res7); // >>> 1 + // REMOVE_START + assertThat(res7).isEqualTo(1L); + // REMOVE_END + return asyncCommands.hincrby("bike:1:stats", "rides", 1); + }).thenCompose(res8 -> { + System.out.println(res8); // >>> 2 + // REMOVE_START + assertThat(res8).isEqualTo(2L); + // REMOVE_END + return asyncCommands.hincrby("bike:1:stats", "rides", 1); + }).thenCompose(res9 -> { + System.out.println(res9); // >>> 3 + // REMOVE_START + assertThat(res9).isEqualTo(3L); + // REMOVE_END + return asyncCommands.hincrby("bike:1:stats", "crashes", 1); + }).thenCompose(res10 -> { + System.out.println(res10); // >>> 1 + // REMOVE_START + assertThat(res10).isEqualTo(1L); + // REMOVE_END + return asyncCommands.hincrby("bike:1:stats", "owners", 1); + }).thenCompose(res11 -> { + System.out.println(res11); // >>> 1 + // REMOVE_START + assertThat(res11).isEqualTo(1L); + // REMOVE_END + return asyncCommands.hget("bike:1:stats", "rides"); + }).thenCompose(res12 -> { + System.out.println(res12); // >>> 3 + // REMOVE_START + assertThat(res12).isEqualTo("3"); + // REMOVE_END + return asyncCommands.hmget("bike:1:stats", "crashes", "owners"); + }) + // REMOVE_START + .thenApply(res -> { + assertThat(res.toString()).isEqualTo("[KeyValue[crashes, 1], KeyValue[owners, 1]]"); + return res; + }) + // REMOVE_END + .thenAccept(System.out::println) + // >>> [KeyValue[crashes, 1], KeyValue[owners, 1]] + .toCompletableFuture(); + // STEP_END + + CompletableFuture.allOf( + // REMOVE_START + delResult, + // REMOVE_END + hIncrBy, incrByGetMget).join(); + } finally { + redisClient.shutdown(); + } + } + +} diff --git a/local_examples/tmp/lettuce-async/SetExample.java b/local_examples/tmp/lettuce-async/SetExample.java new file mode 100644 index 0000000000..dfb361f664 --- /dev/null +++ b/local_examples/tmp/lettuce-async/SetExample.java @@ -0,0 +1,226 @@ +// EXAMPLE: sets_tutorial +package io.redis.examples.async; + +import io.lettuce.core.*; +import io.lettuce.core.api.async.RedisAsyncCommands; +import io.lettuce.core.api.StatefulRedisConnection; +// REMOVE_START +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +// REMOVE_END + +import java.util.*; +import java.util.concurrent.CompletableFuture; + +public class SetExample { + + // REMOVE_START + @Test + // REMOVE_END + public void run() { + RedisClient redisClient = RedisClient.create("redis://localhost:6379"); + + try (StatefulRedisConnection connection = redisClient.connect()) { + RedisAsyncCommands asyncCommands = connection.async(); + // REMOVE_START + // Clean up any existing data + asyncCommands.del("bikes:racing:france", "bikes:racing:usa", "bikes:racing:italy") + .toCompletableFuture().join(); + // REMOVE_END + + // STEP_START sadd + CompletableFuture sAdd = asyncCommands.sadd("bikes:racing:france", "bike:1").thenCompose(res1 -> { + System.out.println(res1); // >>> 1 + // REMOVE_START + assertThat(res1).isEqualTo(1L); + // REMOVE_END + return asyncCommands.sadd("bikes:racing:france", "bike:1"); + }).thenCompose(res2 -> { + System.out.println(res2); // >>> 0 + // REMOVE_START + assertThat(res2).isEqualTo(0L); + // REMOVE_END + return asyncCommands.sadd("bikes:racing:france", "bike:2", "bike:3"); + }).thenCompose(res3 -> { + System.out.println(res3); // >>> 2 + // REMOVE_START + assertThat(res3).isEqualTo(2L); + // REMOVE_END + return asyncCommands.sadd("bikes:racing:usa", "bike:1", "bike:4"); + }).thenAccept(res4 -> { + System.out.println(res4); // >>> 2 + // REMOVE_START + assertThat(res4).isEqualTo(2L); + // REMOVE_END + }).toCompletableFuture(); + // STEP_END + sAdd.join(); + + // STEP_START sismember + CompletableFuture sIsMember = asyncCommands.sismember("bikes:racing:usa", "bike:1").thenCompose(res5 -> { + System.out.println(res5); // >>> true + // REMOVE_START + assertThat(res5).isTrue(); + // REMOVE_END + return asyncCommands.sismember("bikes:racing:usa", "bike:2"); + }).thenAccept(res6 -> { + System.out.println(res6); // >>> false + // REMOVE_START + assertThat(res6).isFalse(); + // REMOVE_END + }).toCompletableFuture(); + // STEP_END + + // STEP_START sinter + CompletableFuture sInter = asyncCommands.sinter("bikes:racing:france", "bikes:racing:usa") + .thenAccept(res7 -> { + System.out.println(res7); // >>> [bike:1] + // REMOVE_START + assertThat(res7).containsExactly("bike:1"); + // REMOVE_END + }).toCompletableFuture(); + // STEP_END + + // STEP_START scard + CompletableFuture sCard = asyncCommands.scard("bikes:racing:france").thenAccept(res8 -> { + System.out.println(res8); // >>> 3 + // REMOVE_START + assertThat(res8).isEqualTo(3L); + // REMOVE_END + }).toCompletableFuture(); + // STEP_END + + CompletableFuture.allOf(sIsMember, sInter, sCard).join(); + + // STEP_START sadd_smembers + CompletableFuture sAddSMembers = asyncCommands.sadd("bikes:racing:france", "bike:1", "bike:2", "bike:3") + .thenCompose(res9 -> { + System.out.println(res9); // >>> 3 + // REMOVE_START + assertThat(res9).isEqualTo(0L); + // REMOVE_END + return asyncCommands.smembers("bikes:racing:france"); + }).thenAccept(res10 -> { + System.out.println(res10); // >>> [bike:1, bike:2, bike:3] + // REMOVE_START + assertThat(res10).containsExactlyInAnyOrder("bike:1", "bike:2", "bike:3"); + // REMOVE_END + }).toCompletableFuture(); + // STEP_END + sAddSMembers.join(); + + // STEP_START smismember + // Recreate the set so this example runs on its own. The chain is + // ordered, so the DEL + SADD complete before the reads below. + CompletableFuture sMIsMember = asyncCommands.del("bikes:racing:france") + .thenCompose(d -> asyncCommands.sadd("bikes:racing:france", "bike:1", "bike:2", "bike:3")) + .thenCompose(a -> asyncCommands.sismember("bikes:racing:france", "bike:1")).thenCompose(res11 -> { + System.out.println(res11); // >>> true + // REMOVE_START + assertThat(res11).isTrue(); + // REMOVE_END + return asyncCommands.smismember("bikes:racing:france", "bike:2", "bike:3", "bike:4"); + }).thenAccept(res12 -> { + System.out.println(res12); // >>> [true, true, false] + // REMOVE_START + assertThat(res12).containsExactly(true, true, false); + // REMOVE_END + }).toCompletableFuture(); + // STEP_END + sMIsMember.join(); + + // STEP_START sdiff + CompletableFuture sDiff = asyncCommands.sdiff("bikes:racing:france", "bikes:racing:usa") + .thenAccept(res13 -> { + System.out.println(res13); // >>> [bike:2, bike:3] + // REMOVE_START + assertThat(res13).containsExactlyInAnyOrder("bike:2", "bike:3"); + // REMOVE_END + }).toCompletableFuture(); + // STEP_END + sDiff.join(); + + // STEP_START multisets + CompletableFuture multisets = asyncCommands.sadd("bikes:racing:france", "bike:1", "bike:2", "bike:3") + .thenCompose(res14 -> { + System.out.println(res14); // >>> 0 + // REMOVE_START + assertThat(res14).isEqualTo(0L); + // REMOVE_END + return asyncCommands.sadd("bikes:racing:usa", "bike:1", "bike:4"); + }).thenCompose(res15 -> { + System.out.println(res15); // >>> 0 + // REMOVE_START + assertThat(res15).isEqualTo(0L); + // REMOVE_END + return asyncCommands.sadd("bikes:racing:italy", "bike:1", "bike:2", "bike:3", "bike:4"); + }).thenCompose(res16 -> { + System.out.println(res16); // >>> 4 + // REMOVE_START + assertThat(res16).isEqualTo(4L); + // REMOVE_END + return asyncCommands.sinter("bikes:racing:france", "bikes:racing:usa", "bikes:racing:italy"); + }).thenCompose(res17 -> { + System.out.println(res17); // >>> [bike:1] + // REMOVE_START + assertThat(res17).containsExactly("bike:1"); + // REMOVE_END + return asyncCommands.sunion("bikes:racing:france", "bikes:racing:usa", "bikes:racing:italy"); + }).thenCompose(res18 -> { + System.out.println(res18); // >>> [bike:1, bike:2, bike:3, bike:4] + // REMOVE_START + assertThat(res18).containsExactlyInAnyOrder("bike:1", "bike:2", "bike:3", "bike:4"); + // REMOVE_END + return asyncCommands.sdiff("bikes:racing:france", "bikes:racing:usa", "bikes:racing:italy"); + }).thenCompose(res19 -> { + System.out.println(res19); // >>> [] + // REMOVE_START + assertThat(res19).isEmpty(); + // REMOVE_END + return asyncCommands.sdiff("bikes:racing:usa", "bikes:racing:france"); + }).thenCompose(res20 -> { + System.out.println(res20); // >>> [bike:4] + // REMOVE_START + assertThat(res20).containsExactly("bike:4"); + // REMOVE_END + return asyncCommands.sdiff("bikes:racing:france", "bikes:racing:usa"); + }).thenAccept(res21 -> { + System.out.println(res21); // >>> [bike:2, bike:3] + // REMOVE_START + assertThat(res21).containsExactlyInAnyOrder("bike:2", "bike:3"); + // REMOVE_END + }).toCompletableFuture(); + // STEP_END + multisets.join(); + + // STEP_START srem + CompletableFuture sRem = asyncCommands + .sadd("bikes:racing:france", "bike:1", "bike:2", "bike:3", "bike:4", "bike:5").thenCompose(res22 -> { + System.out.println(res22); // >>> 2 + // REMOVE_START + assertThat(res22).isEqualTo(2L); + // REMOVE_END + return asyncCommands.srem("bikes:racing:france", "bike:1"); + }).thenCompose(res23 -> { + System.out.println(res23); // >>> 1 + // REMOVE_START + assertThat(res23).isEqualTo(1L); + // REMOVE_END + return asyncCommands.spop("bikes:racing:france"); + }).thenCompose(res24 -> { + System.out.println(res24); // >>> bike:3 (for example) + return asyncCommands.smembers("bikes:racing:france"); + }).thenCompose(res25 -> { + System.out.println(res25); // >>> [bike:2, bike:4, bike:5] (for example) + return asyncCommands.srandmember("bikes:racing:france"); + }).thenAccept(res26 -> { + System.out.println(res26); // >>> bike:4 (for example) + }).toCompletableFuture(); + // STEP_END + sRem.join(); + } finally { + redisClient.shutdown(); + } + } + +} diff --git a/local_examples/tmp/lettuce-reactive/HashExample.java b/local_examples/tmp/lettuce-reactive/HashExample.java index 44f9678438..d4cf51e33a 100644 --- a/local_examples/tmp/lettuce-reactive/HashExample.java +++ b/local_examples/tmp/lettuce-reactive/HashExample.java @@ -73,7 +73,10 @@ public void run() { // STEP_END // STEP_START hmget - Mono>> hmGet = reactiveCommands.hmget("bike:1", "model", "price").collectList() + // Recreate the bike:1 hash so this example runs on its own. + Mono>> hmGet = reactiveCommands.del("bike:1") + .then(reactiveCommands.hset("bike:1", bike1)) + .then(reactiveCommands.hmget("bike:1", "model", "price").collectList()) .doOnNext(result -> { System.out.println(result); // >>> [KeyValue[model, Deimos], KeyValue[price, 4972]] @@ -85,10 +88,15 @@ public void run() { }); // STEP_END - Mono.when(getModel, getPrice, getAll, hmGet).block(); + // Run the set_get_all reads together, then the self-contained hmget. + Mono.when(getModel, getPrice, getAll).block(); + hmGet.block(); // STEP_START hincrby - Mono hIncrBy = reactiveCommands.hincrby("bike:1", "price", 100).doOnNext(result -> { + // Recreate the bike:1 hash so this example runs on its own. + Mono hIncrBy = reactiveCommands.del("bike:1") + .then(reactiveCommands.hset("bike:1", bike1)) + .then(reactiveCommands.hincrby("bike:1", "price", 100)).doOnNext(result -> { System.out.println(result); // >>> 5072 // REMOVE_START assertThat(result).isEqualTo(5072L); diff --git a/local_examples/tmp/lettuce-reactive/SetExample.java b/local_examples/tmp/lettuce-reactive/SetExample.java index 30808e2d0d..87312431d6 100644 --- a/local_examples/tmp/lettuce-reactive/SetExample.java +++ b/local_examples/tmp/lettuce-reactive/SetExample.java @@ -108,6 +108,13 @@ public void run() { sAddSMembers.block(); // STEP_START smismember + // Recreate the set so this example runs on its own. Block it + // sequentially: a DEL inside the shared Mono.when() below would + // race the sibling reads on the same key. + reactiveCommands.del("bikes:racing:france") + .then(reactiveCommands.sadd("bikes:racing:france", "bike:1", "bike:2", "bike:3")) + .block(); + Mono sIsMember3 = reactiveCommands.sismember("bikes:racing:france", "bike:1").doOnNext(result -> { System.out.println(result); // >>> true // REMOVE_START