# Parquet와 Polars로 텍스트 임베딩을 효율적으로 사용하는 방법

> Clean Markdown view of GeekNews topic #19601. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=19601](https://news.hada.io/topic?id=19601)
- GeekNews Markdown: [https://news.hada.io/topic/19601.md](https://news.hada.io/topic/19601.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2025-03-06T22:53:20+09:00
- Updated: 2025-03-06T22:53:20+09:00
- Original source: [minimaxir.com](https://minimaxir.com/2025/02/embeddings-parquet/)
- Points: 7
- Comments: 1

## Summary

텍스트 임베딩을 효율적으로 저장하고 검색하기 위해서는 Parquet 파일 형식과 Polars 라이브러리를 사용하는 것이 효과적입니다. Parquet는 컬럼 기반 데이터 저장 형식으로, 임베딩 데이터를 빠르게 저장하고 로드할 수 있으며, Polars는 `float32` 배열을 그대로 유지하여 성능을 최적화합니다. 특히 작은 규모의 프로젝트에서는 Parquet와 Polars 조합이 벡터 데이터베이스보다 더 빠르고 비용 효율적인 대안이 될 수 있습니다.

## Topic Body

### 텍스트 임베딩을 휴대 가능하게 사용하는 최고의 방법은 Parquet와 Polars  
- 텍스트 임베딩은 대형 언어 모델에서 생성된 벡터로, 단어, 문장, 문서를 수치적으로 표현하는 방식임  
- 2025년 2월 기준, 총 32,254개의 "매직: 더 개더링" 카드 임베딩을 생성함  
- 이를 통해 카드의 디자인 및 기계적 속성을 기반으로 유사성을 수학적으로 분석 가능함  
- 생성된 임베딩을 2D [UMAP](https://umap-learn.readthedocs.io/en/latest/) 차원 축소를 통해 시각화 가능  
- 사용한 임베딩 모델은 [gte-modernbert-base](https://huggingface.co/Alibaba-NLP/gte-modernbert-base)이며, 상세 과정은 [GitHub 저장소](https://github.com/minimaxir/mtg-embeddings)에 정리됨  
- 해당 임베딩 데이터셋은 [Hugging Face](https://huggingface.co/datasets/minimaxir/mtg-embeddings)에서 제공됨  
  
### 벡터 데이터베이스의 필요성 재고  
- 일반적으로 벡터 데이터베이스([faiss](https://github.com/facebookresearch/faiss), [qdrant](https://qdrant.tech/), [Pinecone](https://www.pinecone.io/))를 활용하여 임베딩을 저장 및 검색함  
- 그러나 벡터 데이터베이스는 복잡한 설정이 필요하며, 클라우드 서비스는 비용이 높을 수 있음  
- 작은 규모의 데이터(수만 개 수준)라면 벡터 데이터베이스 없이도 numpy를 활용하여 빠른 유사도 검색 가능함  
- numpy의 `dot product` 연산을 활용하면 단순한 코사인 유사도 계산이 가능하며, 32,254개 임베딩에 대해 평균 1.08ms 소요됨  
  
```python  
def fast_dot_product(query, matrix, k=3):  
    dot_products = query @ matrix.T  
  
    idx = np.argpartition(dot_products, -k)[-k:]  
    idx = idx[np.argsort(dot_products[idx])[::-1]]  
  
    score = dot_products[idx]  
  
    return idx, score  
```  
  
- 벡터 데이터베이스를 사용하면 특정 라이브러리 및 서비스에 종속될 가능성이 큼  
- GPU 서버에서 임베딩을 생성 후 로컬로 다운로드하는 경우, 효율적인 데이터 저장 및 전송 방식이 필요함  
  
### 최악의 임베딩 저장 방식  
- **CSV 파일**  
  - 부동소수점(`float32`) 데이터를 텍스트로 저장하면 크기가 6배 이상 증가함  
  - OpenAI의 [공식 튜토리얼](https://github.com/openai/openai-cookbook/blob/a3e98ea4dcf866b5e7a3cb7d63dccaa68c7d63aa/examples/Embedding_Wikipedia_articles_for_search.ipynb)에서도 작은 데이터셋에만 CSV 사용을 권장함  
  - numpy의 `.savetxt()`를 사용하여 저장하면 파일 크기가 **631.5MB**로 증가함  
- **pickle 파일**  
  - 빠르게 저장 및 로드 가능하나 보안 위험이 존재하며, 버전 호환성이 떨어짐  
  - 파일 크기는 **94.49MB**로 원본 메모리 크기와 동일하지만, 이식성이 낮음  
  
### 나쁘지는 않지만 최적이 아닌 저장 방식  
- **numpy의 `.npy` 형식**  
  - `allow_pickle=False` 설정을 통해 pickle 저장을 방지 가능함  
  - 파일 크기와 속도는 pickle 방식과 동일하며, 개별 메타데이터를 함께 저장하기 어려움  
- **메타데이터와 분리된 저장 구조의 문제점**  
  - numpy 배열(`.npy`)로 저장하면 카드 정보(이름, 텍스트 등)와 임베딩이 분리됨  
  - 데이터가 변경(추가/삭제)될 경우 메타데이터와 임베딩의 매칭이 어려워짐  
  - 벡터 데이터베이스에서는 메타데이터와 벡터를 함께 저장하고 필터링 기능을 제공함  
  
### 최적의 임베딩 저장 방식: Parquet + polars  
#### Parquet 파일 형식 소개  
- [Apache Parquet](https://parquet.apache.org/)는 컬럼 기반 데이터 저장 형식으로, 각 컬럼의 데이터 타입을 명확하게 지정 가능함  
- 리스트 형태(`float32` 배열)의 데이터를 저장할 수 있어 임베딩 저장에 적합함  
- CSV보다 빠른 저장 및 로드 성능을 제공하며, 일부 데이터만 선택적으로 로드 가능함  
- 압축 기능 제공하지만, 임베딩 데이터는 중복성이 낮아 압축 효과가 적음  
#### Python에서 Parquet 파일 활용  
- **pandas**를 활용한 Parquet 파일 저장 및 로드:  
  ```python  
  df = pd.read_parquet("mtg-embeddings.parquet", columns=["name", "embedding"])  
  df  
  ```  
  - pandas는 중첩된 데이터(리스트)를 효율적으로 처리하지 못하며, numpy `object`로 변환됨  
  - numpy 배열 변환 시 추가적인 연산(`np.vstack()`)이 필요하여 성능 저하 발생 가능함  
- **polars**를 활용한 Parquet 파일 저장 및 로드:  
  ```python  
  df = pl.read_parquet("mtg-embeddings.parquet", columns=["name", "embedding"])  
  df  
  ```  
  - polars는 `float32` 배열을 그대로 유지하며, `to_numpy()` 호출 시 즉시 2D numpy 배열 반환 가능함  
  - `allow_copy=False` 설정을 통해 불필요한 데이터 복사를 방지 가능함  
  ```python  
  embeddings = df["embedding"].to_numpy(allow_copy=False)  
  ```  
- 새로운 임베딩을 추가할 때도 간단하게 컬럼을 추가하여 저장 가능함  
  ```python  
  df = df.with_columns(embedding=embeddings)  
  df.write_parquet("mtg-embeddings.parquet")  
  ```  
### Parquet + polars를 활용한 유사도 검색 및 필터링  
- 특정 조건을 만족하는 데이터만 필터링한 후 유사도 검색 수행 가능함  
- 예: 특정 카드(`query_embed`)와 유사한 카드를 찾되, 'Sorcery' 타입이며 'Black' 색상이 포함된 카드만 검색  
  ```python  
  df_filter = df.filter(  
      pl.col("type").str.contains("Sorcery"),  
      pl.col("manaCost").str.contains("B"),  
  )  
  
  embeddings_filter = df_filter["embedding"].to_numpy(allow_copy=False)  
  idx, _ = fast_dot_product(query_embed, embeddings_filter, k=4)  
  related_cards = df_filter[idx]  
  ```  
- 평균 실행 시간 **1.48ms**로, 전체 데이터 검색보다 37% 느리지만 여전히 빠름  
  
### 대규모 벡터 데이터 처리를 위한 대안  
  
- Parquet와 dot product 방식은 **수십만 개 임베딩**까지는 충분히 처리 가능함  
- 더 큰 데이터셋을 다룰 경우, 벡터 데이터베이스 사용이 필요할 수 있음  
- 대안으로 [SQLite](https://www.sqlite.org/) 기반의 [sqlite-vec](https://alexgarcia.xyz/sqlite-vec/)을 활용하면 추가적인 벡터 검색 및 필터링 가능함  
  
### 결론  
  
- 벡터 데이터베이스가 필수적인 것은 아님  
- Parquet + polars 조합은 임베딩을 효율적으로 저장, 검색 및 필터링할 수 있는 강력한 대안임  
- 특히 작은 규모의 프로젝트에서는 Parquet 파일을 활용하는 것이 더 빠르고 비용 효율적임  
- 프로젝트에 따라 Parquet와 벡터 데이터베이스 중 적절한 솔루션을 선택하는 것이 중요함  
- [GitHub 저장소](https://github.com/minimaxir/mtg-embeddings)에서 코드 및 데이터 확인 가능함

## Comments



### Comment 35532

- Author: neo
- Created: 2025-03-06T22:53:20+09:00
- Points: 1

###### [Hacker News 의견](https://news.ycombinator.com/item?id=43162995) 
* Parquet의 문제점은 정적이라는 것임. 지속적인 쓰기와 업데이트가 필요한 경우에는 적합하지 않음. 그러나 DuckDB와 객체 저장소의 Parquet 파일을 사용했을 때는 좋은 결과를 얻었음. 빠른 로드 시간임
  - 자체 임베딩 모델을 호스팅하면 numpy float32 압축 배열을 바이트로 전송한 후 다시 numpy 배열로 디코딩할 수 있음
  - 개인적으로는 SQLite와 usearch 확장을 사용하는 것을 선호함. 이진 벡터를 사용한 후 상위 100개를 float32로 재정렬함. 약 20,000개의 항목에 대해 약 2ms가 소요되며, 이는 LanceDB보다 빠름. 더 큰 컬렉션에서는 Lance가 이길 수도 있음. 그러나 내 사용 사례에서는 각 사용자가 전용 SQLite 파일을 가지고 있어 잘 작동함
  - 이동성을 위해서는 Litestream이 있음

* 정말 멋진 기사임. 오랫동안 당신의 작업을 즐겼음. SQLite 구현에 뛰어드는 사람들을 위해 DuckDB가 Parquet을 읽고 이 사용 사례를 완벽하게 다루는 몇 가지 벡터 유사성 기능을 시작했다는 점을 추가할 수 있음

* 데이터프레임을 여전히 좋아하지 않지만, Polars는 pandas보다 훨씬 나음
  - 시간 시리즈 계산을 하고 있었는데, 기본적으로 간단한 주식 가격 조정을 했음
  - 코드 읽기와 테스트가 실제로 가능하다는 점에서 놀라웠음
  - 실행 속도가 너무 빨라서 고장 난 것처럼 보였음

* Unum의 usearch를 확인해 보세요. 무엇이든 이기고 사용하기 매우 쉬움. 필요한 것을 정확히 수행함

* 시도해 보고 싶다면 HF에서 게으르게 로드하고 필터링을 적용할 수 있음
  - Polars는 사용하기 훌륭하며 강력히 추천함. 단일 노드에서 CPU를 포화시키는 데 탁월하며, 작업을 분산해야 한다면 Ray Actor에 POLARS_MAX_THREADS를 적용하여 단일 노드의 포화 정도에 따라 조정할 수 있음

* 많은 훌륭한 발견이 있음
  - 구조화된 데이터를 임베딩 API에 전달하는 것이 더 나은지, 비구조화된 데이터를 전달하는 것이 더 나은지 궁금함. ChatGPT에 물어보면 비구조화된 데이터를 보내는 것이 더 낫다고 함
  - 내 사용 사례는 jsonresume을 위한 것임. 전체 json 버전을 문자열로 보내 임베딩을 생성하고 있지만, 먼저 resume.json을 전체 텍스트 버전으로 번역한 후 임베딩을 생성하는 모델을 사용해 실험하고 있음. 결과가 더 나은 것 같지만, 이에 대한 구체적인 의견은 보지 못했음
  - 비구조화된 데이터가 더 나은 이유는 자연어로 인해 텍스트/의미론적 의미를 포함하기 때문임

* Vespa 문서에서 벡터를 이진수로 변환한 후 16진수 표현을 사용하는 깔끔한 트릭이 있음
  - 이 트릭은 페이로드 크기를 줄이는 데 사용할 수 있음. Vespa에서는 이 형식을 지원하며, 동일한 벡터가 문서에서 여러 번 참조될 때 특히 유용함. ColBERT 또는 ColPaLi와 같은 경우(여러 임베딩 벡터가 있는 경우) 디스크에 저장된 벡터의 크기를 크게 줄일 수 있음

* Polars + Parquet는 이동성과 성능에 있어 훌륭함. 이 게시물은 Python 이동성에 중점을 두었지만, Polars는 엔진을 여러 곳에 임베딩할 수 있는 사용하기 쉬운 Rust API를 가지고 있음

* Polars의 열렬한 팬이지만, 이를 사용하여 임베딩을 저장하는 방법을 고려하지 않았음 (sqlite-vec을 가지고 실험 중이었음). 정말 흥미로운 아이디어인 것 같음

* 전체 텍스트 인덱싱 및 변경 사항 버전 관리 기능과 같은 뛰어난 성능과 기능을 가진 또 다른 라이브러리로 lancedb를 추천함
  - 벡터 데이터베이스이며 더 복잡하지만, 인덱스를 생성하지 않고 사용할 수 있으며, 뛰어난 polars 및 pandas 제로 복사 화살표 지원도 있음
