multi vector retrieval 로 똑똑한 문서 검색 챗봇 만들기

Photo of author

By mimoofdm@naver.com

multi vector retrieval 을 이용해서 여러 문서에서 키워드와 유사한 단어와 같은 의미를 지닌 문장을 벡터 검색하는 방법에 대해서 자세히 알아보겠습니다. langchain 에서 제공되는 chroma 데이터베이스를 vectore store 를 실제로 구현해 보도록 하겠습니다.

llm 에 대해서 공부를 하면서 기본적인 실습을 진행하면서 langchain 에 대해서 세부적으로 알아야 하겠다는 생각이 들었습니다. 이미지 docker image 로 만들어져서 오류 없이 한번에 동작 시킬 수 있는 챗봇을 만드는 방법을 아래 포스팅에 자세히 소개를 드렸으니 한 번 씩 읽어보시기 바랍니다.

ollama 를 이용하여 llama3 챗봇 쉽게 이용하기

llama3 rag 를 이용해서 나만의 챗봇 따라서 만들기

이전 포스팅에서 사용한 방법들은 Retrieval Augmented Generation 기술을 적용하더라도 간단한 형태로 pdf 문서를 불러와서 문서 안에 있는 단어와 문장 속에서 내가 검색하고자 하는 키워드와 완전히 일치하는 키워드가 담긴 문장을 검색해 내거나 유사한 단어가 담긴 문장을 검색하였습니다.

여러 문서를 동시에 검색하는 챗봇이 왜 필요한가요?

여러 개의 문서를 동시에 검색한다는 말은 무슨 뜻일까요? 사실은 제가 숙제를 해야 하는데 인터넷에서 키워드를 검색해서 나온 결과를 보여주면 안되고 제가 입력한 pdf 문서에 들어 있는 내용 안에서만 검색 결과를 보여줘야 합니다.

학교에서 숙제를 하거나 회사에서 여러 개의 자료를 입력 받아서 이슈가 될만한 내용이나 내가 관심이 있는 내용이 담겨 있는 핵심 문장과 문단을 챗봇이 똑똑하게 찾아주면 좋습니다.

최근에는 라마3 을 주로 이용해서 챗봇을 만들거나 windows 10 에서 이미 만들어져 있는 llama3 docker 를 다운로드 받아서 간편하게 설치해서 llama3 을 이용하는 방법을 실습 해봤습니다.

라마3 을 이용해서 llama3 챗봇을 만들 때에도 rag 기술을 이용했었는데요. 생성형 AI 가 가지고 있는 본질적인 환각 현상을 극복하기 위해서는 어쩔 수 없이 Retrieval Augmented Generation 기술을 사용할 수 밖에 없습니다.

예전 포스팅에서 실습을 할 때 PDF 문서를 1개만 가져올 수 있었는데요. 이번 프로젝트 부터 2개 이상의 문서를 가지고 와서 내가 검색하는 키워드와 완전히 동일한 키워드가 담겨있는 문장을 찾아주거나 의미론 적으로 유사한 뜻을 담고 있는 유사 단어가 들어가 있는 문장을 찾아주도록 개발하려고 합니다.

내가 검색한 키워드와 유사 단어가 포함되어 있지 않더라도 문장 전체의 취지가 유사하면 동일한 문장이라고 판단하여 원래 문장을 가져오고 한발 더 나아가서 요약을 해주는 방법에 대해서 직접 구현해 보도록 하겠습니다.

multi vector retrieval 란 무엇인가?

multi vector retrieval 이란 랭체인 ( LangChain) 에서 벡터 스토어를 여러개 만들어서 검색 키워드와 동일한 단어와 유사한 의미를 지닌 단어와 문장을 찾는 기술을 의미합니다. 벡터 검색을 이용하려면 검색할 키워드를 임베딩하여 벡터로 변환하여야 합니다. 벡터화된 키워드는 단어와 문장을 AI 모델링할 때 사용하기 편하도록 임베딩 이라는 과정을 거쳐서 벡터로 만들어서 저장하는데 이 저장 공간을 벡터 스토어라고 부릅니다.

키워드를 벡터화 시킨 목표 벡터와 유사한 벡터를 찾기 위해서 병렬적으로 벡터 스토어에서 유사도가 가장 높은 벡터들을 찾는 방식입니다.

LangChain 에서 Chroma DB 설치 방법

실습은 구글 콜랩에서 진행하였습니다. LangChain 에서 제공하는 Chroma DB 는 데이터를 입력하여 벡터로 임베딩하여 저장할 때 가장 많이 사용하는 벡터 데이터베이스입니다. 크로마 DB로 생성된 데이터베이스에 액세스하려면 ID 를 이용하여 해당 벡터에서 벡터를 가져올 수 있습니다.

아래의 코드는 LangChain 공식 홈페이지에서 멀티벡터 리트리버를 구현한 예제 코드입니다. LangChain 패키지에서 MultiVectorRetriever 를 가져다 사용하는 것 만으로도 대부분의 기능을 활용할 수 있습니다.

chromadb 도 LangChain 패키지에서 제공된 것을 import 해서 사용하며 텍스트를 로딩하기 위해서 TextLoader 패키지도 가져옵니다. 임베딩은 OpenAIEmbeddings 을 가져와서 사용하기로 합니다. 문장을 더 작은 단위의 청크 ( chunk )로 쪼개기 위해서 RecursiveCharacterTextSplitter 를 임포트해서 사용합니다.

from langchain.retrievers.multi_vector import MultiVectorRetriever

from langchain.storage import InMemoryByteStore
from langchain_chroma import Chroma
from langchain_community.document_loaders import TextLoader
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter


예제로서 paul_graham_essay.txt 문서와 state_of_the_union.txt 텍스트 문서를 동시에 RAG 로 가져와서 임베딩 시켜서 벡터로 만들기 위한 준비를 합니다. RecursiveCharacterTextSplitter( ) 메소드가 사실상 중요한 역할을 합니다. chunk 의 사이즈를 10000 바이트로 설정하여 원래 길었던 텍스트 글을 chunk 크기 만큼 더 작은 글의 집합으로 구분해 줍니다.

loaders = [
    TextLoader("../../paul_graham_essay.txt"),
    TextLoader("../../state_of_the_union.txt"),
]
docs = []
for loader in loaders:
    docs.extend(loader.load())
text_splitter = RecursiveCharacterTextSplitter(chunk_size=10000)
docs = text_splitter.split_documents(docs)

chromadb 를 생성할 때 임베딩 시키는 방식을 OpenAIEmbeddings( ) 을 이용해서 OpenAI 사에서 정의한 임베딩 룰을 적용합니다. MultiVectorRetriever( )를 생성할 때 문서의 ID와 생성된 vectorstore 를 입력하여 생성합니다.


vectorstore = Chroma(
    collection_name="full_documents", embedding_function=OpenAIEmbeddings()
)
# The storage layer for the parent documents
store = InMemoryByteStore()
id_key = "doc_id"
# The retriever (empty to start)
retriever = MultiVectorRetriever(
    vectorstore=vectorstore,
    byte_store=store,
    id_key=id_key,
)


import uuid

doc_ids = [str(uuid.uuid4()) for _ in docs]



# The splitter to use to create smaller chunks
child_text_splitter = RecursiveCharacterTextSplitter(chunk_size=400)



sub_docs = []
for i, doc in enumerate(docs):
    _id = doc_ids[i]
    _sub_docs = child_text_splitter.split_documents([doc])
    for _doc in _sub_docs:
        _doc.metadata[id_key] = _id
    sub_docs.extend(_sub_docs)


retriever.vectorstore.add_documents(sub_docs)
retriever.docstore.mset(list(zip(doc_ids, docs)))





아래 코드가 가장 핵심적인 부분입니다. 유사도를 바탕으로 “justice breyer” 벡터 검색을 한 후에 가장 유사도가 높은 chunk 를 VectorStore에서 찾아주는 것입니다.


retriever.vectorstore.similarity_search("justice breyer")[0]

“justice breyer”와 유사도가 가장 높은 chunk 를 찾은 후에 chunk 에 담겨 있는 컨텐츠의 길이를 측정해서 표시해줍니다.


# Retriever returns larger chunks
len(retriever.invoke("justice breyer")[0].page_content)



from langchain.retrievers.multi_vector import SearchType

retriever.search_type = SearchType.mmr

len(retriever.invoke("justice breyer")[0].page_content)

마무리

multi vector retrieval 을 이용해서 여러 개의 텍스트에서 검색하고자 하는 키워드와 가장 유사한 컨텐츠를 찾는 벡터 검색 기술을 직접 구현해 보았습니다. 다음 포스팅에서는 가장 기본적인 벡터 검색 방식으로서 벡터 검색 정확도를 더 높이기 위해서는 chunk 를 나눌 때 Parent Chunk 를 더 작은 사이즈인 여러 개인 Child Chunk 로 나누고 유사도 벡터 검색을 하여 더 정확하게 검색하는 방법에 대해서 다루도록 하겠습니다.