구조화된 데이터(엑셀·테이블) RAG에 통합하기
지난 글에서 Query Routing을 통해 정형 데이터 질문은 Text-to-SQL로 분기하는 구조를 다뤘습니다. 그런데 실무에서 다루는 정형 데이터가 항상 정돈된 DB 테이블 형태인 것은 아닙니다. 엑셀 파일, CSV로 흩어진 사내 데이터가 훨씬 많습니다. 이번 글에서는 이런 구조화된 데이터를 RAG 파이프라인에 통합하는 방법을 다룹니다.
1. 엑셀 데이터를 텍스트처럼 임베딩하면 안 되는 이유
지금까지 다룬 청킹 방식을 엑셀에 그대로 적용하면 문제가 생깁니다.
[엑셀 원본]
| 부서 | 담당자 | 예산(만원) |
|------|--------|-----------|
| 개발팀 | 김철수 | 5000 |
| 영업팀 | 이영희 | 3200 |
| 인사팀 | 박민수 | 1800 |
[텍스트로 그대로 청킹한 경우]
"부서 담당자 예산(만원) 개발팀 김철수 5000 영업팀 이영희 3200..."
→ 행과 열의 관계가 사라져, "개발팀 예산이 얼마야?" 같은 질문에
정확히 답하기 어려움
지난 멀티모달 RAG 글에서 표 이미지를 다룰 때와 비슷한 문제입니다. 다만 엑셀은 이미 구조화된 데이터이기 때문에, Vision LLM으로 이미지를 해석하는 대신 훨씬 정확하고 저렴한 방법을 쓸 수 있습니다.
2. 두 가지 접근 - 텍스트 변환 vs DB 적재
엑셀 데이터를 다루는 방법도 지난 글의 정형 데이터 라우팅과 비슷하게 두 갈래로 나뉩니다.
| 방식 | 적합한 경우 |
|---|---|
| 행 단위로 자연어 문장 변환 후 임베딩 | 검색·요약 위주 질문, 벡터 검색과 자연스럽게 통합 |
| DB 테이블로 적재 후 Text-to-SQL 활용 | 집계·필터링·정렬이 필요한 질문 |
"개발팀 담당자가 누구야?" 같은 단순 조회는 첫 번째 방식으로 충분하지만, "예산이 3000만원 넘는 부서는?"처럼 조건 필터링이 필요한 질문은 지난 글의 Text-to-SQL 경로가 훨씬 정확합니다. 두 방식을 함께 갖춰두는 것이 이상적입니다.
3. 행 단위 자연어 변환하기
각 행을 하나의 완결된 문장으로 바꿔서, 행이 곧 하나의 검색 가능한 청크가 되도록 만듭니다.
import pandas as pd
def row_to_sentence(row):
return f"{row['부서']}의 담당자는 {row['담당자']}이며, 예산은 {row['예산(만원)']}만원입니다."
df = pd.read_excel("department_budget.xlsx")
sentences = df.apply(row_to_sentence, axis=1).tolist()
for sentence in sentences:
print(sentence)
[변환 결과]
개발팀의 담당자는 김철수이며, 예산은 5000만원입니다.
영업팀의 담당자는 이영희이며, 예산은 3200만원입니다.
인사팀의 담당자는 박민수이며, 예산은 1800만원입니다.
이렇게 변환된 문장은 지금까지 다룬 청킹·임베딩·벡터 저장 파이프라인에 그대로 태울 수 있습니다. 행 하나가 이미 적절한 크기의 청크이기 때문에, 별도의 추가 분할이 필요 없는 경우가 많습니다.
4. 컬럼이 많은 표는 문장 템플릿을 신중히 설계해야 한다
컬럼 수가 많아지면 기계적으로 모든 값을 나열하기보다, 자주 검색될 만한 조합을 고려해서 문장을 구성하는 것이 검색 품질에 유리합니다.
# 컬럼이 많은 경우 예시
def row_to_sentence_detailed(row):
return (
f"{row['부서']} 부서는 {row['담당자']}가 담당하며, "
f"{row['분기']} 기준 예산은 {row['예산(만원)']}만원이고 "
f"집행률은 {row['집행률']}%입니다."
)
컬럼을 단순 나열하면 검색 시 불필요한 정보까지 함께 매칭되어 유사도가 흐려질 수 있습니다. 실제 질문 패턴("담당자가 누구야", "예산이 얼마야", "집행률이 어때")을 미리 고려해서 문장 구조를 짜는 편이 좋습니다.
5. DB 테이블로 적재해 Text-to-SQL과 연결하기
집계나 조건 필터링이 필요한 질문에 대응하려면, 엑셀 데이터를 아예 RDBMS 테이블로 적재해두고 지난 글의 Text-to-SQL 경로로 연결합니다.
import pandas as pd
from sqlalchemy import create_engine
engine = create_engine("postgresql+psycopg2://postgres:postgres@localhost:5432/postgres")
df = pd.read_excel("department_budget.xlsx")
df.to_sql("department_budget", engine, if_exists="replace", index=False)
private static final String SCHEMA_INFO = """
테이블: department_budget (부서, 담당자, 예산_만원, 분기, 집행률)
""";
지난 글에서 만든 TextToSqlService의 스키마 정보에 이 테이블만 추가해주면, "예산이 3000만원 넘는 부서 알려줘" 같은 질문은 자동으로 SQL 조회 경로로 라우팅되어 정확한 답을 받을 수 있습니다.
6. 엑셀 업로드부터 적재까지 자동화하기
관리자가 엑셀 파일을 업로드하면, 두 가지 형태(자연어 청크 + DB 테이블)로 동시에 적재되도록 파이프라인을 구성합니다.
@Service
@RequiredArgsConstructor
public class ExcelIngestService {
private final VectorStore vectorStore;
private final JdbcTemplate jdbcTemplate;
@Async("documentIngestExecutor")
public void ingestExcel(MultipartFile file, String sheetName) {
List<Map<String, Object>> rows = parseExcel(file, sheetName);
// 1) 자연어 청크로 변환 후 벡터 저장
List<Document> documents = rows.stream()
.map(row -> new Document(
rowToSentence(row),
Map.of("source", file.getOriginalFilename(), "content_type", "table_row")))
.toList();
vectorStore.add(documents);
// 2) DB 테이블로도 적재
insertIntoTable(sheetName, rows);
}
}
업로드 한 번으로 "검색용 자연어 청크"와 "집계용 DB 테이블"이 함께 준비되어, 질문 유형에 따라 지난 글의 라우터가 알아서 적절한 경로를 선택하게 됩니다.
7. 정합성 유지하기 - 원본과 두 표현 동기화
엑셀이 갱신되면, 자연어 청크와 DB 테이블 둘 다 최신 상태로 함께 갱신해야 합니다. 어느 한쪽만 갱신되면 벡터 검색 결과와 SQL 조회 결과가 서로 다른 답을 낼 수 있습니다.
@Async("documentIngestExecutor")
public void reingestExcel(MultipartFile newFile, String sheetName, String source) {
vectorStore.delete(List.of("source", source)); // 기존 청크 삭제
ingestExcel(newFile, sheetName); // 청크 재생성 + 테이블 재적재(REPLACE)
}
지난 임베딩 마이그레이션 글에서 다룬 "갱신 시 관련 데이터를 함께 정리한다"는 원칙이 여기서도 그대로 적용됩니다.
8. 정리
- 엑셀 같은 표 데이터를 그대로 텍스트 청킹하면 행과 열의 관계가 깨져 검색 정확도가 떨어집니다.
- 단순 조회 질문은 행 단위 자연어 변환 후 벡터 검색으로, 집계·필터링 질문은 DB 테이블 적재 후 Text-to-SQL로 나눠서 대응하는 것이 효과적입니다.
- 두 표현(자연어 청크, DB 테이블)을 함께 유지·갱신해야 벡터 검색과 SQL 조회 결과 간 정합성이 깨지지 않습니다.
다음 글에서는 지금까지 텍스트 위주로 다룬 원본 문서 처리 범위를 넓혀서, 스캔된 PDF나 이미지 문서에서 텍스트를 정확히 뽑아내는 OCR과 레이아웃 인식 기법을 다뤄보겠습니다.
'RAG를 활용한 LLM 서비스 구축' 카테고리의 다른 글
| STEP 25 - RAG 감사 로그 (1) | 2026.07.23 |
|---|---|
| STEP 24 - OCR과 레이아웃 인식 (0) | 2026.07.21 |
| STEP 22 - Query Routing (0) | 2026.07.20 |
| STEP 21 - 멀티모달 RAG (0) | 2026.07.20 |
| STEP 20 - GraphRAG (0) | 2026.07.15 |