본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 14. 08:32

Vector Database에 비용을 지불하는 것을 중단하세요: Postgres에서 AI 검색을 구축하는 방법

요약

본 문서는 RAG(Retrieval-Augmented Generation) 시스템 구축 시, 고가의 전용 벡터 데이터베이스 대신 PostgreSQL의 `pgvector` 확장과 Ruby gem인 `neighbor`를 활용하여 비용 효율적이고 간편하게 AI 검색 기능을 구현하는 방법을 안내합니다. 이 접근 방식은 표준 Rails 스택 내에서 모든 데이터를 관리할 수 있게 하여 개발 복잡성을 크게 줄입니다. 핵심 원리는 텍스트를 고차원 벡터(임베딩)로 변환하여, 사용자의 질문과 가장 유사한 의미를 가진 문서를 데이터베이스에서 '물리적으로' 검색하는 것입니다. 이 과정을 통해 별도의 인프라 구축 없이도 강력한 AI 기반 검색 기능을 확보할 수 있습니다.

핵심 포인트

  • RAG 시스템을 구축하기 위해 Pinecone이나 Milvus 같은 전용 벡터 DB 대신 PostgreSQL과 `pgvector` 확장을 사용할 수 있다. PostgreSQL의 표준 데이터베이스 내에서 모든 데이터를 관리하여 개발 복잡성과 비용을 절감한다. AI 검색은 텍스트를 임베딩(Embedding, 숫자의 배열)으로 변환하고, 이 좌표 공간에서 질문과 가장 가까운 문서를 찾는 방식으로 작동한다. `neighbor` gem을 사용하면 ActiveRecord의 편의성을 유지하면서 벡터 유사도 검색 기능을 쉽게 추가할 수 있다.

저는 개발자들이 자신의 특정 회사 데이터를 알고 있는 "AI Chatbots"를 구축하려고 시도하는 것을 봅니다. 그들은 AI가 자신들의 PDF, 내부 위키(wikis), 또는 과거 고객 지원 티켓을 읽고 해당 데이터를 기반으로 질문에 답변하기를 원합니다. 이 기술을 RAG (Retrieval-Augmented Generation, 검색 증강 생성)라고 부릅니다. AI 열풍이 처음 시작되었을 때, 개발자들은 이를 수행하기 위해 Pinecone이나 Milvus와 같은 비싸고 전용인 "Vector Databases (벡터 데이터베이스)"에 비용을 지불해야 한다고 생각했습니다. 그들은 단지 약간의 AI 데이터를 저장하기 위해 자신들의 스택에 거대한 복잡성 계층을 추가했습니다. 2026년에는, 이를 수행하는 Rails 방식이 훨씬 더 간단합니다. 그냥 PostgreSQL을 사용하면 됩니다. pgvector 확장을 사용하고 neighbor라는 훌륭한 Ruby gem을 사용하면, 모든 AI 데이터를 표준 Rails 데이터베이스 내에 완벽하게 동기화된 상태로 유지할 수 있습니다. ActiveRecord의 편안함을 벗어나지 않고도 RAG의 강력함을 얻을 수 있습니다. 다음은 4단계로 "데이터베이스와 채팅하기"를 구축하는 정확한 방법입니다.

멘탈 모델: 임베딩 (Embeddings)이란 무엇인가? 코드를 작성하기 전에, AI가 텍스트를 어떻게 "검색"하는지 이해해야 합니다. AI는 단어를 읽지 않습니다. 수학을 읽습니다. 텍스트 단락을 AI(OpenAI의 임베딩 모델과 같은)에 보내면, 그것은 임베딩 (Embedding) — 즉, 1,536개의 숫자로 이루어진 거대한 배열을 반환합니다. 이 배열을 지도 위의 좌표 집합이라고 생각하세요. 유사한 내용을 다루는 단락들은 이 지도 위에서 서로 더 가깝게 배치됩니다. 답을 찾기 위해, 우리는 사용자의 질문을 좌표로 변환하고 데이터베이스에 다음과 같이 요청합니다: "지도상에서 이 질문과 물리적으로 가장 가까운 단락은 무엇인가요?"

1단계: 데이터베이스 설정
먼저, PostgreSQL이 이러한 거대한 숫자 배열을 저장할 수 있도록 허용해야 합니다. 이를 위해 vector 확장을 활성화합니다. Gemfile에 gem을 추가하세요:

gem 'ruby-openai' # ChatGPT와 통신하기 위해
gem 'neighbor' # ActiveRecord에 벡터 검색을 추가하기 위해

run bundle install 을 실행하세요. 다음으로, 확장을 활성화하고 검색하려는 테이블(Document 모델을 사용해 보겠습니다)에 vector 컬럼을 추가하는 마이그레이션(migration)을 생성합니다.

rails g migration AddEmbeddingsToDocuments # db/migrate/20260506120000_add_embeddings_to_documents.rb

class AddEmbeddingsToDocuments < ActiveRecord::Migration[8.0]
  def change
    # 1. Postgres 확장(extension) 활성화
    enable_extension "vector"

    # 2. 컬럼 추가. OpenAI의 표준 모델은 1536 차원(dimensions)을 출력합니다.
    add_column :documents, :embedding, :vector, limit: 1536
  end
end

rails db:migrate를 실행합니다.

이제 모델을 열고 neighbor gem이 해당 컬럼을 추적하도록 설정합니다:

# app/models/document.rb
class Document < ApplicationRecord
  has_neighbors :embedding
end

STEP 2: 임베딩(Embeddings) 생성하기

사용자가 앱에서 새로운 Document를 생성할 때, 해당 텍스트를 임베딩(embedding)으로 변환하여 데이터베이스에 저장해야 합니다. (참고: API 호출은 느리기 때문에, 이 작업은 Solid Queue 백그라운드 작업(background job)에서 수행해야 합니다!)

# app/services/embedding_service.rb
class EmbeddingService
  def self.generate(document)
    client = OpenAI::Client.new(access_token: ENV['OPENAI_API_KEY'])
    response = client.embeddings(
      parameters: {
        model: "text-embedding-3-small",
        input: document.content
      }
    )

    # 1536개의 부동 소수점(floats) 배열 추출
    vector = response.dig("data", 0, "embedding")

    # Postgres 컬럼에 직접 저장
    document.update!(embedding: vector)
  end
end

STEP 3: 벡터 검색 (컨텍스트 찾기)

이제 마법을 부릴 차례입니다. 사용자가 다음과 같은 질문을 합니다: "우리 회사의 환불 정책은 무엇인가요?"

먼저, 정확히 동일한 OpenAI 모델을 사용하여 질문을 벡터로 변환해야 합니다. 그런 다음, neighbor gem의 .nearest_neighbors 메서드를 사용하여 Postgres를 검색합니다.

# app/services/rag_search_service.rb
class RagSearchService
  def self.search(question)
    client = OpenAI::Client.new(access_token: ENV['OPENAI_API_KEY'])

    # 1. 질문을 좌표로 변환
    question_vector = client.embeddings(
      parameters: {
        model: "text-embedding-3-small",
        input: question
      }
    ).dig("data", 0, "embedding")

    # 2.

Postgres에게 가장 가까운 문서 3개를 찾도록 요청합니다. # "inner_product"는 OpenAI 임베딩 (embeddings)에 가장 빠른 거리 측정 지표 (distance metric)입니다.

relevant_docs = Document
  .nearest_neighbors(:embedding, question_vector, distance: "inner_product")
  .limit(3)

relevant_docs
end
end

neighbor 젬 (gem) 덕분에, 벡터 검색이 표준 ActiveRecord 쿼리와 완전히 똑같이 느껴집니다!

STEP 4: RAG 프롬프트 (RAG Prompt)
사용자의 질문이 있고, 정답을 포함하고 있는 3개의 문서가 있습니다. 이제 이것들을 하나의 거대한 프롬프트로 합쳐서 ChatGPT에 보내 인간처럼 들리는 응답을 생성하게 합니다.

# app/controllers/chats_controller.rb
class ChatsController < ApplicationController
  def create
    user_question = params[:question]

    # 1. Postgres에서 관련 데이터를 가져옵니다.
    docs = RagSearchService.search(user_question)

    # 2. 컨텍스트 (context) 문자열을 구축합니다.
    context = docs.map(&:content).join(" \n\n --- \n\n ")

    # 3. RAG 프롬프트를 구축합니다.
    system_prompt = <<~ PROMPT
      You are a helpful company assistant. Answer the user's question using ONLY the context provided below. If the answer is not in the context, say "I don't know."

      CONTEXT:
      #{context}
    PROMPT

    # 4. AI에게 요청합니다.
    client = OpenAI::Client.new(access_token: ENV['OPENAI_API_KEY'])
    response = client.chat(parameters: {
      model: "gpt-4o",
      messages: [
        { role: "system", content: system_prompt },
        { role: "user", content: user_question }
      ]
    })

    @answer = response.dig("choices", 0, "message", "content")

    # 여기에 Hotwire 뷰를 렌더링합니다...
  end
end

요약
수십억 달러 규모의 전체 "RAG" 산업은 이 믿을 수 없을 정도로 단순한 파이프라인(pipeline)으로 귀결됩니다:

텍스트 (Text) -> OpenAI -> 숫자 (Numbers, Postgres에 저장됨).
질문 (Question) -> OpenAI -> 숫자 (Numbers).
neighbor를 사용하여 Postgres에서 가장 가까운 숫자를 찾습니다.
질문 + 찾은 텍스트를 보냅니다 -> OpenAI -> 최종 답변 (Final Answer).

pgvector와 ActiveRecord를 활용함으로써, 우리는 스택 (stack)에 완전히 새로운 인프라 조각을 추가하는 것을 피할 수 있습니다.

여러분의 AI 데이터는 사용자 데이터 바로 옆에 존재하며, 함께 백업되고, 여러분이 이미 알고 있고 애정하는 동일한 Ruby 구문을 사용하여 쿼리(query)됩니다. "One Person Framework"가 다시 한번 빛을 발하는 순간입니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
1

댓글

0