생각/글쓰기

leptos+postgresql+pgvector 활용해 rag 시스템 구축

3604 2026. 8. 10. 22:25
728x90

Windows 11 PC에서 WSL2(Ubuntu 24.04)를 활용해 Docker 환경으로 Leptos + PostgreSQL(pgvector) RAG 시스템을 구축하는 사전 설치 목록, 설치 위치(경로), 권한 및 전체 프로젝트 파일 가이드

1. 사전 필요한 프로그램 의존성 목록

[Windows 11 (클라이언트 영역)]

설치 위치: Windows 기본 C 드라이브 (C:\Program Files\...)

필요 프로그램:

WSL2 (Windows Subsystem for Linux)

VS Code (Visual Studio Code)

VS Code 확장: WSL (Microsoft제)

[WSL2 Ubuntu 24.04 (서버/컨테이너 구동 영역)]

설치 위치: Linux 루트 파이프라인 (/usr/bin/, /var/lib/docker/ 등)

필요 패키지 및 프로그램:

기본 패키지: curl, git, build-essential, ca-certificates

Docker Engine & Docker Compose: 서버 환경(DB, Rust Leptos) 전체를 용기화(Containerization)하여 실행

2. 단계별 환경 구축 권한 설정 (WSL Ubuntu 24.04)

Step 1: Windows 11에서 WSL2  Ubuntu 24.04 설치

Windows PowerShell을 관리자 권한으로 실행 후 실행합니다.

PowerShell

 # WSL2 및 기본 Ubuntu 설치

wsl --install -d Ubuntu-24.04

설치 후 Ubuntu 계정 생성(예: username) 및 비밀번호를 설정합니다.

Step 2: VS Code 및 WSL 확장 연결

Windows 11에 VS Code 설치.

VS Code 왼쪽 확장 탭(Ctrl+Shift+X)에서 WSL 검색 후 설치.

Step 3: WSL Ubuntu 24.04 패키지 업데이트 및 Docker 설치

WSL 터미널(Ubuntu)을 열고 실행합니다.

Bash

 

# 1. 패키지 목록 업데이트 (sudo 권한 필요)

sudo apt update && sudo apt upgrade -y

 

# 2. 필수 빌드 패키지 설치

sudo apt install -y curl git build-essential ca-certificates gnupg

 

# 3. Docker 공식 설치 스크립트 실행

curl -fsSL https://get.docker.com -o get-docker.sh

sudo sh get-docker.sh

 

# 4. [중요 권한 설정] 현재 사용자($USER)에게 sudo 없이 docker를 실행할 권한 부여

sudo usermod -aG docker $USER

 

# 5. 그룹 변경 사항 현재 세션에 바로 적용

newgrp docker

 

# 6. Docker 서비스 시작 및 동작 확인

sudo service docker start

docker --version

docker compose version

권한 안내:

usermod -aG docker $USER 명령을 적용하면 매번 sudo docker ...를 입력할 필요 없이 일반 사용자 권한으로 컨테이너를 관리할 수 있습니다.

#7. rust 설치

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

---

설치 후 적용 방법 간단한 방법은 2가지 중 하나를 선택

방법 A: 터미널 창을 껐다 다시 켜기 (가장 추천!)

지금 열려 있는 터미널 창을 엑스($\times$) 눌러 닫고, 새로 터미널을 켜세요.

그러면 자동으로 Rust 도구 위치를 인식합니다.

방법 B: 명령어 한 줄 입력해서 즉시 반영하기

터미널을 끄기 싫다면, 현재 사용 중인 셸(Shell) 종류에 맞춰 아래 명령어를 복사해서 붙여넣고 엔터를 치면 됩니다.

일반적인 Linux / Mac (Bash, Zsh 등):
Bash

. "$HOME/.cargo/env"

(
맨 앞에 점(.)과 한 칸 띄어쓰기가 있는 것에 주의하세요!)

Fish 
셸을 쓰는 경우:


source "$HOME/.cargo/env.fish"

PowerShell (Windows)
을 쓰는 경우:
PowerShell

. "$HOME/.cargo/env.ps1"

3. 
잘 적용되었는지 확인하려면?

터미널을 다시 켜거나 위 명령어를 입력한 뒤, 아래 명령어를 쳐보세요.

Bash

rustc –version

---

3. 프로젝트 디렉토리 생성 파일 구성

프로젝트 경로

추천 위치: /home/<사용자이름>/projects/my-rag-app

(⚠️ Windows 파일 시스템인 /mnt/c/... 경로에 저장하면 Docker I/O 성능이 매우 떨어지므로 반드시 Linux 홈 디렉토리 내에 생성하세요.)

Bash

 

# 홈 디렉토리에 프로젝트 폴더 생성 및 이동

mkdir -p ~/projects/my-rag-app

cd ~/projects/my-rag-app

전체 프로젝트 구조

Plaintext

 

/home/<사용자이름>/projects/my-rag-app/

├── Cargo.toml          # Rust 패키지 및 의존성 정의
├── Dockerfile          # Leptos 멀티스테이지 빌드 정의
├── docker-compose.yml  # PostgreSQL+pgvector 및 Leptos 컨테이너 오케스트레이션
├── init.sql            # DB 초기화 스크립트 (pgvector 활성화 & 테이블 생성)
├── .env                # 환경 변수 (DB URL, OpenAI API Key)
├── .gitignore          # 
├── public               # 정적 파비콘이나 이미지 파일 등을 담을 public 폴더
├── style                 # css 파일 등 
└── src/
    ├── main.rs         # Axum 백엔드 진입점
    ├── app.rs          # Leptos UI & RAG Server Function
    └── lib.rs          # Leptos UI & RAG Server Function

파일 1: Cargo.toml

[package]
name = "my-rag-app"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]
path = "src/lib.rs"

[[bin]]
name = "my-rag-app"
path = "src/main.rs"

[dependencies]
leptos = { version = "0.7" }
leptos_axum = { version = "0.7", optional = true }
axum = { version = "0.7", optional = true }
tokio = { version = "1", optional = true }

serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
console_error_panic_hook = "0.1"
wasm-bindgen = "=0.2.99"

# reqwest: default-features
를 끄고 rustls-tls 사용
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"], optional = true }
# sqlx: runtime-tokio-rustls
사용
sqlx = { version = "0.8.3",default-features = false, features = ["runtime-tokio-rustls", "postgres", "macros"], optional = true }
pgvector = { version = "0.4.0", features = ["sqlx"], optional = true }
dotenvy = { version = "0.15", optional = true }

[features]
👇 1. default feature에 ssr을 기본 포함해 줍니다 (bin 단독 컴파일 시 필수!)
default = ["ssr"]

👇 2. ssr feature 활성화 시 optional 패키지들이 켜지도록 바인딩
ssr = [
  "leptos/ssr",
  "dep:leptos_axum",
  "dep:axum",
  "dep:tokio",
  "tokio/full",
  "dep:reqwest",
  "dep:sqlx",
  "dep:pgvector",
  "dep:dotenvy",
]
hydrate = [
  "leptos/hydrate",
]

[package.metadata.leptos]
output-name = "my-rag-app"
site-root = "target/site"
site-pkg-dir = "pkg"
assets-dir = "public"
style-file = "style/main.scss"
site-addr = "0.0.0.0:3000"
reload-port = 3001
browser-query = "defaults"
embed-conf = false

👇 3. leptos 빌드 도구가 바이너리를 컴파일할 때 'ssr' feature를 활성화하도록 지정
bin-package = "my-rag-app"
bin-features = ["ssr"]
lib-features = ["hydrate"]

 

파일 2: .env

# PostgreSQL 연결 정보
DATABASE_URL=postgres://postgres:postgres@localhost:5432/ragdb

# 로컬 LLM 서버 URL (Windows에서 WSL2 접근)
LLM_SERVER_URL=http://localhost:8080
EMBED_SERVER_URL=http://localhost:8081

# Rust 환경 설정
RUST_LOG=info

파일 2: .gitignore

# Rust build artifacts
/target/
**/*.rs.bk
*.pdb

# IDE files
.idea/
.vscode/
*.swp
*.swo
*~

# OS generated files
.DS_Store
Thumbs.db

# Docker related
.docker/
*.pid

# Environment files
.env
.env.local
.env.*.local

# Logs
*.log

# Temporary files
tmp/
temp/

# Build output
/build/
/dist/

# Cargo lock (optional - commit this if you're building a binary)
# /Cargo.lock

 

파일 3: init.sql

SQL 

-- pgvector 확장 활성화

CREATE EXTENSION IF NOT EXISTS vector; 

-- 
문서 저장 테이블
CREATE TABLE IF NOT EXISTS documents (
    id BIGSERIAL PRIMARY KEY,
    content TEXT NOT NULL,
    embedding VECTOR(1536) NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW()
); 

-- Cosine Distance HNSW 
인덱스 생성
CREATE INDEX IF NOT EXISTS docs_embedding_hnsw_idx
ON documents USING hnsw (embedding vector_cosine_ops);

파일 4: src/main.rs

mkdir -p src 명령어 실행 후 작성합니다.

Rust

use axum::Router;
use leptos::prelude::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use my_rag_app::app::*;

#[tokio::main]
async fn main() {
    let conf = get_configuration(None).unwrap();
    let leptos_options = conf.leptos_options; 
    let addr = leptos_options.site_addr;
    let routes = generate_route_list(App);

    let app = Router::new()
        .leptos_routes(&leptos_options, routes, {
            let opts = leptos_options.clone();
            move || shell(opts.clone())
        })
        .fallback(leptos_axum::file_and_error_handler(shell))
        .with_state(leptos_options); 

    // 올바른 방법: axum::serve + into_make_service()
    let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
    println!("Server running on http://{}", addr);
    axum::serve(listener, app.into_make_service()).await.unwrap();
}

파일 5: src/app.rs

Rust

use leptos::prelude::*;
#use serde::{Deserialize, Serialize}; 

pub fn shell(options: LeptosOptions) -> impl IntoView {
    view! {
        <!DOCTYPE html>
        <html lang="ko">
            <head>
                <meta charset="utf-8"/>
                <meta name="viewport" content="width=device-width, initial-scale=1"/>
                <AutoReload options=options.clone() />
                <HydrationScripts options=options />
            </head>
            <body>
                <App/>
            </body>
        </html>
    }
}

// Leptos Server Function: 질문 시 백엔드에서 벡터 검색 및 LLM 답변 생성
#[server(AskRag, "/api")]
pub async fn ask_rag(question: String) -> Result<String, ServerFnError> {
    use pgvector::Vector;
    use sqlx::PgPool;
    use std::env;

    dotenvy::dotenv().ok();
    let db_url = env::var("DATABASE_URL").map_err(|_| ServerFnError::new("DATABASE_URL missing"))?;
    let openai_key = env::var("OPENAI_API_KEY").map_err(|_| ServerFnError::new("OPENAI_API_KEY missing"))?;

    let pool = PgPool::connect(&db_url).await.map_err(|e| ServerFnError::new(e.to_string()))?;
    let client = reqwest::Client::new();

    // 1. 질문을 1536차원 벡터로 변환
    let embed_res = client
        .post("https://api.openai.com/v1/embeddings")
        .bearer_auth(&openai_key)
        .json(&serde_json::json!({
            "model": "text-embedding-3-small",
            "input": question
        }))
        .send()
        .await
        .map_err(|e| ServerFnError::new(e.to_string()))?;

    let embed_json: serde_json::Value = embed_res.json().await.map_err(|e| ServerFnError::new(e.to_string()))?;
    let vec_data: Vec<f32> = serde_json::from_value(embed_json["data"][0]["embedding"].clone())
        .map_err(|e| ServerFnError::new(e.to_string()))?;

    let query_vector = Vector::from(vec_data);

// 2. pgvector Cosine similarity 검색 (<=> 연산자)

   // 1. Vec<f32>를 PostgreSQL vector 포맷 문자열인 "[0.1, 0.2, ...]" 형태로 변환합니다.
    let vec_str = format!(
        "[{}]",
        query_vector
             .to_vec()
             .iter()
             .map(|f| f.to_string())
             .collect::<Vec<String>>()
             .join(",")
    );

    let docs: Vec<String> = sqlx::query_scalar(
        r#"
        SELECT content
        FROM documents
        ORDER BY embedding <-> $1::vector
        LIMIT 3
        "#
    )
    .bind(vec_str) // &Vector_query 로 바인딩
    .fetch_all(&pool)
    .await
    .map_err(|e| ServerFnError::new(e.to_string()))?;

    // docs가 Vec<String>이므로 바로 .as_str() 또는 .join() 사용
    // 또는 간단하게: let context = docs.join("\n---\n");
    let context = docs.iter().map(|d| d.as_str()).collect::<Vec<&str>>().join("\n---\n");

    // 3. Prompt 구성 및 OpenAI Chat Completions 호출
    let prompt = format!(
        "다음 문맥을 참조하여 질문에 답변하세요.\n\n[문맥]\n{}\n\n[질문]\n{}",
        if context.is_empty() { "관련 검색 데이터 없음" } else { &context },
        question
    );

    let llm_res = client
        .post("https://api.openai.com/v1/chat/completions")
        .bearer_auth(&openai_key)
        .json(&serde_json::json!({
            "model": "gpt-4o-mini",
            "messages": [
                {"role": "system", "content": "너는 제공된 문맥을 기반으로 정확하게 답변하는 도우미이다."},
                {"role": "user", "content": prompt}
            ]
        }))
        .send()
        .await
        .map_err(|e| ServerFnError::new(e.to_string()))?;

    let llm_json: serde_json::Value = llm_res.json().await.map_err(|e| ServerFnError::new(e.to_string()))?;
    let answer = llm_json["choices"][0]["message"]["content"]
        .as_str()
        .unwrap_or("답변을 생성하지 못했습니다.")
        .to_string();

    Ok(answer)
}

#[component]
pub fn App() -> impl IntoView {
    let (question, set_question) = signal(String::new());
    let ask_action = ServerAction::<AskRag>::new();

    let pending = ask_action.pending();
    let value = ask_action.value();

    let on_submit = move |ev: leptos::ev::SubmitEvent| {
        ev.prevent_default();
        if !question.get().trim().is_empty() {
            ask_action.dispatch(AskRag { question: question.get() });
        }
    };

    view! {
        <div style="max-width: 600px; margin: 40px auto; font-family: sans-serif; padding: 20px;">
            <h2>"Leptos + PostgreSQL + pgvector RAG (Docker)"</h2>
            <form on:submit=on_submit style="display: flex; gap: 10px; margin-bottom: 20px;">
                <input
                    type="text"
                    style="flex: 1; padding: 10px; border: 1px solid #ccc; border-radius: 4px;"
                    placeholder="질문을 입력하세요..."
                    prop:value=question
                    on:input=move |e| set_question.set(event_target_value(&e))
                />
                <button type="submit" disabled=move || pending.get() style="padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;">
                    {move || if pending.get() { "검색 중..." } else { "질문하기" }}
                </button>
            </form>

            <div style="background: #f8f9fa; padding: 15px; border-radius: 4px; border: 1px solid #e9ecef; min-height: 100px;">
            {move || match value.get() {
                    None => view! { <p style="color: #6c757d;">{"질문을 입력하세요.".to_string()}</p> }.into_view(),
                    Some(Ok(ans)) => view! { <p style="white-space: pre-wrap;">{ans}</p> }.into_view(),
                    Some(Err(err)) => view! { <p style="color: red;">{format!("오류: {}", err)}</p> }.into_view(),
                }}
            </div>
        </div>
    }
}

 

 

 

파일 5: src/lib.rs

// lib.rs
pub mod app;

#[cfg(feature="hydrate")]
use wasm_bindgen::prelude::*;

#[cfg(feature = "hydrate")]
#[wasm_bindgen]
pub fn hydrate() {
    use crate::app::*;
    console_error_panic_hook::set_once();
    leptos::mount::mount_to_body(App);
}

파일 6: Dockerfile

Dockerfile

 

FROM rust:latest AS builder

RUN apt-get update && apt-get install -y \ pkg-config \ libssl-dev \ curl \ git \ && rm -rf /var/lib/apt/lists/*
RUN rustup target add wasm32-unknown-unknown

RUN curl -sSfL https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz | tar -xzv -C /usr/local/cargo/bin/ && \
    cargo binstall -y cargo-leptos --version 0.2.24

WORKDIR /app
COPY . .
RUN cargo update -p wasm-bindgen --precise 0.2.99
RUN cargo leptos build --release

FROM debian:bookworm-slim

RUN apt-get update && apt-get install -y ca-certificates libssl-dev && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY --from=builder /app/target/release/my-rag-app /app/
COPY --from=builder /app/target/site /app/site
COPY --from=builder /app/.env /app/.env

EXPOSE 3000
ENV LEPTOS_SITE_ADDR="0.0.0.0:3000"
ENV LEPTOS_SITE_ROOT="site"

CMD ["./my-rag-app"]

 

파일 7: docker-compose.yml

YAML

 #version: '3.8'
services:
  db:
    image: pgvector/pgvector:pg16
    container_name: rag_postgres
    restart: always
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: ragdb
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql

  app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: rag_leptos_app
    restart: always
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://raguser:ragpassword@0.0.0.0:5432/ragdb
    depends_on:
      - db

volumes:
  pgdata:

4. 실행 테스트 과정

1) 빌드 및 컨테이너 구동

Bash 

cd ~/projects/my-rag-app

rm -f Cargo.lock

#빌드 시 proc-macro-error2  warning 발생하는 경우: cargo build  또는 cargo update -p proc-macro-error2
# 빌드 시 실패하여 코드 수정한 로컬에서 빠른 타입 체크: cargo check
docker compose build --no-cache
docker compose up -d

(첫 빌드 시 약 3~5분 정도 소요될 수 있습니다.)

2) 테스트용 샘플 문서 임베딩 등록

Bash

# rag_postgres psql : d docker-compose.yml 파일의 container_name: rag_postgres 또는 services 항목에 정의된 서비스명
# postgres: postgre  user

# ragdb: postgre  db
  
docker exec -it rag_postgres psql -U postgres -d ragdb -c

"INSERT INTO documents (content, embedding)
VALUES ('Leptos
 Rust 언어로 작성된 매우 빠른 Full-stack 웹 프레임워크입니다.', array_fill(0.01, ARRAY[1536])::vector);"

3) Windows 11 브라우저에서 웹페이지 접속

Windows 11의 Chrome 또는 Edge 브라우저를 열고 접속합니다. 

 

 

728x90