Chore/added_ilike : added ilike and notIlike filter operator (#8595)

* chore(added-ilike): added ilike operator in qbv5

* chore(added-ilike): added test cases
This commit is contained in:
aniketio-ctrl 2025-07-29 17:28:25 +05:30 committed by GitHub
parent 7df5c33ce9
commit 771ba45d01
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 123 additions and 6 deletions

View File

@ -333,6 +333,8 @@ export const OPERATORS = {
'<': '<',
HAS: 'HAS',
NHAS: 'NHAS',
ILIKE: 'ILIKE',
NOTILIKE: 'NOT_ILIKE',
};
export const QUERY_BUILDER_OPERATORS_BY_TYPES = {
@ -349,6 +351,8 @@ export const QUERY_BUILDER_OPERATORS_BY_TYPES = {
OPERATORS.NOT_EXISTS,
OPERATORS.REGEX,
OPERATORS.NREGEX,
OPERATORS.ILIKE,
OPERATORS.NOTILIKE,
],
int64: [
OPERATORS['='],
@ -389,6 +393,8 @@ export const QUERY_BUILDER_OPERATORS_BY_TYPES = {
OPERATORS.NOT_EXISTS,
OPERATORS.LIKE,
OPERATORS.NLIKE,
OPERATORS.ILIKE,
OPERATORS.NOTILIKE,
OPERATORS['>='],
OPERATORS['>'],
OPERATORS['<='],

View File

@ -7,7 +7,7 @@ import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { orderByValueDelimiter } from '../OrderByFilter/utils';
// eslint-disable-next-line no-useless-escape
export const tagRegexp = /^\s*(.*?)\s*(\bIN\b|\bNOT_IN\b|\bLIKE\b|\bNOT_LIKE\b|\bREGEX\b|\bNOT_REGEX\b|=|!=|\bEXISTS\b|\bNOT_EXISTS\b|\bCONTAINS\b|\bNOT_CONTAINS\b|>=|>|<=|<|\bHAS\b|\bNHAS\b)\s*(.*)$/gi;
export const tagRegexp = /^\s*(.*?)\s*(\bIN\b|\bNOT_IN\b|\bLIKE\b|\bNOT_LIKE\b|\bILIKE\b|\bNOT_ILIKE\b|\bREGEX\b|\bNOT_REGEX\b|=|!=|\bEXISTS\b|\bNOT_EXISTS\b|\bCONTAINS\b|\bNOT_CONTAINS\b|>=|>|<=|<|\bHAS\b|\bNHAS\b)\s*(.*)$/gi;
export function isInNInOperator(value: string): boolean {
return value === OPERATORS.IN || value === OPERATORS.NIN;
@ -79,6 +79,10 @@ export function getOperatorValue(op: string): string {
return 'contains';
case 'NOT_CONTAINS':
return 'ncontains';
case 'ILIKE':
return 'ilike';
case 'NOT_ILIKE':
return 'notilike';
default:
return op;
}

View File

@ -17,6 +17,8 @@ export const operatorTypeMapper: Record<string, OperatorType> = {
[OPERATORS['>']]: 'SINGLE_VALUE',
[OPERATORS.LIKE]: 'SINGLE_VALUE',
[OPERATORS.NLIKE]: 'SINGLE_VALUE',
[OPERATORS.ILIKE]: 'SINGLE_VALUE',
[OPERATORS.NOTILIKE]: 'SINGLE_VALUE',
[OPERATORS.REGEX]: 'SINGLE_VALUE',
[OPERATORS.NREGEX]: 'SINGLE_VALUE',
[OPERATORS.CONTAINS]: 'SINGLE_VALUE',

View File

@ -28,6 +28,8 @@ var logOperators = map[v3.FilterOperator]string{
v3.FilterOperatorNotIn: "NOT IN",
v3.FilterOperatorExists: "mapContains(%s_%s, '%s')",
v3.FilterOperatorNotExists: "not mapContains(%s_%s, '%s')",
v3.FilterOperatorILike: "ILIKE",
v3.FilterOperatorNotILike: "NOT ILIKE",
}
var skipExistsFilter = map[v3.FilterOperator]struct{}{
@ -175,6 +177,9 @@ func buildAttributeFilter(item v3.FilterItem) (string, error) {
} else {
return fmt.Sprintf("%s %s '%s'", keyName, logsOp, val), nil
}
case v3.FilterOperatorILike, v3.FilterOperatorNotILike:
val := utils.QuoteEscapedString(fmt.Sprintf("%s", item.Value))
return fmt.Sprintf("%s %s '%s'", keyName, logsOp, val), nil
default:
return fmt.Sprintf("%s %s %s", keyName, logsOp, fmtVal), nil
}

View File

@ -297,6 +297,36 @@ func Test_buildAttributeFilter(t *testing.T) {
},
want: "lower(body) LIKE lower('test')",
},
{
name: "build attribute filter like-body",
args: args{
item: v3.FilterItem{
Key: v3.AttributeKey{
Key: "body",
DataType: v3.AttributeKeyDataTypeString,
IsColumn: true,
},
Operator: v3.FilterOperatorILike,
Value: "test",
},
},
want: "body ILIKE 'test'",
},
{
name: "build attribute filter not ilike-body",
args: args{
item: v3.FilterItem{
Key: v3.AttributeKey{
Key: "body",
DataType: v3.AttributeKeyDataTypeString,
IsColumn: true,
},
Operator: v3.FilterOperatorNotILike,
Value: "test",
},
},
want: "body NOT ILIKE 'test'",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@ -327,6 +327,10 @@ func PrepareTimeseriesFilterQuery(start, end int64, mq *v3.BuilderQuery) (string
conditions = append(conditions, fmt.Sprintf("has(JSONExtractKeys(labels), '%s')", item.Key.Key))
case v3.FilterOperatorNotExists:
conditions = append(conditions, fmt.Sprintf("not has(JSONExtractKeys(labels), '%s')", item.Key.Key))
case v3.FilterOperatorILike:
conditions = append(conditions, fmt.Sprintf("ilike(JSONExtractString(labels, '%s'), %s)", item.Key.Key, fmtVal))
case v3.FilterOperatorNotILike:
conditions = append(conditions, fmt.Sprintf("notILike(JSONExtractString(labels, '%s'), %s)", item.Key.Key, fmtVal))
default:
return "", fmt.Errorf("unsupported filter operator")
}

View File

@ -140,6 +140,54 @@ func TestPrepareTimeseriesFilterQuery(t *testing.T) {
},
expectedQueryContains: "SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['http_requests'] AND temporality = 'Cumulative' AND __normalized = true AND unix_milli >= 1706428800000 AND unix_milli < 1706434026000 AND JSONExtractString(labels, 'service_name') != 'payment_service' AND JSONExtractString(labels, 'endpoint') IN ['/paycallback','/payme','/paypal']",
},
{
name: "test prepare time series with filters and multiple group by",
builderQuery: &v3.BuilderQuery{
QueryName: "A",
StepInterval: 60,
DataSource: v3.DataSourceMetrics,
AggregateAttribute: v3.AttributeKey{
Key: "http_requests",
DataType: v3.AttributeKeyDataTypeFloat64,
Type: v3.AttributeKeyTypeUnspecified,
IsColumn: true,
IsJSON: false,
},
Temporality: v3.Cumulative,
Filters: &v3.FilterSet{
Operator: "AND",
Items: []v3.FilterItem{
{
Key: v3.AttributeKey{
Key: "service_name",
Type: v3.AttributeKeyTypeTag,
DataType: v3.AttributeKeyDataTypeString,
},
Operator: v3.FilterOperatorILike,
Value: "payment_service",
},
{
Key: v3.AttributeKey{
Key: "endpoint",
Type: v3.AttributeKeyTypeTag,
DataType: v3.AttributeKeyDataTypeString,
},
Operator: v3.FilterOperatorNotILike,
Value: "payment_service",
},
},
},
GroupBy: []v3.AttributeKey{{
Key: "service_name",
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeTag,
}},
Expression: "A",
Disabled: false,
// remaining struct fields are not needed here
},
expectedQueryContains: "SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['http_requests'] AND temporality = 'Cumulative' AND __normalized = true AND unix_milli >= 1706428800000 AND unix_milli < 1706434026000 AND ilike(JSONExtractString(labels, 'service_name'), 'payment_service') AND notILike(JSONExtractString(labels, 'endpoint'), 'payment_service')",
},
}
for _, testCase := range testCases {

View File

@ -25,6 +25,8 @@ var resourceLogOperators = map[v3.FilterOperator]string{
v3.FilterOperatorNotIn: "NOT IN",
v3.FilterOperatorExists: "mapContains(%s_%s, '%s')",
v3.FilterOperatorNotExists: "not mapContains(%s_%s, '%s')",
v3.FilterOperatorILike: "ILIKE",
v3.FilterOperatorNotILike: "NOT ILIKE",
}
// buildResourceFilter builds a clickhouse filter string for resource labels
@ -51,7 +53,7 @@ func buildResourceFilter(logsOp string, key string, op v3.FilterOperator, value
// we also want to treat %, _ as literals for contains
escapedStringValue := utils.QuoteEscapedStringForContains(lowerValue, false)
return fmt.Sprintf("%s %s '%%%s%%'", lowerSearchKey, logsOp, escapedStringValue)
case v3.FilterOperatorLike, v3.FilterOperatorNotLike:
case v3.FilterOperatorLike, v3.FilterOperatorNotLike, v3.FilterOperatorILike, v3.FilterOperatorNotILike:
// this is required as clickhouseFormattedValue add's quotes to the string
escapedStringValue := utils.QuoteEscapedString(lowerValue)
return fmt.Sprintf("%s %s '%s'", lowerSearchKey, logsOp, escapedStringValue)
@ -120,9 +122,9 @@ func buildResourceIndexFilter(key string, op v3.FilterOperator, value interface{
return fmt.Sprintf("labels like '%%%s\":\"%s%%'", key, fmtValEscapedForContains)
case v3.FilterOperatorNotEqual:
return fmt.Sprintf("labels not like '%%%s\":\"%s%%'", key, fmtValEscapedForContains)
case v3.FilterOperatorLike:
case v3.FilterOperatorLike, v3.FilterOperatorILike:
return fmt.Sprintf("lower(labels) like '%%%s%%%s%%'", key, fmtValEscapedLower)
case v3.FilterOperatorNotLike:
case v3.FilterOperatorNotLike, v3.FilterOperatorNotILike:
// cannot apply not contains x%y as y can be somewhere else
return ""
case v3.FilterOperatorContains:

View File

@ -208,6 +208,15 @@ func Test_buildResourceIndexFilter(t *testing.T) {
},
want: `lower(labels) like '%service.name%application%_test%'`,
},
{
name: "test not like with % and _",
args: args{
key: "service.name",
op: v3.FilterOperatorILike,
value: "application%_test",
},
want: `lower(labels) like '%service.name%application%_test%'`,
},
{
name: "test contains",
args: args{

View File

@ -30,6 +30,8 @@ var tracesOperatorMappingV3 = map[v3.FilterOperator]string{
v3.FilterOperatorNotContains: "NOT ILIKE",
v3.FilterOperatorExists: "mapContains(%s, '%s')",
v3.FilterOperatorNotExists: "NOT mapContains(%s, '%s')",
v3.FilterOperatorILike: "ILIKE",
v3.FilterOperatorNotILike: "NOT ILIKE",
}
func getClickHouseTracesColumnType(columnType v3.AttributeKeyType) string {

View File

@ -284,7 +284,7 @@ func Test_buildTracesFilterQuery(t *testing.T) {
wantErr: false,
},
{
name: "Test contains, ncontains, like, nlike, regex, nregex",
name: "Test contains, ncontains, like, nlike, regex, nregex, ilike, nilike",
args: args{
fs: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
{Key: v3.AttributeKey{Key: "host", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "102.%", Operator: v3.FilterOperatorContains},
@ -293,10 +293,12 @@ func Test_buildTracesFilterQuery(t *testing.T) {
{Key: v3.AttributeKey{Key: "host", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "102", Operator: v3.FilterOperatorNotLike},
{Key: v3.AttributeKey{Key: "path", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag, IsColumn: true}, Value: "/mypath", Operator: v3.FilterOperatorRegex},
{Key: v3.AttributeKey{Key: "path", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag, IsColumn: true}, Value: "/health.*", Operator: v3.FilterOperatorNotRegex},
{Key: v3.AttributeKey{Key: "host", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "102.", Operator: v3.FilterOperatorILike},
{Key: v3.AttributeKey{Key: "host", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "102", Operator: v3.FilterOperatorNotLike},
}},
},
want: "attributes_string['host'] ILIKE '%102.\\%%' AND attributes_string['host'] NOT ILIKE '%103\\_%' AND attributes_string['host'] ILIKE '102.' AND attributes_string['host'] NOT ILIKE '102' AND " +
"match(`attribute_string_path`, '/mypath') AND NOT match(`attribute_string_path`, '/health.*')",
"match(`attribute_string_path`, '/mypath') AND NOT match(`attribute_string_path`, '/health.*') AND attributes_string['host'] ILIKE '102.' AND attributes_string['host'] NOT ILIKE '102'",
},
{
name: "Test exists, nexists",

View File

@ -1227,6 +1227,9 @@ const (
FilterOperatorHas FilterOperator = "has"
FilterOperatorNotHas FilterOperator = "nhas"
FilterOperatorILike FilterOperator = "ilike"
FilterOperatorNotILike FilterOperator = "notilike"
)
type FilterItem struct {