Hands-on tutorial · PostgreSQL · pgvector · Gemini · RAG
I attended a hands-on workshop on PostgreSQL for AI applications and completed it end to end. This guide turns that experience into chapters you can follow step by step, from installing PostgreSQL to a working RAG assistant that answers questions from your own data. No prior database or AI knowledge is needed.
Ordinary keyword search needs matching words, and people rarely use the same words as the person who wrote the answer. Look at these two sentences:
A person sees straight away that they mean almost the same thing. A computer comparing letters sees only one shared word: “I”. Semantic search compares meaning instead of words, and this guide shows you how to build it inside PostgreSQL.
Below are the five documents used in the workshop's semantic-search lab (Chapter 8). The box runs the same kind of keyword match as SQL's ILIKE '%word%'. Search for system, which is the word a user asking “Which system can manage structured information?” might type.
Keyword search returns 0 rows for “system”. In the workshop, semantic search ranked “PostgreSQL can store and manage structured information.” first for the same question, because it matched on meaning.
By the end you will have an AI FAQ Assistant. A user types a question, the system finds the most relevant FAQs by meaning in PostgreSQL, and Gemini writes a short answer using only those FAQs. This pattern is called RAG: Retrieval-Augmented Generation. In one line: find the right information first, then let AI write the answer from it.
| Tool | Its one job |
|---|---|
| PostgreSQL | Stores the information (here: 50 FAQs with id, question, answer) |
| pgvector (extension) | Adds a VECTOR(768) column type and distance operators such as <=> |
Gemini embeddings (gemini-embedding-001) | Turns any text into 768 numbers that capture its meaning |
| Gemini text model | Writes the final answer from the retrieved FAQs |
| Python | Glues everything together: google-genai talks to Gemini, psycopg/psycopg2 talk to PostgreSQL |
One database stores both the normal data and the vectors, so you don't need a separate vector database.
Each step builds on the one before it. Colours: store data · connect code · understand meaning · answer questions.
You need four things: PostgreSQL (the database server), pgAdmin 4 or DBeaver (a SQL editor), Python with Jupyter Notebook (to run code cell by cell), and the pgvector extension. The detailed steps below are for Windows, which is what I used. Mac and Linux users can follow the official links in section 2.3.
Before you begin: a 64-bit Windows PC (the EDB download page lists only Windows x86-64 as supported), an administrator account (the installer sets PostgreSQL up as a Windows service), an internet connection, and a password for the database superuser postgres. Write the password down, because you need it every time you connect. The install takes about 10 minutes, most of it spent waiting for files to unpack.
| Setting | Value used |
|---|---|
| Version | PostgreSQL 18 (installer from EDB) |
| Installation directory | C:\Program Files\PostgreSQL\18 |
| Data directory | C:\Program Files\PostgreSQL\18\data |
| Components | PostgreSQL Server, pgAdmin 4, Command Line Tools (Stack Builder unticked) |
| Superuser | postgres (password chosen during setup) |
| Port | 5432 (default) |
| Locale | DEFAULT |
| Windows service name | postgresql-x64-18 |
Search for postgresql download and open postgresql.org/download.
Under Select your operating system family, click Windows.
The Windows page explains that the interactive installer is certified by EDB. Click Download the installer, which takes you to enterprisedb.com.
Click the download icon in the Windows x86-64 column for the newest 18.x row. Older versions (17, 16) are listed below; choose one only if a project needs it.
If nothing downloads, click the Click here link on the “Your download should begin in a few seconds” page. Then open the downloaded file.
Double-click the installer. If Windows asks for permission, choose Yes.
Click Next >.
Keep C:\Program Files\PostgreSQL\18, then Next >.
Keep PostgreSQL Server, pgAdmin 4 and Command Line Tools (includes psql) ticked. Untick Stack Builder; it is only needed for extra drivers later.
Keep the default C:\Program Files\PostgreSQL\18\data.
Type and retype a password for postgres. Don't forget it. Use a strong one if the machine is shared or reachable from a network.
Keep 5432. Change it only if another program already uses that port.
Leave the locale as DEFAULT.
Check it matches the settings table above. It also shows the log file location (install-postgresql.log in your Windows temp folder), which is useful if you need to troubleshoot.
On Ready to Install, click Next >. This took about five minutes for me. Initialising the database cluster can look stuck for a minute or two; that is normal, so don't click Cancel.
Click Finish on Completing the PostgreSQL Setup Wizard.
Press the Windows key, type pgAdmin 4 and press Enter. The first start can take a while; a splash screen saying “Waiting for pgAdmin 4 to start” is normal.
In the left tree, expand Servers > PostgreSQL 18 > Databases > postgres. Enter your password if asked.
Open the Query Tool and run the query below. If the result shows PostgreSQL 18, the installation is complete.
SELECT version();Optional: connect from the command line with psql
"C:\Program Files\PostgreSQL\18\bin\psql.exe" -U postgres -h localhost -p 5432
postgres=# SELECT version();
postgres=# \qOptional: add PostgreSQL to PATH so you can type just psql. Open Start, search Edit the system environment variables, click Environment Variables, select Path under System variables, then Edit > New. Add C:\Program Files\PostgreSQL\18\bin and click OK on all windows. Open a new Command Prompt and run psql -U postgres.
Optional: check the Windows service. Press Win + R, type services.msc, and find postgresql-x64-18. Its status should be Running and its start type Automatic.
pgvector is a separate add-on. If CREATE EXTENSION vector; fails with a message that the extension is not available, pgvector isn't installed yet. On Windows you compile it with Visual Studio's C++ tools. This guide uses pgvector 0.8.7 (released 1 October 2026) with PostgreSQL 18.
| Prerequisite | Detail |
|---|---|
| PostgreSQL 18 | Installed in C:\Program Files\PostgreSQL\18 |
| Visual Studio | With the Desktop development with C++ workload |
| Git | Available from Command Prompt |
| Administrator access | Recommended for the build/install command |
Open the official Microsoft Visual Studio website and download the installer.
If User Account Control appears, confirm the publisher is Microsoft Corporation and continue.
Select Desktop development with C++, click Install, and wait for it to finish. This provides the Microsoft C++ compiler and build tools.
Search for x64 Native Tools Command Prompt for VS, right-click it and choose Run as administrator. A normal Command Prompt will not work.
Check that each one completes before running the next.
set "PGROOT=C:\Program Files\PostgreSQL\18"
cd %TEMP%
git clone --branch v0.8.7 https://github.com/pgvector/pgvector.git
cd pgvector
nmake /F Makefile.win
nmake /F Makefile.win install| Command | What it does |
|---|---|
set "PGROOT=..." | Tells the build where PostgreSQL 18 is installed |
cd %TEMP% | Moves to the Windows temporary folder to download the source |
git clone --branch v0.8.7 ... | Downloads the pgvector 0.8.7 source code from GitHub |
cd pgvector | Enters the folder containing Makefile.win |
nmake /F Makefile.win | Compiles the extension with Visual Studio's compiler (MSVC) |
nmake /F Makefile.win install | Copies the compiled files into your PostgreSQL 18 installation |
Enable and verify. Installing makes the extension available. You must still enable it once in each database where you want to use it. In pgAdmin or psql, connected to that database, run:
CREATE EXTENSION vector;
SELECT extname, extversion
FROM pg_extension
WHERE extname = 'vector'; -- a fresh install reports 0.8.7Quick test. Create a tiny table with 3-number vectors and run a nearest-neighbour query (<-> is Euclidean/L2 distance):
CREATE TABLE items (
id BIGSERIAL PRIMARY KEY,
embedding vector(3)
);
INSERT INTO items (embedding) VALUES ('[1,2,3]'), ('[4,5,6]');
SELECT * FROM items;
SELECT * FROM items
ORDER BY embedding <-> '[3,1,2]'
LIMIT 5;If these run, pgvector is installed and working. Build errors are covered in Chapter 11.
My workshop used Windows, so I haven't tested other systems. Please follow the official instructions:
Everything from CREATE EXTENSION vector; onwards is the same on every operating system.
The workshop ran every step in a Jupyter notebook, cell by cell.
Create a free key in Google AI Studio.
Create a file named api_key.txt next to your notebook and paste only the key into it. Never share a notebook that contains your real key.
Run this in a notebook cell. The ! runs it as a shell command, and -q hides the long install log.
!pip install -q -U google-genai "psycopg[binary]" psycopg2-binary pandas| Library | Used for |
|---|---|
google-genai | Google's current Python SDK for Gemini (embeddings + text generation) |
psycopg[binary] | psycopg 3, the PostgreSQL driver used in the warm-up labs (Chapters 4 and 8) |
psycopg2-binary | psycopg2, the driver used in the main FAQ Assistant notebook (Chapter 10) |
pandas | Shows data as a neat table |
Close an app and open it tomorrow, and your data is still there. A college app keeps names, departments and marks; a shopping app keeps products and orders; a chat app keeps users and messages. The system that keeps this information safe, organised and searchable is a database.
Why not a text file? Four lines are easy, but forty thousand are not. Finding every CSE student quickly, correcting one age without breaking other lines, removing one record safely, or letting two people edit at once are all hard with a plain file. A database gives you ready-made tools for all of them.
PostgreSQL is a free, open-source relational database management system (RDBMS) that stores structured information in tables. Others include MySQL, Oracle, SQL Server and SQLite. With the pgvector extension, PostgreSQL can also store AI embeddings. One PostgreSQL server holds many databases, a database holds many tables, and a table holds rows and columns.
| id | name | department | age |
|---|---|---|---|
| 1 | Ravi | CSE | 21 |
| 2 | Priya | ECE | 22 |
| 3 | Arun | CSE | 20 |
| 4 | Meena | IT | 21 |
| Term | Meaning |
|---|---|
| Table | A structure that organises data in rows and columns |
| Row | One complete record, e.g. 1 | Ravi | CSE | 21 |
| Column | One kind of information for every row: id, name, … |
| Primary key | A value that uniquely identifies each row (id). No two rows share an id. |
In pgAdmin or DBeaver, check the server is reachable, then create a database:
SELECT version();
CREATE DATABASE ai_workshop;ai_workshop, switch your connection to it in pgAdmin/DBeaver. Otherwise new tables land in the default postgres database.CREATE TABLE developers (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
department VARCHAR(50),
age INTEGER
);| Part | Meaning |
|---|---|
developers | Name of the table |
id | Identifier of each developer |
SERIAL | PostgreSQL fills 1, 2, 3 … automatically. You never type ids yourself. |
PRIMARY KEY | Each id is unique, so there is exactly one row per id |
VARCHAR(100) | Text up to 100 characters |
INTEGER | Whole numbers (used for age) |
INSERT INTO developers (name, department, age)
VALUES ('Ravi', 'CSE', 21),
('Priya', 'ECE', 22),
('Arun', 'CSE', 20),
('Meena', 'IT', 21);
SELECT * FROM developers; -- * means "all columns"Text values go in single quotes; numbers don't. You never supply id because SERIAL fills it.
SELECT * FROM developers WHERE department = 'CSE'; -- Ravi and Arun
SELECT * FROM developers ORDER BY age DESC; -- oldest first
SELECT * FROM developers ORDER BY age ASC; -- youngest first (ASC is the default)ORDER BY distance: the closest meaning comes first.UPDATE developers SET age = 22 WHERE id = 1; -- Ravi's age
DELETE FROM developers WHERE id = 4; -- remove Meena
SELECT * FROM developers; -- checkUPDATE changes every row and DELETE removes every row. Filter on the primary key (id) to touch exactly one record. Run SELECT after each step to see the effect.INSERTSELECTUPDATEDELETEThat is all the SQL this project needs: CREATE TABLE, INSERT, SELECT, WHERE, ORDER BY, UPDATE, DELETE.
Real applications don't type SQL into pgAdmin; their code sends it automatically. Python speaks Python and PostgreSQL speaks SQL, so a driver library such as psycopg acts as the phone line between them. It carries SQL from your code to the server and brings the rows back. Later, Python will fetch embeddings from Gemini and store them through this same line.
import psycopg
conn = psycopg.connect(
host="localhost",
dbname="ai_workshop",
user="postgres",
password="YOUR_POSTGRES_PASSWORD"
)
print("Connected!")| Argument | Meaning |
|---|---|
host | PostgreSQL runs on this same computer |
dbname | The database we created |
user | The PostgreSQL user account |
password | Set during installation |
If Connected! prints, Python can reach the database.
with conn.cursor() as cur:
cur.execute("SELECT * FROM developers")
rows = cur.fetchall()
for row in rows:
print(row)(1, 'Ravi', 'CSE', 22)
(2, 'Priya', 'ECE', 22)
(3, 'Arun', 'CSE', 20)The cursor sends SQL and receives results. fetchall() returns a list in which each row is a Python tuple. with … as cur closes the cursor automatically. This Python → SQL → PostgreSQL → rows-back round trip is the heart of the AI project.
with conn.cursor() as cur:
cur.execute(
"""
INSERT INTO developers (name, department, age)
VALUES (%s, %s, %s)
""",
("Kiran", "CSE", 21)
)
conn.commit()
print("Data inserted!")An API (Application Programming Interface) is a way for one software application to communicate with another. Analogy: you (Python) never enter the kitchen (the Gemini model, which runs on Google's servers). You give your order to the waiter (the API), who returns with the dish (the response).
This reads the key from api_key.txt (created in Chapter 2). .strip() removes stray spaces or newlines. The client is your connection to Gemini, and every Gemini call goes through it.
from google import genai
api_key = open("api_key.txt").read().strip()
client = genai.Client(api_key=api_key)
print("Gemini client created successfully")The project uses two Gemini models with different jobs. Keep the names in variables so you only change them in one place.
TEXT_MODEL = "gemini-3.5-flash-lite" # writes answers (text generation)
EMBEDDING_MODEL = "gemini-embedding-001" # converts text into vectors
response = client.models.generate_content(
model=TEXT_MODEL,
contents="Explain postgresql in one simple sentence."
)
print(response.text)PostgreSQL is a powerful, open-source relational database management system known for its reliability, advanced features, and ability to handle complex data at scale.gemini-3.5-flash-lite is the text model I used and it worked with my key. If you get a 404 / model not found error, check Google AI Studio for a text model your key can use and update TEXT_MODEL.If a one-sentence answer prints, three things work: your key, the SDK and your internet connection.
Computers compare numbers, not meanings. Which two of these are most similar?
Obviously A and B, but a computer can't “see” that from letters. Goal: give every sentence a list of numbers so that similar meanings get similar numbers. Once meaning becomes numbers, comparing meaning becomes arithmetic.
A vector is simply a list of numbers. The number of values is its number of dimensions.
| Example | Vector | Dimensions |
|---|---|---|
| A point on graph paper | [3, 5] | 2 |
| A place on a map (lat, long) | [13.08, 80.27] | 2 |
| A colour (red, green, blue) | [255, 128, 0] | 3 |
| A sentence embedding | [0.21, -0.14, 0.73, …] | 768 |
Map analogy: a place needs 2 numbers, and nearby places have nearby numbers. An embedding is a “meaning address” with 768 numbers instead of 2.
Real models (transformers) adjust each token using its neighbours, so “bank” of a river differs from a “bank” account. In the toy model below we simply average the token vectors and scale the result to length 1.
| Word | animal | feeling | finance | Why these values? |
|---|---|---|---|---|
| I | 0.1 | 0.1 | 0.0 | A plain word with almost no meaning of its own |
| love | 0.1 | 0.7 | 0.0 | Strongly a feeling |
| dogs | 1.0 | 0.1 | 0.0 | Strongly an animal |
| animal | feeling | finance | |
|---|---|---|---|
| Sum | 0.1 + 0.1 + 1.0 = 1.2 | 0.1 + 0.7 + 0.1 = 0.9 | 0.0 |
| Average (÷ 3) | 0.4 | 0.3 | 0.0 |
Sentence A before normalising = [0.4, 0.3, 0.0]: mostly animal, some feeling, no money.
We care about the direction of the arrow, not its length. A long sentence can produce bigger numbers than a short one even when they mean the same thing, and normalising removes that effect.
length = √(0.4² + 0.3² + 0.0²) = √(0.16 + 0.09 + 0) = √0.25 = 0.5
animal = 0.4 ÷ 0.5 = 0.8
feeling = 0.3 ÷ 0.5 = 0.6
finance = 0.0 ÷ 0.5 = 0.0
Embedding of "I love dogs" = [0.8, 0.6, 0.0]
Check: 0.8² + 0.6² + 0.0² = 0.64 + 0.36 = 1.00 -> √1.00 = 1 ✓The same three steps give B “I like puppies” = [0.6, 0.8, 0.0] (like = [0.1, 0.9, 0.0], puppies = [0.7, 0.2, 0.0]) and C “The stock market fell today” = [0.0, 0.28, 0.96]. C has 5 words, so you divide by 5 (The = [0, 0, 0], stock = [0, 0, 0.9], market = [0, 0, 1.0], fell = [0, 0.6, 0.3], today = [0, 0.1, 0.2]).
Type sentences using the toy words and watch every step: lookup, average, length, normalise. Chapter 7 explains the similarity numbers it also shows.
Words in the toy lookup table: I, love, dogs, like, puppies, The, stock, market, fell, today.
output_dimensionality=768) and what the model practises on during training.Google hasn't published every detail of how Gemini's embedding model was trained; this is the standard approach for models of this kind.
Why can't we name the 768 positions? One position may respond a little to several ideas at once, and one idea is spread across many positions. Meaning lives in directions. A famous result from an older model (word2vec) is king − man + woman ≈ queen, which works with no column called “gender”.
This is the embedding function from the workshop slides. It asks for 768 numbers per text.
from google.genai import types
EMBEDDING_MODEL = "gemini-embedding-001"
def create_embedding(text):
result = client.models.embed_content(
model=EMBEDDING_MODEL,
contents=text,
config=types.EmbedContentConfig(output_dimensionality=768)
)
return result.embeddings[0].valuesembed_content can embed many texts, so it returns a list; [0] takes the first. .values is a plain Python list of 768 decimal numbers.
embedding = create_embedding("PostgreSQL is a database.")
print(len(embedding)) # 768
print(embedding[:10]) # first 10 numbers
sentences = ["I love dogs.", "I like puppies.", "The stock market fell today."]
embeddings = [create_embedding(t) for t in sentences]
print(len(embeddings)) # 3
print(len(embeddings[0])) # 768Your actual numbers will differ from anything shown here. What matters is the length: 768.
Similar meanings point in similar directions. A small angle between two arrows means the sentences mean nearly the same thing (A and B); a large angle means they are about different things (A and C). You measure the angle with cosine.
A = [0.8, 0.6, 0.0] B = [0.6, 0.8, 0.0]
A · B = 0.8×0.6 + 0.6×0.8 + 0.0×0.0 = 0.48 + 0.48 + 0.00 = 0.96|A| = √(0.64 + 0.36 + 0) = 1.0 and |B| = 1.0. Because we normalised, every length is 1.
| cos θ | Angle | Meaning |
|---|---|---|
| 1 | 0° | Same direction: same meaning |
| 0 | 90° | Unrelated |
| −1 | 180° | Opposite direction |
In PostgreSQL, embedding <=> query returns this value: 0 = same meaning, 1 = unrelated, 2 = opposite. Smaller means closer, so you ORDER BY it smallest first.
| Pair | Dot product (= cos θ, lengths are 1) | Cosine distance | Verdict |
|---|---|---|---|
| A ↔ B | 0.8×0.6 + 0.6×0.8 + 0×0 = 0.96 | 0.04 | Very similar |
| A ↔ C | 0.8×0 + 0.6×0.28 + 0×0.96 = 0.168 | 0.832 | Different |
| B ↔ C | 0.6×0 + 0.8×0.28 + 0×0.96 = 0.224 | 0.776 | Different |
The maths agrees with human judgement. Sorting by distance is exactly what semantic search does. You can check these three rows with the calculator in Chapter 6.
| Operator | Name | Idea |
|---|---|---|
<=> | Cosine distance | Angle between arrows (our choice) |
<-> | Euclidean (L2) distance | Straight-line gap between the two points. A ↔ B = √(0.04 + 0.04) ≈ 0.28 |
<#> | Negative inner product | Minus the dot product |
For length-1 vectors all three give the same ranking. Cosine is the usual choice for text.
pgvector adds a VECTOR type next to TEXT, INTEGER, DATE and BOOLEAN, so the text and its meaning live in the same row.
| id | content (TEXT) | embedding (VECTOR(768)) |
|---|---|---|
| 1 | PostgreSQL is an open-source relational database. | [0.021, -0.014, 0.073, … 768 values] |
Illustrative values. Your real numbers will differ.
Run this in pgAdmin, connected to ai_workshop:
CREATE EXTENSION IF NOT EXISTS vector;
SELECT extname FROM pg_extension; -- 'vector' should be listed
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
content TEXT,
embedding VECTOR(768)
);VECTOR(768) must match output_dimensionality=768. A vector of any other size is rejected.CREATE EXTENSION only enables pgvector. If it isn't installed on the server you get “could not open extension control file”. See Chapter 2.These five were chosen so the results are easy to judge: two about PostgreSQL, one about another database, one about Python and one about machine learning.
documents = [
"PostgreSQL is an open-source relational database.",
"PostgreSQL can store and manage structured information.",
"MongoDB is a document-oriented database.",
"Python is a popular programming language used in AI.",
"Machine learning allows computers to learn patterns from data.",
]
with conn.cursor() as cur:
cur.execute("TRUNCATE TABLE documents RESTART IDENTITY")
conn.commit()
for text in documents:
embedding = create_embedding(text)
with conn.cursor() as cur:
cur.execute(
"INSERT INTO documents (content, embedding) VALUES (%s, %s)",
(text, embedding))
conn.commit()
with conn.cursor() as cur:
cur.execute("SELECT COUNT(*) FROM documents")
print(cur.fetchone()[0]) # 5TRUNCATE … RESTART IDENTITY empties the table and restarts ids at 1, so nothing is duplicated. Count shows 6 or 10? A cell ran twice, so re-run the TRUNCATE and insert again. When inserting into a VECTOR column, the Python list is converted to a vector automatically. Check with SELECT id, content FROM documents; skip the 768 numbers because they are hard to read.
SELECT content,
embedding <=> %s::vector AS distance
FROM documents
ORDER BY embedding <=> %s::vector
LIMIT 5;Read it aloud: for every document, compute its distance to the question, sort closest first, and return 5. The question vector appears twice: once to show the distance and once to sort by it.
<=> needs a vector. Without the cast you get operator does not exist: vector <=> double precision[]. (An alternative from the workshop: pip install pgvector, then from pgvector.psycopg import register_vector; register_vector(conn).) The %s placeholders only work from Python, so this query can't be pasted into pgAdmin as-is.def semantic_search(query, top_k=3):
query_embedding = create_embedding(query)
with conn.cursor() as cur:
cur.execute("""
SELECT content, embedding <=> %s::vector AS distance
FROM documents
ORDER BY embedding <=> %s::vector
LIMIT %s""",
(query_embedding, query_embedding, top_k))
return cur.fetchall()
for content, distance in semantic_search(
"Which system can manage structured information?"):
print(round(distance, 3), content)Expected: the two PostgreSQL documents appear at the top. This function becomes the retrieval part of RAG.
for q in ["Which database can store information?",
"Which programming language is popular in AI?",
"How can computers learn from data?"]:
print("QUESTION:", q)
for content, distance in semantic_search(q):
print(f" {distance:.3f} {content}")| Question | Expected top document |
|---|---|
| Which database can store information? | A PostgreSQL document (MongoDB may also rank high) |
| Which programming language is popular in AI? | Python is a popular programming language used in AI. |
| How can computers learn from data? | Machine learning allows computers to learn patterns from data. |
This lab compares the question against every row (exact search). That always finds the true closest match, but it slows down as rows grow into millions. A vector index, like the index at the back of a book, jumps to nearby vectors fast; results are approximate but very good. pgvector offers HNSW, and vector_cosine_ops matches the <=> operator:
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops);You don't need it for small tables like these; just know it exists.
Semantic search returns documents, but a person still has to read them and write the answer. Can AI do that step? First, ask Gemini on its own: “What is the last date to pay the exam fee at our college?”
Gemini: “The last date to pay the exam fee is 15 March.” ✗ sounds right, but invented
Each side has half of the answer. Gemini writes clear, natural answers but doesn't know our information. Our database knows our information but can't write answers. RAG joins the two: the database finds, Gemini writes.
| In the open-book exam | In our system | Done by |
|---|---|---|
| The textbook | Our table of FAQs | PostgreSQL |
| Finding the right page | Semantic search by meaning | pgvector (<=>) |
| Keeping the page next to the question | Putting the found FAQs into the prompt | Python |
| Writing the answer | Generating the reply | Gemini |
Another way to see it: the librarian (pgvector) doesn't write anything; it quickly fetches the pages that match your question by meaning. The writer (Gemini) doesn't search anything; it writes a clear answer using only the pages given. The writer is only as good as the pages the librarian brings.
A student types “Till when can I pay my exam fees?”. The FAQ texts and distances below are illustrative examples from the workshop.
Setup (done once): 50 college FAQs → create_embedding() → stored in the documents table with pgvector.
The same create_embedding() used for the FAQs turns it into 768 numbers, so the question and the FAQs live in the same “meaning space”. You must use the same embedding model for both.
semantic_search(question, top_k=3) returns: “The exam fee must be paid before 10 October through the student portal.” (0.12), “A late fee of ₹500 applies for payments between 11 and 15 October.” (0.19), and “Hall tickets are issued only after fee payment.” (0.31). The question said till when … fees and the FAQ says must be paid before … fee: different words, same meaning.
Add the found FAQs to the prompt. You aren't changing Gemini at all, only giving it a better prompt.
Gemini writes: “You can pay the exam fee until 10 October through the student portal. Between 11 and 15 October you can still pay, but a late fee of ₹500 applies.” It combined FAQ 1 and FAQ 2 into one natural answer.
Answer the question using ONLY the information below.
If the answer is not there, say "I don't know".
Information:
1. The exam fee must be paid before 10 October through the student portal.
2. A late fee of ₹500 applies for payments between 11 and 15 October.
3. Hall tickets are issued only after fee payment.
Question: Till when can I pay my exam fees?Chunk = a small piece of a long document, so each embedding captures one focused idea. Each FAQ here is already small, so one FAQ = one chunk.
| Gemini alone | Gemini with RAG | |
|---|---|---|
| Knows our college's dates? | No | Yes, from our table |
| Up to date? | Only up to its training date | As fresh as our table |
| Can show where the answer came from? | No | Yes: the retrieved FAQs |
| Risk of made-up answers | Higher | Lower: told to use only the given information |
If the exam date changes, retraining an AI model would need huge data, powerful GPUs, days or weeks, and high cost. With RAG you update one row in the table, and its embedding, in seconds:
new_text = "The exam fee must be paid before 12 October."
cur.execute("UPDATE documents SET content = %s, embedding = %s::vector WHERE id = 1",
(new_text, create_embedding(new_text)))
conn.commit()Answer in your head, then click to reveal.
This is the main project, exactly as I ran it in a Jupyter notebook. It uses psycopg2 and connects to the default postgres database. The outputs shown are the real ones from my run. Yours may differ slightly, especially the AI-written answers.
api_key.txt with your Gemini key must sit next to the notebook. If you connect to a different database (for example ai_workshop), pgvector must be enabled in that database; the code below does this for you.!pip install -q google-genai psycopg2-binary pandas
import pandas as pd
import psycopg2
from google import genai
api_key = open("api_key.txt").read().strip()
client = genai.Client(api_key=api_key)
print("gemini client created successfully")
TEXT_MODEL = "gemini-3.5-flash-lite"
EMBEDDING_MODEL = "gemini-embedding-001"DB_HOST = "localhost"
DB_PORT = 5432
DB_NAME = "postgres"
DB_USER = "postgres"
DB_PASSWORD = "YOUR_POSTGRES_PASSWORD" # better: read it from an environment variable
conn = psycopg2.connect(host=DB_HOST, port=DB_PORT, database=DB_NAME,
user=DB_USER, password=DB_PASSWORD)
cur = conn.cursor()
print("connected to postgresql successfully!")| Object | What it is | What we use it for |
|---|---|---|
conn | The connection: an open line to the server | commit() saves changes · rollback() undoes after an error · close() ends it |
cur | The cursor: sends SQL and brings results back | execute(...) runs SQL · fetchone() reads one row · fetchall() reads all rows |
Analogy: conn is the phone call to the database, and cur is you speaking and listening during the call.
cur.execute("CREATE EXTENSION IF NOT EXISTS vector;")
conn.commit()
print("pgvector enabled")
cur.execute("""
CREATE TABLE IF NOT EXISTS faqs (
id SERIAL PRIMARY KEY, -- 1, 2, 3 ... assigned automatically
question TEXT NOT NULL,
answer TEXT NOT NULL,
embedding VECTOR(768) -- exactly 768 numbers
);""")
conn.commit()
print("FAQ table created!")Text and vectors live side by side in the same row. The embedding column starts empty (NULL) and gets filled in Part 2. IF NOT EXISTS means running the cell twice does no harm.
The knowledge base: 50 FAQs. These are the only facts the assistant will answer from. The topics match the workshop: databases & SQL (12), Python & programming (7), APIs & API keys (2), AI/ML/NLP (5), embeddings & vectors (4), vector databases & pgvector (3), semantic search (4), RAG (4), prompts/LLMs/Gemini (3), Docker & cloud (3) and data in AI apps (3).
faq_data = [
{
"question": "What is PostgreSQL?",
"answer": "PostgreSQL is a free and open-source relational database system used to store, organize, and manage data."
},
{
"question": "What is SQL?",
"answer": "SQL stands for Structured Query Language. It is used to work with data stored in databases."
},
{
"question": "What is a database?",
"answer": "A database is an organized collection of information that can be stored, searched, and managed by a computer system."
},
{
"question": "What is a table in a database?",
"answer": "A table is a structure inside a database that stores information using rows and columns."
},
{
"question": "What is a row?",
"answer": "A row represents one complete record in a database table."
},
{
"question": "What is a column?",
"answer": "A column represents one type or category of information stored in a database table."
},
{
"question": "What is a primary key?",
"answer": "A primary key is a column that uniquely identifies each row in a database table."
},
{
"question": "What is an index?",
"answer": "An index is a database structure that helps the database find information more efficiently."
},
{
"question": "What is SELECT in SQL?",
"answer": "SELECT is an SQL command used to read or retrieve data from a database."
},
{
"question": "What is INSERT in SQL?",
"answer": "INSERT is an SQL command used to add new records to a database table."
},
{
"question": "What is UPDATE in SQL?",
"answer": "UPDATE is an SQL command used to change existing data in a database table."
},
{
"question": "What is DELETE in SQL?",
"answer": "DELETE is an SQL command used to remove records from a database table."
},
{
"question": "What is Python?",
"answer": "Python is a popular programming language known for its simple syntax and use in software development, data science, and AI."
},
{
"question": "What is a variable in Python?",
"answer": "A variable is a name used to store a value in a Python program."
},
{
"question": "What is a Python function?",
"answer": "A function is a reusable block of Python code that performs a particular task."
},
{
"question": "What is a Python list?",
"answer": "A Python list is a collection that can store multiple values in a single variable."
},
{
"question": "What is a Python dictionary?",
"answer": "A Python dictionary stores information as key-value pairs."
},
{
"question": "What is programming?",
"answer": "Programming is the process of writing instructions that tell a computer what to do."
},
{
"question": "What is a programming language?",
"answer": "A programming language is a language used by programmers to write instructions that computers can execute."
},
{
"question": "What is an API?",
"answer": "An API is a way for one software application to communicate with another software application."
},
{
"question": "What is an API key?",
"answer": "An API key is a piece of information used by an application to identify or authenticate access to an API."
},
{
"question": "What is Artificial Intelligence?",
"answer": "Artificial Intelligence, or AI, is technology that allows computers to perform tasks that normally require human-like intelligence."
},
{
"question": "What is Machine Learning?",
"answer": "Machine Learning is a part of AI where computers learn patterns from data and use those patterns to make predictions or decisions."
},
{
"question": "What is an AI model?",
"answer": "An AI model is a computer system that has learned patterns from data and can perform tasks such as generating text or making predictions."
},
{
"question": "What is Natural Language Processing?",
"answer": "Natural Language Processing, or NLP, is a field of AI that helps computers work with human language."
},
{
"question": "What is text generation?",
"answer": "Text generation is the process of using an AI model to create human-readable text from an input."
},
{
"question": "What is an embedding?",
"answer": "An embedding is a list of numbers that represents information such as text in a form that an AI system can process."
},
{
"question": "What is a vector?",
"answer": "A vector is a list of numbers. In AI applications, vectors can be used to represent information such as the meaning of text."
},
{
"question": "Why do we use embeddings?",
"answer": "Embeddings allow computers to represent the meaning of information as numbers so that similar information can be compared."
},
{
"question": "How does text become an embedding?",
"answer": "An embedding model takes text as input and converts it into a list of numbers that represents information about the text."
},
{
"question": "What is a vector database?",
"answer": "A vector database is a database system designed to store and search vector representations of information."
},
{
"question": "What is pgvector?",
"answer": "pgvector is a PostgreSQL extension that allows PostgreSQL to store and search vector data."
},
{
"question": "Why use pgvector with PostgreSQL?",
"answer": "pgvector allows PostgreSQL to store normal data together with vector data and perform similarity searches."
},
{
"question": "What is semantic search?",
"answer": "Semantic search finds information based on meaning instead of only looking for exact matching words."
},
{
"question": "How is semantic search different from keyword search?",
"answer": "Keyword search mainly looks for matching words, while semantic search uses embeddings to compare the meaning of information."
},
{
"question": "What is similarity search?",
"answer": "Similarity search finds information whose vector representation is similar to the vector representation of the user's question."
},
{
"question": "Why are vectors useful for search?",
"answer": "Vectors allow information to be represented as numbers so that a system can compare the similarity between different pieces of information."
},
{
"question": "What is RAG?",
"answer": "RAG stands for Retrieval-Augmented Generation. It retrieves relevant information and gives that information to an AI model to generate an answer."
},
{
"question": "How does RAG work?",
"answer": "RAG first searches a knowledge source for relevant information and then gives the retrieved information to an AI model before generating the final answer."
},
{
"question": "Why do we use RAG?",
"answer": "RAG allows an AI application to use information retrieved from a knowledge source when generating an answer."
},
{
"question": "What is a RAG application?",
"answer": "A RAG application is an AI application that retrieves relevant information from a knowledge source and uses that information to generate an answer."
},
{
"question": "What is a prompt?",
"answer": "A prompt is an instruction or question given to an AI model."
},
{
"question": "What is a language model?",
"answer": "A language model is an AI model that can process and generate human language."
},
{
"question": "What is Gemini?",
"answer": "Gemini is a family of AI models from Google that can be used for tasks such as understanding and generating content."
},
{
"question": "What is Docker?",
"answer": "Docker is a technology used to package applications and their dependencies into containers."
},
{
"question": "What is a Docker container?",
"answer": "A Docker container is an isolated environment that contains an application and the components it needs to run."
},
{
"question": "What is cloud computing?",
"answer": "Cloud computing means using computing resources such as servers and storage over the internet."
},
{
"question": "What is data?",
"answer": "Data is information that can be stored, processed, and analyzed by a computer."
},
{
"question": "Why do AI applications need databases?",
"answer": "AI applications can use databases to store information, retrieve useful data, and provide information to AI models."
},
{
"question": "How can PostgreSQL be used in an AI application?",
"answer": "PostgreSQL can store application data and, with pgvector, can also store and search vector embeddings for AI applications."
}
]print("number of faqs:", len(faq_data)) # 50
df = pd.DataFrame(faq_data)
df # preview as a table
for item in faq_data:
cur.execute("INSERT INTO faqs (question, answer) VALUES (%s, %s)",
(item["question"], item["answer"]))
conn.commit()
cur.execute("SELECT COUNT(*) FROM faqs;")
print("Number of faqs in postgresql:", cur.fetchone()[0])number of faqs: 50
Number of faqs in postgresql: 50cur.execute("TRUNCATE faqs RESTART IDENTITY;"); conn.commit()An embedding is meaning written as numbers. Every text, short or long, becomes exactly 768 numbers. Texts with similar meaning get vectors that point the same way. No single number means anything alone; the pattern as a whole encodes the meaning.
def create_embedding(text):
result = client.models.embed_content(
model=EMBEDDING_MODEL,
contents=text,
config={"output_dimensionality": 768} # must match VECTOR(768)
)
return result.embeddings[0].values
embedding = create_embedding(" ")
print("Number of values:", len(embedding))
print("First 10 values:", embedding[:10])Number of values: 768
First 10 values: [-0.0053882604, 0.0076578804, 0.011579545, -0.0575647, 0.029505298, 0.011536011, -0.0020401385, 0.003505577, -0.007628863, 0.0032300877]Embed question + answer together, so the vector carries the full meaning of the FAQ. That way a user's question can match ideas that appear only in the answer.
def faq_text(item):
return f"""
question:
{item["question"]}
Answer:
{item["answer"]}
"""
for i, item in enumerate(faq_data):
print(f"Processing FAQ {i+1} of {len(faq_data)}")
text = faq_text(item)
embedding = create_embedding(text)
faq_id = i + 1
embedding_string = "[" + ",".join(str(x) for x in embedding) + "]"
cur.execute("UPDATE faqs SET embedding = %s WHERE id = %s",
(embedding_string, faq_id))
conn.commit()
print("All faqs embedding stored successfully")Each Python list is converted to the text format pgvector understands ("[0.012,-0.013,…]"), then saved with UPDATE … WHERE id. faq_id = i + 1 works because the table was filled once, in the same order. This makes one API call per FAQ, so it takes a little while.
cur.execute("SELECT COUNT(*) FROM faqs WHERE embedding IS NOT NULL;")
print("FAQs with embeddings:", cur.fetchone()[0])FAQs with embeddings: 50conn.rollback(), fix the problem and re-run. My notebook has a conn.rollback() cell for exactly this.Semantic search is one SQL query: embed the question with the same model as the FAQs, ORDER BY embedding <=> question_vector (nearest meaning first), and keep the best top_k. %s::vector converts the text "[...]" into a pgvector value.
def search_faq(question, top_k=3):
query_embedding = create_embedding(question)
embedding_string = "[" + ",".join(str(x) for x in query_embedding) + "]"
cur.execute("""
SELECT question, answer,
embedding <=> %s::vector AS distance
FROM faqs
WHERE embedding IS NOT NULL
ORDER BY embedding <=> %s::vector
LIMIT %s;""",
(embedding_string, embedding_string, top_k))
return cur.fetchall()
results = search_faq("What is a database?", top_k=3)
for result in results:
print("Question:", result[0])
print("Answer:", result[1])
print("Distance:", result[2])
print("=" * 70)Click each question to see the top 3 FAQs pgvector returned in my run.
search_faq("What is a database?") Exact match to a stored FAQ
search_faq("can you explain computer understanding text") No FAQ uses these words
search_faq("How can computers understand the meaning of text?") Paraphrase
search_faq("What can i stored") Typo kept exactly as I typed it
search_faq("today is suny?") Off-topic: nothing in the FAQs covers this
Number = cosine distance (smaller is closer). Longer bar = closer meaning.
What these show: exact questions match closely (0.234). Questions that share no words with an FAQ still find the right ones (0.297–0.326). A typo still lands on related FAQs, though less sharply (0.42+). The off-topic question's best match was 0.525, clearly worse than every relevant match. Distance is a useful signal for “nothing relevant found”; choose a cutoff by testing your own data.
| Letter | Step | Function |
|---|---|---|
| R | Retrieve | search_faq() finds the 3 FAQs closest in meaning |
| A | Augment | get_context() pastes them into the prompt as context |
| G | Generate | generate_answer(): Gemini writes a short answer using that context |
Gemini never searches the database itself. We give it the facts, and we can always show which FAQs it used.
def get_context(question):
results = search_faq(question, top_k=3)
context = ""
for result in results:
context += f"""
Question:{result[0]}
Answer: {result[1]}"""
return context
print(get_context(" What is pgvector"))Question:What is pgvector?
Answer: pgvector is a PostgreSQL extension that allows PostgreSQL to store and search vector data.
Question:Why use pgvector with PostgreSQL?
Answer: pgvector allows PostgreSQL to store normal data together with vector data and perform similarity searches.
Question:How can PostgreSQL be used in an AI application?
Answer: PostgreSQL can store application data and, with pgvector, can also store and search vector embeddings for AI applications.def generate_answer(question, context):
prompt = f"""
You are a helpful beginner-friendly AI assistant.
Use the information below to answer the user question.
Retrieved information:
{context}
User question:
{question}
Instructions:
1. Answer in simple English
2. Use the retrieved information
3. Do not invent information
4. Keep the answer suitable for a beginner
5. Give a clear and short explanation
"""
response = client.models.generate_content(model=TEXT_MODEL, contents=prompt)
return response.text
def ask_ai(question):
context = get_context(question) # step 1: retrieval
answer = generate_answer(question, context) # step 2: generation
return answerInstruction 3, “Do not invent information”, is the guardrail that keeps Gemini inside the retrieved context. The other instructions control tone and length.
None of the last two questions match an FAQ word for word, and the last one even has a typo. Semantic search still found the right facts.
def explain_rag_process(question):
print("STEP 1 — USER QUESTION:", question)
query_embedding = create_embedding(question)
print("STEP 2 — EMBEDDING CREATED. Number of values:", len(query_embedding))
print("STEP 3 — SEARCH POSTGRESQL")
for i, result in enumerate(search_faq(question, top_k=3)):
print(f"RESULT {i+1}:", result[0], "| distance:", result[2])
context = get_context(question)
print("STEP 4 — RAG CONTEXT:", context)
print("STEP 5 — SEND CONTEXT TO GEMINI")
answer = generate_answer(question, context)
print("STEP 6 — FINAL AI ANSWER:", answer)
explain_rag_process("How can computers understand the meaning of text?")while True:
question = input("\nAsk a question: ")
if question.lower() == "exit":
print("Goodbye!")
break
print("\nAI:")
print(ask_ai(question))
conn.close() # when you are finishedTry: What is pgvector? · How can I search by meaning? · Why do AI applications need databases? · and an off-topic question: the retrieved FAQs won't help, so does Gemini admit it, or drift?
| Idea | How |
|---|---|
| Add an index | CREATE INDEX ON faqs USING hnsw (embedding vector_cosine_ops); keeps search fast with thousands or millions of rows |
| Batch the API calls | Embed many texts per request instead of one at a time |
| Use real documents | Split PDFs, lecture notes or forum posts into chunks, with one row per chunk |
| Keep secrets out | Read the database password and API key from environment variables |
| Problem | What to do |
|---|---|
| Download does not start | On the EDB page, click Click here under “Your download should begin in a few seconds”. |
| Installer will not open or fails at start | Right-click the installer and choose Run as administrator. |
| Port 5432 already in use | Re-run the installer and choose another port (e.g. 5433). Use that port when connecting. |
| Seems frozen at “Initialising the database cluster” | Wait a few minutes. If it truly fails, read install-postgresql.log in %TEMP%. |
psql is not recognised | Use the full path to psql.exe or add the bin folder to PATH (Chapter 2). |
| Password authentication failed | Use the password from setup and the user postgres. Passwords are case-sensitive. |
| Forgot the postgres password | Temporarily set the method to trust for local connections in pg_hba.conf, restart the service, run ALTER USER postgres PASSWORD 'new_password';, then restore the file and restart again. |
| Error / symptom | What to check |
|---|---|
Cannot open include file: 'postgres.h' | PGROOT must point to your PostgreSQL folder, e.g. C:\Program Files\PostgreSQL\18. |
error C2196: case value '4' already used | Make sure you opened the x64 Native Tools Command Prompt. Clean with nmake /F Makefile.win clean, then rebuild. |
| Access is denied | Run the x64 Native Tools Command Prompt as Administrator and rerun the install command. |
'nmake' is not recognized | You are in a normal Command Prompt. Use the Visual Studio x64 Native Tools Command Prompt. |
fatal: destination path 'pgvector' already exists | A previous clone exists. Remove or rename the old folder, or clone into a clean location. |
| Error or symptom | Likely cause | Fix |
|---|---|---|
relation "developers" does not exist | Table is in another database | Connect to ai_workshop (editor and Python) |
| Password authentication failed | Wrong password | Use the password set at installation |
| Connection refused | PostgreSQL service not running | Start the PostgreSQL service |
ModuleNotFoundError: psycopg | Library not in this kernel | pip install "psycopg[binary]", then restart the kernel |
| Inserted row missing later | conn.commit() not called | Commit after INSERT / UPDATE / DELETE |
current transaction is aborted | An earlier statement failed | conn.rollback(), fix the SQL, re-run |
| Error or symptom | Likely cause | Fix |
|---|---|---|
| API key not valid / 401 | Wrong or incomplete key | Re-copy the key from Google AI Studio |
| Model not found / 404 | Placeholder or retired model name | Set TEXT_MODEL to an available model |
429 RESOURCE_EXHAUSTED | Free-tier rate limit | Wait a minute; avoid rapid loops |
| Could not open extension control file | pgvector not installed on the server | Install pgvector first (Chapter 2) |
| Expected 768 dimensions | output_dimensionality missing | Keep output_dimensionality=768 |
operator does not exist: vector <=> double precision[] | List sent as an array | Use %s::vector in the search |
COUNT(*) shows 6 or 10 (or 100) | Insert cell run twice | Re-run TRUNCATE, then insert again |
1. What is PostgreSQL?
2. What is SQL?
3. What is a table?
4. What is a primary key?
5. What is an embedding?
6. What is a vector?
7. What is pgvector?
8. What is semantic search?
9. What is RAG?
10. In pgvector, what does a smaller value from <=> mean?
| Technology | Role | In one line |
|---|---|---|
| PostgreSQL | Stores information | PostgreSQL = database |
| SQL | Works with PostgreSQL | SQL = talk to the database |
| Python | Connects all the parts | Python = connect everything |
| Gemini | Provides AI capabilities | Gemini = AI model |
| Embedding | Turns text into numbers | Text → embedding → vector |
| Vector | A list of numbers | [0.21, -0.14, 0.73, …] |
| pgvector | Stores and searches vectors | PostgreSQL + pgvector = vector search |
| Semantic search | Searches by meaning | Question → vector → similar vector |
| RAG | Retrieval + generation | Retrieve + Generate = RAG |
RAG in one line: find the right information first, then let AI write the answer from it.
Based on my notes, code and real outputs from the workshop “PostgreSQL for AI Applications” and its AI FAQ Assistant project. Toy-model numbers are invented for teaching, as marked. Illustrative examples are labelled.