Skip to content

Commit 48420be

Browse files
committed
Search Command Revamp
Signed-off-by: Vamsi Manohar <reddyvam@amazon.com>
1 parent 6d69440 commit 48420be

59 files changed

Lines changed: 3507 additions & 218 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

core/src/main/java/org/opensearch/sql/analysis/Analyzer.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
import org.opensearch.sql.ast.tree.Rename;
8787
import org.opensearch.sql.ast.tree.Reverse;
8888
import org.opensearch.sql.ast.tree.Rex;
89+
import org.opensearch.sql.ast.tree.Search;
8990
import org.opensearch.sql.ast.tree.Sort;
9091
import org.opensearch.sql.ast.tree.Sort.SortOption;
9192
import org.opensearch.sql.ast.tree.SubqueryAlias;
@@ -278,6 +279,18 @@ public LogicalPlan visitLimit(Limit node, AnalysisContext context) {
278279
return new LogicalLimit(child, node.getLimit(), node.getOffset());
279280
}
280281

282+
@Override
283+
public LogicalPlan visitSearch(Search node, AnalysisContext context) {
284+
LogicalPlan child = node.getChild().get(0).accept(this, context);
285+
Function queryStringFunc =
286+
AstDSL.function(
287+
"query_string",
288+
AstDSL.unresolvedArg("query", AstDSL.stringLiteral(node.getQueryString())));
289+
290+
Expression analyzed = expressionAnalyzer.analyze(queryStringFunc, context);
291+
return new LogicalFilter(child, analyzed);
292+
}
293+
281294
@Override
282295
public LogicalPlan visitFilter(Filter node, AnalysisContext context) {
283296
LogicalPlan child = node.getChild().get(0).accept(this, context);

core/src/main/java/org/opensearch/sql/ast/AbstractNodeVisitor.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
import org.opensearch.sql.ast.tree.Reverse;
7676
import org.opensearch.sql.ast.tree.Rex;
7777
import org.opensearch.sql.ast.tree.SPath;
78+
import org.opensearch.sql.ast.tree.Search;
7879
import org.opensearch.sql.ast.tree.Sort;
7980
import org.opensearch.sql.ast.tree.SubqueryAlias;
8081
import org.opensearch.sql.ast.tree.TableFunction;
@@ -131,6 +132,10 @@ public T visitTableFunction(TableFunction node, C context) {
131132
return visitChildren(node, context);
132133
}
133134

135+
public T visitSearch(Search node, C context) {
136+
return visitChildren(node, context);
137+
}
138+
134139
public T visitFilter(Filter node, C context) {
135140
return visitChildren(node, context);
136141
}

core/src/main/java/org/opensearch/sql/ast/dsl/AstDSL.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
import org.opensearch.sql.ast.tree.RelationSubquery;
7272
import org.opensearch.sql.ast.tree.Rename;
7373
import org.opensearch.sql.ast.tree.SPath;
74+
import org.opensearch.sql.ast.tree.Search;
7475
import org.opensearch.sql.ast.tree.Sort;
7576
import org.opensearch.sql.ast.tree.Sort.SortOption;
7677
import org.opensearch.sql.ast.tree.SpanBin;
@@ -109,6 +110,10 @@ public UnresolvedPlan describe(String tableName) {
109110
return new DescribeRelation(qualifiedName(tableName));
110111
}
111112

113+
public static UnresolvedPlan search(UnresolvedPlan input, String queryString) {
114+
return new Search(input, queryString);
115+
}
116+
112117
public UnresolvedPlan subqueryAlias(UnresolvedPlan child, String alias) {
113118
return new SubqueryAlias(child, alias);
114119
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.sql.ast.expression;
7+
8+
/** Utility class for query_string syntax operations. */
9+
public class QueryStringUtils {
10+
11+
// For field names, we typically don't escape dots as they're used for nested fields
12+
// But we escape other special characters
13+
public static final String LUCENE_SPECIAL_CHARS = "+-&|!(){}[]^\"~*?:\\/";
14+
15+
/**
16+
* Escape field name for query_string syntax. Only spaces need to be escaped in field names. Other
17+
* special characters are handled automatically by the query parser in field position.
18+
*
19+
* @param fieldName the field name to escape
20+
* @return escaped field name
21+
*/
22+
public static String escapeFieldName(String fieldName) {
23+
// Only escape spaces in field names
24+
return fieldName.replace(" ", "\\ ");
25+
}
26+
27+
/**
28+
* Escape Lucene/query_string special characters. Special characters: + - && || ! ( ) { } [ ] ^ "
29+
* ~ : / Note: * and ? are NOT escaped to allow wildcard pattern matching
30+
*
31+
* @param text the text to escape
32+
* @return escaped text with wildcards preserved
33+
*/
34+
public static String escapeLuceneSpecialCharacters(String text) {
35+
// List of special characters that need escaping (excluding * and ? for wildcard support)
36+
String specialChars = "+-&|!(){}[]^\"~:/";
37+
38+
StringBuilder escaped = new StringBuilder();
39+
for (int i = 0; i < text.length(); i++) {
40+
char c = text.charAt(i);
41+
42+
// Check if this is a special character that needs escaping
43+
if (specialChars.indexOf(c) >= 0) {
44+
// Special handling for && and ||
45+
if ((c == '&' || c == '|') && i + 1 < text.length() && text.charAt(i + 1) == c) {
46+
// Escape double && or ||
47+
escaped.append('\\').append(c).append('\\').append(c);
48+
i++; // Skip next character as we've handled it
49+
} else {
50+
// Escape single special character
51+
escaped.append('\\').append(c);
52+
}
53+
} else {
54+
escaped.append(c);
55+
}
56+
}
57+
58+
return escaped.toString();
59+
}
60+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.sql.ast.expression;
7+
8+
import java.util.Arrays;
9+
import java.util.List;
10+
import lombok.EqualsAndHashCode;
11+
import lombok.Getter;
12+
import lombok.RequiredArgsConstructor;
13+
import lombok.ToString;
14+
15+
/** Search expression for AND operator. */
16+
@Getter
17+
@RequiredArgsConstructor
18+
@EqualsAndHashCode(callSuper = false)
19+
@ToString
20+
public class SearchAnd extends SearchExpression {
21+
22+
private final SearchExpression left;
23+
private final SearchExpression right;
24+
25+
@Override
26+
public String toQueryString() {
27+
return left.toQueryString() + " AND " + right.toQueryString();
28+
}
29+
30+
@Override
31+
public List<? extends UnresolvedExpression> getChild() {
32+
return Arrays.asList(left, right);
33+
}
34+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.sql.ast.expression;
7+
8+
import java.util.Arrays;
9+
import java.util.List;
10+
import lombok.EqualsAndHashCode;
11+
import lombok.Getter;
12+
import lombok.RequiredArgsConstructor;
13+
import lombok.ToString;
14+
15+
/** Search expression for field comparisons. */
16+
@Getter
17+
@RequiredArgsConstructor
18+
@EqualsAndHashCode(callSuper = false)
19+
@ToString
20+
public class SearchComparison extends SearchExpression {
21+
22+
public enum Operator {
23+
EQUALS("="),
24+
NOT_EQUALS("!="),
25+
LESS_THAN("<"),
26+
LESS_OR_EQUAL("<="),
27+
GREATER_THAN(">"),
28+
GREATER_OR_EQUAL(">=");
29+
30+
private final String symbol;
31+
32+
Operator(String symbol) {
33+
this.symbol = symbol;
34+
}
35+
36+
public String getSymbol() {
37+
return symbol;
38+
}
39+
}
40+
41+
private final Field field;
42+
private final Operator operator;
43+
private final SearchLiteral value;
44+
45+
@Override
46+
public String toQueryString() {
47+
String fieldName = QueryStringUtils.escapeFieldName(field.getField().toString());
48+
String valueStr = value.toQueryString();
49+
return switch (operator) {
50+
case NOT_EQUALS -> "( _exists_:"
51+
+ fieldName
52+
+ " AND NOT "
53+
+ fieldName
54+
+ ":"
55+
+ valueStr
56+
+ " )";
57+
case GREATER_THAN -> fieldName + ":>" + valueStr;
58+
case GREATER_OR_EQUAL -> fieldName + ":>=" + valueStr;
59+
case LESS_THAN -> fieldName + ":<" + valueStr;
60+
case LESS_OR_EQUAL -> fieldName + ":<=" + valueStr;
61+
default -> fieldName + ":" + valueStr;
62+
};
63+
}
64+
65+
@Override
66+
public List<? extends UnresolvedExpression> getChild() {
67+
return Arrays.asList(field, value);
68+
}
69+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.sql.ast.expression;
7+
8+
import org.opensearch.sql.ast.AbstractNodeVisitor;
9+
10+
/** Base class for search expressions that get converted to query_string syntax. */
11+
public abstract class SearchExpression extends UnresolvedExpression {
12+
13+
/**
14+
* Convert this search expression to query_string syntax.
15+
*
16+
* @return the query string representation
17+
*/
18+
public abstract String toQueryString();
19+
20+
@Override
21+
public <R, C> R accept(AbstractNodeVisitor<R, C> nodeVisitor, C context) {
22+
return nodeVisitor.visitChildren(this, context);
23+
}
24+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.sql.ast.expression;
7+
8+
import java.util.Collections;
9+
import java.util.List;
10+
import lombok.EqualsAndHashCode;
11+
import lombok.Getter;
12+
import lombok.RequiredArgsConstructor;
13+
import lombok.ToString;
14+
15+
/** Search expression for grouped expressions (parentheses). */
16+
@Getter
17+
@RequiredArgsConstructor
18+
@EqualsAndHashCode(callSuper = false)
19+
@ToString
20+
public class SearchGroup extends SearchExpression {
21+
22+
private final SearchExpression expression;
23+
24+
@Override
25+
public String toQueryString() {
26+
return "(" + expression.toQueryString() + ")";
27+
}
28+
29+
@Override
30+
public List<? extends UnresolvedExpression> getChild() {
31+
return Collections.singletonList(expression);
32+
}
33+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.sql.ast.expression;
7+
8+
import java.util.ArrayList;
9+
import java.util.List;
10+
import java.util.stream.Collectors;
11+
import lombok.EqualsAndHashCode;
12+
import lombok.Getter;
13+
import lombok.RequiredArgsConstructor;
14+
import lombok.ToString;
15+
16+
/** Search expression for IN operator. */
17+
@Getter
18+
@RequiredArgsConstructor
19+
@EqualsAndHashCode(callSuper = false)
20+
@ToString
21+
public class SearchIn extends SearchExpression {
22+
23+
private final Field field;
24+
private final List<SearchLiteral> values;
25+
26+
@Override
27+
public String toQueryString() {
28+
String fieldName = QueryStringUtils.escapeFieldName(field.getField().toString());
29+
String valueList =
30+
values.stream().map(SearchLiteral::toQueryString).collect(Collectors.joining(" OR "));
31+
32+
return fieldName + ":( " + valueList + " )";
33+
}
34+
35+
@Override
36+
public List<? extends UnresolvedExpression> getChild() {
37+
List<UnresolvedExpression> children = new ArrayList<>();
38+
children.add(field);
39+
// SearchLiteral extends SearchExpression which extends UnresolvedExpression
40+
children.addAll(values);
41+
return children;
42+
}
43+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.sql.ast.expression;
7+
8+
import java.util.Collections;
9+
import java.util.List;
10+
import lombok.AllArgsConstructor;
11+
import lombok.EqualsAndHashCode;
12+
import lombok.Getter;
13+
import lombok.ToString;
14+
15+
/** Search expression for standalone literals. */
16+
@Getter
17+
@AllArgsConstructor
18+
@EqualsAndHashCode(callSuper = false)
19+
@ToString
20+
public class SearchLiteral extends SearchExpression {
21+
22+
private final UnresolvedExpression literal;
23+
private final boolean isPhrase;
24+
25+
@Override
26+
public String toQueryString() {
27+
if (literal instanceof Literal) {
28+
Literal lit = (Literal) literal;
29+
Object val = lit.getValue();
30+
31+
// Numbers don't need escaping
32+
if (val instanceof Number) {
33+
return val.toString();
34+
}
35+
36+
// Strings
37+
if (val instanceof String) {
38+
String str = (String) val;
39+
40+
// Phrase search - preserve quotes
41+
if (isPhrase) {
42+
// Escape special chars inside the phrase
43+
str = QueryStringUtils.escapeLuceneSpecialCharacters(str);
44+
return "\"" + str + "\"";
45+
}
46+
47+
// Regular string - escape special characters
48+
return QueryStringUtils.escapeLuceneSpecialCharacters(str);
49+
}
50+
}
51+
52+
// Default: escape the text representation
53+
String text = literal.toString();
54+
return QueryStringUtils.escapeLuceneSpecialCharacters(text);
55+
}
56+
57+
@Override
58+
public List<? extends UnresolvedExpression> getChild() {
59+
return Collections.singletonList(literal);
60+
}
61+
}

0 commit comments

Comments
 (0)