관련 PR: https://github.com/AiResearch2025/FigVarietyRAGChat/pull/1
feat: vector DB (query by variety / feature) by gitchannn · Pull Request #1 · AiResearch2025/FigVarietyRAGChat
github.com
관련 레포: https://github.com/AiResearch2025/FigVarietyRAGChat/tree/pure-RAG
GitHub - AiResearch2025/FigVarietyRAGChat: RAG 기반 희귀품종 무화과 챗봇 프로젝트
RAG 기반 희귀품종 무화과 챗봇 프로젝트. Contribute to AiResearch2025/FigVarietyRAGChat development by creating an account on GitHub.
github.com
🌏 데이터 수집 from 무화과 도감
우선 RAG의 근간이 될 텍스트 데이터를 모았습니다. 무화과 품종 관련 자료를 모으기 위해서 무화과도감의 데이터를 이용했습니다.

무화과도감을 LLM이 해석한 자연어를 각각은 txt 파일에 넣었습니다. 좀더 와닿도록 하나만 예를 들어서 보여드리자면,
Strawberry_Verte.txt
스트로베리 베르테(Strawberry Verte, SV)는 Marius Nedelcu에 의해 2011~2012년에 프랑스에서 국내로 도입된 품종이다.
국내에서는 ‘SV’, ‘SV_unk’ 등의 이름으로 불리며, 달콤한 베리 향과 진한 붉은 과육으로 유명하다.
이 품종은 미국 동호인들 사이에서 인기를 얻으며 퍼져 나갔으며,
녹색 과피와 붉은 과육의 강한 대비로 인해 시각적인 매력이 매우 뛰어나다.
잘 익은 과실은 시원한 산미와 달콤한 향이 균형을 이루며, 생식용과 상업용 모두 적합한 품종으로 평가된다.
열매는 소형에서 중형 크기이며, Fig의 평균 과중은 32~56g, Breba는 70~118g이다.
열매의 형태는 물방울형이며 과피는 연녹색에서 완숙 시 연한 녹황색으로 변한다.
과육은 진한 붉은색(딸기색에 가까움)으로, 베리 타임의 매우 진한 단맛과 크리미한 식감이 특징이다.
보존성은 높고, 꼭지는 매우 튼튼하다.
...
이런식으로 최대한 해당 품종에 대한 정보를 최대한 자세히 정리했습니당
🌏 벡터 DB 구축 전략
벡터DB를 설계할 때 있어서 크게는 2가지 종류의 쿼리를 고민해 보았습니다.
✅ 확실한 무화과 품종 이름을 알고 물어보는 경우
BNR이 정말 무화과의 황제 급으로 달아? 몇 브릭스야?
햇빛이 더 많이 필요한건 CDDB vs 하디시카고? (두가지 비교)
벡터DB에는 품종 이름만 임베딩하고, 이후에 이 품종이 사용자의 질문에서 언급된 것과 얼마나 유사한가를 찾는 역할만 하도록 했습니다. (오탈자, 약어, 유사명 등을 처리하는 lookup index로 활용)
예를 들어서 AI Agent가 “Ciccio Nero”가 언급되었다고 판단하면, 벡터DB에서 품종명 유사도 검색 → 가장 가까운 품종명 key 반환 → 해당 품종 txt 파일 로드 → RAG context 구성, 이 순서로 진행됩니다.
| 품종명 | 임베딩 내용 |
|---|---|
| Brunswick | “Brunswick fig variety” |
| Ciccio Nero | “Ciccio Nero fig variety” |
| Hardy Chicago | “Hardy Chicago fig variety” |
| ... | ... |
처음에는 그냥 txt 파일 내용 전체를 함께 임베딩하는 방안을 생각해 보았지만, chunk로 끊어서 문장을 저장하다보면 문장 내에 서로 상충되는 단어들이 종종 존재하기도 하고 가장 치명적으로 느꼈던 부분은, 하나의 품종을 설명할 때 다른 품종과의 비교를 하는 부분에 있어서 이런 부분이 나중에 오동작의 원인이 될 것 같았기 때문입니다.


청크로 저런 부분 500자가 함께 저장된다면,, 요즘 vector db는 그래도 성능이 괜찮다고는 하지만, 무화과는 워낙에 품종별로 아주 미묘한 차이가 있기 때문에 세밀히 분간하기 어렵다고 판단했습니다.
결과적으로 품종 이름만을 vector db에 저장했습니다.

# === 벡터DB 생성 ===
import os
from langchain_community.vectorstores import FAISS
from langchain_community.embeddings import HuggingFaceEmbeddings
# === 설정 ===
DATA_DIR = "./varieties"
DB_PATH = "./vector_db"
MODEL_NAME = "intfloat/multilingual-e5-base"
# === 임베딩 모델 로드 ===
embedding = HuggingFaceEmbeddings(model_name=MODEL_NAME)
# === 데이터 생성 (파일 이름 기반) ===
texts = []
metadatas = []
for fname in os.listdir(DATA_DIR):
if fname.endswith(".txt"):
# 파일 이름에서 확장자를 제거하고 " fig variety"를 추가합니다.
variety_name = os.path.splitext(fname)[0]
text = f"{variety_name.replace('_', ' ')} fig variety"
texts.append(text)
metadatas.append({"source": fname})
print("📝 벡터 DB에 저장될 텍스트:")
for t in texts:
print(f"- {t}")
# === FAISS 인덱스 생성 ===
vectorstore = FAISS.from_texts(texts, embedding=embedding, metadatas=metadatas)
# === 저장 ===
os.makedirs(DB_PATH, exist_ok=True)
vectorstore.save_local(DB_PATH)
print(f"\n✅ 벡터DB 저장 완료: {DB_PATH}")
# === 저장된 모든 데이터 확인 ===
print("\n🔍 벡터DB에 저장된 모든 단어 확인:")
# FAISS 인덱스에서 모든 문서를 가져오는 직접적인 방법은 없지만,
# 인덱스의 모든 벡터를 검색하여 내용을 확인할 수 있습니다.
# FAISS는 ID를 0부터 순차적으로 부여하므로, index_to_docstore_id를 통해 접근합니다.
ids = list(vectorstore.index_to_docstore_id.values())
retrieved_docs = vectorstore.docstore._dict
for i, doc_id in enumerate(ids):
content = retrieved_docs[doc_id].page_content
print(f" {i+1}. {content} (Source: {retrieved_docs[doc_id].metadata['source']})")
이제 이런 질문들에는 가볍게 오탈자나 약자를 포함해서 품종을 구분할 수 있다.

✅ 품종은 모르고 특성 가지고 질문하는 경우
1. 당도 높은 무화과들 추천좀 → bnr, cddb 등 응답
2. 산도 있는 무화과는?
3. 잎이 예쁜, 인테리어 효과가 좋은 무화과 품종은?
4. 제주도 돌담에서 월동 가능한 무화과는?
이런 경우에 대처하기 위해서는 더 정확한 쿼리를 위해서 무화과의 특성에 따라 조금 파일을 더 추가해서 임베딩하기로 했다. (이 파일들은 gemini cli가 10초만에 varieties 파일을 순회하더니 아주 야무지게 만들어줬다)

맛.txt
풍부한 단맛 (브라운 슈가 타입): 브런즈윅
달콤한 베리 타입: 씨씨오 네로
강렬한 베리 향과 부드러운 단맛: 콜 드 다마 리마다
진한 단맛과 무화과 향: 하디 시카고
적당한 단맛과 약간의 산미: 호래시
달콤한 베리 향과 시원한 산미: 스트로베리 베르테
Breba_생산.txt
O: 브런즈윅, 씨씨오 네로, 하디 시카고, 호래시, 스트로베리 베르테
X 또는 불명확: 콜 드 다마 리마다
“맛.txt”, “월동.txt”, “잎.txt” 같은 속성 파일들은 속성별 세컨더리 벡터DB로 다루려고 한다. (기존의 varieties를 정리한 벡터 DB와 분리)
우선 저대로 저장하게 되면 너무 다양한 정보가 뭉쳐있다는 단점이 이어지고, 한줄씩 끊어서 저장하기 애매하게 여러 종이 한 줄에 있는 경우도 많아, 아래와 같이 하나의 종 - feature를 문장으로 만들어 features_db에 저장했다.

# === features 벡터DB 생성 ===
import os
from langchain_community.vectorstores import FAISS
from langchain_community.embeddings import HuggingFaceEmbeddings
# === 설정 ===
FEATURES_DIR = "./features" # 특징 파일이 있는 디렉토리
DB_PATH = "./features_db" # 새로운 벡터DB를 저장할 경로
MODEL_NAME = "intfloat/multilingual-e5-base"
# === 임베딩 모델 로드 ===
embedding = HuggingFaceEmbeddings(model_name=MODEL_NAME)
# === 데이터 생성 (특징 파일 기반) ===
texts = []
metadatas = []
print("📝 특징 파일을 파싱하여 문장을 생성합니다...")
# ./result 디렉토리의 모든 .txt 파일을 순회합니다.
for fname in os.listdir(FEATURES_DIR):
if not fname.endswith(".txt"):
continue
feature_category = os.path.splitext(fname)[0].replace('_', ' ')
file_path = os.path.join(FEATURES_DIR, fname)
with open(file_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or ":" not in line:
continue
# "특징 값: 품종1, 품종2, ..." 형식으로 분리합니다.
feature_value, varieties_str = line.split(":", 1)
feature_value = feature_value.strip()
varieties = [v.strip() for v in varieties_str.split(",")]
# 각 품종에 대해 문장을 생성합니다.
for variety in varieties:
if not variety: continue
# 예: "브런즈윅 품종의 나무 수형은(는) 개장형입니다."
text = f'"{variety}" 품종의 "{feature_category}"은(는) "{feature_value}"입니다.'
texts.append(text)
# 메타데이터에는 원본 파일명과 품종명을 저장하여 나중에 참조할 수 있도록 합니다.
metadatas.append({"source_file": fname, "variety": variety})
print(f" - 생성: {text}")
# === FAISS 인덱스 생성 ===
if texts:
vectorstore = FAISS.from_texts(texts, embedding=embedding, metadatas=metadatas)
# === 저장 ===
os.makedirs(DB_PATH, exist_ok=True)
vectorstore.save_local(DB_PATH)
print(f"\n✅ 특징 벡터DB 저장 완료: {DB_PATH}")
# === 저장된 모든 데이터 확인 (옵션) ===
print("\n🔍 특징 벡터DB에 저장된 모든 문장 확인:")
ids = list(vectorstore.index_to_docstore_id.values())
retrieved_docs = vectorstore.docstore._dict
for i, doc_id in enumerate(ids):
content = retrieved_docs[doc_id].page_content
metadata = retrieved_docs[doc_id].metadata
print(f" {i+1}. {content} (Source: {metadata['source_file']}, Variety: {metadata['variety']})")
else:
print("\n❌ 처리할 텍스트가 없어 벡터DB를 생성하지 않았습니다.")


🌏 다음은?
이제 데이터베이스는 우리가 하려고 하는 목표 챗봇에 맞게 구성한 것 같다. 이제는 품종을 물어보면서 정보를 물어보더라도, 특성을 물어보면서 품종 추천을 요청해도 다 대응하는 만능 벡터 DB가 있으니 문제없다!!
다음은 이제 AI Agent를 구성해서 더 원활한 사용자 경험을 제공하도록 해보겠다.

도움이 되었다면, 공감/댓글을 달아주면 깃짱에게 큰 힘이 됩니다!🌟
비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!
'PROJECT > RAG 기반 희귀품종 무화과 챗봇' 카테고리의 다른 글
| [AI/RAG] RAG 기반 희귀품종 무화과 LLM 챗봇 프로젝트(3): AI Agent를 통한 pure-RAG 챗봇 완성! (2) | 2025.10.21 |
|---|---|
| [AI/RAG] RAG 기반 희귀품종 무화과 LLM 챗봇 프로젝트(1): 프로젝트의 시작과 구상 (AI Agent, RAG 적용) (2) | 2025.10.20 |