Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/http/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub mod handler;
use api::VectorDb;
use axum::{
Router,
extract::DefaultBodyLimit,
routing::{get, post},
};
use defs::BoxError;
Expand Down Expand Up @@ -36,6 +37,7 @@ pub fn create_router(db: Arc<VectorDb>) -> Router {
.route("/points/batch", post(batch_insert_handler))
.route("/points/search/batch", post(batch_search_handler))
.with_state(app_state)
.layer(DefaultBodyLimit::max(50 * 1024 * 1024)) // 50MB limit
}

/// Runs the HTTP server on the specified address.
Expand Down
8 changes: 8 additions & 0 deletions demo/document-rag/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
OPENAI_API_KEY=sk-your-api-key-here
VORTEXDB_HOST=vortexdb
VORTEXDB_PORT=3034
EMBEDDING_MODEL=text-embedding-3-small
LLM_MODEL=gpt-4o-mini
CHUNK_SIZE=512
CHUNK_OVERLAP=50
TOP_K=5
83 changes: 83 additions & 0 deletions demo/document-rag/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# VectorDB RAG Demo

A fully containerized Document RAG demo with a dark-themed web UI. Upload documents, chat with your knowledge base.

## Quick Start

```bash
# 1. Fill in your API key
cp .env.example .env
# Edit .env and set OPENAI_API_KEY=sk-your-key-here

# 2. Build and start everything
docker compose up -d --build

# 3. Open browser
open http://localhost:3035
```

That's it! No other setup required.

## Features

- **File Upload** - Drag & drop or browse documents (PDF, TXT, MD, DOCX, CSV)
- **Chat Interface** - Ask questions, get AI-powered answers
- **Fully Containerized** - VortexDB + Backend + Frontend in Docker

## Architecture

```
Browser (localhost:3035) → Frontend (nginx)
Backend API (port 8000)
┌───────────────┴───────────────┐
↓ ↓
OpenAI API VortexDB
(embeddings + LLM) (HTTP port 3000)
```

## Configuration

### .env file

```env
OPENAI_API_KEY=sk-your-api-key-here
VORTEXDB_HOST=vortexdb
VORTEXDB_PORT=3000
EMBEDDING_MODEL=text-embedding-3-small
LLM_MODEL=gpt-4o-mini
CHUNK_SIZE=512
CHUNK_OVERLAP=50
TOP_K=5
```

## Project Structure

```
demo/document-rag/
├── docker-compose.yml
├── .env.example
├── README.md
├── backend/
│ ├── src/
│ │ ├── main.py # FastAPI app
│ │ ├── config.py # Config from env
│ │ ├── chunker.py # Text chunking
│ │ ├── embedder.py # OpenAI embeddings
│ │ ├── generator.py # Chat completion
│ │ ├── extractor.py # Document parsing
│ │ └── vectorstore.py # VortexDB HTTP client
│ ├── requirements.txt
│ └── Dockerfile
└── frontend/
├── src/
│ ├── App.jsx # Main React component
│ ├── App.css # Styles
│ └── main.jsx # Entry point
├── index.html
├── package.json
├── vite.config.js
├── nginx.conf
└── Dockerfile
```
16 changes: 16 additions & 0 deletions demo/document-rag/backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY src/ ./src/

RUN mkdir -p /app/uploads

ENV PYTHONPATH=/app

EXPOSE 8000

CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
9 changes: 9 additions & 0 deletions demo/document-rag/backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
fastapi==0.109.2
uvicorn[standard]==0.27.1
python-multipart==0.0.9
openai==1.12.0
httpx==0.27.0
pypdf2==3.0.1
python-docx==1.1.0
pydantic==2.6.1
python-dotenv==1.0.1
34 changes: 34 additions & 0 deletions demo/document-rag/backend/src/chunker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import re
from typing import List


def chunk_text(text: str, chunk_size: int = 512, chunk_overlap: int = 50) -> List[str]:
"""
Split text into overlapping chunks.
"""
if not text or not text.strip():
return []

text = re.sub(r'\s+', ' ', text).strip()

chunks = []
start = 0
text_len = len(text)

while start < text_len:
end = start + chunk_size
chunk = text[start:end]

if end < text_len:
last_period = chunk.rfind('. ')
last_newline = chunk.rfind('\n')
split_pos = max(last_period, last_newline)

if split_pos > chunk_size // 2:
chunk = chunk[:split_pos + 1]
end = start + split_pos + 1

chunks.append(chunk.strip())
start = end - chunk_overlap if end < text_len else text_len

return [c for c in chunks if c]
15 changes: 15 additions & 0 deletions demo/document-rag/backend/src/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import os
from dotenv import load_dotenv

load_dotenv()

class Config:
OPENAI_API_KEY: str = os.getenv("OPENAI_API_KEY", "")
VORTEXDB_HOST: str = os.getenv("VORTEXDB_HOST", "localhost")
VORTEXDB_PORT: int = int(os.getenv("VORTEXDB_PORT", "3034"))
EMBEDDING_MODEL: str = os.getenv("EMBEDDING_MODEL", "text-embedding-3-small")
LLM_MODEL: str = os.getenv("LLM_MODEL", "gpt-4o-mini")
CHUNK_SIZE: int = int(os.getenv("CHUNK_SIZE", "512"))
CHUNK_OVERLAP: int = int(os.getenv("CHUNK_OVERLAP", "50"))
TOP_K: int = int(os.getenv("TOP_K", "5"))
VECTOR_SIZE: int = 1536
27 changes: 27 additions & 0 deletions demo/document-rag/backend/src/embedder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import openai
from openai import OpenAI
from typing import List
from src.config import Config


class Embedder:
def __init__(self, api_key: str):
self.client = OpenAI(api_key=api_key)
self.model = Config.EMBEDDING_MODEL

def embed(self, texts: List[str]) -> List[List[float]]:
"""Generate embeddings for a list of texts."""
if not texts:
return []

response = self.client.embeddings.create(
model=self.model,
input=texts
)

return [item.embedding for item in response.data]

def embed_single(self, text: str) -> List[float]:
"""Generate embedding for a single text."""
embeddings = self.embed([text])
return embeddings[0] if embeddings else []
55 changes: 55 additions & 0 deletions demo/document-rag/backend/src/extractor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from pathlib import Path
from PyPDF2 import PdfReader
import docx


SUPPORTED_EXTENSIONS = {'.txt', '.md', '.pdf', '.docx', '.csv'}


def extract_text(file_path: str) -> str:
path = Path(file_path)
ext = path.suffix.lower()

if ext not in SUPPORTED_EXTENSIONS:
raise ValueError(f"Unsupported format: {ext}")

extractors = {
'.txt': extract_txt,
'.md': extract_markdown,
'.pdf': extract_pdf,
'.docx': extract_docx,
'.csv': extract_csv,
}

return extractors[ext](file_path)


def extract_txt(file_path: str) -> str:
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
return f.read()


def extract_markdown(file_path: str) -> str:
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
return f.read()


def extract_pdf(file_path: str) -> str:
reader = PdfReader(file_path)
text_parts = []
for page in reader.pages:
text = page.extract_text()
if text:
text_parts.append(text)
return "\n\n".join(text_parts)


def extract_docx(file_path: str) -> str:
doc = docx.Document(file_path)
paragraphs = [p.text for p in doc.paragraphs if p.text.strip()]
return "\n\n".join(paragraphs)


def extract_csv(file_path: str) -> str:
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
return f.read()
53 changes: 53 additions & 0 deletions demo/document-rag/backend/src/generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import openai
from openai import OpenAI
from typing import List, Dict
from src.config import Config


class Generator:
def __init__(self, api_key: str):
self.client = OpenAI(api_key=api_key)
self.model = Config.LLM_MODEL

def generate(
self,
question: str,
context_chunks: List[Dict[str, any]]
) -> str:
"""
Generate answer using RAG prompt.
"""
if not context_chunks:
return "No relevant documents found. Please upload a document first."

context_text = "\n\n".join([
f"[Document {i+1}]\n{chunk['text']}"
for i, chunk in enumerate(context_chunks)
])

prompt = f"""You are a helpful assistant answering questions based on provided documents.

Context from documents:
{context_text}

Question: {question}

Instructions:
- Answer based ONLY on the context provided above
- If the answer is not in the context, say "I couldn't find this information in the uploaded documents."
- Be concise and helpful
- Cite which document(s) you're using when relevant

Answer:"""

response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": "You are a helpful assistant that answers questions based on provided documents."},
{"role": "user", "content": prompt}
],
temperature=0.3,
max_tokens=1000
)

return response.choices[0].message.content
Loading
Loading