Ask HN: 데이터+임베딩+API 파이프라인을 위한 기능적 DSL은 왜 없는가?
요약
RAG(Retrieval-Augmented Generation) 워크로드 증가로 인해, 구조화된 데이터(JSONL)를 읽어 임베딩 벡터로 변환하고 외부 API/벡터 DB에 배치 전송하는 과정이 필수적입니다. 하지만 현재는 복잡한 중첩 for-loop와 수동적인 JSON 파싱 등 지저분하고 비효율적인 명령형 Python 코드로 처리해야 합니다. 본 글은 이 과정을 Unix 파이프나 Lisp/Elixir 같은 함수형 언어의 DSL처럼 선언적(declarative)이고 컴포저블한 방식으로 처리할 수 있는 전용 프레임워크 또는 DSL의 부
핵심 포인트
- 현재 데이터-임베딩 파이프라인은 JSONL 파일 읽기, 키 기반 매칭, (텍스트, 임베딩) 쌍 변환, 배치(batching), 외부 시스템 전송 등 복잡한 단계를 거치며 비효율적인 명령형 Python 코드를 요구합니다.
- 필자는 이 과정을 Unix 파이프(`|`)나 Lisp/Elixir의 함수형 구문처럼 `Source | Match | Transform | Filter | Batch | Sink`와 같은 선언적이고 컴포저블한 DSL로 구현해야 한다고 주장합니다.
- RAG(Retrieval-Augmented Generation)가 표준화되면서 이 패턴이 일반화되었음에도 불구하고, 기존의 Pandas나 Dask 등 범용 라이브러리로는 '구조화된 데이터 -> 변환 -> API 전송'이라는 특정 워크로드를 통합적으로 해결하기 어렵습니다.
- 이상적인 솔루션은 `cat input.jsonl | match output.jsonl on custom_id | extract (text, embedding) | filter not-empty | batch 50 | send-to-chroma`와 같이 직관적이고 파이프라인 지향적인 구문이어야 합니다.
Ask HN: 데이터+임베딩+API 파이프라인을 위한 기능적 DSL은 왜 없는가?
저는 상당히 흔한 문제를 다루고 있습니다:
- JSONL 파일에 구조화된 데이터를 가지고 있습니다 (in.jsonl, out.jsonl)
- 키를 기준으로 라인을 매칭합니다.
- 이를 (텍스트, 임베딩) 쌍으로 변환합니다.
- 선택적으로 필터링/매핑을 수행합니다.
- 배치 처리합니다 (50개 청크 단위).
- 각 배치를 외부 시스템(예: 벡터 DB, Chroma)에 푸시합니다.
그게 전부입니다. 사소하게 들립니다. 하지만 금세 지저분한 명령형 Python 코드가 됩니다: 중첩된 for 루프, 전역 인덱스, += 연산자, 수동 배치 처리, 라인별 처리, 저수준 JSON 파싱.
실제로는 다음과 같은 형태가 됩니다:
with open("in.json", "r") as fin:
with open("out.json", "r") as fout:
for in_line, out_line in zip(fin, fout):
in_data = json.loads(in_line)
out_data = json.loads(out_line)
if in_data["custom_id"] != out_data["custom_id"]:
raise Exception...
texts = in_data["body"]["input"]
embeddings = [d["embedding"] for d in out_data["response"]["body"]["data"]]
for i in range(len(texts)):
doc = texts[i]
emb = embeddings[i]
metadata = {
"source": f"chunk-{global_ids}",
우리는 2025년인데, 데이터가 API로 연결되는 방식이 이렇습니다.
왜 우리는 이것을 용인하는 걸까요?
이것은 선언적(declarative)이며 스트리밍(streaming) 데이터 처리 문제입니다. 왜 더 우아한 것을 사용하지 않는 걸까요? 기능적 파이프라인처럼, 더 조합 가능한(composable) 것이요?
저는 스스로에게 묻습니다: 이런 종류의 작업에 대해 조합 가능하고, 스트리밍하며, 기능적인 DSL은 왜 없는가?
유닉스 파이프처럼 만들 수는 없을까요?
제가 원하는 것은 다음과 같은 느낌입니다:
cat input.jsonl \
| match output.jsonl on custom_id \
| extract (text, embedding) \
| filter not-empty \
| batch 50 \
| send-to-chroma
Lisp / Clojure에서는:
(->> (zip input output)
(filter (= :custom_id))
(mapcat (fn [[in out]] (zip (:input in) (:embedding out))))
(partition-all 50)
(map send-to-chroma))
Elixir + Broadway에서는:
Broadway
|> read_stream("in.jsonl", "out.jsonl")
|> match_on(:custom_id)
|> map(&{&1.text, &1.embedding})
|> batch_every(50)
|> send_to_chroma()
그리고 다시 Python으로 돌아와서..
우리는 명령형스러운 찌꺼기를 작성하거나 다음과 같은 방식으로 임시방편적인 DSL을 구축하는 데 빠져 있습니다:
load_json_pairs()
| where(is_valid)
| select(to_embedding_record)
| batch(50)
| foreach(send_to_chroma)
...또는, 더 현실적으로는 with open(...) as f를 수천 줄 작성하는 것입니다.
tf.data.Dataset, dask.bag, pandas, 또는 pipe 같은 라이브러리가 존재함에도 불구하고, 이들은 어떤 것도 응집력 있고 표현력이 풍부한 방식으로 이 사용 사례를 해결하지 못합니다. 그들은
AI 자동 생성 콘텐츠
본 콘텐츠는 HN AI Engineering의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기