동적 스키마 레코드 수집
동적 스키마 레코드 수집 (Ingest Records with Dynamic Schemas)
로그처럼 레코드마다 키가 달라지는 동적 데이터를, 고정 스키마 테이블에 안전하게 넣고 텍스트 검색까지 가능하게 만드는 SchemaConformingTransformer를 소개해요.
본문
일부 도메인(예: 로깅)에서는 각 레코드가 서로 다른 키 집합을 가질 수 있는데, Pinot 테이블은 상대적으로 고정된 스키마를 가집니다. 키가 다양하게 변하는 레코드에서 각 필드를 테이블 컬럼에 따로 저장하는 것은 비현실적입니다. 하지만 (전부는 아니더라도) 대부분의 필드가 중요할 수 있으므로 필드를 불필요하게 버려서는 안 됩니다.
또한 그런 테이블의 검색 패턴도 복잡해지고 자주 바뀔 수 있습니다. 기존 키나 새로 생긴 키·값에 대해 정확 일치, 범위 쿼리, 접두사/접미사 매치, 와일드카드 검색, 집계 함수를 사용할 수 있어야 합니다.
SchemaConformingTransformer
SchemaConformingTransformer 는 동적 스키마 레코드를 고정 스키마 테이블에 수집할 수 있게 변환하는 RecordTransformer 입니다. 이 트랜스포머는 스키마에 없는 레코드 필드를 가져와 일종의 catchall 필드에 저장합니다. 또한 __mergedTextIndex 필드를 만들어 Lucene을 활용해 텍스트 검색을 수행합니다.
예를 들어 다음 레코드를 봅시다:
{
"arrayField":[0, 1, 2, 3],
"stringField":"a",
"intField_noIndex":9,
"string_noIndex":"z",
"message": "a",
"mapField":{
"arrayField":[0, 1, 2, 3],
"stringField":"a",
"intField_noIndex":9,
"string_noIndex":"z"
},
"mapField_noIndex":{
"arrayField":[0, 1, 2, 3],
"stringField":"a",
},
"nestedFields":{
"arrayField":[0, 1, 2, 3],
"stringField":"a",
"intField_noIndex":9,
"string_noIndex":"z",
"mapField":{
"arrayField":[0, 1, 2, 3],
"stringField":"a",
"intField_noIndex":9,
"string_noIndex":"z"
}
}
}
테이블 스키마에 다음 필드가 있다고 가정해 봅시다:
- arrayField
- mapField
- nestedFields
- nestedFields.stringField
- json\_data
- json\_data\_no\_idx
- \_\_mergedTextIndex
이 트랜스포머가 없으면 stringField와 _noIdx로 끝나는 필드는 버려집니다. mapField와 nestedFields의 저장은 복잡한 트랜스포머의 전역 설정에 의존하게 됩니다. 하지만 이 트랜스포머를 쓰면 레코드는 다음과 같이 변환됩니다:
{
"arrayField":[0, 1, 2, 3],
"nestedFields.stringField":"a",
"json_data":{
"stringField":"a",
"mapField":{
"arrayField":[0, 1, 2, 3],
"stringField":"a",
"stringField":"aA_123"
},
"nestedFields":{
"arrayField":[0, 1, 2, 3],
"mapField":{
"arrayField":[0, 1, 2, 3],
"stringField":"a"
}
}
},
"json_data_no_idx":{
"intField_noIndex":9,
"string_noIndex":"z",
"mapField":{
"intField_noIndex":9,
"string_noIndex":"z"
},
"mapField_noIndex":{
"arrayField":[0, 1, 2, 3],
"stringField":"a",
},
"nestedFields":{
"intField_noIndex":9,
"string_noIndex":"z",
"mapField":{
"intField_noIndex":9,
"string_noIndex":"z"
}
}
},
"__mergedTextIndex": [
// To be explained in following sections
]
}
예약된(그리고 설정 가능한) 필드 json_data, json_data_no_idx, __mergedTextIndex가 3개 보입니다. 이 트랜스포머는 다음을 수행합니다:
- 중첩 필드를 리프 노드까지 모두 펼치고:
- 설정에 따라 필요하면 특별 처리를 수행
- 키 경로가 스키마와 일치하면 전용 필드에 데이터를 넣음
- 그렇지 않으면 키 접미사에 따라
json_data또는json_data_no_idx에 넣음
- 전용 컬럼이나
json_data에 있는 키에 대해 "Begin Anchor + value + Separator + key + End Anchor" 형태로__mergedTextIndex에 넣어 텍스트 매치를 지원 - 설정으로 추가 기능 제공
fieldPathsToDrop필드 버리기- 펼치지 않고 서브트리 보존
fieldPathsToPreserveInput,fieldPathsToPreserveInputWithIndex - 저장은 하지 않지만 인덱싱만 수행(예제의
message)fieldPathsToSkipStorage - 필드 인덱싱 스킵
unindexableFieldSuffix - 대소문자 무시 검색 최적화
optimizeCaseInsensitiveSearch - 입력 키 경로를 스키마 이름으로 매핑
columnNameToJsonKeyPathMap - 익명 점(
{'a.b': 'c'}vs{'a': {'b': 'c'}}) 지원useAnonymousDotInFieldNames - 길이로 값 잘라내기
mergedTextIndexDocumentMaxLength - 스키마 진화를 위한 이중 수집
fieldsToDoubleIngest
테이블 구성 (Table Configurations)
SchemaConformingTransformer 구성
트랜스포머를 사용하려면 테이블 구성의 ingestionConfig 섹션에 schemaConformingTransformerConfig 옵션을 추가합니다. 예를 들면:
"schemaConformingTransformerConfig": {
"enableIndexableExtras": true,
"indexableExtrasField": "json_data",
"enableUnindexableExtras": true,
"unindexableExtrasField": "json_data_no_idx",
"unindexableFieldSuffix": "_noindex",
"fieldPathsToDrop": [],
"fieldPathsToSkipStorage": [
"message"
],
"columnNameToJsonKeyPathMap": {},
"mergedTextIndexField": "__mergedTextIndex",
"useAnonymousDotInFieldNames": true,
"optimizeCaseInsensitiveSearch": false,
"reverseTextIndexKeyValueOrder": true,
"mergedTextIndexDocumentMaxLength": 32766,
"mergedTextIndexBinaryDocumentDetectionMinLength": 512,
"mergedTextIndexPathToExclude": [
"_timestampMillisNegative",
"__mergedTextIndex",
"_timestampMillis"
],
"fieldsToDoubleIngest": [],
"jsonKeyValueSeparator": "\u001e",
"mergedTextIndexBeginOfDocAnchor": "\u0002",
"mergedTextIndexEndOfDocAnchor": "\u0003",
"fieldPathsToPreserveInput": [],
"fieldPathsToPreserveInputWithIndex": []
}
사용 가능한 구성 옵션은 SchemaConformingTransformerConfig 에 나와 있습니다.
예약 필드 구성
3개의 예약 컬럼에 대한 인덱스 구성은 다음과 같이 설정할 수 있습니다:
"fieldConfigList": [
{
"name": "json_data",
"encodingType": "RAW",
"indexTypes": [],
"compressionCodec": "LZ4",
"indexes": null,
"properties": {
"rawIndexWriterVersion": "4"
},
"tierOverwrites": null
},
{
"name": "json_data_no_idx",
"encodingType": "RAW",
"indexTypes": [],
"compressionCodec": "ZSTANDARD",
"indexes": null,
"properties": {
"rawIndexWriterVersion": "4"
},
"tierOverwrites": null
},
{
"name": "__mergedTextIndex",
"encodingType": "RAW",
"indexType": "TEXT",
"indexTypes": [
"TEXT"
],
"compressionCodec": "LZ4",
"indexes": null,
"properties": {
"enableQueryCacheForTextIndex": "false",
"luceneAnalyzerClass": <analyzerClass>,
"luceneAnalyzerClassArgTypes": <>,
"luceneAnalyzerClassArgs": <>,
"luceneMaxBufferSizeMB": "50",
"luceneQueryParserClass": <parserClass>,
"luceneUseCompoundFile": "true",
"noRawDataForTextIndex": "true",
"rawIndexWriterVersion": "4"
},
"tierOverwrites": null
}
]
"jsonIndexConfigs": {
"json_data": {
"disabled": false,
"maxLevels": 3,
"excludeArray": true,
"disableCrossArrayUnnest": true,
"maxValueLength": 1000,
"skipInvalidJson": true
}
}
특히 커스터마이즈 가능한 json 인덱스는 json 인덱스 indexPaths 에 따라 설정할 수 있습니다.
텍스트 검색 활성화 (Power the text search)
스키마 설계
SchemaConformingTransformer를 사용하면 테이블 스키마에 특별한 전용 컬럼을 지정하지 않아도 모든 데이터를 보존할 수 있습니다. 하지만 저장 공간과 다양한 쿼리 패턴을 최적화하려면 사용에 따라 전용 컬럼을 만들어야 합니다:
- 빈번한 정확 일치 쿼리 필드(예: region, log\_level, runtime\_env)
- 범위 쿼리 필드(예: timestamp)
- 메시지에서 고빈도로 나오는 필드
- json 인덱스 크기 줄이기
- group by 쿼리 최적화
텍스트 검색
각 key/value 쌍을 __mergedTextIndex 필드에 넣은 뒤에는 문서를 토큰화할 luceneAnalyzerClass와 토큰으로 쿼리할 luceneQueryParserClass가 필요합니다. 흔한 검색 패턴과 그 쿼리의 예는 다음과 같습니다:
- 정확한 키/값 매치
TEXT_MATCH(__mergedTextIndex, '"valuer:key"') - 특정 키에서 와일드카드 값 검색
TEXT_MATCH(__mergedTextIndex, '/.* value .*:key/') - 키 존재 확인
TEXT_MATCH(__mergedTextIndex, '/.*:key/') - 전역 값 정확 매치
TEXT_MATCH(__mergedTextIndex, '/"value"/') - 전역 값 와일드카드 매치
TEXT_MATCH(__mergedTextIndex, '/.* value .*/')
luceneAnalyzerClass와 luceneQueryParserClass는 보통 비슷한 구분자 집합을 가져야 합니다. 아래 값들도 고려해야 합니다.
"jsonKeyValueSeparator": "\u001e",
"mergedTextIndexBeginOfDocAnchor": "\u0002",
"mergedTextIndexEndOfDocAnchor": "\u0003",
주어진 예에서 각 key/value 쌍은 "\u0002value\u001ekey\u0003" 형태로 저장됩니다. 키 또는 값의 접두사/접미사 매치는 luceneQueryParserClass에서 그에 맞게 조정해야 합니다.