diff --git a/src/main/java/org/opensearch/plugin/insights/core/listener/QueryInsightsListener.java b/src/main/java/org/opensearch/plugin/insights/core/listener/QueryInsightsListener.java index 0cd8412..a2201eb 100644 --- a/src/main/java/org/opensearch/plugin/insights/core/listener/QueryInsightsListener.java +++ b/src/main/java/org/opensearch/plugin/insights/core/listener/QueryInsightsListener.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeUnit; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -239,7 +240,16 @@ private void constructSearchQueryRecord(final SearchPhaseContext context, final ); } - String hashcode = QueryShapeGenerator.getShapeHashCodeAsString(request.source(), false); + Set successfulShardIndices = searchRequestContext.getSuccessfulSearchShardIndices(); + if (successfulShardIndices == null) { + successfulShardIndices = Collections.emptySet(); + } + String hashcode = QueryShapeGenerator.getShapeHashCodeAsString( + request.source(), + false, + clusterService.state().getMetadata(), + successfulShardIndices + ); Map attributes = new HashMap<>(); attributes.put(Attribute.SEARCH_TYPE, request.searchType().toString().toLowerCase(Locale.ROOT)); diff --git a/src/main/java/org/opensearch/plugin/insights/core/service/categorizer/QueryShapeGenerator.java b/src/main/java/org/opensearch/plugin/insights/core/service/categorizer/QueryShapeGenerator.java index 4aaeff8..0a7d1d1 100644 --- a/src/main/java/org/opensearch/plugin/insights/core/service/categorizer/QueryShapeGenerator.java +++ b/src/main/java/org/opensearch/plugin/insights/core/service/categorizer/QueryShapeGenerator.java @@ -12,7 +12,12 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.Set; import org.apache.lucene.util.BytesRef; +import org.opensearch.cluster.metadata.IndexMetadata; +import org.opensearch.cluster.metadata.MappingMetadata; +import org.opensearch.cluster.metadata.Metadata; import org.opensearch.common.hash.MurmurHash3; import org.opensearch.core.common.io.stream.NamedWriteable; import org.opensearch.index.query.QueryBuilder; @@ -36,14 +41,24 @@ public class QueryShapeGenerator { * @param showFields whether to include field data in query shape * @return Hash code of query shape as a MurmurHash3.Hash128 object (128-bit) */ - public static MurmurHash3.Hash128 getShapeHashCode(SearchSourceBuilder source, Boolean showFields) { - final String shape = buildShape(source, showFields); + public static MurmurHash3.Hash128 getShapeHashCode( + SearchSourceBuilder source, + Boolean showFields, + Metadata metadata, + Set searchIndices + ) { + final String shape = buildShape(source, showFields, metadata, searchIndices); final BytesRef shapeBytes = new BytesRef(shape); return MurmurHash3.hash128(shapeBytes.bytes, 0, shapeBytes.length, 0, new MurmurHash3.Hash128()); } - public static String getShapeHashCodeAsString(SearchSourceBuilder source, Boolean showFields) { - MurmurHash3.Hash128 hashcode = getShapeHashCode(source, showFields); + public static String getShapeHashCodeAsString( + SearchSourceBuilder source, + Boolean showFields, + Metadata metadata, + Set searchIndices + ) { + MurmurHash3.Hash128 hashcode = getShapeHashCode(source, showFields, metadata, searchIndices); String hashAsString = Long.toHexString(hashcode.h1) + Long.toHexString(hashcode.h2); return hashAsString; } @@ -54,11 +69,11 @@ public static String getShapeHashCodeAsString(SearchSourceBuilder source, Boolea * @param showFields whether to append field data * @return Search query shape as String */ - public static String buildShape(SearchSourceBuilder source, Boolean showFields) { + public static String buildShape(SearchSourceBuilder source, Boolean showFields, Metadata metadata, Set searchIndices) { StringBuilder shape = new StringBuilder(); - shape.append(buildQueryShape(source.query(), showFields)); - shape.append(buildAggregationShape(source.aggregations(), showFields)); - shape.append(buildSortShape(source.sorts(), showFields)); + shape.append(buildQueryShape(source.query(), showFields, metadata, searchIndices)); + shape.append(buildAggregationShape(source.aggregations(), showFields, metadata, searchIndices)); + shape.append(buildSortShape(source.sorts(), showFields, metadata, searchIndices)); return shape.toString(); } @@ -68,11 +83,11 @@ public static String buildShape(SearchSourceBuilder source, Boolean showFields) * @param showFields whether to append field data * @return Query-section shape as String */ - static String buildQueryShape(QueryBuilder queryBuilder, Boolean showFields) { + static String buildQueryShape(QueryBuilder queryBuilder, Boolean showFields, Metadata metadata, Set searchIndices) { if (queryBuilder == null) { return EMPTY_STRING; } - QueryShapeVisitor shapeVisitor = new QueryShapeVisitor(); + QueryShapeVisitor shapeVisitor = new QueryShapeVisitor(metadata, searchIndices); queryBuilder.visit(shapeVisitor); return shapeVisitor.prettyPrintTree(EMPTY_STRING, showFields); } @@ -83,7 +98,12 @@ static String buildQueryShape(QueryBuilder queryBuilder, Boolean showFields) { * @param showFields whether to append field data * @return Aggregation shape as String */ - static String buildAggregationShape(AggregatorFactories.Builder aggregationsBuilder, Boolean showFields) { + static String buildAggregationShape( + AggregatorFactories.Builder aggregationsBuilder, + Boolean showFields, + Metadata metadata, + Set searchIndices + ) { if (aggregationsBuilder == null) { return EMPTY_STRING; } @@ -92,7 +112,9 @@ static String buildAggregationShape(AggregatorFactories.Builder aggregationsBuil aggregationsBuilder.getPipelineAggregatorFactories(), new StringBuilder(), new StringBuilder(), - showFields + showFields, + metadata, + searchIndices ); return aggregationShape.toString(); } @@ -102,7 +124,9 @@ static StringBuilder recursiveAggregationShapeBuilder( Collection pipelineAggregations, StringBuilder outputBuilder, StringBuilder baseIndent, - Boolean showFields + Boolean showFields, + Metadata metadata, + Set searchIndices ) { //// Normal Aggregations //// if (aggregationBuilders.isEmpty() == false) { @@ -113,7 +137,7 @@ static StringBuilder recursiveAggregationShapeBuilder( StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(baseIndent).append(ONE_SPACE_INDENT.repeat(2)).append(aggBuilder.getType()); if (showFields) { - stringBuilder.append(buildFieldDataString(aggBuilder)); + stringBuilder.append(buildFieldDataString(aggBuilder, metadata, searchIndices)); } stringBuilder.append("\n"); @@ -124,7 +148,9 @@ static StringBuilder recursiveAggregationShapeBuilder( aggBuilder.getPipelineAggregations(), stringBuilder, baseIndent.append(ONE_SPACE_INDENT.repeat(4)), - showFields + showFields, + metadata, + searchIndices ); baseIndent.delete(0, 4); } @@ -167,7 +193,7 @@ static StringBuilder recursiveAggregationShapeBuilder( * @param showFields whether to append field data * @return Sort shape as String */ - static String buildSortShape(List> sortBuilderList, Boolean showFields) { + static String buildSortShape(List> sortBuilderList, Boolean showFields, Metadata metadata, Set searchIndices) { if (sortBuilderList == null || sortBuilderList.isEmpty()) { return EMPTY_STRING; } @@ -179,7 +205,7 @@ static String buildSortShape(List> sortBuilderList, Boolean showF StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(ONE_SPACE_INDENT.repeat(2)).append(sortBuilder.order()); if (showFields) { - stringBuilder.append(buildFieldDataString(sortBuilder)); + stringBuilder.append(buildFieldDataString(sortBuilder, metadata, searchIndices)); } shapeStrings.add(stringBuilder.toString()); } @@ -195,11 +221,57 @@ static String buildSortShape(List> sortBuilderList, Boolean showF * @return String: comma separated list with leading space in square brackets * Ex: " [my_field, width:5]" */ - static String buildFieldDataString(NamedWriteable builder) { + static String buildFieldDataString(NamedWriteable builder, Metadata metadata, Set searchIndices) { List fieldDataList = new ArrayList<>(); if (builder instanceof WithFieldName) { - fieldDataList.add(((WithFieldName) builder).fieldName()); + String fieldName = ((WithFieldName) builder).fieldName(); + fieldDataList.add(fieldName); + fieldDataList.add(getFieldType(fieldName, metadata, searchIndices)); } return " [" + String.join(", ", fieldDataList) + "]"; } + + /** + * Method to get a field's type + * @return String field type + */ + static String getFieldType(String fieldName, Metadata metadata, Set searchIndices) { + if (metadata == null) { + return "none"; + } + for (String searchIndex : searchIndices) { + IndexMetadata indexMetadata = metadata.index(searchIndex); + if (indexMetadata == null) { + continue; + } + MappingMetadata mappingMetadata = indexMetadata.mapping(); + if (mappingMetadata == null) { + continue; + } + Map sourceMap = mappingMetadata.getSourceAsMap(); + + String fieldType = findFieldType((Map) sourceMap.get("properties"), fieldName); + if (fieldType != null) { + return fieldType; + } + } + return "none"; + } + + public static String findFieldType(Map properties, String fieldName) { + for (Map.Entry entry : properties.entrySet()) { + Map field = (Map) entry.getValue(); + if (entry.getKey().equals(fieldName)) { + return (String) field.get("type"); + } + // TODO: Add support for multi-fields + // if (field.containsKey("fields")) { + // String type = findFieldType((Map) field.get("fields"), fieldName); + // if (type != null) { + // return type; + // } + // } + } + return null; + } } diff --git a/src/main/java/org/opensearch/plugin/insights/core/service/categorizer/QueryShapeVisitor.java b/src/main/java/org/opensearch/plugin/insights/core/service/categorizer/QueryShapeVisitor.java index d4d0b5b..a5aa86a 100644 --- a/src/main/java/org/opensearch/plugin/insights/core/service/categorizer/QueryShapeVisitor.java +++ b/src/main/java/org/opensearch/plugin/insights/core/service/categorizer/QueryShapeVisitor.java @@ -16,7 +16,9 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; import org.apache.lucene.search.BooleanClause; +import org.opensearch.cluster.metadata.Metadata; import org.opensearch.common.SetOnce; import org.opensearch.index.query.QueryBuilder; import org.opensearch.index.query.QueryBuilderVisitor; @@ -28,11 +30,13 @@ public final class QueryShapeVisitor implements QueryBuilderVisitor { private final SetOnce queryType = new SetOnce<>(); private final SetOnce fieldData = new SetOnce<>(); private final Map> childVisitors = new EnumMap<>(BooleanClause.Occur.class); + private final Metadata metadata; + private final Set searchIndices; @Override public void accept(QueryBuilder queryBuilder) { queryType.set(queryBuilder.getName()); - fieldData.set(buildFieldDataString(queryBuilder)); + fieldData.set(buildFieldDataString(queryBuilder, metadata, searchIndices)); } @Override @@ -47,7 +51,7 @@ public QueryBuilderVisitor getChildVisitor(BooleanClause.Occur occur) { @Override public void accept(QueryBuilder qb) { - currentChild = new QueryShapeVisitor(); + currentChild = new QueryShapeVisitor(metadata, searchIndices); childVisitorList.add(currentChild); currentChild.accept(qb); } @@ -110,5 +114,8 @@ public String prettyPrintTree(String indent, Boolean showFields) { /** * Default constructor */ - public QueryShapeVisitor() {} + public QueryShapeVisitor(Metadata metadata, Set searchIndices) { + this.metadata = metadata; + this.searchIndices = searchIndices; + } } diff --git a/src/main/java/org/opensearch/plugin/insights/core/service/categorizer/SearchQueryCategorizer.java b/src/main/java/org/opensearch/plugin/insights/core/service/categorizer/SearchQueryCategorizer.java index df3cb7b..43ceddd 100644 --- a/src/main/java/org/opensearch/plugin/insights/core/service/categorizer/SearchQueryCategorizer.java +++ b/src/main/java/org/opensearch/plugin/insights/core/service/categorizer/SearchQueryCategorizer.java @@ -87,11 +87,6 @@ public void categorize(SearchQueryRecord record) { incrementQueryTypeCounters(source.query(), measurements); incrementQueryAggregationCounters(source.aggregations(), measurements); incrementQuerySortCounters(source.sorts(), measurements); - - if (logger.isTraceEnabled()) { - String searchShape = QueryShapeGenerator.buildShape(source, true); - logger.trace(searchShape); - } } private void incrementQuerySortCounters(List> sorts, Map measurements) { diff --git a/src/test/java/org/opensearch/plugin/insights/core/service/categorizer/QueryShapeGeneratorTests.java b/src/test/java/org/opensearch/plugin/insights/core/service/categorizer/QueryShapeGeneratorTests.java index 0226464..2920516 100644 --- a/src/test/java/org/opensearch/plugin/insights/core/service/categorizer/QueryShapeGeneratorTests.java +++ b/src/test/java/org/opensearch/plugin/insights/core/service/categorizer/QueryShapeGeneratorTests.java @@ -8,17 +8,113 @@ package org.opensearch.plugin.insights.core.service.categorizer; +import static org.opensearch.plugin.insights.core.service.categorizer.QueryShapeGenerator.findFieldType; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import org.opensearch.cluster.metadata.Metadata; import org.opensearch.common.hash.MurmurHash3; import org.opensearch.plugin.insights.SearchSourceBuilderUtils; import org.opensearch.search.builder.SearchSourceBuilder; import org.opensearch.test.OpenSearchTestCase; public final class QueryShapeGeneratorTests extends OpenSearchTestCase { + static Set SEARCH_SHARD_INDICES = Set.of("index1", "index2", "index3"); + static Metadata mockMetadata; + + /** + * Helper function to return flat index map. + * + *
+     * {
+     *   properties = {
+     *     field1 = { type = long },
+     *     field2 = { type = text },
+     *     field3 = { type = boolean }
+     *   }
+     * }
+     * 
+ */ + private static Map getFlatIndexMapping() { + return new HashMap<>() { + { + put("properties", new HashMap<>() { + { + put("field1", new HashMap<>() { + { + put("type", "long"); + } + }); + put("field2", new HashMap<>() { + { + put("type", "text"); + } + }); + put("field3", new HashMap<>() { + { + put("type", "boolean"); + } + }); + } + }); + } + }; + } + + /** + * Helper function to return nested index map. + * + *
+     * {
+     *   properties = {
+     *     field1 = { type = long, fields = {
+     *       field2 = { type = text, fields = {
+     *         field3 = { type = keyword, ignore_above = 256 }
+     *     } } }
+     *   }
+     * }
+     * 
+ */ + private static Map getNestedIndexMapping() { + return new HashMap<>() { + { + put("properties", new HashMap<>() { + { + put("field1", new HashMap<>() { + { + put("type", "long"); + put("fields", new HashMap<>() { + { + put("field2", new HashMap<>() { + { + put("type", "text"); + put("fields", new HashMap<>() { + { + put("field3", new HashMap<>() { + { + put("type", "boolean"); + put("ignore_above", 256); + } + }); + } + }); + } + }); + } + }); + } + }); + } + }); + } + }; + } public void testComplexSearch() { SearchSourceBuilder sourceBuilder = SearchSourceBuilderUtils.createDefaultSearchSourceBuilder(); - String shapeShowFieldsTrue = QueryShapeGenerator.buildShape(sourceBuilder, true); + String shapeShowFieldsTrue = QueryShapeGenerator.buildShape(sourceBuilder, true, mockMetadata, SEARCH_SHARD_INDICES); String expectedShowFieldsTrue = "bool []\n" + " must:\n" + " term [field1]\n" @@ -53,7 +149,7 @@ public void testComplexSearch() { + " asc [album]\n"; assertEquals(expectedShowFieldsTrue, shapeShowFieldsTrue); - String shapeShowFieldsFalse = QueryShapeGenerator.buildShape(sourceBuilder, false); + String shapeShowFieldsFalse = QueryShapeGenerator.buildShape(sourceBuilder, false, mockMetadata, SEARCH_SHARD_INDICES); String expectedShowFieldsFalse = "bool\n" + " must:\n" + " term\n" @@ -92,7 +188,7 @@ public void testComplexSearch() { public void testQueryShape() { SearchSourceBuilder sourceBuilder = SearchSourceBuilderUtils.createQuerySearchSourceBuilder(); - String shapeShowFieldsTrue = QueryShapeGenerator.buildShape(sourceBuilder, true); + String shapeShowFieldsTrue = QueryShapeGenerator.buildShape(sourceBuilder, true, mockMetadata, SEARCH_SHARD_INDICES); String expectedShowFieldsTrue = "bool []\n" + " must:\n" + " term [field1]\n" @@ -103,7 +199,7 @@ public void testQueryShape() { + " regexp [field3]\n"; assertEquals(expectedShowFieldsTrue, shapeShowFieldsTrue); - String shapeShowFieldsFalse = QueryShapeGenerator.buildShape(sourceBuilder, false); + String shapeShowFieldsFalse = QueryShapeGenerator.buildShape(sourceBuilder, false, mockMetadata, SEARCH_SHARD_INDICES); String expectedShowFieldsFalse = "bool\n" + " must:\n" + " term\n" @@ -118,7 +214,7 @@ public void testQueryShape() { public void testAggregationShape() { SearchSourceBuilder sourceBuilder = SearchSourceBuilderUtils.createAggregationSearchSourceBuilder(); - String shapeShowFieldsTrue = QueryShapeGenerator.buildShape(sourceBuilder, true); + String shapeShowFieldsTrue = QueryShapeGenerator.buildShape(sourceBuilder, true, mockMetadata, SEARCH_SHARD_INDICES); String expectedShowFieldsTrue = "aggregation:\n" + " significant_text []\n" + " terms [key]\n" @@ -140,7 +236,7 @@ public void testAggregationShape() { + " max_bucket\n"; assertEquals(expectedShowFieldsTrue, shapeShowFieldsTrue); - String shapeShowFieldsFalse = QueryShapeGenerator.buildShape(sourceBuilder, false); + String shapeShowFieldsFalse = QueryShapeGenerator.buildShape(sourceBuilder, false, mockMetadata, SEARCH_SHARD_INDICES); String expectedShowFieldsFalse = "aggregation:\n" + " significant_text\n" + " terms\n" @@ -166,11 +262,11 @@ public void testAggregationShape() { public void testSortShape() { SearchSourceBuilder sourceBuilder = SearchSourceBuilderUtils.createSortSearchSourceBuilder(); - String shapeShowFieldsTrue = QueryShapeGenerator.buildShape(sourceBuilder, true); + String shapeShowFieldsTrue = QueryShapeGenerator.buildShape(sourceBuilder, true, mockMetadata, SEARCH_SHARD_INDICES); String expectedShowFieldsTrue = "sort:\n" + " desc [color]\n" + " desc [vendor]\n" + " asc [price]\n" + " asc [album]\n"; assertEquals(expectedShowFieldsTrue, shapeShowFieldsTrue); - String shapeShowFieldsFalse = QueryShapeGenerator.buildShape(sourceBuilder, false); + String shapeShowFieldsFalse = QueryShapeGenerator.buildShape(sourceBuilder, false, mockMetadata, SEARCH_SHARD_INDICES); String expectedShowFieldsFalse = "sort:\n" + " desc\n" + " desc\n" + " asc\n" + " asc\n"; assertEquals(expectedShowFieldsFalse, shapeShowFieldsFalse); } @@ -181,21 +277,58 @@ public void testHashCode() { SearchSourceBuilder querySourceBuilder = SearchSourceBuilderUtils.createQuerySearchSourceBuilder(); // showFields true - MurmurHash3.Hash128 defaultHashTrue = QueryShapeGenerator.getShapeHashCode(defaultSourceBuilder, true); - MurmurHash3.Hash128 queryHashTrue = QueryShapeGenerator.getShapeHashCode(querySourceBuilder, true); - assertEquals(defaultHashTrue, QueryShapeGenerator.getShapeHashCode(defaultSourceBuilder, true)); - assertEquals(queryHashTrue, QueryShapeGenerator.getShapeHashCode(querySourceBuilder, true)); + MurmurHash3.Hash128 defaultHashTrue = QueryShapeGenerator.getShapeHashCode( + defaultSourceBuilder, + true, + mockMetadata, + SEARCH_SHARD_INDICES + ); + MurmurHash3.Hash128 queryHashTrue = QueryShapeGenerator.getShapeHashCode( + querySourceBuilder, + true, + mockMetadata, + SEARCH_SHARD_INDICES + ); + assertEquals(defaultHashTrue, QueryShapeGenerator.getShapeHashCode(defaultSourceBuilder, true, mockMetadata, SEARCH_SHARD_INDICES)); + assertEquals(queryHashTrue, QueryShapeGenerator.getShapeHashCode(querySourceBuilder, true, mockMetadata, SEARCH_SHARD_INDICES)); assertNotEquals(defaultHashTrue, queryHashTrue); // showFields false - MurmurHash3.Hash128 defaultHashFalse = QueryShapeGenerator.getShapeHashCode(defaultSourceBuilder, false); - MurmurHash3.Hash128 queryHashFalse = QueryShapeGenerator.getShapeHashCode(querySourceBuilder, false); - assertEquals(defaultHashFalse, QueryShapeGenerator.getShapeHashCode(defaultSourceBuilder, false)); - assertEquals(queryHashFalse, QueryShapeGenerator.getShapeHashCode(querySourceBuilder, false)); + MurmurHash3.Hash128 defaultHashFalse = QueryShapeGenerator.getShapeHashCode( + defaultSourceBuilder, + false, + mockMetadata, + SEARCH_SHARD_INDICES + ); + MurmurHash3.Hash128 queryHashFalse = QueryShapeGenerator.getShapeHashCode( + querySourceBuilder, + false, + mockMetadata, + SEARCH_SHARD_INDICES + ); + assertEquals( + defaultHashFalse, + QueryShapeGenerator.getShapeHashCode(defaultSourceBuilder, false, mockMetadata, SEARCH_SHARD_INDICES) + ); + assertEquals(queryHashFalse, QueryShapeGenerator.getShapeHashCode(querySourceBuilder, false, mockMetadata, SEARCH_SHARD_INDICES)); assertNotEquals(defaultHashFalse, queryHashFalse); // Compare field data on vs off assertNotEquals(defaultHashTrue, defaultHashFalse); assertNotEquals(queryHashTrue, queryHashFalse); } + + public void testFindFieldType() { + Map properties = (Map) getFlatIndexMapping().get("properties"); + String fieldType1 = findFieldType(properties, "field1"); + String fieldType2 = findFieldType(properties, "field2"); + String fieldType3 = findFieldType(properties, "field3"); + assertEquals("long", fieldType1); + assertEquals("text", fieldType2); + assertEquals("boolean", fieldType3); + + properties = (Map) getNestedIndexMapping().get("properties"); + fieldType1 = findFieldType(properties, "field1"); + assertEquals("long", fieldType1); + } } diff --git a/src/test/java/org/opensearch/plugin/insights/core/service/categorizer/QueryShapeVisitorTests.java b/src/test/java/org/opensearch/plugin/insights/core/service/categorizer/QueryShapeVisitorTests.java index 298f4f8..530a15f 100644 --- a/src/test/java/org/opensearch/plugin/insights/core/service/categorizer/QueryShapeVisitorTests.java +++ b/src/test/java/org/opensearch/plugin/insights/core/service/categorizer/QueryShapeVisitorTests.java @@ -8,6 +8,10 @@ package org.opensearch.plugin.insights.core.service.categorizer; +import static org.mockito.Mockito.mock; + +import java.util.Set; +import org.opensearch.cluster.metadata.Metadata; import org.opensearch.index.query.BoolQueryBuilder; import org.opensearch.index.query.ConstantScoreQueryBuilder; import org.opensearch.index.query.MatchQueryBuilder; @@ -27,7 +31,7 @@ public void testQueryShapeVisitor() { .mustNot(new RegexpQueryBuilder("color", "red.*")) ) .must(new TermsQueryBuilder("genre", "action", "drama", "romance")); - QueryShapeVisitor shapeVisitor = new QueryShapeVisitor(); + QueryShapeVisitor shapeVisitor = new QueryShapeVisitor(mock(Metadata.class), Set.of()); builder.visit(shapeVisitor); assertEquals( "{\"type\":\"bool\",\"must\"[{\"type\":\"term\"},{\"type\":\"terms\"}],\"filter\"[{\"type\":\"constant_score\",\"filter\"[{\"type\":\"range\"}]}],\"should\"[{\"type\":\"bool\",\"must\"[{\"type\":\"match\"}],\"must_not\"[{\"type\":\"regexp\"}]}]}",