diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexDefinition.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexDefinition.java index 9d69551d1c4..8439feca0ee 100644 --- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexDefinition.java +++ b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexDefinition.java @@ -31,6 +31,7 @@ import org.apache.jackrabbit.oak.api.Type; import org.apache.jackrabbit.oak.commons.collections.StreamUtils; +import org.apache.jackrabbit.oak.plugins.index.elastic.util.ElasticIndexUtils; import org.apache.jackrabbit.oak.plugins.index.search.FieldNames; import org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants; import org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition; @@ -318,19 +319,18 @@ public float getSimilarityTagsBoost() { */ public String getElasticKeyword(String propertyName) { List propertyDefinitions = propertiesByName.get(propertyName); + String field = ElasticIndexUtils.fieldName(propertyName); if (propertyDefinitions == null) { // if there are no property definitions we return the default keyword name // this can happen for properties that were not explicitly defined (eg: created with a regex) ElasticPropertyDefinition pd = getMatchingRegexPropertyDefinition(propertyName); - if (pd != null) { - if (pd.isFlattened()) { - return FieldNames.FLATTENED_FIELD_PREFIX + pd.nodeName + "." + propertyName; - } + if (pd != null && pd.isFlattened()) { + return FieldNames.FLATTENED_FIELD_PREFIX + + ElasticIndexUtils.fieldName(pd.nodeName) + "." + field; + } else { + return field + ".keyword"; } - return propertyName + ".keyword"; } - - String field = propertyName; // it's ok to look at the first property since we are sure they all have the same type int type = propertyDefinitions.get(0).getType(); if (isAnalyzable.apply(type) && isAnalyzed(propertyDefinitions)) { diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexStatistics.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexStatistics.java index ed52c9b57d7..4d64d77ac5f 100644 --- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexStatistics.java +++ b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexStatistics.java @@ -25,6 +25,8 @@ import java.util.concurrent.TimeUnit; import co.elastic.clients.elasticsearch._types.query_dsl.Query; + +import org.apache.jackrabbit.oak.plugins.index.elastic.util.ElasticIndexUtils; import org.apache.jackrabbit.oak.plugins.index.search.IndexStatistics; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -111,8 +113,9 @@ public int numDocs() { */ @Override public int getDocCountFor(String field) { + String elasticField = ElasticIndexUtils.fieldName(field); return countCache.getUnchecked( - new StatsRequestDescriptor(elasticConnection, indexDefinition.getIndexAlias(), field, null) + new StatsRequestDescriptor(elasticConnection, indexDefinition.getIndexAlias(), elasticField, null) ); } diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticDocument.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticDocument.java index 3c7dc6f4f32..2f1ee7e26e9 100644 --- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticDocument.java +++ b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticDocument.java @@ -147,9 +147,9 @@ void addProperty(String fieldName, Object value) { properties.put(fieldName, finalValue); } - void addSimilarityField(String name, Blob value) throws IOException { + void addSimilarityField(String fieldName, Blob value) throws IOException { byte[] bytes = value.getNewStream().readAllBytes(); - addProperty(FieldNames.createSimilarityFieldName(name), toFloats(bytes)); + addProperty(FieldNames.createSimilarityFieldName(fieldName), toFloats(bytes)); } void indexAncestors(String path) { @@ -160,8 +160,8 @@ void indexAncestors(String path) { addProperty(FieldNames.PATH_DEPTH, depth); } - void addDynamicBoostField(String propName, String value, double boost) { - addProperty(propName, + void addDynamicBoostField(String fieldName, String value, double boost) { + addProperty(fieldName, Map.of( ElasticIndexHelper.DYNAMIC_BOOST_NESTED_VALUE, value, ElasticIndexHelper.DYNAMIC_BOOST_NESTED_BOOST, boost diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticDocumentMaker.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticDocumentMaker.java index 5316cf1127f..59c84f9b019 100644 --- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticDocumentMaker.java +++ b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticDocumentMaker.java @@ -24,6 +24,7 @@ import org.apache.jackrabbit.oak.commons.log.LogSilencer; import org.apache.jackrabbit.oak.plugins.index.elastic.ElasticIndexDefinition; import org.apache.jackrabbit.oak.plugins.index.elastic.ElasticPropertyDefinition; +import org.apache.jackrabbit.oak.plugins.index.elastic.util.ElasticIndexUtils; import org.apache.jackrabbit.oak.plugins.index.search.Aggregate; import org.apache.jackrabbit.oak.plugins.index.search.FieldNames; import org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition; @@ -165,16 +166,17 @@ protected boolean addTypedFields(ElasticDocument doc, PropertyState property, St } @Override - protected void indexTypedProperty(ElasticDocument doc, PropertyState property, String pname, PropertyDefinition pd, int i) { + protected void indexTypedProperty(ElasticDocument doc, PropertyState property, String propertyName, PropertyDefinition pd, int i) { // Get the Type tag from the defined index definition here - and not from the actual persisted property state - this way in case // If the actual property value is different from the property type defined in the index definition/mapping - this will try to convert the property if possible, // otherwise will log a warning and not try and add the property to index. If we try and index incompatible data types (like String to Date), // we would get an exception while indexing the node on elastic search and other properties for the node will also don't get indexed. (See OAK-9665). - String fieldName = pname; + String fieldName = ElasticIndexUtils.fieldName(propertyName); if (pd.isRegexp) { ElasticPropertyDefinition epd = (ElasticPropertyDefinition) pd; if (epd.isFlattened()) { - fieldName = FieldNames.FLATTENED_FIELD_PREFIX + epd.nodeName + "." + pname; + fieldName = FieldNames.FLATTENED_FIELD_PREFIX + + ElasticIndexUtils.fieldName(epd.nodeName) + "." + fieldName; } } int tag = pd.getType(); @@ -197,7 +199,7 @@ protected void indexTypedProperty(ElasticDocument doc, PropertyState property, S if (!LOG_SILENCER.silence(LOG_KEY_COULD_NOT_CONVERT_PROPERTY)) { LOG.warn( "[{}] Ignoring property. Could not convert property {} (field {}) of type {} to type {} for path {}. Error: {}", - getIndexName(), pname, fieldName, + getIndexName(), propertyName, fieldName, Type.fromTag(property.getType().tag(), false), Type.fromTag(tag, false), path, e.toString()); } @@ -252,7 +254,7 @@ protected void indexSimilarityBinaries(ElasticDocument doc, PropertyDefinition p if (pd.getSimilaritySearchDenseVectorSize() == blob.length() / BLOB_LENGTH_DIVISOR) { // see https://www.elastic.co/blog/text-similarity-search-with-vectors-in-elasticsearch // see https://www.elastic.co/guide/en/elasticsearch/reference/current/dense-vector.html - doc.addSimilarityField(pd.name, blob); + doc.addSimilarityField(ElasticIndexUtils.fieldName(pd.name), blob); } else { if (!LOG_SILENCER.silence(LOG_KEY_SIMILARITY_BINARIES_WRONG_DIMENSION)) { LOG.warn("[{}] Ignoring binary property {} for path {}. Expected dimension is {} but got {}", @@ -275,7 +277,7 @@ protected boolean augmentCustomFields(String path, ElasticDocument doc, NodeStat @Override protected boolean indexDynamicBoost(ElasticDocument doc, String parent, String nodeName, String token, double boost) { if (!token.isEmpty()) { - doc.addDynamicBoostField(nodeName, token, boost); + doc.addDynamicBoostField(ElasticIndexUtils.fieldName(nodeName), token, boost); return true; } return false; diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexHelper.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexHelper.java index f42432ea0cc..cd9ac9f2b35 100644 --- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexHelper.java +++ b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexHelper.java @@ -22,6 +22,7 @@ import org.apache.jackrabbit.oak.api.Type; import org.apache.jackrabbit.oak.plugins.index.elastic.ElasticIndexDefinition; import org.apache.jackrabbit.oak.plugins.index.elastic.ElasticPropertyDefinition; +import org.apache.jackrabbit.oak.plugins.index.elastic.util.ElasticIndexUtils; import org.apache.jackrabbit.oak.plugins.index.search.FieldNames; import org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition.IndexingRule; import org.apache.jackrabbit.oak.plugins.index.search.PropertyDefinition; @@ -149,7 +150,8 @@ private static void mapInferenceDefinition(@NotNull TypeMapping.Builder builder, builder.meta("inference", JsonData.of(inferenceDefinition)); if (inferenceDefinition.properties != null) { - inferenceDefinition.properties.forEach(p -> builder.properties(p.name, + inferenceDefinition.properties.forEach(p -> builder.properties( + ElasticIndexUtils.fieldName(p.name), b -> b.object(bo -> bo .properties("value", pb -> pb.denseVector(dv -> dv.index(true) @@ -243,13 +245,15 @@ private static void mapIndexRules(@NotNull TypeMapping.Builder builder, if (epd.isFlattened()) { Property.Builder pBuilder = new Property.Builder(); pBuilder.flattened(b2 -> b2.index(true)); - builder.properties(FieldNames.FLATTENED_FIELD_PREFIX + pd.nodeName, pBuilder.build()); + builder.properties(FieldNames.FLATTENED_FIELD_PREFIX + + ElasticIndexUtils.fieldName(pd.nodeName), pBuilder.build()); } } } for (Map.Entry> entry : indexDefinition.getPropertiesByName().entrySet()) { - final String name = entry.getKey(); - final List propertyDefinitions = entry.getValue(); + String propertyName = entry.getKey(); + String fieldName = ElasticIndexUtils.fieldName(propertyName); + List propertyDefinitions = entry.getValue(); Type type = null; for (PropertyDefinition pd : propertyDefinitions) { type = Type.fromTag(pd.getType(), false); @@ -280,10 +284,10 @@ private static void mapIndexRules(@NotNull TypeMapping.Builder builder, pBuilder.keyword(b1 -> b1.ignoreAbove(256)); } } - builder.properties(name, pBuilder.build()); + builder.properties(fieldName, pBuilder.build()); for (PropertyDefinition pd : indexDefinition.getDynamicBoostProperties()) { - builder.properties(pd.nodeName, + builder.properties(ElasticIndexUtils.fieldName(pd.nodeName), b1 -> b1.nested( b2 -> b2.properties(DYNAMIC_BOOST_NESTED_VALUE, b3 -> b3.text( @@ -305,7 +309,9 @@ private static void mapIndexRules(@NotNull TypeMapping.Builder builder, .similarity(DEFAULT_SIMILARITY_METRIC) .build(); - builder.properties(FieldNames.createSimilarityFieldName(pd.name), b1 -> b1.denseVector(denseVectorProperty)); + builder.properties(FieldNames.createSimilarityFieldName( + ElasticIndexUtils.fieldName(pd.name)), + b1 -> b1.denseVector(denseVectorProperty)); } builder.properties(ElasticIndexDefinition.SIMILARITY_TAGS, diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticRequestHandler.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticRequestHandler.java index 330aecc1671..6b9d71eec27 100644 --- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticRequestHandler.java +++ b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticRequestHandler.java @@ -263,7 +263,8 @@ public Optional similarityQuery(@NotNull String text, List inference(BoolQuery.Builder b, String propertyN } private Stream dynamicScoreQueries(String text) { - return elasticIndexDefinition.getDynamicBoostProperties().stream().map(pd -> NestedQuery.of(n -> n - .path(pd.nodeName) - .query(q -> q.functionScore(s -> s + return elasticIndexDefinition.getDynamicBoostProperties().stream() + .map(pd -> { + String field = ElasticIndexUtils.fieldName(pd.nodeName); + return NestedQuery.of(n -> n + .path(field) + .query(q -> q.functionScore(s -> s .boost(DYNAMIC_BOOST_WEIGHT) - .query(fq -> fq.match(m -> m.field(pd.nodeName + ".value").query(FieldValue.of(text)))) - .functions(f -> f.fieldValueFactor(fv -> fv.field(pd.nodeName + ".boost"))))) - .scoreMode(ChildScoreMode.Avg)) + .query(fq -> fq.match(m -> m.field( + field + ".value"). + query(FieldValue.of(text)))) + .functions(f -> f.fieldValueFactor(fv -> fv.field( + field + ".boost"))))) + .scoreMode(ChildScoreMode.Avg)); + } ); } @@ -889,8 +897,8 @@ private static QueryStringQuery.Builder fullTextQuery(String text, String fieldN .type(TextQueryType.CrossFields) .tieBreaker(0.5d); if (FieldNames.FULLTEXT.equals(fieldName)) { - for(PropertyDefinition pd: pr.indexingRule.getNodeScopeAnalyzedProps()) { - qsqBuilder.fields(pd.name + "^" + pd.boost); + for (PropertyDefinition pd : pr.indexingRule.getNodeScopeAnalyzedProps()) { + qsqBuilder.fields(ElasticIndexUtils.fieldName(pd.name) + "^" + pd.boost); } // dynamic boost is included only for :fulltext field if (includeDynamicBoostedValues) { @@ -951,6 +959,11 @@ private String getElasticFulltextFieldName(@Nullable String propertyName) { if (planResult.isPathTransformed()) { propertyName = PathUtils.getName(propertyName); } - return propertyName; + if ("*".equals(propertyName)) { + // elasticsearch does support the pseudo-field "*" meaning all fields, + // but (arguably) what we really want is the field ":fulltext". + return FieldNames.FULLTEXT; + } + return ElasticIndexUtils.fieldName(propertyName); } } diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/async/facets/ElasticSecureFacetAsyncProvider.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/async/facets/ElasticSecureFacetAsyncProvider.java index dec588c62da..79107050aa3 100644 --- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/async/facets/ElasticSecureFacetAsyncProvider.java +++ b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/async/facets/ElasticSecureFacetAsyncProvider.java @@ -22,6 +22,7 @@ import org.apache.jackrabbit.oak.plugins.index.elastic.query.ElasticRequestHandler; import org.apache.jackrabbit.oak.plugins.index.elastic.query.ElasticResponseHandler; import org.apache.jackrabbit.oak.plugins.index.elastic.query.async.ElasticResponseListener; +import org.apache.jackrabbit.oak.plugins.index.elastic.util.ElasticIndexUtils; import org.apache.jackrabbit.oak.plugins.index.search.spi.query.FulltextIndex; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -57,7 +58,9 @@ class ElasticSecureFacetAsyncProvider implements ElasticFacetProvider, ElasticRe ) { this.elasticResponseHandler = elasticResponseHandler; this.isAccessible = isAccessible; - this.facetFields = elasticRequestHandler.facetFields().collect(Collectors.toSet()); + this.facetFields = elasticRequestHandler.facetFields(). + map(ElasticIndexUtils::fieldName). + collect(Collectors.toSet()); } @Override @@ -129,6 +132,7 @@ public List getFacets(int numberOfFacets, String columnName throw new IllegalStateException("Error while waiting for facets", e); } LOG.trace("Reading facets for {} from {}", columnName, facets); - return facets != null ? facets.get(FulltextIndex.parseFacetField(columnName)) : null; + String field = ElasticIndexUtils.fieldName(FulltextIndex.parseFacetField(columnName)); + return facets != null ? facets.get(field) : null; } } diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/async/facets/ElasticStatisticalFacetAsyncProvider.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/async/facets/ElasticStatisticalFacetAsyncProvider.java index f8adf3fab29..ae7aa02618f 100644 --- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/async/facets/ElasticStatisticalFacetAsyncProvider.java +++ b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/async/facets/ElasticStatisticalFacetAsyncProvider.java @@ -23,6 +23,7 @@ import org.apache.jackrabbit.oak.plugins.index.elastic.ElasticIndexDefinition; import org.apache.jackrabbit.oak.plugins.index.elastic.query.ElasticRequestHandler; import org.apache.jackrabbit.oak.plugins.index.elastic.query.ElasticResponseHandler; +import org.apache.jackrabbit.oak.plugins.index.elastic.util.ElasticIndexUtils; import org.apache.jackrabbit.oak.plugins.index.search.FieldNames; import org.apache.jackrabbit.oak.plugins.index.search.spi.query.FulltextIndex; import org.slf4j.Logger; @@ -74,7 +75,9 @@ public class ElasticStatisticalFacetAsyncProvider implements ElasticFacetProvide this.elasticResponseHandler = elasticResponseHandler; this.isAccessible = isAccessible; - this.facetFields = elasticRequestHandler.facetFields().collect(Collectors.toSet()); + this.facetFields = elasticRequestHandler.facetFields(). + map(ElasticIndexUtils::fieldName). + collect(Collectors.toSet()); SearchRequest searchRequest = SearchRequest.of(srb -> srb.index(indexDefinition.getIndexAlias()) .trackTotalHits(thb -> thb.enabled(true)) @@ -128,7 +131,8 @@ public List getFacets(int numberOfFacets, String columnName throw new IllegalStateException("Error while waiting for facets", e); } LOG.trace("Reading facets for {} from {}", columnName, facets); - return facets != null ? facets.get(FulltextIndex.parseFacetField(columnName)) : null; + String field = ElasticIndexUtils.fieldName(FulltextIndex.parseFacetField(columnName)); + return facets != null ? facets.get(field) : null; } private void processHit(Hit searchHit) { diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/util/ElasticIndexUtils.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/util/ElasticIndexUtils.java index 5332b1c2450..b34a908f5ea 100644 --- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/util/ElasticIndexUtils.java +++ b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/util/ElasticIndexUtils.java @@ -31,6 +31,69 @@ public class ElasticIndexUtils { private static final Logger LOG = LoggerFactory.getLogger(ElasticIndexUtils.class); + /** + * Convert a JCR property name to a Elasticsearch field name. + * Notice that "|" is not allowed in JCR names. + * + * @param propertyName the property name + * @return the field name + */ + public static String fieldName(String propertyName) { + if(propertyName.startsWith(":")) { + // there are some hardcoded field names + return propertyName; + } + String fieldName = propertyName; + boolean blank = fieldName.isBlank(); + boolean escape = false; + if (blank) { + // empty field name or field names that only consist of spaces + escape = true; + } else { + // 99.99% property names are OK, + // so we loop over the characters first + for (int i = 0; i < fieldName.length() && !escape ; i++) { + switch (fieldName.charAt(i)) { + case '|': + case '.': + case '^': + case '_': + escape = true; + } + } + } + if (escape) { + StringBuilder buff = new StringBuilder(fieldName.length()); + if (fieldName.startsWith("_") || blank) { + // internal field start with a _ + // we also support empty or just spaces + buff.append('|'); + } + for (int i = 0; i < fieldName.length(); i++) { + char c = fieldName.charAt(i); + // For performance, the logic for the currently supported + // characters is hardcoded. + // In case more characters need to be escaped, + // buff.append('|').append(Integer.toHexString(c)).append('|'); + switch (c) { + case '|': + buff.append("||"); + break; + case '.': + buff.append("|2e|"); + break; + case '^': + buff.append("|5e|"); + break; + default: + buff.append(c); + } + } + fieldName = buff.toString(); + } + return fieldName; + } + /** * Transforms a path into an _id compatible with Elasticsearch specification. The path cannot be larger than 512 * bytes. For performance reasons paths that are already compatible are returned untouched. Otherwise, SHA-256 @@ -58,7 +121,7 @@ public static String idFromPath(@NotNull String path) { * @return list of floats */ public static List toFloats(byte[] array) { - int blockSize = Float.SIZE / Byte.SIZE; + int blockSize = Float.BYTES; ByteBuffer wrap = ByteBuffer.wrap(array); if (array.length % blockSize != 0) { LOG.warn("Unexpected byte array length {}", array.length); @@ -78,10 +141,9 @@ public static List toFloats(byte[] array) { * @return byte array */ public static byte[] toByteArray(List values) { - int blockSize = Float.SIZE / Byte.SIZE; - byte[] bytes = new byte[values.size() * blockSize]; + byte[] bytes = new byte[values.size() * Float.BYTES]; ByteBuffer wrap = ByteBuffer.wrap(bytes); - for (int i = 0, j = 0; i < values.size(); i++, j += blockSize) { + for (int i = 0; i < values.size(); i++) { wrap.putFloat(values.get(i)); } return bytes; diff --git a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticConnectionRule.java b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticConnectionRule.java index 499404aded1..690e3efdf80 100644 --- a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticConnectionRule.java +++ b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticConnectionRule.java @@ -61,7 +61,7 @@ public ElasticConnectionRule() { public ElasticConnectionRule(String elasticConnectionString) { this(elasticConnectionString, "elastic_test_" + - RandomStringUtils.random(5, true, false).toLowerCase() + + RandomStringUtils.insecure().next(5, true, false).toLowerCase() + System.currentTimeMillis() ); } diff --git a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticContentTest.java b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticContentTest.java index aaa15690938..775339aa249 100644 --- a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticContentTest.java +++ b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticContentTest.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import org.apache.jackrabbit.oak.api.Tree; import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.plugins.index.elastic.util.ElasticIndexUtils; import org.apache.jackrabbit.oak.plugins.index.search.util.IndexDefinitionBuilder; import org.apache.jackrabbit.oak.stats.StatisticsProvider; import org.junit.Ignore; @@ -285,12 +286,12 @@ public void deduplicateFields() throws Exception { assertEventually(() -> { ObjectNode indexed1 = getDocument(index, "/content/indexed1"); - assertThat(indexed1.get("a").asText(), equalTo("foo")); + assertThat(indexed1.get(ElasticIndexUtils.fieldName("a")).asText(), equalTo("foo")); ObjectNode indexed2 = getDocument(index, "/content/indexed2"); - assertThat(indexed2.get("a").size(), equalTo(2)); - assertThat(indexed2.get("a").get(0).asText(), equalTo("foo")); - assertThat(indexed2.get("a").get(1).asText(), equalTo("bar")); + assertThat(indexed2.get(ElasticIndexUtils.fieldName("a")).size(), equalTo(2)); + assertThat(indexed2.get(ElasticIndexUtils.fieldName("a")).get(0).asText(), equalTo("foo")); + assertThat(indexed2.get(ElasticIndexUtils.fieldName("a")).get(1).asText(), equalTo("bar")); }); } diff --git a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexAggregationTest.java b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexAggregationTest.java index 174c62c0178..204c14f488e 100644 --- a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexAggregationTest.java +++ b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexAggregationTest.java @@ -16,12 +16,26 @@ */ package org.apache.jackrabbit.oak.plugins.index.elastic; +import static org.apache.jackrabbit.JcrConstants.JCR_CONTENT; +import static org.apache.jackrabbit.JcrConstants.JCR_DATA; +import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; +import static org.apache.jackrabbit.JcrConstants.NT_FILE; +import static org.apache.jackrabbit.JcrConstants.NT_FOLDER; +import static org.apache.jackrabbit.oak.plugins.memory.BinaryPropertyState.binaryProperty; + +import java.util.Calendar; +import java.util.List; + import org.apache.jackrabbit.oak.api.CommitFailedException; import org.apache.jackrabbit.oak.api.ContentRepository; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.api.Type; import org.apache.jackrabbit.oak.plugins.index.IndexAggregationCommonTest; import org.junit.ClassRule; import org.junit.Ignore; import org.junit.Test; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; public class ElasticIndexAggregationTest extends IndexAggregationCommonTest { @@ -44,4 +58,72 @@ protected ContentRepository createRepository() { public void oak3371AggregateV1() throws CommitFailedException { super.oak3371AggregateV1(); } + + @Test + public void testChildNodeWithOrCompositePlan() throws Exception { + Tree content = root.getTree("/").addChild("content"); + Tree folder = content.addChild("myFolder"); + folder.setProperty(JCR_PRIMARYTYPE, NT_FOLDER, Type.NAME); + Tree file = folder.addChild("myFile"); + file.setProperty(JCR_PRIMARYTYPE, NT_FILE, Type.NAME); + file.setProperty("jcr:title", "title"); + file.setProperty("jcr:description", "description"); + + Tree resource = file.addChild(JCR_CONTENT); + resource.setProperty(JCR_PRIMARYTYPE, "nt:resource", Type.NAME); + resource.setProperty("jcr:lastModified", Calendar.getInstance()); + resource.setProperty("jcr:encoding", "UTF-8"); + resource.setProperty("jcr:mimeType", "text/plain"); + resource.setProperty(binaryProperty(JCR_DATA, + "the quick brown fox jumps over the lazy dog.")); + + root.commit(); + + assertEventually(() -> { + String matchContentAll = "//element(*, nt:folder)[(jcr:contains(., 'dog'))]"; + assertThat(explainXPath(matchContentAll), containsString( + "\"fields\":[\":fulltext\"],\"query\":\"dog\"")); + assertQuery(matchContentAll, "xpath", List.of("/content/myFolder")); + + String matchContentSimple = "//element(*, nt:folder)[(jcr:contains(myFile, 'dog'))]"; + assertThat(explainXPath(matchContentSimple), containsString( + "\"fields\":[\":fulltext\"],\"query\":\"dog\"")); + assertQuery(matchContentSimple, "xpath", List.of("/content/myFolder")); + + String matchContent = " //element(*, nt:folder)[(jcr:contains(myFile, 'dog') or jcr:contains(myFile/@jcr:title, 'invalid') or jcr:contains(myFile/@jcr:description, 'invalid'))]"; + assertThat(explainXPath(matchContent), containsString( + "\"fields\":[\":fulltext\"],\"query\":\"dog\"")); + assertQuery(matchContent, "xpath", List.of("/content/myFolder")); + + String matchTitle = " //element(*, nt:folder)[(jcr:contains(myFile, 'invalid') or jcr:contains(myFile/@jcr:title, 'title') or jcr:contains(myFile/@jcr:description, 'invalid'))]"; + assertThat(explainXPath(matchTitle), containsString( + "\"fields\":[\":fulltext\"],\"query\":\"invalid\"")); + assertQuery(matchTitle, "xpath", List.of("/content/myFolder")); + + String matchDesc = " //element(*, nt:folder)[(jcr:contains(myFile, 'invalid') or jcr:contains(myFile/@jcr:title, 'invalid') or jcr:contains(myFile/@jcr:description, 'description'))]"; + assertThat(explainXPath(matchDesc), containsString( + "\"fields\":[\":fulltext\"],\"query\":\"invalid\"")); + assertQuery(matchDesc, "xpath", List.of("/content/myFolder")); + + String matchNone = " //element(*, nt:folder)[(jcr:contains(myFile, 'invalid') or jcr:contains(myFile/@jcr:title, 'invalid') or jcr:contains(myFile/@jcr:description, 'invalid'))]"; + assertThat(explainXPath(matchNone), containsString( + "\"fields\":[\":fulltext\"],\"query\":\"invalid\"")); + assertQuery(matchNone, "xpath", List.of()); + + String matchOnlyTitleOr = " //element(*, nt:folder)[(jcr:contains(myFile/@jcr:title, 'title') or jcr:contains(myFile/@jcr:title, 'unknown') )]"; + assertThat(explainXPath(matchOnlyTitleOr), containsString( + "\"fields\":[\"jcr:title\"],\"query\":\"title\"")); + assertQuery(matchOnlyTitleOr, "xpath", List.of("/content/myFolder")); + }); + } + + protected String explainXPath(String query) { + return explain(query, XPATH); + } + + protected String explain(String query, String language) { + String explain = "explain " + query; + return executeQuery(explain, language).get(0); + } + } diff --git a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexSuggestionCommonTest.java b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexSuggestionCommonTest.java index 32e97323eba..040c2c0beb0 100644 --- a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexSuggestionCommonTest.java +++ b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexSuggestionCommonTest.java @@ -63,7 +63,7 @@ public void explain() throws Exception { String expected = "{\"_source\":{\"includes\":[\":path\"]},\"query\":{\"bool\":{\"must\":[{\"nested\":{\"inner_hits\":" + "{\"size\":100},\"path\":\":suggest\",\"query\":{\"match_bool_prefix\":{\":suggest.value\":{\"operator\":\"and\",\"query\":\"boo\"}}},\"score_mode\":\"max\"}}]}},\"size\":100}"; - Query q = qm.createQuery(sql, Query.SQL); + Query q = qm.createQuery(sql, Query.JCR_SQL2); Row row = q.execute().getRows().nextRow(); MatcherAssert.assertThat(row.getValue("plan").getString(), CoreMatchers.containsString(expected)); } diff --git a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticPropertyIndexTest.java b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticPropertyIndexTest.java index 0e930f5c473..caa597ad98d 100644 --- a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticPropertyIndexTest.java +++ b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticPropertyIndexTest.java @@ -22,7 +22,10 @@ import org.apache.jackrabbit.oak.api.Tree; import org.apache.jackrabbit.oak.commons.junit.LogCustomizer; import org.apache.jackrabbit.oak.plugins.index.elastic.util.ElasticIndexDefinitionBuilder; +import org.apache.jackrabbit.oak.plugins.index.elastic.util.ElasticIndexUtils; +import org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants; import org.apache.jackrabbit.oak.plugins.index.search.util.IndexDefinitionBuilder; +import org.apache.jackrabbit.oak.plugins.index.search.util.IndexDefinitionBuilder.PropertyRule; import org.junit.Assert; import org.junit.Test; @@ -177,16 +180,18 @@ public void nodeNameViaPropDefinition() throws Exception { // OAK-11530 @Test - public void propertyWithDot() throws Exception { + public void propertyWithDotPrefix() throws Exception { IndexDefinitionBuilder builder = createIndex(); builder.includedPaths("/test") .indexRule("nt:base") - .property("test", "./test"); + .property("foo", "foo").propertyIndex() + .property("test", "./test").propertyIndex(); setIndex("test1", builder); root.commit(); //add content - root.getTree("/").addChild("test").setProperty("test", "1"); + root.getTree("/").addChild("test") + .setProperty("test", "1"); root.commit(); String query = "select [jcr:path] from [nt:base] " + @@ -196,6 +201,59 @@ public void propertyWithDot() throws Exception { String explanation = explain(query); assertThat(explanation, containsString("no-index")); }); + + String queryFoo = "select [jcr:path] from [nt:base] " + + "where foo = '1'"; + assertEventually(() -> { + String explanation = explain(queryFoo); + assertThat(explanation, containsString("/oak:index/test1")); + assertThat(explanation, containsString("{\"term\":{\"foo\":{\"value\":\"1\"")); + assertQuery(query, List.of()); + }); + } + + @Test + public void propertyWithDot() throws Exception { + IndexDefinitionBuilder builder = createIndex(); + builder.includedPaths("/test") + .indexRule("nt:base") + .property("firstName", "first.name").propertyIndex() + .property("lowerFirstName", "first.name"); + PropertyRule lowerFirstName = builder.indexRule("nt:base").property("lowerFirstName"); + lowerFirstName.getBuilderTree().setProperty( + FulltextIndexConstants.PROP_FUNCTION, "lower([first.name])"); + setIndex("test1", builder); + root.commit(); + + //add content + root.getTree("/").addChild("test").setProperty("first.name", "Antonio"); + root.commit(); + + String query = "select [jcr:path] from [nt:base] " + + "where [first.name] = 'Antonio'"; + + assertEventually(() -> { + String explanation = explain(query); + assertThat(explanation, containsString("/oak:index/test1")); + assertThat(explanation, containsString( + "{\"term\":{\"" + + ElasticIndexUtils.fieldName("first.name") + + "\":{\"value\":\"Antonio\"")); + assertQuery(query, List.of("/test")); + }); + + String lowerQuery = "select [jcr:path] from [nt:base] " + + "where lower([first.name]) = 'antonio'"; + + assertEventually(() -> { + String explanation = explain(lowerQuery); + assertThat(explanation, containsString("/oak:index/test1")); + assertThat(explanation, containsString( + "{\"term\":{\"" + + ElasticIndexUtils.fieldName("function*lower*@first.name") + + "\":{\"value\":\"antonio\"")); + assertQuery(lowerQuery, List.of("/test")); + }); } @Test diff --git a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticRegexPropertyIndexTest.java b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticRegexPropertyIndexTest.java index 1804a97735a..6f4b33ee062 100644 --- a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticRegexPropertyIndexTest.java +++ b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticRegexPropertyIndexTest.java @@ -25,6 +25,7 @@ import org.apache.jackrabbit.oak.api.CommitFailedException; import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.plugins.index.elastic.util.ElasticIndexUtils; import org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants; import org.apache.jackrabbit.oak.plugins.index.search.util.IndexDefinitionBuilder; import org.apache.jackrabbit.oak.plugins.index.search.util.IndexDefinitionBuilder.PropertyRule; @@ -65,7 +66,10 @@ public void regexPropertyWithFlattened() throws Exception { assertEventually(() -> { String explain = explain(propaQuery); assertThat(explain, containsString("elasticsearch:test1")); - assertThat(explain, containsString("[{\"term\":{\"flat:allProperties.propa\":{\"value\":\"foo\"}}}]")); + assertThat(explain, containsString("[{\"term\":{\"flat:" + + ElasticIndexUtils.fieldName("allProperties") + "." + + ElasticIndexUtils.fieldName("propa") + + "\":{\"value\":\"foo\"}}}]")); assertQuery(propaQuery, List.of("/test/a", "/test/b")); }); @@ -74,8 +78,14 @@ public void regexPropertyWithFlattened() throws Exception { assertEventually(() -> { String explain = explain(propaOrderQuery); assertThat(explain, containsString("elasticsearch:test1")); - assertThat(explain, containsString("\"query\":{\"bool\":{\"filter\":[{\"prefix\":{\"flat:allProperties.propd\":{\"value\":\"foo\"}}}]}}")); - assertThat(explain, containsString("\"sort\":[{\"flat:allProperties.propd\":{\"order\":\"asc\"}},{\":path\":{\"order\":\"asc\"}}]")); + assertThat(explain, containsString("\"query\":{\"bool\":{\"filter\":[{\"prefix\":{\"flat:" + + ElasticIndexUtils.fieldName("allProperties") + "." + + ElasticIndexUtils.fieldName("propd") + + "\":{\"value\":\"foo\"}}}]}}")); + assertThat(explain, containsString("\"sort\":[{\"flat:" + + ElasticIndexUtils.fieldName("allProperties") + "." + + ElasticIndexUtils.fieldName("propd") + + "\":{\"order\":\"asc\"}},{\":path\":{\"order\":\"asc\"}}]")); assertThat(explain, containsString("sortOrder: [{ propertyName : propd, propertyType : UNDEFINED, order : ASCENDING }]")); assertQuery(propaOrderQuery, List.of("/test/f", "/test/e")); }); diff --git a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticSimilarQueryTest.java b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticSimilarQueryTest.java index 6195152a2eb..575b55d2f06 100644 --- a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticSimilarQueryTest.java +++ b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticSimilarQueryTest.java @@ -25,6 +25,7 @@ import org.apache.jackrabbit.oak.api.Blob; import org.apache.jackrabbit.oak.api.Tree; import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.plugins.index.elastic.util.ElasticIndexUtils; import org.apache.jackrabbit.oak.plugins.index.search.FieldNames; import org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants; import org.apache.jackrabbit.oak.plugins.index.search.util.IndexDefinitionBuilder; @@ -174,10 +175,11 @@ public void similarityTagsAffectRelevance() throws Exception { @Test public void vectorSimilarityIndexConfiguration() throws Exception { final String indexName = "test1"; - final String fieldName1 = "fv1"; + String propertyName = "fv1"; + final String fieldName1 = ElasticIndexUtils.fieldName(propertyName); final String similarityFieldName1 = FieldNames.createSimilarityFieldName(fieldName1); - IndexDefinitionBuilder builder = createIndex(fieldName1); - Tree tree = builder.indexRule("nt:base").property(fieldName1).useInSimilarity(true).nodeScopeIndex() + IndexDefinitionBuilder builder = createIndex(propertyName); + Tree tree = builder.indexRule("nt:base").property(propertyName).useInSimilarity(true).nodeScopeIndex() .similaritySearchDenseVectorSize(2048).getBuilderTree(); tree.setProperty(ElasticPropertyDefinition.PROP_SIMILARITY_METRIC, "cosine"); diff --git a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticDocumentMakerLargeStringPropertiesLogTest.java b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticDocumentMakerLargeStringPropertiesLogTest.java index a5890e8d174..f77619f7bb7 100644 --- a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticDocumentMakerLargeStringPropertiesLogTest.java +++ b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticDocumentMakerLargeStringPropertiesLogTest.java @@ -73,7 +73,7 @@ private void setThresholdLimit(String threshold) { System.setProperty(FulltextDocumentMaker.WARN_LOG_STRING_SIZE_THRESHOLD_KEY, threshold); } - private ElasticDocumentMaker addPropertyAccordingToType(NodeBuilder test, Type type, String... str) { + private ElasticDocumentMaker addPropertyAccordingToType(NodeBuilder test, Type type, String... str) { NodeState root = INITIAL_CONTENT; ElasticIndexDefinitionBuilder builder = new ElasticIndexDefinitionBuilder(); builder.indexRule("nt:base") diff --git a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexHelperTest.java b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexHelperTest.java index 4873aa6e9e1..9b7372967ea 100644 --- a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexHelperTest.java +++ b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexHelperTest.java @@ -28,6 +28,7 @@ import org.apache.jackrabbit.oak.api.Tree; import org.apache.jackrabbit.oak.plugins.index.elastic.ElasticIndexDefinition; import org.apache.jackrabbit.oak.plugins.index.elastic.util.ElasticIndexDefinitionBuilder; +import org.apache.jackrabbit.oak.plugins.index.elastic.util.ElasticIndexUtils; import org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants; import org.apache.jackrabbit.oak.plugins.index.search.util.IndexDefinitionBuilder; import org.apache.jackrabbit.oak.spi.state.NodeState; @@ -71,7 +72,7 @@ public void multiRulesWithSamePropertyNames() { TypeMapping fooPropertyMappings = request.mappings(); assertThat(fooPropertyMappings, notNullValue()); - Property fooProperty = fooPropertyMappings.properties().get("foo"); + Property fooProperty = fooPropertyMappings.properties().get(ElasticIndexUtils.fieldName("foo")); assertThat(fooProperty, is(notNullValue())); assertThat(fooProperty._kind(), is(Property.Kind.Text)); TextProperty fooTextProperty = fooProperty.text(); @@ -151,7 +152,7 @@ public void oakAnalyzer() { TypeMapping fooMappings = request.mappings(); assertThat(fooMappings, notNullValue()); - Property fooProperty = fooMappings.properties().get("foo"); + Property fooProperty = fooMappings.properties().get(ElasticIndexUtils.fieldName("foo")); assertThat(fooProperty, is(notNullValue())); TextProperty textProperty = fooProperty.text(); assertThat(textProperty.analyzer(), is("oak_analyzer")); @@ -160,7 +161,7 @@ public void oakAnalyzer() { TypeMapping barMappings = request.mappings(); assertThat(barMappings, notNullValue()); - Property barProperty = barMappings.properties().get("bar"); + Property barProperty = barMappings.properties().get(ElasticIndexUtils.fieldName("bar")); assertThat(barProperty._kind(), is(Property.Kind.Keyword)); } diff --git a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/util/ElasticIndexUtilsTest.java b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/util/ElasticIndexUtilsTest.java new file mode 100644 index 00000000000..afcb4886fc3 --- /dev/null +++ b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/util/ElasticIndexUtilsTest.java @@ -0,0 +1,139 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.index.elastic.util; + +import static org.junit.Assert.assertEquals; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Random; + +import org.junit.Test; + +public class ElasticIndexUtilsTest { + + @Test + public void fieldName() { + assertEquals("regular", ElasticIndexUtils.fieldName("regular")); + assertEquals(":nodeName", ElasticIndexUtils.fieldName(":nodeName")); + assertEquals("first|2e|name", ElasticIndexUtils.fieldName("first.name")); + assertEquals("weird|5e|", ElasticIndexUtils.fieldName("weird^")); + assertEquals("embedded_is_fine", ElasticIndexUtils.fieldName("embedded_is_fine")); + assertEquals("|_id", ElasticIndexUtils.fieldName("_id")); + assertEquals("|", ElasticIndexUtils.fieldName("")); + assertEquals("| ", ElasticIndexUtils.fieldName(" ")); + assertEquals("||", ElasticIndexUtils.fieldName("|")); + assertEquals("||test||", ElasticIndexUtils.fieldName("|test|")); + } + + @Test + public void randomFieldNames() { + propertyNameFromFieldName(""); + Random r = new Random(1); + for (int i = 0; i < 1000; i++) { + StringBuilder buff = new StringBuilder(); + int len = 1 + r.nextInt(5); + String chars = "|^._ 25ex"; + for (int j = 0; j < len; j++) { + buff.append(chars.charAt(r.nextInt(chars.length()))); + } + String p = buff.toString(); + String f = ElasticIndexUtils.fieldName(p); + String p2 = propertyNameFromFieldName(f); + if (!p.equals(p2)) { + p2 = propertyNameFromFieldName(f); + assertEquals(p, p2); + } + // just to make sure there are no exceptions (within some limits) + propertyNameFromFieldName(p); + } + } + + @Test + public void idFromPath() { + assertEquals("/content", ElasticIndexUtils.idFromPath("/content")); + assertEquals("%40%0Bz%DF%B4%22%29%EF%BF%BD%EF%BF%BD%3Cfh%EF%BF%BD%27%EF%BF%BD%7E%EF%BF%BDM%EF%BF%BD%EF%BF%BD%EF%BF%BD%22I%EF%BF%BD%7C%EF%BF%BDGn%0A+%25", + URLEncoder.encode(ElasticIndexUtils.idFromPath("/content".repeat(100)),StandardCharsets.UTF_8)); + } + + @Test + public void toByteArray() { + assertEquals("[1.0, 0.1]", + ElasticIndexUtils.toFloats( + ElasticIndexUtils.toByteArray(List.of(1.0f, 0.1f))).toString()); + assertEquals("[-0.0, 0.0]", + ElasticIndexUtils.toFloats( + ElasticIndexUtils.toByteArray(List.of(-0.0f, 0.0f))).toString()); + assertEquals("[Infinity, -Infinity]", + ElasticIndexUtils.toFloats( + ElasticIndexUtils.toByteArray(List.of(Float.POSITIVE_INFINITY, Float.NEGATIVE_INFINITY))).toString()); + assertEquals("[NaN, 3.4028235E38]", + ElasticIndexUtils.toFloats( + ElasticIndexUtils.toByteArray(List.of(Float.NaN, Float.MAX_VALUE))).toString()); + } + + /** + * Convert an elasticsearch field name to a JCR property name. + * Please note this method is not optimized for performance. + * + * @param fieldName the field name + * @return the property name + */ + public static String propertyNameFromFieldName(String fieldName) { + if (fieldName.indexOf('|') < 0) { + return fieldName; + } + if (fieldName.startsWith("|")) { + if (fieldName.equals("|")) { + return ""; + } if (fieldName.startsWith("|_") || fieldName.substring(1).isBlank()) { + fieldName = fieldName.substring(1); + } + } + StringBuilder buff = new StringBuilder(fieldName.length()); + for (int i = 0; i < fieldName.length(); i++) { + char c = fieldName.charAt(i); + switch (c) { + case '|': + String next = fieldName.substring(i + 1); + if (next.startsWith("|")) { + buff.append('|'); + i++; + } else { + int end = next.indexOf('|'); + if (end < 0) { + buff.append(next); + break; + } + String code = next.substring(0, end); + try { + buff.append((char) Integer.parseInt(code, 16)); + } catch (NumberFormatException e) { + buff.append(code); + } + i += code.length() + 1; + } + break; + default: + buff.append(c); + } + } + return buff.toString(); + } + +} diff --git a/oak-search/src/test/java/org/apache/jackrabbit/oak/plugins/index/IndexPlannerCommonTest.java b/oak-search/src/test/java/org/apache/jackrabbit/oak/plugins/index/IndexPlannerCommonTest.java index e8a557e923c..881610482fc 100644 --- a/oak-search/src/test/java/org/apache/jackrabbit/oak/plugins/index/IndexPlannerCommonTest.java +++ b/oak-search/src/test/java/org/apache/jackrabbit/oak/plugins/index/IndexPlannerCommonTest.java @@ -1508,7 +1508,7 @@ private static NodeBuilder getNode(@NotNull NodeBuilder node, @NotNull String pa } private static String generateRandomIndexName(String prefix) { - return prefix + RandomStringUtils.random(5, true, false); + return prefix + RandomStringUtils.insecure().next(5, true, false); } /**