📝 업무 내용

시작일 종료 예정일(목표) 실제 종료일(완료)
2026-01-21 2026-01-22 2026-01-23

1. 진행 상태

  • [x] 인프라 설정: PostgreSQL pgvector 확장 활성화
CREATE EXTENSION vector;
  • [x] 임베딩 모델 준비: Ollama에 bge-m3 모델 풀다운 및 서빙 확인
  • [x] 데이터 스키마 확장: Knowledge 모델에 1024차원 embedding 벡터 필드 추가
  • [x] 임베딩 파이프라인:
    • [x] OllamaClientembed_text 비동기 메서드 추가
    • [x] 게시글 저장/수정 시 요약과 함께 임베딩 값을 생성하여 저장
  • [x] 검색 엔진 교체: 1024차원 코사인 유사도 기반 검색 로직 구현
  • [x] 하이브리드 검색 구현: 키워드 검색 점수(BM25)와 벡터 검색 점수를 합산하여 최적의 결과를 내는 RRF(Reciprocal Rank Fusion) 알고리즘 적용.

2. 주요 설정

  • postgreSQL Extentions: pgvector를 활용한 vector DB 사용
  • 비동기 방식 통신 설정: async-await, sync_to_async, asyncio.gather 를 이용한 비동기 방식 처리
  • 의미기반검색 & RRF 사용: 하이브리드 검색 방식 선택, 장애 발생시 RRF 방식으로 진행
  • CosineDistance: 코사인 유사도 거리를 이용한 방식으로 일반적인 거리만 비교하는게 아닌 검색어가 가르키는 방향도 포함하여 계산
🌳 하위 업무 (Sub-tasks) 추가
등록된 하위 업무가 없습니다.
📦 Reference Keeper
[Sample] Django Model Field Reference
https://docs.djangoproject.com/en/5.1/ref/models/fields/
💻 기술 명세
# 샘플 파일 경로
/usr/tmp/sample1.jsp\n/src/main/config/settings_sample.py
async def list_documents(filters):
    """
    Full-Text Search(FTS)와 Vector Search를 결합한 하이브리드 검색 로직
    """
    # 1. N+1 문제 방지를 위한 Eager Loading 및 임베딩 필드 지연 로딩
    qs = Document.objects.select_related('author').prefetch_related('tags').defer('embedding').all()

    if filters.q:
        try :
            # AI 모델을 통한 키워드 벡터화 (병렬 처리를 위해 태스크 생성)
            query_vector_task = ai_client.fetch_embedding(filters.q)

            # 2. FTS(Full-Text Search) 쿼리 정의
            search_query = SearchQuery(filters.q)
            fts_qs = qs.annotate(
                rank=SearchRank('search_vector', search_query)
            ).filter(search_vector=search_query).order_by('-rank')[:50]

            # 3. 벡터 추출 대기 및 벡터 검색 수행
            query_vector = await query_vector_task

            if query_vector:
                vector_qs = qs.annotate(
                    distance = CosineDistance('embedding', query_vector)
                ).order_by('distance')[:50]

                # DB 쿼리를 비동기로 병렬 실행하여 레이턴시 최적화
                v_task = sync_to_async(list)(vector_qs)
                f_task = sync_to_async(list)(fts_qs)
                v_results, f_results = await asyncio.gather(v_task, f_task)
                
                # 4. RRF(Reciprocal Rank Fusion) 적용하여 순위 재조합
                final_ids = apply_rrf_algorithm(v_results, f_results)

                if final_ids:
                    # 결과 ID 순서를 보존하며 최종 QuerySet 반환
                    preserved_order = Case(
                        *[When(id=pk, then=Value(pos)) for pos, pk in enumerate(final_ids)],
                        output_field=IntegerField()
                    )
                    qs = Document.objects.filter(id__in=final_ids).order_by(preserved_order)
                else:
                    qs = Document.objects.none()
            else:
                qs = fts_qs

        except Exception as e:
            # 보안을 위해 상세 에러는 내부 로그에만 기록
            logger.error(f"Search filtering failed: {str(e)}")
            # 장애 발생 시 키워드 기반 검색으로 안정적인 Fallback
            qs = qs.annotate(rank=SearchRank('search_vector', SearchQuery(filters.q))).filter(search_vector=SearchQuery(filters.q)).order_by('-rank')
    else:
        qs = qs.order_by('-created_at')

    # 필터링 조건 적용 (유형, 태그 등)
    if filters.type: qs = qs.filter(type=filters.type)
    if filters.tags: qs = qs.filter(tags__name__in=filters.tags).distinct()
        
    return await sync_to_async(list)(qs)
-- 샘플 Query
SELECT * FROM dual;