쿼리 언어가 Lua이며 내부에서 RAG를 구축할 수 있는 키-값 저장소 (Key-Value Store)
요약
Liath는 Lua를 쿼리 언어로 사용하는 임베디드 키-값 저장소로, 데이터를 애플리케이션으로 가져오는 대신 로직을 데이터로 보내 효율적인 처리를 지원합니다. 플러그인 시스템을 통해 RAG 구축, 벡터 인덱싱, LLM 호출 기능을 Lua 스크립트 내에서 단일 호출로 수행할 수 있습니다.
핵심 포인트
- Lua를 활용해 데이터 근처에서 로직을 실행하여 왕복 비용(Round-trip) 최소화
- LevelDB 및 RocksDB를 지원하는 플러그형 스토리지 백엔드 제공
- 임베딩, 벡터 인덱싱, LLM 호출 등 RAG 관련 기능을 플러그인으로 지원
- 네임스페이스를 통한 멀티 테넌트 격리 구현 가능
대부분의 임베디드 데이터베이스 (Embedded Database)는 두 가지 동사와 어깨를 으쓱하는 반응만을 제공합니다. 값을 넣고(Put), 값을 가져오는(Get) 것 말이죠.
그보다 더 영리한 작업, 즉 필터링(Filtering), 변환(Transforming), 읽기 결합(Combining reads) 등은 애플리케이션 언어에서 수행해야 하며, 이는 데이터를 경계 너머로 가져와서 작업한 뒤 다시 밀어 넣어야 함을 의미합니다.
이러한 왕복 과정(Round-trip)은 로직이 흥미로워지기 전까지는 괜찮습니다. 원자적으로 카운터를 증가시키거나, 문서를 읽고 임베딩(Embedding)하여 벡터(Vector)를 저장하거나, 유사도 검색(Similarity search)을 수행한 뒤 상위 결과값을 LLM에 전달하는 작업 등이 그렇습니다. 이 모든 과정은 앱 코드를 접착제(Glue)로 사용하여 여러 번의 클라이언트 호출로 나누어지게 됩니다.
Liath는 다른 선택을 합니다. Liath는 쿼리 언어가 실제 프로그래밍 언어인 Lua이며, 데이터 바로 옆에서 실행되는 임베디드 키-값 저장소 (Embedded Key-Value Store)입니다. 데이터를 로직으로 끌고 오는 대신, 로직을 데이터로 보냅니다.
핵심 아이디어
작동하는 데이터베이스를 만드는 데 필요한 설명은 세 줄이면 충분합니다:
from liath import EmbeddedLiath
db = EmbeddedLiath(data_dir="./data")
...
이것은 지루한 절반입니다. 흥미로운 나머지 절반은 Liath에 Lua 스크립트를 전달하면 db 객체와 일련의 plugins를 스크립트 내부에서 사용할 수 있는 상태로 단 한 번의 호출로 저장소에 대해 실행된다는 점입니다:
db.execute_lua('''
-- 데이터 저장
db:put("counter", "0")
...
읽기, 수정, 쓰기가 세 번의 Python 호출 대신 서버 측에서 한 번의 실행으로 이루어집니다. Lua는 이를 위한 좋은 선택입니다. 매우 작고 깔끔하게 임베딩되며, 바로 이러한 이유로 사람들이 이미 Redis와 Nginx 내부에서 수용하고 있는 언어이기 때문입니다.
작동 방식
Liath는 플러그형 스토리지 백엔드(Pluggable storage backend)와 Lua 런타임(Runtime)을 확장하는 플러그인 시스템을 갖춘 Python 패키지입니다.
스토리지(Storage)는 고정되어 있지 않습니다. 개발용으로는 LevelDB를, 운영용으로는 RocksDB를 사용할 수 있으며, storage_type을 auto, rocksdb, 또는 leveldb로 설정하여 엔진을 선택할 수 있습니다. 운영 환경 배포를 위해서는 다음과 같이 명시적으로 지정합니다:
db = EmbeddedLiath(
data_dir="/var/lib/liath",
storage_type="rocksdb"
...
Liath를 단순한 KV (Key-Value) 래퍼 그 이상으로 만드는 기능은 Lua 내부의 plugins 테이블 아래에 노출된 플러그인(plugins)에서 비롯됩니다. 일부는 항상 존재하며, 다른 것들은 추가 기능으로 설치합니다:
| 플러그인 (Plugin) | 기능 (Function) | 설치 (Install) |
|---|---|---|
db | 핵심 CRUD | 포함됨 |
| ... |
따라서 pip install liath[embed,vdb,llm] 명령을 통해 키-값 저장소를 텍스트를 임베딩(embedding)하고, 벡터를 인덱싱(indexing)하며, 모델을 호출할 수 있는 무언가로 변환할 수 있으며, 이 모든 것은 Lua 스크립트에서 접근 가능합니다.
네임스페이스(Namespaces)를 사용하면 별도의 데이터베이스를 실행하지 않고도 멀티 테넌트 격리(multi-tenant isolation)를 구현할 수 있습니다. 네임스페이스를 생성하고 해당 네임스페이스로 전환하면, 그곳에 기록된 키는 다른 네임스페이스와 충돌하지 않습니다:
db.create_namespace("production")
db.set_namespace("production")
db.put("config", '{"debug": false}')
...
또한 PluginBase를 상속받고 Lua에서 호출 가능한 함수를 노출함으로써 Python으로 자신만의 플러그인을 작성할 수 있으며, plugins_dir를 통해 이를 로드할 수 있습니다. 이는 내장 기능만으로 충분하지 않을 때 사용할 수 있는 탈출구(escape hatch) 역할을 합니다.
데이터베이스를 떠나지 않는 RAG
로직과 데이터를 같은 곳에 배치하는 것이 왜 중요한지를 보여주는 사례는 검색 증강 생성 (RAG, Retrieval-Augmented Generation)입니다. 대부분의 스택에서 RAG는 임베딩 서비스, 벡터 데이터베이스, 그리고 LLM API를 결합하는 오케스트레이션(orchestration) 스크립트로 구현됩니다. Liath에서는 README가 각 단계마다 하나의 Lua 스크립트로 이를 수행합니다.
인덱싱(Indexing)은 벡터 인덱스를 생성하고, 문서를 저장하며, 이를 임베딩하고, 벡터를 추가합니다:
db.execute_lua('''
local json = require("cjson")
...
쿼리(Querying)는 질문을 임베딩하고, 이웃(neighbours)을 검색하며, 일치하는 문서를 가져오고, 프롬프트(prompt)를 구축한 뒤 모델을 호출합니다. 이 모든 과정이 하나의 서버 측 스크립트에서 이루어집니다:
answer = db.execute_lua('''
local json = require("cjson")
local query = "What is Liath?"
...
임베딩 백엔드(embedding backend)는 FastEmbed이며, 벡터 검색(vector search)은 USearch를 사용하고, LLM 플러그인은 OpenAI 또는 Llama를 대상으로 합니다. 핵심은 이 중 어느 하나가 새롭다는 것이 아닙니다. 검색(retrieve), 증강(augment), 생성(generate) 루프가 데이터가 이미 존재하는 곳에서 실행된다는 점입니다.
적합하지 않은 경우
이제 솔직한 이야기를 해보겠습니다.
Liath는 단일 노드(single-node)이며 임베디드(embedded) 방식입니다. HTTP API를 제공하는 liath-server와 liath-cli가 존재하지만, 아키텍처는 분산 클러스터(distributed cluster)가 아닌 임베디드 저장소(embedded store)입니다. 수평적 확장(horizontal scale), 복제(replication), 또는 장애 조치(failover)가 필요하다면 이 도구는 적합하지 않습니다.
데이터베이스 내부에서 애플리케이션 로직을 실행하는 것은 눈을 뜨고 신중하게 결정해야 하는 트레이드오프(trade-off)입니다. 이는 사람들이 수년 동안 Redis Lua 및 Postgres 저장 프로시저(stored procedures)를 두고 논쟁해 온 것과 동일한 긴장 관계입니다. 로직이 데이터 바로 옆에 있기 때문에 속도는 빠르지만, 일반적인 애플리케이션 코드보다 테스트, 버전 관리 및 디버깅(debug)이 더 어려운 곳에 존재하게 됩니다. Python 파일 내의 Lua 문자열은 사용자가 직접 그렇게 만들지 않는 한, 테스트 스위트(test suite)의 일급 시민(first-class citizen)이 아닙니다.
AI 플러그인들은 강력하지만, 동시에 새로운 공격 표면(surface area)이기도 합니다. 임베딩(Embeddings), 벡터 검색(vector search), 그리고 LLM 호출(LLM calls)은 각각 고유한 의존성(dependency)과 실패 모드(failure modes)를 불러옵니다. Lua 스크립트 내부에서의 LLM 호출은 여전히 네트워크 지연 시간(network latency), 속도 제한(rate limits), 그리고 비용 문제를 수반합니다. 공동 배치(Co-location)는 서비스 간의 왕복 시간(round-trips)을 제거해주지만, 저 멀리 떨어져 있는 모델 API를 제거해주지는 않습니다.
그리고 Lua는 실제 언어이며, 이는 Lua가 '진짜 밧줄(real rope)'임을 의미합니다. 표현력이 풍부한 서버 측 스크립팅(server-side scripting)은 팀이 해당 스크립트를 나머지 코드와 동일한 규율로 다루지 않는다면, 유지보수가 불가능한 쓰레기 더미로 변질될 수 있는 바로 그런 종류의 강력한 힘입니다.
핵심 요약 (Takeaways)
- 로직을 데이터로 보내는 것(데이터를 로직으로 보내는 대신)은 임베디드 저장소로서 진정으로 다른 형태를 띠며, Lua는 이를 위한 검증된 선택입니다.
- 플러그인 모델이 핵심입니다. 선택적인
embed,vdb,llm확장 기능을 통해 동일한 KV 저장소가 완전한 RAG 루프를 호스팅할 수 있습니다. - 플러그인 가능한 RocksDB 또는 LevelDB 백엔드와 네임스페이스 격리(namespace isolation)는 단일 노드의 한계 내에서 이 도구를 단순한 장난감이 아닌 실제 운영 환경에 적합한(production-shaped) 형태로 만들어 줍니다.
- 데이터베이스 내부의 Lua를 저장 프로시저(stored procedures)처럼 다루십시오. 강력하고 빠르지만 남용하기 쉽습니다. 테스트하고, 버전 관리하십시오. 그렇지 않으면 후회하게 될 것입니다.
설치 방법, 코드 및 플러그인 참조는 여기에서 확인할 수 있습니다:
https://github.com/incredlabs/liath
만약 현재 RAG 파이프라인을 세 개의 별도 서비스로 운영하고 있다면, 이를 하나의 임베디드 저장소 (embedded store)로 통합하는 것이 여러분의 워크로드(workload)에 안도감을 줄지, 아니면 실수하기 쉬운 위험 요소(footgun)가 될지 알고 싶습니다. pip install liath[embed,vdb,llm]를 실행하여 RAG 예제를 돌려보시고, 어디서 문제가 발생하는지 저에게 알려주세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기