Leviathan: 대용량 티켓과 로그를 grep 대신 수백 토큰의 검색 결과로 에이전트에 건네는 로컬 인덱서

핵심 요약

  • Leviathan은 JSONL, CSV, SQLite 같은 레코드를 SQLite 검색 인덱스 파일 하나로 만드는 Rust 단일 바이너리입니다. 에이전트는 질문마다 순위가 매겨진 결과 카드 몇 장만 받습니다.
  • 개발자가 공개한 합성 데이터 벤치마크(100만 건)에서 질문당 에이전트 컨텍스트에 들어간 토큰은 중앙값 436개였고, 가장 나은 grep 전략은 107,122개였습니다.
  • 셸을 쓸 수 있는 코딩 에이전트는 CLI와 스킬 파일로 바로 쓸 수 있고, 읽기 전용 MCP 서버도 선택적으로 제공합니다.
  • 검색은 임베딩 없는 BM25 어휘 검색이고, 한국어는 조사가 붙은 낱말을 별개 토큰으로 다루므로 데이터 전처리나 질의 표현에 신경 써야 합니다.

Leviathan 소개

고객 지원 티켓, 설비 정비 기록, 장애 로그처럼 한 대상에 기록이 수천 건씩 쌓이는 데이터에 코딩 에이전트를 연결하면, 에이전트는 보통 grep으로 해당 고객이나 장비의 이력을 추출해 통째로 읽습니다. 이력이 짧을 때는 이 방법으로 충분합니다. 데이터가 커지면 한 번의 질문에 수만에서 수십만 토큰이 컨텍스트로 들어가고, 에이전트 셸이 도구 출력을 일정 길이에서 자르면 답이 담긴 레코드가 잘린 뒤쪽에 남습니다. 질문에 쓰인 낱말과 기록에 적힌 낱말이 다르거나 "완료", "조치함" 같은 형식적인 기록이 대부분을 차지하면 키워드 grep만으로는 의미 있는 레코드를 골라내기도 어렵습니다. 이번에 소개하는 Leviathan은 이런 레코드를 한 번 색인해 두고, 에이전트가 질문할 때마다 관련 레코드 몇 건만 짧은 카드로 돌려주는 로컬 검색 도구입니다.

Leviathan은 원본을 매번 읽는 대신 SQLite FTS5 전문 검색(Full-Text Search) 인덱스를 미리 만들어 두는 접근을 택했습니다. 사용자는 레코드의 ID, 제목, 본문, 그룹, 날짜, 필터 필드를 매핑해 둡니다. 여기서 그룹은 고객, 장비, 호스트처럼 사람들이 이름으로 묻는 대상입니다. 그러면 에이전트는 -g acme 처럼 그룹을 이름이나 이름 일부로 지정하고 자연어 그대로 질문할 수 있습니다. 결과는 BM25(Best Matching 25) 점수 순으로 정렬되고, 각 카드에는 레코드 ID와 날짜, 설정한 표시 필드, 질문과 일치한 문장이 들어갑니다. 원본 전체가 필요하면 에이전트가 카드의 ID로 leviathan get 을 호출합니다. 개발자는 이 방식으로 데이터 크기와 상관없이 답변 하나가 약 450토큰 안팎에 머문다고 설명합니다.

Leviathan은 Joshua Baker(elstongun)가 만든 프로젝트로, 개발자는 GitHub 프로필에서 고빈도 매매(HFT) 인프라를 다루다 현재 공장 지능화 일을 하고 있다고 소개합니다. Leviathan은 Rust로 작성되어 런타임(runtime) 의존성이 없고, 인덱스는 SQLite 파일 하나입니다. 또한 네트워크 연결을 열지 않고 데이터베이스 접속 정보도 갖지 않습니다. 데이터베이스는 psql, duckdb 같은 각자의 CLI가 내보낸 출력을 표준 입력으로 받아 색인합니다. 2026년 10월 9일 기준 저장소는 v0.1.0 첫 릴리스와 커밋 5개뿐인 초기 상태입니다.

Leviathan vs. grep, QMD

Leviathan이 직접 비교한 대상은 ripgrep 기반의 grep 전략입니다. 같은 로컬 검색 계열인 QMD와 함께 놓으면 다루는 데이터와 검색 방식이 다음과 같이 다릅니다:

항목 Leviathan grep (ripgrep) QMD
주요 대상 티켓, 로그, 주문 같은 레코드 (JSONL, CSV, SQLite, DB CLI 출력) 임의의 텍스트 파일 마크다운 노트, 회의록, 문서
검색 방식 FTS5 BM25 어휘 검색 + 그룹 범위 지정 + 필터 문자열, 정규식 일치 BM25 + 벡터 검색 + LLM 재순위
모델 의존성 없음 없음 로컬 GGUF 모델 (node-llama-cpp)
검색 범위 지정 그룹(고객, 장비 등) 단위, 키와 이름과 부분 이름으로 해석하고 모호하면 후보를 보여주고 종료 파일 경로와 키 문자열을 직접 지정 컬렉션 단위
에이전트가 받는 출력 길이 상한이 있는 결과 카드와 shown N of M 일치한 원본 줄 전체 순위가 매겨진 문서

Leviathan과 QMD의 가장 큰 차이는 의미 검색 여부입니다. QMD는 질문을 확장하고 벡터 검색과 LLM 재순위를 거쳐 표현이 다른 문서도 찾지만, Leviathan은 임베딩 없이 낱말 일치와 어간 추출(stemming)에만 의존합니다. 개발자도 한계 항목에서 질문은 "weeping" 이라고 쓰고 기록은 "leak" 이라고 쓴 경우 질문의 다른 낱말이 일치해야 찾을 수 있다고 적어 두었습니다. 반면 Leviathan은 모델을 내려받거나 GPU를 쓰지 않고, 같은 장비나 고객의 기록 안에서만 찾도록 범위를 좁히는 기능과 날짜, 상태 필터를 기본으로 제공합니다. 셸 명령 출력 자체를 압축하는 rtk와 비교하면, rtk는 이미 실행한 명령의 출력을 줄이고 Leviathan은 원본을 읽는 명령 자체를 검색 한 번으로 바꾼다는 점이 다릅니다.

Leviathan을 사용하면 좋을 사용자

Leviathan은 고객, 장비, 호스트처럼 이름으로 부르는 대상이 있고 대상마다 기록이 많이 쌓인 운영 데이터를 에이전트에게 물어보는 팀에 맞습니다. 지원 티켓에서 "이 고객이 전에 같은 문제를 겪었나", 정비 기록에서 "이 장비는 지난번에 무엇으로 고쳤나" 같은 질문이 벤치마크가 가정한 형태입니다. 민감한 데이터를 외부로 보내지 않아야 하는 환경에서도 네트워크와 텔레메트리가 없고 MCP 서버가 읽기 전용이라는 점이 도입 근거가 됩니다. 반대로 데이터 전체를 가로지르는 집계 질문이나 표현이 크게 다른 질문까지 찾아야 하는 경우, 그리고 한국어 원문을 전처리 없이 그대로 색인하려는 팀에게는 Leviathan이 아직 적절한 선택지가 아닙니다. 첫 릴리스 직후의 프로젝트이므로 운영 환경에 붙이기 전에 자체 데이터로 검색 품질을 먼저 확인하는 편이 안전합니다.

Leviathan의 검색 동작 방식

Leviathan은 레코드를 스트리밍으로 읽어 SQLite 파일 하나에 저장하고, 제목과 본문, 그룹 이름, 필터 값을 FTS5 인덱스에 넣습니다. 그룹과 필터 값은 별도의 토큰으로 색인되므로, 그룹 범위 지정과 --where status=closed 같은 필터는 검색 뒤에 결과를 거르는 방식이 아니라 FTS5 질의 한 번 안에서 함께 처리됩니다. 점수는 BM25에 설정한 가중치(rank.boost)를 곱해 계산하고, 상위 N건만 디코딩해 길이 상한이 있는 카드로 만듭니다. 새 인덱스는 빌드가 끝난 뒤 원자적으로 교체되고, 원본과 매핑이 바뀌지 않았으면 빌드를 건너뜁니다. 일상적인 변경은 upsert와 delete로 반영합니다.

그룹 이름 해석은 정확한 키, 대소문자를 무시한 키, 이름, 부분 이름, 퍼지 일치 순서로 진행합니다. 한 단계에서 후보가 둘 이상 나오면 Leviathan은 추측하지 않고 후보 목록을 출력한 뒤 종료 코드 3으로 끝납니다. 일치하는 그룹이 아예 없을 때도 종료 코드 3이지만, 후보 대신 resolve 를 써 보라는 안내만 출력합니다. 직접 빌드해 실행해 보니 존재하지 않는 그룹을 지정했을 때 다음과 같이 출력했습니다:

leviathan search · no customer matches "zzz"; try `resolve` with part of the name, or search without one

지정한 그룹 안에서 일치하는 레코드가 없으면 Leviathan은 다른 그룹의 결과에 OTHER CUSTOMER 같은 라벨을 붙여 보여줍니다. 모든 응답 머리말에는 shown N of M 이 붙어서, 에이전트가 "일치하는 기록이 없음"과 "데이터가 없음"을 구분할 수 있습니다. 함께 제공하는 에이전트 스킬 파일도 종료 코드 3을 받으면 후보를 사용자에게 보여주고 그룹을 추측하지 말라고 에이전트에게 지시합니다.

Leviathan으로 한국어 레코드를 검색할 때의 제약

Leviathan의 FTS5 인덱스는 porter unicode61 토크나이저를 씁니다(src/index.rs). 이 토크나이저는 공백과 구두점으로 낱말을 나누므로, 한국어에서 쿠키를과 쿠키는 서로 다른 토큰이 됩니다. 질의도 낱말을 따옴표로 감싸 정확히 일치하는 토큰만 찾고 접두어 일치를 붙이지 않습니다(src/text.rs). Porter 어간 추출은 영어 규칙이라 한국어 조사와 어미에는 적용되지 않습니다.

v0.1.0 코드(커밋 702ea92)를 직접 빌드해 한국어 레코드 3건으로 확인한 결과는 다음과 같습니다:

기록에 적힌 표현 질의 결과
세션 쿠키를 삭제해서 쿠키 0건
세션 쿠키를 삭제해서 세션 쿠키 1건 (세션만 일치)
비밀번호를 재설정하면 비밀번호를 1건
로그인 화면으로 로그인 1건

같은 실험에서 형태소 단위로 띄어 쓴 필드(쿠키 를 삭제 해서)를 레코드에 추가하고 --text body --text body_morph 로 검색 대상에 넣자 쿠키 질의가 해당 레코드를 찾았습니다. 한국어 데이터라면 형태소 분석기로 이런 보조 필드를 만든 뒤 색인하는 방법을 생각할 수 있습니다. 다만 이 방법은 Leviathan이 공식으로 안내하는 기능이 아니며, 실제 데이터에서의 검색 품질은 확인하지 않았습니다.

Leviathan 벤치마크

Leviathan 개발자가 직접 측정해 공개한 벤치마크는 생성기가 만든 합성 설비 정비 기록을 씁니다. 장비 1,500대에 기록이 지프(Zipf) 분포로 쏠려 있고, 고장 수리 기록의 조치 내용 중 27%는 "done", "fixed" 같은 형식적인 문구, 15%는 빈 값으로 두어 검색을 일부러 어렵게 만들었습니다. 규모마다 질문 200개를 만들고, 같은 장비의 같은 고장 유형이면서 실제 조치 내용이 담긴 레코드를 정답으로 봅니다. Leviathan은 질문마다 상위 5건(-n 5)을 돌려주도록 실행했고, 토큰은 tiktoken의 o200k_base로 셌습니다. 또한 grep 전략에는 질문이 장비를 이름으로 불렀더라도 정확한 키를 미리 넘겨 주었습니다.

아래 그림은 100만 건 데이터에서 질문 하나에 에이전트 컨텍스트로 들어가는 토큰 수의 중앙값을 로그 축으로 비교한 것입니다. 가로축은 "median tokens put into the agent's context per question" 입니다:

6개 규모 중 세 규모의 수치를 옮기면 다음과 같습니다:

레코드 수 Leviathan 토큰 중앙값 grep + 질문 낱말 grep 대상 전체 이력 Leviathan hit@1 / hit@5 Leviathan 지연 p50
10,000 404 1,182 2,193 90.0% / 97.5% 10.8 ms
100,000 443 7,750 18,484 93.5% / 98.0% 14.5 ms
1,000,000 436 107,122 209,412 98.5% / 99.0% 33.4 ms

grep 쪽 정확도는 정답 레코드가 출력 앞부분 3만 자 안에 들어오는지로 쟀고, 100만 건에서 grep + 질문 낱말은 96.0%, 대상 전체 이력은 83.0%였습니다. 출력 길이 제한이 없으면 grep 출력에도 정답이 98.5~100% 들어 있으므로, 정확도 차이보다 토큰 차이가 이 벤치마크의 핵심입니다. 꼬리 분포에서 차이가 더 벌어져, 1,200개 질문 전체에서 Leviathan의 최대 답변은 602토큰이었고 grep + 질문 낱말의 최댓값은 약 970만 토큰이었습니다. 아래 두 그림은 데이터 크기에 따른 토큰 수(세로축 "median tokens per question")와 정답 포함 비율(세로축 "questions answered")입니다:

개발자가 밝힌 한계도 함께 봐야 합니다. 데이터가 합성이라 정확도는 상한으로 봐야 하고, 데이터 형태도 "한 대상에 기록이 많고 한 대상에 대해 묻는" 한 가지뿐입니다. 이 벤치마크는 검색 결과만 측정하며, LLM이 출력을 읽고 최종 답을 낸 결과가 아닙니다. LLM을 포함한 벤치마크는 로드맵에 있다고 적혀 있습니다. 지연 시간은 2018년 데스크톱 CPU(AMD Ryzen 7 2700X) 한 대에서 한 번 측정한 값이며, 10만 건 이하에서는 grep이 Leviathan과 비슷하거나 더 빨랐습니다. 인덱스 크기는 원본의 약 1.8배(100만 건, 678 MB 원본에 1.2 GB)이고 빌드에 52초가 걸렸습니다. 전체 수치와 재현 방법은 저장소의 docs/BENCHMARKS.md에 있습니다.

Leviathan 설치와 사용

Leviathan은 crates.io에 leviathan-index 라는 이름으로 올라가 있고, 설치되는 실행 파일 이름은 leviathan 입니다. macOS, Linux, Windows용 빌드 파일은 릴리스 페이지에서 받을 수 있습니다. 저장소의 예제 티켓 24건으로 시작하는 기본 사용법은 다음과 같습니다:

cargo install leviathan-index       # or: cargo install --git https://github.com/elstongun/leviathan
cd examples/tickets && leviathan index
leviathan search -g acme "sso login loop after password reset"

examples/tickets 는 저장소를 내려받아야 있는 경로이므로, 위 명령은 저장소를 clone한 뒤 그 안에서 실행합니다. 자신의 데이터는 leviathan init 으로 필드 매핑을 제안받거나 플래그로 직접 지정해 색인합니다. 데이터베이스는 각자의 CLI 출력을 파이프로 넘깁니다:

leviathan init ./export                       # infer a commented leviathan.toml
leviathan index tickets.csv --id "Ticket ID" --group customer_id --date created_at   # or flags
leviathan describe                            # fields, groups, filter values, example calls

psql "$DATABASE_URL" -At -c "SELECT row_to_json(t) FROM tickets t" | leviathan index - -c tickets.toml
duckdb -json -c "SELECT * FROM 'events/*.parquet'" | leviathan index - -c events.toml

init 은 레코드를 최대 2,000건 표본 추출해 필드마다 등장 빈도와 고유값 수, 길이, 날짜 여부를 보고, 선택한 이유를 주석으로 단 leviathan.toml 을 씁니다. 필드 매핑 전체 규칙은 docs/CONFIG.md에 있습니다.

에이전트 연결 방법은 두 가지입니다. 개발자가 권장하는 방법은 skills/leviathan 스킬 파일을 ~/.claude/skills/ 에 복사하거나 AGENTS.md 에 붙여 넣는 것입니다. 이 방법은 셸을 쓰는 에이전트에서 동작하고, 호출하기 전까지 토큰을 쓰지 않습니다. MCP 서버로 쓰려면 leviathan mcp 를 실행합니다. 이 서버는 search, resolve_group, get, describe 네 가지 읽기 전용 도구를 stdio로 제공합니다. 도구 설명에 데이터셋 요약이 들어가므로 세션마다 약 640토큰을 씁니다. 에이전트별 설정은 leviathan wrap 이 출력합니다:

leviathan wrap claude    # claude | cursor | codex | vscode | gemini | windsurf | generic

Leviathan의 라이선스

Leviathan은 Apache-2.0 라이선스로 공개되어 있어 개인 및 상업적 목적으로 자유롭게 사용할 수 있습니다.

:github: Leviathan 프로젝트 GitHub 저장소

더 읽어보기




이 글은 GPT 모델로 정리한 초안을 바탕으로 한 것으로, 원문의 내용 또는 의도와 다르게 정리된 내용이 있을 수 있습니다. 관심있는 내용이시라면 원문도 함께 참고해주세요! 읽으시면서 어색하거나 잘못된 내용을 발견하시면 댓글로 알려주시기를 부탁드립니다. :hugs:

:pytorch:파이토치 한국 사용자 모임:south_korea:에서 이런 글들을 계속 정리하고 있습니다. 회원 가입으로 주요 글들을 이메일:love_letter:로, 텔레그램(Telegram)이나 Slack/Discord/Teams/Dooray/GoogleChat 등으로 새 글 알림을 받아보세요! :smiley:

:wrapped_gift: 아래:down_right_arrow:쪽에 좋아요:+1:를 눌러주시면 새로운 소식들을 정리하고 공유하는데 힘이 됩니다~ :star_struck: