본문으로 건너뛰기

© 2026 Molayo

Qiita헤드라인2026. 05. 21. 14:19

GPU 없이 만드는 로컬 RAG 정밀도 개선편 (지원 파일 형식 확장 및 Hybrid Search)

요약

GPU 없이 동작하는 로컬 RAG 시스템의 성능을 개선하기 위해 지원 파일 형식을 확장하고 Hybrid Search를 도입하는 방법을 다룹니다. docx, xlsx, pptx, csv 등 다양한 Office 파일 형식을 처리하는 로직과 BM25를 결합하여 검색 정밀도를 높이는 기술적 상세 내용을 제공합니다.

핵심 포인트

  • docx, xlsx, pptx, csv 등 다양한 문서 형식에 대한 로더 확장 및 커스텀 구현 방법
  • CSV의 BOM(utf-8-sig) 문제 및 Excel 타이틀 행 처리 로직을 통한 데이터 정제
  • Windows 환경의 Office 임시 잠금 파일(~$로 시작하는 파일) 예외 처리
  • 벡터 검색의 한계를 보완하기 위해 BM25를 결합한 Hybrid Search 도입

이전 기사 「GPU 없이 만드는 로컬 RAG 입문 (LM Studio + LangChain + Chroma)」에서는 TXT / Markdown / PDF를 대상으로 하는 오프라인 동작 RAG 시스템을 구축했습니다.

본 기사에서는 지난번 구성에 대해 다음과 같은 2가지 개선 사항을 추가합니다.

지원 파일 형식의 확장: docx / xlsx / pptx / csv 대응 -
검색 정밀도 향상: BM25와 벡터 검색을 결합한 Hybrid Search 도입

지난 기사의 구성을 전제로 합니다. 변경 대상은 ingest.pyingest_v2.py, rag.pyrag_hybrid.py의 2개 파일입니다.

pip install docx2txt openpyxl python-pptx rank-bm25

지난번으로부터의 추가 사항은 위의 4개 패키지만입니다.

형식로더로드 단위
docxDocx2txtLoader (LangChain 표준)파일 전체로 1개 문서
csvCSVLoader (LangChain 표준)1행으로 1개 문서
xlsx커스텀 구현 (openpyxl)1행으로 1개 문서 (복수 시트 대응)
pptx커스텀 구현 (python-pptx)1슬라이드로 1개 문서

xlsx와 pptx에 대해서는 LangChain에 UnstructuredExcelLoader 등의 표준 로더가 존재하지만, unstructured는 의존 패키지가 매우 무겁고 오프라인 환경과의 상성도 좋지 않기 때문에, openpyxlpython-pptx를 직접 사용한 커스텀 구현을 채택하고 있습니다.

CSV의 BOM 문제

Excel에서 CSV를 「다른 이름으로 저장」하면 BOM이 포함된 UTF-8 (utf-8-sig)이 됩니다. CSVLoader의 기본값은 encoding='utf-8'이기 때문에, 그대로 두면 선두 필드명에 가 혼입되어 인덱스에 등록되어도 검색에서 히트하지 않게 됩니다.

# encoding='utf-8'인 상태라면 선두 필드가 깨짐
인시던트 ID: INC-2024-0001
# encoding='utf-8-sig'로 하면 정상
...

encoding='utf-8-sig'를 지정함으로써 해결할 수 있습니다.

XLSX의 타이틀 행 문제

Excel 파일의 첫 번째 행에 시트 타이틀(「사내 지식 — 사용자 대장」 등)이 들어있는 경우가 있습니다. rows[0]을 무조건 헤더로 취급하면 필드명이 깨진 상태로 인덱스에 등록됩니다.

# 타이틀 행을 헤더로 오인식했을 경우
사내 지식 — 사용자 대장: USR-005
: 야마다 하나코
...

「1개 셀에만 값이 있는 첫 번째 행」을 타이틀 행으로 판정하여 스킵하는 로직을 추가했습니다.

Office의 임시 잠금 파일 문제 (Windows)

Word나 PowerPoint에서 파일을 열고 있는 동안, Windows는 ~$rag_sample.pptx와 같은 임시 잠금 파일을 자동으로 생성합니다. path.rglob("*")로 이 파일을 잡아버리면 PackageNotFoundError로 크래시가 발생합니다. 루프의 시작 부분에서 ~$로 시작하는 파일을 스킵함으로써 회피할 수 있습니다.

import os
os.environ["HF_HUB_OFFLINE"] = "1"
from pathlib import Path
...

지난번 구성 (벡터 검색만 사용)으로 다음 쿼리를 시도한 결과입니다.

질문: INC-2024-0004는 어떤 장애인가요
답변: 문서에 기재되어 있지 않습니다
질문: 과제·리스크는
...

문서에는 INC-2024-0004 정보가 등록되어 있음에도 불구하고 히트하지 않았습니다.

원인은 벡터 검색 (Vector Search)의 특성에 있습니다. 벡터 검색은 의미적인 유사도로 검색하기 때문에, INC-2024-0004와 같은 ID 코드나 과제·리스크...

와 같은 짧은 키워드와의 유사도 계산이 약해집니다. 이러한 키워드 일치가 중요한 케이스에는 BM25가 유효합니다.

BM25 (Best Match 25)는 단어의 출현 빈도와 문서 빈도에 기반한 키워드 검색 알고리즘입니다. ID 코드나 고유명사와의 일치에 강하며, 벡터 검색 (Vector Search)과 상보적인 관계에 있습니다.

이번에는 BM25와 벡터 검색의 결과를 RRF (Reciprocal Rank Fusion)로 통합합니다. RRF는 각 리스트의 순위로부터 score = 1 / (k + rank)

를 산출하여 합산하는 수법입니다 (k=60은 논문 권장값). 스코어의 절대값에 의존하지 않기 때문에, BM25와 벡터 검색의 스코어 스케일 (Score Scale) 차이를 조정할 필요가 없으며, 하이퍼파라미터 (Hyperparameter) 튜닝이 불필요합니다.

BM25의 기본 토크나이저 (Tokenizer)가 일본어를 지원하지 않음

BM25Retriever의 기본 구현은 text.split() (공백 구분)입니다. 일본어는 공백 없이 연속되기 때문에, INC-2024-0004はどんな障害ですか가 1개의 토큰 덩어리가 되어, 코퍼스 (Corpus) 측의 INC-2024-0004와 일치하지 않습니다. 결과적으로 BM25가 아무것도 검색해내지 못해, 벡터 검색만으로 동작하던 지난번과 실질적으로 동일한 동작이 됩니다.

# 기본 토크나이즈 결과
"INC-2024-0004はどんな障害ですか" → ['INC-2024-0004はどんな障害ですか'] # 1토큰이 되어버림
"課題・リスクは" → ['課題・リスクは']

preprocess_func에 커스텀 토크나이저를 전달함으로써 해결합니다.

def japanese_tokenizer(text: str) -> list[str]:
"""
BM25용 일본어 토크나이저. 추가 패키지 불필요.
...

토크나이즈 동작 예시입니다.

"INC-2024-0004はどんな障害ですか" → ['INC-2024-0004', 'はど', 'どん', 'んな', 'な障', '障害', ...]
"課題・リスクは" → ['課題', '題・', '・リ', 'リス', 'スク', 'クは']

ID 코드는 그대로 1토큰으로서 추출되어 코퍼스 측과 일치하게 됩니다. MeCab 등의 형태소 분석기 (Morphological Analyzer)를 사용하면 더욱 고정밀도가 되지만, 추가 패키지가 필요하기 때문에 이번에는 오프라인 구성의 심플함을 우선하여 바이그램 (Bigram) 방식을 채택했습니다.

동일한 쿼리 (Query)로 변경 전후의 결과를 비교했습니다.

변경 전 (벡터 검색만 사용)

질문: INC-2024-0004はどんな障害ですか
답변: 문서에 기재되어 있지 않습니다
질문: 課題・リスクは
...

변경 후 (Hybrid Search)

질문: INC-2024-0004はどんな障害ですか
답변: 인시던트 ID: INC-2024-0004의 제목은 「VPN 접속 타임아웃」입니다.
질문: 課題・リスクは
...

ID 코드나 짧은 키워드에 의한 쿼리로도 히트(Hit)할 수 있게 되었습니다.

import os
os.environ["HF_HUB_OFFLINE"] = "1"
import re
...
# VectorDB를 삭제하고 재인덱싱
rm -r vector_db
python ingest_v2.py
...

이번에 추가한 변경 사항을 정리합니다.

파일 형식의 확장

추가 형식주요 주의 사항
docx특이사항 없음
csvencoding='utf-8-sig'로 BOM이 포함된 UTF-8 (Excel에서 저장한 CSV)에 대응
xlsx첫 행이 타이틀 행인 경우 두 번째 행을 헤더로 취급
pptx~$로 시작하는 임시 잠금 파일을 스킵 (Windows)

Hybrid Search

변경점내용
BM25 토크나이저기본값 (공백 구분)에서 일본어 대응 바이그램 방식으로 변경
Retriever 통합BM25와 벡터 검색의 결과를 RRF로 통합
효과ID 코드나 짧은 키워리 쿼리로도 히트할 수 있게 됨

AI 자동 생성 콘텐츠

본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0