---
title: "Compare String values"
description: "Expression comparison operators order String values by UTF-8 bytes. Search operations use canonical equivalence. Normalize mixed forms with normalize_nfc."
---

# Compare String values

> For the complete documentation index see: [llms.txt](https://aerospike.com/docs/llms.txt)
> 
> All documentation pages available in markdown.

The [expression comparison operators](https://aerospike.com/docs/develop/expressions/comparison) — [`eq`](https://aerospike.com/docs/develop/expressions/comparison#eq), [`ne`](https://aerospike.com/docs/develop/expressions/comparison#ne), [`gt`](https://aerospike.com/docs/develop/expressions/comparison#gt), [`ge`](https://aerospike.com/docs/develop/expressions/comparison#ge), [`lt`](https://aerospike.com/docs/develop/expressions/comparison#lt), and [`le`](https://aerospike.com/docs/develop/expressions/comparison#le), available on String values since Database 5.2 — order [String](https://aerospike.com/docs/develop/data-types/string) values by UTF-8 bytes. [String search operations](https://aerospike.com/docs/develop/data-types/string/operations) and [`normalize_nfc`](https://aerospike.com/docs/develop/data-types/string/operations#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"` is `true`, because `Z` sorts before `a`.
-   `"Ålesund" > "Zurich"` is `true`, because `Å` is a multi-byte UTF-8 letter and sorts after every ASCII letter.

This is the same byte order a [List](https://aerospike.com/docs/develop/data-types/collections/list) or [Map](https://aerospike.com/docs/develop/data-types/collections/map) uses to order String elements, which [Order and compare collection elements](https://aerospike.com/docs/develop/data-types/collections/ordering) 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`](https://aerospike.com/docs/develop/data-types/string/operations#contains), [`find`](https://aerospike.com/docs/develop/data-types/string/operations#find), [`starts_with`](https://aerospike.com/docs/develop/data-types/string/operations#starts_with), and [`ends_with`](https://aerospike.com/docs/develop/data-types/string/operations#ends_with) match. `contains` returns `true`. `find` returns a non-negative index.
-   [`replace`](https://aerospike.com/docs/develop/data-types/string/operations#replace) and [`replace_all`](https://aerospike.com/docs/develop/data-types/string/operations#replace_all) substitute that spelling.
-   Expression [`eq`](https://aerospike.com/docs/develop/expressions/comparison#eq) returns `false`.

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.

-   [Java SDK](#tab-panel-4540)
-   [Python SDK](#tab-panel-4541)
-   [Rust](#tab-panel-4542)
-   [C#](#tab-panel-4543)
-   [Go](#tab-panel-4544)
-   [Node.js](#tab-panel-4545)
-   [C](#tab-panel-4546)
-   [Java](#tab-panel-4547)
-   [Python](#tab-panel-4548)

```java
// city holds NFC "café" (precomposed é, U+00E9)

String eqExp = "$.city:STRING == 'cafe\u0301'";

// eqExp evaluates to false — eq compares UTF-8 bytes

String containsExp = "$.city:STRING.contains(needle: 'cafe\u0301')";

// containsExp evaluates to true — search uses canonical equivalence
```

```python
# city holds NFC "café" (precomposed é, U+00E9)

eq_exp = "$.city:STRING == 'cafe\u0301'"

# eq_exp evaluates to False — eq compares UTF-8 bytes

contains_exp = "$.city:STRING.contains(needle: 'cafe\u0301')"

# contains_exp evaluates to True — search uses canonical equivalence
```

```rust
use 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 bytes

let contains_exp = str_exp::contains(string_bin("city".into()), string_val(nfd.into()));

// contains_exp evaluates to true — search uses canonical equivalence
```

```csharp
// 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 bytes

Expression containsExp = Exp.Build(StringExp.Contains(Exp.Val(nfd), Exp.StringBin("city")));

// containsExp evaluates to true — search uses canonical equivalence
```

```go
// 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 bytes

containsExp := as.ExpStringContains(as.ExpStringBin("city"), as.ExpStringVal(nfd))

// containsExp evaluates to true — search uses canonical equivalence
```

```javascript
const 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 bytes

const containsExp = exp.string.contains(nfd, exp.binStr('city'))

// containsExp evaluates to true — search uses canonical equivalence
```

```c
// 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
```

```java
// 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 bytes

Expression containsExp = Exp.build(StringExp.contains(Exp.val(nfd), Exp.stringBin("city")));

// containsExp evaluates to true — search uses canonical equivalence
```

```python
from aerospike_helpers.expressions import Eq, StrBin

from 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 bytes

contains_exp = str_expr.Contains(needle=nfd, bin="city").compile()

# contains_exp evaluates to True — search uses canonical equivalence
```

Search matching is case-sensitive. `"Café"` and `"café"` do not match.

## Normalize before you compare

[`normalize_nfc`](https://aerospike.com/docs/develop/data-types/string/operations#normalize_nfc) rewrites a String bin to NFC. The expression form [`string_normalize_nfc`](https://aerospike.com/docs/develop/expressions/string#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.

-   [Java SDK](#tab-panel-4549)
-   [Python SDK](#tab-panel-4550)
-   [Rust](#tab-panel-4551)
-   [C#](#tab-panel-4552)
-   [Go](#tab-panel-4553)
-   [Node.js](#tab-panel-4554)
-   [C](#tab-panel-4555)
-   [Java](#tab-panel-4556)
-   [Python](#tab-panel-4557)

```java
// 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 é)
```

```python
# city holds NFD "café" (e + combining acute)

session.upsert(key).bin("city").str_normalize_nfc().execute()

# city now holds NFC "café" (precomposed é)
```

```rust
// 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 é)
```

```csharp
// city holds NFD "café" (e + combining acute)

client.Operate(null, key,

    StringOperation.NormalizeNFC(StringPolicy.Default, "city"));

// city now holds NFC "café" (precomposed é)
```

```go
// 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 é)
```

```javascript
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 é)
```

```c
// 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 é)
```

```java
// city holds NFD "café" (e + combining acute)

client.operate(null, key,

    StringOperation.normalizeNFC(StringPolicy.Default, "city"));

// city now holds NFC "café" (precomposed é)
```

```python
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 é)
```

-   [Java SDK](#tab-panel-4558)
-   [Python SDK](#tab-panel-4559)
-   [Rust](#tab-panel-4560)
-   [C#](#tab-panel-4561)
-   [Go](#tab-panel-4562)
-   [Node.js](#tab-panel-4563)
-   [C](#tab-panel-4564)
-   [Java](#tab-panel-4565)
-   [Python](#tab-panel-4566)

```java
String exp = "$.city:STRING.normalizeNFC() == 'caf\u00e9'";

// true whichever form city is stored in
```

```python
exp = "$.city:STRING.normalizeNFC() == 'caf\u00e9'"

# True whichever form city is stored in
```

```rust
use 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 in
```

```csharp
Expression exp = Exp.Build(Exp.EQ(

  StringExp.NormalizeNFC(StringPolicy.Default, Exp.StringBin("city")),

  Exp.Val("caf\u00e9")));

// true whichever form city is stored in
```

```go
// 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 in
```

```javascript
const 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 in
```

```c
as_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 in
```

```java
Expression exp = Exp.build(Exp.eq(

  StringExp.normalizeNFC(StringPolicy.Default, Exp.stringBin("city")),

  Exp.val("caf\u00e9")));

// true whichever form city is stored in
```

```python
from aerospike_helpers.expressions import Eq

from aerospike_helpers.expressions import string as str_expr

from aerospike_helpers.string_helpers import StringPolicy

exp = Eq(

    str_expr.NormalizeNFC(StringPolicy(), bin="city"),

    "caf\u00e9",

).compile()

# True whichever form city is stored in
```

Invalid 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](https://aerospike.com/docs/database/advanced/special-upgrades/820-upgrade#string-operations-and-utf-8-validation).

## Next steps

-   [Comparison](https://aerospike.com/docs/develop/expressions/comparison) operators
-   [String operations](https://aerospike.com/docs/develop/data-types/string/operations)
-   [String expressions](https://aerospike.com/docs/develop/expressions/string)
-   [Upgrade to Database 8.2.0 and later](https://aerospike.com/docs/database/advanced/special-upgrades/820-upgrade#string-operations-and-utf-8-validation)