Compare String values
For the complete documentation index see: llms.txt
All documentation pages available in markdown.
The expression comparison operators — eq, ne, gt, ge, lt, and le, available on String values since Database 5.2 — order String values by UTF-8 bytes.
String search operations and normalize_nfc (Database 8.2.0 and later) treat canonically equivalent spellings as the same text.
Those two rules disagree on mixed Unicode forms of the same word.
Byte order
Expression comparison operators order String values by UTF-8 bytes, which matches Unicode code-point order. All ASCII sorts before any accented letter, and every uppercase ASCII letter sorts before every lowercase ASCII letter. When one value is a prefix of the other, the shorter value sorts first.
Consequences of that order:
"Zebra" < "apple"istrue, becauseZsorts beforea."Ålesund" > "Zurich"istrue, becauseÅis a multi-byte UTF-8 letter and sorts after every ASCII letter.
This is the same byte order a List or Map uses to order String elements, which Order and compare collection elements covers for every data type.
Worked example
Seven stored city names, with Málaga stored twice: once with a precomposed á (U+00E1), and once as a plus a combining acute accent (U+0301).
The two Málaga spellings look the same on screen.
| # | Byte order (expression eq, ne, gt, ge, lt, le) | Typical dictionary order |
|---|---|---|
| 1 | Málaga (a + combining acute) | Ålesund |
| 2 | Málaga (precomposed á) | bergen |
| 3 | São Paulo | Málaga |
| 4 | Zurich | Málaga |
| 5 | bergen | Östersund |
| 6 | Ålesund | São Paulo |
| 7 | Östersund | Zurich |
A letter-range filter follows byte order, not dictionary order.
Over the list above, city >= "A" AND city < "B" matches no record: Ålesund is the value a dictionary range would return, and it sorts after Z.
Equality and search disagree
Canonical equivalence means two Unicode spellings represent the same text: a precomposed é (U+00E9) and an e followed by a combining acute accent (U+0301) are equivalent.
Unicode Normalization Form C (NFC) stores the precomposed letter.
Normalization Form D (NFD) stores the letter plus the combining mark.
For a bin holding NFC café (precomposed é), compared with NFD café (e + U+0301):
contains,find,starts_with, andends_withmatch.containsreturnstrue.findreturns a non-negative index.replaceandreplace_allsubstitute that spelling.- Expression
eqreturnsfalse.
The six search operations treat the two spellings as the same text.
eq compares UTF-8 bytes, so the two spellings are not equal.
split, regex_compare, and regex_replace do not use canonical equivalence either; they match on exact code points.
// city holds NFC "café" (precomposed é, U+00E9)String eqExp = "$.city:STRING == 'cafe\u0301'";// eqExp evaluates to false — eq compares UTF-8 bytesString containsExp = "$.city:STRING.contains(needle: 'cafe\u0301')";// containsExp evaluates to true — search uses canonical equivalence# city holds NFC "café" (precomposed é, U+00E9)eq_exp = "$.city:STRING == 'cafe\u0301'"# eq_exp evaluates to False — eq compares UTF-8 bytescontains_exp = "$.city:STRING.contains(needle: 'cafe\u0301')"# contains_exp evaluates to True — search uses canonical equivalenceuse aerospike::expressions::{eq, string as str_exp, string_bin, string_val};
// city holds NFC "café" (precomposed é, U+00E9)let nfd = "cafe\u{301}";let eq_exp = eq(string_bin("city".into()), string_val(nfd.into()));// eq_exp evaluates to false — eq compares UTF-8 byteslet contains_exp = str_exp::contains(string_bin("city".into()), string_val(nfd.into()));// contains_exp evaluates to true — search uses canonical equivalence// city holds NFC "café" (precomposed é, U+00E9)string nfd = "cafe\u0301";Expression eqExp = Exp.Build(Exp.EQ(Exp.StringBin("city"), Exp.Val(nfd)));// eqExp evaluates to false — eq compares UTF-8 bytesExpression containsExp = Exp.Build(StringExp.Contains(Exp.Val(nfd), Exp.StringBin("city")));// containsExp evaluates to true — search uses canonical equivalence// Requires: import as "github.com/aerospike/aerospike-client-go/v8"// city holds NFC "café" (precomposed é, U+00E9)nfd := "cafe\u0301"eqExp := as.ExpEq(as.ExpStringBin("city"), as.ExpStringVal(nfd))// eqExp evaluates to false — eq compares UTF-8 bytescontainsExp := as.ExpStringContains(as.ExpStringBin("city"), as.ExpStringVal(nfd))// containsExp evaluates to true — search uses canonical equivalenceconst Aerospike = require('aerospike')const exp = Aerospike.exp
// city holds NFC "café" (precomposed é, U+00E9)const nfd = 'cafe\u0301'const eqExp = exp.eq(exp.binStr('city'), exp.str(nfd))// eqExp evaluates to false — eq compares UTF-8 bytesconst containsExp = exp.string.contains(nfd, exp.binStr('city'))// containsExp evaluates to true — search uses canonical equivalence// city holds NFC "café" (precomposed é, U+00E9)as_exp_build(eq_exp, as_exp_cmp_eq(as_exp_bin_str("city"), as_exp_str("cafe\u0301")));// eq_exp evaluates to false — eq compares UTF-8 bytes
as_exp_build(contains_exp, as_exp_string_contains("cafe\u0301", as_exp_bin_str("city")));// contains_exp evaluates to true — search uses canonical equivalence// city holds NFC "café" (precomposed é, U+00E9)String nfd = "cafe\u0301";Expression eqExp = Exp.build(Exp.eq(Exp.stringBin("city"), Exp.val(nfd)));// eqExp evaluates to false — eq compares UTF-8 bytesExpression containsExp = Exp.build(StringExp.contains(Exp.val(nfd), Exp.stringBin("city")));// containsExp evaluates to true — search uses canonical equivalencefrom aerospike_helpers.expressions import Eq, StrBinfrom aerospike_helpers.expressions import string as str_expr
# city holds NFC "café" (precomposed é, U+00E9)nfd = "cafe\u0301"eq_exp = Eq(StrBin("city"), nfd).compile()# eq_exp evaluates to False — eq compares UTF-8 bytescontains_exp = str_expr.Contains(needle=nfd, bin="city").compile()# contains_exp evaluates to True — search uses canonical equivalenceSearch matching is case-sensitive.
"Café" and "café" do not match.
Normalize before you compare
normalize_nfc rewrites a String bin to NFC.
The expression form string_normalize_nfc (Aerospike Expression Language (AEL) normalizeNFC()) returns that NFC value without writing the bin.
Normalize to NFC on write so stored values share one form. If mixed forms are already stored, normalize both sides of a comparison, or normalize the bin before comparing it with an NFC literal. Until the stored bytes share one form, comparator results are UTF-8 byte results.
// city holds NFD "café" (e + combining acute)try (RecordStream rs = session.upsert(key) .bin("city").normalizeNfc() .execute()) { rs.next().recordOrThrow();}// city now holds NFC "café" (precomposed é)# city holds NFD "café" (e + combining acute)session.upsert(key).bin("city").str_normalize_nfc().execute()# city now holds NFC "café" (precomposed é)// Requires: use aerospike::operations::string as str_op;// city holds NFD "café" (e + combining acute)client.operate(&WritePolicy::default(), &key, &[str_op::normalize_nfc(&StringPolicy::default(), "city")]).await?;// city now holds NFC "café" (precomposed é)// city holds NFD "café" (e + combining acute)client.Operate(null, key, StringOperation.NormalizeNFC(StringPolicy.Default, "city"));// city now holds NFC "café" (precomposed é)// Requires: import as "github.com/aerospike/aerospike-client-go/v8"// city holds NFD "café" (e + combining acute)client.Operate(nil, key, as.StrNormalizeNFCOp(as.DefaultStringPolicy, "city"))// city now holds NFC "café" (precomposed é)const Aerospike = require('aerospike')const strings = Aerospike.strings
// city holds NFD "café" (e + combining acute)await client.operate(key, [strings.normalizeNfc('city')])// city now holds NFC "café" (precomposed é)// city holds NFD "café" (e + combining acute)as_operations ops;as_operations_init(&ops, 1);as_operations_string_normalize_nfc(&ops, "city", NULL, NULL);
aerospike_key_operate(&as, &err, NULL, &key, &ops, NULL);as_operations_destroy(&ops);// city now holds NFC "café" (precomposed é)// city holds NFD "café" (e + combining acute)client.operate(null, key, StringOperation.normalizeNFC(StringPolicy.Default, "city"));// city now holds NFC "café" (precomposed é)from aerospike_helpers.operations import string_operations as so
# city holds NFD "café" (e + combining acute)client.operate(key, [so.normalize_nfc("city")])# city now holds NFC "café" (precomposed é)String exp = "$.city:STRING.normalizeNFC() == 'caf\u00e9'";// true whichever form city is stored inexp = "$.city:STRING.normalizeNFC() == 'caf\u00e9'"# True whichever form city is stored inuse aerospike::expressions::{eq, string as str_exp, string_bin, string_val};use aerospike::operations::string::StringPolicy;
let exp = eq( str_exp::normalize_nfc(&StringPolicy::default(), string_bin("city".into())), string_val("caf\u{e9}".into()));// true whichever form city is stored inExpression exp = Exp.Build(Exp.EQ( StringExp.NormalizeNFC(StringPolicy.Default, Exp.StringBin("city")), Exp.Val("caf\u00e9")));// true whichever form city is stored in// Requires: import as "github.com/aerospike/aerospike-client-go/v8"exp := as.ExpEq( as.ExpStringNormalizeNFC(as.DefaultStringPolicy, as.ExpStringBin("city")), as.ExpStringVal("caf\u00e9"))// true whichever form city is stored inconst Aerospike = require('aerospike')const exp = Aerospike.exp
const expression = exp.eq( exp.string.normalizeNfc(null, exp.binStr('city')), exp.str('caf\u00e9'))// true whichever form city is stored inas_exp_build(exp, as_exp_cmp_eq( as_exp_string_normalize_nfc(NULL, as_exp_bin_str("city")), as_exp_str("caf\u00e9")));// true whichever form city is stored inExpression exp = Exp.build(Exp.eq( StringExp.normalizeNFC(StringPolicy.Default, Exp.stringBin("city")), Exp.val("caf\u00e9")));// true whichever form city is stored infrom aerospike_helpers.expressions import Eqfrom aerospike_helpers.expressions import string as str_exprfrom aerospike_helpers.string_helpers import StringPolicy
exp = Eq( str_expr.NormalizeNFC(StringPolicy(), bin="city"), "caf\u00e9",).compile()# True whichever form city is stored inInvalid UTF-8 is a different problem from mixed NFC and NFD.
Both NFC and NFD are valid UTF-8, so they pass encoding checks and still compare as different under eq.
For bytes that are not valid UTF-8, see String operations and UTF-8 validation.