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:
2026-05-19 11:52:56 +02:00
parent 4741b18b74
commit 080795c4dc
+79 -78
View File
@@ -24,7 +24,7 @@ ML library is loaded or called at search time.
## Architecture overview ## 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 │ │ PostgreSQL 18 │ │ Oracle 26ai │ │ Oracle 26ai │
│ + pgvector 0.6.0 │ │ (version 23.26.1) │ │ (version 23.26.1) │ │ + pgvector 0.8.2 │ │ (version 23.26.1) │ │ (version 23.26.1) │
│ database: │ │ PDB: FREEPDB1 │ │ PDB: FREEPDB1 │ │ database: │ │ PDB: FREEPDB1 │ │ PDB: FREEPDB1 │
│ vectors_demo │ │ user: vectors_user │ │ schema: VECTOR │ │ vectors_demo │ │ user: vectors_user │ │ schema: VECTOR │
│ HNSW index │ │ HNSW index │ │ HNSW not needed │ │ HNSW index │ │ HNSW index │ │ HNSW not needed │
@@ -57,8 +57,7 @@ ML library is loaded or called at search time.
└──────┬───────┘ └──────┬───────┘ │ port 8002 │ └──────┬───────┘ └──────┬───────┘ │ port 8002 │
│ │ └────────┬─────────┘ │ │ └────────┬─────────┘
▼ ▼ ▼ ▼ ▼ ▼
frontend/index.html frontend/index.html frontend/index_indb.html /ui/ (pgvector) /ui/ (Oracle 26ai) /ui/ (Oracle In-DB)
(badge: pgvector) (badge: Oracle 26ai) (badge: Oracle In-DB)
``` ```
--- ---
@@ -66,50 +65,59 @@ ML library is loaded or called at search time.
## Project structure ## Project structure
``` ```
pgvector-demo/ vector-search-demo/
├── backend/ ├── start.sh # Start all three backends
│ ├── .env # PostgreSQL credentials, photo path ├── stop.sh # Stop all three backends
│ ├── db.py # PostgreSQL connection factory ├── photos/ # 116 JPEG photos (gitignored)
│ ├── embedder.py # CLIP model wrapper ├── pgvector-demo/
│ ├── index_images.py # One-time indexing script │ ├── backend/
└── main.py # FastAPI app (port 8000) │ ├── .env # PostgreSQL credentials, photo path
└── frontend/ │ │ ├── db.py # PostgreSQL connection factory
└── index.html # Search UI │ │ ├── embedder.py # CLIP model wrapper
│ │ ├── index_images.py # One-time indexing script
oravector-demo/ │ │ └── main.py # FastAPI app (port 8000)
│ └── frontend/
│ └── index.html # Search UI (served at /ui/)
└── oravector-demo/
├── backend/ ├── backend/
│ ├── .env # Oracle credentials, photo path │ ├── .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) │ ├── embedder.py # CLIP model wrapper (identical to pgvector)
│ ├── index_images_oracle.py # One-time indexing script (Python embedding) │ ├── index_images_oracle.py # One-time indexing script (Python embedding)
│ ├── main_oracle.py # FastAPI app — Python embedding (port 8001) │ ├── main_oracle.py # FastAPI app — Python embedding (port 8001)
│ └── main_oracle_indb.py # FastAPI app — in-database embedding (port 8002) │ └── main_oracle_indb.py # FastAPI app — in-database embedding (port 8002)
└── frontend/ └── frontend/
├── index.html # Search UI (Oracle 26ai, Python embedding) ├── index.html # Search UI (Oracle 26ai, served at port 8001 /ui/)
└── index_indb.html # Search UI (Oracle 26ai, in-database embedding) └── 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 | | Property | Value |
|---|---|---| |---|---|
| PostgreSQL | 16.13 (Ubuntu) | Relational database | | Image | `pgvector/pgvector:pg18` |
| postgresql-16-pgvector | 0.6.0 | Vector data type and indexes for PostgreSQL | | Version | PostgreSQL 18 |
| Python | 3.12.3 | Runtime for all backend code | | pgvector version | 0.8.2 |
| Podman | — | Container runtime for Oracle 26ai | | 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 ```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 ```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) ### Oracle 26ai (Podman container)
@@ -121,29 +129,15 @@ sudo -u postgres psql -d vectors_demo -c "CREATE EXTENSION vector;"
| Container name | `oracle.free` | | Container name | `oracle.free` |
| Host port | 37611 (mapped to 1521 inside container) | | Host port | 37611 (mapped to 1521 inside container) |
| Pluggable Database | FREEPDB1 | | 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 **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 ```sql
-- Connect as SYSDBA to service FREE (CDB root)
ALTER SYSTEM SET vector_memory_size = 512M SCOPE=SPFILE; 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)`. After restart, the SGA confirms: `Vector Memory Area: 536870912 bytes (512 MB)`.
### Python packages ### Python packages
@@ -206,8 +200,8 @@ The database then finds the photos whose vectors point in the most similar direc
### PostgreSQL + pgvector ### PostgreSQL + pgvector
```sql ```sql
-- database: vectors_demo (PostgreSQL 16) -- database: vectors_demo (PostgreSQL 18)
CREATE EXTENSION vector; -- pgvector 0.6.0 CREATE EXTENSION vector; -- pgvector 0.8.2
CREATE TABLE images ( CREATE TABLE images (
id SERIAL PRIMARY KEY, 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. returns a `psycopg2` connection.
**`db_oracle.py` (Oracle):** **`db_oracle.py` (Oracle):**
Reads `ORA_HOST`, `ORA_PORT`, `ORA_SERVICE`, `ORA_USER`, `ORA_PASSWORD` from `.env` Reads `ORA_HOST`, `ORA_PORT`, `ORA_SERVICE`, `ORA_USER`, `ORA_PASSWORD` (and
and returns an `oracledb` connection. The DSN is assembled as `host:port/service`. `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. 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` | | Run command | `python3 index_images.py` | `python3 index_images_oracle.py` |
| Vector bind | Python `list` passed directly | `array.array("f", embedding)` required | | Vector bind | Python `list` passed directly | `array.array("f", embedding)` required |
| Bind style | `%s` placeholders (psycopg2) | `:1`, `:2`, `:3` positional (oracledb) | | 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?** **Why `array.array` for Oracle?**
The `python-oracledb` driver does not accept a plain Python list for a `VECTOR` 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 ### FastAPI applications
Both apps expose identical endpoints at different ports: All three apps expose identical endpoints:
| Endpoint | Description | | Endpoint | Description |
|---|---| |---|---|
| `GET /search?q=<text>&limit=<n>` | Embed query, run nearest-neighbour search, return ranked results | | `GET /search?q=<text>&limit=<n>` | Embed query, run nearest-neighbour search, return ranked results |
| `GET /stats` | Return count of indexed photos | | `GET /stats` | Return count of indexed photos |
| `GET /photos/<filename>` | Serve original JPEG from the photos directory | | `GET /photos/<filename>` | Serve original JPEG from the photos directory |
| `GET /ui/` | Serve the search frontend (HTML) |
**Search query comparison:** **Search query comparison:**
@@ -326,7 +322,7 @@ FETCH FIRST :lim ROWS ONLY
| Cast required | `$1::vector` — explicit cast | No cast, column type is enforced | | Cast required | `$1::vector` — explicit cast | No cast, column type is enforced |
| Top-N clause | `LIMIT n` | `FETCH FIRST n ROWS ONLY` | | Top-N clause | `LIMIT n` | `FETCH FIRST n ROWS ONLY` |
| Bind style | `$1`, `$2` positional (psycopg2) | `:name` named binds (dict) | | 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(...)` | | Score formula | `1 - (embedding <=> val)` | `1 - VECTOR_DISTANCE(...)` |
In both cases `1 distance` converts cosine distance (0 = identical) into a 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 ## Frontend
Both frontends are identical single HTML files with no build step. Open directly Three single-file HTML frontends, each served by its own backend at `/ui/`:
in a browser.
| | pgvector frontend | Oracle 26ai frontend | | | pgvector | Oracle 26ai | Oracle In-DB |
|---|---|---| |---|---|---|---|
| File | `pgvector-demo/frontend/index.html` | `oravector-demo/frontend/index.html` | | URL | `http://localhost:8000/ui/` | `http://localhost:8001/ui/` | `http://localhost:8002/ui/` |
| Badge label | pgvector | Oracle 26ai | | Badge colour | Blue | Red | Purple |
| API base URL | `http://localhost:8000` | `http://localhost:8001` | | 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, Features: search box, Enter-key support, suggestion chips (trees, water, people,
buildings, sky, street, night, cars), result grid with thumbnails and similarity buildings, sky, street, night, cars), result grid with thumbnails and similarity
@@ -353,29 +348,35 @@ scores in percent.
## Running the applications ## Running the applications
**Start PostgreSQL backend** (Python embedding): ### Start all backends
```bash ```bash
cd pgvector-demo/backend ./start.sh
uvicorn main:app --host 0.0.0.0 --port 8000
``` ```
**Start Oracle backend — Python embedding:** This starts all three backends concurrently. Press Ctrl+C to stop all.
### Stop all backends
```bash ```bash
cd oravector-demo/backend ./stop.sh
uvicorn main_oracle:app --host 0.0.0.0 --port 8001
``` ```
**Start Oracle backend in-database embedding:** ### Start backends individually
```bash ```bash
cd oravector-demo/backend # PostgreSQL backend
uvicorn main_oracle_indb:app --host 0.0.0.0 --port 8002 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 ### Re-index after adding photos
`frontend/index_indb.html` (port 8002) in a browser. All three can run
simultaneously.
**Re-index after adding photos:**
```bash ```bash
# PostgreSQL # PostgreSQL
cd pgvector-demo/backend && python3 index_images.py 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 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` 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 schema uses CLS-token pooling (position 0) instead, which produces a simpler
graph that Oracle accepts. The graph that Oracle accepts. The cosine similarity between EOS-pooling and
cosine similarity between EOS-pooling and CLS-pooling variants is ~0.70. 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) | | Metric | PostgreSQL + pgvector | Oracle 26ai (Python embed) | Oracle 26ai (in-DB embed) |
|---|---|---|---| |---|---|---|---|
| Photos indexed | 115 | 115 | 116 (manually indexed) | | Photos indexed | 116 | 116 | 116 (manually indexed) |
| Indexing time | 26 seconds | 16 seconds | 0 (indexed separately by admin) | | Indexing time | ~26 seconds | ~16 seconds | 0 (indexed separately by admin) |
| Index type | HNSW (on disk) | HNSW (in-memory) | Full table scan (116 rows) | | Index type | HNSW (on disk) | HNSW (in-memory) | Full table scan (116 rows) |
| Memory required | None | 512 MB SGA | 512 MB SGA | | Memory required | None | 512 MB SGA | 512 MB SGA |
| Python CLIP at query time | Yes | Yes | **No** | | Python CLIP at query time | Yes | Yes | **No** |