Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
139c35b
SEARCH-802 - bug fixed - Query rules allows for creation of rules wit…
mridula-s109 Feb 18, 2025
5e1a815
Merge branch 'main' into numeric-bugfix
mridula-s109 Feb 18, 2025
085cec0
[CI] Auto commit changes from spotless
Feb 18, 2025
0240723
Merge branch 'main' into numeric-bugfix
mridula-s109 Feb 18, 2025
da6c113
Worked on the comments given in the PR
mridula-s109 Feb 20, 2025
2161e1b
[CI] Auto commit changes from spotless
Feb 20, 2025
910cda4
Fixed Integration tests
mridula-s109 Feb 20, 2025
da07f22
[CI] Auto commit changes from spotless
Feb 20, 2025
1887bb0
Merge branch 'main' into numeric-bugfix
mridula-s109 Feb 20, 2025
7f9399d
Merge branch 'main' into numeric-bugfix
mridula-s109 Feb 20, 2025
3acf750
Made changes from the PR
mridula-s109 Feb 20, 2025
00571cd
Update docs/changelog/122823.yaml
mridula-s109 Feb 20, 2025
58fe5d4
[CI] Auto commit changes from spotless
Feb 20, 2025
efb0828
Merge branch 'main' into numeric-bugfix
kderusso Feb 20, 2025
74cd5d6
Fixed the duplicate code issue in queryRuleTests
mridula-s109 Feb 21, 2025
f5dc56e
Merge branch 'main' into numeric-bugfix
mridula-s109 Feb 26, 2025
3f023c4
Refactored code to clean it up based on PR comments
mridula-s109 Feb 26, 2025
6a77cec
[CI] Auto commit changes from spotless
Feb 26, 2025
120d755
Merge branch 'main' into numeric-bugfix
mridula-s109 Mar 3, 2025
ce1e953
Merge branch 'main' into numeric-bugfix
mridula-s109 Mar 3, 2025
c8d61c1
Merge branch 'main' into numeric-bugfix
mridula-s109 Mar 3, 2025
ab9f383
Logger statements were removed
mridula-s109 Mar 4, 2025
d1070fd
Cleaned up the QueryRule tests
mridula-s109 Mar 4, 2025
a2f8850
[CI] Auto commit changes from spotless
Mar 4, 2025
ede5605
Merge branch 'main' into numeric-bugfix
mridula-s109 Mar 4, 2025
ecc27e9
Update x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack…
mridula-s109 Mar 11, 2025
96e1fa4
Merge branch 'main' into numeric-bugfix
mridula-s109 Mar 11, 2025
5b572f2
[CI] Auto commit changes from spotless
Mar 11, 2025
fc17b83
Merge branch 'main' into numeric-bugfix
mridula-s109 Mar 11, 2025
c26535c
Merge branch 'main' into numeric-bugfix
mridula-s109 Mar 11, 2025
5878c9c
Merge branch 'main' into numeric-bugfix
mridula-s109 Mar 12, 2025
e3b4577
resolved merge conflicts
mridula-s109 Mar 3, 2025
73f5143
Removed logger
mridula-s109 Mar 3, 2025
f1dc548
Trigger PR update
mridula-s109 Mar 3, 2025
2691f23
Fixed checkstyle issues
mridula-s109 Mar 12, 2025
aa52930
resolved compile errors
mridula-s109 Mar 12, 2025
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 @@ -15,6 +15,16 @@ teardown:
ruleset_id: test-query-ruleset-recreating
ignore: 404

- do:
query_rules.delete_ruleset:
ruleset_id: invalid_numeric_test
ignore: 404

- do:
query_rules.delete_ruleset:
ruleset_id: multiple_values_test
ignore: 404

---
'Create Query Ruleset':
- do:
Expand Down Expand Up @@ -160,3 +170,51 @@ teardown:
- 'id2'

- match: { error.type: 'security_exception' }

---
"Test numeric comparison rule validation":
- skip:
features: headers

- requires:
cluster_features: "query_rules.numeric_validation"
reason: "Query rules enforce numeric validation starting with 8.19"

- do:
catch: /\[query_ruleset\] failed to parse field \[rules\]/
query_rules.put_ruleset:
ruleset_id: invalid_numeric_test
body:
rules:
- rule_id: invalid_numeric_rule
type: pinned
criteria:
- type: lte
metadata: price
values: ["not_a_number"]
actions:
ids: ["1"]

---
"Test numeric comparison with multiple values":
- skip:
features: headers

- requires:
cluster_features: "query_rules.numeric_validation"
reason: "Query rules enforce numeric validation starting with 8.19"

- do:
catch: /\[query_ruleset\] failed to parse field \[rules\]/
query_rules.put_ruleset:
ruleset_id: multiple_values_test
body:
rules:
- rule_id: multiple_values_rule
type: pinned
criteria:
- type: lte
metadata: price
values: ["100", "not_a_number"]
actions:
ids: ["1"]
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,20 @@

import org.elasticsearch.features.FeatureSpecification;
import org.elasticsearch.features.NodeFeature;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.xpack.application.rules.QueryRule;

import java.util.Set;

public class EnterpriseSearchFeatures implements FeatureSpecification {
public class EnterpriseSearchFeatures extends Plugin implements FeatureSpecification {

@Override
public Set<NodeFeature> getFeatures() {
return Set.of();
}

@Override
public Set<NodeFeature> getTestFeatures() {
return Set.of(QueryRule.NUMERIC_VALIDATION);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.features.NodeFeature;
import org.elasticsearch.xcontent.ConstructingObjectParser;
import org.elasticsearch.xcontent.ParseField;
import org.elasticsearch.xcontent.ToXContentObject;
Expand Down Expand Up @@ -81,6 +82,8 @@ public String toString() {
}
}

public static final NodeFeature NUMERIC_VALIDATION = new NodeFeature("query_rules.numeric_validation", true);

/**
* Public constructor.
*
Expand Down Expand Up @@ -140,7 +143,6 @@ public QueryRule(StreamInput in) throws IOException {
}

private void validate() {

if (priority != null && (priority < MIN_PRIORITY || priority > MAX_PRIORITY)) {
throw new IllegalArgumentException("Priority was " + priority + ", must be between " + MIN_PRIORITY + " and " + MAX_PRIORITY);
}
Expand All @@ -155,6 +157,13 @@ private void validate() {
throw new ElasticsearchParseException(type.toString() + " query rule actions must contain only one of either ids or docs");
}
}

criteria.forEach(criterion -> {
List<Object> values = criterion.criteriaValues();
if (values != null) {
values.forEach(criterion.criteriaType()::validateInput);
}
});
}

private void validateIdOrDocAction(Object action) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
import org.elasticsearch.xpack.application.search.TemplateParamValidator;
import org.elasticsearch.xpack.core.action.util.PageParams;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
Expand All @@ -30,15 +31,14 @@
import static org.elasticsearch.test.ESTestCase.generateRandomStringArray;
import static org.elasticsearch.test.ESTestCase.randomAlphaOfLengthBetween;
import static org.elasticsearch.test.ESTestCase.randomBoolean;
import static org.elasticsearch.test.ESTestCase.randomDoubleBetween;
import static org.elasticsearch.test.ESTestCase.randomFrom;
import static org.elasticsearch.test.ESTestCase.randomIdentifier;
import static org.elasticsearch.test.ESTestCase.randomIntBetween;
import static org.elasticsearch.test.ESTestCase.randomList;
import static org.elasticsearch.test.ESTestCase.randomLongBetween;
import static org.elasticsearch.test.ESTestCase.randomMap;
import static org.elasticsearch.xpack.application.rules.QueryRule.MAX_PRIORITY;
import static org.elasticsearch.xpack.application.rules.QueryRule.MIN_PRIORITY;
import static org.elasticsearch.xpack.application.rules.QueryRuleCriteriaType.ALWAYS;

public final class EnterpriseSearchModuleTestUtils {

Expand Down Expand Up @@ -86,19 +86,41 @@ public static Map<String, Object> randomSearchApplicationQueryParams() {
return randomMap(0, 10, () -> Tuple.tuple(randomIdentifier(), randomAlphaOfLengthBetween(0, 10)));
}

public static QueryRuleCriteria randomQueryRuleCriteria() {
// We intentionally don't allow ALWAYS criteria in this method, since we want to test parsing metadata and values
QueryRuleCriteriaType type = randomFrom(Arrays.stream(QueryRuleCriteriaType.values()).filter(t -> t != ALWAYS).toList());
return new QueryRuleCriteria(type, randomAlphaOfLengthBetween(1, 10), randomList(1, 5, () -> randomAlphaOfLengthBetween(1, 10)));
}

public static QueryRule randomQueryRule() {
String id = randomIdentifier();
String ruleId = randomAlphaOfLengthBetween(3, 10);
QueryRule.QueryRuleType type = randomFrom(QueryRule.QueryRuleType.values());
List<QueryRuleCriteria> criteria = List.of(randomQueryRuleCriteria());
Map<String, Object> actions = Map.of(randomFrom("ids", "docs"), List.of(randomAlphaOfLengthBetween(2, 10)));
Integer priority = randomQueryRulePriority();
return new QueryRule(id, type, criteria, actions, priority);
List<QueryRuleCriteria> criteria = new ArrayList<>();
int numCriteria = randomIntBetween(1, 3);

for (int i = 0; i < numCriteria; i++) {
criteria.add(randomQueryRuleCriteria());
}

return new QueryRule(ruleId, type, criteria, randomQueryRuleActions(), randomQueryRulePriority());
}

public static QueryRuleCriteria randomQueryRuleCriteria() {
QueryRuleCriteriaType criteriaType = randomFrom(QueryRuleCriteriaType.values());
String metadata = randomAlphaOfLengthBetween(3, 10);
List<Object> values = new ArrayList<>();
int numValues = randomIntBetween(1, 3);

if (List.of(QueryRuleCriteriaType.GT, QueryRuleCriteriaType.GTE, QueryRuleCriteriaType.LT, QueryRuleCriteriaType.LTE)
.contains(criteriaType)) {
for (int i = 0; i < numValues; i++) {
BigDecimal value = BigDecimal.valueOf(randomDoubleBetween(0, 1000, true)).setScale(2, RoundingMode.HALF_UP);
values.add(value.toString());
}
} else if (criteriaType == QueryRuleCriteriaType.ALWAYS) {
metadata = null;
values = null;
} else {
for (int i = 0; i < numValues; i++) {
values.add(randomAlphaOfLengthBetween(3, 10));
}
}

return new QueryRuleCriteria(criteriaType, metadata, values);
}

public static Integer randomQueryRulePriority() {
Expand All @@ -115,8 +137,24 @@ public static QueryRuleset randomQueryRuleset() {
return new QueryRuleset(id, rules);
}

public static Map<String, Object> randomQueryRuleActions() {
if (randomBoolean()) {
return Map.of(QueryRule.IDS_FIELD.getPreferredName(), List.of(randomAlphaOfLengthBetween(3, 10)));
}
return Map.of(
QueryRule.DOCS_FIELD.getPreferredName(),
List.of(
Map.of(
QueryRule.INDEX_FIELD.getPreferredName(),
randomAlphaOfLengthBetween(3, 10),
QueryRule.ID_FIELD.getPreferredName(),
randomAlphaOfLengthBetween(3, 10)
)
)
);
}

public static Map<String, Object> randomMatchCriteria() {
return randomMap(1, 3, () -> Tuple.tuple(randomIdentifier(), randomAlphaOfLengthBetween(0, 10)));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import org.elasticsearch.search.SearchModule;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xcontent.ToXContent;
import org.elasticsearch.xcontent.XContentBuilder;
import org.elasticsearch.xcontent.XContentFactory;
import org.elasticsearch.xcontent.XContentParser;
import org.elasticsearch.xcontent.XContentType;
import org.elasticsearch.xpack.application.EnterpriseSearchModuleTestUtils;
Expand All @@ -30,6 +32,7 @@
import static org.elasticsearch.common.xcontent.XContentHelper.toXContent;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXContentEquivalent;
import static org.elasticsearch.xpack.application.rules.QueryRuleCriteriaType.EXACT;
import static org.elasticsearch.xpack.application.rules.QueryRuleCriteriaType.LTE;
import static org.elasticsearch.xpack.application.rules.QueryRuleCriteriaType.PREFIX;
import static org.elasticsearch.xpack.application.rules.QueryRuleCriteriaType.SUFFIX;
import static org.hamcrest.CoreMatchers.equalTo;
Expand All @@ -53,6 +56,73 @@ public final void testRandomSerialization() throws IOException {
}
}

public void testNumericValidationWithValidValues() throws IOException {
String content = XContentHelper.stripWhitespace("""
{
"rule_id": "numeric_rule",
"type": "pinned",
"criteria": [
{ "type": "lte", "metadata": "price", "values": ["100.50", "200"] }
],
"actions": {
"ids": ["id1"]
}
}""");
QueryRule queryRule = QueryRule.fromXContentBytes(new BytesArray(content), XContentType.JSON);
boolean humanReadable = true;
BytesReference originalBytes = toShuffledXContent(queryRule, XContentType.JSON, ToXContent.EMPTY_PARAMS, humanReadable);
QueryRule parsed;
try (XContentParser parser = createParser(XContentType.JSON.xContent(), originalBytes)) {
parsed = QueryRule.fromXContent(parser);
}
assertToXContentEquivalent(originalBytes, toXContent(parsed, XContentType.JSON, humanReadable), XContentType.JSON);
}

public void testNumericValidationWithInvalidValues() throws IOException {
String content = XContentHelper.stripWhitespace("""
{
"rule_id": "numeric_rule",
"type": "pinned",
"criteria": [
{ "type": "lte", "metadata": "price", "values": ["abc"] }
],
"actions": {
"ids": ["id1"]
}
}""");
expectThrows(IllegalArgumentException.class, () -> QueryRule.fromXContentBytes(new BytesArray(content), XContentType.JSON));
}

public void testNumericValidationWithMixedValues() throws IOException {
String content = XContentHelper.stripWhitespace("""
{
"rule_id": "numeric_rule",
"type": "pinned",
"criteria": [
{ "type": "lte", "metadata": "price", "values": ["100", "abc", "200"] }
],
"actions": {
"ids": ["id1"]
}
}""");
expectThrows(IllegalArgumentException.class, () -> QueryRule.fromXContentBytes(new BytesArray(content), XContentType.JSON));
}

public void testNumericValidationWithEmptyValues() throws IOException {
String content = XContentHelper.stripWhitespace("""
{
"rule_id": "numeric_rule",
"type": "pinned",
"criteria": [
{ "type": "lte", "metadata": "price", "values": [] }
],
"actions": {
"ids": ["id1"]
}
}""");
expectThrows(IllegalArgumentException.class, () -> QueryRule.fromXContentBytes(new BytesArray(content), XContentType.JSON));
}

public void testToXContent() throws IOException {
String content = XContentHelper.stripWhitespace("""
{
Expand Down Expand Up @@ -333,6 +403,62 @@ public void testApplyRuleWithMultipleCriteria() throws IOException {
assertEquals(Collections.emptyList(), appliedQueryRules.excludedDocs());
}

public void testValidateNumericComparisonRule() {
IllegalArgumentException e = expectThrows(
IllegalArgumentException.class,
() -> new QueryRule(
"test_rule",
QueryRule.QueryRuleType.PINNED,
List.of(new QueryRuleCriteria(QueryRuleCriteriaType.LTE, "price", List.of("not_a_number"))),
Map.of("ids", List.of("1")),
null
)
);
assertEquals("Input [not_a_number] is not valid for CriteriaType [lte]", e.getMessage());
}

public void testParseNumericComparisonRule() throws IOException {
XContentBuilder builder = XContentFactory.jsonBuilder();
builder.startObject();
{
builder.field("rule_id", "test_rule");
builder.field("type", "pinned");
builder.startArray("criteria");
{
builder.startObject();
builder.field("type", "lte");
builder.field("metadata", "price");
builder.startArray("values").value("100").endArray();
builder.endObject();
}
builder.endArray();
builder.startObject("actions");
{
builder.startArray("ids").value("1").endArray();
}
builder.endObject();
}
builder.endObject();

BytesReference bytesRef = BytesReference.bytes(builder);
QueryRule rule = QueryRule.fromXContentBytes(bytesRef, builder.contentType());
assertNotNull(rule);
assertEquals("test_rule", rule.id());
assertEquals(QueryRule.QueryRuleType.PINNED, rule.type());
assertEquals(1, rule.criteria().size());
assertEquals(LTE, rule.criteria().get(0).criteriaType());
assertEquals("100", rule.criteria().get(0).criteriaValues().get(0));
}

public void testIsRuleMatchWithNumericComparison() {
QueryRuleCriteria criteria = new QueryRuleCriteria(LTE, "price", List.of("100"));
QueryRule rule = new QueryRule("test_rule", QueryRule.QueryRuleType.PINNED, List.of(criteria), Map.of("ids", List.of("1")), null);

assertTrue(rule.isRuleMatch(Map.of("price", "50")));
assertFalse(rule.isRuleMatch(Map.of("price", "150")));
assertFalse(rule.isRuleMatch(Map.of("price", "not_a_number")));
}

private void assertXContent(QueryRule queryRule, boolean humanReadable) throws IOException {
BytesReference originalBytes = toShuffledXContent(queryRule, XContentType.JSON, ToXContent.EMPTY_PARAMS, humanReadable);
QueryRule parsed;
Expand Down