Skip to content

Commit 4a282e4

Browse files
committed
Adds Querying for nested SME's
1 parent d542f23 commit 4a282e4

3 files changed

Lines changed: 161 additions & 5 deletions

File tree

  • basyx.common/basyx.querycore/src/main/java/org/eclipse/digitaltwin/basyx/querycore/query
  • basyx.submodelrepository/basyx.submodelrepository-feature-search/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/search

basyx.common/basyx.querycore/src/main/java/org/eclipse/digitaltwin/basyx/querycore/query/converter/ValueConverter.java

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ public Query convertEqualityComparison(Value leftValue, Value rightValue) {
3434
Object value = leftField != null ? extractValue(rightValue) : extractValue(leftValue);
3535

3636
if (fieldName != null && value != null) {
37+
// Check if this is an SME wildcard field
38+
if (isSmeWildcardField(fieldName)) {
39+
return createSmeWildcardQuery(fieldName, value.toString());
40+
}
41+
3742
return QueryBuilders.term()
3843
.field(fieldName)
3944
.value(convertToFieldValue(value))
@@ -60,6 +65,13 @@ public Query convertInequalityComparison(Value leftValue, Value rightValue) {
6065
Object value = leftField != null ? extractValue(rightValue) : extractValue(leftValue);
6166

6267
if (fieldName != null && value != null) {
68+
// Check if this is an SME wildcard field
69+
if (isSmeWildcardField(fieldName)) {
70+
return QueryBuilders.bool()
71+
.mustNot(createSmeWildcardQuery(fieldName, value.toString()))
72+
.build()._toQuery();
73+
}
74+
6375
return QueryBuilders.bool()
6476
.mustNot(QueryBuilders.term()
6577
.field(fieldName)
@@ -98,6 +110,11 @@ public Query convertRangeComparison(Value leftValue,
98110
Object rawValue = leftField != null ? extractValue(rightValue) : extractValue(leftValue);
99111

100112
if (fieldName != null && rawValue != null) {
113+
// Check if this is an SME wildcard field
114+
if (isSmeWildcardField(fieldName)) {
115+
return createSmeWildcardRangeQuery(fieldName, rawValue, operator);
116+
}
117+
101118
JsonData value = JsonData.of(rawValue); // works for all scalar types
102119

103120
return QueryBuilders.range(r -> r
@@ -137,6 +154,11 @@ public Query convertStringComparison(StringValue leftValue, StringValue rightVal
137154
String value = leftField != null ? extractStringValue(rightValue) : extractStringValue(leftValue);
138155

139156
if (fieldName != null && value != null) {
157+
// Check if this is an SME wildcard field
158+
if (isSmeWildcardField(fieldName)) {
159+
return createSmeWildcardStringQuery(fieldName, value, operation);
160+
}
161+
140162
switch (operation) {
141163
case "contains":
142164
return QueryBuilders.wildcard()
@@ -375,6 +397,8 @@ private String convertModelFieldToElasticField(String modelField) {
375397
result = modelField.replace("$sm#", "");
376398
} else if (modelField.startsWith("$sme")) {
377399
result = modelField.replaceFirst("\\$sme(?:\\.[^#]*)?#", "");
400+
// Mark as SME field for wildcard handling
401+
result = "SME_WILDCARD:" + result;
378402
} else if (modelField.startsWith("$cd#")) {
379403
result = modelField.replace("$cd#", "");
380404
} else if (modelField.startsWith("$aasdesc#")) {
@@ -615,4 +639,135 @@ private String generateCastScript(String fieldExpression, String castType) {
615639
return fieldExpression; // No casting applied
616640
}
617641
}
642+
643+
/**
644+
* Checks if a field is an SME wildcard field that needs special handling
645+
*/
646+
private boolean isSmeWildcardField(String fieldName) {
647+
return fieldName != null && fieldName.startsWith("SME_WILDCARD:");
648+
}
649+
650+
/**
651+
* Extracts the actual field name from an SME wildcard field
652+
*/
653+
private String extractSmeFieldName(String wildcardField) {
654+
if (wildcardField.startsWith("SME_WILDCARD:")) {
655+
return wildcardField.substring("SME_WILDCARD:".length());
656+
}
657+
return wildcardField;
658+
}
659+
660+
/**
661+
* Creates a wildcard query using QueryBuilders.queryString for SME fields at any nesting level
662+
*/
663+
private Query createSmeWildcardQuery(String wildcardField, String value) {
664+
String fieldName = extractSmeFieldName(wildcardField);
665+
666+
// Add .keyword suffix for string fields that need exact matching
667+
String searchField = fieldName;
668+
669+
// Create a wildcard pattern that matches the field at any nesting level
670+
// Pattern: submodelElements.*{fieldName}:{value} OR submodelElements.*.smcChildren.*{fieldName}:{value}
671+
String queryPattern = "submodelElements.*."+searchField;
672+
673+
return QueryBuilders.queryString(q -> q
674+
.query(value)
675+
.fields(queryPattern)
676+
);
677+
}
678+
679+
/**
680+
* Creates a wildcard string query using QueryBuilders.queryString for SME fields at any nesting level
681+
*/
682+
private Query createSmeWildcardStringQuery(String wildcardField, String value, String operation) {
683+
String fieldName = extractSmeFieldName(wildcardField);
684+
685+
// Add .keyword suffix for string fields that need exact matching
686+
String searchField = fieldName;
687+
if (isStringField(fieldName)) {
688+
searchField = fieldName + ".keyword";
689+
}
690+
691+
String searchValue;
692+
switch (operation) {
693+
case "contains":
694+
searchValue = "*" + escapeQueryString(value) + "*";
695+
break;
696+
case "starts-with":
697+
searchValue = escapeQueryString(value) + "*";
698+
break;
699+
case "ends-with":
700+
searchValue = "*" + escapeQueryString(value);
701+
break;
702+
case "regex":
703+
// For regex, use the value as-is (queryString supports regex)
704+
searchValue = value;
705+
break;
706+
default:
707+
searchValue = escapeQueryString(value);
708+
break;
709+
}
710+
711+
// Create a wildcard pattern that matches the field at any nesting level
712+
String queryPattern = "submodelElements.*."+searchField;
713+
714+
return QueryBuilders.queryString(q -> q
715+
.query(value)
716+
.fields(queryPattern)
717+
);
718+
}
719+
720+
/**
721+
* Creates a wildcard range query using QueryBuilders.queryString for SME fields at any nesting level
722+
*/
723+
private Query createSmeWildcardRangeQuery(String wildcardField, Object value, String operator) {
724+
String fieldName = extractSmeFieldName(wildcardField);
725+
726+
// Add .keyword suffix for string fields that need exact matching
727+
String searchField = fieldName;
728+
if (isStringField(fieldName)) {
729+
searchField = fieldName + ".keyword";
730+
}
731+
732+
String rangeOperator;
733+
switch (operator) {
734+
case "gt": rangeOperator = ">"; break;
735+
case "gte": rangeOperator = ">="; break;
736+
case "lt": rangeOperator = "<"; break;
737+
case "lte": rangeOperator = "<="; break;
738+
default: throw new IllegalArgumentException("Unsupported range operator: " + operator);
739+
}
740+
741+
// Create a wildcard pattern that matches the field at any nesting level with range comparison
742+
String queryPattern = "submodelElements.*."+searchField;
743+
744+
return QueryBuilders.queryString(q -> q
745+
.query(value.toString())
746+
.fields(queryPattern)
747+
);
748+
}
749+
750+
/**
751+
* Escapes special characters for Elasticsearch query string queries
752+
*/
753+
private String escapeQueryString(String value) {
754+
// Escape special query string characters
755+
return value.replace("\\", "\\\\")
756+
.replace("\"", "\\\"")
757+
.replace("+", "\\+")
758+
.replace("-", "\\-")
759+
.replace("=", "\\=")
760+
.replace("&&", "\\&&")
761+
.replace("||", "\\||")
762+
.replace("!", "\\!")
763+
.replace("(", "\\(")
764+
.replace(")", "\\)")
765+
.replace("{", "\\{")
766+
.replace("}", "\\}")
767+
.replace("[", "\\[")
768+
.replace("]", "\\]")
769+
.replace("^", "\\^")
770+
.replace("~", "\\~")
771+
.replace(":", "\\:");
772+
}
618773
}

basyx.common/basyx.querycore/src/main/java/org/eclipse/digitaltwin/basyx/querycore/query/executor/ESQueryExecutor.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import com.fasterxml.jackson.databind.JsonNode;
1010
import com.fasterxml.jackson.databind.ObjectMapper;
1111
import com.fasterxml.jackson.databind.node.ObjectNode;
12-
import org.eclipse.digitaltwin.aas4j.v3.model.*;
1312
import org.eclipse.digitaltwin.basyx.http.pagination.Base64UrlEncodedCursor;
1413
import org.eclipse.digitaltwin.basyx.querycore.query.model.AASQuery;
1514
import org.eclipse.digitaltwin.basyx.querycore.query.model.QueryPaging;
@@ -72,7 +71,7 @@ public QueryResponse executeQueryAndGetResponse(AASQuery query, Integer limit, B
7271
for(Object hit : objectHits){
7372
if (hit instanceof ObjectNode) {
7473
ObjectNode node = (ObjectNode) hit;
75-
rewriteRecursively(node);
74+
rewriteIterative(node);
7675
} else {
7776
logger.warn("Hit is not an ObjectNode: {}", hit.getClass().getName());
7877
}
@@ -175,7 +174,7 @@ private static void move(ObjectNode node, String from, String to) {
175174
if (v != null) node.set(to, v);
176175
}
177176

178-
public void rewriteRecursively(JsonNode node){
177+
public void rewriteIterative(JsonNode node){
179178
if (node == null || !node.isObject()) return;
180179

181180
Stack<JsonNode> nodeStack = new Stack<>();

basyx.submodelrepository/basyx.submodelrepository-feature-search/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/search/IndexNormalizer.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,12 @@ private static void rewriteIteratively(JsonNode rootNode, Object rootSourceObjec
5858
move(obj, "value", "smcChildren");
5959
} else if (sourceObject instanceof SubmodelElementList) {
6060
move(obj, "value", "smlChildren");
61-
} else if (sourceObject instanceof Reference) {
62-
move(obj, "value", "referenceChildren");
61+
} else if (sourceObject instanceof ReferenceElement) {
62+
move(obj, "value", "referenceElementChildren");
6363
} else if (sourceObject instanceof MultiLanguageProperty) {
6464
move(obj, "value", "langContent");
65+
} else {
66+
move(obj, "value", "peps");
6567
}
6668
}
6769

0 commit comments

Comments
 (0)