feat(search): push AttributeFilter list into ClickHouse WHERE clause
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package com.cameleer.server.app.search;
|
package com.cameleer.server.app.search;
|
||||||
|
|
||||||
import com.cameleer.server.core.alerting.AlertMatchSpec;
|
import com.cameleer.server.core.alerting.AlertMatchSpec;
|
||||||
|
import com.cameleer.server.core.search.AttributeFilter;
|
||||||
import com.cameleer.server.core.search.ExecutionSummary;
|
import com.cameleer.server.core.search.ExecutionSummary;
|
||||||
import com.cameleer.server.core.search.SearchRequest;
|
import com.cameleer.server.core.search.SearchRequest;
|
||||||
import com.cameleer.server.core.search.SearchResult;
|
import com.cameleer.server.core.search.SearchResult;
|
||||||
@@ -256,6 +257,23 @@ public class ClickHouseSearchIndex implements SearchIndex {
|
|||||||
params.add(likeTerm);
|
params.add(likeTerm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Structured attribute filters. Keys were validated at AttributeFilter construction
|
||||||
|
// time against ^[a-zA-Z0-9._-]+$ so they are safe to single-quote-inline; the JSON path
|
||||||
|
// argument of JSONExtractString does not accept a ? placeholder in ClickHouse JDBC
|
||||||
|
// (same constraint as countExecutionsForAlerting below). Values are parameter-bound.
|
||||||
|
for (AttributeFilter filter : request.attributeFilters()) {
|
||||||
|
String escapedKey = filter.key().replace("'", "\\'");
|
||||||
|
if (filter.isKeyOnly()) {
|
||||||
|
conditions.add("JSONHas(attributes, '" + escapedKey + "')");
|
||||||
|
} else if (filter.isWildcard()) {
|
||||||
|
conditions.add("JSONExtractString(attributes, '" + escapedKey + "') LIKE ?");
|
||||||
|
params.add(filter.toLikePattern());
|
||||||
|
} else {
|
||||||
|
conditions.add("JSONExtractString(attributes, '" + escapedKey + "') = ?");
|
||||||
|
params.add(filter.value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return String.join(" AND ", conditions);
|
return String.join(" AND ", conditions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.cameleer.server.app.search;
|
|||||||
|
|
||||||
import com.cameleer.server.app.storage.ClickHouseExecutionStore;
|
import com.cameleer.server.app.storage.ClickHouseExecutionStore;
|
||||||
import com.cameleer.server.core.ingestion.MergedExecution;
|
import com.cameleer.server.core.ingestion.MergedExecution;
|
||||||
|
import com.cameleer.server.core.search.AttributeFilter;
|
||||||
import com.cameleer.server.core.search.ExecutionSummary;
|
import com.cameleer.server.core.search.ExecutionSummary;
|
||||||
import com.cameleer.server.core.search.SearchRequest;
|
import com.cameleer.server.core.search.SearchRequest;
|
||||||
import com.cameleer.server.core.search.SearchResult;
|
import com.cameleer.server.core.search.SearchResult;
|
||||||
@@ -62,7 +63,7 @@ class ClickHouseSearchIndexIT {
|
|||||||
500L,
|
500L,
|
||||||
"", "", "", "", "", "",
|
"", "", "", "", "", "",
|
||||||
"hash-abc", "FULL",
|
"hash-abc", "FULL",
|
||||||
"{\"order\":\"12345\"}", "", "", "", "", "", "{\"env\":\"prod\"}",
|
"", "", "", "", "", "", "{\"order\":\"12345\",\"tenant\":\"acme\"}",
|
||||||
"", "",
|
"", "",
|
||||||
false, false,
|
false, false,
|
||||||
null, null
|
null, null
|
||||||
@@ -79,7 +80,7 @@ class ClickHouseSearchIndexIT {
|
|||||||
"java.lang.NPE\n at Foo.bar(Foo.java:42)",
|
"java.lang.NPE\n at Foo.bar(Foo.java:42)",
|
||||||
"NullPointerException", "RUNTIME", "", "",
|
"NullPointerException", "RUNTIME", "", "",
|
||||||
"", "FULL",
|
"", "FULL",
|
||||||
"", "", "", "", "", "", "",
|
"", "", "", "", "", "", "{\"order\":\"99999\"}",
|
||||||
"", "",
|
"", "",
|
||||||
false, false,
|
false, false,
|
||||||
null, null
|
null, null
|
||||||
@@ -309,4 +310,59 @@ class ClickHouseSearchIndexIT {
|
|||||||
assertThat(result.total()).isEqualTo(1);
|
assertThat(result.total()).isEqualTo(1);
|
||||||
assertThat(result.data().get(0).executionId()).isEqualTo("exec-1");
|
assertThat(result.data().get(0).executionId()).isEqualTo("exec-1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void search_byAttributeFilter_exactMatch_matchesExec1() {
|
||||||
|
SearchRequest request = new SearchRequest(
|
||||||
|
null, null, null, null, null, null, null, null, null, null,
|
||||||
|
null, null, null, null, null, 0, 50, null, null, null, null,
|
||||||
|
List.of(new AttributeFilter("order", "12345")));
|
||||||
|
|
||||||
|
SearchResult<ExecutionSummary> result = searchIndex.search(request);
|
||||||
|
|
||||||
|
assertThat(result.total()).isEqualTo(1);
|
||||||
|
assertThat(result.data().get(0).executionId()).isEqualTo("exec-1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void search_byAttributeFilter_keyOnly_matchesExec1AndExec2() {
|
||||||
|
SearchRequest request = new SearchRequest(
|
||||||
|
null, null, null, null, null, null, null, null, null, null,
|
||||||
|
null, null, null, null, null, 0, 50, null, null, null, null,
|
||||||
|
List.of(new AttributeFilter("order", null)));
|
||||||
|
|
||||||
|
SearchResult<ExecutionSummary> result = searchIndex.search(request);
|
||||||
|
|
||||||
|
assertThat(result.total()).isEqualTo(2);
|
||||||
|
assertThat(result.data()).extracting(ExecutionSummary::executionId)
|
||||||
|
.containsExactlyInAnyOrder("exec-1", "exec-2");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void search_byAttributeFilter_wildcardValue_matchesExec1Only() {
|
||||||
|
SearchRequest request = new SearchRequest(
|
||||||
|
null, null, null, null, null, null, null, null, null, null,
|
||||||
|
null, null, null, null, null, 0, 50, null, null, null, null,
|
||||||
|
List.of(new AttributeFilter("order", "123*")));
|
||||||
|
|
||||||
|
SearchResult<ExecutionSummary> result = searchIndex.search(request);
|
||||||
|
|
||||||
|
assertThat(result.total()).isEqualTo(1);
|
||||||
|
assertThat(result.data().get(0).executionId()).isEqualTo("exec-1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void search_byAttributeFilter_multipleFiltersAreAnded() {
|
||||||
|
SearchRequest request = new SearchRequest(
|
||||||
|
null, null, null, null, null, null, null, null, null, null,
|
||||||
|
null, null, null, null, null, 0, 50, null, null, null, null,
|
||||||
|
List.of(
|
||||||
|
new AttributeFilter("order", "12345"),
|
||||||
|
new AttributeFilter("tenant", "acme")));
|
||||||
|
|
||||||
|
SearchResult<ExecutionSummary> result = searchIndex.search(request);
|
||||||
|
|
||||||
|
assertThat(result.total()).isEqualTo(1);
|
||||||
|
assertThat(result.data().get(0).executionId()).isEqualTo("exec-1");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user