[{"data":1,"prerenderedAt":286},["ShallowReactive",2],{"DefaultLayouten":3,"language-blog-slug-simplifying-search-using-ai-in-our-knowledge-base-i18n-slugs":-1,"language-blog-slug-en-simplifying-search-using-ai-in-our-knowledge-base":134},{"app":4,"menu":31,"footer":66},{"githubUrl":5,"youtubeUrl":6,"linkedinUrl":7,"phoneNumber":8,"emailAddress":9,"legal":10,"addresses":20},"https:\u002F\u002Fgithub.com\u002Fvoorhoede\u002F","https:\u002F\u002Fwww.youtube.com\u002Fchannel\u002FUCzHuhQVYFRixtQN2-swcuGg","https:\u002F\u002Fwww.linkedin.com\u002Fcompany\u002Fde-voorhoede","+31 20 2610 954","post@voorhoede.nl",[11,14,17],{"title":12,"value":13},"KvK","56017235",{"title":15,"value":16},"BTW","NL851944620B01",{"title":18,"value":19},"IBAN","NL14TRIO0320142078",[21,26],{"address":22,"city":23,"googleMapsLink":24,"postalCode":25},"Koivistokade 70","Amsterdam","https:\u002F\u002Fwww.google.com\u002Fmaps\u002Fplace\u002FDe+Voorhoede+%7C+Front-end+Development\u002F@52.396847,4.8700823,17z\u002Fdata=!3m1!4b1!4m5!3m4!1s0x47c5e21d502d2d59:0xbf570944a96ebf45!8m2!3d52.347647!4d4.8502154","1013 BB",{"address":27,"city":28,"googleMapsLink":29,"postalCode":30},"Koornmarkt 22","Delft","https:\u002F\u002Fwww.google.nl\u002Fmaps\u002Fplace\u002FKoornmarkt+22,+2611+EG+Delft\u002F@52.0093477,4.3573054,17z\u002F","2611 EG",{"title":32,"callToActions":33,"links":39},"Site Menu",[34],{"id":35,"title":36,"link":37},"163140902","Contact",{"__typename":38},"ContactRecord",[40,46,51,56,61],{"id":41,"title":42,"link":43},"163140904","Impact",{"__typename":44,"slug":45},"PageRecord","impact",{"id":47,"title":48,"link":49},"163140905","Services",{"__typename":50},"ServiceOverviewRecord",{"id":52,"title":53,"link":54},"163140906","Cases",{"__typename":55},"CaseOverviewRecord",{"id":57,"title":58,"link":59},"163140908","About us",{"__typename":44,"slug":60},"about-us",{"id":62,"title":63,"link":64},"d6WdFJq2SOuc3dWtpibbXQ","Work at",{"__typename":44,"slug":65},"work-at",{"links":67,"copyrightTitle":93,"copyrightLabel":94,"copyrightLink":95,"privacyTitle":96,"privacyLabel":97,"privacyLink":98,"certificatesGrid":99},[68,71,74,77,82,85,88],{"id":69,"title":42,"link":70},"144185264",{"__typename":44,"slug":45},{"id":72,"title":48,"link":73},"144185265",{"__typename":50},{"id":75,"title":53,"link":76},"144185266",{"__typename":55},{"id":78,"title":79,"link":80},"144185267","Blog",{"__typename":81},"BlogPostOverviewRecord",{"id":83,"title":58,"link":84},"144185268",{"__typename":44,"slug":60},{"id":86,"title":36,"link":87},"144185269",{"__typename":38},{"id":89,"title":90,"link":91},"144185270","FAQ",{"__typename":44,"slug":92},"faq","Creative Commons licence and disclaimer","CC BY 4.0","https:\u002F\u002Fcreativecommons.org\u002Flicenses\u002Fby\u002F4.0\u002F","De Voorhoede privacy statement (pdf)","Privacy statement","https:\u002F\u002Fwww.datocms-assets.com\u002F6524\u002F1763455455-vh-isms-006-privacy-statement-de-voorhoede-en.pdf",[100,112,123],{"id":101,"image":102,"link":107},"Xq4bBfg_TZ6Fkjax9mkbLQ",{"url":103,"alt":104,"width":105,"height":106},"https:\u002F\u002Fwww.datocms-assets.com\u002F6524\u002F1687353463-b-corp-logo-black-rgb.png","B Corp logo",404,680,{"__typename":108,"id":109,"title":110,"url":111},"ExternalLinkRecord","fGW1ak8XQYaYDLkBSyncog","B Corp","https:\u002F\u002Fwww.bcorporation.net\u002Fen-us\u002Ffind-a-b-corp\u002Fcompany\u002Fde-voorhoede\u002F",{"id":113,"image":114,"link":119},"c5mCXRTiSraRIB25fw1p7Q",{"url":115,"alt":116,"width":117,"height":118},"https:\u002F\u002Fwww.datocms-assets.com\u002F6524\u002F1687353461-dda-boxlogo-black.png","Dutch Digital Agencies logo",627,480,{"__typename":108,"id":120,"title":121,"url":122},"P6Jh7B0cTv2cKyNEeKVWVQ","Dutch Digital Agencies","https:\u002F\u002Fdutchdigitalagencies.com\u002Fleden\u002Fde-voorhoede\u002F",{"id":124,"image":125,"link":129},"MT5SCyNxSTSr_v5eeATMZw",{"url":126,"alt":127,"width":128,"height":128},"https:\u002F\u002Fwww.datocms-assets.com\u002F6524\u002F1775730283-dnv.png","DNV logo",518,{"id":130,"title":131,"link":132},"BRtNB5HnT5i-7HkA8IYzBw","DIV",{"__typename":44,"slug":133},"impact\u002Fdigitale-producten-privacy-by-design",{"page":135},{"slug":136,"i18nSlugs":137,"social":140,"title":141,"subtitle":79,"isArchived":144,"headerIllustration":143,"date":143,"authors":145,"introTitle":154,"items":155,"pivots":283,"relatedBlogPosts":284,"tags":285,"onMountedScript":159,"onUnmountedScript":159},"simplifying-search-using-ai-in-our-knowledge-base",[138],{"locale":139,"value":136},"en",{"title":141,"description":142,"image":143},"Simplifying Search: Using AI in our Knowledge Base","Discover how we utilized Supabase and the OpenAI API to revamp our internal knowledge base search.",null,false,[146],{"name":147,"lastName":148,"slug":149,"image":150},"Sjoerd","Beentjes","sjoerd",{"url":151,"alt":143,"width":152,"height":153},"https:\u002F\u002Fwww.datocms-assets.com\u002F6524\u002F1683534892-sjoerd.jpg",1637,2182,"Adding search to your website is cool, but have you added OpenAI and Supabase to the mix? Here's how we did just that.",[156,161,164,173,177,184,187,191,194,198,201,205,209,213,218,221,225,229,232,235,239,243,247,250,253,257,260,263,266,269,273,277,280],{"__typename":157,"id":158,"title":159,"body":160},"TextSectionRecord","58mJaTASTVq5wNVxaK6mAg","","\u003Cp>\u003Cspan style=\"font-weight: 400;\">Recently, we did a big refactor to our internal knowledge base&mdash;a place where we document guidelines and processes, including onboarding procedures for new employees. \u003C\u002Fspan>\u003Cspan style=\"font-weight: 400;\">It has a lot of organized information \u003C\u002Fspan>\u003Cspan style=\"font-weight: 400;\">and currently has a basic search feature. To find information, colleagues either use a sidebar with nested links or the search bar to look for specific terms. But what if you could just ask your question and have it answered directly? \u003C\u002Fspan>\u003C\u002Fp>",{"__typename":157,"id":162,"title":159,"body":163},"N5ZNRO1eQuOBLSI_Nq0Uxg","\u003Cp>\u003Cspan style=\"font-weight: 400;\">After reading \u003C\u002Fspan>\u003Ca href=\"https:\u002F\u002Fsupabase.com\u002Fblog\u002Fchatgpt-supabase-docs\">\u003Cspan style=\"font-weight: 400;\">this blog post\u003C\u002Fspan>\u003C\u002Fa>\u003Cspan style=\"font-weight: 400;\">, we tried to build this AI search functionality ourselves and in this article we will delve into the concepts and code of it, using Supabase and the OpenAI API. We wanted users to be able to ask questions and have them answered based on the pages in our knowledge base. The end result looks like this:\u003C\u002Fspan>\u003C\u002Fp>",{"__typename":165,"id":166,"image":167,"caption":159,"fullWidth":144,"captionPosition":172},"ImageRecord","qIDVXD3JRZeVgDiWF6XBWA",{"url":168,"alt":169,"width":170,"height":171},"https:\u002F\u002Fwww.datocms-assets.com\u002F6524\u002F1698072646-image2.gif","A search form that is filled in with the question 'Where do I need to throw away plastic?'. The form is submitted and an answer is given.",640,320,"bottom",{"__typename":157,"id":174,"title":175,"body":176},"GxGSsLd1RiuKIXnD5koP3g","Sectioning Content and Creating Embeddings","\u003Cp>\u003Cspan style=\"font-weight: 400;\">Before we dive into the functionality, let's look into some of the concepts we&rsquo;ll use later on. It's essential to understand how we'll match a posed question to the playbook's content.\u003C\u002Fspan>\u003C\u002Fp>\n\u003Cp>\u003Cspan style=\"font-weight: 400;\">For a simple analogy, consider categorizing animals like cats, bears, and elephants based on two factors: 'size' and 'danger' (note: this data is not factual, just a rough estimation). This comparative approach can be visualized as shown here: \u003C\u002Fspan>\u003C\u002Fp>",{"__typename":178,"id":179,"url":180,"title":181,"caption":182,"previewType":183},"CodePenBlockRecord","KL_JQoLDTpqDg1eEmGssDg","https:\u002F\u002Fstackblitz.com\u002Fedit\u002Fjs-slmuyv?ctl=1&embed=1&file=index.js&hideDevTools=1&hideExplorer=1&hideNavigation=1&view=preview&theme=light","2 dimensional classification","Two dimensional classification","stackblitz",{"__typename":157,"id":185,"title":159,"body":186},"EQPuKnnDSZmLi6bbUEvhSg","\u003Cp>\u003Cspan style=\"font-weight: 400;\">Now when we want to search for an animal most similar to a cow based on these two dimensions, the end result looks like this: \u003C\u002Fspan>\u003C\u002Fp>",{"__typename":178,"id":188,"url":189,"title":190,"caption":190,"previewType":183},"HChYc-TAT6ivAUJclqlDfQ","https:\u002F\u002Fstackblitz.com\u002Fedit\u002Fjs-nggipo?ctl=1&embed=1&file=index.js&hideDevTools=1&hideExplorer=1&hideNavigation=1&theme=light&view=preview","Similarity search",{"__typename":157,"id":192,"title":159,"body":193},"5145iY8nRLmU49hZGpMLmg","\u003Cp>\u003Cspan style=\"font-weight: 400;\">In this example we find the \u003C\u002Fspan>\u003Ca href=\"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FEuclidean_distance\">\u003Ci>\u003Cspan style=\"font-weight: 400;\">euclidean distance\u003C\u002Fspan>\u003C\u002Fi>\u003C\u002Fa>\u003Cspan style=\"font-weight: 400;\"> between &lsquo;cow&rsquo; and each other animal. We filter out the top three and show them in the visualization.\u003C\u002Fspan>\u003C\u002Fp>\n\u003Cp>\u003Cspan style=\"font-weight: 400;\">Furthermore, we can refine the search by introducing a third parameter, like the speed of each animal. The size of each point is now based on the speed of the corresponding animal.\u003C\u002Fspan>\u003C\u002Fp>",{"__typename":178,"id":195,"url":196,"title":197,"caption":197,"previewType":183},"9P-DZ_0SSo-mPWOZ9EGeTg","https:\u002F\u002Fstackblitz.com\u002Fedit\u002Fjs-tuumt2?ctl=1&embed=1&file=index.js&hideDevTools=1&hideExplorer=1&hideNavigation=1&theme=light&view=preview","Three dimensional classifications",{"__typename":157,"id":199,"title":159,"body":200},"J48lYK2DQF6Qyg29r_DK3A","\u003Cp>\u003Cspan style=\"font-weight: 400;\">This extra dimension provides us with a different result than before. By adding even more dimensions the comparison between points would become more refined.&nbsp;\u003C\u002Fspan>\u003C\u002Fp>\n\u003Cp>\u003Cspan style=\"font-weight: 400;\">The OpenAI API allows us to apply this method to blocks of text, analyzing them in 1536 dimensions. This approach results in a highly detailed classification of the text.\u003C\u002Fspan>\u003C\u002Fp>\n\u003Cp>\u003Cspan style=\"font-weight: 400;\">In multi-dimensional spaces, a specific position can be represented by a 'vector'.. Essentially, a vector in a 1536-dimensional space is an array of length 1536. This array can be persisted in a database, making it queryable. The process of converting raw text data into this vector format is known as 'embedding'. \u003C\u002Fspan>\u003Cspan style=\"font-weight: 400;\">Under the hood, this embedding uses algorithms to translate text data into a numerical space, optimizing it for machine learning tasks such as our search functionality.\u003C\u002Fspan>\u003C\u002Fp>\n\u003Cp>\u003Cspan style=\"font-weight: 400;\">When a user asks a question to our AI, we generate an embedding for the question. Subsequently, we compare this question embedding with those created for each section. Since each embedding is simply a point within a defined space, we can identify which points (or section embeddings) are closest to our question.\u003C\u002Fspan>\u003C\u002Fp>",{"__typename":157,"id":202,"title":203,"body":204},"HxdAyQQATtSsDhJhO30Shw","Preparing Embeddings","\u003Cp>\u003Cspan style=\"font-weight: 400;\">We could create an embedding for each page, but to make them more refined we split them up into sections. \u003C\u002Fspan>\u003Ca href=\"https:\u002F\u002Fjs.langchain.com\u002Fdocs\u002Fget_started\u002Fintroduction\">\u003Cspan style=\"font-weight: 400;\">LangChain\u003C\u002Fspan>\u003C\u002Fa>\u003Cspan style=\"font-weight: 400;\"> has an excellent toolchain for this task. Below is an example of how this can be achieved.\u003C\u002Fspan>\u003C\u002Fp>",{"__typename":178,"id":206,"url":207,"title":208,"caption":208,"previewType":183},"LgGKvC_jTCWxkpRboUDAwg","https:\u002F\u002Fstackblitz.com\u002Fedit\u002Fstackblitz-starters-6w2rqz?ctl=1&embed=1&file=index.mjs&hideExplorer=1&hideNavigation=1&theme=light&view=editor&startScript=generate","LangChain splitter example",{"__typename":157,"id":210,"title":211,"body":212},"RMZiKjAFRXq2tcquhTlAJA","Storing Embeddings in a Database","\u003Cp>\u003Cspan style=\"font-weight: 400;\">To store the embeddings in a database, the database needs to support vector storage. At De Voorhoede, we often use \u003C\u002Fspan>\u003Ca href=\"https:\u002F\u002Fsupabase.com\u002F\">\u003Cspan style=\"font-weight: 400;\">Supabase\u003C\u002Fspan>\u003C\u002Fa>\u003Cspan style=\"font-weight: 400;\">, which fortunately includes a vector extension. Here's how we added the migration to the project to enable vector storage:\u003C\u002Fspan>\u003C\u002Fp>",{"__typename":214,"id":215,"language":216,"body":217},"CodeBlockRecord","9ArscYlrQ6O0X6WZkmleeA","sql","-- Enable the pgvector extension to work with embedding vectors\nCREATE EXTENSION IF NOT EXISTS vector;\n\n-- Create a table to store your documents\nCREATE TABLE documents (\n    id          BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,\n    source_url  TEXT,\n    source_name TEXT,\n    checksum    TEXT\n);\n\n-- Create a table to store your document sections\nCREATE TABLE document_sections (\n    id          BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,\n    content     TEXT,\n    metadata    JSONB,\n    embedding   VECTOR(1536),\n    document_id BIGINT NOT NULL REFERENCES public.documents ON DELETE CASCADE\n);",{"__typename":157,"id":219,"title":159,"body":220},"J2PPug63S16jz8DTPcU-_A","\u003Cp>\u003Cspan style=\"font-weight: 400;\">In the code snippet above, we store a vector with 1536 dimensions, which aligns with the dimensions of the embedding obtained from the OpenAI API.\u003C\u002Fspan>\u003C\u002Fp>\n\u003Cp>\u003Cspan style=\"font-weight: 400;\">In the example above, &lsquo;documents&rsquo; represent the pages in our knowledge base and &lsquo;document_sections&rsquo; represent the sections we created using the LangChain text splitter. To make sure we don&rsquo;t create embeddings for a document while its contents didn&rsquo;t change, we create checksums. A checksum of a document is a hash generated based on its content. Below is an example of how this can be done. Change something in the &lsquo;bison.md&rsquo; file and see how the hash does not match anymore:\u003C\u002Fspan>\u003C\u002Fp>",{"__typename":178,"id":222,"url":223,"title":224,"caption":224,"previewType":183},"aHC_9PjtS7qCFw5Eb6x1_Q","https:\u002F\u002Fstackblitz.com\u002Fedit\u002Fstackblitz-starters-gp4ztk?ctl=1&theme=light&embed=1&file=index.mjs&startScript=compare","Checksum comparison",{"__typename":157,"id":226,"title":227,"body":228},"OfsDx4QQRsyMNLjlzCH_dA","Searching the sections","\u003Cp>\u003Cspan style=\"font-weight: 400;\">To identify the most relevant section, we compare the question's vector with the vectors stored in the database for each section. We accomplish this by identifying which vectors are closest in the multidimensional space. The pgvector extension from Supabase allows us to utilize the &lt;=&gt; operator, which computes the &lsquo;\u003C\u002Fspan>\u003Ca href=\"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FCosine_similarity\">\u003Cspan style=\"font-weight: 400;\">cosine similarity\u003C\u002Fspan>\u003C\u002Fa>\u003Cspan style=\"font-weight: 400;\">&rsquo; between two vectors. If you (like me) did not really pay attention during math class but now want to know more about stuff like this, I recommend watching \u003C\u002Fspan>\u003Ca href=\"https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=e9U0QAFbfLI\">\u003Cspan style=\"font-weight: 400;\">this short video\u003C\u002Fspan>\u003C\u002Fa>\u003Cspan style=\"font-weight: 400;\">. It comes down to measuring the cosine of the angle between two vectors to see how much they point in the same direction; a value of 1 indicates high similarity, while 0 indicates no similarity.\u003C\u002Fspan>\u003C\u002Fp>\n\u003Cp>\u003Cspan style=\"font-weight: 400;\">Here's an illustrative query from the Supabase documentation:\u003C\u002Fspan>\u003C\u002Fp>",{"__typename":214,"id":230,"language":216,"body":231},"8168MA5sSOu9quUycRMgEQ","create or replace function match_documents (\n  query_embedding float[] default array[]::float[],\n  match_count int default null,\n  filter jsonb default '{}'\n) returns table (\n  id bigint,\n  content text,\n  metadata jsonb,\n  document_id bigint,\n  source_url text,\n  source_name text,\n  similarity float\n) language plpgsql as $$\n#variable_conflict use_column\nbegin\n  return query\n  select\n    ds.id as id,\n    ds.content,\n    ds.metadata,\n    d.id as document_id,\n    d.source_url,\n    d.source_name,\n    1 - (ds.embedding \u003C=> query_embedding) as similarity\n  from document_sections ds\n  inner join documents d on ds.document_id = d.id\n  where ds.metadata @> filter\n  order by ds.embedding \u003C=> query_embedding\n  limit coalesce(match_count, 10); -- default to 10 if null\nend;\n$$;",{"__typename":157,"id":233,"title":159,"body":234},"jCMSer_oTwKNL-DkeZFPbw","\u003Cp>\u003Cspan style=\"font-weight: 400;\">This is how it looks when implemented in the back-end code:\u003C\u002Fspan>\u003C\u002Fp>",{"__typename":214,"id":236,"language":237,"body":238},"_1S4LIhJTCaSCClKDnL7ww","js","const { error: matchError, data: documentSections } = await supabase.rpc(\n  'match_documents',\n  {\n    query_embedding: queryEmbedding,\n    match_count: 3,\n  }\n);",{"__typename":157,"id":240,"title":241,"body":242},"0T6W90oiTFeH9txqgXaNBg","Formulating Answers","\u003Cp>\u003Cspan style=\"font-weight: 400;\">With our database now in place, it's time to integrate it into both our back-end and front-end systems. Users should have the capability to enter a question and submit it. The back-end will then interact with the database and the OpenAI API to generate an accurate response.\u003C\u002Fspan>\u003C\u002Fp>\n\u003Cp>\u003Cspan style=\"font-weight: 400;\">Using the Postgres function described above, we obtain the relevant information (in this case 3 document sections). Our next step is to distill it into a coherent answer. We accomplish this by feeding all the information to the language model, providing some instructions on formulating the response, and waiting for the output:\u003C\u002Fspan>\u003C\u002Fp>",{"__typename":214,"id":244,"language":245,"body":246},"mdyByjFET4iTj9dB7jT7EQ","txt","You are a helpful Voorhoede assistant who loves to help people! Given the following context, answer the question using only that information. If you are unsure and the answer is not found in the context given, say \"Sorry, I don't know how to help with that.\"\n\nContext sections:\n{{context}}\n\nQuestion:\n{{question}}\n\nAnswer:",{"__typename":157,"id":248,"title":159,"body":249},"1rS5Ns7CRqSxirmlhTfQ3Q","\u003Cp>\u003Cspan style=\"font-weight: 400;\">And use it with the OpenAI API:\u003C\u002Fspan>\u003C\u002Fp>",{"__typename":214,"id":251,"language":237,"body":252},"1QqNYhVkR--a_cpwNg33bA","fetch(\"https:\u002F\u002Fapi.openai.com\u002Fv1\u002Fchat\u002Fcompletions\", {\n  method: \"POST\",\n  headers: {\n    \"Content-Type\": \"application\u002Fjson\",\n    Authorization: `Bearer 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`,\n  },\n  body: JSON.stringify({\n    model: \"gpt-4\",\n    messages: [\n      {\n        role: \"system\",\n        content:\n          'You are a helpful Voorhoede assistant who loves to help people! Given the following context, answer the question using only that information. If you are unsure and the answer is not found in the context given, say \"Sorry, I don\\'t know how to help with that.\"',\n      },\n      {\n        role: \"user\",\n        content: `\nContext sections:\n## **Plastics**\n\nWe collect all the plastics that can be recycled in a bag in the closet next to the phone booth. The bag needs to be taken out every once in a while in a public underground container in the city.\n---\n# **Waste management**\n\nAt our office in Amsterdam we separate waste in 4 ways:\n---\n## **Glass**\n\nWe collect glass in a separate bag in the closet next to the phone booth. The bag needs to be taken out every once in a while in a public glass container in the city.\n---\n\nQuestion:\nWhere do I need to throw away plastic in the office?\n            `,\n      },\n    ],\n  }),\n});\n",{"__typename":157,"id":254,"title":255,"body":256},"YzHS4mDgRhKaZ9pNTkbGFQ","Implementation in the front-end","\u003Cp>\u003Cspan style=\"font-weight: 400;\">To implement this in a user friendly way we want to have the answer streamed to the client. We use edge functions as backend in our project. While we use \u003C\u002Fspan>\u003Ca href=\"https:\u002F\u002Fdocs.netlify.com\u002Fedge-functions\u002Foverview\u002F\">\u003Cspan style=\"font-weight: 400;\">Netlify\u003C\u002Fspan>\u003C\u002Fa>\u003Cspan style=\"font-weight: 400;\"> for this, environments like \u003C\u002Fspan>\u003Ca href=\"https:\u002F\u002Fvercel.com\u002Ffeatures\u002Fedge-functions\">\u003Cspan style=\"font-weight: 400;\">Vercel\u003C\u002Fspan>\u003C\u002Fa>\u003Cspan style=\"font-weight: 400;\"> or \u003C\u002Fspan>\u003Ca href=\"https:\u002F\u002Fdeno.land\u002Fmanual@v1.35.3\u002Fadvanced\u002Fdeploying_deno\u002Fcloudflare_workers#deploying-deno-to-cloudflare-workers\">\u003Cspan style=\"font-weight: 400;\">Deno\u003C\u002Fspan>\u003C\u002Fa>\u003Cspan style=\"font-weight: 400;\"> also support them. To get the nice typewriter effect, we can stream the response to the client word by word. The API is giving us its output everytime it predicts a new word, making the user see the response as fast as possible. \u003C\u002Fspan>\u003Cspan style=\"font-weight: 400;\">An alternative is to wait for the complete output, but this approach delays the response until all predictions are made, compromising user experience.\u003C\u002Fspan>\u003C\u002Fp>",{"__typename":214,"id":258,"language":237,"body":259},"s59ns3YKQ_mzwV-33o6Ufg","fetch(\"https:\u002F\u002Fapi.openai.com\u002Fv1\u002Fchat\u002Fcompletions\", {\n  method: \"POST\",\n  headers: {\n    \"Content-Type\": \"application\u002Fjson\",\n    Authorization: `Bearer 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`,\n  },\n  body: JSON.stringify({\n    model: \"gpt-4\",\n    messages: [...],\n    stream: true\n  }),\n});\n",{"__typename":157,"id":261,"title":159,"body":262},"c9M-MN7IQhWsAvq3opAGCA","\u003Cp>\u003Cspan style=\"font-weight: 400;\">An edge function usually expects you to return a regular Response object, so you can just return the `fetch()` response directly and have it stream the response to the front-end:\u003C\u002Fspan>\u003C\u002Fp>",{"__typename":214,"id":264,"language":237,"body":265},"pxvm3iGvRR6xcKSffF9CIw","export default async (req) => {\n  if (req.method !== \"POST\") {\n    return new Response(\"Method not allowed\", {\n      status: 405,\n      headers: {\n        \"content-type\": \"text\u002Fplain\",\n      },\n    });\n  }\n\n  const question = await req.json();\n\n  \u002F\u002F create embedding and generate query\n\n  const chatResponse = await fetch(\n    \"https:\u002F\u002Fapi.openai.com\u002Fv1\u002Fchat\u002Fcompletions\",\n    {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application\u002Fjson\",\n        Authorization: `Bearer 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`,\n      },\n      body: JSON.stringify({\n        model: \"gpt-4\",\n        messages: [...],\n        stream: true,\n      }),\n    }\n  );\n\n  return new Response(chatResponse.body, {\n    status: chatResponse.status,\n    headers: {\n      \"Content-Type\": \"text\u002Fevent-stream\",\n    },\n  });\n};",{"__typename":157,"id":267,"title":159,"body":268},"E67JbpuJSAW-gCnx8sx5OA","\u003Cp>\u003Cspan style=\"font-weight: 400;\">When fetching this endpoint in the front-end, it needs some special care. Let&rsquo;s assume we&rsquo;re using React, this is how we could handle this:\u003C\u002Fspan>\u003C\u002Fp>",{"__typename":214,"id":270,"language":271,"body":272},"WQaUiTPhTKG6f8REUgEmNQ","jsx","import { useState, useEffect } from 'react';\nimport { fetchEventSource } from '@microsoft\u002Ffetch-event-source';\n\nexport default function App() {\n  const [answer, setAnswer] = useState('');\n\n  async function initializeStream() {\n    await fetchEventSource('\u002Fapi\u002Fai', {\n      onmessage(event) {\n        if (event.data === \"[DONE]\") {\n          return;\n        }\n        \n        const data = JSON.parse(event.data)\n\n        setAnswer((currentAnswer) => `${currentAnswer}${data.choices[0].delta.content}`);\n      },\n    });\n  }\n\n  useEffect(() => {\n    initializeStream();\n  }, []);\n\n  return (\n      \u003Cp>{answer}\u003C\u002Fp>\n  );\n}\n",{"__typename":157,"id":274,"title":275,"body":276},"oxGKz9N4T86NAEvXCef9iw","Wrapping up","\u003Cp>\u003Cspan style=\"font-weight: 400;\">We've now established a streamlined process for answering user queries. We begin with database initialization, followed by segmenting our web pages into distinct sections based on headings. Each section is then transformed into a unique embedding and stored for retrieval. When a user asks a question, we generate a corresponding embedding, compare it with our stored embeddings to identify the most pertinent section, and then craft a tailored prompt for the OpenAI API. The resultant answer is then presented to the user on the front-end.\u003C\u002Fspan>\u003C\u002Fp>\n\u003Cp>\u003Cspan style=\"font-weight: 400;\">The end result looks like this:\u003C\u002Fspan>\u003C\u002Fp>",{"__typename":165,"id":278,"image":279,"caption":159,"fullWidth":144,"captionPosition":172},"O_iyMxciQBi7vzSJ2jbxnw",{"url":168,"alt":169,"width":170,"height":171},{"__typename":157,"id":281,"title":159,"body":282},"uICBZ_szRAmNfIYOXEemrA","\u003Cp>\u003Cspan style=\"font-weight: 400;\">Using the capabilities of OpenAI combined with vector storage, we give the user the option to ask more refined questions, making it even easier to find what they&rsquo;re looking for.\u003C\u002Fspan>\u003C\u002Fp>",[],[],[],1776256134365]