{"course":{"slug":"building-apps-with-ai","title":"Building Apps with AI","url":"https://addaly.com/learn/building-apps-with-ai","level":"beginner","track":"build"},"lesson":{"slug":"giving-the-model-your-own-data","title":"Giving the model your own data","url":"https://addaly.com/learn/building-apps-with-ai/giving-the-model-your-own-data","position":65,"of":87,"minutes":10,"answer":"The model knows nothing about your database; the app fetches the rows this user may see, puts them in the prompt, and asks.","keyIdea":"The model knows nothing about your database; the app fetches the rows this user may see, puts them in the prompt, and asks; and an embedding is how it finds the rows that are about the question rather than the ones that share its words."},"body":{"format":"markdown","text":"## What the model does not know\n\nA hosted model has read a great deal of the public internet and none of your database. \"Make the assistant know about my customers\" cannot mean teaching it; training on your data is expensive, slow, and produces a model that recites approximations. What it means in practice is simpler: at the moment of the question, your code fetches the relevant rows and puts them in the prompt, and the model reads them the way it would read anything else pasted into a message.\n\nThat is the whole technique. The engineering is in the word *relevant*, and in a boundary that must never move.\n\n## Level one: the rows you already know\n\nMost questions are about something with an id. \"Summarise this customer's history\": the customer id is in the URL, the session says which user is asking, and a query returns the twenty rows that matter:\n\n```typescript\nconst rows = await db.select().from(bookings)\n  .where(and(eq(bookings.userId, session.userId), eq(bookings.customerId, customerId)))\n  .orderBy(desc(bookings.startsAt)).limit(20);\n```\n\nFormat them as compact text, one line per row, the fields that matter, and put that in the user message beneath the question. This is retrieval without a search, it covers more features than people expect, and it is safe by construction because the query is scoped the same way every query in the app is scoped.\n\n## Level two: search by words\n\n\"Which of my notes mention the roof?\" needs a search. Postgres has full-text search built in (`to_tsvector` and `to_tsquery`) and SQLite has FTS5. Both find rows containing the words, ranked, in milliseconds, with no new service. Fetch the top ten, paste them, ask. For a small app this is often the right stopping point.\n\nWord search fails when the words differ: \"roof\" does not find \"ceiling leak\", and \"cancelled\" does not find \"called it off\". That is the gap the next level exists for.\n\n## Level three: search by meaning\n\nAn embedding model turns a piece of text into a list of numbers, typically several hundred to a few thousand of them, arranged so that texts with similar meaning produce lists that point in similar directions. \"Roof repair\" and \"ceiling leak\" land close together; \"roof repair\" and \"invoice overdue\" do not.\n\nSimilarity is measured by the angle between the two lists, called cosine similarity: 1 for identical direction, 0 for unrelated. The search is: embed the question, compare with the stored embedding of every note, take the closest ten.\n\n```sql\n-- pgvector, an extension available on Neon and Supabase free tiers\nSELECT id, body, 1 - (embedding <=> $1) AS similarity\nFROM notes\nWHERE user_id = $2                    -- the boundary, before ranking\nORDER BY embedding <=> $1\nLIMIT 10;\n```\n\nThe free path is complete. `pgvector` stores and compares the vectors inside Postgres. The embeddings themselves come from a small model that runs on your own machine through Ollama, or in the browser or on the server through `transformers.js`, at zero cost; hosted embedding endpoints are cheap and give somewhat better results. A few thousand notes embed in minutes and re-embed only when they change.\n\nThe dedicated treatment of embeddings (what the numbers are, why the directions mean anything, where they fail) belongs to the machine-learning course. For the app builder, the working model is: a vector is a position in a space where nearby means similar, and a similarity search returns neighbours.\n\n## The boundary that must not move\n\nLook at the `WHERE user_id = $2` above. It runs before the ranking, not after. Filtering after (search everything, then drop the rows that are not this user's) means other users' rows were considered, and one of them was the closest match at some point. The question in this lesson's check is that mistake, and it is common because the generated version builds the search first and adds the user later as an afterthought.\n\nThe rule from the injection lesson: the model only sees what this session may see. Retrieval is where that is enforced, in the query, and it is not enforceable anywhere later.\n\n## How much to paste\n\nA context window is large and not infinite, and every pasted token is paid for and slows the answer. Ten to twenty rows, trimmed to the fields that matter, is the usual sweet spot. Pasting the whole table because it fits is a habit that works at a hundred rows and produces a $2 call and a confused answer at ten thousand.\n\n## The model still answers from itself\n\nGiven ten relevant rows and a question, the model answers from the rows most of the time, and sometimes from what it remembers about the world, and it does not mark which. Tell it in the prompt to answer only from the provided records and to say when they do not contain the answer. Then test with a question the rows cannot answer, and see whether it says so or invents. That test is the difference between a feature and a liability, and it is the subject of the next lesson.\n\n> Figure: Where the user filter goes in a similarity search. WHERE user_id, then rank: The database narrows to this session's rows first; The nearest neighbours are chosen from those rows only; Another user's note cannot be the closest match at any point; The same scoping as every other query in the app. Rank everything, then drop: Every row in the table is compared with the question; Other people's rows were considered and ranked; The ten best may all belong to somebody else, leaving nothing; Written this way because the search was built before the user was. Both return ten rows belonging to this user. Only one of them never considered anybody else's, and only one of them still holds when the ranking changes."},"check":{"question":"A 'ask about my bookings' feature embeds every bookings row, searches by similarity to the question, and puts the top ten in the prompt. A user asks 'show me Priya's bookings' and gets another customer's rows. Where is the fault?","options":[{"text":"The embedding model is not accurate enough and needs to be replaced with a larger one.","correct":false,"tempting":"The search found rows about Priya correctly; the fault is that rows belonging to other users were in the searchable set at all."},{"text":"The similarity search ran over every user's rows instead of being filtered to this session's user before ranking, so retrieval itself crossed the boundary.","correct":true},{"text":"The prompt failed to tell the model not to reveal other customers' data, and adding a clear instruction to answer only about the current user's own bookings would fix it.","correct":false,"tempting":"By the time the rows are in the prompt the leak has happened; an instruction to the model is not an access control."},{"text":"Ten rows is too many to include; the top three would have been the right user's.","correct":false,"tempting":"The count changes which leaked rows appear, not whether they can; the filter has to happen before ranking."}]},"prev":{"slug":"when-the-model-is-down-or-slow","title":"When the model is down, slow or empty","url":"https://addaly.com/learn/building-apps-with-ai/when-the-model-is-down-or-slow"},"next":{"slug":"is-the-feature-actually-good","title":"Is the feature actually good","url":"https://addaly.com/learn/building-apps-with-ai/is-the-feature-actually-good"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'Giving the model your own data', in Building Apps with AI. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/building-apps-with-ai/giving-the-model-your-own-data (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"Giving the model your own data","courseTitle":"Building Apps with AI","module":{"index":7,"slug":"a-model-inside-your-app","title":"Putting a model inside your own app"},"answer":null,"keyIdea":"The model knows nothing about your database; the app fetches the rows this user may see, puts them in the prompt, and asks; and an embedding is how it finds the rows that are about the question rather than the ones that share its words.","hero":{"kind":"figure","figure":{"t":"figure","label":"Figure","figure":{"left":{"title":"WHERE user_id, then rank","items":["The database narrows to this session’s rows first","The nearest neighbours are chosen from those rows only","Another user’s note cannot be the closest match at any point","The same scoping as every other query in the app"]},"type":"compare","right":{"title":"Rank everything, then drop","items":["Every row in the table is compared with the question","Other people’s rows were considered and ranked","The ten best may all belong to somebody else, leaving nothing","Written this way because the search was built before the user was"]},"title":"Where the user filter goes in a similarity search","caption":"Both return ten rows belonging to this user. Only one of them never considered anybody else’s, and only one of them still holds when the ranking changes."},"alt":"Figure: Where the user filter goes in a similarity search. WHERE user_id, then rank: The database narrows to this session’s rows first; The nearest neighbours are chosen from those rows only; Another user’s note cannot be the closest match at any point; The same scoping as every other query in the app. Rank everything, then drop: Every row in the table is compared with the question; Other people’s rows were considered and ranked; The ten best may all belong to somebody else, leaving nothing; Written this way because the search was built before the user was. Both return ten rows belonging to this user. Only one of them never considered anybody else’s, and only one of them still holds when the ranking changes."}},"blocks":[{"t":"h2","id":"what-the-model-does-not-know","runs":[{"text":"What the model does not know"}]},{"t":"p","runs":[{"text":"A hosted model has read a great deal of the public internet and none of your database. “Make the assistant know about my customers” cannot mean teaching it; training on your data is expensive, slow, and produces a model that recites approximations. What it means in practice is simpler: at the moment of the question, your code fetches the relevant rows and puts them in the prompt, and the model reads them the way it would read anything else pasted into a message."}]},{"t":"p","runs":[{"text":"That is the whole technique. The engineering is in the word "},{"text":"relevant","i":true},{"text":", and in a boundary that must never move."}]},{"t":"h2","id":"level-one-the-rows-you-already-know","runs":[{"text":"Level one: the rows you already know"}]},{"t":"p","runs":[{"text":"Most questions are about something with an id. “Summarise this customer’s history”: the customer id is in the URL, the session says which user is asking, and a query returns the twenty rows that matter:"}]},{"t":"code","lang":"typescript","label":"TypeScript","text":"const rows = await db.select().from(bookings)\n  .where(and(eq(bookings.userId, session.userId), eq(bookings.customerId, customerId)))\n  .orderBy(desc(bookings.startsAt)).limit(20);"},{"t":"p","runs":[{"text":"Format them as compact text, one line per row, the fields that matter, and put that in the user message beneath the question. This is retrieval without a search, it covers more features than people expect, and it is safe by construction because the query is scoped the same way every query in the app is scoped."}]},{"t":"h2","id":"level-two-search-by-words","runs":[{"text":"Level two: search by words"}]},{"t":"p","runs":[{"text":"“Which of my notes mention the roof?” needs a search. Postgres has full-text search built in ("},{"text":"to_tsvector","code":true},{"text":" and "},{"text":"to_tsquery","code":true},{"text":") and SQLite has FTS5. Both find rows containing the words, ranked, in milliseconds, with no new service. Fetch the top ten, paste them, ask. For a small app this is often the right stopping point."}]},{"t":"p","runs":[{"text":"Word search fails when the words differ: “roof” does not find “ceiling leak”, and “cancelled” does not find “called it off”. That is the gap the next level exists for."}]},{"t":"h2","id":"level-three-search-by-meaning","runs":[{"text":"Level three: search by meaning"}]},{"t":"p","runs":[{"text":"An embedding model turns a piece of text into a list of numbers, typically several hundred to a few thousand of them, arranged so that texts with similar meaning produce lists that point in similar directions. “Roof repair” and “ceiling leak” land close together; “roof repair” and “invoice overdue” do not."}]},{"t":"p","runs":[{"text":"Similarity is measured by the angle between the two lists, called cosine similarity: 1 for identical direction, 0 for unrelated. The search is: embed the question, compare with the stored embedding of every note, take the closest ten."}]},{"t":"code","lang":"sql","label":"SQL","text":"-- pgvector, an extension available on Neon and Supabase free tiers\nSELECT id, body, 1 - (embedding <=> $1) AS similarity\nFROM notes\nWHERE user_id = $2                    -- the boundary, before ranking\nORDER BY embedding <=> $1\nLIMIT 10;"},{"t":"p","runs":[{"text":"The free path is complete. "},{"text":"pgvector","code":true},{"text":" stores and compares the vectors inside Postgres. The embeddings themselves come from a small model that runs on your own machine through Ollama, or in the browser or on the server through "},{"text":"transformers.js","code":true},{"text":", at zero cost; hosted embedding endpoints are cheap and give somewhat better results. A few thousand notes embed in minutes and re-embed only when they change."}]},{"t":"p","runs":[{"text":"The dedicated treatment of embeddings (what the numbers are, why the directions mean anything, where they fail) belongs to the machine-learning course. For the app builder, the working model is: a vector is a position in a space where nearby means similar, and a similarity search returns neighbours."}]},{"t":"h2","id":"the-boundary-that-must-not-move","runs":[{"text":"The boundary that must not move"}]},{"t":"p","runs":[{"text":"Look at the "},{"text":"WHERE user_id = $2","code":true},{"text":" above. It runs before the ranking, not after. Filtering after (search everything, then drop the rows that are not this user’s) means other users’ rows were considered, and one of them was the closest match at some point. The question in this lesson’s check is that mistake, and it is common because the generated version builds the search first and adds the user later as an afterthought."}]},{"t":"p","runs":[{"text":"The rule from the injection lesson: the model only sees what this session may see. Retrieval is where that is enforced, in the query, and it is not enforceable anywhere later."}]},{"t":"h2","id":"how-much-to-paste","runs":[{"text":"How much to paste"}]},{"t":"p","runs":[{"text":"A context window is large and not infinite, and every pasted token is paid for and slows the answer. Ten to twenty rows, trimmed to the fields that matter, is the usual sweet spot. Pasting the whole table because it fits is a habit that works at a hundred rows and produces a $2 call and a confused answer at ten thousand."}]},{"t":"h2","id":"the-model-still-answers-from-itself","runs":[{"text":"The model still answers from itself"}]},{"t":"p","runs":[{"text":"Given ten relevant rows and a question, the model answers from the rows most of the time, and sometimes from what it remembers about the world, and it does not mark which. Tell it in the prompt to answer only from the provided records and to say when they do not contain the answer. Then test with a question the rows cannot answer, and see whether it says so or invents. That test is the difference between a feature and a liability, and it is the subject of the next lesson."}]}],"check":{"question":[{"text":"A ‘ask about my bookings’ feature embeds every bookings row, searches by similarity to the question, and puts the top ten in the prompt. A user asks ‘show me Priya’s bookings’ and gets another customer’s rows. Where is the fault?"}],"options":[{"text":[{"text":"The embedding model is not accurate enough and needs to be replaced with a larger one."}],"correct":false,"tempting":[{"text":"The search found rows about Priya correctly; the fault is that rows belonging to other users were in the searchable set at all."}]},{"text":[{"text":"The similarity search ran over every user’s rows instead of being filtered to this session’s user before ranking, so retrieval itself crossed the boundary."}],"correct":true,"tempting":null},{"text":[{"text":"The prompt failed to tell the model not to reveal other customers’ data, and adding a clear instruction to answer only about the current user’s own bookings would fix it."}],"correct":false,"tempting":[{"text":"By the time the rows are in the prompt the leak has happened; an instruction to the model is not an access control."}]},{"text":[{"text":"Ten rows is too many to include; the top three would have been the right user’s."}],"correct":false,"tempting":[{"text":"The count changes which leaked rows appear, not whether they can; the filter has to happen before ranking."}]}],"order":[3,0,1,2],"why":null},"prev":{"slug":"when-the-model-is-down-or-slow","title":"When the model is down, slow or empty","minutes":7},"next":{"slug":"is-the-feature-actually-good","title":"Is the feature actually good","minutes":7}}}