Learning · · 5 min read
Building a Personal Knowledge Base with Gemini Embeddings in Python
A quick guide to building a searchable personal knowledge base with Gemini embeddings, Supabase, and a simple Python CLI.
When I started looking for a place to keep the snippets, notes, and tiny experiments that I collect every day, I realized a simple folder structure was getting messy. That's why I built a personal knowledge base with Gemini embeddings in Python – a tiny system that turns any piece of text into a vector, stores it in Supabase, and lets me retrieve the most relevant notes with a single query. In this post I walk through the exact steps I followed, the tools I used, and the hiccups I ran into, so you can try it yourself without spending any money.
Why a personal knowledge base with Gemini matters
A knowledge base is only useful if you can find what you need quickly. Traditional keyword search works, but it often misses synonyms or paraphrases. Gemini's embedding model converts sentences into dense vectors that capture meaning, so a search for “how to clean data in pandas” will also pull up notes titled “removing NaNs and duplicates”. By coupling those vectors with Supabase’s row‑level security, I get a private, searchable store that scales from a handful of notes to thousands without writing any custom index code.
Setting up the environment
I keep my development stack lightweight: VS Code for editing, Claude Code for quick code suggestions, and the free tier of Supabase for storage. First I created a new Python virtual environment, installed the Gemini SDK and the Supabase client, and added my API keys to a .env file. If you haven't seen my step‑by‑step guide to a Python AI environment, you might want to check it out for details on configuring Claude and VS Code. my guide to setting up a Python AI development environment
python -m venv venv
source venv/bin/activate
pip install "google‑gemini" supabase-py python-dotenv
Getting Gemini embeddings
The Gemini embedding endpoint expects plain text and returns a 768‑dimensional vector. I wrote a tiny helper that reads a markdown file, strips front‑matter, and calls the API. The function returns a list of floats that I can store directly in Supabase. I also added a simple retry wrapper to respect rate limits, and kept the API key out of source code by loading it from the .env file.
import os, json
from google_gemini import GeminiClient
from dotenv import load_dotenv
load_dotenv()
client = GeminiClient(api_key=os.getenv('GEMINI_API_KEY'))
def get_embedding(text: str) -> list[float]:
response = client.embeddings.create(model='gemini-1.0-embed', input=text)
return response['embedding']
# Example usage
with open('notes/example.md') as f:
content = f.read()
emb = get_embedding(content)
print('Embedding length:', len(emb))
Storing embeddings in Supabase
Supabase lets you create a table with a vector column type, so I made a table called notes with id, title, content, and embedding. The embedding column is of type real[] which holds the float list. I also enabled Row Level Security so only my API key can insert or query. Inserting a note is just a single INSERT call with the vector; Supabase handles the array conversion automatically. For a quick look at how I added vector search to a Python chatbot, see my post on that topic.
create table notes (
id bigint generated always as identity primary key,
title text not null,
content text not null,
embedding real[] not null
);
-- enable RLS
alter table notes enable row level security;
create policy "allow_owner" on notes using (auth.role() = 'authenticated');
from supabase import create_client
import os, json
supabase = create_client(os.getenv('SUPABASE_URL'), os.getenv('SUPABASE_ANON_KEY'))
def insert_note(title: str, content: str):
emb = get_embedding(content)
data = {
'title': title,
'content': content,
'embedding': emb
}
supabase.table('notes').insert(data).execute()
# Example
insert_note('Cleaning pandas data', open('notes/example.md').read())
Querying your knowledge base
To find the most relevant note I calculate the cosine similarity between the query vector and every stored embedding. Supabase doesn't have a built‑in similarity function, but you can use the pgvector extension on a self‑hosted Postgres or, for a quick prototype, pull the rows into Python and compute similarity locally. The snippet below shows the latter approach – it fetches all embeddings, computes scores, and returns the top three matches.
import numpy as np
def cosine_similarity(a, b):
a = np.array(a)
b = np.array(b)
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
def search(query: str, top_k: int = 3):
q_emb = get_embedding(query)
rows = supabase.table('notes').select('id, title, content, embedding').execute().data
scores = []
for row in rows:
score = cosine_similarity(q_emb, row['embedding'])
scores.append((score, row))
scores.sort(reverse=True, key=lambda x: x[0])
return [row for _, row in scores[:top_k]]
results = search('how to remove nulls in pandas')
for r in results:
print(r['title'], '-', r['content'][:100])
Putting it together in a simple CLI
Finally I wrapped the steps into a small command‑line tool called kbsearch. You run `python kbsearch.py "your question"` and it prints the title and a short excerpt of the best matches. The script reads the query, gets its embedding, runs the similarity function, and formats the output. Because the whole pipeline lives in a single file, you can drop it into any project or schedule it with a cron job to tag new notes automatically.
import sys
if __name__ == '__main__':
if len(sys.argv) < 2:
print('Usage: python kbsearch.py "your question"')
sys.exit(1)
query = ' '.join(sys.argv[1:])
matches = search(query)
for m in matches:
print(f"\nTitle: {m['title']}\nExcerpt: {m['content'][:200]}...\n")
- Install the Python dependencies (Gemini SDK, supabase-py, python-dotenv).
- Create a .env file with your GEMINI_API_KEY, SUPABASE_URL and SUPABASE_ANON_KEY.
- Run the SQL snippet to create the notes table in Supabase.
- Add a few notes using the insert_note helper or directly via the dashboard.
- Run `python kbsearch.py "your search phrase"` to see results.
What I learned and next steps
Building this tiny knowledge base taught me that a lot of the heavy lifting—vector generation and storage—can be done with free services if you keep the architecture simple. I also realized I was spending too much time copying code; using Claude Code to generate boilerplate saved me minutes each day. Next I plan to explore pgvector on Supabase Edge Functions for server‑side similarity, add tagging support, and maybe turn the CLI into a tiny web UI. For more thoughts on staying in control of AI tools, see my post on using AI without outsourcing my thinking.
If you like this experiment, you can see the full source code and other side projects on my projects page. /projects
If you want to keep reading, I also wrote about Adding Sentiment Analysis to My Python Chatbot with Gemini.
Frequently asked questions
What are Gemini embeddings and how are they different from other embeddings?
Gemini embeddings are dense vector representations generated by Google's Gemini model. They capture semantic meaning of text, so similar sentences have vectors that are close in cosine distance, unlike simple keyword vectors which only reflect exact words.
Do I need a paid Gemini account to use embeddings?
No. Gemini offers a free tier that includes a limited number of embedding calls per month, which is enough for a personal knowledge base with a few hundred notes.
Can I store embeddings in Supabase for free?
Yes. Supabase’s free tier allows you to create tables with array columns (real[]) and store the float vectors there. As long as you stay within the storage and row limits, you won’t incur any cost.
How can I query similar documents using the stored embeddings?
You retrieve the embeddings, compute cosine similarity with the query vector in Python, and sort the results. For larger datasets you can enable the pgvector extension on a self‑hosted Postgres or use Supabase Edge Functions to run the similarity calculation server‑side.