leptos+postgresql+pgvector 활용해 rag 시스템 구축
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 브라우저를 열고 접속합니다.