Skip to content

Commit 9bc89ad

Browse files
committed
feat(ecl): support multivalued string evaluation in concrete values
1 parent b744266 commit 9bc89ad

File tree

4 files changed

+81
-32
lines changed

4 files changed

+81
-32
lines changed

snomed/com.b2international.snowowl.snomed.datastore.tests/src/com/b2international/snowowl/snomed/core/ecl/SnomedEclEvaluationRequestTest.java

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2011-2021 B2i Healthcare Pte Ltd, http://b2i.sg
2+
* Copyright 2011-2022 B2i Healthcare Pte Ltd, http://b2i.sg
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -918,6 +918,18 @@ public void refinementStringNotEquals() throws Exception {
918918
assertEquals(expected, actual);
919919
}
920920

921+
@Test
922+
public void refinementStringManyValuedEquals() throws Exception {
923+
generateDrugHierarchy();
924+
925+
final Expression actual = eval(String.format("<%s: %s = ('PANADOL' 'TRIPHASIL')", DRUG_ROOT, HAS_TRADE_NAME));
926+
final Expression expected = and(
927+
descendantsOf(DRUG_ROOT),
928+
ids(Set.of(PANADOL_TABLET, TRIPHASIL_TABLET))
929+
);
930+
assertEquals(expected, actual);
931+
}
932+
921933
@Test
922934
public void refinementAnyStringNotEquals() throws Exception {
923935
generateDrugHierarchy();

snomed/com.b2international.snowowl.snomed.datastore.tests/src/com/b2international/snowowl/snomed/datastore/AllSnomedDatastoreTests.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2011-2021 B2i Healthcare Pte Ltd, http://b2i.sg
2+
* Copyright 2011-2022 B2i Healthcare Pte Ltd, http://b2i.sg
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.

snomed/com.b2international.snowowl.snomed.datastore/src/com/b2international/snowowl/snomed/core/domain/RelationshipValue.java

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 B2i Healthcare Pte Ltd, http://b2i.sg
2+
* Copyright 2021-2022 B2i Healthcare Pte Ltd, http://b2i.sg
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,10 +20,12 @@
2020

2121
import java.io.Serializable;
2222
import java.math.BigDecimal;
23+
import java.util.List;
2324
import java.util.Objects;
2425
import java.util.function.Consumer;
2526
import java.util.function.Function;
2627

28+
import com.b2international.commons.CompareUtils;
2729
import com.b2international.commons.exceptions.BadRequestException;
2830
import com.b2international.snowowl.core.request.SearchResourceRequest.Operator;
2931
import com.fasterxml.jackson.annotation.JsonCreator;
@@ -286,6 +288,10 @@ public boolean matches(final Operator operator, final RelationshipValue other) {
286288
default: throw new IllegalStateException("Unexpected operator '" + operator + "'.");
287289
}
288290
}
291+
292+
public boolean matchesAny(Operator operator, List<RelationshipValue> values) {
293+
return CompareUtils.isEmpty(values) ? false : values.stream().anyMatch(value -> matches(operator, value));
294+
}
289295

290296
@Override
291297
public boolean equals(final Object obj) {
@@ -314,4 +320,5 @@ public int hashCode() {
314320
public String toString() {
315321
return toLiteral();
316322
}
323+
317324
}

snomed/com.b2international.snowowl.snomed.datastore/src/com/b2international/snowowl/snomed/core/ecl/SnomedEclRefinementEvaluator.java

+59-29
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2011-2021 B2i Healthcare Pte Ltd, http://b2i.sg
2+
* Copyright 2011-2022 B2i Healthcare Pte Ltd, http://b2i.sg
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -22,11 +22,8 @@
2222
import static com.google.common.collect.Sets.newHashSet;
2323

2424
import java.io.IOException;
25-
import java.util.Collection;
26-
import java.util.List;
25+
import java.util.*;
2726
import java.util.Map.Entry;
28-
import java.util.Objects;
29-
import java.util.Set;
3027
import java.util.concurrent.TimeUnit;
3128
import java.util.function.BinaryOperator;
3229
import java.util.stream.Collectors;
@@ -35,6 +32,7 @@
3532

3633
import com.b2international.commons.CompareUtils;
3734
import com.b2international.commons.exceptions.BadRequestException;
35+
import com.b2international.commons.exceptions.NotImplementedException;
3836
import com.b2international.commons.options.Options;
3937
import com.b2international.index.query.Expression;
4038
import com.b2international.index.query.Expressions;
@@ -380,19 +378,27 @@ private Promise<Collection<Property>> evalRefinement(final BranchContext context
380378
}
381379

382380
private Promise<Collection<Property>> evalMembers(BranchContext context, Set<String> focusConceptIds, Collection<String> typeIds, DataTypeComparison comparison) {
383-
final Object value;
381+
final List<Object> values;
384382
final DataType type;
385383
if (comparison instanceof BooleanValueComparison) {
386-
value = ((BooleanValueComparison) comparison).isValue();
384+
values = List.of(((BooleanValueComparison) comparison).isValue());
387385
type = DataType.BOOLEAN;
388386
} else if (comparison instanceof StringValueComparison) {
389-
value = ((StringValueComparison) comparison).getValue();
387+
StringValueComparison stringValueComparison = (StringValueComparison) comparison;
388+
SearchTerm searchTerm = stringValueComparison.getValue();
389+
if (searchTerm instanceof TypedSearchTerm) {
390+
values = List.of(extractTerm(((TypedSearchTerm) searchTerm).getClause()));
391+
} else if (searchTerm instanceof TypedSearchTermSet) {
392+
values = ((TypedSearchTermSet) searchTerm).getClauses().stream().map(this::extractTerm).collect(Collectors.toList());
393+
} else {
394+
return SnomedEclEvaluationRequest.throwUnsupported(searchTerm);
395+
}
390396
type = DataType.STRING;
391397
} else if (comparison instanceof IntegerValueComparison) {
392-
value = ((IntegerValueComparison) comparison).getValue();
398+
values = List.of(((IntegerValueComparison) comparison).getValue());
393399
type = DataType.INTEGER;
394400
} else if (comparison instanceof DecimalValueComparison) {
395-
value = ((DecimalValueComparison) comparison).getValue();
401+
values = List.of(((DecimalValueComparison) comparison).getValue());
396402
type = DataType.DECIMAL;
397403
} else {
398404
return SnomedEclEvaluationRequest.throwUnsupported(comparison);
@@ -404,7 +410,7 @@ private Promise<Collection<Property>> evalMembers(BranchContext context, Set<Str
404410
.put(SnomedRf2Headers.FIELD_CHARACTERISTIC_TYPE_ID, getCharacteristicTypes(expressionForm))
405411
.put(SnomedRf2Headers.FIELD_TYPE_ID, typeIds)
406412
.put(SnomedRefSetMemberIndexEntry.Fields.DATA_TYPE, type)
407-
.put(SnomedRf2Headers.FIELD_VALUE, value)
413+
.put(SnomedRf2Headers.FIELD_VALUE, values)
408414
.put(SearchResourceRequest.operator(SnomedRf2Headers.FIELD_VALUE), operator)
409415
.build();
410416

@@ -434,40 +440,52 @@ private Promise<Collection<Property>> evalStatementsWithValue(
434440
Collection<String> typeIds,
435441
DataTypeComparison comparison) {
436442

437-
final RelationshipValue value;
443+
final List<RelationshipValue> values;
438444
final SearchResourceRequest.Operator operator = toSearchOperator(comparison.getOp());
439445

440446
// XXX: no boolean comparison for relationships with value!
441447
if (comparison instanceof BooleanValueComparison) {
442448
return Promise.immediate(List.of());
443449
} else if (comparison instanceof StringValueComparison) {
444-
value = new RelationshipValue(((StringValueComparison) comparison).getValue());
450+
StringValueComparison stringValueComparison = (StringValueComparison) comparison;
451+
SearchTerm value = stringValueComparison.getValue();
452+
if (value instanceof TypedSearchTerm) {
453+
values = List.of(toRelationshipValue(((TypedSearchTerm) value).getClause()));
454+
} else if (value instanceof TypedSearchTermSet) {
455+
values = ((TypedSearchTermSet) value).getClauses().stream().map(this::toRelationshipValue).collect(Collectors.toList());
456+
} else {
457+
return SnomedEclEvaluationRequest.throwUnsupported(value);
458+
}
445459
} else if (comparison instanceof IntegerValueComparison) {
446-
value = new RelationshipValue(((IntegerValueComparison) comparison).getValue());
460+
values = List.of(new RelationshipValue(((IntegerValueComparison) comparison).getValue()));
447461
} else if (comparison instanceof DecimalValueComparison) {
448-
value = new RelationshipValue(((DecimalValueComparison) comparison).getValue());
462+
values = List.of(new RelationshipValue(((DecimalValueComparison) comparison).getValue()));
449463
} else {
450464
return SnomedEclEvaluationRequest.throwUnsupported(comparison);
451465
}
452466

453-
return evalStatementsWithValue(context, focusConceptIds, typeIds, value, operator);
467+
return evalStatementsWithValue(context, focusConceptIds, typeIds, values, operator);
454468
}
455469

456470
private Promise<Collection<Property>> evalStatementsWithValue(
457471
final BranchContext context,
458472
final Set<String> focusConceptIds,
459473
final Collection<String> typeIds,
460-
final RelationshipValue value,
474+
final List<RelationshipValue> values,
461475
final SearchResourceRequest.Operator operator) {
462476

477+
if (CompareUtils.isEmpty(values)) {
478+
return Promise.immediate(Collections.emptyList());
479+
}
480+
463481
// TODO: does this request need to support filtering by group?
464482
Promise<Collection<Property>> statementsWithValue = SnomedRequests.prepareSearchRelationship()
465483
.filterByActive(true)
466484
.filterByCharacteristicTypes(getCharacteristicTypes(expressionForm))
467485
.filterBySources(focusConceptIds)
468486
.filterByTypes(typeIds)
469-
.filterByValueType(value.type())
470-
.filterByValue(operator, value)
487+
.filterByValueType(Iterables.getFirst(values, null).type())
488+
.filterByValues(operator, values)
471489
.setEclExpressionForm(expressionForm)
472490
.setFields(ID, SOURCE_ID, TYPE_ID, RELATIONSHIP_GROUP, VALUE_TYPE, NUMERIC_VALUE, STRING_VALUE)
473491
.setLimit(context.service(RepositoryConfiguration.class).getIndexConfiguration().getResultWindow())
@@ -481,7 +499,7 @@ private Promise<Collection<Property>> evalStatementsWithValue(
481499
);
482500

483501
if (Trees.STATED_FORM.equals(expressionForm)) {
484-
final Promise<Collection<Property>> axioms = evalAxiomsWithValue(context, focusConceptIds, typeIds, value, operator);
502+
final Promise<Collection<Property>> axioms = evalAxiomsWithValue(context, focusConceptIds, typeIds, values, operator);
485503
return Promise.all(statementsWithValue, axioms).then(results -> {
486504
final Collection<Property> r = (Collection<Property>) results.get(0);
487505
final Collection<Property> a = (Collection<Property>) results.get(1);
@@ -496,23 +514,23 @@ private Promise<Collection<Property>> evalAxiomsWithValue(
496514
BranchContext context,
497515
Set<String> focusConceptIds,
498516
Collection<String> typeIds,
499-
RelationshipValue value,
517+
List<RelationshipValue> values,
500518
SearchResourceRequest.Operator operator) {
501519

502520
// search existing axioms defined for the given set of conceptIds
503-
ExpressionBuilder axiomFilter = Expressions.builder();
521+
ExpressionBuilder axiomFilter = Expressions.bool();
504522

505523
if (typeIds != null) {
506524
axiomFilter.filter(typeIds(typeIds));
507525
}
508526

509527
switch (operator) {
510-
case EQUALS: axiomFilter.filter(values(List.of(value))); break;
511-
case GREATER_THAN: axiomFilter.filter(valueGreaterThan(value, false)); break;
512-
case GREATER_THAN_EQUALS: axiomFilter.filter(valueGreaterThan(value, true)); break;
513-
case LESS_THAN: axiomFilter.filter(valueLessThan(value, false)); break;
514-
case LESS_THAN_EQUALS: axiomFilter.filter(valueLessThan(value, true)); break;
515-
case NOT_EQUALS: axiomFilter.mustNot(values(List.of(value))); break;
528+
case EQUALS: axiomFilter.filter(values(values)); break;
529+
case GREATER_THAN: axiomFilter.filter(valueGreaterThan(Iterables.getFirst(values, null), false)); break;
530+
case GREATER_THAN_EQUALS: axiomFilter.filter(valueGreaterThan(Iterables.getFirst(values, null), true)); break;
531+
case LESS_THAN: axiomFilter.filter(valueLessThan(Iterables.getFirst(values, null), false)); break;
532+
case LESS_THAN_EQUALS: axiomFilter.filter(valueLessThan(Iterables.getFirst(values, null), true)); break;
533+
case NOT_EQUALS: axiomFilter.mustNot(values(values)); break;
516534
default: throw new IllegalStateException("Unexpected operator '" + operator + "'.");
517535
}
518536

@@ -540,7 +558,7 @@ private Promise<Collection<Property>> evalAxiomsWithValue(
540558
owlMember.getClassAxiomRelationships().stream()
541559
.filter(r -> typeIds == null || typeIds.contains(r.getTypeId()))
542560
// We need to find matching OWL relationships in Java
543-
.filter(r -> r.getValueAsObject().matches(operator, value))
561+
.filter(r -> r.getValueAsObject().matchesAny(operator, values))
544562
.map(r -> new Property(
545563
owlMember.getReferencedComponentId(),
546564
r.getTypeId(),
@@ -801,4 +819,16 @@ public String toString() {
801819
return "Property [objectId=" + objectId + ", typeId=" + typeId + ", value=" + value + ", group=" + group + "]";
802820
}
803821
}
822+
823+
private RelationshipValue toRelationshipValue(TypedSearchTermClause clause) {
824+
return new RelationshipValue(extractTerm(clause));
825+
}
826+
827+
private String extractTerm(TypedSearchTermClause clause) {
828+
LexicalSearchType searchType = LexicalSearchType.fromString(clause.getLexicalSearchType());
829+
if (searchType != null && LexicalSearchType.EXACT != searchType) {
830+
throw new NotImplementedException("Not implemented ECL feature: match, wild and regex lexical search types are not supported in concrete value string matching.");
831+
}
832+
return clause.getTerm();
833+
}
804834
}

0 commit comments

Comments
 (0)