[LLM] Semantic Search를 위한 Langchain Custom Retriever 구현하기

2024. 9. 5. 23:27DL

안녕하세요 오늘은 Langchain에서 Custom Retriever를 구현하는 방법을 소개해볼까 합니다. 

 

가끔 이런 경우가 생깁니다.

1. 아 뭔가 Retriever에서 데이터가 출력되기 전에 ReRank를 사용하고 나온다면 어떨가?

2. 어? 뭐야 이건 Retriever가 없네? Langchain_community 라이브러리를 뒤져봐도 내가 원하는 Retriever가 없는경우 (제가 못찾았을 수 있습니다.. 머쓱..)

3. 데이터가 Retriever에서 필터를 가지거나, Similarity Search가 되기전 뭔가 처리를 했으면 좋겠다거나..

 

이런 이유로 기존에 Langchain에서 제공되던 Retriever를 수정하여 사용 할 수 있습니다.

 

1. Base Retriever에서 중요한 매서드들

[API 문서]

https://api.python.langchain.com/en/latest/retrievers/langchain_core.retrievers.BaseRetriever.html

 

위 문서를 보면 기본적으로 Base Retriever가 어떤 구조를 가지고 있는지 알 수 있습니다. 이중에서 우리가 중요하게 생각해야하는 매서드는 아래와 같습니다.

 

1. invoke 매서드와 ainvoke 매서드

 이 매서드는 기본적으로 as_retriever를 사용하게 되어 Retriever의 형태를 갖추면 사용할 수 있습니다. 이 매서드는 여기뿐 아니라 langchain에서 는 Chain이 생성되고도 사용합니다. 주로 어떤 입력값을 받아 LLM또는 Retriever의 값을 Return 해줍니다.

 

2. get_relevant_documents 매서드와 aget_relevant_documents 매서드

 이 매서드들은 invoke가 실행될 때 실제로 이 매서드가 실행 됩니다. 이 매서드는 인자 값을 살펴 보면 주로 Query, tag, **kwargs 등을 받습니다. 이 값들은 검색에 필요한 쿼리와 Search option을 제공하기 위함입니다.

 

사실 이 두개만 알면 어느정도 소화가 가능합니다. ((왜냐하면 오늘은 조회를 목적으로한 Custom Retriever를 소개할 예정입니다. 만약 문서 추가까지 필요한 상황이라면, add_document 매서드 까지 손봐야합니다.))

 

좋습니다. 이제 본격적으로 수정에 돌입해 봅시다.

 

2. Custom Retriever 구현

기본적으로 Class로 구성되며 위에서 언급한 BaseRetriever를 상속받아 Class가 구성됩니다. 그래서 기본적인 골조를 모두 가져온 상태에서 내가 필요한 값을 init에 받고, 실제로 조회가 이루어지는 get_relevant_documents를 수정하여 사용가능한 형태로 가공합니다.

 

이번 예제는 이전시간에 다루었던 OpenSearch의 Lexical Search를 위한 데이터 검색 Class를 만들겠습니다. 

class OpenSearchBM25Retriever(BaseRetriever):
    """OpenSearch retriever that uses BM25."""

    client: Any
    """OpenSearch client."""
    index_name: str
    """Name of the index to use in OpenSearch."""
    search_size: int
    """Search data size to use in OpenSearch."""

    def _get_relevant_documents(
        self, query: str, *, run_manager: CallbackManagerForRetrieverRun
    ) -> List[Document]:
        query_dict = {"query": {"match": {"text": query}}, "size": self.search_size}
        res = self.client.search(index=self.index_name, body=query_dict)

        docs = []
        for r in res["hits"]["hits"]:
            docs.append(Document(page_content=r["_source"]["text"], metadata=r["_source"]["metadata"]))
        return docs

 

코드가 매우 간결하죠? 왜냐하면 1번에서 언급한 내용처럼 add_document에 대한 내용이 전부 빠져있기 때문입니다. 단순히 조회를 위한  Custom은 간단합니다. 만약 Client를 생성하는 방법을 알고 싶다면 이전글을 참고하세요!

https://todaycodeplus.tistory.com/74

 

[LLM] OpenSearch - VectorStore로 써보기

안녕하세요 오늘은 Vector Store로 많이 쓰이는 OpenSearch를 가져와 봤습니다. Elastic Search를 Fork해서 사용하는만큼 Elastic Search와 유사한점도 그리고 다른점도 있습니다. 하지만 오늘은 이 차이점을 설

todaycodeplus.tistory.com

 

 

조금 설명해볼까요? 

1. BaseRetriever를 상속받아 Class OpenSearchBM25Retriever를 열어줍니다. 

2. init인자로 (client, index_name, search_size)를 받아줍니다.

  - 각각 OpenSearch Client, 조회할 index 이름, Score로 정렬된 값에서 상위 몇개를 가져올 것인지 결정하는 값입니다.

3. _get_relevant_documents 매서드를 구현합니다.

  내부에 들어있는 로직은 이전글에서 BM25를 조회할 때 썻던 로직을 그대로 가져다 썼습니다.

4. 이렇게 구성하고 Retriever를 생성하면 BaseRetriver가 가지고 있던 invoke를 사용할 수 있고 이 매서드가 내부에서 사용하는 _get_relevant_documents 매서드는 우리가 작성한 매서드를 사용하게 되어 원하는 로직대로 동작하게 됩니다.

 

3. 마치며

생각보다 너무 간단해서 놀라시진 않았는가 싶습니다.문서를 꼼꼼히 읽어보시면 금방 만드실 수 있는 부분이고 Langchain을 더 잘 사용하는데 도움이 됐으면 좋겠습니다.

 

다음글은 RAG의 LLM기반 성능평가 진행을 해보려고 합니다. 이미 진행하고 있는데, 아직 모듈화가 좀 덜 되어서 공개가좀 어려워서 모듈화를 좀더 마치고, 바로 사용가능한 형태로 해보려고 합니다. Git에 업로드하여 누구나 사용할 수 있도록 Open 하면 좋을것 같아서요

 

다음 글에서 만나요!