Add AI search to existing application A developer demonstrated how to add semantic search to an existing Next.js todo app by storing OpenAI text-embedding-3-small vectors in a Postgres column via the pgvector extension, then ranking rows by distance in an order by clause. The approach replaces naive substring matching, which fails on queries like "groceries" against todos such as "Purchase some apples," with embedding-based similarity search. The full feature amounts to a vector column, an embedding call on row creation, and an embedding of the search term at query time. How to add semantic search to an existing app using an embedding model and pgvector Adding the most basic form of "AI search" to an existing app is three changes: - Add a vector column to the table you want to search.- When a row is created, send its text to an embedding model and store the numbers it returns in that column.- On search, embed the search term the same way and ask the database which rows are closest. I have a super simple todo app https://github.com/codegino/todo-list-with-ai/tree/starting-point written in Next.js connected to a Postgres database. Aside from the usual CRUD operations, this simple todo app has search. It was the naive one everybody writes first: lowercase the query, lowercase the title, check includes . Type "apple" and you get "Purchase some apples" ; type "laundry" and you get "Laundry day" . It looks like it works, as long as you already know the words in the title. Now search that same list for "groceries" . Nothing, even though "Purchase some apples" and "Buy bread and eggs" are sitting right there. Same for "cleaning" against "Laundry day" and "Do the dishes" . The search is not looking at what the todos mean ; it is looking at which letters they contain, and groceries is not a substring of anything. That is the gap people reach for "AI" to fill. The surprise is how little is involved. The part that does the matching is not a model at all. It is arithmetic in your database. If you would rather try the app than read about it, both versions are on GitHub: To run either branch on your machine you only need two things: a Postgres database with pgvector support and an OpenAI API key. Put them in .env as DATABASE URL and OPENAI API KEY , install the dependencies, and start the app. The diff between the two branches https://github.com/codegino/todo-list-with-ai/compare/starting-point...with-ai-search is, genuinely, the entire feature. Forget training, weights, and prompts for a minute. An embedding model is a function. Text goes in, a fixed-length list of numbers comes out: php "Purchase some apples" - 0.021, -0.043, 0.118, ... 1536 numbers "groceries" - 0.019, -0.038, 0.121, ... 1536 numbers Think of those numbers as a location on a map. On a real map, two places with similar coordinates are close to each other. Same idea here, except this map has 1536 directions instead of two. You cannot picture that, and you do not need to. Only the rule matters: text that means similar things ends up close together. 1536 is not a universal number. It is just the output width of the model I picked. Other models give you 768, 1024, 3072, and some let you ask for a shorter output. Whatever you pick becomes part of your schema, so treat it as a decision and not a constant. So "Purchase some apples" sits near "groceries" and far from "renew passport". Nobody programmed that. The model was trained on a very large amount of text, and that placement is the leftover shape of the language it read. Here is the part worth internalizing: Once the text is numbers, matching is just measuring a distance. Your database does that. The AI ended at the point where you got the numbers back. That is it. That is the whole trick. Everything below is plumbing. Postgres cannot store a list of 1536 floats usefully on its own, so we use pgvector https://github.com/pgvector/pgvector , an extension that adds a vector type and, crucially, distance operators that work in order by . -- migrations/002 embeddings.sql -- pgvector is not part of stock Postgres. On Supabase it is available -- but not enabled until you ask for it. create extension if not exists vector; -- 1536 is the native output width of OpenAI's text-embedding-3-small. alter table todos add column if not exists embedding vector 1536 ; Postgres is not the only place you can keep vectors, it just happens to be where my app already lived. I use Supabase, which is free and ships pgvector out of the box. If you run Postgres yourself, note that docker run postgres:17 does not include the extension; use pgvector/pgvector:pg17 instead. Where to store embeddings https://note.carlogino.com/ai/where-to-store-embeddings compares the other options in my notes. Setting that up is not really part of this post, so it lives in my notes instead: Either way you end up with a DATABASE URL and a database that understands vector . The rest of this post does not care which one you picked. Two reasons, and both come up in any real app: The entire "AI dependency" is one HTTP POST. No SDK required. js // src/lib/embeddings.ts const ENDPOINT = 'https://api.openai.com/v1/embeddings'; export const EMBEDDING MODEL = 'text-embedding-3-small'; export async function embed text: string : Promise