diff --git a/src/main/java/com/aerospike/documentapi/AerospikeDocumentClient.java b/src/main/java/com/aerospike/documentapi/AerospikeDocumentClient.java index 99b32fc..1c7de6d 100644 --- a/src/main/java/com/aerospike/documentapi/AerospikeDocumentClient.java +++ b/src/main/java/com/aerospike/documentapi/AerospikeDocumentClient.java @@ -9,10 +9,13 @@ import com.aerospike.client.policy.Policy; import com.aerospike.client.policy.QueryPolicy; import com.aerospike.client.policy.WritePolicy; +import com.aerospike.client.query.Filter; import com.aerospike.client.query.KeyRecord; import com.aerospike.documentapi.batch.BatchOperation; +import com.aerospike.documentapi.data.DocumentFilter; import com.aerospike.documentapi.data.DocumentFilterExp; import com.aerospike.documentapi.data.DocumentQueryStatement; +import com.aerospike.documentapi.data.DocumentFilterSecIndex; import com.aerospike.documentapi.data.KeyResult; import com.aerospike.documentapi.jsonpath.JsonPathObject; import com.aerospike.documentapi.jsonpath.JsonPathParser; @@ -192,12 +195,13 @@ public List batchPerform(List batchOperations, bool } @Override - public Stream query(DocumentQueryStatement queryStatement, DocumentFilterExp... docFilters) { + public Stream query(DocumentQueryStatement queryStatement, DocumentFilter... docFilters) { QueryPolicy policy = new QueryPolicy(queryPolicy); policy.filterExp = getFilterExp(docFilters); + Filter secIndexFilter = getSecIndexFilter(docFilters); Stream keyRecords = StreamSupport.stream(Spliterators.spliteratorUnknownSize( - aerospikeDocumentRepository.query(policy, queryStatement.toStatement()).iterator(), + aerospikeDocumentRepository.query(policy, queryStatement.toStatement(secIndexFilter)).iterator(), Spliterator.ORDERED ), false); @@ -213,16 +217,29 @@ public Stream query(DocumentQueryStatement queryStatement, DocumentFi .filter(Objects::nonNull); } + private Filter getSecIndexFilter(DocumentFilter[] docFilters) { + if (docFilters == null || docFilters.length == 0) return null; + + return Arrays.stream(docFilters) + .filter(Objects::nonNull) + .filter(DocumentFilterSecIndex.class::isInstance) + .map(filterExp -> ((DocumentFilterSecIndex) filterExp).toSecIndexFilter()) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + private KeyResult getKeyResult(Key key, Map results) { return results.isEmpty() ? null : new KeyResult(key, results); } - private Expression getFilterExp(DocumentFilterExp[] docFilters) { + private Expression getFilterExp(DocumentFilter[] docFilters) { if (docFilters == null || docFilters.length == 0) return null; List filterExps = Arrays.stream(docFilters) .filter(Objects::nonNull) - .map(DocumentFilterExp::toFilterExpression) + .filter(DocumentFilterExp.class::isInstance) + .map(filterExp -> ((DocumentFilterExp) filterExp).toFilterExp()) .filter(Objects::nonNull) .collect(Collectors.toList()); @@ -235,7 +252,7 @@ private Expression getFilterExp(DocumentFilterExp[] docFilters) { } private Map getResults(String[] jsonPaths, Map bins) { - if (jsonPaths == null || jsonPaths.length == 0) return null; + if (jsonPaths == null || jsonPaths.length == 0) return Collections.emptyMap(); Map res = new HashMap<>(); bins.values() diff --git a/src/main/java/com/aerospike/documentapi/IAerospikeDocumentClient.java b/src/main/java/com/aerospike/documentapi/IAerospikeDocumentClient.java index 85f3b85..b06b3b0 100644 --- a/src/main/java/com/aerospike/documentapi/IAerospikeDocumentClient.java +++ b/src/main/java/com/aerospike/documentapi/IAerospikeDocumentClient.java @@ -3,7 +3,8 @@ import com.aerospike.client.BatchRecord; import com.aerospike.client.Key; import com.aerospike.documentapi.batch.BatchOperation; -import com.aerospike.documentapi.data.DocumentFilterExp; +import com.aerospike.documentapi.data.DocFilterExp; +import com.aerospike.documentapi.data.DocumentFilter; import com.aerospike.documentapi.data.DocumentQueryStatement; import com.aerospike.documentapi.data.KeyResult; import com.fasterxml.jackson.databind.JsonNode; @@ -134,11 +135,11 @@ public interface IAerospikeDocumentClient { *
  • optional json paths (inner objects less than a bin if necessary).
  • * * - * @param queryStatement object for building query definition, storing required bin names, json paths - * and secondary index filter - * @param documentFilterExpressions filter expressions + * @param queryStatement object for building query definition, storing required bin names and json paths + * @param documentFilters filters (can include one secondary index filter and/or one or more filter expressions; + * if there are multiple filter expressions given they are concatenated using logical AND) * @return stream of {@link KeyResult} objects * @throws DocumentApiException if query fails */ - Stream query(DocumentQueryStatement queryStatement, DocumentFilterExp... documentFilterExpressions); + Stream query(DocumentQueryStatement queryStatement, DocumentFilter... documentFilters); } diff --git a/src/main/java/com/aerospike/documentapi/data/DocFilterExp.java b/src/main/java/com/aerospike/documentapi/data/DocFilterExp.java new file mode 100644 index 0000000..ee4cc71 --- /dev/null +++ b/src/main/java/com/aerospike/documentapi/data/DocFilterExp.java @@ -0,0 +1,46 @@ +package com.aerospike.documentapi.data; + +import com.aerospike.client.exp.Exp; +import com.aerospike.documentapi.util.ExpConverter; + +public class DocFilterExp implements DocumentFilterExp { + + private final String binName; + private final String jsonPath; + private final Operator operator; + private final Object value; + private Integer regexFlags = null; + + public DocFilterExp(String binName, String jsonPath, Operator operator, Object value) { + this.binName = binName; + this.jsonPath = jsonPath; + this.operator = operator; + this.value = value; + } + + public Exp toFilterExp() { + switch (operator) { + case LT: + return ExpConverter.lt(binName, jsonPath, value); + case GT: + return ExpConverter.gt(binName, jsonPath, value); + case LE: + return ExpConverter.le(binName, jsonPath, value); + case GE: + return ExpConverter.ge(binName, jsonPath, value); + case EQ: + return ExpConverter.eq(binName, jsonPath, value); + case NE: + return ExpConverter.ne(binName, jsonPath, value); + case REGEX: + return ExpConverter.regex(binName, jsonPath, value.toString(), regexFlags); + default: + return null; + } + } + + @Override + public void setRegexFlags(int regexFlags) { + this.regexFlags = regexFlags; + } +} diff --git a/src/main/java/com/aerospike/documentapi/data/DocFilterSecIndex.java b/src/main/java/com/aerospike/documentapi/data/DocFilterSecIndex.java new file mode 100644 index 0000000..8b16440 --- /dev/null +++ b/src/main/java/com/aerospike/documentapi/data/DocFilterSecIndex.java @@ -0,0 +1,45 @@ +package com.aerospike.documentapi.data; + +import com.aerospike.client.query.Filter; +import com.aerospike.client.query.IndexCollectionType; +import com.aerospike.documentapi.util.FilterConverter; + +import static com.aerospike.client.query.IndexCollectionType.DEFAULT; + +public class DocFilterSecIndex implements DocumentFilterSecIndex { + + private final String binName; + private final String jsonPath; + private final Operator operator; + private final Object value; + private IndexCollectionType idxCollectionType = DEFAULT; + + public DocFilterSecIndex(String binName, String jsonPath, Operator operator, Object value) { + this.binName = binName; + this.jsonPath = jsonPath; + this.operator = operator; + this.value = value; + } + + public Filter toSecIndexFilter() { + switch (operator) { + case LT: + return FilterConverter.lt(binName, jsonPath, value, idxCollectionType); + case GT: + return FilterConverter.gt(binName, jsonPath, value, idxCollectionType); + case LE: + return FilterConverter.le(binName, jsonPath, value, idxCollectionType); + case GE: + return FilterConverter.ge(binName, jsonPath, value, idxCollectionType); + case EQ: + return FilterConverter.eq(binName, jsonPath, value); + default: + throw new UnsupportedOperationException(String.format("'%s' secondary filter is not supported", operator)); + } + } + + @Override + public void setIdxCollectionType(IndexCollectionType idxCollectionType) { + this.idxCollectionType = idxCollectionType; + } +} diff --git a/src/main/java/com/aerospike/documentapi/data/DocumentFilter.java b/src/main/java/com/aerospike/documentapi/data/DocumentFilter.java new file mode 100644 index 0000000..cfecefe --- /dev/null +++ b/src/main/java/com/aerospike/documentapi/data/DocumentFilter.java @@ -0,0 +1,7 @@ +package com.aerospike.documentapi.data; + +/** + * Document filter interface extended by {@link DocumentFilterExp} and {@link DocumentFilterSecIndex}. + */ +public interface DocumentFilter { +} diff --git a/src/main/java/com/aerospike/documentapi/data/DocumentFilterExp.java b/src/main/java/com/aerospike/documentapi/data/DocumentFilterExp.java index e30f460..b6f2f3f 100644 --- a/src/main/java/com/aerospike/documentapi/data/DocumentFilterExp.java +++ b/src/main/java/com/aerospike/documentapi/data/DocumentFilterExp.java @@ -1,36 +1,24 @@ package com.aerospike.documentapi.data; import com.aerospike.client.exp.Exp; -import com.aerospike.documentapi.util.DocumentExp; -public class DocumentFilterExp { +/** + * Base interface for creating filter expression. + * + *

    For the supported json paths see {@link com.aerospike.documentapi.util.ExpConverter}.

    + *

    Supported operators:

    + *
      + *
    • EQ
    • + *
    • NE
    • + *
    • GT
    • + *
    • GE
    • + *
    • LT
    • + *
    • LE
    • + *
    • REGEX
    • + *
    + */ +public interface DocumentFilterExp extends DocumentFilter { + Exp toFilterExp(); - private Exp exp; - - public DocumentFilterExp(String binName, String jsonPath, Operator.Simple operator, Object value) { - switch (operator) { - case LT: - exp = DocumentExp.lt(binName, jsonPath, value); - break; - case GT: - exp = DocumentExp.gt(binName, jsonPath, value); - break; - case LTE: - exp = DocumentExp.le(binName, jsonPath, value); - break; - case GTE: - exp = DocumentExp.ge(binName, jsonPath, value); - break; - case EQ: - exp = DocumentExp.eq(binName, jsonPath, value); - break; - case NE: - exp = DocumentExp.ne(binName, jsonPath, value); - break; - } - } - - public Exp toFilterExpression() { - return exp; - } + void setRegexFlags(int regexFlags); } diff --git a/src/main/java/com/aerospike/documentapi/data/DocumentFilterSecIndex.java b/src/main/java/com/aerospike/documentapi/data/DocumentFilterSecIndex.java new file mode 100644 index 0000000..cb1f3d7 --- /dev/null +++ b/src/main/java/com/aerospike/documentapi/data/DocumentFilterSecIndex.java @@ -0,0 +1,23 @@ +package com.aerospike.documentapi.data; + +import com.aerospike.client.query.Filter; +import com.aerospike.client.query.IndexCollectionType; + +/** + * Base interface for creating secondary index filter. + * + *

    For the supported json paths see {@link com.aerospike.documentapi.util.FilterConverter}.

    + *

    Supported operators:

    + *
      + *
    • EQ
    • + *
    • GT
    • + *
    • GE
    • + *
    • LT
    • + *
    • LE
    • + *
    + */ +public interface DocumentFilterSecIndex extends DocumentFilter { + Filter toSecIndexFilter(); + + void setIdxCollectionType(IndexCollectionType idxCollectionType); +} diff --git a/src/main/java/com/aerospike/documentapi/data/DocumentQueryStatement.java b/src/main/java/com/aerospike/documentapi/data/DocumentQueryStatement.java index 848f4dd..d8015ef 100644 --- a/src/main/java/com/aerospike/documentapi/data/DocumentQueryStatement.java +++ b/src/main/java/com/aerospike/documentapi/data/DocumentQueryStatement.java @@ -15,9 +15,8 @@ public class DocumentQueryStatement { long maxRecords; int recordsPerSecond; String[] jsonPaths; - Filter secondaryIndexFilter; - public Statement toStatement() { + public Statement toStatement(Filter secIndexFilter) { Statement statement = new Statement(); statement.setNamespace(namespace); statement.setSetName(setName); @@ -25,7 +24,7 @@ public Statement toStatement() { statement.setBinNames(binNames); statement.setMaxRecords(maxRecords); statement.setRecordsPerSecond(recordsPerSecond); - statement.setFilter(secondaryIndexFilter); + statement.setFilter(secIndexFilter); return statement; } } diff --git a/src/main/java/com/aerospike/documentapi/data/Operator.java b/src/main/java/com/aerospike/documentapi/data/Operator.java index c245d34..5ec0a26 100644 --- a/src/main/java/com/aerospike/documentapi/data/Operator.java +++ b/src/main/java/com/aerospike/documentapi/data/Operator.java @@ -1,142 +1,31 @@ package com.aerospike.documentapi.data; -import java.util.Arrays; +public enum Operator { -public class Operator { + EQ("=="), + NE("!="), + LT("<"), + GT(">"), + GE(">="), + LE("<="), + REGEX("=~"); - private Operator() { - } - - public static boolean isSimple(String op) { - return Arrays.stream(Simple.values()).anyMatch(t -> t.getName().equalsIgnoreCase(op)); - } - - public static boolean isLogic(String op) { - return Arrays.stream(Logic.values()).anyMatch(t -> t.getName().equalsIgnoreCase(op)); - } - - public static boolean isLogicUnary(String op) { - return Arrays.stream(LogicUnary.values()).anyMatch(t -> t.getName().equalsIgnoreCase(op)); - } - - public static boolean isSpecial(String op) { - return Arrays.stream(Special.values()).anyMatch(t -> t.name().equalsIgnoreCase(op)); - } + private final String name; - public static boolean isSpecial(OperatorType op) { - return isSpecial(op.toString()); + Operator(String operatorName) { + name = operatorName; } - public enum Simple implements OperatorType { - GTE(">="), - LTE("<="), - EQ("=="), - NE("!="), - LT("<"), - GT(">"); - - private final String name; - - Simple(String op) { - name = op; - } - - public static Simple fromString(String name) { - for (Simple v : Simple.values()) { - if (v.name.equalsIgnoreCase(name)) { - return v; - } - } - return null; - } - - public String getName() { - return this.name; - } - } - - public enum Logic implements OperatorType { - AND("&&"), OR("||"); - - private final String name; - - Logic(String op) { - name = op; - } - - public static Logic fromString(String name) { - for (Logic v : Logic.values()) { - if (v.name.equalsIgnoreCase(name)) { - return v; - } + public static Operator fromString(String name) { + for (Operator v : Operator.values()) { + if (v.name.equalsIgnoreCase(name)) { + return v; } - return null; - } - - public String getName() { - return this.name; - } - } - - public enum LogicUnary implements OperatorType { - NOT_EXISTS("!"), EXISTS("EXISTS"); - - private final String name; - - LogicUnary(String op) { - name = op; - } - - public static LogicUnary fromString(String name) { - for (LogicUnary v : LogicUnary.values()) { - if (v.name.equalsIgnoreCase(name)) { - return v; - } - } - return null; - } - - public String getName() { - return this.name; - } - } - - public enum Special implements OperatorType { - TSEQ("==="), // Type safe equals - TSNE("!=="), // Type safe not equals - REGEX("=~"), - NIN("NIN"), - IN("IN"), - CONTAINS("CONTAINS"), - ALL("ALL"), - SIZE("SIZE"), - TYPE("TYPE"), - MATCHES("MATCHES"), - EMPTY("EMPTY"), - SUBSETOF("SUBSETOF"), - ANYOF("ANYOF"), - NONEOF("NONEOF"); - - private final String name; - - Special(String op) { - name = op; - } - - public static Special fromString(String name) { - for (Special v : Special.values()) { - if (v.name.equalsIgnoreCase(name)) { - return v; - } - } - return null; - } - - public String getName() { - return this.name; } + return null; } - public interface OperatorType { + public String getName() { + return this.name; } } diff --git a/src/main/java/com/aerospike/documentapi/util/DocumentExp.java b/src/main/java/com/aerospike/documentapi/util/ExpConverter.java similarity index 69% rename from src/main/java/com/aerospike/documentapi/util/DocumentExp.java rename to src/main/java/com/aerospike/documentapi/util/ExpConverter.java index 3ececde..8f640a4 100644 --- a/src/main/java/com/aerospike/documentapi/util/DocumentExp.java +++ b/src/main/java/com/aerospike/documentapi/util/ExpConverter.java @@ -18,6 +18,8 @@ import java.util.List; import java.util.Map; +import static com.aerospike.documentapi.util.Utils.validateJsonPathSingleStep; + /** * Utility class for converting JSON path to Aerospike {@link Exp}. * @@ -28,7 +30,7 @@ *
      *
    • $.store.book,
    • *
    • $[0],
    • - *
    • $.store.book[0],
    • + *
    • $.store.book[0],
    • *
    • $.store.book[0][1].title.
    • *
    * @@ -41,129 +43,129 @@ * */ @UtilityClass -public class DocumentExp { +public class ExpConverter { /** * Create equal (==) expression. * - * @param binName the document bin name in a record. - * @param jsonPath the JSON path to build a filter expression from. - * @param value the value object to compare with. - * @return the generated filter expression. + * @param binName document bin name in a record. + * @param jsonPath JSON path to build a filter expression from. + * @param value value object to compare with. + * @return generated filter expression. * @throws DocumentApiException if fails to parse the jsonPath. */ public static Exp eq(String binName, String jsonPath, Object value) throws DocumentApiException { - JsonPathObject jsonPathObject = validateJsonPath(new JsonPathParser().parse(jsonPath)); + JsonPathObject jsonPathObject = validateJsonPathSingleStep(new JsonPathParser().parse(jsonPath), errMsg(jsonPath)); return Exp.eq( buildExp(binName, value, jsonPathObject), - getValueExp(value) + getExpValue(value) ); } /** * Create not equal (!=) expression. * - * @param binName the document bin name in a record. - * @param jsonPath the JSON path to build a filter expression from. - * @param value the value object to compare with. - * @return the generated filter expression. + * @param binName document bin name in a record. + * @param jsonPath JSON path to build a filter expression from. + * @param value value object to compare with. + * @return generated filter expression. * @throws DocumentApiException if fails to parse the jsonPath. */ public static Exp ne(String binName, String jsonPath, Object value) throws DocumentApiException { - JsonPathObject jsonPathObject = validateJsonPath(new JsonPathParser().parse(jsonPath)); + JsonPathObject jsonPathObject = validateJsonPathSingleStep(new JsonPathParser().parse(jsonPath), errMsg(jsonPath)); return Exp.ne( buildExp(binName, value, jsonPathObject), - getValueExp(value) + getExpValue(value) ); } /** * Create greater than (>) expression. * - * @param binName the document bin name in a record. - * @param jsonPath the JSON path to build a filter expression from. - * @param value the value object to compare with. - * @return the generated filter expression. + * @param binName document bin name in a record. + * @param jsonPath JSON path to build a filter expression from. + * @param value value object to compare with. + * @return generated filter expression. * @throws DocumentApiException if fails to parse the jsonPath. */ public static Exp gt(String binName, String jsonPath, Object value) throws DocumentApiException { - JsonPathObject jsonPathObject = validateJsonPath(new JsonPathParser().parse(jsonPath)); + JsonPathObject jsonPathObject = validateJsonPathSingleStep(new JsonPathParser().parse(jsonPath), errMsg(jsonPath)); return Exp.gt( buildExp(binName, value, jsonPathObject), - getValueExp(value) + getExpValue(value) ); } /** * Create greater than or equals (>=) expression. * - * @param binName the document bin name in a record. - * @param jsonPath the JSON path to build a filter expression from. - * @param value the value object to compare with. - * @return the generated filter expression. + * @param binName document bin name in a record. + * @param jsonPath JSON path to build a filter expression from. + * @param value value object to compare with. + * @return generated filter expression. * @throws DocumentApiException if fails to parse the jsonPath. */ public static Exp ge(String binName, String jsonPath, Object value) throws DocumentApiException { - JsonPathObject jsonPathObject = validateJsonPath(new JsonPathParser().parse(jsonPath)); + JsonPathObject jsonPathObject = validateJsonPathSingleStep(new JsonPathParser().parse(jsonPath), errMsg(jsonPath)); return Exp.ge( buildExp(binName, value, jsonPathObject), - getValueExp(value) + getExpValue(value) ); } /** * Create less than (<) expression. * - * @param binName the document bin name in a record. - * @param jsonPath the JSON path to build a filter expression from. - * @param value the value object to compare with. - * @return the generated filter expression. + * @param binName document bin name in a record. + * @param jsonPath JSON path to build a filter expression from. + * @param value value object to compare with. + * @return generated filter expression. * @throws DocumentApiException if fails to parse the jsonPath. */ public static Exp lt(String binName, String jsonPath, Object value) throws DocumentApiException { - JsonPathObject jsonPathObject = validateJsonPath(new JsonPathParser().parse(jsonPath)); + JsonPathObject jsonPathObject = validateJsonPathSingleStep(new JsonPathParser().parse(jsonPath), errMsg(jsonPath)); return Exp.lt( buildExp(binName, value, jsonPathObject), - getValueExp(value) + getExpValue(value) ); } /** * Create less than or equals (<=) expression. * - * @param binName the document bin name in a record. - * @param jsonPath the JSON path to build a filter expression from. - * @param value the value object to compare with. - * @return the generated filter expression. + * @param binName document bin name in a record. + * @param jsonPath JSON path to build a filter expression from. + * @param value value object to compare with. + * @return generated filter expression. * @throws DocumentApiException if fails to parse the jsonPath. */ public static Exp le(String binName, String jsonPath, Object value) throws DocumentApiException { - JsonPathObject jsonPathObject = validateJsonPath(new JsonPathParser().parse(jsonPath)); + JsonPathObject jsonPathObject = validateJsonPathSingleStep(new JsonPathParser().parse(jsonPath), errMsg(jsonPath)); return Exp.le( buildExp(binName, value, jsonPathObject), - getValueExp(value) + getExpValue(value) ); } /** * Create expression that performs a regex match on a value specified by a JSON path. * - * @param binName the document bin name in a record. - * @param jsonPath the JSON path to build a filter expression from. - * @param regex the regular expression string. + * @param binName document bin name in a record. + * @param jsonPath JSON path to build a filter expression from. + * @param regex regular expression string. * @param flags regular expression bit flags. See {@link com.aerospike.client.query.RegexFlag}. - * @return the generated filter expression. + * @return generated filter expression. * @throws DocumentApiException if fails to parse the jsonPath. */ public static Exp regex(String binName, String jsonPath, String regex, int flags) throws DocumentApiException { - JsonPathObject jsonPathObject = validateJsonPath(new JsonPathParser().parse(jsonPath)); + JsonPathObject jsonPathObject = validateJsonPathSingleStep(new JsonPathParser().parse(jsonPath), errMsg(jsonPath)); return Exp.regexCompare( regex, flags, @@ -196,7 +198,7 @@ private static Exp buildExp(String binName, Object value, JsonPathObject jsonPat ctx ); } else { - throw new IllegalArgumentException("Unexpected PathPart type"); + throw new IllegalArgumentException(String.format("Unexpected path token type '%s'", lastPart.getType())); } } @@ -225,7 +227,7 @@ private static Exp.Type getValueType(Object value) { } } - private static Exp getValueExp(Object value) { + private static Exp getExpValue(Object value) { if (value instanceof Integer) { return Exp.val((int) value); } else if (value instanceof Long) { @@ -249,10 +251,7 @@ private static Exp getValueExp(Object value) { } } - public static JsonPathObject validateJsonPath(JsonPathObject jsonPathObject) { - if (jsonPathObject.requiresJsonPathQuery()) { - throw new IllegalArgumentException("A two-step JSON path cannot be converted to a filter expression"); - } - return jsonPathObject; + private static String errMsg(String jsonPath) { + return String.format("Two-step JSON path '%s' cannot be converted to a filter expression", jsonPath); } } diff --git a/src/main/java/com/aerospike/documentapi/util/FilterConverter.java b/src/main/java/com/aerospike/documentapi/util/FilterConverter.java new file mode 100644 index 0000000..2a39c03 --- /dev/null +++ b/src/main/java/com/aerospike/documentapi/util/FilterConverter.java @@ -0,0 +1,170 @@ +package com.aerospike.documentapi.util; + +import com.aerospike.client.cdt.CTX; +import com.aerospike.client.query.Filter; +import com.aerospike.client.query.IndexCollectionType; +import com.aerospike.documentapi.DocumentApiException; +import com.aerospike.documentapi.jsonpath.JsonPathObject; +import com.aerospike.documentapi.jsonpath.JsonPathParser; +import com.aerospike.documentapi.token.ContextAwareToken; +import lombok.experimental.UtilityClass; + +import java.util.ArrayList; +import java.util.List; + +import static com.aerospike.documentapi.util.Utils.validateJsonPathSingleStep; + +/** + * Utility class for converting JSON path to Aerospike {@link Filter}. + * + *

    Supported JSON paths: containing only map and/or array elements, + * without wildcards, recursive descent, filter expressions, functions and scripts. + * + *

    Examples of supported JSON paths:

    + *
      + *
    • $.store.book,
    • + *
    • $[0],
    • + *
    • $.store.book[0],
    • + *
    • $.store.book[0][1].title.
    • + *
    + * + *

    Examples of unsupported JSON paths:

    + *
      + *
    • $.store.book[*].author,
    • + *
    • $.store..price,
    • + *
    • $.store.book[?(@.price < 10)]
    • + *
    • $..book[(@.length-1)]
    • + *
    + */ +@UtilityClass +public class FilterConverter { + + /** + * Create equal (==) Filter. + * + * @param binName document bin name in a record. + * @param jsonPath JSON path to build a filter expression from. + * @param value value object to compare with. + * @return generated Filter. + * @throws DocumentApiException if fails to parse the jsonPath. + */ + public static Filter eq(String binName, String jsonPath, Object value) + throws DocumentApiException { + JsonPathObject jsonPathObject = validateJsonPathSingleStep(new JsonPathParser().parse(jsonPath), errMsg(jsonPath)); + Number val = getNumber(value); + if (val == null) { + return Filter.equal(binName, value.toString(), getCTX(jsonPathObject)); + } else { + return Filter.equal(binName, val.longValue(), getCTX(jsonPathObject)); + } + } + + /** + * Create less than (<) Filter. + * + * @param binName document bin name in a record. + * @param jsonPath JSON path to build a filter expression from. + * @param value value object to compare with. + * @return generated Filter. + * @throws DocumentApiException if fails to parse the jsonPath. + */ + public static Filter lt(String binName, String jsonPath, Object value, IndexCollectionType idxCollectionType) + throws DocumentApiException { + JsonPathObject jsonPathObject = validateJsonPathSingleStep(new JsonPathParser().parse(jsonPath), errMsg(jsonPath)); + Number val = getNumber(value); + if (val == null) { + throw new IllegalArgumentException("'<' operator can be applied only to a number"); + } else { + return Filter.range(binName, idxCollectionType, Long.MIN_VALUE, val.longValue() - 1, + getCTX(jsonPathObject)); + } + } + + /** + * Create greater than (>) Filter. + * + * @param binName document bin name in a record. + * @param jsonPath JSON path to build a filter expression from. + * @param value value object to compare with. + * @return generated Filter. + * @throws DocumentApiException if fails to parse the jsonPath. + */ + public static Filter gt(String binName, String jsonPath, Object value, IndexCollectionType idxCollectionType) + throws DocumentApiException { + JsonPathObject jsonPathObject = validateJsonPathSingleStep(new JsonPathParser().parse(jsonPath), errMsg(jsonPath)); + Number val = getNumber(value); + if (val == null) { + throw new IllegalArgumentException("'>' operator can be applied only to a number"); + } else { + return Filter.range(binName, idxCollectionType, val.longValue() + 1, Long.MAX_VALUE, + getCTX(jsonPathObject)); + } + } + + /** + * Create less than or equals (<=) Filter. + * + * @param binName document bin name in a record. + * @param jsonPath JSON path to build a filter expression from. + * @param value value object to compare with. + * @return generated Filter. + * @throws DocumentApiException if fails to parse the jsonPath. + */ + public static Filter le(String binName, String jsonPath, Object value, IndexCollectionType idxCollectionType) + throws DocumentApiException { + JsonPathObject jsonPathObject = validateJsonPathSingleStep(new JsonPathParser().parse(jsonPath), errMsg(jsonPath)); + Number val = getNumber(value); + if (val == null) { + throw new IllegalArgumentException("'<=' operator can be applied only to a number"); + } else { + return Filter.range(binName, idxCollectionType, Long.MIN_VALUE, val.longValue(), getCTX(jsonPathObject)); + } + } + + /** + * Create greater than or equals (>) Filter. + * + * @param binName document bin name in a record. + * @param jsonPath JSON path to build a filter expression from. + * @param value value object to compare with. + * @return generated Filter. + * @throws DocumentApiException if fails to parse the jsonPath. + */ + public static Filter ge(String binName, String jsonPath, Object value, IndexCollectionType idxCollectionType) + throws DocumentApiException { + JsonPathObject jsonPathObject = validateJsonPathSingleStep(new JsonPathParser().parse(jsonPath), errMsg(jsonPath)); + Number val = getNumber(value); + if (val == null) { + throw new IllegalArgumentException("'>=' operator can be applied only to a number"); + } else { + return Filter.range(binName, idxCollectionType, val.longValue(), Long.MAX_VALUE, getCTX(jsonPathObject)); + } + } + + private static String errMsg(String jsonPath) { + return String.format("Two-step JSON path '%s' cannot be converted to a Filter", jsonPath); + } + + private static Number getNumber(Object value) { + if (value instanceof Integer) { + return (int) value; + } else if (value instanceof Long) { + return (long) value; + } else if (value instanceof Short) { + return (long) value; + } else if (value instanceof String) { + return null; + } else if (value instanceof Boolean) { + return null; + } else { + throw new IllegalArgumentException("Unsupported value type"); + } + } + + private static CTX[] getCTX(JsonPathObject jsonPathObject) { + List partList = new ArrayList<>(jsonPathObject.getTokensNotRequiringSecondStepQuery()); + return partList.stream() + .map(ContextAwareToken::toAerospikeContext) + .toArray(CTX[]::new); + } +} diff --git a/src/main/java/com/aerospike/documentapi/util/Utils.java b/src/main/java/com/aerospike/documentapi/util/Utils.java index eb9081b..7478bfb 100644 --- a/src/main/java/com/aerospike/documentapi/util/Utils.java +++ b/src/main/java/com/aerospike/documentapi/util/Utils.java @@ -3,6 +3,7 @@ import com.aerospike.client.Bin; import com.aerospike.client.cdt.CTX; import com.aerospike.client.cdt.MapOrder; +import com.aerospike.documentapi.jsonpath.JsonPathObject; import com.aerospike.documentapi.jsonpath.JsonPathParser; import com.aerospike.documentapi.jsonpath.PathDetails; import com.aerospike.documentapi.token.ContextAwareToken; @@ -50,4 +51,11 @@ public static PathDetails getPathDetails(List tokens, boolean public static boolean isBlank(String string) { return string == null || string.trim().isEmpty(); } + + public static JsonPathObject validateJsonPathSingleStep(JsonPathObject jsonPathObject, String msg) { + if (jsonPathObject.requiresJsonPathQuery()) { + throw new IllegalArgumentException(msg); + } + return jsonPathObject; + } } diff --git a/src/test/java/com/aerospike/documentapi/DocumentExpTests.java b/src/test/java/com/aerospike/documentapi/DocumentQueryTests.java similarity index 67% rename from src/test/java/com/aerospike/documentapi/DocumentExpTests.java rename to src/test/java/com/aerospike/documentapi/DocumentQueryTests.java index bb20f56..111fead 100644 --- a/src/test/java/com/aerospike/documentapi/DocumentExpTests.java +++ b/src/test/java/com/aerospike/documentapi/DocumentQueryTests.java @@ -5,11 +5,12 @@ import com.aerospike.client.IAerospikeClient; import com.aerospike.client.Key; import com.aerospike.client.ResultCode; +import com.aerospike.client.Value; +import com.aerospike.client.cdt.CTX; import com.aerospike.client.exp.Exp; import com.aerospike.client.policy.Policy; import com.aerospike.client.policy.QueryPolicy; import com.aerospike.client.policy.WritePolicy; -import com.aerospike.client.query.Filter; import com.aerospike.client.query.IndexCollectionType; import com.aerospike.client.query.IndexType; import com.aerospike.client.query.KeyRecord; @@ -17,10 +18,13 @@ import com.aerospike.client.query.RegexFlag; import com.aerospike.client.query.Statement; import com.aerospike.client.task.IndexTask; +import com.aerospike.documentapi.data.DocFilterExp; +import com.aerospike.documentapi.data.DocFilterSecIndex; import com.aerospike.documentapi.data.DocumentFilterExp; import com.aerospike.documentapi.data.DocumentQueryStatement; +import com.aerospike.documentapi.data.DocumentFilterSecIndex; import com.aerospike.documentapi.data.KeyResult; -import com.aerospike.documentapi.util.DocumentExp; +import com.aerospike.documentapi.util.ExpConverter; import net.minidev.json.JSONArray; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -38,14 +42,19 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; -import static com.aerospike.documentapi.data.Operator.Simple.GT; -import static com.aerospike.documentapi.data.Operator.Simple.GTE; +import static com.aerospike.documentapi.data.Operator.EQ; +import static com.aerospike.documentapi.data.Operator.GE; +import static com.aerospike.documentapi.data.Operator.GT; +import static com.aerospike.documentapi.data.Operator.LT; +import static com.aerospike.documentapi.data.Operator.NE; +import static com.aerospike.documentapi.data.Operator.REGEX; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; @TestInstance(TestInstance.Lifecycle.PER_CLASS) -class DocumentExpTests extends BaseTestConfig { +class DocumentQueryTests extends BaseTestConfig { private static final Key QUERY_KEY_1 = new Key(AEROSPIKE_NAMESPACE, AEROSPIKE_SET, "key1"); private static final Key QUERY_KEY_2 = new Key(AEROSPIKE_NAMESPACE, AEROSPIKE_SET, "key2"); @@ -61,8 +70,11 @@ class DocumentExpTests extends BaseTestConfig { void setUp() { client.put(writePolicy(), QUERY_KEY_1, mapBin1, listBin1); client.put(writePolicy(), QUERY_KEY_2, mapBin2, listBin2); - // create collection index on mapKey - createIndex(client, AEROSPIKE_NAMESPACE, AEROSPIKE_SET, "mapkey_index", MAP_BIN_NAME); + createIndex(client, AEROSPIKE_NAMESPACE, AEROSPIKE_SET, "mapkey_k1_k11_idx", MAP_BIN_NAME, + IndexType.NUMERIC, IndexCollectionType.DEFAULT, + CTX.mapKey(Value.get("mapKey")), + CTX.mapKey(Value.get("k1")), + CTX.mapKey(Value.get("k11"))); } @AfterAll @@ -75,26 +87,29 @@ void tearDown() { @Test void queryList() throws DocumentApiException { String jsonPath = "$.listKey[0].k11"; - Exp exp = DocumentExp.lt(MAP_BIN_NAME, jsonPath, 100); - QueryPolicy queryPolicy = new QueryPolicy(writePolicy()); - queryPolicy.filterExp = Exp.build(exp); + DocumentQueryStatement queryStatement = DocumentQueryStatement.builder() + .namespace(AEROSPIKE_NAMESPACE) + .setName(AEROSPIKE_SET) + .jsonPaths(new String[]{jsonPath}) + .build(); - List keyRecords = recordSetToList(client.query(queryPolicy, statement())); - assertEquals(1, keyRecords.size()); - assertEquals("key1", keyRecords.get(0).key.userKey.getObject()); + DocumentFilterExp filterExp = new DocFilterExp(MAP_BIN_NAME, jsonPath, LT, 100); + Stream test = documentClient.query(queryStatement, filterExp); + List keyResults = test.collect(Collectors.toList()); + assertEquals(1, keyResults.size()); + assertEquals("key1", keyResults.get(0).getKey().userKey.getObject()); } @Test void queryMap() throws DocumentApiException { String jsonPath = "$.mapKey.k1.k11"; - DocumentQueryStatement queryStatement = DocumentQueryStatement.builder() .namespace(AEROSPIKE_NAMESPACE) .setName(AEROSPIKE_SET) .jsonPaths(new String[]{jsonPath}) .build(); - DocumentFilterExp filterExp = new DocumentFilterExp(MAP_BIN_NAME, jsonPath, GTE, 100); + DocumentFilterExp filterExp = new DocFilterExp(MAP_BIN_NAME, jsonPath, GE, 100); Stream test = documentClient.query(queryStatement, filterExp); List keyResults = test.collect(Collectors.toList()); assertEquals(1, keyResults.size()); @@ -103,45 +118,105 @@ void queryMap() throws DocumentApiException { @Test void queryMapSecondaryIndex() throws DocumentApiException { + String jsonPath = "$.mapKey.k1.k11"; + DocumentQueryStatement queryStatement = DocumentQueryStatement.builder() .namespace(AEROSPIKE_NAMESPACE) .setName(AEROSPIKE_SET) - .secondaryIndexFilter(Filter.contains(MAP_BIN_NAME, IndexCollectionType.MAPKEYS, "listKey")) .build(); - DocumentFilterExp filterExp = null; - Stream test = documentClient.query(queryStatement, filterExp); + DocumentFilterSecIndex sIndexFilter = new DocFilterSecIndex(MAP_BIN_NAME, jsonPath, GT, 11); + Stream test = documentClient.query(queryStatement, sIndexFilter); List keyResults = test.collect(Collectors.toList()); - assertEquals(2, keyResults.size()); + assertEquals(1, keyResults.size()); } @Test void queryMapSecondaryIndexNoMatch() throws DocumentApiException { + String jsonPath = "$.mapKey.k1.k11"; + // there are only 11 and 111 values of k11 + DocumentFilterSecIndex secIndexFilter = new DocFilterSecIndex(MAP_BIN_NAME, jsonPath, EQ, 100); + DocumentQueryStatement queryStatement = DocumentQueryStatement.builder() .namespace(AEROSPIKE_NAMESPACE) .setName(AEROSPIKE_SET) - .secondaryIndexFilter(Filter.contains(MAP_BIN_NAME, IndexCollectionType.MAPKEYS, "lllll")) .build(); - DocumentFilterExp filterExp = null; - Stream test = documentClient.query(queryStatement, filterExp); + Stream test = documentClient.query(queryStatement, secIndexFilter); List keyResults = test.collect(Collectors.toList()); assertEquals(0, keyResults.size()); } + @Test + void queryMapSecondaryIndexUnsupportedOperator() throws DocumentApiException { + String jsonPath = "$.mapKey.k1.k11"; + // NE is not supported + try { + DocumentFilterSecIndex secIndexFilter = new DocFilterSecIndex(MAP_BIN_NAME, jsonPath, NE, 100); + + DocumentQueryStatement queryStatement = DocumentQueryStatement.builder() + .namespace(AEROSPIKE_NAMESPACE) + .setName(AEROSPIKE_SET) + .build(); + + Stream test = documentClient.query(queryStatement, secIndexFilter); + fail("IllegalArgumentException should have been thrown"); + } catch (UnsupportedOperationException ignored) { + } + } + + @Test + void queryMapSecondaryIndexUnsupportedValueTypeDouble() throws DocumentApiException { + String jsonPath = "$.mapKey.k1.k11"; + // float and double numbers are not supported + try { + DocumentFilterSecIndex secIndexFilter = new DocFilterSecIndex(MAP_BIN_NAME, jsonPath, EQ, 110.335); + + DocumentQueryStatement queryStatement = DocumentQueryStatement.builder() + .namespace(AEROSPIKE_NAMESPACE) + .setName(AEROSPIKE_SET) + .jsonPaths(new String[]{jsonPath}) + .build(); + + Stream test = documentClient.query(queryStatement, secIndexFilter); + fail("IllegalArgumentException should have been thrown"); + } catch (IllegalArgumentException ignored) { + } + } + + @Test + void queryMapSecondaryIndexUnsupportedValueTypeCollection() throws DocumentApiException { + String jsonPath = "$.mapKey.k1.k11"; + // Collections are not supported + try { + DocumentFilterSecIndex secIndexFilter = new DocFilterSecIndex(MAP_BIN_NAME, jsonPath, EQ, + new ArrayList()); + + DocumentQueryStatement queryStatement = DocumentQueryStatement.builder() + .namespace(AEROSPIKE_NAMESPACE) + .setName(AEROSPIKE_SET) + .jsonPaths(new String[]{jsonPath}) + .build(); + + Stream test = documentClient.query(queryStatement, secIndexFilter); + fail("IllegalArgumentException should have been thrown"); + } catch (IllegalArgumentException ignored) { + } + } + @Test void queryMapSecondaryIndexJsonPathFilterExp() throws DocumentApiException { String jsonPath = "$.mapKey.k1.k11"; + DocumentFilterSecIndex secIndexFilter = new DocFilterSecIndex(MAP_BIN_NAME, jsonPath, GE, 100); DocumentQueryStatement queryStatement = DocumentQueryStatement.builder() .namespace(AEROSPIKE_NAMESPACE) .setName(AEROSPIKE_SET) .jsonPaths(new String[]{jsonPath}) - .secondaryIndexFilter(Filter.contains(MAP_BIN_NAME, IndexCollectionType.MAPKEYS, "listKey")) .build(); - DocumentFilterExp filterExp = new DocumentFilterExp(MAP_BIN_NAME, jsonPath, GTE, 100); - Stream test = documentClient.query(queryStatement, filterExp); + DocumentFilterExp filterExp = new DocFilterExp(MAP_BIN_NAME, jsonPath, GE, 100); + Stream test = documentClient.query(queryStatement, filterExp, secIndexFilter); List keyResults = test.collect(Collectors.toList()); assertEquals(1, keyResults.size()); assertEquals("key2", keyResults.get(0).getKey().userKey.toString()); @@ -150,7 +225,6 @@ void queryMapSecondaryIndexJsonPathFilterExp() throws DocumentApiException { @Test void queryMapMultipleBins() throws DocumentApiException { String jsonPath = "$.mapKey.k1.k11"; - DocumentQueryStatement queryStatement = DocumentQueryStatement.builder() .namespace(AEROSPIKE_NAMESPACE) .setName(AEROSPIKE_SET) @@ -158,7 +232,7 @@ void queryMapMultipleBins() throws DocumentApiException { .jsonPaths(new String[]{jsonPath}) .build(); - DocumentFilterExp filterExp = new DocumentFilterExp(MAP_BIN_NAME, jsonPath, GTE, 100); + DocumentFilterExp filterExp = new DocFilterExp(MAP_BIN_NAME, jsonPath, GE, 100); Stream test = documentClient.query(queryStatement, filterExp); List keyResults = test.collect(Collectors.toList()); assertEquals(1, keyResults.size()); @@ -167,7 +241,6 @@ void queryMapMultipleBins() throws DocumentApiException { @Test void queryMapEmptyResult() throws DocumentApiException { String jsonPath = "$.mapKey.k1.k11"; // value exists in MapBin - DocumentQueryStatement queryStatement = DocumentQueryStatement.builder() .namespace(AEROSPIKE_NAMESPACE) .setName(AEROSPIKE_SET) @@ -175,7 +248,7 @@ void queryMapEmptyResult() throws DocumentApiException { .jsonPaths(new String[]{jsonPath}) .build(); - DocumentFilterExp filterExp = new DocumentFilterExp(MAP_BIN_NAME, jsonPath, GTE, 100); + DocumentFilterExp filterExp = new DocFilterExp(MAP_BIN_NAME, jsonPath, GE, 100); Stream test = documentClient.query(queryStatement, filterExp); List keyResults = test.collect(Collectors.toList()); assertEquals(0, keyResults.size()); @@ -193,8 +266,7 @@ void queryMapMultiplePathsNoFilterExp() throws DocumentApiException { .jsonPaths(new String[]{jsonPathMapKey, jsonPathListKey, jsonPathListBin}) .build(); - DocumentFilterExp filterExp = null; - Stream test = documentClient.query(queryStatement, filterExp); + Stream test = documentClient.query(queryStatement); List keyResults = test.collect(Collectors.toList()); assertEquals(2, keyResults.size()); //noinspection unchecked @@ -214,9 +286,9 @@ void queryMapMultiplePathsAndFilterExps() throws DocumentApiException { .jsonPaths(new String[]{jsonPathMapKey, jsonPathListKey, jsonPathListBin}) .build(); - DocumentFilterExp filterExpMapKeyGte100 = new DocumentFilterExp(MAP_BIN_NAME, jsonPathMapKey, GTE, 100); - DocumentFilterExp filterExpListKeyGte100 = new DocumentFilterExp(MAP_BIN_NAME, jsonPathListKey, GT, 100); - DocumentFilterExp filterExpListBin = new DocumentFilterExp(LIST_BIN_NAME, jsonPathListBin, GTE, 100); + DocumentFilterExp filterExpMapKeyGte100 = new DocFilterExp(MAP_BIN_NAME, jsonPathMapKey, GE, 100); + DocumentFilterExp filterExpListKeyGte100 = new DocFilterExp(MAP_BIN_NAME, jsonPathListKey, GT, 100); + DocumentFilterExp filterExpListBin = new DocFilterExp(LIST_BIN_NAME, jsonPathListBin, GE, 100); Stream test = documentClient.query(queryStatement, filterExpMapKeyGte100, filterExpListKeyGte100, filterExpListBin); List keyResults = test.collect(Collectors.toList()); @@ -234,8 +306,7 @@ void queryFilter() throws DocumentApiException { .jsonPaths(new String[]{jsonPath}) .build(); - DocumentFilterExp filterExp = null; - Stream test = documentClient.query(queryStatement, filterExp); + Stream test = documentClient.query(queryStatement); List keyResults = test.collect(Collectors.toList()); assertEquals(1, keyResults.size()); @@ -261,19 +332,24 @@ void queryFilter() throws DocumentApiException { @Test void queryListRegex() throws DocumentApiException { String jsonPath = "$.listKey[2]"; - Exp exp = DocumentExp.regex(MAP_BIN_NAME, jsonPath, "10.*", RegexFlag.ICASE); - QueryPolicy queryPolicy = new QueryPolicy(writePolicy()); - queryPolicy.filterExp = Exp.build(exp); + DocumentQueryStatement queryStatement = DocumentQueryStatement.builder() + .namespace(AEROSPIKE_NAMESPACE) + .setName(AEROSPIKE_SET) + .jsonPaths(new String[]{jsonPath}) + .build(); - List keyRecords = recordSetToList(client.query(queryPolicy, statement())); - assertEquals(1, keyRecords.size()); - assertEquals("key2", keyRecords.get(0).key.userKey.getObject()); + DocumentFilterExp filterExp = new DocFilterExp(MAP_BIN_NAME, jsonPath, REGEX, "10.*"); + filterExp.setRegexFlags(RegexFlag.ICASE); + Stream test = documentClient.query(queryStatement, filterExp); + List keyResults = test.collect(Collectors.toList()); + assertEquals(1, keyResults.size()); + assertEquals("key2", keyResults.get(0).getKey().userKey.getObject()); } @Test void queryRootList() throws DocumentApiException { String jsonPath = "$[1]"; - Exp exp = DocumentExp.ne(LIST_BIN_NAME, jsonPath, 102); + Exp exp = ExpConverter.ne(LIST_BIN_NAME, jsonPath, 102); QueryPolicy queryPolicy = new QueryPolicy(writePolicy()); queryPolicy.filterExp = Exp.build(exp); @@ -287,7 +363,7 @@ void testInvalidJsonPath() { String jsonPath = "abc"; assertThrows( DocumentApiException.class, - () -> DocumentExp.eq(MAP_BIN_NAME, jsonPath, 100) + () -> ExpConverter.eq(MAP_BIN_NAME, jsonPath, 100) ); } @@ -296,7 +372,7 @@ void testTwoStepJsonPath() { String jsonPath = "$.listKey[*]"; assertThrows( IllegalArgumentException.class, - () -> DocumentExp.gt(MAP_BIN_NAME, jsonPath, 100) + () -> ExpConverter.gt(MAP_BIN_NAME, jsonPath, 100) ); } @@ -357,14 +433,17 @@ private void createIndex( String namespace, String set, String indexName, - String binName + String binName, + IndexType idxType, + IndexCollectionType collectionType, + CTX... ctx ) throws RuntimeException { Policy policy = new Policy(); policy.socketTimeout = 0; // Do not time out on index create. try { IndexTask task = client.createIndex(policy, namespace, set, indexName, binName, - IndexType.STRING, IndexCollectionType.MAPKEYS); + idxType, collectionType, ctx); task.waitTillComplete(); } catch (AerospikeException ae) { if (ae.getResultCode() != ResultCode.INDEX_ALREADY_EXISTS) {