본문으로 건너뛰기

[Loki] 대소문자 무시 정규식을 바이너리 연산자로 최적화

PR 링크: grafana/loki#20578 상태: Merged | 변경: +1083 / -205

들어가며

Loki의 쿼리 엔진에는 정규식 최적화 패스가 있어, 단순한 정규식 패턴을 더 효율적인 바이너리 연산(문자열 비교, 서브스트링 매칭 등)으로 변환합니다. 이전 PR(#20297)에서 대소문자 구분 패턴에 대한 최적화가 추가되었지만, (?i) 플래그를 사용한 대소문자 무시 패턴은 여전히 정규식 엔진을 통과해야 했습니다. 이 PR은 대소문자 무시 등호와 서브스트링 매칭을 위한 전용 바이너리 연산자를 도입합니다.

핵심 코드 분석

새로운 바이너리 연산자

// types.go - 4개의 새 연산자
BinaryOpEqCaseInsensitive             // 대소문자 무시 동등 비교
BinaryOpNotEqCaseInsensitive          // 대소문자 무시 부등 비교
BinaryOpMatchSubstrCaseInsensitive    // 대소문자 무시 서브스트링 매칭
BinaryOpNotMatchSubstrCaseInsensitive // 대소문자 무시 서브스트링 부정 매칭

바이트 단위 대소문자 변환 비교

// matchutil 패키지
func EqualUpper(haystack, needle []byte) bool {
    // needle은 이미 대문자로 변환된 상태
    // haystack의 각 바이트를 대문자로 변환하며 비교
}

func ContainsUpper(haystack, needle []byte) bool {
    // 대소문자 무시 서브스트링 검색
}

Go의 정규식 파서는 (?i) 플래그를 처리할 때 리터럴을 대문자로 변환합니다(A < a이므로). 이 특성을 활용하여 needle이 이미 대문자임을 전제하고 비교합니다.

쿼리 엔진 통합

// functions.go
binaryFunctions.register(
    types.BinaryOpEqCaseInsensitive,
    arrow.BinaryTypes.String,
    &genericBoolFunction[*array.String, string]{
        eval: func(a, b string) (bool, error) {
            return matchutil.EqualUpper([]byte(a), []byte(b)), nil
        },
    },
)

로그 스캔 predicate 통합

// dataobjscan_predicate.go
case types.BinaryOpMatchSubstrCaseInsensitive:
    return logs.FuncPredicate{
        Column: col,
        Keep: func(_ *logs.Column, value scalar.Scalar) bool {
            return matchutil.ContainsUpper(getBytes(value), find)
        },
    }, nil

왜 이게 좋은가

1. 정규식 엔진 비용 회피

(?i)error와 같은 단순한 대소문자 무시 패턴도 정규식 엔진을 통과하면 NFA/DFA 구성, 상태 전이 등의 비용이 발생합니다. 바이트 단위 비교는 이 오버헤드를 완전히 제거합니다.

2. 스토리지 레벨 푸시다운

FuncPredicate로 구현했으므로, 데이터 오브젝트 스캔 시점에서 필터링이 가능합니다. 정규식은 일반적으로 푸시다운이 어렵지만, 바이너리 연산자는 스토리지 레이어에서 직접 적용할 수 있습니다.

3. Go 정규식 파서의 특성 활용

Go의 regexp/syntax 패키지가 (?i) 리터럴을 대문자로 정규화하는 동작을 활용하여, 별도의 대소문자 정규화 비용 없이 바로 비교할 수 있습니다.

참고 자료

댓글

관련 포스트

PR Analysis 의 다른글