The missing pieces to your AI app (pgvector + RAG in prod)

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
how do you build a production ready AI app using PG Vector there are lots of tutorials out there on Vector databases generating embeddings even retrieval augmented generation but how do you put these things together into a production application how do you know what good practice or bad practice and how do you put it together quickly we still need authentication rest API quite possibly file storage what if you want to build a chat interface how do you do that easily what if you want to streamer response back from open Ai and then even with embeddings themselves how do you split up a document before you generate EMB on them how do you choose the best embedding model how do you store those embeddings in the database how do you create indexes on those embeddings what's best practices for calculating similarity between those embeddings so in this video we'll address all of those pieces we'll build an end to- endend production ready application using superbase the open source Firebase alternative we'll follow modern best practices all the way across the board so that you can feel confident deploying your application to production in the wild specifically today we're going to build a chat gbt with your own files application where you you can upload your own personal files straight into this app that you'll build and chat directly with them let's get into it okay the way this workshop's going to work today is we're going to work out of this GitHub repo called subbase community/ chat gbt your files I have added this repository to the link in this video's description so feel free to just click on that rather than uh manually typing that out if you do plan on following along today uh I would highly recommend loading up this read me as nearly every single instruction we'll go through today will be documented in here and specifically the way this is going to work is if you do want to follow along just go ahead and clone this repo and then we do have get checkpoints for kind of each Milestone of this project right so in total we have four steps optionally a fifth step if you want to get some better typescript types we'll talk about that at the end and for each step we simply have a G tag for that step so you can just go ahead and check out that step we're going to start on step one today but before we do anything else let me show you what we're actually going to build today so we have this app I've already loged logged in to my account and what you can do is two things number one you can upload files into the application in this case we're going to upload some markdown files and then number two we can actually chat with those files so using GPT under the hood we'll actually be using a technique called rag retrieval augmented generation if you don't know this don't worry we'll talk about this in a second we'll use this technique with open ai's GPT model to actually have a conversation with your files itself so potentially you could upload files that g was never ever trained on they could be personal files or Company files and with this app that we're going to build today we can actually just have a full conversation with those files so from here we can upload to speed things along I've already uploaded three files in this case three files on the Roman Empire when was the last time you thought about the Roman Empire once the files are uploaded we can head on over to chat and ask it literally any question we want so let's say for example we are wondering what was the most common food they ate here we go the most Comm common food consumed by the Romans was cereals and legumes such as bread and pottage and to prove that this actually came from the documents we can head on over to our files um I believe this was in Roman Empire 2 and if we just search for legumes there we go Romans consumed at least 70% of their daily calories in the form of cereals and legumes so our chat application is indeed pulling this information from our files now there's a whole technique behind this uh technique is called retrieval augmented generation or Rag and if you haven't heard of this just hold tight we're going to fully dive into this in this video all right without further Ado let's get into it so some PRX for today uh you will need a Unix based operating system so basically if you're on Windows just use wsl2 if you don't know what that is just uh yeah Google it there's tons of information on WSL 2 we'll be using Docker for local development now this is actually optional within this step-by-step guide we actually have two approaches throughout it so we can use the local stack so for those of you who are interested in like the open source side and you want to run this yourself you want your your database to run on your own local computer you want the embeddings to run locally and everything you can go ahead and follow this local guide which will use Docker under the hood or if you don't have Docker available and you're just interested in using a cloud database that is perfectly great with superbase you do get two databases for free so might as well take advantage of those because I think a lot of you are interested in the local development flow I'm going to go ahead and run through the local Docker approach throughout this Workshop but again anytime there's a situation where you need to choose just come back to these docs and there'll be a cloud option which you can follow those instructions as well for those who have never used superbase all superbase is is just postgress under the hood So within Docker what you're getting is well postgress of course uh with a bunch of extensions added notably for our Workshop today you get the PG Vector extension built right in if you didn't use super base you would have to basically go through the work of creating your own Docker image on top of the postrest image and then in addition to the the postrest image subbase actually has a bunch of extra services that make development really quick so for example autogenerated rest API integrated storage API all this is kind of just built into the stack you don't have to worry about it and personally I love that it's all open source so you can literally just run the entire stack locally quite amazing so go ahead and clone this repo if you haven't already assuming you want to follow along if you don't want to follow along you just want to watch this video like that is totally cool too feel free hopefully you learn something just by watching me as well so I've already cloned the Repository here if after pulling in the repo it says main then go ahead and just copy this command and we're going to check out step one step one is basically a blank slate we do a couple things under the hood the way I created this repository is very simple all I did was run npx create next app at latest with the template with superbase so there is a superbase template available for the create next app and all it does is basically scaffold out your entire next application like you normally would with create next app but with a bunch of super based functionality built in so I've already done that that's what this repo is at step one I did add a couple extra things just because there's a couple little extras we need like some useful Hooks and our sample files of course but for the most part this is all just a vanilla create next app So based on that clearly we're using nextjs as our front end for this project really you could choose any framework you want nextjs makes development in react quite simple so that's what we're going to use today perfect so we're at step one next on the list we're going to just do a quick npm install just to make sure we have all our dependencies up to date that was very quick for me because clearly I already had them installed uh for you it might take just a little bit longer but hopefully not too long okay following the local steps again if you would prefer the cloud follow these Cloud steps instead but to do the local steps using Docker of course make sure you have Docker installed first and once you have that all you need to run is npx superbase start so this super base command is the CLI that superbase offers basically the CLI comes with a bunch of commands you can use to run the entire stack locally you can run migrations you can push your migrations to your Cloud instance you can sync them together kind of everything you'd expect from a super Bas CLI and under the hood the CLI is spining up some Docker images for us and setting up our database as well perfect so when that's done you basically get this list of services these are the different services that I was mentioning that are all included with the superbase stack again it's all open source so you can feel free to look at the source code if you're interested but importantly we care about a couple things here so number one our database it's being hosted on local house 54322 we have an API URL this is our kind of our API Gateway we'll use to connect to the rest API for example there's actually a graphql API so if you prefer graphql you can actually dive into that and use that as well and then one really cool thing here is you actually get the studio URL as well so using this you can actually pull up a full-on studio it's completely local and you can go to the table editor for example and check out your tables we have a a dummy to-dos table that we're actually going to remove in The Next Step but basically the studio gives you full access think of it like a SQL studio right we have a full SQL editor here as well so it's pretty great but let's stay focused Next Step here is to just to set up some environment variables of course from our nextjs front end we're going to need the ability to connect to our super base backend thankfully superbase has a nextjs integration Library available that makes this very seamless so all we got to do is use this command we'll copy it come back here paste and all that did is it created thism dolal file with uh our Bas URL set up automatically which for the local scenario is pointing to our local host and as well as an anonymous key that we use to basically authenticate Anonymous users the anonymous key in subbase is safe to be shared publicly this is something used directly by the client okay now that setup is complete we can get right into the build so first things first we're going to create a migration file so go ahead and copy this command we'll paste it now if you're new to migrations and you're saying Greg what is a migration well let me explain a migration is basically a SQL file that tells our database how to build the entire database so things like table schemas functions everything that we put inside of our database itself we're going to create through migrations and if you've never used migrations before the beauty of migrations is you can have a whole team of developers and everybody can clone this repo and start the project and under the hood it will just run all the migrations for you and basically everyone will have the exact same database schema on their own local computers and let's say in the future somebody needs to add more tables or or modify tables then all that's needed is to add one more migration file and next time they pull the latest changes from the repo it'll just apply that new migration file so migrations are a great way to basically keep track of database changes in a sane way so what we did is we said npx superbase migrations new files what this command does is it creates a new migration file called in this case files why are we calling it files well well this migration is going to basically manage the file upload feature that we're about to build if you recall me saying object storage in super base is actually completely managed within SQL itself which is quite incredible um actually quite useful because what that means is we're able to apply like rule level security and all your like authorization logic directly within the database and it will translate automatically to the object storage now I should mention real quick the raw files themselves aren't actually getting stored in the database right they are stored in a separate object storage under the hood just the relationships and all the policies on those objects are stored in the database so how do you set that up let me explain first things first we're going to create a new schema this isn't a super based thing of course right now we're doing SQL so this is all within postgress itself so if you're not familiar with schemas you can have multiple schemas think of them like Nam spaces um by default you get a public schema which actually superbase will make available publicly to your front end through the rest API of course we can still add um authorization logic around that but if you had any kind of database tables that you don't want to even show up in the rest API at all in any circumstance you would create another schema in this case we're going to call ours private which would never be exposed okay next we're going to go ahead and create our storage bucket copy this command in so insert into storage. buckets storage is yet another schema um and buckets is a table so this table is actually built in to the super base postgress image this all comes kind of for free of course it's empty by default so we're inserting uh a bucket so again if you're familiar with S3 buckets you would create a bucket to store all your files that's exactly what this is we're going to create a bucket literally called files in this case uh the ID and name will make them both files okay next we're going to create some RLS policies let's copy and past those right in there let's talk about this one for a second so RLS what is RLS RLS stands for row level security I think role level security is a lesser known feature of postgress but in my opinion should probably be way more more known it's it's such a powerful feature that once you start using it and you understand it it's like okay this is kind of a no-brainer um why you'd want to set up your authorization logic this way so essentially what role level security does is it applies your well exactly that your authorization logic directly on the table itself and so why is that useful traditionally I think many of us if you have experiened building a full stack application on the back end you would build in your authorization and authentication logic directly within your like API layer so for example if you had a node.js backend um and you're using something like let's say Express or maybe something more advanced like nestjs you would build in all your authorization logic there and like that's totally reasonable honestly if if you're already building a whole nestjs back in then that's quite likely something you would consider doing however what if we took that authorization logic and actually moved it all the way down into the database itself so instead of in our API layer checking it to see if someone has access to certain files or restricting say a t able to rows only that they own we can go ahead and actually set that logic directly within the database itself this is called RLS and it's done through this create policy statement so essentially say create policy the name of your policy in this case we're going to say authenticated users can upload files meaning only once a user is logged into the platform are they able to upload files uh otherwise no dice and then actually on top of that we're going to make sure that they can only upload files to the files bucket right so in case we add more buckets in the future this policy forces them to only do that on files as well as it ensures that the owner is them now this off. uid this is a special function built into superbase postgress that basically captures the current user logging in um how does it do that well since we're using this autogenerated rest API under the hood this is actually a service called postrest postgress this one pretty cute little icon there for the rest yeah this service will essentially monitor your database in real time like so it checks which tables exist in your public schema and it will go ahead and generate a rest API automatically on the fly so inserts updates deletes selects your full crud available automatically without doing a thing of course since this is building your API for you how do you manage security how do you manage authorization that's exactly where RLS policies come in so if you're using something like postrest in this case through superbase then RLS is basically a requirement to make sure that you actually lock that down properly but I really do think that more of us should be considering RLS even for any otherc to be honest because think about it if you create an API route that does you know you you wrote the proper logic to restrict access to a certain table um within that API route uh that's great and it's working but what about next time you build a new endpoint in the future you're going to have to continuously add that logic over and over again or some sort of abstraction um there's more places to get it wrong whereas when you apply the RLS logic literally at the lowest possible level directly onto the table itself within the database there's no question like there's no accidental um opening up API endpoints to you know give users access to tables they shouldn't by accident the security is applied within the database itself so doesn't matter if you're doing joins or creating special functions um at the end of the day the role level security remains in place so just some food for thought of course RLS can be applied to more than just inserts we can apply it to selects updates and deletes all four cred operations uh in this case users can view their own files so basically we're saying uh create a policy on the select where as as long as selecting from the files bucket and the owners themselves then they're allowed to do that by the way owner this is a column right if you're wondering where does owner come from or bucket ID these are just columns these are columns within the buckets table if you're curious what these columns are then we can pull back up our handy dandy Studio what was that again uh if we say MPX superbase status that will pull these back up again if you ever lose them status is the command and then we have our studio URL so coming back here again we can head on over to the table editor by default we're in the public schema right these are the schemas I was talking about head on down to storage and then here we go here's our buckets which we haven't created quite yet cuz we haven't uh applied the migration but that will live there eventually and then we have objects so objects are the there'll be an object for every single file that gets inserted into the bucket so think of an object as as a file it's one to one so from here we can actually head on over to definition and and check this out we can see all the columns within this storage to objects table so particularly we have bucket ID right this maps to bucket ID here so ensuring that they can only pull files from the files bucket and then we also have owner here which is the actual user that this belongs to and that Maps one to one to this column here now if you're wondering where are users stored let me tell you we have another schema for that called off again all these are kind of built-in schemas for super base come down over into users and this table will store all the users that we insert into superbase so so assuming you're using super base for authentication which I would highly recommend because everything is just fully integrated all your users will show up here and it's literally just another table so you can reference these user IDs from all your other tables in this case specifically you can reference it from our storage objects so these are all empty right now we'll fill these out shortly but for now let's finish our migration we'll close some of these tabs come back to our docs okay so next on the list we're going to go ahead and modify our front end so let's get into our next JS project our nextjs project is using next 13 so this means we're going to be using the app router don't worry if you've never used app router before or even xjs in general um kind of everything is set up for you already all you got to do is follow these instructions to kind of modify it but I did try to design this in a way that there isn't too much magic happening most of the things that are already set up for you are just like styling and ux and UI stuff um just because this video could turn into a 10-hour video if if we had to do all that ourselves so thankfully that's all done for you what we'll be doing here is just adding in the appropriate logic okay so we'll head on over to our app files page so here we have uh app files page this our front end code here's our files page this will map one to one to our files page let's quickly spin up our front end so that we can actually see it so to do that we'll do npm run Dev this command comes from package Json here Dev just runs the nextjs dev command which uh just spins up a local front- end project as you can see we're listening on Port 3000 we'll open that guy up and we're greeted with this chat with your files using superbase screen now I guess probably now is a good time to show you the authentication piece uh so let's click login this will bring us to a SL login page this is just a really super simple form login form right here you can check it out here um feel free to go through this on your own time but honestly it's it's very very straightforward all the off logic is actually included in the template for you so again if you run that next create next app- e with super base it will come with all this for you but just super high level it comes with you know all the routes you would need to do proper login and sign up right so we have sign in sign out sign up which is registration and then actually a call back that the authentication route will use to basically redirect once you're signed in and then within all of these of course we're just using our superbase next helper so there's actually not too much logic that needs to happen here to actually interface with the database and create the users if you're interested in all those details like definitely feel free to dive into this on your own time if you're not interested that's totally cool you don't have to do a thing um at this point we can just log in by creating a new user so I'll create a user called Greg at example.com create a password and I'll click sign up now I'm in now of course in a production application You' probably want the user to confirm their email address before it takes them directly in I mean not necessarily but that's a very common flow so let's say you did want that um that's all configurable within superbase so locally here I should mention there's under the superbase folder by the way superbase folder this contains like all the superbase specific stuff right the rest of this is mostly nextjs frontend stuff so all superbase is kind of under this folder and then within this folder we have things like Edge functions which we'll get into migrations which we've already started and then some other things we have a config dotl this is basically configuring our entire stack so say you were interested in emails actually being like confirmed by email before you're allowed in you would just come down here into I believe it's a. email here we go so enable confirmation false it's false by default for local development just because usually you don't want to do that it's kind of a pain but you could set that to True uh in which case it will actually send the email now for local it it will actually go through a different service let me pull that back up again npx superbase status we have this service called inbet uh what in bucket does is it basically just it's like a catch all so when it sends an email and instead of actually sending the email to a real email address it sends it to like a fake inbox that lives here it's just a nice way to capture emails in local development without actually having to um send it off to a real email address so that's available for you I think a lot of people actually missed this so I did want to mention that real quick in case you were interested in experimenting with emails within superbase uh within local by default it will show up in this in bucket okay let's close that at risk of going too far down the rabbit hole let's get back and focus on our job so we've logged in we have our files page let's go and click on that and this is where you can upload files so this page right here SL files this Maps one to one to our uh this is our login page close that here we go our files page app files page. TSX will map to this page so coming on back to our readme we're going to go ahead and copy and paste this so This basically we'll instantiate a sub based client using this command we'll insert this at the top of our component so right up here we can insert that of course it's going to be not imported by default uh there's a actually really handy keyboard shortcut if you're on Mac command shift I I think it's control shift I on Windows if you don't have that you can also I believe hit Commander control Dot and then you can import them one by by one but I just love this is like muscle memory for me now just cuz it's like you just use it all the time to to add in the missing Imports okay we have our client so this will basically instantiate a super based client do note that with this command it's actually creating a Singleton under the hood so if you're a react Guru and you might look at this and be like well this is terrible because what looks like it's going to create a brand new superbase instance every reender uh thankfully no it is using a single tin pattern under the hood so basically this super based client will be global through our entire app um if you do prefer to manage the superbase state yourself there is an option here uh I believe it's is Singleton by default that's true but you can say is Singleton false and now it literally will create it every render so from here you would probably wrap it into your own context or something like that if you really want to manage it yourself but we don't need to do that by default the single D pattern works great um we only have one super based instance so this is awesome next let's actually handle the upload itself so we'll copy this snippet it's going to live in the inputs on change Handler so we'll come down here we have an input box uh right here this input maps to this kind of input button here uh the reason why it shows up like this by the way is because we set the type file um inputs in HTML can have many different types here we go buttons checkbox color date you name it um there's actually there's actually way more than I thought so when you set it to file by default it will look like that we we'll give it a name as well and we have the onchange Handler so this onchange Handler essentially will get triggered when someone selects a file so I've already helped you out uh just grab it the file from the e. Target check to make sure it exists and then let's go ahead and replace this comment with the snippet from the read me all right let's go over this what's actually happening so we have our super based client from here you can do lots of things if you've ever used this before you might be familiar with the from when you say from you can actually just reference a table directly so let's say we had a table called to-dos then you could say super base. from. todos and you could say do insert or do select and then you can select so it's it's kind of think of it like a query Builder under the hood um where you can reference these tables again this is actually going through a rest API under the hood um even though it feels like a query Builder it is going through that API so all your authentication logic and security is in place when you're using this API but for this specific use case we're not going to be actually connecting to a table directly we'll be using the storage subcommand so similar to the root superbase command storage also has a DOT from but in this case it's referencing a bucket instead of a table and then from there we have some special commands that only relate to storage so in this case we're going to call upload uh first parameter is the path name where this file could get uploaded and second parameter is the file itself so of course this is of type file this is a built-in type as part of the web standard perfect so once we select a file this onchange Handler will get triggered and we'll go ahead and upload the file to this path now you might be wondering what's with this uuid thing what's going on there it's important to note that just like any object storage so think again like S3 if you're familiar every file path that you upload needs to be unique right so if say we were to scrap this part and only upload the name of the file which this would literally be whatever name of the file it was on disk that's great for the first time but what if the second time you upload a file with the same name or somebody else uh on some other account uploads a file with the same name uh there'll be a conflict so we need to add some extra logic to make sure that the actual path is unique so in this case we're just using the browser's crypto API uh there's a command called random uid which funny enough I actually didn't know this existed till recently I've always used the uid mpm module till I realize that this is built right into the browser now so how handy clearly I'm behind the times anyways you might be thinking well why do we even need the file name at all why can't we just save it as a random uu ID Well you certainly can that's totally reasonable the only problem with this is you do lose some information specifically you lose the name of the file so say in the future when we're showing the files here like the files that they had uploaded it'd be nice to show like the original file name that they had they had uploaded right uh instead of just some random U ID so with this approach we actually lose that file name so what I propose here is we'll just say slash so as if this was put into a folder under the hood with object storage slashes really don't mean anything it's just a file name but a lot of them treat slashes like as if they're directories uh just from a UI perspective but think of it basically as the whole thing as the file name so basically we're saying create a uu ID and then store the name of the file after that within a quote unquote subfolder uh and pass it in so let's try it out let's see what happens so supposedly that uploaded let's actually look at it again through our Dev tools here so we'll refresh the page and let's do it again clear this okay so never mind we actually got an error what's the error here bucket not found 404 so for whatever reason it thinks that this files bucket doesn't exist and well of course it doesn't exist because we haven't run our migration yet so so the problem is is this command was never run once we actually insert the bucket into the files table it will literally create that bucket for us but until then nothing exists yet and by the way in the studio you can actually come down to storage and this will actually show your buckets as well so this kind of also proves oh yeah we haven't created any buckets yet you do have the option to create buckets through the goey here I would recommend in general doing it through migrations just for the reasons we talked about earlier uh when you're working on a team for those who like to get under the hood also there's that storage bucket here right so recall we have objects this maps to a file and buckets here uh this is also empty so essentially we need to go ahead and run this migration real quick before we run that migration there's one more piece I want to handle and that is we can actually be a bit better on our RLS policy right right now when we insert into our files bucket um all we do is we check the owner and we check the bucket ID which is great but I think there's more we can do here we come back to our readme under improve upload RLS policy let's go ahead and copy this function over we'll just paste it right above of our first policy so this function is called uid or null so essentially what this function does is it checks to see if the value you pass to it is a uuid if it is it will return it if it's not it'll just return no so basically it allows us to identify whether something's a uu ID or not next we're going to go ahead and replace our insert RLS policy and copy that select this paste so we're just going to replace it with this new one and the the addition that we're doing now is this part here so we're checking to see if essentially the path token so when you upload a file into object storage uh there's actually a column called path token this is actually an array column and there'll be an array of all the different paths so back to here uh I told you we delimit by slash so actually this first piece here will be the first path token and then after the slash will be the second path token if we had more slashes more directories quote unquote then those will be additional uh path tokens within that array so if I say path token one that's actually grabbing the first directory in that path and by the way this tripped me up uh it is in fact one not zero for one reason or another postgress arrays actually are one based and not zero based so don't let that trip you up uh one is literally referring to the first segment so big picture what we're doing here is we're basically making sure that this first directory is a uu ID uh because we're checking to make sure it's not null why do we want to do this well it's important to remember the concept of never trust a client right even though in our client code we tell it to be a uid followed by the actual file name nothing actually stops people from from being malicious and changing that right never trust a client so if there's a malicious user they could freely upload a file even if they're logged in right they would have to be logged in actually because we have these first two policies here in place but after that nothing stops them from uploading it to whatever path they want really uh which could conflict in the future or whatever that's it's just bad practice right so let's let's actually Force this first um path to be uuid so that uh malicious users can't do anything bad we'll save that now that we have that in place we can go ahead and and run our first migration this is exciting we'll copy this command open up a terminal if you haven't already by the way you can just click plus here if you're in vs code open a new terminal we'll run superbase migrations or migration up and just like that we've actually applied our files migration we can confirm that this migration has been applied by doing a couple things first we can check if the files bucket exists in studio there we go there's a row for files we can actually also go down to storage now we can see there's a file bucket perfect and last but not least for those like to get under the hood like me there's actually even a super based migration schema and table that actually will track these migrations as they've run right this is important because if I ran this command again it it knows I'm already up to date because it checks this table I just noticed we actually also have a to-dos table this is actually coming from the the original template that we're actually going to need to remove so we might have an issue where we need to reset our database shortly don't worry I'll walk you through that um that's probably going to come up here very shortly but now that we have that in place we can come back to our our front end we'll refresh choose a file upload and great we actually got a 200 instead of a 400 meaning that it supposedly uploaded properly um the response looks like this looks promising we can come back to our studio come down to storage files and boom there's our very first file and well I guess that's the folder and then the file name just as we planned once again this is all integrated right into the database we can come down to the storage schema and objects and boom there is our file now of course I should mention if at this point you aren't following the local stack and you are using the cloud go ahead and run npx superbas DB push and that's the equivalent command to take that migration and push it up to your Cloud instance all right well congratulations Step One is done we're going to move on to step two which is our documents and setting up some tables to manage our documents for retrieval augmented generation so if you are following along and you're not caught up feel free to just run these two commands so we're going to run get stash push uh my work on step one that basically stashes everything we've done and then we're going to go ahead and check out step two now this was the part I was worried about I realized in step two I had removed the to-dos migration this is usually taboo you never remove migrations later on in commits really I should have removed that at the very beginning because we don't we don't ever use that to-dos it was just part of the template but too late now to resolve this if you're on the local stack you can just go ahead and run npx superbase DB reset this is actually quite a handy command what this does is essentially takes your database and wipes it clean completely so if you're ever kind of in a weird State and you're just like what is going on I don't know exactly what I've added or not added and uh you just need like a clean slate this is the perfect command and as you can see now that started back up again it applied our migration here so in this case the only migration that we should have had all along and then actually applied some seeds which we'll talk about shortly close these guys okay so let's take a quick look at what we're about quot to build if you've ever done retrieval augmented generation before you probably know that best practi is to take whatever document you're working with and splitting it into smaller chunks or in this case we're going to call them sections why do we do that well for a couple reasons number one when we're performing retrieval augmented generation recall what we're doing is we're searching our knowledge base so in this case uh searching through some of our tables for the most relevant documents to the user's query right so back here under chat the user is asking oh of course I need to log back in let's close Dev tools the reason why I need to log back in is cuz I did that DB reset right all of my information is reset including our credentials thankfully it's very easy to sign back in So as I was saying coming to chat here uh we're talking about retrieval augmented generation so say the user asks uh what is the most common type of food they ate right so first of all let's assume that I've actually uploaded those files I know it got reset but let's just pretend that those are still there for the Roman Empire if I just send this directly to chat GB let's say it didn't have all the details that we had in these files essentially we want to use retrieval augmented generation here we want to take this query we'll actually generate an embedding on this itself right think of it like a plot on a graph and we want to find all of our documents that are similar to this right so if we're generate embeddings on our entire documents themselves there's a couple challenges with that right number one documents could be really big and if it's big enough it will actually never fit within the context window of an embedding model so you try to generate an embedding on the text it will actually either truncate it most of the time most models just truncate it or some models might just throw an error if you're wondering how many words or characters will fit in an embedding model each embedding model is different the reference I would recommend checking out is the mte leaderboard this is a leaderboard designed by hugging face and they've done a really awesome job here basically uh coming up with data sets to test all these different embeding models against for different use cases and this is this is the leaderboard this is like the combined alltime leaderboard interestingly enough if you come down here open AI is way down here in 15th Place um there's a ton of Open Source models that actually perform better than that at least today actually one of the ones we'll be using today is GTE small which is now 14th Place but it is a very small model which is why it's attractive for us today back to the context window it's a sequence length column here that will tell you that so uh most of these models have a Contex window of 512 tokens now keep in mind we're talking tokens here not characters or words if you're not familiar with tokens every model tokenizes their text differently but think of tokens as just a way for a model to translate um your text into a meaningful representation that the model understands in general a token is usually like four English characters but that's that's a very rough approximation if you're using open AI for example they have a tokenizer that does a really good job of kind of explaining how this works so say I'm thinking about the Roman Empire so you can see how this is working right a token isn't necessarily a word as you can see it split I and press for M into two different tokens in general character sequences that it sees most often tend to turn into a token so often that does translate to words but not always sometimes the word will get split up for example if I add an S to Empires you can see it actually broke that into M Empires for whatever reason it decided that was the optimal choice to split so even though we have 36 characters here it's actually only using eight tokens so keep that mind you might look at this 512 token sequence length and think that's kind of small but in general we're going to be splitting up content anyways and 512 usually is more than enough to fit the content we need plus allows us to use these higher performing embedding models so reeling it back in back to documents that is one reason why we wouldn't want to take the entire document and try to generate embedding on it another reason would be even if we did use a massive context window like open AI has 896 you're working kind of at too large of a granularity right if you create create one embeding for your entire document like that Roman Empire document had like a ton of different topics that were completely unrelated so if you try to create one embeding on that I'm not convinced that embeding will be placed in a location that um would necessarily capture the meaning once we ask get some queries later on right so better just split that into more semantic chunks that are more meaningful right so we have a heading this part of the Roman Empire document is talking about the foods that they ate right this one's talking about where they lived and the building structure Etc so it's better to to chunk by uh semantic more meaningful section so that their embeddings are capturing just that granular piece right so because we're going to be splitting into smaller sections we're actually going to create two tables here we're going to have one table to represent the document itself and as you can see here this is actually just going to be a direct relationship to that object ID on the storage table that we've already looked at so this is a onetoone relationship we're actually going to also store who created that document for our use case today of course for you if you had like say you wanted multiple people to work together on a document or you had uh a teams Concept in your application in that case you wouldn't be storing created by directly here you would be creating a join table to do relationships um if you're interested to learn more about that kind of stuff let me know we actually do have documentation on this on the subase website uh I believe it's under rag with permissions feel free to check that out here we go rag with permissions we have a section here on documents owned by multiple people so feel free to check that out if you're interested in that kind of concept but for now for Simplicity sake we're just going to have one owner per document and then of course we have a one to many relationship between our documents and our document sections where the document section represents kind of a small chunk within that document and notice here this is actually where we're going to be storing our embedding itself using this Vector data type provided by the PG Vector extension and honestly this right here is one thing I love about postgress just in general is you get that full control right we're building an app I imagine that's what most of us are trying to do also this really cool AI stuff is only meaningful if we can actually implement it right so with something like postgress you get full control of your entire schema and then we can actually just use PG Vector to go ahead and store our embedding as a first class data type directly within our existing application data structure right not to mention when I query are embedding it's going to be a lot quicker cuz it's all within the same database right versus going through say like a separate dedicated uh Vector database and lastly of course I'm biased because this is a super based video but we have done benchmarks on PG Vector as well against some of the big Vector databases in this case we did a post on PG vector versus pine cone and just did a a cost and performance comparison and actually found that uh PG Vector performed very very well at the same compute levels this is of course thanks to the latest update which is hnsw this is a a new index type that has been added to PG VOR since hsw has been added PG Vector has actually just been like incredibly performant complete Game Changer so Theory and explanations aside let's actually go ahead and build this we're going to go ahead and create a brand new migration for documents copy that we'll paste it from from here we should expect to see a new migration file there it is for documents so next we're actually going to create some extensions let me explain this we'll copy and paste that in first we're going to create a extension called pget pget is actually a very interesting extension in my opinion it's an extension that allows you to actually perform HTTP requests so think get request or post request literally from within postgress so recall earlier we created like this function you can create these functions within postgress and call them within postgress as we did down here right so what pget does is actually gives you additional functions to call HTTP endpoints which is quite awesome um and then of course we also need to create the vector extension this is in fact the PG Vector extension uh when you go to create it you don't say PG vector by the way you just say Vector um that's how it was kind of registered within postrest so don't let that trip you up Vector is the one you're looking for and once we do these basically both these extensions will be now available without that they won't be so if you're ever trying to use say the vector data type and you're getting an error saying hey you know I don't know what this Vector data type is quite likely you forgot to enable the extension first okay next let's go ahead and create a new table this will be our documents table that we were just looking at here right this guy pretty standard if you're familiar with building SQL tables we have an ID column this is autogenerated and we be Auto incrementing uh also it will be our primary key we have a column for name this just going to keep track of the name of the document storage object ID this is a foreign key reference to our storage objects table again the beauty of integrating our storage directly into SQL means we can actually create proper foreign key references which is awesome uh that is a one to one relationship created by once again foreign key reference but this time to the users table defaulting to just the current user and then also we have a created at time just so we can track when this document was actually added uh defaulting to the current time moving on we'll create a view and I'll explain this in a second go ahead and copy and paste that in this view we're calling documents with storage path uh if you're not familiar with views views are basically the exact same thing as a table except you all they are is like a query so in this case the contents of the view is a select query but you would be able to call this view as if it was a table and it would just do this under the hood for you so the reason why we want to do this is because later on we'll find out that documents is this table is great to have but there's one piece that's handy to have with that and that's the storage object path so the actual um storage object object name coming from the objects table on our front end it's going to be really handy having this available and since these are cross schema it's a lot easier just to do the joins within a view than from the front end that will be a lot more difficult so we're going to create a view now note this part here with security invoker true this is very important so in the future if you're ever creating views pretty much you're almost always going to want this enabled by default at least unless you have a good reason not to what this means is when you create this View and somebody actually calls this view postgress needs to decide what the permission level is for the people using this right we have a bunch of RLS policies that we'll actually create in a second here for the documents table so by default if you don't have this command then this is actually going to be a security definer which means that um the permissions of this view is actually the permissions of the role actually who created this view in the first place so like think of it like an admin role when we run this migration you you're basically an administrator when you create these views and that is the role that it will use to actually call this view which is usually bad practice because that role typically will have way more elev permissions than the permissions you actually want to provide with this view right so when we say security invoker true that means um actually basically inherit their permissions of whoever invoked this view versus defined the view right so this will in fact inherit the same off U ID user all the way from the front end when it calls this as long as we have this in place so just FYI keep that in mind when you're creating views next we're going to create our document sections table once again this one over here very similar to the documents table but this time we're going to actually reference the document ID d as a foreign key right that's the one to many relationship the content itself so what is the content of that document section we need to store that so that we can actually perform that retrieval augmented generation this content is what we will use to inject into our uh GPT prompt later on and then finally last but not least of course we need our embedding this is the name of the column by the way you can call this whatever you want I think embedding is most appropriate uh the data type is Vector and then the size is the number of Dimensions right so we'll actually be using a model today uh I think I pointed that out GTE small which is 384 Dimensions so that is going to be the size of our Vector today worth noting uh when it comes to Dimension size if you can lower Dimensions is typically better when you go to scale at C base we've actually seen lots of customers uh many customers use open AI embedding so the embeddings from Tex embedding 82 which is 1,536 Dimensions those embeddings are great although as I pointed out earlier on the mte leaderboard there are in fact nowadays there's actually many models that perform better but this is actually one one of the highest Dimension models that exist today and the consequence of that unfortunately means that they take up way more space in your database also means it takes way more space in memory takes a lot longer to create indexes on once you scale up to a large number of Records we're seeing lots of issues where people are are actually hitting some scaling issues with this Dimension size that they would have never actually hit if they chose a model with smaller dimensions of course at certain point you're going to have to scale your database appropriately and and increase the compute and memory and dis space that's a necessity that will never go away but I think you can get a lot further when you're using models with lower Dimensions so that's what we'll use today okay back to here let's go ahead and create an index on our document sections table this is the hnsw index the one that I was talking about that's the game changer in general I do recommend creating an hnsw index as opposed to the older type which was called IVF flat I recommend it because of course performance is much better but also you can create the index right away right in this case when we run this migration it will create the table and then immediately create the index which means the index is going to get created on essentially an empty table and for hnsw that's perfectly fine what hnsw will do under the hood is as you add new records it will continuously think of it as like rebuild the index on the fly as you add new data so your hnsw index will always be more or less optimal whereas if you were to use IVF flat IVF flat is actually depends quite greatly on existing data in your table so if you had no data in your table and you create this index it's going to try to create these under the hood it creates these like cell clusters as part of the indexing algorithm and the cluster points that it chooses will be like extremely unoptimal because it has no idea where your data actually lives so in general if you're going to use IVF flat PG Vector recommends that you have sufficient data first that kind of represents the distribution of your data already before you create that so that those Soul clusters are actually created in appropriate places otherwise this index will perform very poorly right if you were to to create this too early so anyways for both of those reasons we're going to use hnsw as our default index just to explain this real quick we're creting on the document sections table right that's referencing this exactly this is the type of index and then this is referencing the column so since we called the column embedding that's what we put here and then finally we choose the operator in this case we'll find out later on when we actually do the similarity search that we're going to be using an operator called inner product also known as dot product this is one of the more performant distance functions and we'll we'll go into this more later but we can use this one because our embeding will be normalized so if to use a different distance measure like cosine distance for example you would need to adjust this accordingly to make sure you're creating the index on the right operator if you're curious what the operators are that is also all documented if you go to sub based documentation Ai and vectors and head on down to Vector indexes you can see here these are the different operators and the the classes that are associated with them if you're curious to learn more information on IVF flat or hsw can just click into these we actually go into like how it all works under the hood moving on let's get our RLS Poli icies created paste those guys in there let me explain them real quick one by one first things first you actually have to enable Ro level security and you might be saying hey Greg we never did that back on the other one well turned out for storage buckets RLS is actually already enabled by default under the hood when you first create your postrest database so you don't have to worry about that in this case since these are our own custom tables we need to explicitly say alter table enable Ro level security for both of those that's an important piece next we're going to do just basic role level security so first of all users can only insert documents for themselves so they can't pretend to upload a a document with somebody else as the user ID they can only upload it for themselves same with querying their own documents same with inserting document sections actually we'll talk about that in one second the select on the documents by the way I should mention with RLS a really good way to think of RLS is think of it as a filter so what I mean by that is think about as if this statement here is actually just getting added as a wear Clause so if I were to say select star from oops select star from documents if I were to run this as a specific user then what it would do with RLS under the hood is that it will actually automatically apply this filter kind of like awar Clause so that this document's result only gets filtered to the documents that were created by that person so that's actually what's going on under the hood with RLS if there's no documents created by me then this will come back with nothing even though this table could have like millions of Records if none of those records belong to me then it won't come back with anything so that that's really the beauty of of RLS right as I said before you could join this with other things Etc and it still at the end of the day will always add this filter for you okay down here we have some RLS for our document sections so our second table here note though that our second table doesn't actually store the user ID in them at all so how do we do RLS with our document sections the answer to that is well we do store the created user in our documents and since we have a foreign key relationship to our documents we can basically determine who owns this document section through the document ID and so the way we do that is using this query here we basically say where the document ID is in and then we select from the documents table where the created by user is the current user uh and this will only return document IDs that are owned by the current user and then therefore uh this ID has to match in that so intrinsically we're we're determining access through this other table same for updates and same for selects as well okay so we're almost on our migration let's pause for a second and uh address another piece that will be necessary in order for us to actually set up some web hooks to actually invoke an edge function from within postgress if you recall top of our migration we have this extension called pget and this pget extension allows us to execute HTTP requests from within postgress essentially this is web hook functionality and I'll explain more about this in a second but when we're developing locally we're going to need to access our Edge functions which we explain again shortly Edge functions will actually use to perform well perform multiple things we'll use Edge functions to actually split up our documents into smaller sections we'll also use Edge functions to perform the actual uh embedding generation itself um if you're not familiar with Edge functions Edge functions are seen a lot these days in different platforms right Cloud flare for example has Edge functions they call these Cloud flare workers I think of edge functions kind of as like AWS lambdas so when we're developing locally within Docker we need to be able to access these Edge functions through that super base Gateway URL so to do that there's actually a really cool technique using something called Vault so go ahead and copy this snippet here and we're going to move on over to a file called seed. SQL so over here on the left hand side we have seed. SQL paste that here essentially what this is doing is it's using this built-in feature called Vault vault in superbase is a way to actually store configuration and secrets within the database itself and the reason why you might want to use Vault here versus some other arbitrary t table is because Vault actually encrypts your information and when I say encrypt I'm not talking about encrypting at rest I'm talking about literally encrypting even within a live database when you're using superbase the encryption keys are actually stored outside of superbase as you would hope for you would never really want to store the key within the database itself otherwise that's like uh leaving your keys within your front door uh and then walking away hoping that no one breaks in so in this specific case we actually didn't really need the encryption part we're just storing the super BAS which isn't sensitive but it's a convenient kind of configuration tool you can use for both configuration and actual Secrets now I should mention if you're not using Docker and you're connecting to a database directly in the cloud just replace this with the API URL from your own database on superbase from this API settings link here you can grab the URL there and just paste that in there and you can actually execute this directly within the SQL editor from Studio so if you come down here and you can paste that in here and that will go ahead and um add that to your Cloud instance coming back to our migration we're going to copy this new function here we'll paste it down here this function is just called superbase URL and all it does is it grabs the decrypted value from our vault I could probably do a whole another video just on vault itself feel free to well let me know if that's something you're interested in also we do have documentation on this as well of course so feel free just to go there under database and then under access and security we have managing secrets with fault and this goes into all the little details k last but not least we're going to add one more snippet to our migration which is a function copy and paste that here and this function is called handle storage update and this is actually a special function for triggers if you're not familiar with postgress triggers postgress trigger basically is exactly like it sounds like when you perform an operation on a table such as insert or update or or whatever you can actually trigger a function to run every single time so in this case what we're saying is anytime there's an insert on the storage. objects table it AKA anytime anybody uploads a file for each row so for each file execute the above procedure handle storage update so basically we can guarantee that for every single file that gets uploaded it'll automatically um perform this trigger function which will follow the logic here we're going to do two things number one we're going to insert a document into the documents table that we just created again this is a onetoone relationship we're going to store the name which is the second path token if you recall uh and then number two we're going to actually perform that HTTP request so you using that net extension pget we're going to call net. HTTP post it's going to perform a post request against our superbase URL that we just configured earlier and we're going to concatenate that with SL function slv1 slpress so this this Edge function doesn't exist quite yet but we're going to create it shortly passing the appropriate headers we're basically forwarding through the authorization header through this which means the user that actually triggered this function in the first place through the insert there are authorization credentials so AKA their JW UT will actually get forwarded all the way through here all the way to the edge function so that even our Edge function itself can continue to inherit that same permissions from the user I think that's pretty powerful pretty huge and also very important right that's something that you know even once we get out of postgress entirely into an edge function it's quite important that we we continue to inherit that user's permissions even from there right and then finally we pass in the document body as Json in this case we're just passing in the document ID and specifically what is this Edge function going to do well we're going to create one shortly here called process and what it will do is well as the name suggests it's going to process our document so essentially this is going to be the edge function that takes our document the file that we uploaded in this case it's a markdown file scans through it splits it up into smaller chunks and then actually inserts those chunks into uh those document sections right if you were interested in extending this application in the future to support say PDFs or some other file extension this process Edge function would be where you implement that logic so that's that's great for all intents and purposes you can basically think of this trigger as a web hook all right let's go ahead and apply that migration if you're running locally you'll copy this command if you're in the cloud you'll run the next command and just like that our new documents migration has now been applied if we go into Studio we should expect to see our new documents and document sections table perfect and actually we even have our view there as well okay this is a long one thanks for bearing with me we're getting close to being done the last piece of this step of course is to create that Edge function that we just talked about so to do that we can create that by using this CLI command npx super based functions new and then the name of the function so we'll say npx superbase functions new process is going to be the name of our Edge function coming over here we'll see a brand new folder here called process and then within that folder we have a file called index.ts the way Edge functions work with superbase is you'll have a folder for every single edge function that you have and then the index.ts will be the actual implementation of that edge function but but you're free to add as many other files as you want in here that support this Edge function if you want and you can reference it from here also if you had common logic that you want to share between multiple different Edge functions the pattern is you use an underscore so if you have an underscore and then a folder name anything within here you can use them as Library files or or common files between the other ones so actually already pre- added this lib folder here for us with a handy util called markdown parser this file has some utility functions that will help us parse our markdown function specifically it's using the unified library to basically parse our markdown into an abstract syntax tree I'm not going to go super deep into this but think of it as basically for each type of element within a markdown file so you got headings you got regular content like paragraph content you've got code Snippets Etc when you split that into a markdown abstract syntax tree it's basically just turns it into Json with types so basically like here's our heading here's our content body here's our snippet and that that's useful because now you can actually manipulate that however you like so in our case we have a nice handy function called process markdown and what it will do is it will scan through the markdown turn it into an abstract syntax tree and then we can do some convenient things like split it by heading so basically what this does is every single time it sees a brand new heading so you know in markdown you have the hashes H1 H2 or H3 let me show you a quick example so here we go our Roman Empire file if we look at the code behind this this is just marked down right so these These are headings right um this would be like an H1 this would be an H2 H3 because of the three um hashes there so basically what we're saying is every single time you come across a new heading split that into its own new chunk why are we deciding to do that well kind of like what we talked about earlier we want to split by semantic meaning and it just so happens the way markdown files are written or just documents in general typically every time you have a brand new heading that's kind of a new thought a new section new semantic meaning right in this case this section is about the transition from the Republic to theend Empire Etc right there'll be one later on about geography and demography right so this is like a perfect split point right because when the user asks a question about something likely they're going to be talking about one of these specific topics right so we don't want to be having multiple of these together within one document section and therefore embedding we want to split them up logically so it's worth noting this is not perfect if I had like a heading say H2 and then immediately an H3 underneath without text in between I'd actually end up with just a blank section that would be kind of meaningless so there's definitely more Advanced things that we could be doing on top of this I'll save that for another time where we could explore those options but for now I think this gets us pretty far from there we just do some extra logic to make sure that the chunk that it split up is isn't too long sometimes even though it splits by headings the that heading and its content is actually still very very large and so it's too large still to fit within an embedding so there's just some extra logic here to to in a worst case scenario split it by character it's not ideal if it has to get to that point but it's kind of like a a worst case fallback coming back to our Edge function we'll go to our read me once again right also worth noting since this is our first time working with Edge functions on this Workshop there are a couple things you probably want to set up ahead of time number one Edge functions actually use Dino under the hood don't let doo scare you uh Doo is basically an alternative to nodejs it's actually built by the same original author of node.js what Doo is is basically fixes all the things that he regrets doing with nodejs and one of the important things about Dino actually is that it follows the web stand like as closely as possible so if you have ever used Doo before you might have noticed that when you're importing things using Doo quite often you're importing from like just straight up URLs right this is something you don't do in node.js you have to like mpm install first do you know you actually just pull them directly from URLs which sounds kind of crazy but actually this is what the web standard does if you if you use um es modules within the browser which is possible nowadays um this is literally how you import modules within just vanilla browser spec so Props to for committing to that so real quick how do URLs work with doo well there's these handy cdns out there in this case we have a CDN called es. sh and what the CDN actually does is it just wraps npm modules so all you have to do is say slash followed by whatever mpm module you would normally have to install so in this case superbase superbas JS this is literally just an mpm module under the hood but when you request that from esm sh it will automatically like go and fetch that mpm module for you under the hood and then serve it to you over HTTP and that essentially makes all these mpm modules available through Dino so we do the same with open AI um and a bunch of other packages now we could have just put this URL directly down here into our import from here but Dino actually supports this handy thing called import Maps which we're going to take advantage of in this Workshop today um you can almost think of of an import map kind of like a package Json where you have your dependency list there it's it's a little bit more specific to just our Imports and one reason why you might want to use an import map instead of just importing the URL directly is it's just a bit cleaner right this is now a little bit cleaner to import from H but more importantly when we have multiple Edge functions it'd be good if we could commit to the same versions across the board so if we create an import map now all we need to do is import superbase JS and we can guarantee that it will be using this exact version between all of our Edge functions right so you're not forced to use these but we'll be using this today for development purposes I would highly recommend you install Dino this will allow us to get all the proper code hints this is the Brew command for those who are on Mac if you're not on Mac feel free to just check out Dino's installation documentation and you can install through one of the many other package managers like choco or through Powershell or whatever you like if you're on Linux same thing go ahead and follow those instructions and then one more thing I want to mention real quick for those who are using VSS code which I assume is many of you there actually is in fact a dino extension available what I actually did with this repository is I actually added that at as a suggested extension when you load this so likely actually when you first cloned this repo and pulled it up in vs code you would have had like a little pop up that said hey looks like there's some extensions that are suggested do you want to install them if you hit yes at that point then that's perfect you you have everything you need if if you didn't hit yes and you regret it and now you want them what you can actually do is just go ahead and hit controller command shift p to open up your command pallette and just type in recommended extensions show recommended extensions hit that and then on the left hand side here it's actually just showing the the exact same um extensions here that were recommended so for this repo we recommended Dino of course their extension will allow us to do all the proper code hints and intellisense within our index and then prettier for code formatting eslint is kind of like a you pretty much always have this extension for linting and then we use actually Tailwind for our nextjs friend end here so I do recommend that extension as well you're not force to don't feel like you need to but if you do choose any of these I probably would at a minimum choose a dino one just because that will make your life way easier when you're actually doing the intellisense here otherwise you'll probably get errors everywhere coming back to our read me let's go ahead and check out this import map everything should be here already so we don't need to make any adjustments but do note that that is there coming down next we're going to go ahead and start working on our Edge function so what I'll suggest we do here is just let's copy the snippet come back to our index.ts select everything delete it and we'll just start from scratch with the snippet first things first we're going to need access to our super based client because recall this is our process function we're going to process our markdown and then we're going to have to store that data back into our document section table and to do that we're going to use the superbase client similar to what we're doing on the on the front end unlike the front end though which got its URL and Anonymous key from that handy uh M.L nextjs file that did a lot of that magic for us with here we have to actually grab those variables ourselves thankfully it's not too hard we just grab it from an environment variable called superbase URL and superbase an non key the stands for the anonymous key know that these environment variables are automatically injected for you so you don't have to worry about like creating an N file in this case to store these variables The superbase Edge functions are actually smart enough to inject these automatically for you based on your project right so if you're working locally like I am in this case then when you spin it up in the docker image it will actually inject these for you and then as soon as you deploy this to the cloud then your Cloud project will have its own unique URL and Anonymous key and it will actually inject those for you there so this is actually really really nice in my opinion because you can just write your code once and it will work in essentially every environment okay first thing we do within this is check to make sure those variables exist in reality they're always going to exist cuz like I just said they're injected in but the thing is typescript doesn't know that uh according to typescript is a string or undefined just because typescript doesn't know that this environment variable exists so I suppose we could have actually just added an exclamation at the end here to force it to be available but the other option is just to do a check here uh it's kind of like a a type guard best practice I to to ensure that those variables are existed I think I'm going to keep that in here just because it is best practice and say you add more variables down the road that may not actually exist I think it's a good habit to get into checking for these first if they don't exist then sending back at 500 right so at this point their string are undefined but afterwards now that we have that type guard we can have superbase URL and if I hover over that you can see it's now no longer undefined perfect moving on uh step four here you'll note that you might need to cash your dependencies so coming back at the top here the superbase JS for me it just loaded in perfectly for you maybe there was a uh squiggly underline that said hey I uh I haven't cached this file if that's you what you can do is actually just uh hit command shift p and type in Cache and there's a handy Doo function that will just cache all your dependencies what that's really doing is it's just taking that dependency from that URL that we talked about and just caching it locally so that we can get like proper types and proper Intellis sense I'll show you real quick if I just change this to say 21.1 let's pretend that was a real version um now I'm getting this underline it's saying uncast your missing remote URL so I can either hit Commander control dot here to hit the cache button or easier you just hit command shift p or control shift p and then type in Cache and this will just cache all of but I'm going to change that back okay let's keep going here so let's copy this next snippet paste it in and I'll explain what's going on so recall in our trigger function we pass through our authorization headers right this this is that JWT coming from our front end and I told you guys that this is actually a really good practice to forward that to our Edge function so that we can inherit those same permissions right this is that next step we need to inherit those permissions so first of all let's grab the JDT from the authorization header make sure it exists if not we'll throw another error and then we'll go ahead and create our superbase client so to do that we have create client pass in our URL and Anonymous key and then using this Global headers we can actually inject in that same authorization header this is how we can inherit those same permissions and then we'll just actually add one more config parameter that says persist session false this just tells the sub based client that it doesn't need to worry about cookies or anything like that we'll keep going on let's copy this next snippet so next we're actually going to grab that document ID recall in our trigger function we passed the document ID that was just inserted up into our process Edge function as the request body so we'll simply call the rec. Json function to pull that out and now we can go ahead and call superbase do from recall when you say. from this is actually targeting a specific specific table or view in this case we're actually going to Target The View because it's going to be convenient for us to have that storage path available as well we're going to select when you just say select with no arguments that's equivalent to just saying select star so this is equivalent to saying star like that next we can add our filters so you can say filter this is like a wear Clause but there's some convenience filters like eek which means um Quality so check to make sure ID is equal to this essentially and then finally do single only returns one result otherwise it will come back as an array but since we know that we're only trying to fetch a single document ID from our documents table we know it's just going to be a single response so we can go ahead and hit single that way this is going to be a single value otherwise if I took that out we can see that turned into an array just like before we're going to first check to make sure the document exists from the database for some reason that didn't exist that's a strange error but we need to throw we need to we need to capture that situation and also we're going to need to Target this storage object path so also check to make sure that exists first if neither of those exists then we're going to return to 500 next let's actually download that file from superbase Storage so paste this next command as you can see here we're now using the storage subc command and we're saying from files bucket and we're going to go ahead and download that storage object path as you can see here it's going to download it as a blob which once again can be null so we need to check that it's not null here first and then afterwards you can see it's Stripped Away the the null part because we've done that guard since this is a blob we'll call the do text function on it which basically is a promise that returns just the text contents of that file hence why we need to await here and in the end we're we're left with the string so literally this file contents is going to is going to map to the exact contents of our markdown file again in the future if you decide to extend this project and you decide to pull in say a PDF then almost guaranteed you're not going to want to call text here you're going to need to find a way to turn this blob into uh something that you can parse as a PDF there's I know there's lots of PDF parsing libraries out there so you need to work with those Li to understand how to parse that for example okay we're almost near the end here this is a big one I know almost done let's copy this next snippet okay finally we get to process our markdown we already went over this process markdown function so I'm not going to go into that again we're going to just simply pass in the file contents which again is just that raw markdown file here's the process version which if you look into the type of this process markdown is literally just going to return an array of these sections where a section is going to contain our content The Heading and potentially multiple Parts if it had to split those chunks like we talked about earlier from here all we got to do is insert these into our document sections right so we can call superb. from document sections so this is going to Target the document sections table this time instead of selecting we're going to insert and we're going to map through our document sections that we just split apart and pass in the appropriate columns into these document sections right so these here need to map exactly to the document sections column names that we created so coming back up here we have document section so we have a document ID and content in this case coming back here document ID content these need to match exactly those columns now notice I'm passing an array like right I'm mapping over these sections and then returning that array into this insert function and that's intentional here actually postgress and therefore superbase supports batch inserts is what you would call this where you can actually insert multiple records in a single transaction this will return in a possible error if there was an error we should check to see if that error exists probably console error it to the to the terminal so that if we're trying to debug this later on we can view that error and and and use that to help debug and then of course also just return a 500 to our front end in this case our front end is actually not a front end it's just our uh trigger function but this is still best practice and then finally we'll log out uh what we just did like how many sections we split it into and the file name just for informational purposes and then finally last but not least let's actually just return a 204 which is the status code you would send when you have no content as you can see here I'm just passing n to the body because we have nothing to return and there's our Edge function we can go ahead and serve this function by running this next command and this will serve it locally so we'll run npx superbase functions serve hit enter there um fun fact actually Edge functions will get served automatically for you as part of the docker when you say superbase uh start it will actually serve these for you but it's it's pretty handy to actually run this manually so that we can monitor the logs here of course if you're using the cloud version go ahead and deploy this now um we worth noting by the way if you are doing this locally like I'm doing right now um we do have a section at the end of this on how to deploy that to production so basically how to take everything you've done locally and then just push it to the cloud at the end I assume most people actually want to go to production that's the whole point of this video um so all that logic is in place so don't worry like if you haven't been following the cloud version along the way all these steps will be available to you at the end of this read me okay and to finish up the step we want to actually show these files that were uploaded back on the front end right it'd be great if back here on files once you upload it if we could actually see the file appear here so let's go ahead and Implement that logic real quick here so going back to the front end app files page. TSX we're going to go ahead and copy and paste this into the top of the component so once again back here app files page. TSX we're going to go to the top of our component and actually what we're going to do is we're going to replace uh this here this was just kind of a dummy placeholder for our documents we'll replace that with the snippet we're using a hook called use Query which comes from the react query Library it's already pre-installed so no need to install that now it's already there go ahead and hit command or control shift I to import that automatically usequery is just a really handy library to basically manage the life cycle of your fetch requests and it can do things like caching invalidation Etc so it is usually good practice to use a library like this plus the hook just makes it very convenient to use in this case we don't have to create like a use effect where we fetch things and create a state to manage it we can just use a single hook to go ahead and fetch our documents and get them available here within the component so what's happening here we just give it a key this is just a key R query uses for invalidation don't worry about it too much if you're not familiar it just needs to be unique for this query next we're actually going to call superb. from you've seen this a number of times by now so you should be familiar documents with storage path this is our special view that's returns our documents table plus that extra storage path column we call Select with nothing which just says select star select everything we have a data or error if there is an error we're going to raise a toast uh in this case it's going to be a destructive toast that will pop up that just says hey we failed to fetch the documents and we'll throw an error otherwise we'll go ahead and return that data so let's save that and let's go ahead and refresh our page in this case we have no documents actually we did have a document if you recall we had a storage object that we uploaded although I think we did a reset since then so we're going to have to just go ahead and upload the file again in this case it takes us directly to our chat interface but let's go back to files and see if that appeared looks like it didn't so what's going on okay guys I realized I accidentally ran this migration before I had saved this file properly so it acally ran the migration on like some of the schema but not all of it so now that I've saved it properly we need to reun this migration of course you can't just say subbase migration up because it already thinks we're up to date so we'll just have to do another superbase DB reset in this situation hopefully if you save this file properly before you R the up then you should be safe okay because we just did a reset we're going to have to come back here refresh we're going to be logged out of course so let's just create an account all over again not a big deal sign up upload let's upload our file looks like our upload went successfully so let's go back to our files and boom there we go we actually have a proper file coming back so this is great I think one last feature to just put the cherry on top of this is allowing users to click on this and download the file again if they wanted to so let's go ahead and Implement that real quick uh to do that we'll just go in the on click Handler for that guy and uh perform this logic to download it so let's copy that head on back to our page. TSX for our file and then here we have this to-do to download the file let's go ahead and Implement that logic here we'll paste it and then let me walk you through it so number one we're using superb. storage from files but note now we're calling this special command called create signed URL now if you're not familiar with signed URLs let me explain what assigned URL essentially does is well by default your storage bucket is private right right we would have had to add extra logic to make that bucket public to the Internet so since it's private the only way to actually access the files is well when you're authenticated so in our case here when we're using the super based client we're authenticated we've authenticated earlier when we logged in but if I were to try to just navigate to this file directly it it wouldn't it wouldn't let me right because it's it's restricted behind a private bucket so instead what we can do is we can call this handy create signed URL which essentially creates a public URL so as soon as you call this it'll actually return a URL that's fully available to anybody in the world but the idea is you create this URL for a very small period of time so here if you look at this our second parameter is our expires in this is in seconds so what I've done here is basically said create a signed URL and it's going to expire in 60 seconds so this link is only good for 1 minute you can make this even shorter than that if you want or you can make it longer depending on your needs I think 60 works for our use case basically the intent with this URL is that it's only ever going to get used by this client this user and and also that they're going to use it right away so assuming that's the case you can keep this expires in very very short and you might be asking hey why can't I just use download like we did before well the problem with download is it's going to return a blob right and in the browser it's not trivial to open a blob in a crossplatform way if you've ever gone down that rabbit hole I'm trying to make this work in all the different browsers uh it can be sometimes a little bit tricky so in general it's it's typically easier to actually just create a signed URL and then you can simply call window. HF equals that signed URL so basically it's as if you're just trying to redirect the page to that signed URL but since it's just a file that we're going to download it actually doesn't perform a redirect it just downloads a file for us and the browser is able to download that file because it's a public signed URL versus some special file behind a private bucket when you just change the location like this you can't pass in any authorization headers or anything right so that's why sign URLs work nicely here of course if there was an error for whatever reason we'll raise a toast just to to let the user know that something went wrong so if we save that and head on back here we can test it out let's go and refresh the page and then let's click this and now we have the option to save it to our computer all right now that we have our processing logic in place we can move on to probably the funner part of this workshop and that is embeddings so as always I'm going to go ahead and stash my old changes and check out step three just so we have a clean slate at this next step let's go ahead and get started so one thing I really like about how we're doing embeddings in this Workshop is the fact that we're actually using triggers or you can think of them as web hooks to actually generate the embeddings and let me explain what I mean by that so first things first let's go ahead and create a new migration called embed that will create a new file under super base migrations embed let's close our other tabs here so we'll go ahead and copy this function first and let me explain what's going on with this one so similar to our documents migration where we had a function at the bottom here called handle storage update this was a trigger function we're going to create another trigger function uh this time we're going to call it embed and essentially the purpose of this trigger function is to invoke uh another Edge function that we're going to create shortly that's responsible for generating the embeddings so you can kind of start to see the pattern that we're doing here right in a way it's kind of reactive right after records are inserted into the table like we did earlier after documents are inserted into our storage table then we have a trigger you can kind of think about this as a web hook as well that will automatically go ahead and create a document in the documents table and then invoke our process function right so in the same way we'll have a embed function that will automatically get triggered anytime we in this case insert data into our document sections table right you might have noticed when we implemented our process logic we actually never generated the embeddings right down here we just inserted the document ID and the content but no embedding so where do those actually come in now is when that comes in so the reason why I opted for this approach is because a couple reasons uh number one is is mainly just a separation of concerns right we have one Edge function that's just responsible for splitting up that mark down storing it back into the database that's it nothing else and then number two say we were to generate the embeddings right here in this function that could add a bunch of extra latency in time that just adds another point of failure for something that's unrelated to the really what this task is all about so if you're able to I would I would encourage you to try this technique of kind of splitting your big tasks into kind of separate modular Edge functions the one thing to note though of course is there will be a certain window of time where there will be document sections that exist but they don't have embeddings in place right until this other embedding trigger has been executed and then asynchronously those new embeddings will be created right but there will be a window of time where those embeddings will still be being populated in our case that's perfectly fine later on we're going to implement a matching function and if there's no embedding in there then it just won't match but if this was an important issue for your app you could easily add in logic that would just check to see to make sure all those embeddings have been populated before for performing matching Etc so hopefully that made sense if not don't worry uh I'm sure it'll become clear very quickly as we go through this so first off this Ed function we're going to put under the private schema once again just because this isn't something we want to expose it is returning a trigger which means this is a trigger function now as you get into the actual function body here it looks a little bit complicated and the reason for that is it kind of is the reason why it looks a little bit complicated here is just because it's doing some batching logic essentially it allows you to batch insert your document sections so recall back here we talking about inserts we're actually doing a batch insert we're passing an array of sections in here not just a single section and so what that actually means is say this insert had like 40 sections so this markdown file was split into 40 different sections by the time it calls this trigger function it's going to have 40 records that it wants to generate embeddings on right so we need to ask ourselves okay are we going to call this embed function 40 times um are we going to call it one time passing all of our embeddings at once and this single function is responsible for creating 40 of them or is there somewhere in between where maybe we can batch them so that say we we call this embed function four times each doing 10 records to do a total of 40 right so just to give you that flexibility and freedom to do the Bastion as you wish I've designed this function with the batching in place which does make it look a little bit more complicated but hopefully that makes sense now that I've explained why that logic is there now if you didn't want to do batching and you do indeed just want to do them one per embedding or one function that does them all you can certainly do that you just need to set your batch size accordingly and we'll get into that next so scroll down here we're going to go ahead and set up the trigger so let's copy this so we're going to call this trigger embed document sections hopefully that's pretty self-explanatory what it's doing after insert on the document sections again that happens here we're going to go ahead and execute the private embed function uh notice this time though as opposed to last time when we set up the trigger there are no arguments this time we actually are adding three arguments first argument is the name of the column that you want to pull the text content out of to generate that embedding so in our case for document section specifically we have a column called content this is a onetoone match to that column this needs to be exactly the same as that name second argument is talking about the destination column so recall in our documents sections we have a embedding column literally verbatim named embedding so that is the name of the second column that needs to match that column name exactly and then the last argument here is our batch size which is what we just talked so in this case I'm going to set the batch size to 10 which means again if say we had 40 sections it will actually call The Edge function four times with a batch of 10 each um you can customize this as best fits your application one more note real quick for those who are like to know the nitty-gritty details notice that for this trigger we're saying for each statement and that's actually different from the last trigger we did which was for each row so what's the difference between for each row and then for each statement when we're saying for each statement it well kind of as the name suggests it's not actually on a per row basis it's actually on a per batch basis in our situation right coming back to our process here we're inserting a batch of sections so here when we're saying for statement it's actually going to take all of those again let's say 40 sections and execute this trigger function one time for that entire 40 batch and this whole referencing new table has inserted this is just the Alias we're giving it so that within this function we have like a temporary table to grab those new records from had we said for each row then this would literally get called for every single document section one by one and we just wouldn't have the opportunity to do this more advanced batching logic if you wanted to use that okay next on the list everything I just explained by the way is all in this readme so pretty much nearly everything I've talked about should be explained in detail in this readme so feel free to come back to this on your own time and go through it if you ever need to kind of refresh so that's actually it we're done that migration that was a pretty quick one thankfully so let's go ahead and spin up that migration we'll save the file for this time run that ah yes we have an issue where since I did the git checkout to a new checkpoint there actually have different migration names forgot about that that's easy though we'll just do uh npx super base DB reset as always if you're ever in doubt and you need a clean slate this DB reset command comes in really handy cool so we have a clean slate come back here refresh it's again it's new slate so we'll log back in now we're not going to test this yet because we're not done uh of course we have this trigger function that's meant to call our embed Edge function but of course we haven't created this Edge function yet so let's go ahead and do that now now so we'll copy this command we'll call The Edge function embed just like before that will create a folder under functions called embed with an index.ts we'll copy this code and again just like last time delete everything that's there and paste that in so this is new stuff let me explain what's going on first of all this Edge function is responsible for generating embeddings how are we actually generating embeddings well for those who are maybe familiar with open AI they have an API to generate embeddings right so that is an option here we could have called a third- party API I mean there's there's many of them out there nowadays but it turns out that superbase edge functions actually support generating embeddings directly within the edge function so we're going to do that today and what do I mean by directly in the edge function I'm talking about literally the model itself is going to get loaded into this Edge function and we're actually going to perform inference directly in the edge function if that sounds crazy to you then I don't blame you it's actually quite incredible that we can do this essentially what we're going to be using is a package called Transformers and Transformers is well Transformers is one of the most well-known py on ML packages that exist but our good friend Joshua at hugging face has actually ported this over to JavaScript and we can actually now run this within JavaScript so here it is feel free to check out their docs under the hood they're using a inference engine called the Onyx runtime if you do have any experience on the python side you might be familiar with something like pie torch or you might have heard of tensor flow think of Onyx runtime as another inference engine just like these ones are uh the difference though is the Onyx runtime can work in many different languages many different platforms that's one of their big things is they focus on the crossplatform aspect and once again for those who like to go really under the hood and wonder like how does this actually work Onyx runtime for different platforms of course is doing different things in the case of the web and Dino it's using web assembly under the hood to actually perform that inference so here what we're doing is we're configuring Dino we're just setting some options that are required for the dino environment today for example we're not in a browser so we don't want it to try to use a browser cache that doesn't exist Etc next step is actually to create the embedding function so this right here isn't actually generating the embedding but it's generating the embedding function and if you're familiar with Transformers they have this concept of a pipeline think of it kind of like a function Builder pattern where you call pipeline first parameter is the type of inference you're doing so in this case for embeddings we call this feature extraction but there's many different types of inference out there Beyond embeddings and then the second parameter is the model that we're going to use So today we're going to use that GTE small that we were looking at earlier but you can of course replace this with whichever model you prefer do keep in mind though that the size of the model will affect how quickly it'll be able to load on the edge right so in general if you're able to use smaller models that's usually better for performance and then also in case it wasn't clear these models are getting pulled directly from hugging face okay moving on we'll come down here and copy this next section I'm not going to go over this again cuz I think this is pretty much identical to what we did on the last Edge function where we have these environment variables that we grabbing that have been injected in we'll do the exact same thing with the authorization headers that we did last time so that we can once once again inherit all the permissions and privileges that that user had that invoked this originally next we actually need to fetch that text itself so let's copy that so what this is doing is this is kind of a dynamic function you can think of right so super base. from but instead of specifying a table we're actually dynamically injecting that table from whatever the trigger passed to us right we're intentionally designing this Edge function and Trigger so that you can actually apply this to pretty much any table you want you might have noticed in the way we design this it's it's very general purpose you can pass in whatever table and column s you want and so naturally within our Edge function we want to continue with that we want to be able to dynamically pass in which table this is coming from and the columns that they're coming from thankfully with the superbase CLI this is dead simple we just say from passing the table select the ID that is one requirement the table will need an ID and then the column only fish the rows for the IDS that were passed in right the current batch and then also just make sure the embedding column is null right if that embedding column was already filled in for whatever reason uh there's no point in generating that embedding again right okay now for the fun part let's actually do the embedding generation paste this next snippet here let me walk through it so we'll go row by row again this is a batch so we we'll go through all the different rows that were part of this batch in this case it'd be like a batch of 10 so we'll grab the actual content this is actually the string text content coming from that table we're going to call generate embedding which recall this is that function that we built earlier from the pipeline passing in the first parameter is the text that we want to generate the embedding on the second parameter is some options and we need to do two things here number one we need to set pooling to mean and we need to set normalization to true now this might be a repeat for those who have heard me talk about these options in the past but I'll repeat it again I'm sure many of you would like to hear about this so first off pooling what does that mean pooling well to understand this we need to get a little bit more low level here right when you're generate embeddings and you're using an embedding model to generate sentence embeddings which is what we're doing here the model itself is not actually outputting one single embedding that represents that whole sentence or paragraph or whatever you passed in would actually get outputed by the model is a whole bunch of embeddings and actually what it is is a whole bunch of different embeddings one for each token that you went in originally so to get that final sentence embedding you actually need to pull all those token embeddings together into one and there's different methods to pull them the most common being mean pooling which is taking the average of them all and that's what's supported with Transformers JS today and for the GTE small model specifically they use the mean pooling by default so we'll continue using that here so this is important right if you were to leave this out and and generate the embedding you'll notice that the output is actually not one single embedding but like an array of a whole bunch of embeddings and that's for each token so don't forget this part it's important and then the second part is normalization right so without getting too deep into like linear algebra Theory right now if you think about your vector it's got a direction and length in whatever n dimensional space it's in and if you normalize that length that is if you take that length and turn it down into a length of one that turns it into a unit vector and that's called normalization why is that important well later on when we calculate similarity so if you recall me mentioning we're going to use dotproduct distance functions like dot product will actually only work when your vector is normalized it has to be a unit Vector if it's not a unit vector and you try to use dot product the outputs that you get will be just completely off and won't actually represent the proper similarity or distance that you expect so if you're planning to use something like dot product your embeding does need to be normalized in general I recommend normalizing it cosine distance on the other hand doesn't require normalization but when your embedding are normalized cosine sign similarity and Dot product will actually produce identical results and the dot product algorithm is actually simpler requires less math under the hood so that actually translates to better performance okay moving on the output of this is actually a tensor under the hood this is a wrapper around the Onyx runtime tensor again we're going to need a bit low level to grab the actual embedding in this case in JavaScript this would be a number array we just got to say AR Ray Dot from the output data and store that into here we're Json stringifying it just because when we pass it up through our superbase client typescript expects it to be a string so we'll just stringify it in this case to make typescript happy feel free to to alter that it's not a big hit on performance but if you're worried about that feel free to just do casting if you want so of course let me explain this real quick we're going to do a from that Dynamic table again and this time we're going to call update we're going to update that embedding column with the embedding that we just generated of course only apply that embedding to the current ID that we're iterating through right now right if we don't say this this would update every single embedding in our table that would be really bad don't forget your filters finally we're going to return our response just like last time and again it's a 204 no content we're not passing anything in the result here just cuz we have no need for that now if you are running on the cloud go ahead and deploy this function now if you're not there'll be the opportunity to deploy this later on so that's it but before we move on to the next step let's quickly test this out to make sure everything's working as we expect so we'll do an npx superbase functions serve again this way we can monitor the logs for our Edge function this serve does serve all the edge functions by the way so simply running this we'll we'll see logs for all of these so this point we kind of have a sequence of events that we need to consider right everything starts from just a simple upload to our storage table that causes a trigger to create a document as well as process that document which in the edge function will generate document sections and then we have a trigger on the document sections that will call the embed function and then go ahead and asynchronously create embeddings on each of those document sections so let's go ahead and upload a file and see if all those steps happen here we'll pull up our studio here so that we can kind of monitor this in real time we have no documents or sections right now so that's perfect let's go ahead and upload our file so we'll start with Roman Empire 1 perfect we'll come back here quick document there's our Roman Empire sections okay our trigger has already ran it's already parsed all these different sections out and put them into our document sections table look looking good notice our embedding Vector column is null at first right cuz these are done asynchronously through that last trigger we'll refresh refresh oh we have some coming in so for some reason we're actually not getting we got some embeddings generated down here but only three why is that ah well here's our problem guys I accidentally put our return response within our Loop so basically after the first row it just returned instead of continue with the rest and the reason why we still got three is cuz recall we had batches right so we call this Edge function three different times in this case um whoops let's put that out there I'm going to go ahead and do a full DB reset just so that we have a clean slate so I can really show you the expected output in the table okay we're reset refresh create our account again come back here we have a blank slate let's upload our Roman Empire 1 come in here there's Roman Empire 1 already went ahead and created those document sections looking good let's refresh refresh they're slowly trickling in here and there we have it we have an embedding generated on each and every document section so to finish off this Workshop we're going to just implement the last piece here which is going to be chat as always let's stash our old changes and then check out step four once again we will need to do a DB reset I already know because those migration names will be slightly different okay and this last piece is going to be mostly front end focused we're essentially going to come into this chat interface and and implement it as you can see right now hello can't do anything can't send it off we haven't implemented any of this logic yet let's go ahead and do that so first things first I want to point out that the way we're going to implement our chat today is by actually generating embeddings on the front end within the browser so first of all why do we even need to generate embeddings again like didn't we already just do all that well we did we did that for our file contents but there's actually one more place that we need to generate embeddings and that is for our user message right whatever you type in here what kinds of food did they eat this message itself this query needs to actually be turned into an embeding and as we talked about before the reason for that is because this is how we're going to determine which pieces of our content which document sections relate most to this right we need to have them both in the embedding space to determine how similar they are to each other and those document sections will be the ones that we actually inject as context for our large language model GPT 3.5 so so the options are we can either generate embedding directly in the browser which again I think is incredible that this is even possible as I explained before we'll be using transformo JS which uses the Onyx runtime under the hood which uses web assembly to accomplish this so that's why we're able to actually do that in the browser I want to show you how to do this on the front end just to switch it up and show you another option for generating embeddings if you weren't interested in this approach and you would have preferred to actually generate the embeding for this message server side within the edge function you can certainly do that as well but since I've already showc Cas that I want to demonstrate how you do this on the front end so of course to do this on the front end we need to install the zenova Transformers package up until now we've only been using that on the edge function so let's install that on the front end and then we're actually going to also install vel's AI SDK and this SDK is going to help us basically set up our chat interface very very easily and seamlessly along with the ability to stream the response back to the front end from open AI let's copy this go ahead and install them next we need to set up our nextjs config specifically webpack we need this extra extra configuration just to tell weback how to deal with Transformers JS copy that and this is going to live under next. config.js and we can add that just as another key here under next config so that's looking good all right next let's import some dependencies we'll use this is going to be on the chat SL page route so we're moving on onto the chat page copy this once again app chat page we'll import these at the top and let's get started so just like before we're going to create a super based client at the top copy that inside the top of our component we'll paste that there recall this is creating a single tin under the hood so it's actually reusing the same superbase client from the other page next we're going to use a new hook called use pipeline let's copy that so let's pause for a second and talk about use pipeline so use pipeline is a hook that I actually built right into this repository fun fact we're actually planning to make this available as its own mpm package soon but for now the implementation is actually just going to be directly within this Workshop essentially what used pipeline does is it uses that same pipeline method that that you saw on the edge function right this should look very familiar the first parameter is the task type and then the second one is the model so it's almost like calling pipeline the difference is it does a lot of convenient things in react such as number one when you're performing inference within the browser best practice would be to actually perform the inference in a web worker so basically another thread and the reason why is because inference can be potentially very computationally heavy so if you were to perform that logic directly within the UI thread then your web page itself will actually get blocked and potentially lag and slow down so that's not good practice we want to split that out into its own thread and then number two just dealing with asynchronous pipelines in react requires a little bit of extra work basically you have to have either some state or a ref under the hood to keep track of your pipeline and basically all that's handled for you within this hook so under the hood this thing will actually spin up a separate web worker thread to perform the logic it does all the message passing back and forth between the thread for you you don't really have to worry about a thing also manage the asynchronous pipeline for you so basically this is just either going to be a pipe function or undefined all you have to do is first check to make sure it's defined before you use it and you're good to go so that's what use pipeline is doing for you and as always this is outputting a function we're going to call it generate embedding that we'll use later on to actually generate that embedding oh and as noted here it's very important that this embedding model here matches the one that you're using on the server side if that wasn't already obvious you can only be comparing embeddings that were generated within the same model right also you got to make make sure that they're also either normalized or not normalized more likely you've normalized them but you need to be consistent on both sides and you need to pull them the same way of course so the mean pooling it's important that these are identical any place that you generate the embedding whether it's front end back end or wherever if you plan on comparing them to each other right if you're using different models then the embeddings will be in completely separate embedding spaces right and they'll be meaningless to compare them with each other okay next we're going to copy this snippet here for use chat I'll explain this in a second uh we'll replace our to-do here for the messages because use chat will actually wrap all of this for us now so let's paste that so let's talk about use chat real quick where does use chat come from as you can see up here use chat's actually coming from that AI package I told you this is a SDK created by vercel verel is the creator of nextjs and essentially what use chat does is it's a very convenient wrapper around basically a typical chat setup that you'd be using with open AI or any other language model so it does things like it handles your message state for you conveniently it gives you like your input and your change handlers for your input so you're still in control of like your interface and how you chat with it which we'll Implement that down below but basically they're managing all like that logic and and state for you under the hood and as we'll find out shortly actually use chat manages streaming for us which is actually quite nuanced and complicated if you try to do this manually so this AI toolkit will really simplify that process so not much to this the one thing to note of course is the API URL we need to set this to our Edge function which will create shortly after this one last Edge function called chat and this Edge function will be the function responsible for taking these messages from the client forwarding them through to open ai's GPT large language model and then of course streaming that response back to the front end you would never want to connect to open a directly from the front end first of all Kors probably prevents that which we'll go into chors in a second here and then second of all you'd be leaking your open AI keys on your front end which I have seen people do this already and it's not a good time for them so yeah rule of thumb never use secret private Keys like open AI gives you on the front end never connect directly to open AI from the front end you're going to want to go through your own trusted backend layer in our case Edge functions are literally perfect for this so unlike the superbase client which automatically infers this next public superbase URL environment variable that we created at the very beginning for us in this case since this is a different SDK uh we'll just need to manually pass that in there not too difficult just grab it from process. M and then tack on functions V1 and then the name of the edge function we'll call this one chat uh and we'll create that in just a little bit right we have an is ready function as well let's copy this and just replace the one that's here this is basically a Boolean that we'll use to know whether or not to enable or disable our send button basically we only want to allow people to send the message once our generate embedding function has loaded we'll go more into that in a second okay in our input we're going to go ahead and set up these props so we can copy this on down to our input and I'm just going to actually just replace the whole thing that essentially what we've done is we added a value and an onchange anytime user types anything in here it's going to call this handle input change which verell handles for us through their AI SDK and then of course since this is a controlled component we need to pass the input that was just changed back to it so that the input's updated okay now let's go into our submit Handler so when someone hits send we want to basically handle that logic so let's copy this come into here so we have our form on submit this will get called anytime they hit that as send button down here so let's go ahead and paste that logic and we'll go through it so first things first let's make make sure that generate embedding function has loaded basically under the hood when you call pipeline from the front end it actually has to go ahead and load that model from hugging face and if this sounds really inefficient to you um it's actually not too bad I would say because yes the first time it has to load that model which could be big yet another reason why I was recommending that you prefer smaller models when possible so that first time load is is not too crazy but the good news is Transformers JS will actually cach this in your browser if you didn't know there's actually a proper caching API available here under cash storage Transformers there we go look at that I've already loaded this once so that's why this is here already actually I can delete this cache entry so I can show you what it's like for the first time but essentially you can see here it's actually caching the different files involved in that model one of these is the model itself and then some of these are just some configuration so what this means essentially is once this is cach the first time as soon as I refresh the page the the model loads instantly cuz it's it's pre-caching and available okay once we checked to make sure that's not undefined now we can go ahead and call our function just like before you pass the text input as the first parameter and then the second parameter is our options as we just talked about we need to set these to the exact same thing we did on the server side so this would be pooling mean and normalize true again just like the server will convert this into a number array and we'll send it off to our API what's going on here well since we're sending this to an edge function we're going to create this in a second called chat you need to be authenticated of course right to connect to that edge function so normally when you're using the superbase client you don't have to worry about this right because all that logic is handled for you within the super based client however since we're using once again this use chat and the AI SDK we're going to have to actually just do a little bit of logic to pass in that authorization Logic for us so to do that we can simply just call Super base. o.get session that grabs the current session which has an access token available this is the JWT and we can simply just pass that in as an authorization header to our submit route again remember handle submit comes from this use chat and we're good to go from there now under the hood this handle submit actually will send those messages for you so uh recall the use chat Hook is keeping track of all of our messages for us the inputs Etc so when you call handle submit actually by default if I just got rid of all this it would already be sending all those messages up to the server on our behalf the reason why we need these extra things is for number one what I just mentioned the authorization piece and number two SDK actually allows you to pass in extra information Beyond those messages in our case we're actually pass in that embedding itself that we just generated because in this case we decided to generate the embedding client side if you didn't want that again and you wanted to to generate the embedding server side you actually wouldn't need this at all you could just send it up as is and then you'd be responsible for generating that embedding using the latest message on the server side but since we're going client side we'll pass that in through the body prop okay last but not least we'll do that is ready disabled logic that I mentioned earlier so we'll copy that come down to our submit button right this is that send button as you can see right now it's disable abled can't do anything because we said disabled is ready actually I realized this is already in place so you don't need to copy and paste this but just to explain it real quick recall this is ready becomes true when that model is available so basically keep that button disabled until that's loaded and then it will enable the button for us let's refresh the page and see what happens and there we go our send button went from disabled to enabled so that was pretty quick right that's how long it takes to load when it's cached if I were to delete this and then refresh it take just a little bit longer there we go now it's loaded so I'll let you decide whether or not this is something you think is worthwhile doing on the front end okay next up we'll create our SQL migration this will be our last SQL migration in this workshop and the purpose of this SQL migration is to create while one last function this is a postgress function that we're going to call match document sections and this function is going to be responsible for actually doing that matching logic essentially it's going to be doing that similarity search so let's go ahead and create that new migration migration lives under superbase migrations here it is and we'll copy in this function so let's go through this match document sections what's going on so this function takes two parameters number one we're going to call embedding this is basically our query embedding the embedding that we want to compare against our database to see what matches with it so specifically in our context whatever they type in here hello world this will be converted into an embedding and that embedding will pass into this function as the first argument second argument is the match threshold basic the way PG Vector works is in addition to this new Vector data type PG Vector introduces three new operators and we briefly touched on this earlier right The Operators look like this this one specifically with the number sign hash is the inner product or dot product operator there's also ukian distance and there's also cosine distance and so the beauty of PG Vector in my opinion is it just turns all this new Vector search Vector similarity logic into just plain old SQL at the end of the day so if if you're familiar with SQL do whatever you would have normally done in SQL just replace it with these new PG Vector operators and you're good to go so for example let's think about this we want to return document sections that match our embedding as similar as possible that is let's order our results by those that are most similar first ranking down to those that are least similar in SQL this is just a simple order by Clause right order by document sections embedding product are query embedding and the result of this operation right here is going to be one being most similar and negative one being most dissimilar and actually for inner product specifically I should mention cuz this this will almost definitely trip you up this is actually not inner product this is actually negative inner product with PG vector and the reason for that is because the way indexes work under the hood they're a bit nuanced and we need to be returning the results in ascending order so assuming you want the results that are most similar they actually needed to implement this as a negative inner product so that when you order them ascending they come back back in the order you expect right so actually this would return ne-1 if it was most similar and return one if it was most dissimilar right so ordering that by ascending will actually go negative - 1 to positive 1 and we get them in the order we expect nothing crazy just it is worth pointing that out so that doesn't trip you up later okay great so now we're ordering by those that are most similar using regular old SQL that's amazing but coming back to this match threshold we're going to implement this as well which is basically saying only include results that are within an expected match threshold and without this essentially what's happening is you're returning everything we're going to be returning every single possible document that this user has access to granted in ascending order so we're still getting the most similar ones first but it's going to keep going all the way down till records that probably aren't very similar at all right and let's just say that perhaps only a couple were actually very similar and the rest starting at like the third record onwards were like not similar at all we probably don't even want to include that third result right but how do we know if if it's going to be the third one that's suddenly not as similar or the fifth or the 10th right so that's where this match threshold comes in where we're basically saying I expect the similarity to be at least let's say Pretend This was 0.8 I expect the similarity to be at least 0.8 in order for it to be considered similar of course you would just need to do a bit of trial and error and adjust this threshold according to your needs for this project we set it to 0.8 and if they're not within that threshold then don't even return them at all right so let's say later on we'll have a limit Clause as well we don't need to do that here we can do that Upstream but let's say we limit by 10 records what we're saying here with this match threshold is sure only limit by 10 records but if everything past the first five are below 0.8 don't even return those so we maybe we only come back with five records and that's just to say like let's only make sure we're including results that actually truly are similar and we don't accidentally inject context to GPD that is completely irrelevant right hopefully that made sense last piece of course we're negating our match threshold once again just because this is a negative inner product and once again I mentioned this earlier but if you are using a embedding model that's not 384 Dimensions just make sure you do replace that with whatever the dimension size is by the way if you are curious to learn more about the negative inner product thing and cosine distance and how all that works here I do have a little section within the workshop read me here feel free to dig through that if you're interested all right and we'll uh spin up that migration we are saved perfect and if you're working off the cloud go ahead and run the DB push command okay the final piece of the puzzle is is going to build our chat Edge function since we're going to use openai as our large language model you will need to generate an open aai API key feel free to follow the link in this read me this will take you directly to the openai API Keys page where you can generate one for this project in general when it comes to Keys uh really when you're using keys anywhere I do recommend that you create one key per situation per environment per app project and a combination of those so for example I'll create a separate key for Dev versus staging versus production and the reason for that is if your keyword ever get leaked for whatever reason the less places you're using it the better right you don't want a key that you're using in both development and in production to get leaked and then now it's not a simple key rotation for your local development machine you have to actually replace the key in production so make your life easier and uh create more keys than less once you have the key you can copy and paste this command uh this will create ourm file let's close some tabs here close close close let's go ahead and and run that command that will add a m file to your superbase functions directory when this file exists in the functions folder The Edge function runtime will actually pull that in automatically so take that key that you just created and just paste that here next up we'll create a new Edge function called chat there's our new Edge function and as always we'll just replace the contents so this should look pretty standard at this point the only difference now of course is we're creating a new open AI client passing in that open API key from our environment and then everything else is the same so we'll copy our next snippet this one is a bit new this code here is actually needed for Cores purposes if you haven't had the pleasure of dealing with cores uh cor stands for cross origin resource sharing basically the gist is when you're building an app within the browser and you're making a fetch request from within your web page to another domain that's actually not allowed by default for security reasons right browsers don't want you to be able to just like arbitrarily fetch information from some other domain unless that domain explicitly allows it how do you explicitly allow it this is through these chors headers so what we're doing here is we're basically just saying allow star so basically allow any domain to access this endpoint specifically we have to say which headers we're allowing and then down here we need to handle the HTTP method called options the options method which is what Kors uses under the hood to determine if you're allowed to use that domain all we have to do is handle the options method if it is an options method then just pass back these chors headers and we're good to go this is great for development I would say though in production probably it's a good practice to restrict these Origins to the actual production domain that you're hosting your front end on that's just a usual best practice rule of thumb okay next up we'll create our super based client we've done all this already once before so let's paste that in there as always pulling in the authorization header inheriting those permissions and creating our client all right now let's get into the meat of this let's copy this snippet so let's go through this line by line first off await request. Json we've done this before this is essenti taking the request body that was sent to this Edge function so in this case it was a Json containing these four things the chat ID message messages and since we have that extra prop that we added in the embedding as well next up we use our super based client to invoke our match document section function now we haven't done this quite yet right up until now I've shown you like the from and we've used dot storage and we haven't used RPC yet so what is RPC it stands for remote procedure call um essentially r R PC is just literally a way to execute postgress functions so since this match document sections function was created uh within postgress like we did back here and also since it was created the public schema which is the the default schema that allows us to actually call RPC on it the first parameter of course is the function itself and then the second parameter is the arguments that you pass to that function so embedding being the first argument and then match threshold being that second argument and as I noted earlier we're using 08 for this project one important thing to note about RPC is the result of this RPC can basically behave like another table itself in this case take a note of the return type here right right now in this function we say returns set of document sections what does that mean well that means the return type of this postgress function actually will have the shape of a document sections so essentially for all intents and purposes the result of this function is equivalent to querying the document sections table that's the return data type so what that means actually in this context is we can continue to to chain multiple operations as if this was just a regular table that we were pulling from so specifically here we can say do select so instead of saying select star or just empty which means select star we can say select content which means actually only pull the content column from the document section and then we can do things like limits so we'll say limit five because let's say we had like 50 plus records that matched that almost guaranteed won't fit within our prompt right right now we're just going to arbitrarily limit it to five likely you might want to consider making this a little bit more sophisticated maybe you pull a little bit more than you need and then you can add extra logic down here that will trim it up but for now just to keep it simple we're going to limit to five records of course check to make sure that we actually got a match and then next step is the prompt itself let's go ahead and copy the snippet and paste it in okay first thing let's look at our prompt since this is a chat-based interface if you've never worked with open ai's uh chat completion models before basically it's an array of messages that you have to form together and for each message in that array It's actually an object containing two things number one is the role so is this the user talking or the assistant talking you actually have full control over that message history and who's saying what when and then you have content which is the actual text content for that message so let's quickly read through our prompt you're an AI assistant who answers questions about documents you're a chat bot so keep your replies succinct you're only allowed to use the documents below to answer the question if the question isn't related to the documents say sorry I couldn't find any information on that if the information isn't available in the blow document say sorry I couldn't find any information on that do not go off topic followed by kind of like this heading documents colon and then the injected documents where does injected documents come from well we build that up here ahead of time basically we just make sure that there's actual documents that were returned from our match function and if so we'll go ahead and map through them grab just the content and then join them with two new lines so basically for every piece of content that it fetched it's just going to concatenate them with two new lines between each one otherwise it'll put in no docu doents found and it will actually inject that down here so that the language model has that context oh there was no documents found it would basically encourage the large language model to basically reiterate that to the end user so that's the first message in our list of messages um everything after that is just dot dot do messages and this basically means inject the other messages that we've had so AKA back here if we had a conversation back and forth multiple messages all of those messages get injected in here so basically what's happening kind of under the hood here is the very first time the user asks a question they'll ask her a question here that will get sent up to the edge function as a single message it'll basically inject in this first message which is our prompt and our injected documents essentially this is what's performing our retrieval augmented generation or rag and then we just pass in that new message from the user from here we'll add some more code in a second but open AI will essentially reply to that send that message back to the front end and we'll now have two messages here right now here's the important piece the next message that the user sends will get sent to the the back end along with the other messages right if this is your first time using large language models or open AI chat completion model specifically it's important to know that there's no memory going on there right every new request to open ai's GPT models you got to think of them like a clean slate they have no memory of previous conversation history so actually in order to achieve this idea of a multim message Chat history well we just send the entire message history every single request that's that's essentially it there's no real other magic going on there the same applies everywhere you see these chat models right assuming you've used the actual chat GPT interface they're doing the exact same thing under the hood right every single time you ask it a question it's actually resending your entire message history along with it or at least as many messages fit into the context window now the only small cave I should add to that is open AI did just release the assistance API which actually does have a true understanding of threads so basically from the API perspective you don't have to send your message history every single time over and over again but still under the hood they would be sending that whole message history to the model it's just a convenience thing for today we're not going to use the sance API just so you can have full control over that message history but you might consider using that in the future so what we're doing is we're passing those same messages every single time and then we're always injecting in this prompt every single time at the beginning so next up we're going to actually perform the completion itself itself oh and fun fact one thing I never really touched on was this code block this is a template tag essentially what this does is it if you put these template tags in front of a template literal in JavaScript a template literal is a multi-line string where you can actually inject variables this code block specifically is a template tag that comes from the common tags Library super handy this library and what it's doing here is it's basically stripping all these indentations right if I didn't have this here then like notice our indentation here right since this is a multi-line string actually everything over here is also getting included in that string by default right that's kind of what you'd expect on a multi-line string so what that means though is when we send this to open AI it's going to include all these extra tabs when really it shouldn't right so with without that template tag function we would pretty much have to do something like this in order to ensure there's no leading indentations everywhere and personally I I just don't love that it breaks our nice code formatting so essentially we we can get away with doing it this way if we had the code block tag in front uh which will strip away all these indents including our injected variables okay so the completion itself recall we're using open ai's Library here comes from the open AI package and again just a quick reminder we are in Edge functions so the reason why we didn't have to put a UR L here is because we have this import map in place right remember this guy open AI is mapping to this URL so coming back down here to perform the inference we're going to call open. chat. completions doc create uh we'll pass in the model our model today is going to be GPT 3.5 turbo you can change this to GPD 4 or whichever model you choose of course GPD 3.5 is much cheaper than 4 so my general rule of thumb when I'm choosing the model is see if 3.5 gives you the results you need and usually you can get quite a long ways with it and then only once you need that really Advanced reasoning or some more advanced situations would you maybe need to try GPT 4 but I would certainly try 3.5 first if you're able to of course in the future there'll likely be more models that will change this rule of thumb a little bit but I assume this will still apply going forward as there's going to be some cheaper models versus more advanced models um another thing to note real quick is today 3.5 is much much faster than four so in a chat user experience like this you would definitely want to on the side of 3.5 if it can do the job because otherwise your users are waiting quite a bit longer to get a response last piece here is a 0613 this is a model version in general in production this is a good practice to tag this on here versus uh like this would work too without it but what this does is it locks it to this current version right it's important to remember that open AI is continually improving these models over time and so they very well could actually release a new version of 3.5 turbo which they have like there there was a version before 0613 they could continue continue to do that and if you've designed your application around a certain model with certain expectations on what is returning and then they change that behind the scenes that could completely mess up or alter your application right so best practice as a general rle of thumb for software but especially here for models to lock in that version okay messages these are the completion messages that we just went over Max tokens this is talking about the max number of tokens that can be sent back GPD 3.5 turbo has a context window of 4,096 tokens uh we've talked about tokens and how tokens differ from like words or characters so you should be familiar with that by now but in terms of context window let's talk about that real quick so GPD 3.5 turbo has a context window of 4,096 tokens that context window limit includes both input tokens and generated output tokens and that's important to know right so what I mean by that is say between all these messages that I sent all of them together added up to 4,095 tokens that means I'm actually only leaving one token left for it to respond with right so that Max token limit is shared between both input and output so keep that in mind when you're considering how many tokens you want as your input specifically this one here is actually talking about what are the max number of tokens I want it to respond with right so as opposed to that Max context window I was just talking about this Max tokens is specifically referring to the generated response why would you want this well tokens do cost you are charged per token so from that perspective you might not want the model to give you this massive responsib maybe you want to encourage it to keep it shorter maybe you want to leave enough room for your input tokens so you don't want it to try to generate too many tokens as the output in general I would maybe start with something like 1024 and then if you're finding that it's continually getting cut off you can increase that accordingly temperature if you've never worked with these models before temperature is talking about how deterministic the response is what does that mean well deterministic in this context means if I give it the exact same input will I get the exact same output and when you set the temperature to zero that means it's fully deterministic meaning if I send the exact same message every single time into the model I'll get the exact same output every single time any temperature above zero and I believe temperature can be between 0 and 1 I think the way a lot of people like to think about it is a high temperature is like a more creative model and a low temperature is more deterministic so any temperature above zero will actually produce different responses given the exact same input so in general when you're using these models within your applications unless you have a good need to increase temperature I generally stick to zero as a default just because as you're developing your prompt as you're developing your application you really do want that deterministic behavior right if you're experimenting with different prompts trying to get them mod to respond in a very specific way and it's actually responding in two different results with the exact same input you might go crazy so in general I would default this to zero and then you can raise it if you have a need later on finally we have this parameter called stream hopefully this one is pretty self-explanatory if you've ever used chat GPT like you would have noticed that the response from the assistant is coming back word by word versus like loading for a long time and then waiting for the entire response all at once it streams the results back token by token essentially in our case we're building a chat application so that's a great option to enable when would you want to use one over the other well for a chat application I would pretty much always turn it on because you want that Snappy user experience right if you turn stream on then as soon as it even has one token it's going to start sending that back to the front end and your end user will get some feedback otherwise if they have a big parag message you might be waiting a long time and to the user it just feels like your backend is super slow of course if you're building a different kind of application that's not chat based and you require that entire response back before you can do anything with it maybe you're getting an open AI to produce some Json or using the function call Api in those situations you pretty much wouldn't be able to use streaming okay finally we just do two more things here we actually wrap our completion stream coming from open AI in this open AI stream where does this come from this actually comes from that AI SDK so that same versel SDK that we were using on the front end they actually have some backend functionality as well that makes it really easy for us to stream that response back so they support multiple different language models open AI being one of them so that's why we have to wrap it in this one and then from here we can actually send it in a streaming text response this also comes from that AI SDK so we pass that here we do need to pass the chorus headers as well anywhere we do a response we need to be uh passing those CH headers through and it's that simple kudos to verol for creating this AI package these streaming wrappers actually make it really really simple to deal with if you've had the pleasure of trying to implement this without this Library there's actually quite a bit more work involved to deal with like server sent events and managing those so this is quite nice one last note real quick if you are using the cloud you will need to set that open AI API key as a secret recall we just set that here in our uhm file but of course that would not work if you're working in the cloud so you can set that using this command here and then of course deploy the function here so that's it we're done let's try it out so let's go ahead and serve up our Edge functions again so we can monitor the logs npx superbase functions serve looks like we did a DB reset so let's go ahead and upload some files I'll close the Chrome Dev Tools looks like our embeddings are coming through I'll actually go ahead and upload all three here all right and let's go ahead and chat so some things we could ask it for example what was the most common food let's do that most common food in ancient Rome was cereals and legumes awesome what's another one what did people do for fun let's give that a shot hoop rolling knuckle bones board games cool beans let's do a followup question I wonder if he can say uh where is Circus Maximus Rome Italy okay I guess that's expected so there you go guys chat GPT with your own files hopefully you learned something new today specifically I hope you're able to learn learn that full end to endend from scratch building an application like this if you want to go a bit further feel free to move on to step five this is a bonus step this is something I'll let you do on your own time you might have noticed that up until now we haven't had the best typescript types coming back from our database that is uh when we have a document or a document section it'd be nice if the objects that came back from those queries actually literally had typescript types that matched their column structure and data types right you can actually do that with superbase you can run superbas gen types typescript and it will actually look at your database and scan the tables and basically generate typescript types for you based on those so if you were to continue on I would highly recommend doing that having those types are very useful and can basically help stop you from shooting yourself in the foot of course after that feel free to follow the going to prod production if you've been developing locally all the steps you need to do that are here including pushing your database migrations uh setting up those Secrets deploying your Edge functions it's all here and then finally if you're interested in extending this application we have some suggestions here of some additional things you can do that would be pretty neat feel free to try those out or just use your imagination I'm actually really curious to see what you guys come up with thanks again guys and I'll catch you next time
Info
Channel: Supabase
Views: 14,603
Rating: undefined out of 5
Keywords:
Id: ibzlEQmgPPY
Channel Id: undefined
Length: 131min 59sec (7919 seconds)
Published: Tue Nov 21 2023
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.