Hands-on tutorial · PostgreSQL · pgvector · Gemini · RAG

Build an AI FAQ Assistant with PostgreSQL, pgvector and Gemini

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.

You'll build a semantic-search + RAG FAQ assistantYou need a Windows PC (Mac/Linux links included), Python, a free Gemini API keyFormat 12 chapters, every step with code you can copy
Your progress: 0 of 12 chapters
Chapter 1

Why semantic search? What we will build

In this chapter
  • Why keyword search misses questions asked in different words
  • The finished system, step by step
  • The four tools and what each one does

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:

“I love dogs.”
vs
“I really like puppies.”

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.

Try it: keyword search on five documents

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.

Enable JavaScript to run this demo. The answer for “system” is 0 rows: no document contains that word.

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.

The finished system

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.

Phase 1 — Indexing (done once) Documents / FAQs Split into chunks Create embeddingsGemini, 768 numbers PostgreSQL+ pgvector search readsstored vectors Phase 2 — Answering (every question) Question Embed question Semantic searchORDER BY <=> Top-k FAQs Prompt =question + FAQs Gemini Answer
The RAG pipeline: prepare the knowledge once, then retrieve → augment → generate for every question.

The tools: four pieces, one database

ToolIts one job
PostgreSQLStores 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 modelWrites the final answer from the retrieved FAQs
PythonGlues 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.

DatabasePostgreSQLSQLPythonAIEmbeddingsVectorspgvectorSemantic searchRAG

Each step builds on the one before it. Colours: store data · connect code · understand meaning · answer questions.

Chapter 2

Set up your machine

In this chapter
  • Install PostgreSQL 18 with pgAdmin 4 on Windows
  • Build and install the pgvector extension on Windows
  • Set up Python, Jupyter and a Gemini API key

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.

2.1 Install PostgreSQL 18 on Windows

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.

SettingValue used
VersionPostgreSQL 18 (installer from EDB)
Installation directoryC:\Program Files\PostgreSQL\18
Data directoryC:\Program Files\PostgreSQL\18\data
ComponentsPostgreSQL Server, pgAdmin 4, Command Line Tools (Stack Builder unticked)
Superuserpostgres (password chosen during setup)
Port5432 (default)
LocaleDEFAULT
Windows service namepostgresql-x64-18

A. Download the installer

  1. 1
    Open the PostgreSQL download page

    Search for postgresql download and open postgresql.org/download.

  2. 2
    Choose Windows

    Under Select your operating system family, click Windows.

  3. 3
    Open the EDB installer link

    The Windows page explains that the interactive installer is certified by EDB. Click Download the installer, which takes you to enterprisedb.com.

  4. 4
    Pick version 18 for Windows x86-64

    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.

  5. 5
    Wait for the download

    If nothing downloads, click the Click here link on the “Your download should begin in a few seconds” page. Then open the downloaded file.

B. Run the setup wizard

Double-click the installer. If Windows asks for permission, choose Yes.

  1. 1
    Welcome screen

    Click Next >.

  2. 2
    Installation directory

    Keep C:\Program Files\PostgreSQL\18, then Next >.

  3. 3
    Select components

    Keep PostgreSQL Server, pgAdmin 4 and Command Line Tools (includes psql) ticked. Untick Stack Builder; it is only needed for extra drivers later.

  4. 4
    Data directory

    Keep the default C:\Program Files\PostgreSQL\18\data.

  5. 5
    Superuser password

    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.

  6. 6
    Port

    Keep 5432. Change it only if another program already uses that port.

  7. 7
    Advanced options

    Leave the locale as DEFAULT.

  8. 8
    Pre-installation summary

    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.

  9. 9
    Install

    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.

  10. 10
    Finish

    Click Finish on Completing the PostgreSQL Setup Wizard.

C. Open pgAdmin 4 and check the server

  1. 1
    Launch pgAdmin 4

    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.

  2. 2
    Connect

    In the left tree, expand Servers > PostgreSQL 18 > Databases > postgres. Enter your password if asked.

  3. 3
    Run a test query

    Open the Query Tool and run the query below. If the result shows PostgreSQL 18, the installation is complete.

SQL
SELECT version();

Optional: connect from the command line with psql

Command Prompt (Windows)
"C:\Program Files\PostgreSQL\18\bin\psql.exe" -U postgres -h localhost -p 5432
postgres=# SELECT version();
postgres=# \q

Optional: 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.

2.2 Install pgvector on Windows (build from source)

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.

PrerequisiteDetail
PostgreSQL 18Installed in C:\Program Files\PostgreSQL\18
Visual StudioWith the Desktop development with C++ workload
GitAvailable from Command Prompt
Administrator accessRecommended for the build/install command
  1. 1
    Download Visual Studio

    Open the official Microsoft Visual Studio website and download the installer.

  2. 2
    Run the Visual Studio Installer

    If User Account Control appears, confirm the publisher is Microsoft Corporation and continue.

  3. 3
    Install the C++ workload

    Select Desktop development with C++, click Install, and wait for it to finish. This provides the Microsoft C++ compiler and build tools.

  4. 4
    Open the right command prompt

    Search for x64 Native Tools Command Prompt for VS, right-click it and choose Run as administrator. A normal Command Prompt will not work.

  5. 5
    Run the six commands below one at a time

    Check that each one completes before running the next.

x64 Native Tools Command Prompt (as Administrator)
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
CommandWhat 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 pgvectorEnters the folder containing Makefile.win
nmake /F Makefile.winCompiles the extension with Visual Studio's compiler (MSVC)
nmake /F Makefile.win installCopies 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:

SQL
CREATE EXTENSION vector;

SELECT extname, extversion
FROM pg_extension
WHERE extname = 'vector';   -- a fresh install reports 0.8.7

Quick test. Create a tiny table with 3-number vectors and run a nearest-neighbour query (<-> is Euclidean/L2 distance):

SQL
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.

2.3 macOS and Linux

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.

2.4 Python, Jupyter and your Gemini API key

  1. 1
    Install Python 3 and Jupyter Notebook

    The workshop ran every step in a Jupyter notebook, cell by cell.

  2. 2
    Create a Gemini API key

    Create a free key in Google AI Studio.

  3. 3
    Save the key in a file, not in your notebook

    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.

  4. 4
    Install the libraries

    Run this in a notebook cell. The ! runs it as a shell command, and -q hides the long install log.

Jupyter cell
!pip install -q -U google-genai "psycopg[binary]" psycopg2-binary pandas
LibraryUsed for
google-genaiGoogle'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-binarypsycopg2, the driver used in the main FAQ Assistant notebook (Chapter 10)
pandasShows data as a neat table
Why two PostgreSQL drivers?
The workshop's warm-up slides used psycopg 3 and the main project notebook used psycopg2. Both work. I kept each chapter's code exactly as it was taught and run, so you can follow along without surprises.
Chapter 3

PostgreSQL and SQL fundamentals

In this chapter
  • What a database, table, row, column and primary key are
  • Create, read, update and delete data (CRUD)
  • Why ORDER BY is the key to semantic search later

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.

Anatomy of a table

idnamedepartmentage
1RaviCSE21
2PriyaECE22
3ArunCSE20
4MeenaIT21
TermMeaning
TableA structure that organises data in rows and columns
RowOne complete record, e.g. 1 | Ravi | CSE | 21
ColumnOne kind of information for every row: id, name, …
Primary keyA value that uniquely identifies each row (id). No two rows share an id.

Lab: create your workshop database

In pgAdmin or DBeaver, check the server is reachable, then create a database:

SQL
SELECT version();
CREATE DATABASE ai_workshop;
Common mistake
After creating ai_workshop, switch your connection to it in pgAdmin/DBeaver. Otherwise new tables land in the default postgres database.

Create the first table

SQL
CREATE TABLE developers (
    id         SERIAL PRIMARY KEY,
    name       VARCHAR(100),
    department VARCHAR(50),
    age        INTEGER
);
PartMeaning
developersName of the table
idIdentifier of each developer
SERIALPostgreSQL fills 1, 2, 3 … automatically. You never type ids yourself.
PRIMARY KEYEach id is unique, so there is exactly one row per id
VARCHAR(100)Text up to 100 characters
INTEGERWhole numbers (used for age)

Insert and view data

SQL
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.

Filter with WHERE, sort with ORDER BY

SQL
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)
Remember ORDER BY!
Semantic search later is simply ORDER BY distance: the closest meaning comes first.

Update and delete

SQL
UPDATE developers SET age = 22 WHERE id = 1;  -- Ravi's age
DELETE FROM developers WHERE id = 4;           -- remove Meena
SELECT * FROM developers;                      -- check
Always use WHERE with UPDATE and DELETE
Without it, UPDATE 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.
CCreateINSERT
RReadSELECT
UUpdateUPDATE
DDeleteDELETE

That is all the SQL this project needs: CREATE TABLE, INSERT, SELECT, WHERE, ORDER BY, UPDATE, DELETE.

Chapter 4

Python talks to PostgreSQL

In this chapter
  • How a Python program sends SQL to PostgreSQL
  • Read and insert rows from Python
  • Why commit() and %s placeholders matter

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.

Install, import and connect (psycopg 3)

Python
import psycopg

conn = psycopg.connect(
    host="localhost",
    dbname="ai_workshop",
    user="postgres",
    password="YOUR_POSTGRES_PASSWORD"
)
print("Connected!")
ArgumentMeaning
hostPostgreSQL runs on this same computer
dbnameThe database we created
userThe PostgreSQL user account
passwordSet during installation

If Connected! prints, Python can reach the database.

Read data using Python

Python
with conn.cursor() as cur:
    cur.execute("SELECT * FROM developers")
    rows = cur.fetchall()

for row in rows:
    print(row)
Expected output (after the UPDATE and DELETE in Chapter 3)
(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.

Insert data using Python

Python
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!")
conn.commit() saves for good
Reads need no commit. INSERT, UPDATE and DELETE do; otherwise the change vanishes when the connection closes.
Why %s placeholders?
Values travel separately from the SQL, and the library inserts them safely and handles quotes. This is a parameterised query, and it protects you from SQL injection.
Chapter 5

AI basics and the Gemini API

In this chapter
  • How AI, machine learning and generative AI relate
  • What an API is, with a restaurant analogy
  • Connect to Gemini and generate your first text
Artificial IntelligenceComputers doing tasks that normally need human-like intelligence: understanding language, recognising images.
Machine LearningA part of AI where computers learn patterns from examples instead of fixed rules.
Generative AIAI that creates new content such as text, images or code. Example: Google Gemini.

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).

Connect to Gemini

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.

Python
from google import genai

api_key = open("api_key.txt").read().strip()
client = genai.Client(api_key=api_key)
print("Gemini client created successfully")

Choose your models and test

The project uses two Gemini models with different jobs. Keep the names in variables so you only change them in one place.

Python
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)
Output from my run
PostgreSQL is a powerful, open-source relational database management system known for its reliability, advanced features, and ability to handle complex data at scale.
Model names change
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.

Chapter 6

Embeddings: turning meaning into numbers

In this chapter
  • What a vector and an embedding are
  • How a model builds an embedding, worked by hand with a toy model
  • How real models learn the numbers, and why 768 positions have no names

Computers compare numbers, not meanings. Which two of these are most similar?

A“I love dogs.”
B“I like puppies.”
C“The stock market fell today.”

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.

What is a vector?

A vector is simply a list of numbers. The number of values is its number of dimensions.

ExampleVectorDimensions
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.

How does a model compute an embedding? (simplified)

SentenceSplit into tokensLook up a vector per tokenMix with contextCombine into one vectorNormalise length

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.

Toy model: numbers invented for teaching
Real embeddings are too big to calculate by hand, so we shrink the idea to 3 named numbers: animal, feeling, finance (0 = none of that idea, 1 = strongly that idea). Gemini does not use these values; it uses 768 unnamed numbers learned from huge amounts of text. The steps are the same: look up, average, normalise.

Step 1: give each word its numbers (the lookup table)

WordanimalfeelingfinanceWhy these values?
I0.10.10.0A plain word with almost no meaning of its own
love0.10.70.0Strongly a feeling
dogs1.00.10.0Strongly an animal

Step 2: combine the words by averaging

animalfeelingfinance
Sum0.1 + 0.1 + 1.0 = 1.20.1 + 0.7 + 0.1 = 0.90.0
Average (÷ 3)0.40.30.0

Sentence A before normalising = [0.4, 0.3, 0.0]: mostly animal, some feeling, no money.

Step 3: normalise to length 1

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.

Worked by hand
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]).

Try it yourself: toy embedding calculator

Type sentences using the toy words and watch every step: lookup, average, length, normalise. Chapter 7 explains the similarity numbers it also shows.

Presets:

Words in the toy lookup table: I, love, dogs, like, puppies, The, stock, market, fell, today.

Enable JavaScript to use the calculator. The worked tables on this page show the same arithmetic.

Where do the real numbers come from?

Decided by people
How many numbers (e.g. 768: you choose this with output_dimensionality=768) and what the model practises on during training.
Learned by the model
Every number for every word and sentence, and what each position ends up meaning. Nobody assigns “position 57 = finance”.
  1. Start random. Before training, “dog” and “puppy” are no more alike than “dog” and “stock”.
  2. Practise fill-in-the-blank, millions of times: “I took my ___ for a walk in the park.” A wrong guess nudges the numbers a little. Words that fit the same blanks get nudged the same way, so their lists slowly become similar.
  3. Pull together, push apart (contrastive learning): “How do I reset my password?” and “I forgot my password, what should I do?” are pulled closer, while an unrelated sentence is pushed away. As a result, similar meanings get nearby vectors.

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”.

Real embeddings with gemini-embedding-001

This is the embedding function from the workshop slides. It asks for 768 numbers per text.

Python
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].values

embed_content can embed many texts, so it returns a list; [0] takes the first. .values is a plain Python list of 768 decimal numbers.

Python
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]))    # 768

Your actual numbers will differ from anything shown here. What matters is the length: 768.

Chapter 7

Measuring similarity: cosine distance

In this chapter
  • The dot product, vector length and cosine similarity, in four small steps
  • Why pgvector's <=> operator returns cosine distance
  • The other distance operators

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.

Step 1: the dot product (multiply matching positions, then add)

Worked by hand
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

Step 2: the length (magnitude) of each vector

|A| = √(0.64 + 0.36 + 0) = 1.0 and |B| = 1.0. Because we normalised, every length is 1.

Step 3: cosine similarity

cos θ = (A · B) ÷ (|A| × |B|) = 0.96 ÷ (1.0 × 1.0) = 0.96
cos θAngleMeaning
10°Same direction: same meaning
090°Unrelated
−1180°Opposite direction

Step 4: cosine distance (what pgvector uses)

cosine distance = 1 − cos θ  →  A ↔ B: 1 − 0.96 = 0.04

In PostgreSQL, embedding <=> query returns this value: 0 = same meaning, 1 = unrelated, 2 = opposite. Smaller means closer, so you ORDER BY it smallest first.

PairDot product (= cos θ, lengths are 1)Cosine distanceVerdict
A ↔ B0.8×0.6 + 0.6×0.8 + 0×0 = 0.960.04Very similar
A ↔ C0.8×0 + 0.6×0.28 + 0×0.96 = 0.1680.832Different
B ↔ C0.6×0 + 0.8×0.28 + 0×0.96 = 0.2240.776Different

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.

Other distance operators in pgvector

OperatorNameIdea
<=>Cosine distanceAngle between arrows (our choice)
<->Euclidean (L2) distanceStraight-line gap between the two points. A ↔ B = √(0.04 + 0.04) ≈ 0.28
<#>Negative inner productMinus the dot product

For length-1 vectors all three give the same ranking. Cosine is the usual choice for text.

Chapter 8

pgvector lab: store and search embeddings

In this chapter
  • Enable pgvector and create a VECTOR(768) column
  • Store text and its embedding in the same row
  • Write semantic search as one SQL query

pgvector adds a VECTOR type next to TEXT, INTEGER, DATE and BOOLEAN, so the text and its meaning live in the same row.

idcontent (TEXT)embedding (VECTOR(768))
1PostgreSQL is an open-source relational database.[0.021, -0.014, 0.073, … 768 values]

Illustrative values. Your real numbers will differ.

Enable pgvector and create the documents table

Run this in pgAdmin, connected to ai_workshop:

SQL
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)
);
768 must match
VECTOR(768) must match output_dimensionality=768. A vector of any other size is rejected.
Enable is not install
CREATE EXTENSION only enables pgvector. If it isn't installed on the server you get “could not open extension control file”. See Chapter 2.

Store five documents

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.

Python
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])      # 5

TRUNCATE … 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.

Semantic search in SQL: it is just ORDER BY

SQL
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.

Don't forget ::vector
psycopg sends a Python list as an array, but <=> 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.

A reusable search function

Python
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.

Predict first, then run

Python
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}")
QuestionExpected 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.

What about a million documents?

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:

SQL
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops);

You don't need it for small tables like these; just know it exists.

Chapter 9

From search to RAG

In this chapter
  • Why an AI model alone gets your own facts wrong
  • The open-book exam and librarian/writer analogies
  • One question walked through all four RAG steps

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?”

It doesn't know
Our college circular was never part of what Gemini learned.
Its knowledge is old
It learned up to a training date. This semester's date is newer.
It may guess
It can answer confidently and still be wrong. This is called hallucination.

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.

Analogy: closed-book vs open-book exam

Closed-book exam = Gemini alone
Answers only from memory. If it has forgotten something, it may guess.
Open-book exam = Gemini with RAG
1. Find the right page. 2. Read it and keep it open next to the question. 3. Write the answer in your own words.
In the open-book examIn our systemDone by
The textbookOur table of FAQsPostgreSQL
Finding the right pageSemantic search by meaningpgvector (<=>)
Keeping the page next to the questionPutting the found FAQs into the promptPython
Writing the answerGenerating the replyGemini

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.

Follow one question through the system

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.

  1. 1
    Embed the question

    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.

  2. 2
    Search: find the right “pages”

    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.

  3. 3
    Build the prompt (augment)

    Add the found FAQs to the prompt. You aren't changing Gemini at all, only giving it a better prompt.

  4. 4
    Generate

    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.

The augmented prompt (step 3)
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?
RRetrievalSteps 1–2: find the relevant FAQs
AAugmentedStep 3: add them to the prompt
GGenerationStep 4: Gemini writes the answer

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 vs Gemini with RAG

Gemini aloneGemini with RAG
Knows our college's dates?NoYes, from our table
Up to date?Only up to its training dateAs fresh as our table
Can show where the answer came from?NoYes: the retrieved FAQs
Risk of made-up answersHigherLower: told to use only the given information

Updating the assistant needs no retraining

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:

Python
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()
Recompute the embedding too
When the text changes, also update its embedding. Otherwise the search still uses the old meaning.

Think first: five questions

Answer in your head, then click to reveal.

Think first Does Gemini learn our FAQs permanently?
No. Gemini forgets after every answer. The FAQs travel inside every prompt; Gemini reads them, answers and keeps nothing. So change the table and the very next answer changes, with nothing inside Gemini to update.
Think first Why not paste all 50 FAQs into the prompt every time?
That's fine for 50 but impossible for 50,000. Size limit: a prompt can hold only so much text. Cost and time: longer prompts cost more and take longer for every question. Noise: thousands of unrelated FAQs distract the model. Retrieval sends only the few that matter, the way an open-book student opens one chapter, not the whole library.
Think first What if the search returns the wrong FAQs?
Wrong information in means a wrong answer out. There are three safeguards. A clear instruction: “Use ONLY this information. If the answer is not there, say I don't know.” Good documents: short, focused FAQs give clean embeddings. Check the distance: a large distance means a weak match that can be ignored.
Think first Is RAG a new AI model?
No. RAG is a method, not a model. PostgreSQL + pgvector finds, Python connects and Gemini writes. No new model is trained; swap the database or the AI model and the method stays the same.
Think first If none of the FAQs fit the question, what should Gemini say?
“I don't know”, and that is a good answer. For example, for “Is the canteen open on Sunday?” the closest FAQs had large distances (0.64 and 0.71 in the illustration). An honest “I don't know” is far better than a confident wrong answer, which is why the prompt tells Gemini to say it.
Chapter 10

Project: build the AI FAQ Assistant

In this chapter
  • Load 50 FAQs into PostgreSQL with a VECTOR(768) column
  • Embed every FAQ with Gemini and search by meaning
  • Add Gemini on top to get a complete RAG assistant

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.

Part 1 Set up and load 50 FAQsPart 2 Turn every FAQ into a vectorPart 3 Search by meaningPart 4 Add Gemini: RAG
Before you start
PostgreSQL must be running, pgvector must be installed on the server (Chapter 2), and 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.

Part 1: set up PostgreSQL and load the FAQs

Cells 1–4: install, import, Gemini client, models
!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"
Cell 5: connect
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!")
ObjectWhat it isWhat we use it for
connThe connection: an open line to the servercommit() saves changes · rollback() undoes after an error · close() ends it
curThe cursor: sends SQL and brings results backexecute(...) 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.

Cell 6: enable pgvector and create the table
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).

Show all 50 FAQs (copy into a notebook cell)
Cell 7: faq_data
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."
    }
]
Cell 8: preview and insert
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])
Output from my run
number of faqs: 50
Number of faqs in postgresql: 50
Run the INSERT cell only once
Running it again adds 50 duplicate rows (ids 51–100) that will never get embeddings. To start over: cur.execute("TRUNCATE faqs RESTART IDENTITY;"); conn.commit()

Part 2: turn every FAQ into a vector

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.

Cell 9: embedding function
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])
Output from my run
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.

Cell 10: embed all 50 FAQs
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.

Cell 11: check
cur.execute("SELECT COUNT(*) FROM faqs WHERE embedding IS NOT NULL;")
print("FAQs with embeddings:", cur.fetchone()[0])
Output from my run
FAQs with embeddings: 50
Got “current transaction is aborted”?
An earlier statement failed. Run conn.rollback(), fix the problem and re-run. My notebook has a conn.rollback() cell for exactly this.

Part 3: search by meaning with pgvector

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.

Cell 12: semantic search
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)

Explore my real search results

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

1. What is a database?0.234
2. What is a table in a database?0.308
3. What is SQL?0.334

search_faq("can you explain computer understanding text") No FAQ uses these words

1. How does text become an embedding?0.297
2. What is Natural Language Processing?0.307
3. What is text generation?0.326

search_faq("How can computers understand the meaning of text?") Paraphrase

1. What is Natural Language Processing?0.299
2. What is semantic search?0.316
3. How does text become an embedding?0.321

search_faq("What can i stored") Typo kept exactly as I typed it

1. What is data?0.422
2. What is a Python dictionary?0.454
3. What is SQL?0.455

search_faq("today is suny?") Off-topic: nothing in the FAQs covers this

1. What is an API key?0.525
2. What is RAG?0.529
3. What is Gemini?0.533

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.

Part 4: add Gemini (Retrieval-Augmented Generation)

LetterStepFunction
RRetrievesearch_faq() finds the 3 FAQs closest in meaning
AAugmentget_context() pastes them into the prompt as context
GGenerategenerate_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.

Cell 13: build the context
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"))
Output from my run
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.
Cell 14: generate and the complete assistant
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 answer

Instruction 3, “Do not invent information”, is the guardrail that keeps Gemini inside the retrieved context. The other instructions control tone and length.

Real answers from my run

What is PostgreSQL?
PostgreSQL is a free and open-source system used to store, organize, and manage your data.
How can I store information?
Based on the information provided, you can store information using:
• A computer: Computers can store, process, and analyze information (data).
• A database: This is an organized collection of information that a computer system can store, search, and manage.
• A table: This is a structure inside a database that stores information using rows and columns.
How can computer understant text
Computers understand text through a field of AI called Natural Language Processing (NLP), which helps computers work with human language. To do this, an embedding model takes the text as input and converts it into a list of numbers that represents information about the text.

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.

See every RAG step (great for demos)

Cell 15: show the whole process (condensed from my notebook)
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?")

Chat with your assistant

Cell 16: interactive loop (type exit to stop)
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 finished

Try: 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?

Taking it beyond 50 FAQs

IdeaHow
Add an indexCREATE INDEX ON faqs USING hnsw (embedding vector_cosine_ops); keeps search fast with thousands or millions of rows
Batch the API callsEmbed many texts per request instead of one at a time
Use real documentsSplit PDFs, lecture notes or forum posts into chunks, with one row per chunk
Keep secrets outRead the database password and API key from environment variables
Chapter 11

Troubleshooting

In this chapter
  • Fix installation, Python, Gemini and pgvector errors quickly

Installing PostgreSQL on Windows

ProblemWhat to do
Download does not startOn the EDB page, click Click here under “Your download should begin in a few seconds”.
Installer will not open or fails at startRight-click the installer and choose Run as administrator.
Port 5432 already in useRe-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 recognisedUse the full path to psql.exe or add the bin folder to PATH (Chapter 2).
Password authentication failedUse the password from setup and the user postgres. Passwords are case-sensitive.
Forgot the postgres passwordTemporarily 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.

Building pgvector on Windows

Error / symptomWhat 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 usedMake sure you opened the x64 Native Tools Command Prompt. Clean with nmake /F Makefile.win clean, then rebuild.
Access is deniedRun the x64 Native Tools Command Prompt as Administrator and rerun the install command.
'nmake' is not recognizedYou are in a normal Command Prompt. Use the Visual Studio x64 Native Tools Command Prompt.
fatal: destination path 'pgvector' already existsA previous clone exists. Remove or rename the old folder, or clone into a clean location.

Database and Python

Error or symptomLikely causeFix
relation "developers" does not existTable is in another databaseConnect to ai_workshop (editor and Python)
Password authentication failedWrong passwordUse the password set at installation
Connection refusedPostgreSQL service not runningStart the PostgreSQL service
ModuleNotFoundError: psycopgLibrary not in this kernelpip install "psycopg[binary]", then restart the kernel
Inserted row missing laterconn.commit() not calledCommit after INSERT / UPDATE / DELETE
current transaction is abortedAn earlier statement failedconn.rollback(), fix the SQL, re-run

AI, pgvector and search

Error or symptomLikely causeFix
API key not valid / 401Wrong or incomplete keyRe-copy the key from Google AI Studio
Model not found / 404Placeholder or retired model nameSet TEXT_MODEL to an available model
429 RESOURCE_EXHAUSTEDFree-tier rate limitWait a minute; avoid rapid loops
Could not open extension control filepgvector not installed on the serverInstall pgvector first (Chapter 2)
Expected 768 dimensionsoutput_dimensionality missingKeep output_dimensionality=768
operator does not exist: vector <=> double precision[]List sent as an arrayUse %s::vector in the search
COUNT(*) shows 6 or 10 (or 100)Insert cell run twiceRe-run TRUNCATE, then insert again
Chapter 12

Quiz, checklist and summary

In this chapter
  • Test yourself
  • Confirm you completed every hands-on step
  • One table that sums up every technology

Quick quiz

1. What is PostgreSQL?

Show answerA relational database management system

2. What is SQL?

Show answerA language used to work with relational databases

3. What is a table?

Show answerA structure that stores data in rows and columns

4. What is a primary key?

Show answerA value that uniquely identifies a row

5. What is an embedding?

Show answerA numerical representation of information such as text

6. What is a vector?

Show answerA list of numbers

7. What is pgvector?

Show answerA PostgreSQL extension for storing and searching vector data

8. What is semantic search?

Show answerSearching based on meaning rather than only exact words

9. What is RAG?

Show answerRetrieval-Augmented Generation: retrieve relevant information and use it to generate an AI answer

10. In pgvector, what does a smaller value from <=> mean?

Show answerCloser meaning

Hands-on checklist

What each technology does

TechnologyRoleIn one line
PostgreSQLStores informationPostgreSQL = database
SQLWorks with PostgreSQLSQL = talk to the database
PythonConnects all the partsPython = connect everything
GeminiProvides AI capabilitiesGemini = AI model
EmbeddingTurns text into numbersText → embedding → vector
VectorA list of numbers[0.21, -0.14, 0.73, …]
pgvectorStores and searches vectorsPostgreSQL + pgvector = vector search
Semantic searchSearches by meaningQuestion → vector → similar vector
RAGRetrieval + generationRetrieve + Generate = RAG

RAG in one line: find the right information first, then let AI write the answer from it.

Official references

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.