Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@

import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.opensearch.sql.data.type.ExprType;

/** Search expression for AND operator. */
@Getter
Expand All @@ -23,8 +25,8 @@ public class SearchAnd extends SearchExpression {
private final SearchExpression right;

@Override
public String toQueryString() {
return left.toQueryString() + " AND " + right.toQueryString();
public String toQueryString(Function<String, ExprType> fieldTypeResolver) {
return left.toQueryString(fieldTypeResolver) + " AND " + right.toQueryString(fieldTypeResolver);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@

import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.opensearch.sql.data.type.ExprType;
import org.opensearch.sql.utils.QueryStringUtils;

/** Search expression for field comparisons. */
Expand Down Expand Up @@ -46,9 +48,11 @@ public String getSymbol() {
private final SearchLiteral value;

@Override
public String toQueryString() {
String fieldName = QueryStringUtils.escapeFieldName(field.getField().toString());
String valueStr = value.toQueryString();
public String toQueryString(Function<String, ExprType> fieldTypeResolver) {
String rawFieldName = field.getField().toString();
String fieldName = QueryStringUtils.escapeFieldName(rawFieldName);
ExprType resolvedType = fieldTypeResolver.apply(rawFieldName);
String valueStr = value.toQueryString(resolvedType);
switch (operator) {
case NOT_EQUALS:
return "( _exists_:" + fieldName + " AND NOT " + fieldName + ":" + valueStr + " )";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,33 @@

package org.opensearch.sql.ast.expression;

import java.util.function.Function;
import org.opensearch.sql.ast.AbstractNodeVisitor;
import org.opensearch.sql.data.type.ExprType;

/** Base class for search expressions that get converted to query_string syntax. */
public abstract class SearchExpression extends UnresolvedExpression {

/**
* Convert this search expression to query_string syntax.
* Convert this search expression to query_string syntax without field-type awareness.
*
* @return the query string representation
*/
public abstract String toQueryString();
public String toQueryString() {
return toQueryString(f -> null);
}

/**
* Convert this search expression to query_string syntax, using {@code fieldTypeResolver} to
* resolve the OpenSearch type of a field when the emission depends on whether the field is
* keyword vs. text. When the resolver returns {@code null}, emission falls back to the
* field-type-agnostic form (same as {@link #toQueryString()}).
*
* @param fieldTypeResolver maps a field name to its resolved {@link ExprType}, or null when
* unknown
* @return the query string representation
*/
public abstract String toQueryString(Function<String, ExprType> fieldTypeResolver);

/**
* Convert the search expression to anonymized string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@

import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.opensearch.sql.data.type.ExprType;

/** Search expression for grouped expressions (parentheses). */
@Getter
Expand All @@ -22,8 +24,8 @@ public class SearchGroup extends SearchExpression {
private final SearchExpression expression;

@Override
public String toQueryString() {
return "(" + expression.toQueryString() + ")";
public String toQueryString(Function<String, ExprType> fieldTypeResolver) {
return "(" + expression.toQueryString(fieldTypeResolver) + ")";
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.opensearch.sql.data.type.ExprType;
import org.opensearch.sql.utils.QueryStringUtils;

/** Search expression for IN operator. */
Expand All @@ -25,10 +27,12 @@ public class SearchIn extends SearchExpression {
private final List<SearchLiteral> values;

@Override
public String toQueryString() {
public String toQueryString(Function<String, ExprType> fieldTypeResolver) {
String rawFieldName = field.getField().toString();
String fieldName = QueryStringUtils.escapeFieldName(field.getField().toString());
ExprType resolvedType = fieldTypeResolver.apply(rawFieldName);
String valueList =
values.stream().map(SearchLiteral::toQueryString).collect(Collectors.joining(" OR "));
values.stream().map(v -> v.toQueryString(resolvedType)).collect(Collectors.joining(" OR "));

return fieldName + ":( " + valueList + " )";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@

import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import org.opensearch.sql.data.type.ExprType;
import org.opensearch.sql.utils.QueryStringUtils;

/** Search expression for standalone literals. */
Expand All @@ -24,7 +26,23 @@ public class SearchLiteral extends SearchExpression {
private final boolean isPhrase;

@Override
public String toQueryString() {
public String toQueryString(Function<String, ExprType> fieldTypeResolver) {
// Unfielded literal: no enclosing field, so no index type. Take the field-agnostic branch.
return toQueryString((ExprType) null);
}

/**
* Emits the query_string form for a literal on the RHS of {@link SearchComparison} or inside
* {@link SearchIn}. The decision tree is documented in {@code
* docs/dev/ppl-search-command-contract-empirical.md} — briefly: whitespace + wildcard on a
* non-text index escapes the space so the parser keeps the value as one whole-value pattern;
* everything else falls through to phrase (with whitespace) or unquoted-escaped (without).
*
* @param indexType the enclosing field's OpenSearch index-mapping type (text/keyword/...) — used
* only to distinguish text-like from everything else; null means unknown, treated as
* text-like so we don't regress the phrase form.
*/
public String toQueryString(ExprType indexType) {
if (literal instanceof Literal) {
Literal lit = (Literal) literal;
Object val = lit.getValue();
Expand All @@ -38,23 +56,52 @@ public String toQueryString() {
if (val instanceof String) {
String str = (String) val;

// Phrase search - preserve quotes
// [D] whitespace + wildcard on a non-text index: single term with space escaped, so the
// query_string parser keeps the value as one whole-value pattern (a raw space would
// split it into two clauses and drop the field binding on the right half).
if (isPhrase && !isTextLike(indexType) && hasUnescapedWildcard(str)) {
return QueryStringUtils.escapeLuceneSpecialCharacters(str).replace(" ", "\\ ");
}

// [B]/[C] quoted phrase.
if (isPhrase) {
// Escape special chars inside the phrase
str = QueryStringUtils.escapeLuceneSpecialCharacters(str);
return "\"" + str + "\"";
}

// Regular string - escape special characters
// [A] unquoted; escape Lucene specials, wildcards preserved.
return QueryStringUtils.escapeLuceneSpecialCharacters(str);
}
}

// Default: escape the text representation
String text = literal.toString();
return QueryStringUtils.escapeLuceneSpecialCharacters(text);
}

private static boolean isTextLike(ExprType type) {
if (type == null) {
// Unknown type → treat as text-like so we take the phrase branch and avoid a text
// regression when the resolver fails to identify the field.
return true;
}
String legacyName = type.getOriginalExprType().legacyTypeName();
return "TEXT".equalsIgnoreCase(legacyName) || "MATCH_ONLY_TEXT".equalsIgnoreCase(legacyName);
}

private static boolean hasUnescapedWildcard(String s) {
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '\\' && i + 1 < s.length()) {
i++;
continue;
}
if (c == '*' || c == '?') {
return true;
}
}
return false;
}

@Override
public String toAnonymizedString() {
return "***";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@

import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.opensearch.sql.data.type.ExprType;

/** Search expression for NOT operator. */
@Getter
Expand All @@ -22,8 +24,8 @@ public class SearchNot extends SearchExpression {
private final SearchExpression expression;

@Override
public String toQueryString() {
return "NOT(" + expression.toQueryString() + ")";
public String toQueryString(Function<String, ExprType> fieldTypeResolver) {
return "NOT(" + expression.toQueryString(fieldTypeResolver) + ")";
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@

import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.opensearch.sql.data.type.ExprType;

/** Search expression for OR operator. */
@Getter
Expand All @@ -23,8 +25,8 @@ public class SearchOr extends SearchExpression {
private final SearchExpression right;

@Override
public String toQueryString() {
return left.toQueryString() + " OR " + right.toQueryString();
public String toQueryString(Function<String, ExprType> fieldTypeResolver) {
return left.toQueryString(fieldTypeResolver) + " OR " + right.toQueryString(fieldTypeResolver);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@
import org.opensearch.sql.ast.tree.Values;
import org.opensearch.sql.ast.tree.Window;
import org.opensearch.sql.ast.tree.Xyseries;
import org.opensearch.sql.calcite.plan.AbstractOpenSearchTable;
import org.opensearch.sql.calcite.plan.AliasFieldsWrappable;
import org.opensearch.sql.calcite.plan.HighlightPushDown;
import org.opensearch.sql.calcite.plan.OpenSearchConstants;
Expand All @@ -192,6 +193,7 @@
import org.opensearch.sql.common.patterns.PatternUtils;
import org.opensearch.sql.common.utils.StringUtils;
import org.opensearch.sql.data.type.ExprCoreType;
import org.opensearch.sql.data.type.ExprType;
import org.opensearch.sql.datasource.DataSourceService;
import org.opensearch.sql.exception.CalciteUnsupportedException;
import org.opensearch.sql.exception.SemanticCheckException;
Expand Down Expand Up @@ -297,11 +299,33 @@ private RelBuilder scan(RelOptTable tableSchema, CalcitePlanContext context) {
public RelNode visitSearch(Search node, CalcitePlanContext context) {
// Visit the Relation child to get the scan
node.getChild().get(0).accept(this, context);
// Resolve query_string from the structured expression when available so we can consult the
// OpenSearch table's field-type map for per-field text/keyword awareness (e.g. escape
// space + wildcard on keyword vs. quoted phrase on text). Falls back to the pre-computed
// string for callers that never populated the structured expression.
String queryString;
if (node.getOriginalExpression() != null) {
// TODO: index-mapping type (text/keyword) is storage metadata, not a data type — the right
// home is a field/scan annotation on RelDataType, but that needs a Calcite rule-pipeline
// audit (rules rebuild row types and can drop custom fields). For now, unwrap the table
// and read the ExprType map directly.
java.util.Map<String, ExprType> typesByName = new java.util.HashMap<>();
RelNode scan = context.relBuilder.peek();
RelOptTable relOptTable = scan.getTable();
if (relOptTable != null) {
AbstractOpenSearchTable osTable = relOptTable.unwrap(AbstractOpenSearchTable.class);
if (osTable != null) {
typesByName.putAll(osTable.getFieldTypes());
}
}
queryString = node.getOriginalExpression().toQueryString(typesByName::get);
} else {
queryString = node.getQueryString();
}
// Create query_string function
Function queryStringFunc =
AstDSL.function(
"query_string",
AstDSL.unresolvedArg("query", AstDSL.stringLiteral(node.getQueryString())));
"query_string", AstDSL.unresolvedArg("query", AstDSL.stringLiteral(queryString)));
RexNode queryStringRex = rexVisitor.analyze(queryStringFunc, context);

context.relBuilder.filter(queryStringRex);
Expand Down
Loading
Loading