diff --git a/client-java/controller/pom.xml b/client-java/controller/pom.xml index 989ae4457f..2aa2128835 100644 --- a/client-java/controller/pom.xml +++ b/client-java/controller/pom.xml @@ -205,6 +205,13 @@ test + + + org.opensearch.client + opensearch-java + test + + io.grpc diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/OpenSearchHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/OpenSearchHeuristicsCalculator.java index dacf98e438..e6bd2ca970 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/OpenSearchHeuristicsCalculator.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/OpenSearchHeuristicsCalculator.java @@ -5,6 +5,17 @@ import java.util.function.DoubleUnaryOperator; import org.evomaster.client.java.controller.opensearch.operations.ComparisonOperation; import org.evomaster.client.java.controller.opensearch.operations.TermOperation; +import org.evomaster.client.java.controller.opensearch.operations.TermsOperation; +import org.evomaster.client.java.controller.opensearch.operations.TermsSetOperation; +import org.evomaster.client.java.controller.opensearch.operations.IdsOperation; +import org.evomaster.client.java.controller.opensearch.operations.RangeOperation; +import org.evomaster.client.java.controller.opensearch.operations.PrefixOperation; +import org.evomaster.client.java.controller.opensearch.operations.ExistsOperation; +import org.evomaster.client.java.controller.opensearch.operations.FuzzyOperation; +import org.evomaster.client.java.controller.opensearch.operations.WildcardOperation; +import org.evomaster.client.java.controller.opensearch.operations.RegexpOperation; +import org.evomaster.client.java.controller.opensearch.operations.BoolOperation; +import org.evomaster.client.java.controller.opensearch.operations.MatchOperation; import org.evomaster.client.java.controller.opensearch.operations.QueryOperation; import org.evomaster.client.java.distance.heuristics.DistanceHelper; import org.evomaster.client.java.sql.internal.TaintHandler; @@ -29,10 +40,54 @@ private QueryOperation getOperation(Object query) { return new OpenSearchQueryParser().parse(query); } - private double calculateDistance(QueryOperation operation, Object doc) { + protected double calculateDistance(QueryOperation operation, Object doc) { if (operation instanceof TermOperation) { return calculateDistanceForEquals((TermOperation) operation, doc); } + + if (operation instanceof TermsOperation) { + return calculateDistanceForTerms((TermsOperation) operation, doc); + } + + if (operation instanceof TermsSetOperation) { + return calculateDistanceForTermsSet((TermsSetOperation) operation, doc); + } + + if (operation instanceof IdsOperation) { + return calculateDistanceForIds((IdsOperation) operation, doc); + } + + if (operation instanceof RangeOperation) { + return calculateDistanceForRange((RangeOperation) operation, doc); + } + + if (operation instanceof PrefixOperation) { + return calculateDistanceForPrefix((PrefixOperation) operation, doc); + } + + if (operation instanceof ExistsOperation) { + return calculateDistanceForExists((ExistsOperation) operation, doc); + } + + if (operation instanceof FuzzyOperation) { + return calculateDistanceForFuzzy((FuzzyOperation) operation, doc); + } + + if (operation instanceof WildcardOperation) { + return calculateDistanceForWildcard((WildcardOperation) operation, doc); + } + + if (operation instanceof RegexpOperation) { + return calculateDistanceForRegexp((RegexpOperation) operation, doc); + } + + if (operation instanceof BoolOperation) { + return calculateDistanceForBool((BoolOperation) operation, doc); + } + + if (operation instanceof MatchOperation) { + return calculateDistanceForMatch((MatchOperation) operation, doc); + } return Double.MAX_VALUE; } @@ -41,6 +96,96 @@ private double calculateDistanceForEquals(TermOperation operation, Object doc return calculateDistanceForComparisonOperation(operation, doc, (Math::abs)); } + /** + * Calculate distance for Terms operation. + * A Terms query matches if the document field value matches ANY of the terms in the list. + * Distance is the minimum distance to any of the terms. + */ + private double calculateDistanceForTerms(TermsOperation operation, Object doc) { + String field = operation.getFieldName(); + List expectedValues = operation.getValues(); + + if (!((Map) doc).containsKey(field)) { + return Double.MAX_VALUE; + } + + Object actualValue = ((Map) doc).get(field); + + // Find the minimum distance to any of the terms + double minDistance = Double.MAX_VALUE; + for (Object expectedValue : expectedValues) { + double distance = compareValues(actualValue, expectedValue, false); + if (distance == 0) { + return 0.0; + } + if (distance < minDistance) { + minDistance = distance; + } + } + + return Math.abs(minDistance); + } + + /** + * Calculate distance for TermsSet operation. + * A TermsSet query matches if the document field contains at least the minimum required number of matching terms. + * For heuristic calculation, we simulate the minimum requirement by counting actual matches and calculating distance + * based on how many more matches are needed. + */ + private double calculateDistanceForTermsSet(TermsSetOperation operation, Object doc) { + String field = operation.getFieldName(); + List expectedTerms = operation.getTerms(); + String minimumShouldMatchField = operation.getMinimumShouldMatchField(); + + if (!((Map) doc).containsKey(field)) { + return Double.MAX_VALUE; + } + + Object actualValue = ((Map) doc).get(field); + + // Count how many terms match + int matchCount = 0; + double totalDistance = 0.0; + + for (Object expectedTerm : expectedTerms) { + double distance = compareValues(actualValue, expectedTerm, false); + if (distance == 0) { + matchCount++; + } else { + totalDistance += distance; + } + } + + // Get minimum required matches - for simplicity, we assume it's available in the document + // In a real scenario, this would be more complex involving field lookup or script evaluation + int minimumRequired = getMinimumShouldMatch(doc, minimumShouldMatchField); + + if (matchCount >= minimumRequired) { + // Requirement satisfied + return 0.0; + } else { + // Not enough matches - distance based on how many more matches needed + int shortfall = minimumRequired - matchCount; + // Return a distance that reflects both the shortfall and the quality of non-matching terms + return shortfall * 10.0 + (totalDistance / expectedTerms.size()); + } + } + + /** + * Helper method to get the minimum should match value from the document or use a default. + * In real scenarios, this would involve more complex logic for field lookup or script evaluation. + */ + private int getMinimumShouldMatch(Object doc, String minimumShouldMatchField) { + if (minimumShouldMatchField != null && ((Map) doc).containsKey(minimumShouldMatchField)) { + Object value = ((Map) doc).get(minimumShouldMatchField); + if (value instanceof Number) { + return ((Number) value).intValue(); + } + } + // Default to 1 if no field specified or found + return 1; + } + private double calculateDistanceForComparisonOperation(ComparisonOperation operation, Object doc, DoubleUnaryOperator calculateDistance) { Object expectedValue = operation.getValue(); String field = operation.getFieldName(); @@ -50,12 +195,23 @@ private double calculateDistanceForComparisonOperation(ComparisonOperation op } Object actualValue = ((Map) doc).get(field); - double dif = compareValues(actualValue, expectedValue); + + // Handle case sensitivity for TermOperation + boolean caseInsensitive = false; + if (operation instanceof TermOperation) { + caseInsensitive = ((TermOperation) operation).getCaseInsensitive(); + } + + double dif = compareValues(actualValue, expectedValue, caseInsensitive); return calculateDistance.applyAsDouble(dif); } private double compareValues(Object val1, Object val2) { + return compareValues(val1, val2, false); + } + + private double compareValues(Object val1, Object val2, boolean caseInsensitive) { if (val1 instanceof Number && val2 instanceof Number) { double x = ((Number) val1).doubleValue(); @@ -64,12 +220,20 @@ private double compareValues(Object val1, Object val2) { } if (val1 instanceof String && val2 instanceof String) { + String str1 = (String) val1; + String str2 = (String) val2; + + // Apply case insensitive comparison if needed + if (caseInsensitive) { + str1 = str1.toLowerCase(); + str2 = str2.toLowerCase(); + } - if(taintHandler!=null){ - taintHandler.handleTaintForStringEquals((String)val1,(String)val2, false); + if (taintHandler != null) { + taintHandler.handleTaintForStringEquals(str1, str2, false); } - return (double) DistanceHelper.getLeftAlignmentDistance((String) val1, (String) val2); + return (double) DistanceHelper.getLeftAlignmentDistance(str1, str2); } if (val1 instanceof Boolean && val2 instanceof Boolean) { @@ -77,21 +241,48 @@ private double compareValues(Object val1, Object val2) { } if (val1 instanceof String && isObjectId(val2)) { + String str1 = (String) val1; + String str2 = val2.toString(); + + // Apply case insensitive comparison if needed + if (caseInsensitive) { + str1 = str1.toLowerCase(); + str2 = str2.toLowerCase(); + } + if(taintHandler!=null){ - taintHandler.handleTaintForStringEquals((String)val1,val2.toString(),false); + taintHandler.handleTaintForStringEquals(str1, str2, false); } - return (double) DistanceHelper.getLeftAlignmentDistance((String) val1, val2.toString()); + return (double) DistanceHelper.getLeftAlignmentDistance(str1, str2); } if (val2 instanceof String && isObjectId(val1)) { - if(taintHandler!=null){ - taintHandler.handleTaintForStringEquals(val1.toString(),val2.toString(),false); + String str1 = val1.toString(); + String str2 = (String) val2; + + // Apply case insensitive comparison if needed + if (caseInsensitive) { + str1 = str1.toLowerCase(); + str2 = str2.toLowerCase(); + } + + if (taintHandler != null) { + taintHandler.handleTaintForStringEquals(str1, str2, false); } - return (double) DistanceHelper.getLeftAlignmentDistance(val1.toString(), (String) val2); + return (double) DistanceHelper.getLeftAlignmentDistance(str1, str2); } if (isObjectId(val2) && isObjectId(val1)) { - return (double) DistanceHelper.getLeftAlignmentDistance(val1.toString(), val2.toString()); + String str1 = val1.toString(); + String str2 = val2.toString(); + + // Apply case insensitive comparison if needed + if (caseInsensitive) { + str1 = str1.toLowerCase(); + str2 = str2.toLowerCase(); + } + + return (double) DistanceHelper.getLeftAlignmentDistance(str1, str2); } @@ -103,6 +294,463 @@ private double compareValues(Object val1, Object val2) { return Double.MAX_VALUE; } + /** + * Calculate distance for IDs operation. + * IDs query matches if the document _id field matches any of the specified IDs. + */ + private double calculateDistanceForIds(IdsOperation operation, Object doc) { + List expectedIds = operation.getValues(); + + // Get document ID - assuming it's available in the document as "_id" + if (!((Map) doc).containsKey("_id")) { + return Double.MAX_VALUE; + } + + Object actualId = ((Map) doc).get("_id"); + String actualIdStr = actualId.toString(); + + // Find minimum distance to any of the expected IDs + double minDistance = Double.MAX_VALUE; + for (String expectedId : expectedIds) { + double distance = compareValues(actualIdStr, expectedId, false); + if (distance == 0) { + return 0.0; + } + if (distance < minDistance) { + minDistance = distance; + } + } + + return Math.abs(minDistance); + } + + /** + * Calculate distance for Range operation. + * Range query matches if the document field value is within the specified range. + */ + private double calculateDistanceForRange(RangeOperation operation, Object doc) { + String field = operation.getFieldName(); + + if (!((Map) doc).containsKey(field)) { + return Double.MAX_VALUE; + } + + Object actualValue = ((Map) doc).get(field); + + // Convert to numeric value for comparison + if (!(actualValue instanceof Number)) { + // For non-numeric values, try to parse as double + try { + actualValue = Double.parseDouble(actualValue.toString()); + } catch (NumberFormatException e) { + return Double.MAX_VALUE; + } + } + + double actualNum = ((Number) actualValue).doubleValue(); + double distance = 0.0; + + // Check greater than or equal to (gte) + if (operation.getGte() != null) { + double gteValue = ((Number) operation.getGte()).doubleValue(); + if (actualNum < gteValue) { + distance += gteValue - actualNum; + } + } + + // Check greater than (gt) + if (operation.getGt() != null) { + double gtValue = ((Number) operation.getGt()).doubleValue(); + if (actualNum <= gtValue) { + distance += gtValue - actualNum + 1; + } + } + + // Check less than or equal to (lte) + if (operation.getLte() != null) { + double lteValue = ((Number) operation.getLte()).doubleValue(); + if (actualNum > lteValue) { + distance += actualNum - lteValue; + } + } + + // Check less than (lt) + if (operation.getLt() != null) { + double ltValue = ((Number) operation.getLt()).doubleValue(); + if (actualNum >= ltValue) { + distance += actualNum - ltValue + 1; + } + } + + return distance; + } + + /** + * Calculate distance for Prefix operation. + * Prefix query matches if the document field value starts with the specified prefix. + */ + private double calculateDistanceForPrefix(PrefixOperation operation, Object doc) { + String field = operation.getFieldName(); + String expectedPrefix = operation.getValue(); + Boolean caseInsensitive = operation.getCaseInsensitive(); + + if (!((Map) doc).containsKey(field)) { + return Double.MAX_VALUE; + } + + Object actualValue = ((Map) doc).get(field); + String actualStr = actualValue.toString(); + + // Apply case insensitive comparison if needed + if (caseInsensitive != null && caseInsensitive) { + actualStr = actualStr.toLowerCase(); + expectedPrefix = expectedPrefix.toLowerCase(); + } + + if (actualStr.startsWith(expectedPrefix)) { + return 0.0; + } + + // Calculate prefix distance using left alignment + return (double) DistanceHelper.getLeftAlignmentDistance(actualStr, expectedPrefix); + } + + /** + * Calculate distance for Exists operation. + * Exists query matches if the document contains the specified field. + */ + private double calculateDistanceForExists(ExistsOperation operation, Object doc) { + String field = operation.getField(); + + if (((Map) doc).containsKey(field)) { + Object value = ((Map) doc).get(field); + // Field exists and is not null + if (value != null) { + return 0.0; + } + } + + // Field doesn't exist or is null + return 1.0; + } + + /** + * Calculate distance for Fuzzy operation. + * Fuzzy query matches if the document field value is within the specified edit distance (fuzziness). + */ + private double calculateDistanceForFuzzy(FuzzyOperation operation, Object doc) { + String field = operation.getFieldName(); + String expectedValue = operation.getValue(); + Integer fuzziness = operation.getFuzziness(); + Boolean transpositions = operation.getTranspositions(); + + if (!((Map) doc).containsKey(field)) { + return Double.MAX_VALUE; + } + + Object actualValue = ((Map) doc).get(field); + String actualStr = actualValue.toString(); + + // Calculate edit distance (Levenshtein distance) + int editDistance = calculateEditDistance(actualStr, expectedValue, transpositions != null ? transpositions : true); + + // If fuzziness is specified, check if within allowed distance + if (fuzziness != null) { + if (editDistance <= fuzziness) { + return 0.0; + } else { + return editDistance - fuzziness; + } + } + + // Default fuzziness is AUTO, which is typically 0, 1, or 2 based on term length + int defaultFuzziness = getAutoFuzziness(expectedValue.length()); + if (editDistance <= defaultFuzziness) { + return 0.0; + } + + return editDistance - defaultFuzziness; + } + + /** + * Calculate distance for Wildcard operation. + * Wildcard query matches if the document field value matches the wildcard pattern. + */ + private double calculateDistanceForWildcard(WildcardOperation operation, Object doc) { + String field = operation.getFieldName(); + String pattern = operation.getValue(); + Boolean caseInsensitive = operation.getCaseInsensitive(); + + if (!((Map) doc).containsKey(field)) { + return Double.MAX_VALUE; + } + + Object actualValue = ((Map) doc).get(field); + String actualStr = actualValue.toString(); + + // Apply case insensitive comparison if needed + if (caseInsensitive != null && caseInsensitive) { + actualStr = actualStr.toLowerCase(); + pattern = pattern.toLowerCase(); + } + + if (matchesWildcard(actualStr, pattern)) { + return 0.0; + } + + // Calculate approximate distance based on pattern similarity + return (double) DistanceHelper.getLeftAlignmentDistance(actualStr, pattern.replace("*", "").replace("?", "")); + } + + /** + * Calculate distance for Regexp operation. + * Regexp query matches if the document field value matches the regular expression. + */ + private double calculateDistanceForRegexp(RegexpOperation operation, Object doc) { + String field = operation.getFieldName(); + String regex = operation.getValue(); + Boolean caseInsensitive = operation.getCaseInsensitive(); + + if (!((Map) doc).containsKey(field)) { + return Double.MAX_VALUE; + } + + Object actualValue = ((Map) doc).get(field); + String actualStr = actualValue.toString(); + + try { + // Create pattern with case insensitive flag if needed + java.util.regex.Pattern pattern; + if (caseInsensitive != null && caseInsensitive) { + pattern = java.util.regex.Pattern.compile(regex, java.util.regex.Pattern.CASE_INSENSITIVE); + } else { + pattern = java.util.regex.Pattern.compile(regex); + } + + if (pattern.matcher(actualStr).matches()) { + return 0.0; + } + + // For non-matches, return a distance based on string similarity + return (double) DistanceHelper.getLeftAlignmentDistance(actualStr, regex); + + } catch (java.util.regex.PatternSyntaxException e) { + // Invalid regex pattern + return Double.MAX_VALUE; + } + } + + /** + * Calculate distance for Bool operation. + * Bool query combines multiple clauses with Boolean logic. + */ + private double calculateDistanceForBool(BoolOperation operation, Object doc) { + double totalDistance = 0.0; + + // MUST clauses - all must match (AND logic) + List mustClauses = operation.getMust(); + if (mustClauses != null && !mustClauses.isEmpty()) { + for (QueryOperation clause : mustClauses) { + double distance = calculateDistance(clause, doc); + if (distance == Double.MAX_VALUE) { + return Double.MAX_VALUE; // If any must clause fails completely + } + totalDistance += distance; + } + } + + // MUST_NOT clauses - none must match (NOT logic) + List mustNotClauses = operation.getMustNot(); + if (mustNotClauses != null && !mustNotClauses.isEmpty()) { + for (QueryOperation clause : mustNotClauses) { + double distance = calculateDistance(clause, doc); + if (distance == 0.0) { + return Double.MAX_VALUE; // If any must_not clause matches + } + // For must_not, closer matches are worse, so we invert the distance logic + totalDistance += Math.max(0, 10.0 - distance); + } + } + + // SHOULD clauses - at least minimum_should_match must match (OR logic) + List shouldClauses = operation.getShould(); + if (shouldClauses != null && !shouldClauses.isEmpty()) { + int minimumShouldMatch = operation.getMinimumShouldMatch() != null ? + operation.getMinimumShouldMatch() : 1; + + // Sort should clauses by their distances + List shouldDistances = new java.util.ArrayList<>(); + for (QueryOperation clause : shouldClauses) { + shouldDistances.add(calculateDistance(clause, doc)); + } + shouldDistances.sort(Double::compareTo); + + // Check if we have enough matches + int matches = 0; + for (double distance : shouldDistances) { + if (distance == 0.0) matches++; + } + + if (matches < minimumShouldMatch) { + // Add penalty for not meeting minimum should match + totalDistance += (minimumShouldMatch - matches) * 10.0; + // Add distance from best non-matching clauses + for (int i = matches; i < Math.min(minimumShouldMatch, shouldDistances.size()); i++) { + totalDistance += shouldDistances.get(i); + } + } + } + + // FILTER clauses - all must match but don't contribute to score + List filterClauses = operation.getFilter(); + if (filterClauses != null && !filterClauses.isEmpty()) { + for (QueryOperation clause : filterClauses) { + double distance = calculateDistance(clause, doc); + if (distance != 0.0) { + return Double.MAX_VALUE; // Filter clauses must match exactly + } + } + } + + return totalDistance; + } + + /** + * Calculate distance for Match operation. + * Match query performs full-text search with analysis and scoring. + */ + private double calculateDistanceForMatch(MatchOperation operation, Object doc) { + String field = operation.getFieldName(); + String queryText = operation.getValue(); + String operator = operation.getOperator(); + + if (!((Map) doc).containsKey(field)) { + return Double.MAX_VALUE; + } + + Object actualValue = ((Map) doc).get(field); + String actualStr = actualValue.toString(); + + // Simple tokenization and matching logic + String[] queryTokens = queryText.toLowerCase().split("\\s+"); + String[] docTokens = actualStr.toLowerCase().split("\\s+"); + + if ("and".equals(operator)) { + // All query tokens must match (AND logic) + double totalDistance = 0.0; + for (String queryToken : queryTokens) { + double minTokenDistance = Double.MAX_VALUE; + for (String docToken : docTokens) { + double distance = DistanceHelper.getLeftAlignmentDistance(docToken, queryToken); + if (distance == 0) { + minTokenDistance = 0; + break; + } + if (distance < minTokenDistance) { + minTokenDistance = distance; + } + } + totalDistance += minTokenDistance; + } + return totalDistance; + } else { + // At least one query token must match (OR logic - default) + double minDistance = Double.MAX_VALUE; + for (String queryToken : queryTokens) { + for (String docToken : docTokens) { + double distance = DistanceHelper.getLeftAlignmentDistance(docToken, queryToken); + if (distance == 0) { + return 0.0; // Perfect match found + } + if (distance < minDistance) { + minDistance = distance; + } + } + } + return minDistance; + } + } + + // Helper methods for advanced operations + + private int calculateEditDistance(String s1, String s2, boolean allowTranspositions) { + if (allowTranspositions) { + return calculateDamerauLevenshteinDistance(s1, s2); + } else { + return calculateLevenshteinDistance(s1, s2); + } + } + + private int calculateLevenshteinDistance(String s1, String s2) { + int[][] dp = new int[s1.length() + 1][s2.length() + 1]; + + for (int i = 0; i <= s1.length(); i++) dp[i][0] = i; + for (int j = 0; j <= s2.length(); j++) dp[0][j] = j; + + for (int i = 1; i <= s1.length(); i++) { + for (int j = 1; j <= s2.length(); j++) { + if (s1.charAt(i-1) == s2.charAt(j-1)) { + dp[i][j] = dp[i-1][j-1]; + } else { + dp[i][j] = 1 + Math.min(Math.min(dp[i-1][j], dp[i][j-1]), dp[i-1][j-1]); + } + } + } + + return dp[s1.length()][s2.length()]; + } + + private int calculateDamerauLevenshteinDistance(String s1, String s2) { + // Simplified Damerau-Levenshtein distance (includes transpositions) + int len1 = s1.length(); + int len2 = s2.length(); + int[][] dp = new int[len1 + 1][len2 + 1]; + + for (int i = 0; i <= len1; i++) dp[i][0] = i; + for (int j = 0; j <= len2; j++) dp[0][j] = j; + + for (int i = 1; i <= len1; i++) { + for (int j = 1; j <= len2; j++) { + int cost = (s1.charAt(i-1) == s2.charAt(j-1)) ? 0 : 1; + + dp[i][j] = Math.min(Math.min( + dp[i-1][j] + 1, // deletion + dp[i][j-1] + 1), // insertion + dp[i-1][j-1] + cost // substitution + ); + + // Transposition + if (i > 1 && j > 1 && + s1.charAt(i-1) == s2.charAt(j-2) && + s1.charAt(i-2) == s2.charAt(j-1)) { + dp[i][j] = Math.min(dp[i][j], dp[i-2][j-2] + cost); + } + } + } + + return dp[len1][len2]; + } + + private int getAutoFuzziness(int termLength) { + if (termLength <= 2) return 0; + if (termLength <= 5) return 1; + return 2; + } + + private boolean matchesWildcard(String text, String pattern) { + // Convert wildcard pattern to regex + String regex = pattern + .replace(".", "\\.") + .replace("*", ".*") + .replace("?", "."); + + try { + return text.matches(regex); + } catch (java.util.regex.PatternSyntaxException e) { + return false; + } + } + private static boolean isObjectId(Object obj) { return obj.getClass().getName().equals("org.bson.types.ObjectId"); } diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/OpenSearchQueryParser.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/OpenSearchQueryParser.java index 1372b61d70..471c7d25f0 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/OpenSearchQueryParser.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/OpenSearchQueryParser.java @@ -5,11 +5,35 @@ import java.util.Objects; import org.evomaster.client.java.controller.opensearch.operations.QueryOperation; import org.evomaster.client.java.controller.opensearch.selectors.TermSelector; +import org.evomaster.client.java.controller.opensearch.selectors.TermsSelector; +import org.evomaster.client.java.controller.opensearch.selectors.TermsSetSelector; +import org.evomaster.client.java.controller.opensearch.selectors.IdsSelector; +import org.evomaster.client.java.controller.opensearch.selectors.RangeSelector; +import org.evomaster.client.java.controller.opensearch.selectors.PrefixSelector; +import org.evomaster.client.java.controller.opensearch.selectors.ExistsSelector; +import org.evomaster.client.java.controller.opensearch.selectors.FuzzySelector; +import org.evomaster.client.java.controller.opensearch.selectors.WildcardSelector; +import org.evomaster.client.java.controller.opensearch.selectors.RegexpSelector; +import org.evomaster.client.java.controller.opensearch.selectors.BoolSelector; +import org.evomaster.client.java.controller.opensearch.selectors.MatchSelector; import org.evomaster.client.java.controller.opensearch.selectors.QuerySelector; public class OpenSearchQueryParser { - List selectors = Arrays.asList(new TermSelector()); + List selectors = Arrays.asList( + new TermSelector(), + new TermsSelector(), + new TermsSetSelector(), + new IdsSelector(), + new RangeSelector(), + new PrefixSelector(), + new ExistsSelector(), + new FuzzySelector(), + new WildcardSelector(), + new RegexpSelector(), + new BoolSelector(), + new MatchSelector() + ); public QueryOperation parse(Object query) { return selectors.stream() diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/BoolOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/BoolOperation.java new file mode 100644 index 0000000000..316c63cdb1 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/BoolOperation.java @@ -0,0 +1,59 @@ +package org.evomaster.client.java.controller.opensearch.operations; + +import java.util.List; + +/** + * Represents Bool operation. + * A Boolean query can combine several query clauses into one advanced query. The clauses are combined + * with Boolean logic to find matching documents returned in the results. + *

+ * OpenSearch Bool Operation + */ +public class BoolOperation extends QueryOperation { + private final List must; + private final List mustNot; + private final List should; + private final List filter; + private final Integer minimumShouldMatch; + private final Float boost; + + public BoolOperation(List must, List mustNot, + List should, List filter) { + this(must, mustNot, should, filter, null, null); + } + + public BoolOperation(List must, List mustNot, + List should, List filter, + Integer minimumShouldMatch, Float boost) { + this.must = must; + this.mustNot = mustNot; + this.should = should; + this.filter = filter; + this.minimumShouldMatch = minimumShouldMatch; + this.boost = boost; + } + + public List getMust() { + return must; + } + + public List getMustNot() { + return mustNot; + } + + public List getShould() { + return should; + } + + public List getFilter() { + return filter; + } + + public Integer getMinimumShouldMatch() { + return minimumShouldMatch; + } + + public Float getBoost() { + return boost; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/CommonQueryParameters.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/CommonQueryParameters.java new file mode 100644 index 0000000000..60a925ef3f --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/CommonQueryParameters.java @@ -0,0 +1,77 @@ +package org.evomaster.client.java.controller.opensearch.operations; + +/** + * Common parameters shared across multiple OpenSearch query operations. + */ +public class CommonQueryParameters { + private final Float boost; + private final String name; + private final String rewrite; + private final Boolean caseInsensitive; + + public CommonQueryParameters(Float boost, String name, String rewrite, Boolean caseInsensitive) { + this.boost = boost; + this.name = name; + this.rewrite = rewrite; + this.caseInsensitive = caseInsensitive; + } + + public static CommonQueryParameters empty() { + return new CommonQueryParameters(null, null, null, null); + } + + public static CommonQueryParameters withBoost(Float boost) { + return new CommonQueryParameters(boost, null, null, null); + } + + public static CommonQueryParameters withCaseInsensitive(Boolean caseInsensitive) { + return new CommonQueryParameters(null, null, null, caseInsensitive); + } + + public Float getBoost() { + return boost; + } + + public String getName() { + return name; + } + + public String getRewrite() { + return rewrite; + } + + public Boolean getCaseInsensitive() { + return caseInsensitive; + } + + public static class Builder { + private Float boost; + private String name; + private String rewrite; + private Boolean caseInsensitive; + + public Builder boost(Float boost) { + this.boost = boost; + return this; + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder rewrite(String rewrite) { + this.rewrite = rewrite; + return this; + } + + public Builder caseInsensitive(Boolean caseInsensitive) { + this.caseInsensitive = caseInsensitive; + return this; + } + + public CommonQueryParameters build() { + return new CommonQueryParameters(boost, name, rewrite, caseInsensitive); + } + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/ExistsOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/ExistsOperation.java new file mode 100644 index 0000000000..e6bd92403e --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/ExistsOperation.java @@ -0,0 +1,29 @@ +package org.evomaster.client.java.controller.opensearch.operations; + +/** + * Represents Exists operation. + * Searches for documents that contain a specific field. + *

+ * OpenSearch Exists Operation + */ +public class ExistsOperation extends QueryOperation { + private final String field; + private final Float boost; + + public ExistsOperation(String field) { + this(field, null); + } + + public ExistsOperation(String field, Float boost) { + this.field = field; + this.boost = boost; + } + + public String getField() { + return field; + } + + public Float getBoost() { + return boost; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/FieldValueOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/FieldValueOperation.java new file mode 100644 index 0000000000..008cb2e144 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/FieldValueOperation.java @@ -0,0 +1,45 @@ +package org.evomaster.client.java.controller.opensearch.operations; + +/** + * Abstract base class for OpenSearch operations that operate on a specific field with a value. + * This includes operations like Term, Prefix, Wildcard, Fuzzy, and Regexp. + */ +public abstract class FieldValueOperation extends QueryOperation { + private final String fieldName; + private final String value; + private final CommonQueryParameters commonParams; + + protected FieldValueOperation(String fieldName, String value, CommonQueryParameters commonParams) { + this.fieldName = fieldName; + this.value = value; + this.commonParams = commonParams != null ? commonParams : CommonQueryParameters.empty(); + } + + public String getFieldName() { + return fieldName; + } + + public String getValue() { + return value; + } + + public Float getBoost() { + return commonParams.getBoost(); + } + + public String getName() { + return commonParams.getName(); + } + + public String getRewrite() { + return commonParams.getRewrite(); + } + + public Boolean getCaseInsensitive() { + return commonParams.getCaseInsensitive(); + } + + protected CommonQueryParameters getCommonParams() { + return commonParams; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/FuzzyOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/FuzzyOperation.java new file mode 100644 index 0000000000..f5fae92516 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/FuzzyOperation.java @@ -0,0 +1,68 @@ +package org.evomaster.client.java.controller.opensearch.operations; + +/** + * Represents Fuzzy operation. + * Searches for documents containing terms that are similar to the search term within the maximum allowed + * Damerau-Levenshtein distance. The Damerau-Levenshtein distance measures the number of one-character + * changes needed to change one term to another term. + *

+ * OpenSearch Fuzzy Operation + */ +public class FuzzyOperation extends QueryOperation { + private final String fieldName; + private final String value; + private final Float boost; + private final Integer fuzziness; + private final Integer maxExpansions; + private final Integer prefixLength; + private final Boolean transpositions; + private final String rewrite; + + public FuzzyOperation(String fieldName, String value) { + this(fieldName, value, null, null, null, null, null, null); + } + + public FuzzyOperation(String fieldName, String value, Float boost, Integer fuzziness, + Integer maxExpansions, Integer prefixLength, Boolean transpositions, String rewrite) { + this.fieldName = fieldName; + this.value = value; + this.boost = boost; + this.fuzziness = fuzziness; + this.maxExpansions = maxExpansions; + this.prefixLength = prefixLength; + this.transpositions = transpositions; + this.rewrite = rewrite; + } + + public String getFieldName() { + return fieldName; + } + + public String getValue() { + return value; + } + + public Float getBoost() { + return boost; + } + + public Integer getFuzziness() { + return fuzziness; + } + + public Integer getMaxExpansions() { + return maxExpansions; + } + + public Integer getPrefixLength() { + return prefixLength; + } + + public Boolean getTranspositions() { + return transpositions; + } + + public String getRewrite() { + return rewrite; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/IdsOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/IdsOperation.java new file mode 100644 index 0000000000..7b2eb1b77a --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/IdsOperation.java @@ -0,0 +1,31 @@ +package org.evomaster.client.java.controller.opensearch.operations; + +import java.util.List; + +/** + * Represents IDs operation. + * Searches for documents with one or more specific document ID values in the _id field. + *

+ * OpenSearch IDs Operation + */ +public class IdsOperation extends QueryOperation { + private final List values; + private final Float boost; + + public IdsOperation(List values) { + this(values, null); + } + + public IdsOperation(List values, Float boost) { + this.values = values; + this.boost = boost; + } + + public List getValues() { + return values; + } + + public Float getBoost() { + return boost; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/MatchOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/MatchOperation.java new file mode 100644 index 0000000000..046b321393 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/MatchOperation.java @@ -0,0 +1,81 @@ +package org.evomaster.client.java.controller.opensearch.operations; + +/** + * Represents Match operation. + * The match query is the standard query for performing full-text searches, including fuzzy matching and phrase or proximity searches. + *

+ * OpenSearch Match Operation + */ +public class MatchOperation extends FieldValueOperation { + private final String operator; // "and" or "or" + private final Integer minimumShouldMatch; + private final String fuzziness; + private final Integer prefixLength; + private final Integer maxExpansions; + private final String analyzer; + private final Boolean fuzzyTranspositions; + private final Boolean lenient; + private final Boolean zeroTermsQuery; + private final Float cutoffFrequency; + + public MatchOperation(String fieldName, String value) { + this(fieldName, value, CommonQueryParameters.empty(), null, null, null, null, null, null, null, null, null, null); + } + + public MatchOperation(String fieldName, String value, CommonQueryParameters commonParams, + String operator, Integer minimumShouldMatch, String fuzziness, Integer prefixLength, + Integer maxExpansions, String analyzer, Boolean fuzzyTranspositions, + Boolean lenient, Boolean zeroTermsQuery, Float cutoffFrequency) { + super(fieldName, value, commonParams); + this.operator = operator; + this.minimumShouldMatch = minimumShouldMatch; + this.fuzziness = fuzziness; + this.prefixLength = prefixLength; + this.maxExpansions = maxExpansions; + this.analyzer = analyzer; + this.fuzzyTranspositions = fuzzyTranspositions; + this.lenient = lenient; + this.zeroTermsQuery = zeroTermsQuery; + this.cutoffFrequency = cutoffFrequency; + } + + public String getOperator() { + return operator; + } + + public Integer getMinimumShouldMatch() { + return minimumShouldMatch; + } + + public String getFuzziness() { + return fuzziness; + } + + public Integer getPrefixLength() { + return prefixLength; + } + + public Integer getMaxExpansions() { + return maxExpansions; + } + + public String getAnalyzer() { + return analyzer; + } + + public Boolean getFuzzyTranspositions() { + return fuzzyTranspositions; + } + + public Boolean getLenient() { + return lenient; + } + + public Boolean getZeroTermsQuery() { + return zeroTermsQuery; + } + + public Float getCutoffFrequency() { + return cutoffFrequency; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/PrefixOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/PrefixOperation.java new file mode 100644 index 0000000000..10bb9de8e4 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/PrefixOperation.java @@ -0,0 +1,22 @@ +package org.evomaster.client.java.controller.opensearch.operations; + +/** + * Represents Prefix operation. + * Searches for terms that begin with a specific prefix. + *

+ * OpenSearch Prefix Operation + */ +public class PrefixOperation extends FieldValueOperation { + + public PrefixOperation(String fieldName, String value) { + this(fieldName, value, CommonQueryParameters.empty()); + } + + public PrefixOperation(String fieldName, String value, Float boost, Boolean caseInsensitive, String rewrite) { + this(fieldName, value, new CommonQueryParameters(boost, null, rewrite, caseInsensitive)); + } + + public PrefixOperation(String fieldName, String value, CommonQueryParameters commonParams) { + super(fieldName, value, commonParams); + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/RangeOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/RangeOperation.java new file mode 100644 index 0000000000..9ce6da8891 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/RangeOperation.java @@ -0,0 +1,72 @@ +package org.evomaster.client.java.controller.opensearch.operations; + +/** + * Represents Range operation. + * Searches for a range of values in a field using operators like gte, gt, lte, lt. + *

+ * OpenSearch Range Operation + */ +public class RangeOperation extends QueryOperation { + private final String fieldName; + private final Object gte; // Greater than or equal to + private final Object gt; // Greater than + private final Object lte; // Less than or equal to + private final Object lt; // Less than + private final String format; + private final String relation; + private final Float boost; + private final String timeZone; + + public RangeOperation(String fieldName, Object gte, Object gt, Object lte, Object lt) { + this(fieldName, gte, gt, lte, lt, null, null, null, null); + } + + public RangeOperation(String fieldName, Object gte, Object gt, Object lte, Object lt, + String format, String relation, Float boost, String timeZone) { + this.fieldName = fieldName; + this.gte = gte; + this.gt = gt; + this.lte = lte; + this.lt = lt; + this.format = format; + this.relation = relation; + this.boost = boost; + this.timeZone = timeZone; + } + + public String getFieldName() { + return fieldName; + } + + public Object getGte() { + return gte; + } + + public Object getGt() { + return gt; + } + + public Object getLte() { + return lte; + } + + public Object getLt() { + return lt; + } + + public String getFormat() { + return format; + } + + public String getRelation() { + return relation; + } + + public Float getBoost() { + return boost; + } + + public String getTimeZone() { + return timeZone; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/RegexpOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/RegexpOperation.java new file mode 100644 index 0000000000..27e7024864 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/RegexpOperation.java @@ -0,0 +1,39 @@ +package org.evomaster.client.java.controller.opensearch.operations; + +/** + * Represents Regexp operation. + * Searches for terms that match a regular expression. Regular expressions are applied to the terms + * (that is, tokens) in the field—not to the entire field. + *

+ * OpenSearch Regexp Operation + */ +public class RegexpOperation extends FieldValueOperation { + private final String flags; + private final Integer maxDeterminizedStates; + + public RegexpOperation(String fieldName, String value) { + this(fieldName, value, null, null, null, null, null); + } + + public RegexpOperation(String fieldName, String value, Float boost, Boolean caseInsensitive, + String flags, Integer maxDeterminizedStates, String rewrite) { + super(fieldName, value, new CommonQueryParameters(boost, null, rewrite, caseInsensitive)); + this.flags = flags; + this.maxDeterminizedStates = maxDeterminizedStates; + } + + public RegexpOperation(String fieldName, String value, CommonQueryParameters commonParams, + String flags, Integer maxDeterminizedStates) { + super(fieldName, value, commonParams); + this.flags = flags; + this.maxDeterminizedStates = maxDeterminizedStates; + } + + public String getFlags() { + return flags; + } + + public Integer getMaxDeterminizedStates() { + return maxDeterminizedStates; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/TermOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/TermOperation.java index 67fc8d8f9b..88f723aa55 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/TermOperation.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/TermOperation.java @@ -3,9 +3,38 @@ /** * Represent Term operation. * Matches documents where the value of a field equals the specified value. + *

+ * OpenSearch Term Operation */ public class TermOperation extends ComparisonOperation { + private final Boolean caseInsensitive; + private final Float boost; + private final String name; + public TermOperation(String fieldName, V value) { + this(fieldName, value, Boolean.FALSE, null, null); + } + + public TermOperation(String fieldName, V value, Boolean caseInsensitive) { + this(fieldName, value, caseInsensitive, null, null); + } + + public TermOperation(String fieldName, V value, Boolean caseInsensitive, Float boost, String name) { super(fieldName, value); + this.caseInsensitive = caseInsensitive; + this.boost = boost; + this.name = name; + } + + public Boolean getCaseInsensitive() { + return caseInsensitive; + } + + public Float getBoost() { + return boost; + } + + public String getName() { + return name; } } \ No newline at end of file diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/TermsOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/TermsOperation.java new file mode 100644 index 0000000000..d53d373422 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/TermsOperation.java @@ -0,0 +1,49 @@ +package org.evomaster.client.java.controller.opensearch.operations; + +import java.util.List; + +/** + * Represents Terms operation. + * Matches documents where the value of a field matches any of the specified terms. + *

+ * OpenSearch Terms Operation + */ +public class TermsOperation extends QueryOperation { + private final String fieldName; + private final List values; + private final Float boost; + private final String name; + private final String valueType; + + public TermsOperation(String fieldName, List values) { + this(fieldName, values, null, null, null); + } + + public TermsOperation(String fieldName, List values, Float boost, String name, String valueType) { + this.fieldName = fieldName; + this.values = values; + this.boost = boost; + this.name = name; + this.valueType = valueType; + } + + public String getFieldName() { + return fieldName; + } + + public List getValues() { + return values; + } + + public Float getBoost() { + return boost; + } + + public String getName() { + return name; + } + + public String getValueType() { + return valueType; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/TermsSetOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/TermsSetOperation.java new file mode 100644 index 0000000000..5c79de572c --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/TermsSetOperation.java @@ -0,0 +1,51 @@ +package org.evomaster.client.java.controller.opensearch.operations; + +import java.util.List; + +/** + * Represents Terms Set operation. + * Matches documents that contain a minimum number of exact terms in a specified field. + * Similar to Terms query but allows specifying minimum number of matching terms required. + *

+ * OpenSearch Terms Set Operation + */ +public class TermsSetOperation extends QueryOperation { + private final String fieldName; + private final List terms; + private final String minimumShouldMatchField; + private final String minimumShouldMatchScript; + private final Float boost; + + public TermsSetOperation(String fieldName, List terms, String minimumShouldMatchField) { + this(fieldName, terms, minimumShouldMatchField, null, null); + } + + public TermsSetOperation(String fieldName, List terms, String minimumShouldMatchField, + String minimumShouldMatchScript, Float boost) { + this.fieldName = fieldName; + this.terms = terms; + this.minimumShouldMatchField = minimumShouldMatchField; + this.minimumShouldMatchScript = minimumShouldMatchScript; + this.boost = boost; + } + + public String getFieldName() { + return fieldName; + } + + public List getTerms() { + return terms; + } + + public String getMinimumShouldMatchField() { + return minimumShouldMatchField; + } + + public String getMinimumShouldMatchScript() { + return minimumShouldMatchScript; + } + + public Float getBoost() { + return boost; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/WildcardOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/WildcardOperation.java new file mode 100644 index 0000000000..0552a04954 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/operations/WildcardOperation.java @@ -0,0 +1,24 @@ +package org.evomaster.client.java.controller.opensearch.operations; + +/** + * Represents Wildcard operation. + * Searches for terms that match a wildcard pattern. Wildcard queries support the following operators: + * * (asterisk) - Matches zero or more characters + * ? (question mark) - Matches any single character + *

+ * OpenSearch Wildcard Operation + */ +public class WildcardOperation extends FieldValueOperation { + + public WildcardOperation(String fieldName, String value) { + this(fieldName, value, CommonQueryParameters.empty()); + } + + public WildcardOperation(String fieldName, String value, Float boost, Boolean caseInsensitive, String rewrite) { + this(fieldName, value, new CommonQueryParameters(boost, null, rewrite, caseInsensitive)); + } + + public WildcardOperation(String fieldName, String value, CommonQueryParameters commonParams) { + super(fieldName, value, commonParams); + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/BoolSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/BoolSelector.java new file mode 100644 index 0000000000..cca9958dc3 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/BoolSelector.java @@ -0,0 +1,59 @@ +package org.evomaster.client.java.controller.opensearch.selectors; + +import org.evomaster.client.java.controller.opensearch.operations.BoolOperation; +import org.evomaster.client.java.controller.opensearch.operations.QueryOperation; +import org.evomaster.client.java.controller.opensearch.OpenSearchQueryParser; + +import java.util.ArrayList; +import java.util.List; + +import static org.evomaster.client.java.controller.opensearch.utils.OpenSearchQueryHelper.extractBoolClause; +import static org.evomaster.client.java.controller.opensearch.utils.OpenSearchQueryHelper.extractBoost; +import static org.evomaster.client.java.controller.opensearch.utils.OpenSearchQueryHelper.extractIntegerParameter; + +/** + * Selector for Bool queries. + * Structure: { bool: { must: [...], must_not: [...], should: [...], filter: [...] } } + */ +public class BoolSelector extends SingleConditionQuerySelector { + + private static final String OPERATOR = "Bool"; + private static final String STRUCTURE = "bool"; + + @Override + protected QueryOperation parse(Object query) { + List must = parseNestedQueries(extractBoolClause(query, structure(), "must")); + List mustNot = parseNestedQueries(extractBoolClause(query, structure(), "mustNot")); + List should = parseNestedQueries(extractBoolClause(query, structure(), "should")); + List filter = parseNestedQueries(extractBoolClause(query, structure(), "filter")); + + Integer minimumShouldMatch = extractIntegerParameter(query, structure(), "minimumShouldMatch"); + Float boost = extractBoost(query, structure()); + + return new BoolOperation(must, mustNot, should, filter, minimumShouldMatch, boost); + } + + private List parseNestedQueries(List queryObjects) { + List operations = new ArrayList<>(); + OpenSearchQueryParser parser = new OpenSearchQueryParser(); + + for (Object queryObj : queryObjects) { + QueryOperation operation = parser.parse(queryObj); + if (operation != null) { + operations.add(operation); + } + } + + return operations; + } + + @Override + protected String operator() { + return OPERATOR; + } + + @Override + protected String structure() { + return STRUCTURE; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/ExistsSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/ExistsSelector.java new file mode 100644 index 0000000000..3092da970f --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/ExistsSelector.java @@ -0,0 +1,35 @@ +package org.evomaster.client.java.controller.opensearch.selectors; + +import org.evomaster.client.java.controller.opensearch.operations.ExistsOperation; +import org.evomaster.client.java.controller.opensearch.operations.QueryOperation; + +import static org.evomaster.client.java.controller.opensearch.utils.OpenSearchQueryHelper.extractExistsField; +import static org.evomaster.client.java.controller.opensearch.utils.OpenSearchQueryHelper.extractBoost; + +/** + * Selector for Exists queries. + * Structure: { exists: { field: "fieldname" } } + */ +public class ExistsSelector extends SingleConditionQuerySelector { + + private static final String OPERATOR = "Exists"; + private static final String STRUCTURE = "exists"; + + @Override + protected QueryOperation parse(Object query) { + String field = extractExistsField(query, structure()); + Float boost = extractBoost(query, structure()); + + return new ExistsOperation(field, boost); + } + + @Override + protected String operator() { + return OPERATOR; + } + + @Override + protected String structure() { + return STRUCTURE; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/FuzzySelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/FuzzySelector.java new file mode 100644 index 0000000000..633efc49e6 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/FuzzySelector.java @@ -0,0 +1,34 @@ +package org.evomaster.client.java.controller.opensearch.selectors; + +import org.evomaster.client.java.controller.opensearch.operations.FuzzyOperation; +import org.evomaster.client.java.controller.opensearch.operations.QueryOperation; +import org.evomaster.client.java.controller.opensearch.utils.ParameterExtractor; + +/** + * Selector for Fuzzy queries. + * Structure: { fuzzy: { field: { value: "term", fuzziness: 2, ... } } } + */ +public class FuzzySelector extends SingleConditionQuerySelector { + + private static final String OPERATOR = "Fuzzy"; + private static final String STRUCTURE = "fuzzy"; + + @Override + protected QueryOperation parse(Object query) { + ParameterExtractor.FuzzyParams params = ParameterExtractor.extractFuzzyParams(query, structure()); + return new FuzzyOperation(params.baseParams.fieldName, params.baseParams.value, + params.baseParams.commonParams.getBoost(), params.fuzziness, + params.maxExpansions, params.prefixLength, params.transpositions, + params.baseParams.commonParams.getRewrite()); + } + + @Override + protected String operator() { + return OPERATOR; + } + + @Override + protected String structure() { + return STRUCTURE; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/IdsSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/IdsSelector.java new file mode 100644 index 0000000000..d01bbf3e9b --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/IdsSelector.java @@ -0,0 +1,37 @@ +package org.evomaster.client.java.controller.opensearch.selectors; + +import org.evomaster.client.java.controller.opensearch.operations.IdsOperation; +import org.evomaster.client.java.controller.opensearch.operations.QueryOperation; + +import java.util.List; + +import static org.evomaster.client.java.controller.opensearch.utils.OpenSearchQueryHelper.extractIdsValues; +import static org.evomaster.client.java.controller.opensearch.utils.OpenSearchQueryHelper.extractBoost; + +/** + * Selector for IDs queries. + * Structure: { ids: { values: ["id1", "id2", ...] } } + */ +public class IdsSelector extends SingleConditionQuerySelector { + + private static final String OPERATOR = "Ids"; + private static final String STRUCTURE = "ids"; + + @Override + protected QueryOperation parse(Object query) { + List values = extractIdsValues(query, structure()); + Float boost = extractBoost(query, structure()); + + return new IdsOperation(values, boost); + } + + @Override + protected String operator() { + return OPERATOR; + } + + @Override + protected String structure() { + return STRUCTURE; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/MatchSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/MatchSelector.java new file mode 100644 index 0000000000..0892c8cf38 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/MatchSelector.java @@ -0,0 +1,35 @@ +package org.evomaster.client.java.controller.opensearch.selectors; + +import org.evomaster.client.java.controller.opensearch.operations.MatchOperation; +import org.evomaster.client.java.controller.opensearch.operations.QueryOperation; +import org.evomaster.client.java.controller.opensearch.utils.ParameterExtractor; + +/** + * Selector for Match queries. + * Structure: { match: { field: { query: "text", operator: "and", ... } } } + */ +public class MatchSelector extends SingleConditionQuerySelector { + + private static final String OPERATOR = "Match"; + private static final String STRUCTURE = "match"; + + @Override + protected QueryOperation parse(Object query) { + ParameterExtractor.MatchParams params = ParameterExtractor.extractMatchParams(query, structure()); + + return new MatchOperation(params.baseParams.fieldName, params.baseParams.value, + params.baseParams.commonParams, params.operator, params.minimumShouldMatch, + params.fuzziness, params.prefixLength, params.maxExpansions, params.analyzer, + params.fuzzyTranspositions, params.lenient, params.zeroTermsQuery, null); + } + + @Override + protected String operator() { + return OPERATOR; + } + + @Override + protected String structure() { + return STRUCTURE; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/PrefixSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/PrefixSelector.java new file mode 100644 index 0000000000..c662dfdef4 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/PrefixSelector.java @@ -0,0 +1,31 @@ +package org.evomaster.client.java.controller.opensearch.selectors; + +import org.evomaster.client.java.controller.opensearch.operations.PrefixOperation; +import org.evomaster.client.java.controller.opensearch.operations.QueryOperation; +import org.evomaster.client.java.controller.opensearch.utils.ParameterExtractor; + +/** + * Selector for Prefix queries. + * Structure: { prefix: { field: { value: "prefix" } } } + */ +public class PrefixSelector extends SingleConditionQuerySelector { + + private static final String OPERATOR = "Prefix"; + private static final String STRUCTURE = "prefix"; + + @Override + protected QueryOperation parse(Object query) { + ParameterExtractor.FieldValueParams params = ParameterExtractor.extractFieldValueParams(query, structure()); + return new PrefixOperation(params.fieldName, params.value, params.commonParams); + } + + @Override + protected String operator() { + return OPERATOR; + } + + @Override + protected String structure() { + return STRUCTURE; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/RangeSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/RangeSelector.java new file mode 100644 index 0000000000..3b6d78f552 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/RangeSelector.java @@ -0,0 +1,44 @@ +package org.evomaster.client.java.controller.opensearch.selectors; + +import org.evomaster.client.java.controller.opensearch.operations.RangeOperation; +import org.evomaster.client.java.controller.opensearch.operations.QueryOperation; + +import static org.evomaster.client.java.controller.opensearch.utils.OpenSearchQueryHelper.extractFieldName; +import static org.evomaster.client.java.controller.opensearch.utils.OpenSearchQueryHelper.extractRangeParameter; +import static org.evomaster.client.java.controller.opensearch.utils.OpenSearchQueryHelper.extractRangeStringParameter; +import static org.evomaster.client.java.controller.opensearch.utils.OpenSearchQueryHelper.extractBoost; + +/** + * Selector for Range queries. + * Structure: { range: { field: { gte: value, lte: value, ... } } } + */ +public class RangeSelector extends SingleConditionQuerySelector { + + private static final String OPERATOR = "Range"; + private static final String STRUCTURE = "range"; + + @Override + protected QueryOperation parse(Object query) { + String fieldName = extractFieldName(query, structure()); + Object gte = extractRangeParameter(query, structure(), "gte"); + Object gt = extractRangeParameter(query, structure(), "gt"); + Object lte = extractRangeParameter(query, structure(), "lte"); + Object lt = extractRangeParameter(query, structure(), "lt"); + String format = extractRangeStringParameter(query, structure(), "format"); + String relation = extractRangeStringParameter(query, structure(), "relation"); + Float boost = extractBoost(query, structure()); + String timeZone = extractRangeStringParameter(query, structure(), "timeZone"); + + return new RangeOperation(fieldName, gte, gt, lte, lt, format, relation, boost, timeZone); + } + + @Override + protected String operator() { + return OPERATOR; + } + + @Override + protected String structure() { + return STRUCTURE; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/RegexpSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/RegexpSelector.java new file mode 100644 index 0000000000..4490b054e5 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/RegexpSelector.java @@ -0,0 +1,32 @@ +package org.evomaster.client.java.controller.opensearch.selectors; + +import org.evomaster.client.java.controller.opensearch.operations.RegexpOperation; +import org.evomaster.client.java.controller.opensearch.operations.QueryOperation; +import org.evomaster.client.java.controller.opensearch.utils.ParameterExtractor; + +/** + * Selector for Regexp queries. + * Structure: { regexp: { field: { value: "[a-zA-Z]amlet", flags: "ALL", ... } } } + */ +public class RegexpSelector extends SingleConditionQuerySelector { + + private static final String OPERATOR = "Regexp"; + private static final String STRUCTURE = "regexp"; + + @Override + protected QueryOperation parse(Object query) { + ParameterExtractor.RegexpParams params = ParameterExtractor.extractRegexpParams(query, structure()); + return new RegexpOperation(params.baseParams.fieldName, params.baseParams.value, + params.baseParams.commonParams, params.flags, params.maxDeterminizedStates); + } + + @Override + protected String operator() { + return OPERATOR; + } + + @Override + protected String structure() { + return STRUCTURE; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/TermSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/TermSelector.java index d900ca6c67..77ddbda564 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/TermSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/TermSelector.java @@ -5,6 +5,9 @@ import static org.evomaster.client.java.controller.opensearch.utils.OpenSearchQueryHelper.extractFieldName; import static org.evomaster.client.java.controller.opensearch.utils.OpenSearchQueryHelper.extractFieldValue; +import static org.evomaster.client.java.controller.opensearch.utils.OpenSearchQueryHelper.extractCaseInsensitive; +import static org.evomaster.client.java.controller.opensearch.utils.OpenSearchQueryHelper.extractBoost; +import static org.evomaster.client.java.controller.opensearch.utils.OpenSearchQueryHelper.extractQueryName; /** * { term: { field: value } } @@ -18,7 +21,10 @@ public class TermSelector extends SingleConditionQuerySelector { protected QueryOperation parse(Object query) { String fieldName = extractFieldName(query, structure()); Object value = extractFieldValue(query, structure()); - return new TermOperation<>(fieldName, value); + Boolean caseInsensitive = extractCaseInsensitive(query, structure()); + Float boost = extractBoost(query, structure()); + String name = extractQueryName(query, structure()); + return new TermOperation<>(fieldName, value, caseInsensitive, boost, name); } @Override diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/TermsSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/TermsSelector.java new file mode 100644 index 0000000000..54cc70f58a --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/TermsSelector.java @@ -0,0 +1,43 @@ +package org.evomaster.client.java.controller.opensearch.selectors; + +import org.evomaster.client.java.controller.opensearch.operations.TermsOperation; +import org.evomaster.client.java.controller.opensearch.operations.QueryOperation; + +import java.util.List; + +import static org.evomaster.client.java.controller.opensearch.utils.OpenSearchQueryHelper.extractFieldName; +import static org.evomaster.client.java.controller.opensearch.utils.OpenSearchQueryHelper.extractTermsArray; +import static org.evomaster.client.java.controller.opensearch.utils.OpenSearchQueryHelper.extractBoost; +import static org.evomaster.client.java.controller.opensearch.utils.OpenSearchQueryHelper.extractQueryName; +import static org.evomaster.client.java.controller.opensearch.utils.OpenSearchQueryHelper.extractValueType; + +/** + * Selector for Terms queries. + * Structure: { terms: { field: [value1, value2, ...] } } + */ +public class TermsSelector extends SingleConditionQuerySelector { + + private static final String OPERATOR = "Terms"; + private static final String STRUCTURE = "terms"; + + @Override + protected QueryOperation parse(Object query) { + String fieldName = extractFieldName(query, structure()); + List values = extractTermsArray(query, structure()); + Float boost = extractBoost(query, structure()); + String name = extractQueryName(query, structure()); + String valueType = extractValueType(query, structure()); + + return new TermsOperation<>(fieldName, values, boost, name, valueType); + } + + @Override + protected String operator() { + return OPERATOR; + } + + @Override + protected String structure() { + return STRUCTURE; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/TermsSetSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/TermsSetSelector.java new file mode 100644 index 0000000000..0ccff92495 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/TermsSetSelector.java @@ -0,0 +1,43 @@ +package org.evomaster.client.java.controller.opensearch.selectors; + +import org.evomaster.client.java.controller.opensearch.operations.TermsSetOperation; +import org.evomaster.client.java.controller.opensearch.operations.QueryOperation; + +import java.util.List; + +import static org.evomaster.client.java.controller.opensearch.utils.OpenSearchQueryHelper.extractFieldName; +import static org.evomaster.client.java.controller.opensearch.utils.OpenSearchQueryHelper.extractTermsArray; +import static org.evomaster.client.java.controller.opensearch.utils.OpenSearchQueryHelper.extractBoost; +import static org.evomaster.client.java.controller.opensearch.utils.OpenSearchQueryHelper.extractMinimumShouldMatchField; +import static org.evomaster.client.java.controller.opensearch.utils.OpenSearchQueryHelper.extractMinimumShouldMatchScript; + +/** + * Selector for Terms Set queries. + * Structure: { terms_set: { field: { terms: [...], minimum_should_match_field: "field" } } } + */ +public class TermsSetSelector extends SingleConditionQuerySelector { + + private static final String OPERATOR = "TermsSet"; + private static final String STRUCTURE = "terms_set"; + + @Override + protected QueryOperation parse(Object query) { + String fieldName = extractFieldName(query, structure()); + List terms = extractTermsArray(query, structure()); + String minimumShouldMatchField = extractMinimumShouldMatchField(query, structure()); + String minimumShouldMatchScript = extractMinimumShouldMatchScript(query, structure()); + Float boost = extractBoost(query, structure()); + + return new TermsSetOperation<>(fieldName, terms, minimumShouldMatchField, minimumShouldMatchScript, boost); + } + + @Override + protected String operator() { + return OPERATOR; + } + + @Override + protected String structure() { + return STRUCTURE; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/WildcardSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/WildcardSelector.java new file mode 100644 index 0000000000..d5f3b5cbe8 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/selectors/WildcardSelector.java @@ -0,0 +1,31 @@ +package org.evomaster.client.java.controller.opensearch.selectors; + +import org.evomaster.client.java.controller.opensearch.operations.WildcardOperation; +import org.evomaster.client.java.controller.opensearch.operations.QueryOperation; +import org.evomaster.client.java.controller.opensearch.utils.ParameterExtractor; + +/** + * Selector for Wildcard queries. + * Structure: { wildcard: { field: { value: "pattern*", case_insensitive: false, ... } } } + */ +public class WildcardSelector extends SingleConditionQuerySelector { + + private static final String OPERATOR = "Wildcard"; + private static final String STRUCTURE = "wildcard"; + + @Override + protected QueryOperation parse(Object query) { + ParameterExtractor.FieldValueParams params = ParameterExtractor.extractFieldValueParams(query, structure()); + return new WildcardOperation(params.fieldName, params.value, params.commonParams); + } + + @Override + protected String operator() { + return OPERATOR; + } + + @Override + protected String structure() { + return STRUCTURE; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/utils/OpenSearchQueryHelper.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/utils/OpenSearchQueryHelper.java index 1ae02d659b..80e70811ed 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/utils/OpenSearchQueryHelper.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/utils/OpenSearchQueryHelper.java @@ -47,6 +47,341 @@ public static Object extractFieldValue(Object query, String structure) { } } + /** + * Extracts the case_insensitive parameter from a term query object. + */ + public static Boolean extractCaseInsensitive(Object query, String structure) { + try { + Object term = query.getClass().getMethod(structure).invoke(query); + try { + Object caseInsensitive = term.getClass().getMethod("caseInsensitive").invoke(term); + if (caseInsensitive != null) { + return (Boolean) caseInsensitive; + } + } catch (NoSuchMethodException e) { + // case_insensitive parameter is optional, return default value + SimpleLogger.debug("[OpenSearch] case_insensitive parameter not found, using default (false)"); + } + return false; // Default value as per OpenSearch documentation + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + /** + * Extracts the terms array from a terms query object. + */ + public static java.util.List extractTermsArray(Object query, String structure) { + try { + Object terms = query.getClass().getMethod(structure).invoke(query); + + // Get the terms array/list + Object termsValue = terms.getClass().getMethod("terms").invoke(terms); + if (termsValue instanceof java.util.List) { + java.util.List result = new java.util.ArrayList<>(); + for (Object term : (java.util.List) termsValue) { + result.add(extractTypedFieldValue(term)); + } + return result; + } + return new java.util.ArrayList<>(); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + /** + * Extracts the boost parameter from a query object. + */ + public static Float extractBoost(Object query, String structure) { + try { + Object queryObj = query.getClass().getMethod(structure).invoke(query); + try { + Object boost = queryObj.getClass().getMethod("boost").invoke(queryObj); + if (boost != null) { + return ((Number) boost).floatValue(); + } + } catch (NoSuchMethodException e) { + // boost parameter is optional + SimpleLogger.debug("[OpenSearch] boost parameter not found, using default"); + } + return null; // Default value + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + /** + * Extracts the _name parameter from a query object. + */ + public static String extractQueryName(Object query, String structure) { + try { + Object queryObj = query.getClass().getMethod(structure).invoke(query); + try { + Object name = queryObj.getClass().getMethod("queryName").invoke(queryObj); + if (name != null) { + return (String) name; + } + } catch (NoSuchMethodException e) { + // _name parameter is optional + SimpleLogger.debug("[OpenSearch] _name parameter not found"); + } + return null; // Default value + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + /** + * Extracts the value_type parameter from a terms query object. + */ + public static String extractValueType(Object query, String structure) { + try { + Object queryObj = query.getClass().getMethod(structure).invoke(query); + try { + Object valueType = queryObj.getClass().getMethod("valueType").invoke(queryObj); + if (valueType != null) { + return (String) valueType; + } + } catch (NoSuchMethodException e) { + // value_type parameter is optional + SimpleLogger.debug("[OpenSearch] value_type parameter not found, using default"); + } + return "default"; // Default value as per OpenSearch documentation + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + /** + * Extracts the minimum_should_match_field parameter from a terms_set query object. + */ + public static String extractMinimumShouldMatchField(Object query, String structure) { + try { + Object queryObj = query.getClass().getMethod(structure).invoke(query); + try { + Object field = queryObj.getClass().getMethod("minimumShouldMatchField").invoke(queryObj); + if (field != null) { + return (String) field; + } + } catch (NoSuchMethodException e) { + // minimum_should_match_field parameter is optional + SimpleLogger.debug("[OpenSearch] minimum_should_match_field parameter not found"); + } + return null; + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + /** + * Extracts the minimum_should_match_script parameter from a terms_set query object. + */ + public static String extractMinimumShouldMatchScript(Object query, String structure) { + try { + Object queryObj = query.getClass().getMethod(structure).invoke(query); + try { + Object script = queryObj.getClass().getMethod("minimumShouldMatchScript").invoke(queryObj); + if (script != null) { + return (String) script; + } + } catch (NoSuchMethodException e) { + // minimum_should_match_script parameter is optional + SimpleLogger.debug("[OpenSearch] minimum_should_match_script parameter not found"); + } + return null; + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + /** + * Extracts the values array from an ids query object. + */ + public static java.util.List extractIdsValues(Object query, String structure) { + try { + Object ids = query.getClass().getMethod(structure).invoke(query); + Object valuesObj = ids.getClass().getMethod("values").invoke(ids); + + if (valuesObj instanceof java.util.List) { + java.util.List result = new java.util.ArrayList<>(); + for (Object value : (java.util.List) valuesObj) { + result.add((String) value); + } + return result; + } + return new java.util.ArrayList<>(); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + /** + * Extracts range parameters from a range query object. + */ + public static Object extractRangeParameter(Object query, String structure, String parameterName) { + try { + Object range = query.getClass().getMethod(structure).invoke(query); + String fieldName = extractFieldName(query, structure); + + // Get the field-specific range object + Object fieldRange = range.getClass().getMethod(fieldName).invoke(range); + + try { + Object param = fieldRange.getClass().getMethod(parameterName).invoke(fieldRange); + if (param != null) { + return extractTypedFieldValue(param); + } + } catch (NoSuchMethodException e) { + // Parameter is optional + SimpleLogger.debug("[OpenSearch] Range parameter " + parameterName + " not found"); + } + return null; + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + /** + * Extracts string parameter from range query object. + */ + public static String extractRangeStringParameter(Object query, String structure, String parameterName) { + try { + Object range = query.getClass().getMethod(structure).invoke(query); + String fieldName = extractFieldName(query, structure); + + // Get the field-specific range object + Object fieldRange = range.getClass().getMethod(fieldName).invoke(range); + + try { + Object param = fieldRange.getClass().getMethod(parameterName).invoke(fieldRange); + if (param != null) { + return (String) param; + } + } catch (NoSuchMethodException e) { + // Parameter is optional + SimpleLogger.debug("[OpenSearch] Range string parameter " + parameterName + " not found"); + } + return null; + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + /** + * Extracts the rewrite parameter from a prefix query object. + */ + public static String extractRewrite(Object query, String structure) { + try { + Object queryObj = query.getClass().getMethod(structure).invoke(query); + try { + Object rewrite = queryObj.getClass().getMethod("rewrite").invoke(queryObj); + if (rewrite != null) { + return (String) rewrite; + } + } catch (NoSuchMethodException e) { + // rewrite parameter is optional + SimpleLogger.debug("[OpenSearch] rewrite parameter not found"); + } + return null; + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + /** + * Extracts the field parameter from an exists query object. + */ + public static String extractExistsField(Object query, String structure) { + try { + Object exists = query.getClass().getMethod(structure).invoke(query); + Object field = exists.getClass().getMethod("field").invoke(exists); + return (String) field; + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + /** + * Extracts integer parameter from a query object. + */ + public static Integer extractIntegerParameter(Object query, String structure, String parameterName) { + try { + Object queryObj = query.getClass().getMethod(structure).invoke(query); + try { + Object param = queryObj.getClass().getMethod(parameterName).invoke(queryObj); + if (param != null) { + return ((Number) param).intValue(); + } + } catch (NoSuchMethodException e) { + SimpleLogger.debug("[OpenSearch] Integer parameter " + parameterName + " not found"); + } + return null; + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + /** + * Extracts string parameter from a query object. + */ + public static String extractStringParameter(Object query, String structure, String parameterName) { + try { + Object queryObj = query.getClass().getMethod(structure).invoke(query); + try { + Object param = queryObj.getClass().getMethod(parameterName).invoke(queryObj); + if (param != null) { + return (String) param; + } + } catch (NoSuchMethodException e) { + SimpleLogger.debug("[OpenSearch] String parameter " + parameterName + " not found"); + } + return null; + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + /** + * Extracts boolean parameter from a query object. + */ + public static Boolean extractBooleanParameter(Object query, String structure, String parameterName) { + try { + Object queryObj = query.getClass().getMethod(structure).invoke(query); + try { + Object param = queryObj.getClass().getMethod(parameterName).invoke(queryObj); + if (param != null) { + return (Boolean) param; + } + } catch (NoSuchMethodException e) { + SimpleLogger.debug("[OpenSearch] Boolean parameter " + parameterName + " not found"); + } + return null; + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + /** + * Extracts nested query list from bool query clauses. + */ + public static java.util.List extractBoolClause(Object query, String structure, String clauseName) { + try { + Object bool = query.getClass().getMethod(structure).invoke(query); + try { + Object clause = bool.getClass().getMethod(clauseName).invoke(bool); + if (clause instanceof java.util.List) { + return (java.util.List) clause; + } + } catch (NoSuchMethodException e) { + SimpleLogger.debug("[OpenSearch] Bool clause " + clauseName + " not found"); + } + return new java.util.ArrayList<>(); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + /** * Extracts the value from a field by its type (Double, Long, Boolean, String, Null). */ diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/utils/ParameterExtractor.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/utils/ParameterExtractor.java new file mode 100644 index 0000000000..a5a83eb93d --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/opensearch/utils/ParameterExtractor.java @@ -0,0 +1,187 @@ +package org.evomaster.client.java.controller.opensearch.utils; + +import org.evomaster.client.java.controller.opensearch.operations.CommonQueryParameters; + +import static org.evomaster.client.java.controller.opensearch.utils.OpenSearchQueryHelper.*; + +/** + * Utility class to extract common parameters and reduce code duplication in selectors. + */ +public class ParameterExtractor { + + /** + * Extract common query parameters (boost, name, rewrite, case_insensitive) from a query object. + */ + public static CommonQueryParameters extractCommonParameters(Object query, String structure) { + return new CommonQueryParameters.Builder() + .boost(extractBoost(query, structure)) + .name(extractQueryName(query, structure)) + .rewrite(extractRewrite(query, structure)) + .caseInsensitive(extractCaseInsensitive(query, structure)) + .build(); + } + + /** + * Extract field name and value for field-value operations. + */ + public static FieldValueParams extractFieldValueParams(Object query, String structure) { + String fieldName = extractFieldName(query, structure); + String value = (String) extractFieldValue(query, structure); + CommonQueryParameters commonParams = extractCommonParameters(query, structure); + + return new FieldValueParams(fieldName, value, commonParams); + } + + /** + * Extract fuzzy-specific parameters. + */ + public static FuzzyParams extractFuzzyParams(Object query, String structure) { + FieldValueParams baseParams = extractFieldValueParams(query, structure); + + Integer fuzziness = extractIntegerParameter(query, structure, "fuzziness"); + Integer maxExpansions = extractIntegerParameter(query, structure, "maxExpansions"); + Integer prefixLength = extractIntegerParameter(query, structure, "prefixLength"); + Boolean transpositions = extractBooleanParameter(query, structure, "transpositions"); + + return new FuzzyParams(baseParams, fuzziness, maxExpansions, prefixLength, transpositions); + } + + /** + * Extract regexp-specific parameters. + */ + public static RegexpParams extractRegexpParams(Object query, String structure) { + FieldValueParams baseParams = extractFieldValueParams(query, structure); + + String flags = extractStringParameter(query, structure, "flags"); + Integer maxDeterminizedStates = extractIntegerParameter(query, structure, "maxDeterminizedStates"); + + return new RegexpParams(baseParams, flags, maxDeterminizedStates); + } + + /** + * Extract match-specific parameters. + */ + public static MatchParams extractMatchParams(Object query, String structure) { + FieldValueParams baseParams = extractFieldValueParams(query, structure); + + String operator = extractStringParameter(query, structure, "operator"); + Integer minimumShouldMatch = extractIntegerParameter(query, structure, "minimumShouldMatch"); + String fuzziness = extractStringParameter(query, structure, "fuzziness"); + Integer prefixLength = extractIntegerParameter(query, structure, "prefixLength"); + Integer maxExpansions = extractIntegerParameter(query, structure, "maxExpansions"); + String analyzer = extractStringParameter(query, structure, "analyzer"); + Boolean fuzzyTranspositions = extractBooleanParameter(query, structure, "fuzzyTranspositions"); + Boolean lenient = extractBooleanParameter(query, structure, "lenient"); + Boolean zeroTermsQuery = extractBooleanParameter(query, structure, "zeroTermsQuery"); + + return new MatchParams(baseParams, operator, minimumShouldMatch, fuzziness, prefixLength, + maxExpansions, analyzer, fuzzyTranspositions, lenient, zeroTermsQuery); + } + + /** + * Extract range-specific parameters. + */ + public static RangeParams extractRangeParams(Object query, String structure) { + String fieldName = extractFieldName(query, structure); + Object gte = extractRangeParameter(query, structure, "gte"); + Object gt = extractRangeParameter(query, structure, "gt"); + Object lte = extractRangeParameter(query, structure, "lte"); + Object lt = extractRangeParameter(query, structure, "lt"); + String format = extractRangeStringParameter(query, structure, "format"); + String relation = extractRangeStringParameter(query, structure, "relation"); + Float boost = extractBoost(query, structure); + String timeZone = extractRangeStringParameter(query, structure, "timeZone"); + + return new RangeParams(fieldName, gte, gt, lte, lt, format, relation, boost, timeZone); + } + + // Parameter holder classes + public static class FieldValueParams { + public final String fieldName; + public final String value; + public final CommonQueryParameters commonParams; + + public FieldValueParams(String fieldName, String value, CommonQueryParameters commonParams) { + this.fieldName = fieldName; + this.value = value; + this.commonParams = commonParams; + } + } + + public static class FuzzyParams { + public final FieldValueParams baseParams; + public final Integer fuzziness; + public final Integer maxExpansions; + public final Integer prefixLength; + public final Boolean transpositions; + + public FuzzyParams(FieldValueParams baseParams, Integer fuzziness, Integer maxExpansions, + Integer prefixLength, Boolean transpositions) { + this.baseParams = baseParams; + this.fuzziness = fuzziness; + this.maxExpansions = maxExpansions; + this.prefixLength = prefixLength; + this.transpositions = transpositions; + } + } + + public static class RegexpParams { + public final FieldValueParams baseParams; + public final String flags; + public final Integer maxDeterminizedStates; + + public RegexpParams(FieldValueParams baseParams, String flags, Integer maxDeterminizedStates) { + this.baseParams = baseParams; + this.flags = flags; + this.maxDeterminizedStates = maxDeterminizedStates; + } + } + + public static class MatchParams { + public final FieldValueParams baseParams; + public final String operator; + public final Integer minimumShouldMatch; + public final String fuzziness; + public final Integer prefixLength; + public final Integer maxExpansions; + public final String analyzer; + public final Boolean fuzzyTranspositions; + public final Boolean lenient; + public final Boolean zeroTermsQuery; + + public MatchParams(FieldValueParams baseParams, String operator, Integer minimumShouldMatch, + String fuzziness, Integer prefixLength, Integer maxExpansions, String analyzer, + Boolean fuzzyTranspositions, Boolean lenient, Boolean zeroTermsQuery) { + this.baseParams = baseParams; + this.operator = operator; + this.minimumShouldMatch = minimumShouldMatch; + this.fuzziness = fuzziness; + this.prefixLength = prefixLength; + this.maxExpansions = maxExpansions; + this.analyzer = analyzer; + this.fuzzyTranspositions = fuzzyTranspositions; + this.lenient = lenient; + this.zeroTermsQuery = zeroTermsQuery; + } + } + + public static class RangeParams { + public final String fieldName; + public final Object gte, gt, lte, lt; + public final String format, relation, timeZone; + public final Float boost; + + public RangeParams(String fieldName, Object gte, Object gt, Object lte, Object lt, + String format, String relation, Float boost, String timeZone) { + this.fieldName = fieldName; + this.gte = gte; + this.gt = gt; + this.lte = lte; + this.lt = lt; + this.format = format; + this.relation = relation; + this.boost = boost; + this.timeZone = timeZone; + } + } +} diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/opensearch/OpenSearchHeuristicsCalculatorDirectTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/opensearch/OpenSearchHeuristicsCalculatorDirectTest.java new file mode 100644 index 0000000000..25067fff7d --- /dev/null +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/opensearch/OpenSearchHeuristicsCalculatorDirectTest.java @@ -0,0 +1,676 @@ +package org.evomaster.client.java.controller.opensearch; + +import org.evomaster.client.java.controller.opensearch.operations.*; +import org.junit.jupiter.api.Test; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for OpenSearchHeuristicsCalculator methods. + */ +class OpenSearchHeuristicsCalculatorDirectTest { + + private final OpenSearchHeuristicsCalculator calculator = new OpenSearchHeuristicsCalculator(); + + // Test data creation helpers + private Map createDoc(String field, Object value) { + Map doc = new HashMap<>(); + doc.put(field, value); + return doc; + } + + private Map createDoc(Map fields) { + return new HashMap<>(fields); + } + + @Test + void testCalculateDistanceForEquals() { + TermOperation operation = new TermOperation<>("name", "john"); + Map docMatch = createDoc("name", "john"); + Map docNoMatch = createDoc("name", "jane"); + Map docMissing = createDoc("age", 25); + + double distanceMatch = calculator.calculateDistance(operation, docMatch); + double distanceNoMatch = calculator.calculateDistance(operation, docNoMatch); + double distanceMissing = calculator.calculateDistance(operation, docMissing); + + assertEquals(0.0, distanceMatch, "Exact match should return 0 distance"); + assertTrue(distanceNoMatch > 0, "Different values should return positive distance"); + assertEquals(Double.MAX_VALUE, distanceMissing, "Missing field should return MAX_VALUE"); + } + + @Test + void testCalculateDistanceForEqualsNumeric() { + TermOperation operation = new TermOperation<>("age", 25); + Map doc = createDoc("age", 30); + + double distance = calculator.calculateDistance(operation, doc); + assertEquals(5.0, distance, "Numeric difference should be absolute value"); + } + + @Test + void testCalculateDistanceForEqualsCaseInsensitive() { + TermOperation operation = new TermOperation<>("name", "JOHN", true); + Map doc = createDoc("name", "john"); + + double distance = calculator.calculateDistance(operation, doc); + assertEquals(0.0, distance, "Case insensitive match should return 0 distance"); + } + + @Test + void testCalculateDistanceForTerms() { + TermsOperation operation = new TermsOperation<>("category", Arrays.asList("sports", "music", "tech")); + Map docMatch = createDoc("category", "sports"); + Map docNoMatch = createDoc("category", "science"); + Map docMissing = createDoc("age", 25); + + double distanceMatch = calculator.calculateDistance(operation, docMatch); + double distanceNoMatch = calculator.calculateDistance(operation, docNoMatch); + double distanceMissing = calculator.calculateDistance(operation, docMissing); + + assertEquals(0.0, distanceMatch, "Matching any term should return 0 distance"); + assertTrue(distanceNoMatch > 0, "No matching terms should return positive distance"); + assertEquals(Double.MAX_VALUE, distanceMissing, "Missing field should return MAX_VALUE"); + } + + @Test + void testCalculateDistanceForTermsSet() { + TermsSetOperation operation = new TermsSetOperation<>("tags", + Arrays.asList("java", "spring", "database"), "min_match"); + Map docSatisfied = new HashMap<>(); + docSatisfied.put("tags", "java"); + docSatisfied.put("min_match", 1); + + Map docNotSatisfied = new HashMap<>(); + docNotSatisfied.put("tags", "python"); + docNotSatisfied.put("min_match", 2); + + double distanceSatisfied = calculator.calculateDistance(operation, docSatisfied); + double distanceNotSatisfied = calculator.calculateDistance(operation, docNotSatisfied); + + assertEquals(0.0, distanceSatisfied, "Meeting minimum requirement should return 0 distance"); + assertTrue(distanceNotSatisfied > 0, "Not meeting minimum requirement should return positive distance"); + } + + @Test + void testCalculateDistanceForRangeGte() { + RangeOperation operation = new RangeOperation("age", 18, null, null, null); + Map docWithin = createDoc("age", 25); + Map docViolation = createDoc("age", 15); + + double distanceWithin = calculator.calculateDistance(operation, docWithin); + double distanceViolation = calculator.calculateDistance(operation, docViolation); + + assertEquals(0.0, distanceWithin, "Value >= lower bound should return 0 distance"); + assertEquals(3.0, distanceViolation, "Value < lower bound should return positive distance"); + } + + @Test + void testCalculateDistanceForRangeGt() { + RangeOperation operation = new RangeOperation("score", null, 50, null, null); + Map docWithin = createDoc("score", 60); + Map docViolation = createDoc("score", 50); + + double distanceWithin = calculator.calculateDistance(operation, docWithin); + double distanceViolation = calculator.calculateDistance(operation, docViolation); + + assertEquals(0.0, distanceWithin, "Value > lower bound should return 0 distance"); + assertEquals(1.0, distanceViolation, "Value <= lower bound should return positive distance"); + } + + @Test + void testCalculateDistanceForRangeLte() { + RangeOperation operation = new RangeOperation("price", null, null, 100, null); + Map docWithin = createDoc("price", 80); + Map docViolation = createDoc("price", 120); + + double distanceWithin = calculator.calculateDistance(operation, docWithin); + double distanceViolation = calculator.calculateDistance(operation, docViolation); + + assertEquals(0.0, distanceWithin, "Value <= upper bound should return 0 distance"); + assertEquals(20.0, distanceViolation, "Value > upper bound should return positive distance"); + } + + @Test + void testCalculateDistanceForRangeLt() { + RangeOperation operation = new RangeOperation("temperature", null, null, null, 100); + Map docWithin = createDoc("temperature", 80); + Map docViolation = createDoc("temperature", 100); + + double distanceWithin = calculator.calculateDistance(operation, docWithin); + double distanceViolation = calculator.calculateDistance(operation, docViolation); + + assertEquals(0.0, distanceWithin, "Value < upper bound should return 0 distance"); + assertEquals(1.0, distanceViolation, "Value >= upper bound should return positive distance"); + } + + @Test + void testCalculateDistanceForRangeMissingField() { + RangeOperation operation = new RangeOperation("age", 18, null, null, null); + Map doc = createDoc("name", "john"); + + double distance = calculator.calculateDistance(operation, doc); + assertEquals(Double.MAX_VALUE, distance, "Missing field should return MAX_VALUE"); + } + + @Test + void testCalculateDistanceForPrefix() { + PrefixOperation operation = new PrefixOperation("title", "hello"); + Map docMatch = createDoc("title", "hello world"); + Map docNoMatch = createDoc("title", "goodbye world"); + + double distanceMatch = calculator.calculateDistance(operation, docMatch); + double distanceNoMatch = calculator.calculateDistance(operation, docNoMatch); + + assertEquals(0.0, distanceMatch, "Matching prefix should return 0 distance"); + assertTrue(distanceNoMatch > 0, "Non-matching prefix should return positive distance"); + } + + @Test + void testCalculateDistanceForPrefixCaseInsensitive() { + PrefixOperation operation = new PrefixOperation("title", "HELLO", CommonQueryParameters.withCaseInsensitive(true)); + Map doc = createDoc("title", "hello world"); + + double distance = calculator.calculateDistance(operation, doc); + assertEquals(0.0, distance, "Case insensitive prefix match should return 0 distance"); + } + + @Test + void testCalculateDistanceForExists() { + ExistsOperation operation = new ExistsOperation("name"); + Map docExists = createDoc("name", "john"); + Map docMissing = createDoc("age", 25); + Map docNull = createDoc("name", null); + + double distanceExists = calculator.calculateDistance(operation, docExists); + double distanceMissing = calculator.calculateDistance(operation, docMissing); + double distanceNull = calculator.calculateDistance(operation, docNull); + + assertEquals(0.0, distanceExists, "Existing field should return 0 distance"); + assertEquals(1.0, distanceMissing, "Missing field should return 1.0 distance"); + assertEquals(1.0, distanceNull, "Null field should return 1.0 distance"); + } + + @Test + void testCalculateDistanceForIds() { + IdsOperation operation = new IdsOperation(Arrays.asList("doc1", "doc2", "doc3")); + Map docMatch = createDoc("_id", "doc2"); + Map docNoMatch = createDoc("_id", "doc5"); + Map docMissing = createDoc("name", "john"); + + double distanceMatch = calculator.calculateDistance(operation, docMatch); + double distanceNoMatch = calculator.calculateDistance(operation, docNoMatch); + double distanceMissing = calculator.calculateDistance(operation, docMissing); + + assertEquals(0.0, distanceMatch, "Matching ID should return 0 distance"); + assertTrue(distanceNoMatch > 0, "Non-matching ID should return positive distance"); + assertEquals(Double.MAX_VALUE, distanceMissing, "Missing _id field should return MAX_VALUE"); + } + + @Test + void testCalculateDistanceForFuzzy() { + FuzzyOperation operation = new FuzzyOperation("name", "john", null, 2, null, null, true, null); + Map docExact = createDoc("name", "john"); + Map docWithin = createDoc("name", "johnn"); // 1 edit distance + Map docExceeds = createDoc("name", "jones"); // > 2 edit distance (likely) + + double distanceExact = calculator.calculateDistance(operation, docExact); + double distanceWithin = calculator.calculateDistance(operation, docWithin); + double distanceExceeds = calculator.calculateDistance(operation, docExceeds); + + assertEquals(0.0, distanceExact, "Exact match should return 0 distance"); + assertEquals(0.0, distanceWithin, "Within fuzziness should return 0 distance"); + assertTrue(distanceExceeds > 0, "Exceeding fuzziness should return positive distance"); + } + + @Test + void testCalculateDistanceForWildcard() { + WildcardOperation operation = new WildcardOperation("title", "hel*world"); + Map docMatch = createDoc("title", "hello world"); + Map docNoMatch = createDoc("title", "goodbye earth"); + + double distanceMatch = calculator.calculateDistance(operation, docMatch); + double distanceNoMatch = calculator.calculateDistance(operation, docNoMatch); + + assertEquals(0.0, distanceMatch, "Matching wildcard should return 0 distance"); + assertTrue(distanceNoMatch > 0, "Non-matching wildcard should return positive distance"); + } + + @Test + void testCalculateDistanceForRegexp() { + RegexpOperation operation = new RegexpOperation("email", ".*@gmail\\.com", null, false, null, null, null); + Map docMatch = createDoc("email", "user@gmail.com"); + Map docNoMatch = createDoc("email", "user@yahoo.com"); + + double distanceMatch = calculator.calculateDistance(operation, docMatch); + double distanceNoMatch = calculator.calculateDistance(operation, docNoMatch); + + assertEquals(0.0, distanceMatch, "Matching regex should return 0 distance"); + assertTrue(distanceNoMatch > 0, "Non-matching regex should return positive distance"); + } + + @Test + void testCalculateDistanceForMatchSingle() { + MatchOperation operation = new MatchOperation("content", "java", CommonQueryParameters.empty(), "or", null, null, null, null, null, null, null, null, null); + Map doc = createDoc("content", "I love java programming"); + + double distance = calculator.calculateDistance(operation, doc); + assertEquals(0.0, distance, "Matching token should return 0 distance"); + } + + @Test + void testCalculateDistanceForMatchAnd() { + MatchOperation operation = new MatchOperation("content", "java programming", CommonQueryParameters.empty(), "and", null, null, null, null, null, null, null, null, null); + Map doc = createDoc("content", "I love java and programming"); + + double distance = calculator.calculateDistance(operation, doc); + assertEquals(0.0, distance, "All tokens matching should return 0 distance"); + } + + @Test + void testCalculateDistanceForMatchOr() { + MatchOperation operation = new MatchOperation("content", "java python", CommonQueryParameters.empty(), "or", null, null, null, null, null, null, null, null, null); + Map doc = createDoc("content", "I love java programming"); + + double distance = calculator.calculateDistance(operation, doc); + assertEquals(0.0, distance, "Any token matching should return 0 distance"); + } + + @Test + void testCalculateDistanceForBoolMust() { + TermOperation term1 = new TermOperation<>("category", "tech"); + TermOperation term2 = new TermOperation<>("score", 85); + BoolOperation operation = new BoolOperation( + Arrays.asList(term1, term2), // must + null, // must_not + null, // should + null // filter + ); + + Map doc = new HashMap<>(); + doc.put("category", "tech"); + doc.put("score", 85); + + double distance = calculator.calculateDistance(operation, doc); + assertEquals(0.0, distance, "All must clauses matching should return 0 distance"); + } + + @Test + void testCalculateDistanceForBoolMustNot() { + TermOperation term1 = new TermOperation<>("status", "deleted"); + BoolOperation operation = new BoolOperation( + null, // must + Arrays.asList(term1), // must_not + null, // should + null // filter + ); + + Map docGood = createDoc("status", "active"); + Map docBad = createDoc("status", "deleted"); + + double distanceGood = calculator.calculateDistance(operation, docGood); + double distanceBad = calculator.calculateDistance(operation, docBad); + + assertEquals(0.0, distanceGood, "Must_not clause not matching should return 0 distance"); + assertEquals(Double.MAX_VALUE, distanceBad, "Must_not clause matching should return MAX_VALUE"); + } + + @Test + void testCalculateDistanceForBoolShould() { + TermOperation term1 = new TermOperation<>("category", "tech"); + TermOperation term2 = new TermOperation<>("category", "science"); + BoolOperation operation = new BoolOperation( + null, // must + null, // must_not + Arrays.asList(term1, term2), // should + null, // filter + 1, // minimum_should_match + null // boost + ); + + Map doc = createDoc("category", "tech"); + + double distance = calculator.calculateDistance(operation, doc); + assertEquals(0.0, distance, "Meeting minimum_should_match should return 0 distance"); + } + + @Test + void testUnknownOperationType() { + QueryOperation unknownOperation = new QueryOperation() {}; + Map doc = createDoc("field", "value"); + + double distance = calculator.calculateDistance(unknownOperation, doc); + assertEquals(Double.MAX_VALUE, distance, "Unknown operation type should return MAX_VALUE"); + } + + @Test + void testRangeOperationWithStringValue() { + RangeOperation operation = new RangeOperation("age", 18, null, null, null); + Map doc = createDoc("age", "twenty-five"); + + double distance = calculator.calculateDistance(operation, doc); + assertEquals(Double.MAX_VALUE, distance, "Non-numeric value in range operation should return MAX_VALUE"); + } + + @Test + void testCalculateDistanceForTermBoolean() { + TermOperation operation = new TermOperation<>("active", true); + Map docMatch = createDoc("active", true); + Map docNoMatch = createDoc("active", false); + + double distanceMatch = calculator.calculateDistance(operation, docMatch); + double distanceNoMatch = calculator.calculateDistance(operation, docNoMatch); + + assertEquals(0.0, distanceMatch, "Matching boolean should return 0 distance"); + assertEquals(1.0, distanceNoMatch, "Different boolean should return 1.0 distance"); + } + + @Test + void testCalculateDistanceForTermsNumeric() { + TermsOperation operation = new TermsOperation<>("score", Arrays.asList(85, 90, 95)); + Map docMatch = createDoc("score", 90); + Map docNoMatch = createDoc("score", 75); + + double distanceMatch = calculator.calculateDistance(operation, docMatch); + double distanceNoMatch = calculator.calculateDistance(operation, docNoMatch); + + assertEquals(0.0, distanceMatch, "Matching numeric term should return 0 distance"); + assertTrue(distanceNoMatch > 0, "Non-matching numeric should return positive distance"); + } + + @Test + void testCalculateDistanceForRangeBothBounds() { + RangeOperation operation = new RangeOperation("age", 18, null, 65, null); + Map docWithin = createDoc("age", 30); + Map docBelowLower = createDoc("age", 15); + Map docAboveUpper = createDoc("age", 70); + + double distanceWithin = calculator.calculateDistance(operation, docWithin); + double distanceBelowLower = calculator.calculateDistance(operation, docBelowLower); + double distanceAboveUpper = calculator.calculateDistance(operation, docAboveUpper); + + assertEquals(0.0, distanceWithin, "Value within range should return 0 distance"); + assertEquals(3.0, distanceBelowLower, "Value below lower bound should return positive distance"); + assertEquals(5.0, distanceAboveUpper, "Value above upper bound should return positive distance"); + } + + @Test + void testCalculateDistanceForPrefixMissingField() { + PrefixOperation operation = new PrefixOperation("title", "hello"); + Map doc = createDoc("content", "some text"); + + double distance = calculator.calculateDistance(operation, doc); + assertEquals(Double.MAX_VALUE, distance, "Missing field should return MAX_VALUE"); + } + + @Test + void testCalculateDistanceForWildcardQuestionMark() { + WildcardOperation operation = new WildcardOperation("code", "test?123"); + Map docMatch = createDoc("code", "test1123"); + Map docNoMatch = createDoc("code", "testABC"); + + double distanceMatch = calculator.calculateDistance(operation, docMatch); + double distanceNoMatch = calculator.calculateDistance(operation, docNoMatch); + + assertEquals(0.0, distanceMatch, "Matching wildcard with ? should return 0 distance"); + assertTrue(distanceNoMatch > 0, "Non-matching wildcard should return positive distance"); + } + + @Test + void testCalculateDistanceForWildcardCaseInsensitive() { + WildcardOperation operation = new WildcardOperation("title", "HEL*", CommonQueryParameters.withCaseInsensitive(true)); + Map doc = createDoc("title", "hello world"); + + double distance = calculator.calculateDistance(operation, doc); + assertEquals(0.0, distance, "Case insensitive wildcard match should return 0 distance"); + } + + @Test + void testCalculateDistanceForRegexpCaseInsensitive() { + RegexpOperation operation = new RegexpOperation("name", "JO.*", null, true, null, null, null); + Map doc = createDoc("name", "john"); + + double distance = calculator.calculateDistance(operation, doc); + assertEquals(0.0, distance, "Case insensitive regex match should return 0 distance"); + } + + @Test + void testCalculateDistanceForRegexpInvalidPattern() { + RegexpOperation operation = new RegexpOperation("field", "[invalid(", null, false, null, null, null); + Map doc = createDoc("field", "value"); + + double distance = calculator.calculateDistance(operation, doc); + assertEquals(Double.MAX_VALUE, distance, "Invalid regex pattern should return MAX_VALUE"); + } + + @Test + void testCalculateDistanceForFuzzyAutoFuzziness() { + // Testing AUTO fuzziness with different term lengths + FuzzyOperation shortTerm = new FuzzyOperation("name", "ab", null, null, null, null, true, null); + FuzzyOperation mediumTerm = new FuzzyOperation("name", "test", null, null, null, null, true, null); + FuzzyOperation longTerm = new FuzzyOperation("name", "testing", null, null, null, null, true, null); + + Map doc1 = createDoc("name", "ab"); // exact match + Map doc2 = createDoc("name", "tess"); // 1 edit from "test" + Map doc3 = createDoc("name", "testang"); // 1 edit from "testing" + + assertEquals(0.0, calculator.calculateDistance(shortTerm, doc1)); + assertEquals(0.0, calculator.calculateDistance(mediumTerm, doc2)); // AUTO fuzziness = 1 for length 4 + assertEquals(0.0, calculator.calculateDistance(longTerm, doc3)); // AUTO fuzziness = 2 for length > 5 + } + + @Test + void testCalculateDistanceForFuzzyWithoutTranspositions() { + FuzzyOperation operation = new FuzzyOperation("word", "test", null, 2, null, null, false, null); + Map doc = createDoc("word", "tset"); // transposition of 'es' + + double distance = calculator.calculateDistance(operation, doc); + // Without transpositions, this is 2 substitutions, within fuzziness of 2 + assertEquals(0.0, distance, "Within fuzziness without transpositions should return 0 distance"); + } + + @Test + void testCalculateDistanceForMatchMissingField() { + MatchOperation operation = new MatchOperation("content", "java", CommonQueryParameters.empty(), "or", null, null, null, null, null, null, null, null, null); + Map doc = createDoc("title", "some title"); + + double distance = calculator.calculateDistance(operation, doc); + assertEquals(Double.MAX_VALUE, distance, "Missing field in match should return MAX_VALUE"); + } + + @Test + void testCalculateDistanceForMatchAndPartialMatch() { + MatchOperation operation = new MatchOperation("content", "java spring", CommonQueryParameters.empty(), "and", null, null, null, null, null, null, null, null, null); + Map doc = createDoc("content", "I love java"); // Missing "spring" + + double distance = calculator.calculateDistance(operation, doc); + assertTrue(distance > 0, "Partial match with AND operator should return positive distance"); + } + + @Test + void testCalculateDistanceForBoolComplexQuery() { + // Create a complex bool query with multiple clause types + TermOperation mustTerm1 = new TermOperation<>("status", "active"); + TermOperation mustTerm2 = new TermOperation<>("priority", 5); + TermOperation mustNotTerm = new TermOperation<>("deleted", "true"); + TermOperation shouldTerm1 = new TermOperation<>("category", "tech"); + TermOperation shouldTerm2 = new TermOperation<>("category", "science"); + + BoolOperation operation = new BoolOperation( + Arrays.asList(mustTerm1, mustTerm2), // must + Arrays.asList(mustNotTerm), // must_not + Arrays.asList(shouldTerm1, shouldTerm2), // should + null, // filter + 1, // minimum_should_match + null // boost + ); + + Map docMatch = new HashMap<>(); + docMatch.put("status", "active"); + docMatch.put("priority", 5); + docMatch.put("deleted", "false"); + docMatch.put("category", "tech"); + + double distance = calculator.calculateDistance(operation, docMatch); + assertEquals(0.0, distance, "All conditions satisfied should return 0 distance"); + } + + @Test + void testCalculateDistanceForBoolOnlyFilter() { + TermOperation filterTerm = new TermOperation<>("status", "published"); + BoolOperation operation = new BoolOperation( + null, // must + null, // must_not + null, // should + Arrays.asList(filterTerm) // filter + ); + + Map docMatch = createDoc("status", "published"); + Map docNoMatch = createDoc("status", "draft"); + + double distanceMatch = calculator.calculateDistance(operation, docMatch); + double distanceNoMatch = calculator.calculateDistance(operation, docNoMatch); + + assertEquals(0.0, distanceMatch, "Matching filter should return 0 distance"); + assertEquals(Double.MAX_VALUE, distanceNoMatch, "Non-matching filter should return MAX_VALUE"); + } + + @Test + void testCalculateDistanceForBoolShouldMinimumNotMet() { + TermOperation term1 = new TermOperation<>("tag", "java"); + TermOperation term2 = new TermOperation<>("tag", "python"); + TermOperation term3 = new TermOperation<>("tag", "rust"); + + BoolOperation operation = new BoolOperation( + null, // must + null, // must_not + Arrays.asList(term1, term2, term3), // should + null, // filter + 2, // minimum_should_match = 2 + null + ); + + Map docOneMatch = createDoc("tag", "java"); // Only 1 match + + double distance = calculator.calculateDistance(operation, docOneMatch); + assertTrue(distance > 0, "Not meeting minimum_should_match should return positive distance"); + } + + @Test + void testCalculateDistanceForTermsSetRequirementSatisfied() { + TermsSetOperation operation = new TermsSetOperation<>("skills", + Arrays.asList("java", "python", "sql", "docker"), "required_skills"); + + Map doc = new HashMap<>(); + doc.put("skills", "java"); // matches 1 term + doc.put("required_skills", 1); // requires at least 1 + + double distance = calculator.calculateDistance(operation, doc); + assertEquals(0.0, distance, "Meeting minimum requirement should return 0 distance"); + } + + @Test + void testCalculateDistanceForTermsSetRequirementNotSatisfied() { + TermsSetOperation operation = new TermsSetOperation<>("skills", + Arrays.asList("java", "python", "sql"), "required_skills"); + + Map doc = new HashMap<>(); + doc.put("skills", "javascript"); // matches 0 terms + doc.put("required_skills", 2); // requires at least 2 + + double distance = calculator.calculateDistance(operation, doc); + assertTrue(distance > 0, "Not meeting minimum requirement should return positive distance"); + } + + @Test + void testCalculateDistanceForIdsMissingField() { + IdsOperation operation = new IdsOperation(Arrays.asList("doc1", "doc2")); + Map doc = createDoc("name", "john"); // No _id field + + double distance = calculator.calculateDistance(operation, doc); + assertEquals(Double.MAX_VALUE, distance, "Missing _id field should return MAX_VALUE"); + } + + @Test + void testCalculateDistanceForFuzzyMissingField() { + FuzzyOperation operation = new FuzzyOperation("name", "john", null, 2, null, null, true, null); + Map doc = createDoc("title", "some title"); + + double distance = calculator.calculateDistance(operation, doc); + assertEquals(Double.MAX_VALUE, distance, "Missing field should return MAX_VALUE"); + } + + @Test + void testCalculateDistanceForWildcardMissingField() { + WildcardOperation operation = new WildcardOperation("pattern", "test*"); + Map doc = createDoc("other", "value"); + + double distance = calculator.calculateDistance(operation, doc); + assertEquals(Double.MAX_VALUE, distance, "Missing field should return MAX_VALUE"); + } + + @Test + void testCalculateDistanceForRegexpMissingField() { + RegexpOperation operation = new RegexpOperation("email", ".*@example\\.com", null, false, null, null, null); + Map doc = createDoc("username", "john"); + + double distance = calculator.calculateDistance(operation, doc); + assertEquals(Double.MAX_VALUE, distance, "Missing field should return MAX_VALUE"); + } + + @Test + void testCalculateDistanceForRangeNumericParseable() { + RangeOperation operation = new RangeOperation("age", 18, null, null, null); + Map doc = createDoc("age", "25"); // String that can be parsed as number + + double distance = calculator.calculateDistance(operation, doc); + assertEquals(0.0, distance, "Parseable numeric string should be handled correctly"); + } + + @Test + void testCalculateDistanceForTermsEmpty() { + TermsOperation operation = new TermsOperation<>("category", Arrays.asList()); + Map doc = createDoc("category", "tech"); + + double distance = calculator.calculateDistance(operation, doc); + // With empty terms list, no term can match + assertEquals(Double.MAX_VALUE, distance, "Empty terms list should return MAX_VALUE"); + } + + @Test + void testCalculateDistanceForBoolEmptyMust() { + BoolOperation operation = new BoolOperation( + new ArrayList<>(), // empty must + null, + null, + null + ); + + Map doc = createDoc("field", "value"); + + double distance = calculator.calculateDistance(operation, doc); + assertEquals(0.0, distance, "Empty must clause should return 0 distance"); + } + + @Test + void testCalculateDistanceForBoolMustFailure() { + TermOperation term1 = new TermOperation<>("field1", "value1"); + TermOperation term2 = new TermOperation<>("field2", "value2"); + + BoolOperation operation = new BoolOperation( + Arrays.asList(term1, term2), + null, + null, + null + ); + + Map doc = new HashMap<>(); + doc.put("field1", "value1"); + // field2 is missing + + double distance = calculator.calculateDistance(operation, doc); + assertEquals(Double.MAX_VALUE, distance, "Must clause with missing field should return MAX_VALUE"); + } +} diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/opensearch/utils/OpenSearchQueryHelperTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/opensearch/utils/OpenSearchQueryHelperTest.java new file mode 100644 index 0000000000..182777a9b7 --- /dev/null +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/opensearch/utils/OpenSearchQueryHelperTest.java @@ -0,0 +1,410 @@ +package org.evomaster.client.java.controller.opensearch.utils; + +import org.junit.jupiter.api.Test; +import org.opensearch.client.opensearch._types.FieldValue; +import org.opensearch.client.opensearch._types.query_dsl.Query; +import org.opensearch.client.opensearch.core.SearchRequest; +import org.opensearch.client.json.JsonData; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for OpenSearchQueryHelper methods using actual OpenSearch query objects. + */ +class OpenSearchQueryHelperTest { + @Test + void testExtractQueryKind_Term() { + Query query = Query.of(q -> q.term(t -> t.field("name").value(FieldValue.of("test")))); + String kind = OpenSearchQueryHelper.extractQueryKind(query); + assertEquals("Term", kind); + } + + @Test + void testExtractQueryKind_Match() { + Query query = Query.of(q -> q.match(m -> m.field("description").query(FieldValue.of("hello")))); + String kind = OpenSearchQueryHelper.extractQueryKind(query); + assertEquals("Match", kind); + } + + @Test + void testExtractQueryKind_Range() { + Query query = Query.of(q -> q.range(r -> r.field("age").gte(JsonData.of(18)))); + String kind = OpenSearchQueryHelper.extractQueryKind(query); + assertEquals("Range", kind); + } + + @Test + void testExtractQueryKind_Prefix() { + Query query = Query.of(q -> q.prefix(p -> p.field("name").value("test"))); + String kind = OpenSearchQueryHelper.extractQueryKind(query); + assertEquals("Prefix", kind); + } + + @Test + void testExtractQueryKind_Exists() { + Query query = Query.of(q -> q.exists(e -> e.field("username"))); + String kind = OpenSearchQueryHelper.extractQueryKind(query); + assertEquals("Exists", kind); + } + + @Test + void testExtractQueryKind_Fuzzy() { + Query query = Query.of(q -> q.fuzzy(f -> f.field("name").value(FieldValue.of("test")))); + String kind = OpenSearchQueryHelper.extractQueryKind(query); + assertEquals("Fuzzy", kind); + } + + @Test + void testExtractQueryKind_Wildcard() { + Query query = Query.of(q -> q.wildcard(w -> w.field("pattern").value("test*"))); + String kind = OpenSearchQueryHelper.extractQueryKind(query); + assertEquals("Wildcard", kind); + } + + @Test + void testExtractQueryKind_Regexp() { + Query query = Query.of(q -> q.regexp(r -> r.field("email").value(".*@test\\.com"))); + String kind = OpenSearchQueryHelper.extractQueryKind(query); + assertEquals("Regexp", kind); + } + + @Test + void testExtractQueryKind_Bool() { + Query query = Query.of(q -> q.bool(b -> b + .must(Query.of(mq -> mq.term(t -> t.field("status").value(FieldValue.of("active"))))) + )); + String kind = OpenSearchQueryHelper.extractQueryKind(query); + assertEquals("Bool", kind); + } + + @Test + void testExtractFieldName_Term() { + Query query = Query.of(q -> q.term(t -> t.field("username").value(FieldValue.of("john")))); + String fieldName = OpenSearchQueryHelper.extractFieldName(query, "term"); + assertEquals("username", fieldName); + } + + @Test + void testExtractFieldName_DifferentFields() { + Query query1 = Query.of(q -> q.term(t -> t.field("email").value(FieldValue.of("test@example.com")))); + Query query2 = Query.of(q -> q.term(t -> t.field("age").value(FieldValue.of(25)))); + + assertEquals("email", OpenSearchQueryHelper.extractFieldName(query1, "term")); + assertEquals("age", OpenSearchQueryHelper.extractFieldName(query2, "term")); + } + + @Test + void testExtractFieldName_Range() { + Query query = Query.of(q -> q.range(r -> r.field("price").gte(JsonData.of(100)))); + String fieldName = OpenSearchQueryHelper.extractFieldName(query, "range"); + assertEquals("price", fieldName); + } + + @Test + void testExtractFieldName_Prefix() { + Query query = Query.of(q -> q.prefix(p -> p.field("title").value("hello"))); + String fieldName = OpenSearchQueryHelper.extractFieldName(query, "prefix"); + assertEquals("title", fieldName); + } + + @Test + void testExtractFieldValue_String() { + Query query = Query.of(q -> q.term(t -> t.field("name").value(FieldValue.of("Alice")))); + Object value = OpenSearchQueryHelper.extractFieldValue(query, "term"); + assertEquals("Alice", value); + } + + @Test + void testExtractFieldValue_Long() { + Query query = Query.of(q -> q.term(t -> t.field("count").value(FieldValue.of(42L)))); + Object value = OpenSearchQueryHelper.extractFieldValue(query, "term"); + assertEquals(42L, value); + } + + @Test + void testExtractFieldValue_Double() { + Query query = Query.of(q -> q.term(t -> t.field("price").value(FieldValue.of(99.99)))); + Object value = OpenSearchQueryHelper.extractFieldValue(query, "term"); + assertEquals(99.99, value); + } + + @Test + void testExtractFieldValue_Boolean() { + Query query = Query.of(q -> q.term(t -> t.field("active").value(FieldValue.of(true)))); + Object value = OpenSearchQueryHelper.extractFieldValue(query, "term"); + assertEquals(true, value); + } + + @Test + void testExtractCaseInsensitive_True() { + Query query = Query.of(q -> q.term(t -> t.field("name").value(FieldValue.of("john")).caseInsensitive(true))); + Boolean caseInsensitive = OpenSearchQueryHelper.extractCaseInsensitive(query, "term"); + assertTrue(caseInsensitive); + } + + @Test + void testExtractCaseInsensitive_False() { + Query query = Query.of(q -> q.term(t -> t.field("name").value(FieldValue.of("john")).caseInsensitive(false))); + Boolean caseInsensitive = OpenSearchQueryHelper.extractCaseInsensitive(query, "term"); + assertFalse(caseInsensitive); + } + + @Test + void testExtractCaseInsensitive_Null_DefaultsFalse() { + Query query = Query.of(q -> q.term(t -> t.field("name").value(FieldValue.of("john")))); + Boolean caseInsensitive = OpenSearchQueryHelper.extractCaseInsensitive(query, "term"); + assertFalse(caseInsensitive); + } + + @Test + void testExtractBoost_WithValue() { + Query query = Query.of(q -> q.term(t -> t.field("name").value(FieldValue.of("value")).boost(2.5f))); + Float boost = OpenSearchQueryHelper.extractBoost(query, "term"); + assertEquals(2.5f, boost); + } + + @Test + void testExtractBoost_Null() { + Query query = Query.of(q -> q.term(t -> t.field("name").value(FieldValue.of("value")))); + Float boost = OpenSearchQueryHelper.extractBoost(query, "term"); + assertNull(boost); + } + + @Test + void testExtractQueryName_WithValue() { + Query query = Query.of(q -> q.term(t -> t.field("field").value(FieldValue.of("value")).queryName("my_query_name"))); + String name = OpenSearchQueryHelper.extractQueryName(query, "term"); + assertEquals("my_query_name", name); + } + + @Test + void testExtractQueryName_Null() { + Query query = Query.of(q -> q.term(t -> t.field("field").value(FieldValue.of("value")))); + String name = OpenSearchQueryHelper.extractQueryName(query, "term"); + assertNull(name); + } + + @Test + void testExtractRewrite_Prefix_WithValue() { + Query query = Query.of(q -> q.prefix(p -> p.field("field").value("value").rewrite("constant_score"))); + String rewrite = OpenSearchQueryHelper.extractRewrite(query, "prefix"); + assertEquals("constant_score", rewrite); + } + + @Test + void testExtractRewrite_Prefix_Null() { + Query query = Query.of(q -> q.prefix(p -> p.field("field").value("value"))); + String rewrite = OpenSearchQueryHelper.extractRewrite(query, "prefix"); + assertNull(rewrite); + } + + @Test + void testExtractIdsValues_MultipleIds() { + Query query = Query.of(q -> q.ids(i -> i.values(Arrays.asList("id1", "id2", "id3")))); + List ids = OpenSearchQueryHelper.extractIdsValues(query, "ids"); + + assertNotNull(ids); + assertEquals(3, ids.size()); + assertEquals("id1", ids.get(0)); + assertEquals("id2", ids.get(1)); + assertEquals("id3", ids.get(2)); + } + + @Test + void testExtractIdsValues_SingleId() { + Query query = Query.of(q -> q.ids(i -> i.values(Arrays.asList("doc123")))); + List ids = OpenSearchQueryHelper.extractIdsValues(query, "ids"); + + assertNotNull(ids); + assertEquals(1, ids.size()); + assertEquals("doc123", ids.get(0)); + } + + @Test + void testExtractIdsValues_EmptyList() { + Query query = Query.of(q -> q.ids(i -> i.values(new ArrayList<>()))); + List ids = OpenSearchQueryHelper.extractIdsValues(query, "ids"); + + assertNotNull(ids); + assertTrue(ids.isEmpty()); + } + + @Test + void testExtractExistsField() { + Query query = Query.of(q -> q.exists(e -> e.field("username"))); + String field = OpenSearchQueryHelper.extractExistsField(query, "exists"); + assertEquals("username", field); + } + + @Test + void testExtractExistsField_DifferentFields() { + Query query1 = Query.of(q -> q.exists(e -> e.field("email"))); + Query query2 = Query.of(q -> q.exists(e -> e.field("phone"))); + + assertEquals("email", OpenSearchQueryHelper.extractExistsField(query1, "exists")); + assertEquals("phone", OpenSearchQueryHelper.extractExistsField(query2, "exists")); + } + + @Test + void testExtractBooleanParameter_CaseInsensitive_True() { + Query query = Query.of(q -> q.wildcard(w -> w.field("name").value("test*").caseInsensitive(true))); + Boolean caseInsensitive = OpenSearchQueryHelper.extractBooleanParameter(query, "wildcard", "caseInsensitive"); + assertTrue(caseInsensitive); + } + + @Test + void testExtractBooleanParameter_CaseInsensitive_False() { + Query query = Query.of(q -> q.wildcard(w -> w.field("name").value("test*").caseInsensitive(false))); + Boolean caseInsensitive = OpenSearchQueryHelper.extractBooleanParameter(query, "wildcard", "caseInsensitive"); + assertFalse(caseInsensitive); + } + + @Test + void testExtractBoolClause_Must() { + Query query = Query.of(q -> q.bool(b -> b + .must(Query.of(mq -> mq.term(t -> t.field("status").value(FieldValue.of("active"))))) + .must(Query.of(mq -> mq.range(r -> r.field("age").gte(JsonData.of(18))))) + )); + + List must = OpenSearchQueryHelper.extractBoolClause(query, "bool", "must"); + assertNotNull(must); + assertEquals(2, must.size()); + } + + @Test + void testExtractBoolClause_Should() { + Query query = Query.of(q -> q.bool(b -> b + .should(Query.of(sq -> sq.match(m -> m.field("title").query(FieldValue.of("test"))))) + )); + + List should = OpenSearchQueryHelper.extractBoolClause(query, "bool", "should"); + assertNotNull(should); + assertEquals(1, should.size()); + } + + @Test + void testExtractBoolClause_MustNot() { + Query query = Query.of(q -> q.bool(b -> b + .mustNot(Query.of(mnq -> mnq.term(t -> t.field("deleted").value(FieldValue.of(true))))) + .mustNot(Query.of(mnq -> mnq.exists(e -> e.field("archived")))) + )); + + List mustNot = OpenSearchQueryHelper.extractBoolClause(query, "bool", "mustNot"); + assertNotNull(mustNot); + assertEquals(2, mustNot.size()); + } + + @Test + void testExtractBoolClause_Filter() { + Query query = Query.of(q -> q.bool(b -> b + .filter(Query.of(fq -> fq.range(r -> r.field("price").gte(JsonData.of(10))))) + )); + + List filter = OpenSearchQueryHelper.extractBoolClause(query, "bool", "filter"); + assertNotNull(filter); + assertEquals(1, filter.size()); + } + + @Test + void testExtractBoolClause_EmptyClause() { + Query query = Query.of(q -> q.bool(b -> b)); + + List must = OpenSearchQueryHelper.extractBoolClause(query, "bool", "must"); + assertNotNull(must); + assertTrue(must.isEmpty()); + } + + @Test + void testExtractTypedFieldValue_String() { + Query query = Query.of(q -> q.term(t -> t.field("name").value(FieldValue.of("hello")))); + Object value = OpenSearchQueryHelper.extractFieldValue(query, "term"); + assertEquals("hello", value); + assertTrue(value instanceof String); + } + + @Test + void testExtractTypedFieldValue_Long() { + Query query = Query.of(q -> q.term(t -> t.field("count").value(FieldValue.of(42L)))); + Object value = OpenSearchQueryHelper.extractFieldValue(query, "term"); + assertEquals(42L, value); + assertTrue(value instanceof Long); + } + + @Test + void testExtractTypedFieldValue_Double() { + Query query = Query.of(q -> q.term(t -> t.field("price").value(FieldValue.of(3.14)))); + Object value = OpenSearchQueryHelper.extractFieldValue(query, "term"); + assertEquals(3.14, value); + assertTrue(value instanceof Double); + } + + @Test + void testExtractTypedFieldValue_Boolean() { + Query query = Query.of(q -> q.term(t -> t.field("active").value(FieldValue.of(true)))); + Object value = OpenSearchQueryHelper.extractFieldValue(query, "term"); + assertEquals(true, value); + assertTrue(value instanceof Boolean); + } + + // ===== Integration tests with SearchRequest ===== + + @Test + void testExtractFromSearchRequest_Term() { + SearchRequest request = new SearchRequest.Builder() + .index("products") + .query(q -> q.term(t -> t.field("category").value(FieldValue.of("electronics")))) + .build(); + + Query query = request.query(); + assertNotNull(query); + String kind = OpenSearchQueryHelper.extractQueryKind(query); + assertEquals("Term", kind); + + String field = OpenSearchQueryHelper.extractFieldName(query, "term"); + assertEquals("category", field); + + Object value = OpenSearchQueryHelper.extractFieldValue(query, "term"); + assertEquals("electronics", value); + } + + @Test + void testExtractFromSearchRequest_Bool() { + SearchRequest request = new SearchRequest.Builder() + .index("products") + .query(q -> q.bool(b -> b + .must(Query.of(mq -> mq.term(t -> t.field("status").value(FieldValue.of("active"))))) + .filter(Query.of(fq -> fq.range(r -> r.field("price").gte(JsonData.of(10)).lte(JsonData.of(100))))) + )) + .build(); + + Query query = request.query(); + assertNotNull(query); + String kind = OpenSearchQueryHelper.extractQueryKind(query); + assertEquals("Bool", kind); + + List must = OpenSearchQueryHelper.extractBoolClause(query, "bool", "must"); + assertEquals(1, must.size()); + + List filter = OpenSearchQueryHelper.extractBoolClause(query, "bool", "filter"); + assertEquals(1, filter.size()); + } + + @Test + void testExtractFromSearchRequest_Exists() { + SearchRequest request = new SearchRequest.Builder() + .index("users") + .query(q -> q.exists(e -> e.field("email"))) + .build(); + + Query query = request.query(); + String kind = OpenSearchQueryHelper.extractQueryKind(query); + assertEquals("Exists", kind); + + String field = OpenSearchQueryHelper.extractExistsField(query, "exists"); + assertEquals("email", field); + } +} diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/opensearch/utils/ParameterExtractorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/opensearch/utils/ParameterExtractorTest.java new file mode 100644 index 0000000000..69b22f950f --- /dev/null +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/opensearch/utils/ParameterExtractorTest.java @@ -0,0 +1,294 @@ +package org.evomaster.client.java.controller.opensearch.utils; + +import org.evomaster.client.java.controller.opensearch.operations.CommonQueryParameters; +import org.junit.jupiter.api.Test; +import org.opensearch.client.opensearch._types.FieldValue; +import org.opensearch.client.opensearch._types.query_dsl.Query; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for ParameterExtractor methods using actual OpenSearch query objects. + */ +class ParameterExtractorTest { + + @Test + void testExtractCommonParameters_Term_AllFields() { + Query query = Query.of(q -> q.term(t -> t + .field("name") + .value(FieldValue.of("value")) + .boost(2.0f) + .queryName("myQuery") + .caseInsensitive(true) + )); + + CommonQueryParameters params = ParameterExtractor.extractCommonParameters(query, "term"); + + assertNotNull(params); + assertEquals(2.0f, params.getBoost()); + assertEquals("myQuery", params.getName()); + assertNull(params.getRewrite()); + assertTrue(params.getCaseInsensitive()); + } + + @Test + void testExtractCommonParameters_Term_NullFields() { + Query query = Query.of(q -> q.term(t -> t + .field("name") + .value(FieldValue.of("value")) + )); + + CommonQueryParameters params = ParameterExtractor.extractCommonParameters(query, "term"); + + assertNotNull(params); + assertNull(params.getBoost()); + assertNull(params.getName()); + assertNull(params.getRewrite()); + assertFalse(params.getCaseInsensitive()); + } + + @Test + void testExtractCommonParameters_Term_PartialFields() { + Query query = Query.of(q -> q.term(t -> t + .field("name") + .value(FieldValue.of("value")) + .boost(1.5f) + .caseInsensitive(false) + )); + + CommonQueryParameters params = ParameterExtractor.extractCommonParameters(query, "term"); + + assertNotNull(params); + assertEquals(1.5f, params.getBoost()); + assertNull(params.getName()); + assertNull(params.getRewrite()); + assertFalse(params.getCaseInsensitive()); + } + + @Test + void testExtractCommonParameters_Prefix_WithRewrite() { + Query query = Query.of(q -> q.prefix(p -> p + .field("name") + .value("test") + .boost(1.2f) + .queryName("user_search") + .rewrite("constant_score") + )); + + CommonQueryParameters params = ParameterExtractor.extractCommonParameters(query, "prefix"); + + assertNotNull(params); + assertEquals(1.2f, params.getBoost()); + assertEquals("user_search", params.getName()); + assertEquals("constant_score", params.getRewrite()); + } + + @Test + void testExtractCommonParameters_Wildcard_CaseInsensitive() { + Query query = Query.of(q -> q.wildcard(w -> w + .field("pattern") + .value("test*") + .caseInsensitive(true) + .boost(2.0f) + )); + + CommonQueryParameters params = ParameterExtractor.extractCommonParameters(query, "wildcard"); + + assertNotNull(params); + assertEquals(2.0f, params.getBoost()); + assertTrue(params.getCaseInsensitive()); + } + + @Test + void testExtractFieldValueParams_Term_StringValue() { + Query query = Query.of(q -> q.term(t -> t + .field("username") + .value(FieldValue.of("john_doe")) + .boost(1.2f) + .queryName("user_search") + )); + + ParameterExtractor.FieldValueParams params = ParameterExtractor.extractFieldValueParams(query, "term"); + + assertNotNull(params); + assertEquals("username", params.fieldName); + assertEquals("john_doe", params.value); + assertNotNull(params.commonParams); + assertEquals(1.2f, params.commonParams.getBoost()); + } + + @Test + void testExtractFieldValueParams_Term_EmptyValue() { + Query query = Query.of(q -> q.term(t -> t + .field("description") + .value(FieldValue.of("")) + )); + + ParameterExtractor.FieldValueParams params = ParameterExtractor.extractFieldValueParams(query, "term"); + + assertNotNull(params); + assertEquals("description", params.fieldName); + assertEquals("", params.value); + } + + @Test + void testExtractCommonParameters_Prefix() { + Query query = Query.of(q -> q.prefix(p -> p + .field("name") + .value("test") + .boost(2.0f) + .caseInsensitive(true) + )); + + CommonQueryParameters params = ParameterExtractor.extractCommonParameters(query, "prefix"); + + assertNotNull(params); + assertEquals(2.0f, params.getBoost()); + assertTrue(params.getCaseInsensitive()); + } + + @Test + void testExtractCommonParameters_Wildcard() { + Query query = Query.of(q -> q.wildcard(w -> w + .field("pattern") + .value("test*") + .caseInsensitive(false) + .boost(1.5f) + )); + + CommonQueryParameters params = ParameterExtractor.extractCommonParameters(query, "wildcard"); + + assertNotNull(params); + assertEquals(1.5f, params.getBoost()); + assertFalse(params.getCaseInsensitive()); + } + + @Test + void testExtractCommonParameters_Regexp() { + Query query = Query.of(q -> q.regexp(r -> r + .field("email") + .value(".*@test\\.com") + .caseInsensitive(true) + )); + + CommonQueryParameters params = ParameterExtractor.extractCommonParameters(query, "regexp"); + + assertNotNull(params); + assertTrue(params.getCaseInsensitive()); + } + + @Test + void testFieldValueParams_Constructor() { + CommonQueryParameters common = CommonQueryParameters.withBoost(2.5f); + ParameterExtractor.FieldValueParams params = new ParameterExtractor.FieldValueParams("field", "value", common); + + assertEquals("field", params.fieldName); + assertEquals("value", params.value); + assertEquals(2.5f, params.commonParams.getBoost()); + } + + @Test + void testFuzzyParams_Constructor() { + ParameterExtractor.FieldValueParams baseParams = new ParameterExtractor.FieldValueParams( + "field", "value", CommonQueryParameters.empty() + ); + ParameterExtractor.FuzzyParams params = new ParameterExtractor.FuzzyParams(baseParams, 2, 50, 1, true); + + assertEquals("field", params.baseParams.fieldName); + assertEquals(2, params.fuzziness); + assertEquals(50, params.maxExpansions); + assertEquals(1, params.prefixLength); + assertTrue(params.transpositions); + } + + @Test + void testRegexpParams_Constructor() { + ParameterExtractor.FieldValueParams baseParams = new ParameterExtractor.FieldValueParams( + "pattern", ".*test", CommonQueryParameters.empty() + ); + ParameterExtractor.RegexpParams params = new ParameterExtractor.RegexpParams(baseParams, "ALL", 10000); + + assertEquals("pattern", params.baseParams.fieldName); + assertEquals("ALL", params.flags); + assertEquals(10000, params.maxDeterminizedStates); + } + + @Test + void testMatchParams_Constructor() { + ParameterExtractor.FieldValueParams baseParams = new ParameterExtractor.FieldValueParams( + "content", "search", CommonQueryParameters.empty() + ); + ParameterExtractor.MatchParams params = new ParameterExtractor.MatchParams( + baseParams, "and", 2, "AUTO", 1, 50, "standard", true, false, false + ); + + assertEquals("content", params.baseParams.fieldName); + assertEquals("and", params.operator); + assertEquals(2, params.minimumShouldMatch); + assertEquals("AUTO", params.fuzziness); + } + + @Test + void testRangeParams_Constructor() { + ParameterExtractor.RangeParams params = new ParameterExtractor.RangeParams( + "age", 18, 20, 65, 70, "yyyy-MM-dd", "INTERSECTS", 1.5f, "UTC" + ); + + assertEquals("age", params.fieldName); + assertEquals(18, params.gte); + assertEquals(20, params.gt); + assertEquals(65, params.lte); + assertEquals(70, params.lt); + assertEquals("yyyy-MM-dd", params.format); + assertEquals("INTERSECTS", params.relation); + assertEquals(1.5f, params.boost); + assertEquals("UTC", params.timeZone); + } + + @Test + void testExtractStringParameter_Regexp_Flags() { + Query query = Query.of(q -> q.regexp(r -> r + .field("email") + .value(".*@test\\.com") + .flags("ALL") + )); + + String flags = OpenSearchQueryHelper.extractStringParameter(query, "regexp", "flags"); + assertEquals("ALL", flags); + } + + @Test + void testExtractStringParameter_Prefix_Rewrite() { + Query query = Query.of(q -> q.prefix(p -> p + .field("name") + .value("test") + .rewrite("scoring_boolean") + )); + + String rewrite = OpenSearchQueryHelper.extractStringParameter(query, "prefix", "rewrite"); + assertEquals("scoring_boolean", rewrite); + } + + @Test + void testExtractIntegerParameter_Regexp_MaxDeterminizedStates() { + Query query = Query.of(q -> q.regexp(r -> r + .field("pattern") + .value("test.*") + .maxDeterminizedStates(10000) + )); + + Integer maxStates = OpenSearchQueryHelper.extractIntegerParameter(query, "regexp", "maxDeterminizedStates"); + assertEquals(10000, maxStates); + } + + @Test + void testExtractIntegerParameter_Wildcard_Null() { + Query query = Query.of(q -> q.wildcard(w -> w + .field("pattern") + .value("test*") + )); + + Integer maxExpansions = OpenSearchQueryHelper.extractIntegerParameter(query, "wildcard", "maxExpansions"); + assertNull(maxExpansions); + } +} diff --git a/core-tests/e2e-tests/spring-rest-opensearch/src/main/java/com/opensearch/age/OpenSearchAgeRest.java b/core-tests/e2e-tests/spring-rest-opensearch/src/main/java/com/opensearch/age/OpenSearchAgeRest.java index e0405a73fd..7d8d38ccee 100644 --- a/core-tests/e2e-tests/spring-rest-opensearch/src/main/java/com/opensearch/age/OpenSearchAgeRest.java +++ b/core-tests/e2e-tests/spring-rest-opensearch/src/main/java/com/opensearch/age/OpenSearchAgeRest.java @@ -26,45 +26,44 @@ public List findByAge(@PathVariable("age") Integer age) throws IOException return ageRepo.findByAge(age); } - // TODO: Update mappings once we have added support for range queries -// @GetMapping("gte/{gte}") -// public List findGteAge(@PathVariable("gte") Integer gte) throws IOException { -// return ageRepo.findGteAge(gte); -// } -// -// @GetMapping("lte/{lte}") -// public List findLteAge(@PathVariable("lte") Integer lte) throws IOException { -// return ageRepo.findLteAge(lte); -// } -// -// @GetMapping("gt/{gt}") -// public List findGtAge(@PathVariable("gt") Integer gt) throws IOException { -// return ageRepo.findGtAge(gt); -// } -// -// @GetMapping("lt/{lt}") -// public List findLtAge(@PathVariable("lt") Integer lt) throws IOException { -// return ageRepo.findLtAge(lt); -// } -// -// @GetMapping("gte-lte/{gte}/{lte}") -// public List findGteLteAge(@PathVariable("gte") Integer gte, @PathVariable("lte") Integer lte) throws IOException { -// return ageRepo.findGteLteAge(gte, lte); -// } -// -// @GetMapping("gte-lt/{gte}/{lt}") -// public List findGteLtAge(@PathVariable("gte") Integer gte, @PathVariable("lt") Integer lt) throws IOException { -// return ageRepo.findGteLtAge(gte, lt); -// } -// -// @GetMapping("gt-lte/{gt}/{lte}") -// public List findGtLteAge(@PathVariable("gt") Integer gt, @PathVariable("lte") Integer lte) throws IOException { -// return ageRepo.findGtLteAge(gt, lte); -// } -// -// @GetMapping("gt-lt/{gt}/{lt}") -// public List findGtLtAge(@PathVariable("gt") Integer gt, @PathVariable("lt") Integer lt) throws IOException { -// return ageRepo.findGtLtAge(gt, lt); -// } + @GetMapping("gte/{gte}") + public List findGteAge(@PathVariable("gte") Integer gte) throws IOException { + return ageRepo.findGteAge(gte); + } + + @GetMapping("lte/{lte}") + public List findLteAge(@PathVariable("lte") Integer lte) throws IOException { + return ageRepo.findLteAge(lte); + } + + @GetMapping("gt/{gt}") + public List findGtAge(@PathVariable("gt") Integer gt) throws IOException { + return ageRepo.findGtAge(gt); + } + + @GetMapping("lt/{lt}") + public List findLtAge(@PathVariable("lt") Integer lt) throws IOException { + return ageRepo.findLtAge(lt); + } + + @GetMapping("gte-lte/{gte}/{lte}") + public List findGteLteAge(@PathVariable("gte") Integer gte, @PathVariable("lte") Integer lte) throws IOException { + return ageRepo.findGteLteAge(gte, lte); + } + + @GetMapping("gte-lt/{gte}/{lt}") + public List findGteLtAge(@PathVariable("gte") Integer gte, @PathVariable("lt") Integer lt) throws IOException { + return ageRepo.findGteLtAge(gte, lt); + } + + @GetMapping("gt-lte/{gt}/{lte}") + public List findGtLteAge(@PathVariable("gt") Integer gt, @PathVariable("lte") Integer lte) throws IOException { + return ageRepo.findGtLteAge(gt, lte); + } + + @GetMapping("gt-lt/{gt}/{lt}") + public List findGtLtAge(@PathVariable("gt") Integer gt, @PathVariable("lt") Integer lt) throws IOException { + return ageRepo.findGtLtAge(gt, lt); + } } diff --git a/core-tests/e2e-tests/spring-rest-opensearch/src/main/java/com/opensearch/students/Student.java b/core-tests/e2e-tests/spring-rest-opensearch/src/main/java/com/opensearch/students/Student.java index 9ac6a4aa38..0e6765d4c6 100644 --- a/core-tests/e2e-tests/spring-rest-opensearch/src/main/java/com/opensearch/students/Student.java +++ b/core-tests/e2e-tests/spring-rest-opensearch/src/main/java/com/opensearch/students/Student.java @@ -4,6 +4,11 @@ public class Student { public String firstName; public String lastName; + public Student() { + this.firstName = "firstName"; + this.lastName = "lastName"; + } + public Student(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; diff --git a/core-tests/e2e-tests/spring-rest-opensearch/src/test/java/org/evomaster/e2etests/spring/rest/opensearch/age/OpenSearchAgeEMTest.java b/core-tests/e2e-tests/spring-rest-opensearch/src/test/java/org/evomaster/e2etests/spring/rest/opensearch/age/OpenSearchAgeEMTest.java index 9eb98bae08..a1e3b1889b 100644 --- a/core-tests/e2e-tests/spring-rest-opensearch/src/test/java/org/evomaster/e2etests/spring/rest/opensearch/age/OpenSearchAgeEMTest.java +++ b/core-tests/e2e-tests/spring-rest-opensearch/src/test/java/org/evomaster/e2etests/spring/rest/opensearch/age/OpenSearchAgeEMTest.java @@ -2,6 +2,7 @@ import com.foo.spring.rest.opensearch.age.OpenSearchAgeController; import org.evomaster.core.EMConfig; +import org.evomaster.core.problem.rest.data.HttpVerb; import org.evomaster.core.problem.rest.data.RestIndividual; import org.evomaster.core.search.Solution; import org.evomaster.e2etests.utils.RestTestBase; @@ -29,7 +30,9 @@ public void testRunEM() throws Throwable { Solution solution = initAndRun(args); -// assertHasAtLeastOne(solution, HttpVerb.GET, 404, "/age/{q}", null); + assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/age/{age}", null); + assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/age/gte/{gte}", null); + assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/age/age", null); }); } } diff --git a/core-tests/e2e-tests/spring-rest-opensearch/src/test/java/org/evomaster/e2etests/spring/rest/opensearch/students/OpenSearchStudentsEMTest.java b/core-tests/e2e-tests/spring-rest-opensearch/src/test/java/org/evomaster/e2etests/spring/rest/opensearch/students/OpenSearchStudentsEMTest.java index 0bea1a621b..7366047f97 100644 --- a/core-tests/e2e-tests/spring-rest-opensearch/src/test/java/org/evomaster/e2etests/spring/rest/opensearch/students/OpenSearchStudentsEMTest.java +++ b/core-tests/e2e-tests/spring-rest-opensearch/src/test/java/org/evomaster/e2etests/spring/rest/opensearch/students/OpenSearchStudentsEMTest.java @@ -2,11 +2,12 @@ import com.foo.spring.rest.opensearch.students.OpenSearchStudentsController; import org.evomaster.core.EMConfig; +import org.evomaster.core.problem.rest.data.HttpVerb; import org.evomaster.core.problem.rest.data.RestIndividual; import org.evomaster.core.search.Solution; import org.evomaster.e2etests.utils.RestTestBase; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; public class OpenSearchStudentsEMTest extends RestTestBase { @@ -17,7 +18,7 @@ public static void initClass() throws Exception { RestTestBase.initClass(new OpenSearchStudentsController(), config); } - @Disabled("TODO: Enable once we have added support for query generation") + @Test public void testRunEM() throws Throwable { runTestHandlingFlaky( "OpenSearchStudentsEM", @@ -29,7 +30,8 @@ public void testRunEM() throws Throwable { Solution solution = initAndRun(args); -// assertHasAtLeastOne(solution, HttpVerb.GET, 404, "/students/{q}", null); + assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/students/{lastName}", null); + assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/students/jorge", null); }); } } diff --git a/e2e-tests/spring-rest-opensearch/src/main/java/com/opensearch/queries/OpenSearchQueriesApp.java b/e2e-tests/spring-rest-opensearch/src/main/java/com/opensearch/queries/OpenSearchQueriesApp.java new file mode 100644 index 0000000000..fc9b6ec528 --- /dev/null +++ b/e2e-tests/spring-rest-opensearch/src/main/java/com/opensearch/queries/OpenSearchQueriesApp.java @@ -0,0 +1,21 @@ +package com.opensearch.queries; + +import com.opensearch.config.SwaggerConfiguration; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +@EnableSwagger2 +@SpringBootApplication(exclude = SecurityAutoConfiguration.class) +@ComponentScan({"com.opensearch.config", "com.opensearch.queries"}) +public class OpenSearchQueriesApp extends SwaggerConfiguration { + public OpenSearchQueriesApp() { + super("queries"); + } + + public static void main(String[] args) { + SpringApplication.run(OpenSearchQueriesApp.class, args); + } +} diff --git a/e2e-tests/spring-rest-opensearch/src/main/java/com/opensearch/queries/OpenSearchQueriesRest.java b/e2e-tests/spring-rest-opensearch/src/main/java/com/opensearch/queries/OpenSearchQueriesRest.java new file mode 100644 index 0000000000..a74e89b367 --- /dev/null +++ b/e2e-tests/spring-rest-opensearch/src/main/java/com/opensearch/queries/OpenSearchQueriesRest.java @@ -0,0 +1,110 @@ +package com.opensearch.queries; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +@RestController +@RequestMapping(path = "/queries") +public class OpenSearchQueriesRest { + + @Autowired + private ProductRepository productRepository; + + @PostMapping("setup") + public void setupTestData() throws IOException { + // Create test products with various characteristics + productRepository.index(new Product("1", "iPhone 15", "Latest Apple smartphone with advanced features", + "electronics", 999.99, 5, true, Arrays.asList("apple", "smartphone", "mobile"), "Apple", "contact@apple.com")); + + productRepository.index(new Product("2", "Samsung Galaxy", "Android smartphone with great camera", + "electronics", 799.99, 4, true, Arrays.asList("samsung", "android", "mobile"), "Samsung", null)); + + productRepository.index(new Product("3", "MacBook Pro", "Professional laptop for developers", + "computers", 1999.99, 5, false, Arrays.asList("apple", "laptop", "professional"), "Apple", "support@apple.com")); + + productRepository.index(new Product("4", "Dell XPS", "Windows laptop for business", + "computers", 1299.99, 4, true, Arrays.asList("dell", "windows", "business"), "Dell", "info@dell.com")); + + productRepository.index(new Product("5", "iPad Air", "Tablet for creativity and productivity", + "tablets", 599.99, 4, true, Arrays.asList("apple", "tablet", "creative"), "Apple", null)); + } + + // Term selector tests + @GetMapping("category/{category}") + public List findByCategory(@PathVariable String category) throws IOException { + return productRepository.findByCategory(category); + } + + // Terms selector tests + @GetMapping("categories") + public List findByCategories(@RequestParam List cats) throws IOException { + return productRepository.findByCategories(cats); + } + + // Range selector tests + @GetMapping("price-range") + public List findByPriceRange(@RequestParam(required = false) Double min, + @RequestParam(required = false) Double max) throws IOException { + return productRepository.findByPriceRange(min, max); + } + + @GetMapping("rating-gte/{rating}") + public List findByRatingGte(@PathVariable Integer rating) throws IOException { + return productRepository.findByRatingGte(rating); + } + + // Prefix selector tests + @GetMapping("name-prefix/{prefix}") + public List findByNamePrefix(@PathVariable String prefix) throws IOException { + return productRepository.findByNamePrefix(prefix); + } + + // Exists selector tests + @GetMapping("with-email") + public List findWithEmail() throws IOException { + return productRepository.findWithEmail(); + } + + // Fuzzy selector tests + @GetMapping("name-fuzzy/{name}") + public List findByNameFuzzy(@PathVariable String name, + @RequestParam(defaultValue = "2") Integer fuzziness) throws IOException { + return productRepository.findByNameFuzzy(name, fuzziness); + } + + // Wildcard selector tests + @GetMapping("name-wildcard/{pattern}") + public List findByNameWildcard(@PathVariable String pattern) throws IOException { + return productRepository.findByNameWildcard(pattern); + } + + // Regexp selector tests + @GetMapping("email-pattern/{pattern}") + public List findByEmailPattern(@PathVariable String pattern) throws IOException { + return productRepository.findByEmailPattern(pattern); + } + + // Match selector tests + @GetMapping("description-match/{text}") + public List findByDescriptionMatch(@PathVariable String text) throws IOException { + return productRepository.findByDescriptionMatch(text); + } + + // Bool selector tests + @GetMapping("complex") + public List findByComplexQuery(@RequestParam(required = false) String category, + @RequestParam(required = false) Double minPrice, + @RequestParam(required = false) Boolean inStock) throws IOException { + return productRepository.findByComplexQuery(category, minPrice, inStock); + } + + // IDs selector tests + @GetMapping("by-ids") + public List findByIds(@RequestParam List ids) throws IOException { + return productRepository.findByIds(ids); + } +} diff --git a/e2e-tests/spring-rest-opensearch/src/main/java/com/opensearch/queries/Product.java b/e2e-tests/spring-rest-opensearch/src/main/java/com/opensearch/queries/Product.java new file mode 100644 index 0000000000..6bd48be47d --- /dev/null +++ b/e2e-tests/spring-rest-opensearch/src/main/java/com/opensearch/queries/Product.java @@ -0,0 +1,83 @@ +package com.opensearch.queries; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public class Product { + @JsonProperty("id") + private String id; + + @JsonProperty("name") + private String name; + + @JsonProperty("description") + private String description; + + @JsonProperty("category") + private String category; + + @JsonProperty("price") + private Double price; + + @JsonProperty("rating") + private Integer rating; + + @JsonProperty("inStock") + private Boolean inStock; + + @JsonProperty("tags") + private List tags; + + @JsonProperty("brand") + private String brand; + + @JsonProperty("email") + private String email; + + public Product() {} + + public Product(String id, String name, String description, String category, Double price, + Integer rating, Boolean inStock, List tags, String brand, String email) { + this.id = id; + this.name = name; + this.description = description; + this.category = category; + this.price = price; + this.rating = rating; + this.inStock = inStock; + this.tags = tags; + this.brand = brand; + this.email = email; + } + + public String getId() { return id; } + public void setId(String id) { this.id = id; } + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public String getCategory() { return category; } + public void setCategory(String category) { this.category = category; } + + public Double getPrice() { return price; } + public void setPrice(Double price) { this.price = price; } + + public Integer getRating() { return rating; } + public void setRating(Integer rating) { this.rating = rating; } + + public Boolean getInStock() { return inStock; } + public void setInStock(Boolean inStock) { this.inStock = inStock; } + + public List getTags() { return tags; } + public void setTags(List tags) { this.tags = tags; } + + public String getBrand() { return brand; } + public void setBrand(String brand) { this.brand = brand; } + + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } +} diff --git a/e2e-tests/spring-rest-opensearch/src/main/java/com/opensearch/queries/ProductRepository.java b/e2e-tests/spring-rest-opensearch/src/main/java/com/opensearch/queries/ProductRepository.java new file mode 100644 index 0000000000..6e238dfbb1 --- /dev/null +++ b/e2e-tests/spring-rest-opensearch/src/main/java/com/opensearch/queries/ProductRepository.java @@ -0,0 +1,191 @@ +package com.opensearch.queries; + +import com.opensearch.config.OpenSearchRepository; +import java.util.stream.Collectors; +import org.opensearch.client.json.JsonData; +import org.opensearch.client.opensearch._types.FieldValue; +import org.opensearch.client.opensearch._types.query_dsl.RangeQuery; +import org.opensearch.client.opensearch.core.IndexRequest; +import org.opensearch.client.opensearch.core.SearchRequest; +import org.springframework.stereotype.Repository; + +import java.io.IOException; +import java.util.List; + +@Repository +public class ProductRepository { + private final OpenSearchRepository openSearchRepository; + + public ProductRepository(OpenSearchRepository openSearchRepository) { + this.openSearchRepository = openSearchRepository; + } + + public void index(Product product) throws IOException { + IndexRequest request = new IndexRequest.Builder() + .index("products") + .id(product.getId()) + .document(product) + .build(); + openSearchRepository.index(request); + } + + // Term queries + public List findByCategory(String category) throws IOException { + SearchRequest request = new SearchRequest.Builder() + .index("products") + .query(query -> query.term(term -> + term.field("category").value(value -> value.stringValue(category)))) + .build(); + return openSearchRepository.search(request, Product.class); + } + + // Terms queries + public List findByCategories(List categories) throws IOException { + SearchRequest request = new SearchRequest.Builder() + .index("products") + .query(query -> query.terms(terms -> + terms.field("category").terms(termsValue -> + termsValue.value(categories.stream().map(FieldValue::of).collect(Collectors.toList())) + ))) + .build(); + return openSearchRepository.search(request, Product.class); + } + + // Range queries + public List findByPriceRange(Double gte, Double lte) throws IOException { + SearchRequest request = + new SearchRequest.Builder() + .index("products") + .query( + query -> + query.range( + range -> { + RangeQuery.Builder rangeQuery = range.field("price"); + if (gte != null) rangeQuery.gte(JsonData.of(gte)); + if (lte != null) rangeQuery.lte(JsonData.of(lte)); + return rangeQuery; + })) + .build(); + return openSearchRepository.search(request, Product.class); + } + + public List findByRatingGte(Integer rating) throws IOException { + SearchRequest request = new SearchRequest.Builder() + .index("products") + .query(query -> query.range(range -> + range.field("rating").gte(JsonData.of(rating)))) + .build(); + return openSearchRepository.search(request, Product.class); + } + + // Prefix queries + public List findByNamePrefix(String prefix) throws IOException { + SearchRequest request = new SearchRequest.Builder() + .index("products") + .query(query -> query.prefix(prefixQuery -> + prefixQuery.field("name").value(prefix))) + .build(); + return openSearchRepository.search(request, Product.class); + } + + // Exists queries + public List findWithEmail() throws IOException { + SearchRequest request = new SearchRequest.Builder() + .index("products") + .query(query -> query.exists(exists -> + exists.field("email"))) + .build(); + return openSearchRepository.search(request, Product.class); + } + + // Fuzzy queries + public List findByNameFuzzy(String name, Integer fuzziness) throws IOException { + SearchRequest request = new SearchRequest.Builder() + .index("products") + .query(query -> query.fuzzy(fuzzy -> + fuzzy.field("name").value(FieldValue.of(name)).fuzziness(String.valueOf(fuzziness)))) + .build(); + return openSearchRepository.search(request, Product.class); + } + + // Wildcard queries + public List findByNameWildcard(String pattern) throws IOException { + SearchRequest request = new SearchRequest.Builder() + .index("products") + .query(query -> query.wildcard(wildcard -> + wildcard.field("name").value(pattern))) + .build(); + return openSearchRepository.search(request, Product.class); + } + + // Regexp queries + public List findByEmailPattern(String pattern) throws IOException { + SearchRequest request = new SearchRequest.Builder() + .index("products") + .query(query -> query.regexp(regexp -> + regexp.field("email").value(pattern))) + .build(); + return openSearchRepository.search(request, Product.class); + } + + // Match queries + public List findByDescriptionMatch(String text) throws IOException { + SearchRequest request = new SearchRequest.Builder() + .index("products") + .query(query -> query.match(match -> + match.field("description").query(FieldValue.of(text)))) + .build(); + return openSearchRepository.search(request, Product.class); + } + + // Bool queries + public List findByComplexQuery(String category, Double minPrice, Boolean inStock) throws IOException { + SearchRequest request = + new SearchRequest.Builder() + .index("products") + .query( + query -> + query.bool( + bool -> { + if (category != null) { + bool.must( + must -> + must.term( + term -> + term.field("category") + .value(value -> value.stringValue(category)))); + } + + if (inStock != null) { + bool.must( + must -> + must.term( + term -> + term.field("inStock") + .value(value -> value.booleanValue(inStock)))); + } + + // Filter clause + if (minPrice != null) { + bool.filter( + filter -> + filter.range( + range -> range.field("price").gte(JsonData.of(minPrice)))); + } + + return bool; + })) + .build(); + return openSearchRepository.search(request, Product.class); + } + + // IDs queries + public List findByIds(List ids) throws IOException { + SearchRequest request = new SearchRequest.Builder() + .index("products") + .query(query -> query.ids(idsQuery -> + idsQuery.values(ids))) + .build(); + return openSearchRepository.search(request, Product.class); + } +} diff --git a/e2e-tests/spring-rest-opensearch/src/test/java/com/foo/spring/rest/opensearch/queries/OpenSearchQueriesController.java b/e2e-tests/spring-rest-opensearch/src/test/java/com/foo/spring/rest/opensearch/queries/OpenSearchQueriesController.java new file mode 100644 index 0000000000..e098032fe1 --- /dev/null +++ b/e2e-tests/spring-rest-opensearch/src/test/java/com/foo/spring/rest/opensearch/queries/OpenSearchQueriesController.java @@ -0,0 +1,15 @@ +package com.foo.spring.rest.opensearch.queries; + +import com.foo.spring.rest.opensearch.OpenSearchController; +import com.opensearch.queries.OpenSearchQueriesApp; + +public class OpenSearchQueriesController extends OpenSearchController { + public OpenSearchQueriesController() { + super("products", OpenSearchQueriesApp.class); + } + + @Override + public String getPackagePrefixesToCover() { + return "com.opensearch.queries"; + } +} diff --git a/e2e-tests/spring-rest-opensearch/src/test/java/org/evomaster/e2etests/spring/rest/opensearch/queries/OpenSearchQueriesEMTest.java b/e2e-tests/spring-rest-opensearch/src/test/java/org/evomaster/e2etests/spring/rest/opensearch/queries/OpenSearchQueriesEMTest.java new file mode 100644 index 0000000000..e668c375a5 --- /dev/null +++ b/e2e-tests/spring-rest-opensearch/src/test/java/org/evomaster/e2etests/spring/rest/opensearch/queries/OpenSearchQueriesEMTest.java @@ -0,0 +1,134 @@ +package org.evomaster.e2etests.spring.rest.opensearch.queries; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.foo.spring.rest.opensearch.queries.OpenSearchQueriesController; +import org.evomaster.core.EMConfig; +import org.evomaster.core.problem.rest.data.HttpVerb; +import org.evomaster.core.problem.rest.data.RestIndividual; +import org.evomaster.core.search.Solution; +import org.evomaster.e2etests.utils.RestTestBase; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class OpenSearchQueriesEMTest extends RestTestBase { + + @BeforeAll + public static void initClass() throws Exception { + EMConfig config = new EMConfig(); + config.setInstrumentMR_OPENSEARCH(true); + RestTestBase.initClass(new OpenSearchQueriesController(), config); + } + + @Test + public void testTermQueries() throws Throwable { + runTestHandlingFlaky( + "OpenSearchTermQueriesEM", + "org.foo.spring.rest.opensearch.OpenSearchTermQueriesEM", + 1000, + true, + (args) -> { + setOption(args, "instrumentMR_OPENSEARCH", "true"); + + Solution solution = initAndRun(args); + + assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/queries/category/{category}", null); + assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/queries/setup", null); + }); + } + + @Test + public void testRangeQueries() throws Throwable { + runTestHandlingFlaky( + "OpenSearchRangeQueriesEM", + "org.foo.spring.rest.opensearch.OpenSearchRangeQueriesEM", + 1000, + true, + (args) -> { + setOption(args, "instrumentMR_OPENSEARCH", "true"); + + Solution solution = initAndRun(args); + + assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/queries/price-range", null); + assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/queries/rating-gte/{rating}", null); + }); + } + + @Test + public void testTextQueries() throws Throwable { + runTestHandlingFlaky( + "OpenSearchTextQueriesEM", + "org.foo.spring.rest.opensearch.OpenSearchTextQueriesEM", + 1000, + true, + (args) -> { + setOption(args, "instrumentMR_OPENSEARCH", "true"); + + Solution solution = initAndRun(args); + + assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/queries/name-prefix/{prefix}", null); + assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/queries/name-fuzzy/{name}", null); + assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/queries/name-wildcard/{pattern}", null); + assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/queries/description-match/{text}", null); + }); + } + + @Test + public void testAdvancedQueries() throws Throwable { + runTestHandlingFlaky( + "OpenSearchAdvancedQueriesEM", + "org.foo.spring.rest.opensearch.OpenSearchAdvancedQueriesEM", + 1000, + true, + (args) -> { + setOption(args, "instrumentMR_OPENSEARCH", "true"); + + Solution solution = initAndRun(args); + + assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/queries/with-email", null); + assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/queries/email-pattern/{pattern}", null); + assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/queries/complex", null); + assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/queries/by-ids", null); + }); + } + + @Test + public void testAllSelectorsIntegration() throws Throwable { + runTestHandlingFlaky( + "OpenSearchAllSelectorsEM", + "org.foo.spring.rest.opensearch.OpenSearchAllSelectorsEM", + 1500, + true, + (args) -> { + setOption(args, "instrumentMR_OPENSEARCH", "true"); + + Solution solution = initAndRun(args); + + assertFalse(solution.getIndividuals().isEmpty()); + assertTrue(solution.getOverall().getSize() >= 0.0); + + /*// Verify that various endpoints are covered + long termQueries = solution.getIndividuals().stream() + .flatMap(ind -> ind.see().stream()) + .filter(action -> action.toString().contains("/queries/category/")) + .count(); + + long rangeQueries = solution.getIndividuals().stream() + .flatMap(ind -> ind.getMain().stream()) + .filter(action -> action.toString().contains("/queries/price-range") || + action.toString().contains("/queries/rating-gte/")) + .count(); + + long textQueries = solution.getIndividuals().stream() + .flatMap(ind -> ind.seeMainExecutableActions().stream()) + .filter(action -> action.toString().contains("/queries/name-prefix/") || + action.toString().contains("/queries/name-fuzzy/") || + action.toString().contains("/queries/description-match/")) + .count(); + + assertTrue(termQueries > 0 || rangeQueries > 0 || textQueries > 0, + "Should have executed some OpenSearch queries");*/ + }); + } +}