aiohttp, b64encoding (1)base64.b64encode를 하는 이유
결론부터 말씀드리면, 네, 맞습니다. 현재 llm/scoring.py에서 asyncpg를 직접 사용하여 conn.fetch(...)로 데이터를 가져오는 방식 대신, apps/deps.py에 만들어두신 get_vector_store() 함수를 사용하는 것이 훨씬 더 효율적이고 LangChain의 설계 철학에 부합하는 방법입니다.
현재 방식과 개선된 방식을 비교하며 왜 바꿔야 하는지, 그리고 어떻게 바꿀 수 있는지 설명해 드리겠습니다.
현재 llm/scoring.py의 방식과 문제점
- 동작 방식:
get_score함수가 호출될 때마다announcement_embedding테이블의 모든 청크와 모든 임베딩 벡터를 메모리로 불러옵니다 (SELECT ...).- 불러온 전체 데이터를 기반으로
BM25Okapi모델을 매번 새로 학습시킵니다. HybridScorer는 메모리에 있는 전체 벡터와 쿼리 벡터 간의 코사인 유사도를 처음부터 다시 계산합니다.
- 문제점:
- 비효율적인 메모리 사용: 청크가 수만, 수십만 개로 늘어나면 모든 데이터를 메모리에 올리는 것 자체가 불가능해지거나 시스템에 큰 부담을 줍니다.
- 느린 속도: 매번 BM25 모델을 새로 학습하고, 전체 벡터에 대해 유사도를 계산하는 것은 매우 느립니다.
- DB 인덱스 미활용:
pgvector를 사용하는 가장 큰 이유 중 하나는HNSW나IVFFlat같은 고속 벡터 인덱스를 사용하기 위함입니다. 현재 방식은 이 인덱스를 전혀 활용하지 않고, 단순히 데이터를 저장하는 용도로만 사용하고 있습니다.
개선된 방식: get_vector_store와 Retriever 활용
apps/deps.py의 get_vector_store()는 LangChain의 PGVector 클래스 인스턴스를 반환합니다. 이 객체는 단순한 데이터 저장소가 아니라, 데이터베이스의 벡터 인덱스를 활용하여 효율적인 검색을 수행할 수 있는 강력한 검색기(Retriever) 역할을 합니다.
이 방식을 사용하면 llm/scoring.py의 로직을 다음과 같이 근본적으로 바꿀 수 있습니다.
- 벡터 검색은 DB에 위임: 전체 벡터를 메모리에 올리는 대신,
retriever.ainvoke(query)를 호출합니다. 그러면pgvector가 DB 인덱스를 사용해 쿼리와 가장 유사한 상위 K개의 후보 문서만 빠르게 찾아 반환합니다.
- BM25는 후보군에만 적용: 전체 코퍼스가 아닌, 벡터 검색으로 추려진 소수의 후보 문서들(예: 상위 100개)에 대해서만 BM25 점수를 계산합니다.
- 하이브리드 점수 계산: 이 후보 문서들에 대해서만 하이브리드 점수를 계산하고, 재조정(re-ranking)합니다.
이 접근법을 **"Two-Stage Retrieval" (2단계 검색)**이라고 부르며, 효율성과 정확성을 모두 잡는 표준적인 방법입니다.
코드 수정 제안 (llm/scoring.py)
기존 get_score 함수를 2단계 검색 방식으로 리팩토링하는 것을 제안합니다.
안녕하세요! sqlalchemy.exc.MissingGreenlet 오류가 발생하셨군요. 이 오류는 SQLAlchemy가 asyncpg와 같은 비동기 데이터베이스 드라이버를 사용할 때, 동기적인 코드 경로에서 비동기 I/O 작업을 수행하려고 시도할 때 발생하는 문제입니다.
오류의 흐름을 따라가 보면 원인을 명확히 알 수 있습니다.
오류의 원인
llm/scoring.py의get_score함수에서await get_vector_store()를 호출합니다.
apps/deps.py의get_vector_store함수는PGVector(...)를 호출하여PGVector객체를 생성합니다.
langchain-postgres의PGVector클래스 생성자(__init__)는 동기(synchronous) 함수입니다.
- 이 동기 생성자 내부에서, 데이터베이스에
vector확장 기능이 설치되어 있는지 확인하고 없으면 생성하기 위해create_vector_extension()이라는 메서드를 호출합니다. 이 과정에서 **데이터베이스 연결(I/O)**이 발생합니다.
- 문제는
asyncpg드라이버가 비동기 전용이므로, 동기 함수 내에서 DB 연결을 시도하면 SQLAlchemy는greenlet이라는 라이브러리를 사용하여 이 비동기 작업을 "동기처럼" 처리하려고 합니다.
- 오류 메시지
greenlet_spawn has not been called는greenlet라이브러리가 설치되어 있지 않거나, 설치되었더라도 현재 컨텍스트에서 사용할 수 없음을 의미합니다.
결론적으로, PGVector의 동기적인 생성자 안에서 비동기 I/O가 발생하면서 greenlet이 필요하게 되었고, 이 의존성이 충족되지 않아 오류가 발생한 것입니다.
해결 방법
이 문제를 해결하는 가장 확실한 방법은 SQLAlchemy의 비동기 지원에 필수적인 greenlet 라이브러리를 설치하는 것입니다.
1단계: requirements.txt 파일에 greenlet 추가
requirements.txt 파일에 greenlet을 추가하여, pip가 이 패키지를 설치하도록 명시해야 합니다.
2단계: 패키지 재설치
터미널에서 아래 명령어를 실행하여 greenlet을 포함한 패키지들을 다시 설치합니다.
이 간단한 수정으로 SQLAlchemy가 asyncpg 드라이버와 함께 비동기 작업을 올바르게 처리할 수 있게 되어, PGVector 객체가 정상적으로 생성되고 오류가 해결될 것입니다.
오류의 근본 원인
apps/deps.py의get_vector_store함수에서PGVector객체를 생성할 때,connection인자로postgresql+psycopg://...와 같은 **연결 문자열(string)**을 전달했습니다.
langchain-postgres는 연결 문자열을 받으면, 기본적으로 동기(synchronous)용 SQLAlchemy 엔진을 생성합니다.
- 이후
llm/scoring.py에서retriever.ainvoke(user_query)와 같은 비동기(asynchronous) 함수를 호출합니다.
- 이 비동기 함수는 내부적으로 비동기 작업을 위한 **비동기 엔진(
_async_engine)**을 찾으려고 시도합니다.
- 하지만
PGVector객체는 동기 엔진만 가지고 있고 비동기 엔진은 생성되지 않았기 때문에,AssertionError: _async_engine not found오류가 발생하며 프로그램이 중단됩니다.
해결 방법: 비동기 엔진(AsyncEngine)을 직접 생성하여 전달
이 문제를 해결하는 가장 확실한 방법은, PGVector에 연결 문자열을 직접 전달하는 대신, SQLAlchemy의 create_async_engine을 사용하여 비동기 엔진을 명시적으로 만들어 전달하는 것입니다.
아래와 같이 c:\Users\ashd\OneDrive - UOS\바탕 화면\notice_rag\apps\deps.py 파일을 수정하여 이 문제를 해결할 수 있습니다.
수정 내용 설명
create_async_engine임포트: SQLAlchemy에서 비동기 엔진을 생성하는 함수를 가져옵니다.
asyncio.to_thread제거: 더 이상 동기 함수를 별도 스레드에서 실행할 필요가 없으므로asyncio.to_thread호출을 제거했습니다.
- 비동기 엔진 생성:
create_async_engine(connection_string)을 호출하여psycopg드라이버를 사용하는 비동기 데이터베이스 엔진을 명시적으로 생성합니다.
PGVector.from_engine사용:PGVector를 직접 생성하는 대신,PGVector.from_engine()클래스 메서드를 사용합니다. 이 메서드는 비동기 엔진을 인자로 받아, 비동기 작업에 완벽하게 호환되는PGVector인스턴스를 생성합니다.
이 수정으로 PGVector 객체는 처음부터 비동기 엔진을 갖게 되어, asimilarity_search와 같은 비동기 함수를 호출할 때 _async_engine을 정상적으로 찾을 수 있습니다. 따라서 AssertionError가 해결되고 코드가 원활하게 실행될 것입니다.