THE DEVLOG

scribbly.

Next.js 블로그 프로젝트

2025.05.17 07:51:57

16, 17일차에 구현했던 텍스트 임베딩과 코사인 유사도에 관한 내용을 정리한 글.

본래 인공지능 게시글 추천 기능에 대해 고민하였는데, 게시글 120개를 chatGPT가 모두 확인하고 처리하는 것이 불가능하였다.(회당 토큰 수 초과)

이에 따라 가장 간단하고 확실한 방법인 코사인 유사도를 통한 게시글 추천 기능을 구현하였다.

텍스트 임베딩이란?

텍스트 임베딩이란 문장이나 단어를 고차원 벡터 공간의 좌표로 변환하는 것.
예를 들어, "나는 밥을 먹었어"[0.12, -0.98, 0.45, ..., 0.33]
(보통 384, 768, 1536차원 등)

핵심 요점

  • 같은 의미를 가진 문장은 비슷한 벡터로 표현된다.
  • 임베딩 벡터끼리의 거리각도를 이용해 유사도를 계산할 수 있다.
  • 텍스트의 벡터화에는 OpenAI text-embedding-3-small를 활용하였다. 가격은 text-embedding-2보다 저렴하고, 차원은 더 넓게 사용하는 경향이 있다고 알려져 있다.

코사인 유사도

벡터끼리 얼마나 방향이 비슷한지를 계산하는 방식이다.

1.00

  • 1에 가까울수록 → 완전히 같은 의미
  • 0에 가까울수록 → 전혀 관련 없음
  • (−1은 반대 의미, 일반적으로는 거의 사용되지 않음)

예시

문자를 벡터로 변환하였다면 아래와 같이 비교하게 된다.

문장 A문장 B유사도
"나는 점심을 먹었다""밥을 먹었어"0.94
"나는 점심을 먹었다""운동을 했다"0.21

 

인공지능 요약 및 코사인 유사도 구현

인공지능 요약 버튼

main/components/post/right-panel/ai-panel.tsx
 

요약 생성 및 코사인 유사도 분석은 아래와 같은 과정으로 이루어진다.

  1. 인공지능 요약 생성 버튼을 누르면 api/summary API를 POST로 호출한다.
    1. chatgpt-4o-latest 모델로 요약을 생성한다.
    2. 생성된 요약을 text-embedding-3-small 모델로 벡터화한다.
  2. 요약과 벡터를 각각 sumaries, summary_vectors 테이블에 저장한다.
  3. 이어서 api/summary/recommended를 호출한다.
    1. DB에서 summary vector를 불러온다.
    2. target과 다른 게시글들 사이의 코사인 유사도를 분석한다.
    3. 코사인 유사도가 높은 10개의 게시글을 post_similarities 테이블에 저장한다.
      1. post_similarities는 source가 target보다 사전 순으로 앞으로 오고, 두 쌍이 유니크 하도록 제약 조건을 걸어두었다.
        ALTER TABLE post_similarities
        ADD CONSTRAINT enforce_sorted_pair
        CHECK (source_post_id < target_post_id);
        
        CREATE UNIQUE INDEX unique_similarity_pair
        ON post_similarities (source_post_id, target_post_id);
        
      2. 따라서 코사인 유사도 분석한 결과를 sort로 정렬하고, upsert를 통해 데이터에 저장한다.
          // 결과 포맷 변환
          const res = top10.map((item) => {
            const [a, b] = [sourceData.post_id, item.target_post_id].sort(); // 단방향 제약 추가
            return {
              source_post_id: a,
              target_post_id: b,
              similarity: item.similarity,
            };
          });
        
            const { data } = await supabase
              .from("post_similarities")
              .upsert(sims, {
                onConflict: "source_post_id, target_post_id",
              })
              .select();
        
      3. 요청이 완료되면 AiSummary 패널의 데이터를 revalidate하고 toast를 띄운다.