String operations
For the complete documentation index see: llms.txt
All documentation pages available in markdown.
Applies to
- Aerospike Developer SDK (Java 21+ and Python 3.11+)
- Aerospike Database 6.0 or later unless a section states otherwise
Learn how to inspect and transform string bins on the server, without reading the whole value to the client first. This guide covers reading substrings and search results, and modifying strings in place (case conversion, trimming, padding, regex replace, and more). It also covers converting between string and other types, and applying the same operations inside filter and projection expressions.
Except where noted, snippets on this page use the imports below. A snippet lists additional import lines only when it needs a type not shown here. When this page includes a Complete example section, that block is fully self-contained with every import required to run it.
import com.aerospike.client.sdk.DataSet;import com.aerospike.client.sdk.Record;import com.aerospike.client.sdk.RecordStream;import com.aerospike.client.sdk.StringWriteOptions;import com.aerospike.client.sdk.exp.Exp;import com.aerospike.client.sdk.exp.StringExp;from aerospike_sdk import DataSet, Exp, StringWriteFlagsTwo ways to work with strings
| Surface | Role |
|---|---|
BinBuilder (session.query(key).bin("s").<op>() / session.upsert(key).bin("s").<op>()) | Fluent chain. Each method queues one server-side string operation on the named bin. |
StringExp (Java) / Exp.string_* (Python) | Expression builders for filters (.where(...)), selectFrom/select_from projections, and composing reads. Modify-style expressions return the transformed value, and don’t write it back to the bin. |
Multiple operations on one bin
When a query or write queues several operations against the same bin, as in the examples throughout this page, the results come back in submission order, one slot per operation. Modify operations contribute a null/None slot: the server returns no value for them. Chain a trailing .get() on the bin when you want the value after the modifications.
Use Record.operationResult(index) with the zero-based position of the operation in the call chain, then a typed getter such as getLong() or getString(). This is distinct from record.bins.get(name), which reads a single bin’s current value (see Update records for that pattern with numeric bins).
Use RecordResult.operation_result(index) with the zero-based position of the operation in the call chain. The accessor is on the RecordResult row returned by first_or_raise(). By-name access through record.bins[name] holds a list when several operations target one bin, a single value when only one operation returned a value for that bin, and no entry at all when every operation on the bin was a modify.
Read operations
Indexes are Unicode codepoints, left to right. Negative indexes count from the end (-1 is the last codepoint). Out-of-range indexes are clamped and do not throw an error.
For example, for the string “hello”, substr(3, 100) / str_substr(3, 100) returns “lo”.
| Java | Python | Returns |
|---|---|---|
strlen() | str_strlen() | Codepoint count (int64) |
substr(start[, end]) | str_substr(start[, end]) | Substring, half-open [start, end) when end is given, otherwise through the end of the string |
charAt(index) | str_char_at(index) | Single-codepoint string at index |
find(needle[, occurrence]) | str_find(needle[, occurrence]) | Codepoint index of the match, or -1. occurrence is 1-based (1 = first, -1 = last), not a start offset. Defaults to 1 |
contains(needle) | str_contains(needle) | Boolean |
startsWith(prefix) | str_starts_with(prefix) | Boolean |
endsWith(suffix) | str_ends_with(suffix) | Boolean |
byteLength() | str_byte_length() | UTF-8 byte length |
isNumeric([numericType]) | str_is_numeric([numeric_type]) | Boolean, optionally restricted to INT or FLOAT (see Type conversion) |
isUpper() / isLower() | str_is_upper() / str_is_lower() | Boolean |
split([separator]) | str_split([separator]) | List of strings. Omitted separator splits per codepoint |
b64Decode() | str_b64_decode() | Blob decoded from base64 |
regexCompare(pattern[, regexFlags]) | str_regex_compare(pattern[, flags]) | Boolean match result (see Regex flags) |
DataSet docs = DataSet.of("test", "docs");String key = "row1";
session.upsert(docs.id(key)).bin("message").setTo("hello world").execute();
RecordStream stream = session.query(docs.id(key)) .bin("message").strlen() .bin("message").substr(6) .bin("message").substr(0, 5) .bin("message").find("o", -1) .bin("message").contains("world") .execute();
Record rec = stream.getFirst().orElseThrow().recordOrThrow();System.out.println("length: " + rec.operationResult(0).getLong());System.out.println("substr(6): " + rec.operationResult(1).getString());System.out.println("substr(0,5): " + rec.operationResult(2).getString());System.out.println("last 'o' at: " + rec.operationResult(3).getLong());System.out.println("contains 'world': " + rec.operationResult(4).getBoolean());stream.close();📖 API reference:
DataSet.of(...)|DataSet.id(...)|Session.query(Key)|ChainableQueryBuilder.execute()|RecordStream.getFirst()|RecordStream.close()|RecordResult.recordOrThrow()|Record.operationResult(...)
docs = DataSet.of("test", "docs")key = docs.id("row1")
await session.upsert(key).put({"message": "hello world"}).execute()
stream = await ( session.query(key) .bin("message").str_strlen() .bin("message").str_substr(6) .bin("message").str_substr(0, 5) .bin("message").str_find("o", -1) .bin("message").str_contains("world") .execute())row = await stream.first_or_raise()print(f"length: {row.operation_result(0)}")print(f"substr(6): {row.operation_result(1)}")print(f"substr(0,5): {row.operation_result(2)}")print(f"last 'o' at: {row.operation_result(3)}")print(f"contains 'world': {row.operation_result(4)}")stream.close()📖 API reference:
DataSet.of()|DataSet.id()|Session.query()|Session.upsert()|WriteSegmentBuilder.put()|RecordResult.operation_result()|RecordStream.first_or_raise()|RecordStream.close()
Modify operations
Modify operations change the bin in place. They return no value: each one contributes a null/None slot to the positional results, so chain a trailing .get() on the bin to read the value after the modifications. All modify operations accept optional write flags. See String write flags.
| Java | Python | Effect |
|---|---|---|
insert(index, value[, options]) | str_insert(index, value[, flags=]) | Splice value into the bin at codepoint index |
overwrite(index, value[, options]) | str_overwrite(index, value[, flags=]) | Overwrite codepoints starting at index. Result can grow past the original length |
concat(fragment or fragments[, options]) | str_concat(value[, flags=]) | Append one fragment or, given an ordered list, append each fragment in order |
append(fragment[, options]) | str_append(value[, flags=]) | Append one fragment |
prepend(fragment[, options]) | str_prepend(value[, flags=]) | Prepend one fragment |
snip(start[, end][, options]) | str_snip(start[, end][, flags=]) | Remove codepoints from start through end (exclusive), or through the end of the string if end is omitted |
replace(needle, replacement[, options]) | str_replace(needle, replacement[, flags=]) | Replace the first occurrence of needle |
replaceAll(needle, replacement[, options]) | str_replace_all(needle, replacement[, flags=]) | Replace every occurrence of needle |
upper([options]) / lower([options]) | str_upper([flags=]) / str_lower([flags=]) | Uppercase / lowercase the bin |
caseFold([options]) | str_case_fold([flags=]) | Locale-independent case fold (for normalized comparison keys) |
normalizeNfc([options]) | str_normalize_nfc([flags=]) | Normalize to Unicode Normalization Form C (NFC) |
trimStart() / trimEnd() / trim() | str_trim_start() / str_trim_end() / str_trim() | Remove Unicode whitespace from one or both ends. Each accepts [options] / [flags=] |
padStart(targetLength, padString[, options]) | str_pad_start(target_length, pad_string[, flags=]) | Left-pad to a minimum length |
padEnd(targetLength, padString[, options]) | str_pad_end(target_length, pad_string[, flags=]) | Right-pad to a minimum length |
repeat(count[, options]) | str_repeat(count[, flags=]) | Repeat the bin’s contents count times |
regexReplace(pattern, replacement[, regexFlags][, options]) | str_regex_replace(pattern, replacement[, flags][, write_flags=]) | Regex-based replace. See Regex flags |
DataSet docs = DataSet.of("test", "docs");String key = "row1";
session.upsert(docs.id(key)).bin("title").setTo(" the Quick Brown Fox ").execute();
RecordStream stream = session.update(docs.id(key)) .bin("title").trim() .bin("title").upper() .bin("title").padEnd(30, ".") .bin("title").get() .execute();
Record rec = stream.getFirst().orElseThrow().recordOrThrow();// Slots 0-2 are the modify operations and hold null; slot 3 is the trailing get.System.out.println("after trim, upper, padEnd: " + rec.operationResult(3).getString());stream.close();
// CREATE_ONLY plus NO_FAIL: create the bin if it's missing, otherwise leave it// unchanged instead of failing with BIN_EXISTS_ERROR. "subtitle" doesn't exist// yet, so the first run creates it and a rerun leaves it alone.session.update(docs.id(key)) .bin("subtitle").concat("draft", opt -> opt.createOnly().noFail()) .execute();📖 API reference:
DataSet.of(...)|DataSet.id(...)|Session.update(DataSet)|ChainableQueryBuilder.execute()|RecordStream.getFirst()|RecordStream.close()|RecordResult.recordOrThrow()|Record.operationResult(...)|StringWriteOptions.createOnly()|StringWriteOptions.noFail()
docs = DataSet.of("test", "docs")key = docs.id("row1")
await session.upsert(key).put({"title": " the Quick Brown Fox "}).execute()
stream = await ( session.update(key) .bin("title").str_trim() .bin("title").str_upper() .bin("title").str_pad_end(30, ".") .bin("title").get() .execute())row = await stream.first_or_raise()# Slots 0-2 are the modify operations and hold None; slot 3 is the trailing get.print(f"after trim, upper, pad_end: {row.operation_result(3)}")stream.close()
# CREATE_ONLY plus NO_FAIL: create the bin if it's missing, otherwise leave it# unchanged instead of failing with BIN_EXISTS_ERROR. "subtitle" doesn't exist# yet, so the first run creates it and a rerun leaves it alone.await ( session.update(key) .bin("subtitle").str_concat("draft", flags=StringWriteFlags.CREATE_ONLY | StringWriteFlags.NO_FAIL) .execute())📖 API reference:
DataSet.of()|DataSet.id()|Session.update()|Session.upsert()|WriteSegmentBuilder.put()|RecordResult.operation_result()|RecordStream.first_or_raise()|RecordStream.close()
String write flags
Java exposes the flags as StringWriteOptions builder methods on the fluent API (opt -> opt.createOnly(), opt -> opt.noFail()) and as StringWriteFlags int constants on StringExp. Python uses the StringWriteFlags enum everywhere. Combine members with bitwise OR.
| Flag | Java | Python | Effect |
|---|---|---|---|
| Default | StringWriteFlags.DEFAULT | StringWriteFlags.DEFAULT | On the operations that can create a bin, create it if missing, otherwise update it. On every other operation a missing bin is a silent no-op, not an error |
| Create only | StringWriteFlags.CREATE_ONLY / opt -> opt.createOnly() | StringWriteFlags.CREATE_ONLY | Apply only if the bin doesn’t exist yet. Fails with BIN_EXISTS_ERROR on an existing bin. Valid only on operations that can create a bin (insert, overwrite, concat, append, prepend, padStart, padEnd, repeat), and never with a nested collection data type (CDT) path |
| Update only | StringWriteFlags.UPDATE_ONLY / opt -> opt.updateOnly() | StringWriteFlags.UPDATE_ONLY | Apply only if the bin exists. On a missing bin the operation is a silent no-op and doesn’t create the bin |
| No-fail | StringWriteFlags.NO_FAIL / opt -> opt.noFail() | StringWriteFlags.NO_FAIL | Turn an in-operation failure into a silent no-op: BIN_EXISTS_ERROR from CREATE_ONLY, or OP_NOT_APPLICABLE from a nested path that doesn’t resolve. Leaves the bin unchanged and returns a null/None slot for that operation |
CREATE_ONLY and UPDATE_ONLY are mutually exclusive. The server rejects the combination with PARAMETER_ERROR.
Regex flags
Used with regexCompare()/str_regex_compare() and regexReplace()/str_regex_replace(). Combine with bitwise OR. Both SDKs expose them as StringRegexFlags.
| Flag | Effect |
|---|---|
DEFAULT | No flags |
CASE_INSENSITIVE | Case-insensitive matching |
MULTILINE | ^ and $ match the start/end of any line, not just the start/end of the input |
DOTALL | . matches any character, including line terminators |
UNIX_LINES | Treat only \n as a line terminator |
GLOBAL | Replace every match, not just the first. Only applicable to regexReplace/str_regex_replace |
Patterns use International Components for Unicode (ICU) regex syntax.
Type conversion
| Java | Python | Effect |
|---|---|---|
readAsString() | read_as_string() | Convert the bin’s value to its string representation. Accepts int, float, string, bool, or valid UTF-8 blob bins. This is type-agnostic: it operates on any convertible bin type, not just strings, so it’s not named toString()/to_string(), avoiding a naming collision with generic conversions |
stringToInteger() | str_to_integer() | Parse the string bin as an int64 |
stringToDouble() | str_to_double() | Parse the string bin as a float64 |
stringToBlob() | str_to_blob() | Reinterpret the string bin’s UTF-8 bytes as a blob |
In Java, readAsString() is available on the write builders (session.update(...)/session.upsert(...)), not on session.query(key).bin(...). Python offers read_as_string() on both.
A parse failure (for example, stringToInteger() / str_to_integer() on a non-numeric string) fails the call with OP_NOT_APPLICABLE, raised as BinOpInvalidException (Java) / BinOpInvalidError (Python). Guard with isNumeric(StringNumericType.INT) / str_is_numeric(StringNumericType.INT) first when the input isn’t trusted, or catch the exception. Calling readAsString()/read_as_string() on a bin type outside the supported list (for example, a List or Map bin) fails with BIN_TYPE_ERROR.
DataSet docs = DataSet.of("test", "docs");String key = "row1";
session.upsert(docs.id(key)).bin("count").setTo("42").execute();
RecordStream stream = session.query(docs.id(key)) .bin("count").stringToInteger() .execute();Record rec = stream.getFirst().orElseThrow().recordOrThrow();System.out.println("parsed: " + rec.operationResult(0).getLong());stream.close();📖 API reference:
DataSet.of(...)|DataSet.id(...)|Session.query(Key)|ChainableQueryBuilder.execute()|RecordStream.getFirst()|RecordStream.close()|RecordResult.recordOrThrow()|Record.operationResult(...)
docs = DataSet.of("test", "docs")key = docs.id("row1")
await session.upsert(key).put({"count": "42"}).execute()
stream = await session.query(key).bin("count").str_to_integer().execute()row = await stream.first_or_raise()print(f"parsed: {row.operation_result(0)}")stream.close()📖 API reference:
DataSet.of()|DataSet.id()|Session.query()|Session.upsert()|WriteSegmentBuilder.put()|RecordResult.operation_result()|RecordStream.first_or_raise()|RecordStream.close()
String expressions
StringExp (Java) and Exp.string_* (Python) mirror the same operations as expressions, for use in .where(...) filters and selectFrom/select_from projections. Modify-style expressions such as upper and replace return the transformed value without writing it back to the bin. Pass the result to a write operation if you need to persist it. Modify-style expressions take the write flags as their first argument (0 for the default), followed by the operation’s arguments and the source expression last.
DataSet docs = DataSet.of("test", "docs");String key = "row1";
session.upsert(docs.id(key)).bin("email").setTo("Alice@Example.com").execute();
// Filter plus a projection that lowercases the email, both on a key read.RecordStream stream = session.query(docs.id(key)) .where(StringExp.startsWith( Exp.val("alice"), StringExp.lower(0, Exp.stringBin("email")))) .bin("emailLower").selectFrom(StringExp.lower(0, Exp.stringBin("email"))) .execute();
Record rec = stream.getFirst().orElseThrow().recordOrThrow();System.out.println("lowercased email: " + rec.getString("emailLower"));stream.close();📖 API reference:
StringExp.startsWith(...)|StringExp.lower(...)|Exp.stringBin(...)|Exp.val(...)|ChainableQueryBuilder.where(...)|QueryBinBuilder.selectFrom(...)
docs = DataSet.of("test", "docs")key = docs.id("row1")
await session.upsert(key).put({"email": "Alice@Example.com"}).execute()
# Filter: only match records where the lowercased email starts with "alice".# A full-dataset scan works here for Python. Java's equivalent must be a# key read: see the "Java: ops projection is key-reads only" note.stream = await ( session.query(docs) .where(Exp.string_starts_with( Exp.val("alice"), Exp.string_lower(0, Exp.string_bin("email")))) .bin("email_lower").select_from(Exp.string_lower(0, Exp.string_bin("email"))) .execute())row = await stream.first_or_raise()record = row.record_or_raise()print(f"lowercased email: {record.bins['email_lower']}")stream.close()📖 API reference:
Exp|QueryBuilder.where()|QueryBinBuilder.select_from()|RecordResult.record_or_raise()
Nested strings
Both APIs can target a string value nested inside a list or map through the same CDT path methods used for collection operations (onMapKey() / on_map_key(), onListIndex() / on_list_index(), and similar). The nested value must already be a string. Operations on a non-string leaf return a bin-type error. CREATE_ONLY can’t be combined with a nested path (PARAMETER_ERROR).
// Additional import for this example:import java.util.Map;
DataSet docs = DataSet.of("test", "docs");String key = "row1";
session.upsert(docs.id(key)) .bin("profile").setTo(Map.of("bio", " Loves hiking ")) .execute();
// Trim the nested "bio" string inside the "profile" map, then read the map back.RecordStream stream = session.update(docs.id(key)) .bin("profile").onMapKey("bio").trim() .bin("profile").get() .execute();Record rec = stream.getFirst().orElseThrow().recordOrThrow();System.out.println("profile after trim: " + rec.operationResult(1).getMap());stream.close();📖 API reference:
BinBuilder.onMapKey(...)|Session.update(DataSet)|Record.operationResult(...)
docs = DataSet.of("test", "docs")key = docs.id("row1")
await session.upsert(key).put({"profile": {"bio": " Loves hiking "}}).execute()
# Trim the nested "bio" string inside the "profile" map, then read the map back.stream = await ( session.update(key) .bin("profile").on_map_key("bio").str_trim() .bin("profile").get() .execute())row = await stream.first_or_raise()print(f"profile after trim: {row.operation_result(1)}")stream.close()📖 API reference:
WriteBinBuilder.on_map_key()|Session.update()|RecordResult.operation_result()
Version requirements
String read/modify operations require Aerospike Database 8.2.0 or later. Sending these operations to an older server returns an error. During a rolling upgrade, don’t issue these operations until every node in the cluster reports Database 8.2.0 or later — a partially upgraded cluster accepts them on upgraded nodes and rejects them on nodes that aren’t upgraded yet. Check your SDK release notes for the minimum client version. See Version Compatibility.
Complete example
This example is self-contained. It lists every import needed to run standalone.
import com.aerospike.client.sdk.Cluster;import com.aerospike.client.sdk.ClusterDefinition;import com.aerospike.client.sdk.DataSet;import com.aerospike.client.sdk.Record;import com.aerospike.client.sdk.RecordStream;import com.aerospike.client.sdk.Session;import com.aerospike.client.sdk.policy.Behavior;
public class StringOperationsExample { public static void main(String[] args) { try (Cluster cluster = new ClusterDefinition("localhost", 3000).connect()) { Session session = cluster.createSession(Behavior.DEFAULT); DataSet docs = DataSet.of("test", "docs"); String key = "string-example-doc";
// Seed data so the example is repeatable. session.upsert(docs.id(key)).bin("title").setTo(" the Quick Brown Fox ").execute();
// Read operations: inspect the string without modifying it. RecordStream readStream = session.query(docs.id(key)) .bin("title").strlen() .bin("title").contains("Fox") .execute(); Record read = readStream.getFirst().orElseThrow().recordOrThrow(); System.out.println("length: " + read.operationResult(0).getLong()); System.out.println("contains 'Fox': " + read.operationResult(1).getBoolean()); readStream.close();
// Modify operations: trim, uppercase, and pad the bin in place, // then read the result back in the same call. RecordStream modifyStream = session.update(docs.id(key)) .bin("title").trim() .bin("title").upper() .bin("title").padEnd(30, ".") .bin("title").get() .execute(); Record modified = modifyStream.getFirst().orElseThrow().recordOrThrow(); System.out.println("after modify: " + modified.operationResult(3).getString()); modifyStream.close();
// Type conversion: parse a numeric-looking string bin. session.upsert(docs.id(key)).bin("count").setTo("42").execute(); RecordStream convertStream = session.query(docs.id(key)) .bin("count").stringToInteger() .execute(); Record converted = convertStream.getFirst().orElseThrow().recordOrThrow(); System.out.println("parsed count: " + converted.operationResult(0).getLong()); convertStream.close(); } }}📖 API reference:
ClusterDefinition(String,int)|ClusterDefinition.connect()|Cluster.createSession(Behavior)|DataSet.of(...)|Session.upsert(DataSet)|Session.query(Key)|Session.update(DataSet)|RecordStream.getFirst()|RecordStream.close()|Record.operationResult(...)
import asynciofrom aerospike_sdk import Behavior, ClusterDefinition, DataSet
async def main(): async with await ClusterDefinition("localhost", 3000).connect() as cluster: session = cluster.create_session(Behavior.DEFAULT) docs = DataSet.of("test", "docs") key = docs.id("string-example-doc")
# Seed data so the example is repeatable. await session.upsert(key).put({"title": " the Quick Brown Fox "}).execute()
# Read operations: inspect the string without modifying it. stream = await ( session.query(key) .bin("title").str_strlen() .bin("title").str_contains("Fox") .execute() ) read = await stream.first_or_raise() print(f"length: {read.operation_result(0)}") print(f"contains 'Fox': {read.operation_result(1)}") stream.close()
# Modify operations: trim, uppercase, and pad the bin in place, # then read the result back in the same call. stream = await ( session.update(key) .bin("title").str_trim() .bin("title").str_upper() .bin("title").str_pad_end(30, ".") .bin("title").get() .execute() ) modified = await stream.first_or_raise() print(f"after modify: {modified.operation_result(3)}") stream.close()
# Type conversion: parse a numeric-looking string bin. await session.upsert(key).put({"count": "42"}).execute() stream = await session.query(key).bin("count").str_to_integer().execute() converted = await stream.first_or_raise() print(f"parsed count: {converted.operation_result(0)}") stream.close()
if __name__ == "__main__": asyncio.run(main())📖 API reference:
ClusterDefinition|ClusterDefinition.connect()|Cluster.create_session()|DataSet.of()|DataSet.id()|Session.query()|Session.update()|Session.upsert()|RecordResult.operation_result()|RecordStream.first_or_raise()|RecordStream.close()
API reference summary
| Category | Java | Python |
|---|---|---|
| Read | strlen(), substr(...), charAt(...), find(...), contains(...), startsWith(...), endsWith(...), byteLength(), isNumeric(...), isUpper(), isLower(), split(...), b64Decode(), regexCompare(...) | str_strlen(), str_substr(...), str_char_at(...), str_find(...), str_contains(...), str_starts_with(...), str_ends_with(...), str_byte_length(), str_is_numeric(...), str_is_upper(), str_is_lower(), str_split(...), str_b64_decode(), str_regex_compare(...) |
| Modify | insert(...), overwrite(...), concat(...), append(...), prepend(...), snip(...), replace(...), replaceAll(...), upper(...), lower(...), caseFold(...), normalizeNfc(...), trimStart(...), trimEnd(...), trim(...), padStart(...), padEnd(...), repeat(...), regexReplace(...) | str_insert(...), str_overwrite(...), str_concat(...), str_append(...), str_prepend(...), str_snip(...), str_replace(...), str_replace_all(...), str_upper(...), str_lower(...), str_case_fold(...), str_normalize_nfc(...), str_trim_start(...), str_trim_end(...), str_trim(...), str_pad_start(...), str_pad_end(...), str_repeat(...), str_regex_replace(...) |
| Type conversion | readAsString(), stringToInteger(), stringToDouble(), stringToBlob() | read_as_string(), str_to_integer(), str_to_double(), str_to_blob() |
| Expressions | StringExp.* | Exp.string_* |
| Write flags | StringWriteOptions (fluent) / StringWriteFlags.DEFAULT, CREATE_ONLY, UPDATE_ONLY, NO_FAIL | StringWriteFlags.DEFAULT, CREATE_ONLY, UPDATE_ONLY, NO_FAIL (see String write flags) |
| Regex flags | StringRegexFlags.* | StringRegexFlags.* |
| Positional results | Record.operationResult(i) | RecordResult.operation_result(i) |
Next steps
Update Records
Modify bins, increment numbers, and update CDTs.
AEL query language
Filter records with readable expression syntax.
Data Model
Understand namespaces, sets, and bins.
Error Handling
Handle type-mismatch and parse errors.