Articlelangchain.com·2026년 6월 25일·0

How we built SmithDB’s inverted index for full-text search

Quick Summary

SmithDB는 수집 시점에 역색인을 함께 만들고, 객체 스토리지 친화적인 레이아웃·병합·쿼리 경로를 통해 대규모 실행 로그의 JSON 필드와 전문 검색을 빠르게 처리한다.

How we built SmithDB’s inverted index for full-text search 관련 대표 이미지

🖼️ 인포그래픽

How we built SmithDB’s inverted index for full-text search 내용을 설명하는 본문 이미지

🖼️ 4컷 인포그래픽

How we built SmithDB’s inverted index for full-text search 내용을 설명하는 본문 이미지

💡 한 줄 요약

SmithDB는 수집 시점에 역색인을 함께 만들고, 객체 스토리지 친화적인 레이아웃·병합·쿼리 경로를 통해 대규모 실행 로그의 JSON 필드와 전문 검색을 빠르게 처리한다.

📌 핵심 요약

  • 이 글은 SmithDB에서 객체 스토리지 기반 역색인을 어떻게 생성하고, 압축 과정에서 병합하며, 쿼리 시 DataFusion과 Vortex의 기존 실행 경로 안에 통합해 사용하는지를 설명한다.
  • 색인 생성은 데이터 수집 과정에서 인라인으로 수행되므로 새로 들어온 run은 수 초 안에 검색 가능해지며, 최신 데이터는 작성 노드의 로컬 저장소에서 바로 읽어 객체 스토리지 왕복을 줄인다.
  • 큰 JSON 입력·출력 필드를 효율적으로 처리하기 위해 SmithDB는 Apache Arrow의 arrow-json 및 simdjson의 tape format에서 영감을 받은 평탄한 JSON tape 구조를 사용하고, 경로와 leaf 값을 빠르게 추출한다.
  • 정렬 비용을 줄이기 위해 반복되는 JSON 경로와 토큰 값을 문자열 interning으로 정수 ID에 매핑하고, postings를 term 기준으로 radix sort한 뒤 FST writer에 공급한다.
  • 쿼리 경로에서는 세그먼트별 색인 존재 여부에 따라 index read, core column scan, no matches를 미리 결정하고, term_info·postings·positions 범위를 coalesce해 객체 스토리지 GET 횟수와 메모리 사용을 제한한다.

🧩 주요 포인트

  1. 이 글은 SmithDB에서 객체 스토리지 기반 역색인을 어떻게 생성하고, 압축 과정에서 병합하며, 쿼리 시 DataFusion과 Vortex의 기존 실행 경로 안에 통합해 사용하는지를 설명한다.
  2. 색인 생성은 데이터 수집 과정에서 인라인으로 수행되므로 새로 들어온 run은 수 초 안에 검색 가능해지며, 최신 데이터는 작성 노드의 로컬 저장소에서 바로 읽어 객체 스토리지 왕복을 줄인다.
  3. 큰 JSON 입력·출력 필드를 효율적으로 처리하기 위해 SmithDB는 Apache Arrow의 arrow-json 및 simdjson의 tape format에서 영감을 받은 평탄한 JSON tape 구조를 사용하고, 경로와 leaf 값을 빠르게 추출한다.
  4. 정렬 비용을 줄이기 위해 반복되는 JSON 경로와 토큰 값을 문자열 interning으로 정수 ID에 매핑하고, postings를 term 기준으로 radix sort한 뒤 FST writer에 공급한다.
  5. 쿼리 경로에서는 세그먼트별 색인 존재 여부에 따라 index read, core column scan, no matches를 미리 결정하고, term_info·postings·positions 범위를 coalesce해 객체 스토리지 GET 횟수와 메모리 사용을 제한한다.

🧠 상세 정리

1. SmithDB 전문 검색 색인의 목적과 전체 흐름

글은 SmithDB에서 전문 검색을 지원하기 위해 설계한 객체 스토리지 기반 역색인을 실제로 어떻게 만들고 사용하는지에 초점을 둔다. 이전 글이 역색인 구현의 저장소 설계를 다뤘다면, 이 글은 색인 생성, 압축 시 병합, 쿼리 수행이라는 실행 흐름을 설명한다. SmithDB는 run payload 안의 큰 inputs와 outputs 필드, 그리고 일부 다른 필드를 대상으로 검색 가능한 구조를 만든다. 핵심 목표는 새로 수집된 실행 로그를 빠르게 검색 가능하게 하면서도, 객체 스토리지에서 많은 작은 읽기가 발생할 때 생기는 지연과 비용을 제어하는 것이다.

2. 수집 중 인라인 색인 생성과 최신 데이터 처리

역색인 생성은 별도의 후처리만으로 미뤄지지 않고 ingestion 과정에서 인라인으로 수행된다. 이 덕분에 새 run은 수 초 안에 검색 가능해지며, 관측성 쿼리처럼 방금 들어온 trace를 디버깅에 바로 써야 하는 상황을 지원할 수 있다. 특히 가장 최신 데이터는 아직 해당 데이터를 쓴 노드의 로컬 저장소에 남아 있으므로, 선두 구간의 쿼리는 색인과 core data file을 객체 스토리지까지 왕복하지 않고 로컬 저장소에서 직접 읽는다. source_body는 이를 L0 로컬 SSD 계층으로 설명하며, 배치 수집 시 per-column 역색인이 로컬 SSD에 기록되어 빠르게 visible해진다고 말한다.

3. JSON payload 파싱: flattened path와 leaf value 추출

SmithDB가 색인화하는 inputs와 outputs는 크고 깊게 중첩된 JSON payload일 수 있어, 일반적인 JSON 파싱 비용이 색인 생성 시간의 큰 비중을 차지한다. 하지만 색인 생성에 필요한 것은 전체 객체 모델이 아니라 평탄화된 key path와 leaf value이므로, SmithDB는 Apache Arrow의 arrow-json crate와 simdjson의 tape format에서 영감을 받은 JSON tape를 구현했다. 이 구조는 토큰을 평탄한 순차 배열에 담고 문자열 바이트를 하나의 연속 버퍼에 저장해 per-field allocation과 숫자 변환을 피한다. 이후 단일 패스 iterator가 문서를 path와 leaf_value 쌍으로 펼치며, 중첩 객체는 dotted path가 되고 배열 요소는 부모 key 아래 값으로 처리된다.

4. 토큰화, 문자열 interning, 정렬 비용 절감

값이 색인 term이 되기 전에는 토큰화 과정을 거친다. SmithDB는 non-alphanumeric boundary를 기준으로 값을 나누고, 소문자화하며, stop word를 제거하고, 토큰 길이를 256자로 제한한다. 이후 FST 기반 term layout에 공급하기 위해 flat postings table을 term 기준으로 정렬해야 하는데, agent trace에서는 동일한 JSON path와 token value가 tenant와 tracing project 안에서 반복되기 때문에 단순 문자열 비교 정렬은 비용이 커진다. 이를 줄이기 위해 각 unique term을 compact integer ID로 매핑하는 string interning을 사용하고, 비교 비용이 occurrence 수가 아니라 distinct term 수에 비례하도록 만들어 벤치마크에서 약 2.2배의 생성 시간 개선을 얻었다.

5. postings 기록과 flush threshold 설계

각 term occurrence는 doc_id, term_rank, position triple로 flat table에 기록되고, radix sort를 통해 postings를 term별로 O(n)에 그룹화한 뒤 FST writer로 전달된다. 색인 write 과정에서는 특정 고빈도 term이 무한히 커지는 것을 막기 위해 세 가지 flush boundary가 사용된다. 첫째는 32MB postings, 50만 term, 64MB raw term bytes 기준의 row group이고, 둘째는 약 2MB aligned chunk로 postings와 positions가 같은 document boundary에서 flush되도록 한다. 셋째는 8MB mid-term position spill로, 매우 자주 등장하는 term이 term 종료 전까지 수억 개의 position을 메모리에 쌓는 상황을 피하는 escape hatch 역할을 한다.

6. compaction 중 streaming merge

SmithDB의 compaction service는 ingestion이 만든 작은 파일들을 쿼리에 더 적합한 큰 파일로 병합한다. core file을 compact하는 동시에 각 core file에 연결된 per-file index도 streaming merge 방식으로 처리한다. 이 과정에서 min-heap은 여러 입력을 term 단위로 한 번에 하나씩 전진시키고, 각 입력은 전체 segment가 아니라 디코딩된 chunk 하나만 메모리에 유지한다. 병합 결과는 FST writing에 필요한 정렬 상태로 나오며, 생성 단계에서 사용한 row group, aligned chunk, mid-term position spill threshold가 출력 builder에도 동일하게 적용된다. 따라서 메모리 사용량은 전체 색인 크기가 아니라 병합 중인 input 수에 따라 증가한다.

7. 쿼리 경로: 기존 DataFusion·Vortex 파이프라인 안의 색인

읽기 경로는 SmithDB의 다른 쿼리가 이미 사용하는 DataFusion과 Vortex LayoutReader pipeline을 재사용한다. 역색인은 planner가 predicate를 pushdown할 수 있는 또 하나의 layout으로 들어가며, SQL surface나 query planner가 색인의 존재를 별도로 알아야 하는 구조가 아니다. TableProvider와 LayoutReader 구현 내부에서 세그먼트 후보를 metastore에서 해석한 뒤, 해당 세그먼트가 질의 대상 column의 색인을 갖고 있는지 확인한다. 색인이 없는 세그먼트는 조용히 column scan으로 보내고, 색인이 있는 세그먼트는 index path로 보내며, 이 결정은 객체 스토리지 요청이 발생하기 전에 끝난다.

8. 하나의 세그먼트를 여러 파일로 다루는 방식

SmithDB에서 metastore entry 하나는 row data를 담은 core file 하나와, 색인된 column마다 존재하는 sibling index file을 가리킨다. 문제는 이 여러 파일을 DataFusion 위쪽에는 하나의 addressable entity처럼 보이게 하면서도, IO scheduler 아래쪽에는 실제 필요한 파일과 byte range만 열 수 있게 만드는 것이다. plan time에는 predicate가 세그먼트마다 한 번 검사되어 index read, core file column scan, no matches 중 하나로 라우팅된다. runtime에는 core file과 각 index file이 하나의 virtual layout으로 합성되어 projection pushdown, predicate pushdown, row-index space가 일관되게 작동한다. 그래서 100개 column 중 3개만 만지는 쿼리는 정확히 3개의 index file만 열고, index를 쓰지 않는 predicate는 index file을 전혀 열지 않는다.

9. row group에서 객체 스토리지 GET으로 이어지는 읽기

역색인 row group read는 range registration과 decode라는 두 단계로 진행된다. reader는 FST에서 term을 해석하고 term_info entry를 읽어 postings, phrase query의 경우 positions offset과 length를 얻은 뒤, 필요한 모든 range를 Vortex segment scheduler에 등록한다. scheduler는 인접 range를 병합하고, 최대 1MB 간격의 비인접 range도 합치며, coalesced window를 16MB로 제한한다. 객체 스토리지에서는 작은 read의 고정 지연과 요청 비용이 중요하므로, 약간의 불필요한 전송을 감수하고 round trip 수를 줄이는 것이 유리하다. layout은 term 관련 range가 물리적으로 가깝게 놓이도록 설계되어 selective phrase lookup은 term_info, postings, positions를 하나의 GET으로 가져올 수 있다.

10. 세 가지 쿼리 형태와 positions column의 역할

색인 layout으로 predicate가 전달되면 세 가지 query shape가 v2 column인 term, term_info, postings, positions에 직접 매핑된다. json_key의 path-only 검색은 term_key FST를 걷고 postings만 반환하므로 positions를 읽지 않아 가장 저렴하며, prefix-pattern path query는 zone-level pruning의 도움을 크게 받을 수 있다. json_key_search의 keyed-search는 token\0path keying을 사용해 term_value에 대해 FST lookup을 수행하고, multi-token phrase 변형은 postings를 먼저 교차한 뒤 PhraseQuery로 adjacency를 확인한다. search는 plain-text column에서는 exact lookup을, JSON value에 대한 free-text에서는 token\0 prefix scan으로 해당 token이 등장한 모든 path의 postings를 union한다. PhraseQuery는 postings 교차로 후보 문서를 줄인 뒤 그 후보에 대해서만 positions를 디코딩하므로, 적은 문서에만 매칭되는 phrase query는 positions IO를 거의 지불하지 않는다.

🧾 핵심 주장 / 시사점

  • SmithDB의 핵심 최적화는 색인을 단순한 보조 파일로 두는 것이 아니라, DataFusion과 Vortex의 기존 layout abstraction 안에 자연스럽게 끼워 넣어 SQL 표면과 planner 변경을 최소화한 점이다.
  • JSON payload 처리, string interning, radix sort, flush threshold는 모두 같은 방향을 향한다. 반복이 많은 observability trace 데이터에서 문자열·position·객체 스토리지 요청이 폭증하지 않도록, 생성 시점부터 쿼리 시점까지 비용 상한을 명시적으로 둔다.
  • positions column을 postings와 분리한 설계는 phrase query의 비용 구조를 바꾼다. 먼저 postings 교차로 후보를 좁힌 뒤 필요한 경우에만 positions를 읽기 때문에, phrase 검색을 지원하면서도 일반 검색과 선택적인 phrase lookup의 IO를 분리할 수 있다.

✅ 액션 아이템

  • 수집 파이프라인에서 역색인 생성 단계를 수집 작업과 인라인화해 새 run이 수 초 이내 검색 가능한지 동작 조건을 점검한다.
  • Apache Arrow arrow-json와 simdjson tape 구조를 적용해 큰 JSON 필드에서 경로·leaf 추출 과정을 평탄화하고 비용 절감 폭을 확인한다.
  • 쿼리 단계에서 세그먼트별 색인 존재성 판단 뒤 term_info·postings·positions 범위를 coalesce해 객체 스토리지 GET와 메모리 사용량을 제한한다.

❓ 열린 질문

  • 로컬 작성 노드에서의 신속 조회로 객체 스토리지 왕복을 줄이는 방식이 어떤 로그 패턴에서 깨질 수 있으며, 그 경계는 어디인가?
  • 반복 경로와 토큰을 문자열 interning으로 정수 ID로 치환할 때, 성능 이득이 감소하거나 역전되는 데이터 규모 구간은 어떤가?
  • term별 postings를 radix sort해 FST writer로 넘기는 방식이 쿼리 지연에 미치는 trade-off는 어느 부하 구간에서 결정적인가?

관련 문서

공통 태그와 주제 흐름을 기준으로 같이 보면 좋은 문서를 이어서 제안합니다.