Update README to reflect current implementation
- PostgreSQL now runs in Docker (pgvector/pgvector:pg18) on port 5433 - pgvector version updated to 0.8.2, PostgreSQL to version 18 - Project structure updated with start.sh, stop.sh, and indb/ subfolder - Running section now documents start.sh / stop.sh - Frontend URLs updated to /ui/ endpoint - Photo count corrected to 116 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,7 @@ ML library is loaded or called at search time.
|
||||
## Architecture overview
|
||||
|
||||
```
|
||||
115 JPEG photos
|
||||
116 JPEG photos
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────┐
|
||||
@@ -37,8 +37,8 @@ ML library is loaded or called at search time.
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────────────────┐ ┌──────────────────────┐ ┌───────────────────────┐
|
||||
│ PostgreSQL 16 │ │ Oracle 26ai │ │ Oracle 26ai │
|
||||
│ + pgvector 0.6.0 │ │ (version 23.26.1) │ │ (version 23.26.1) │
|
||||
│ PostgreSQL 18 │ │ Oracle 26ai │ │ Oracle 26ai │
|
||||
│ + pgvector 0.8.2 │ │ (version 23.26.1) │ │ (version 23.26.1) │
|
||||
│ database: │ │ PDB: FREEPDB1 │ │ PDB: FREEPDB1 │
|
||||
│ vectors_demo │ │ user: vectors_user │ │ schema: VECTOR │
|
||||
│ HNSW index │ │ HNSW index │ │ HNSW not needed │
|
||||
@@ -57,8 +57,7 @@ ML library is loaded or called at search time.
|
||||
└──────┬───────┘ └──────┬───────┘ │ port 8002 │
|
||||
│ │ └────────┬─────────┘
|
||||
▼ ▼ ▼
|
||||
frontend/index.html frontend/index.html frontend/index_indb.html
|
||||
(badge: pgvector) (badge: Oracle 26ai) (badge: Oracle In-DB)
|
||||
/ui/ (pgvector) /ui/ (Oracle 26ai) /ui/ (Oracle In-DB)
|
||||
```
|
||||
|
||||
---
|
||||
@@ -66,50 +65,59 @@ ML library is loaded or called at search time.
|
||||
## Project structure
|
||||
|
||||
```
|
||||
pgvector-demo/
|
||||
├── backend/
|
||||
│ ├── .env # PostgreSQL credentials, photo path
|
||||
│ ├── db.py # PostgreSQL connection factory
|
||||
│ ├── embedder.py # CLIP model wrapper
|
||||
│ ├── index_images.py # One-time indexing script
|
||||
│ └── main.py # FastAPI app (port 8000)
|
||||
└── frontend/
|
||||
└── index.html # Search UI
|
||||
|
||||
oravector-demo/
|
||||
vector-search-demo/
|
||||
├── start.sh # Start all three backends
|
||||
├── stop.sh # Stop all three backends
|
||||
├── photos/ # 116 JPEG photos (gitignored)
|
||||
├── pgvector-demo/
|
||||
│ ├── backend/
|
||||
│ │ ├── .env # PostgreSQL credentials, photo path
|
||||
│ │ ├── db.py # PostgreSQL connection factory
|
||||
│ │ ├── embedder.py # CLIP model wrapper
|
||||
│ │ ├── index_images.py # One-time indexing script
|
||||
│ │ └── main.py # FastAPI app (port 8000)
|
||||
│ └── frontend/
|
||||
│ └── index.html # Search UI (served at /ui/)
|
||||
└── oravector-demo/
|
||||
├── backend/
|
||||
│ ├── .env # Oracle credentials, photo path
|
||||
│ ├── db_oracle.py # Oracle connection factory (vectors_user)
|
||||
│ ├── db_oracle.py # Oracle connection factory
|
||||
│ ├── embedder.py # CLIP model wrapper (identical to pgvector)
|
||||
│ ├── index_images_oracle.py # One-time indexing script (Python embedding)
|
||||
│ ├── main_oracle.py # FastAPI app — Python embedding (port 8001)
|
||||
│ └── main_oracle_indb.py # FastAPI app — in-database embedding (port 8002)
|
||||
└── frontend/
|
||||
├── index.html # Search UI (Oracle 26ai, Python embedding)
|
||||
└── index_indb.html # Search UI (Oracle 26ai, in-database embedding)
|
||||
├── index.html # Search UI (Oracle 26ai, served at port 8001 /ui/)
|
||||
└── indb/
|
||||
└── index.html # Search UI (Oracle In-DB, served at port 8002 /ui/)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## System components installed
|
||||
## System components
|
||||
|
||||
### Operating system packages
|
||||
### PostgreSQL (Docker)
|
||||
|
||||
| Package | Version | Purpose |
|
||||
|---|---|---|
|
||||
| PostgreSQL | 16.13 (Ubuntu) | Relational database |
|
||||
| postgresql-16-pgvector | 0.6.0 | Vector data type and indexes for PostgreSQL |
|
||||
| Python | 3.12.3 | Runtime for all backend code |
|
||||
| Podman | — | Container runtime for Oracle 26ai |
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| Image | `pgvector/pgvector:pg18` |
|
||||
| Version | PostgreSQL 18 |
|
||||
| pgvector version | 0.8.2 |
|
||||
| Host port | 5433 (mapped to 5432 inside container) |
|
||||
| Database | `vectors_demo` |
|
||||
| User | `dl` |
|
||||
| Compose file | `~/docker/postgresql/docker-compose.yml` |
|
||||
|
||||
**PostgreSQL pgvector installation:**
|
||||
**Start PostgreSQL:**
|
||||
```bash
|
||||
sudo apt install postgresql-16-pgvector
|
||||
cd ~/docker/postgresql && docker compose up -d
|
||||
```
|
||||
|
||||
**pgvector extension activation** (requires superuser, run once per database):
|
||||
The `pgvector/pgvector:pg18` image includes pgvector pre-installed. The extension
|
||||
must be activated once per database:
|
||||
|
||||
```bash
|
||||
sudo -u postgres psql -d vectors_demo -c "CREATE EXTENSION vector;"
|
||||
docker exec postgresql-database-1 psql -U dl -d vectors_demo -c "CREATE EXTENSION vector;"
|
||||
```
|
||||
|
||||
### Oracle 26ai (Podman container)
|
||||
@@ -121,29 +129,15 @@ sudo -u postgres psql -d vectors_demo -c "CREATE EXTENSION vector;"
|
||||
| Container name | `oracle.free` |
|
||||
| Host port | 37611 (mapped to 1521 inside container) |
|
||||
| Pluggable Database | FREEPDB1 |
|
||||
| Schema user | `vectors_user` |
|
||||
| Schema users | `vectors_user`, `VECTOR` |
|
||||
|
||||
**Oracle vector memory** — the HNSW index is held entirely in the SGA's Vector
|
||||
Memory Area. This must be configured before the database starts:
|
||||
Memory Area. This is already configured:
|
||||
|
||||
```sql
|
||||
-- Connect as SYSDBA to service FREE (CDB root)
|
||||
ALTER SYSTEM SET vector_memory_size = 512M SCOPE=SPFILE;
|
||||
```
|
||||
|
||||
Then restart Oracle inside the container:
|
||||
```bash
|
||||
podman exec oracle.free bash -c "sqlplus -s / as sysdba <<'EOF'
|
||||
SHUTDOWN ABORT;
|
||||
EXIT;
|
||||
EOF"
|
||||
|
||||
podman exec oracle.free bash -c "sqlplus -s / as sysdba <<'EOF'
|
||||
STARTUP;
|
||||
EXIT;
|
||||
EOF"
|
||||
```
|
||||
|
||||
After restart, the SGA confirms: `Vector Memory Area: 536870912 bytes (512 MB)`.
|
||||
|
||||
### Python packages
|
||||
@@ -206,8 +200,8 @@ The database then finds the photos whose vectors point in the most similar direc
|
||||
### PostgreSQL + pgvector
|
||||
|
||||
```sql
|
||||
-- database: vectors_demo (PostgreSQL 16)
|
||||
CREATE EXTENSION vector; -- pgvector 0.6.0
|
||||
-- database: vectors_demo (PostgreSQL 18)
|
||||
CREATE EXTENSION vector; -- pgvector 0.8.2
|
||||
|
||||
CREATE TABLE images (
|
||||
id SERIAL PRIMARY KEY,
|
||||
@@ -264,8 +258,9 @@ Reads `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` from `.env` and
|
||||
returns a `psycopg2` connection.
|
||||
|
||||
**`db_oracle.py` (Oracle):**
|
||||
Reads `ORA_HOST`, `ORA_PORT`, `ORA_SERVICE`, `ORA_USER`, `ORA_PASSWORD` from `.env`
|
||||
and returns an `oracledb` connection. The DSN is assembled as `host:port/service`.
|
||||
Reads `ORA_HOST`, `ORA_PORT`, `ORA_SERVICE`, `ORA_USER`, `ORA_PASSWORD` (and
|
||||
`ORA_USER_INDB`, `ORA_PASSWORD_INDB` for the in-DB backend) from `.env` and
|
||||
returns an `oracledb` connection. The DSN is assembled as `host:port/service`.
|
||||
Runs in **thin mode** — no Oracle Instant Client installation is required on the host.
|
||||
|
||||
---
|
||||
@@ -280,7 +275,7 @@ photos. Each photo is committed individually so a crash does not lose prior work
|
||||
| Run command | `python3 index_images.py` | `python3 index_images_oracle.py` |
|
||||
| Vector bind | Python `list` passed directly | `array.array("f", embedding)` required |
|
||||
| Bind style | `%s` placeholders (psycopg2) | `:1`, `:2`, `:3` positional (oracledb) |
|
||||
| Runtime (115 photos, CPU) | **26 seconds** | **16 seconds** |
|
||||
| Runtime (116 photos, CPU) | ~26 seconds | ~16 seconds |
|
||||
|
||||
**Why `array.array` for Oracle?**
|
||||
The `python-oracledb` driver does not accept a plain Python list for a `VECTOR`
|
||||
@@ -291,13 +286,14 @@ matching the `FLOAT32` declaration in the Oracle column type.
|
||||
|
||||
### FastAPI applications
|
||||
|
||||
Both apps expose identical endpoints at different ports:
|
||||
All three apps expose identical endpoints:
|
||||
|
||||
| Endpoint | Description |
|
||||
|---|---|
|
||||
| `GET /search?q=<text>&limit=<n>` | Embed query, run nearest-neighbour search, return ranked results |
|
||||
| `GET /stats` | Return count of indexed photos |
|
||||
| `GET /photos/<filename>` | Serve original JPEG from the photos directory |
|
||||
| `GET /ui/` | Serve the search frontend (HTML) |
|
||||
|
||||
**Search query comparison:**
|
||||
|
||||
@@ -326,7 +322,7 @@ FETCH FIRST :lim ROWS ONLY
|
||||
| Cast required | `$1::vector` — explicit cast | No cast, column type is enforced |
|
||||
| Top-N clause | `LIMIT n` | `FETCH FIRST n ROWS ONLY` |
|
||||
| Bind style | `$1`, `$2` positional (psycopg2) | `:name` named binds (dict) |
|
||||
| Repeated param | `$1` can appear multiple times | Same `:name` can appear multiple times; positional `:1` cannot be reused |
|
||||
| Repeated param | `$1` can appear multiple times | Same `:name` can appear multiple times |
|
||||
| Score formula | `1 - (embedding <=> val)` | `1 - VECTOR_DISTANCE(...)` |
|
||||
|
||||
In both cases `1 − distance` converts cosine distance (0 = identical) into a
|
||||
@@ -336,14 +332,13 @@ similarity score (1.0 = identical), displayed as a percentage in the frontend.
|
||||
|
||||
## Frontend
|
||||
|
||||
Both frontends are identical single HTML files with no build step. Open directly
|
||||
in a browser.
|
||||
Three single-file HTML frontends, each served by its own backend at `/ui/`:
|
||||
|
||||
| | pgvector frontend | Oracle 26ai frontend |
|
||||
|---|---|---|
|
||||
| File | `pgvector-demo/frontend/index.html` | `oravector-demo/frontend/index.html` |
|
||||
| Badge label | pgvector | Oracle 26ai |
|
||||
| API base URL | `http://localhost:8000` | `http://localhost:8001` |
|
||||
| | pgvector | Oracle 26ai | Oracle In-DB |
|
||||
|---|---|---|---|
|
||||
| URL | `http://localhost:8000/ui/` | `http://localhost:8001/ui/` | `http://localhost:8002/ui/` |
|
||||
| Badge colour | Blue | Red | Purple |
|
||||
| File | `pgvector-demo/frontend/index.html` | `oravector-demo/frontend/index.html` | `oravector-demo/frontend/indb/index.html` |
|
||||
|
||||
Features: search box, Enter-key support, suggestion chips (trees, water, people,
|
||||
buildings, sky, street, night, cars), result grid with thumbnails and similarity
|
||||
@@ -353,29 +348,35 @@ scores in percent.
|
||||
|
||||
## Running the applications
|
||||
|
||||
**Start PostgreSQL backend** (Python embedding):
|
||||
### Start all backends
|
||||
|
||||
```bash
|
||||
cd pgvector-demo/backend
|
||||
uvicorn main:app --host 0.0.0.0 --port 8000
|
||||
./start.sh
|
||||
```
|
||||
|
||||
**Start Oracle backend — Python embedding:**
|
||||
This starts all three backends concurrently. Press Ctrl+C to stop all.
|
||||
|
||||
### Stop all backends
|
||||
|
||||
```bash
|
||||
cd oravector-demo/backend
|
||||
uvicorn main_oracle:app --host 0.0.0.0 --port 8001
|
||||
./stop.sh
|
||||
```
|
||||
|
||||
**Start Oracle backend — in-database embedding:**
|
||||
### Start backends individually
|
||||
|
||||
```bash
|
||||
cd oravector-demo/backend
|
||||
uvicorn main_oracle_indb:app --host 0.0.0.0 --port 8002
|
||||
# PostgreSQL backend
|
||||
cd pgvector-demo/backend && uvicorn main:app --host 0.0.0.0 --port 8000
|
||||
|
||||
# Oracle backend — Python embedding
|
||||
cd oravector-demo/backend && uvicorn main_oracle:app --host 0.0.0.0 --port 8001
|
||||
|
||||
# Oracle backend — in-database embedding
|
||||
cd oravector-demo/backend && uvicorn main_oracle_indb:app --host 0.0.0.0 --port 8002
|
||||
```
|
||||
|
||||
Open the matching `frontend/index.html` (ports 8000/8001) or
|
||||
`frontend/index_indb.html` (port 8002) in a browser. All three can run
|
||||
simultaneously.
|
||||
### Re-index after adding photos
|
||||
|
||||
**Re-index after adding photos:**
|
||||
```bash
|
||||
# PostgreSQL
|
||||
cd pgvector-demo/backend && python3 index_images.py
|
||||
@@ -441,8 +442,8 @@ Oracle's `DBMS_VECTOR.LOAD_ONNX_MODEL` requires the model's ONNX graph to use
|
||||
export uses `input_ids` additionally in `ArgMax` for EOS-token pooling, which
|
||||
Oracle's validator rejects. The manually loaded CLIP_TXT model in the `VECTOR`
|
||||
schema uses CLS-token pooling (position 0) instead, which produces a simpler
|
||||
graph that Oracle accepts. The
|
||||
cosine similarity between EOS-pooling and CLS-pooling variants is ~0.70.
|
||||
graph that Oracle accepts. The cosine similarity between EOS-pooling and
|
||||
CLS-pooling variants is ~0.70.
|
||||
|
||||
---
|
||||
|
||||
@@ -452,8 +453,8 @@ Measured on this installation (CPU only, no GPU):
|
||||
|
||||
| Metric | PostgreSQL + pgvector | Oracle 26ai (Python embed) | Oracle 26ai (in-DB embed) |
|
||||
|---|---|---|---|
|
||||
| Photos indexed | 115 | 115 | 116 (manually indexed) |
|
||||
| Indexing time | 26 seconds | 16 seconds | 0 (indexed separately by admin) |
|
||||
| Photos indexed | 116 | 116 | 116 (manually indexed) |
|
||||
| Indexing time | ~26 seconds | ~16 seconds | 0 (indexed separately by admin) |
|
||||
| Index type | HNSW (on disk) | HNSW (in-memory) | Full table scan (116 rows) |
|
||||
| Memory required | None | 512 MB SGA | 512 MB SGA |
|
||||
| Python CLIP at query time | Yes | Yes | **No** |
|
||||
|
||||
Reference in New Issue
Block a user