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
|
## 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** |
|
||||||
|
|||||||
Reference in New Issue
Block a user