diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fe66ea7fb32..b5d3188a5b3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,7 @@ To ensure a positive and inclusive environment, please read our [code of conduc If you find a bug, please create an Issue and we’ll triage it. -- Please search [existing Issues](https://github.com/supabase/supabase) before creating a new one. +- Please search [existing Issues](https://github.com/supabase/supabase/issues) before creating a new one. - Please include a clear description of the problem along with steps to reproduce it. Exact steps with screenshots and urls really help here. ## Pull Requests @@ -23,7 +23,7 @@ We actively welcome your Pull Requests! A couple of things to keep in mind befor - If you’re fixing an Issue, make sure someone else hasn’t already created a PR fixing the same issue. Likewise, make sure to link your PR to the related Issue(s). - We will always try to accept the first viable PR that resolves the Issue. -- If you're new, we encourage you to take a look at issues tagged with [good first issue](https://github.com/supabase/supabase/pulls?q=is%3Aopen+is%3Apr+label%3A%22good+first+issue%22). +- If you're new, we encourage you to take a look at issues tagged with [good first issue](https://github.com/supabase/supabase/labels/good%20first%20issue). - If you’re submitting a new feature, make sure you have opened a [Discussion](https://github.com/orgs/supabase/discussions/new/choose) to discuss the new feature before opening a PR. We’d love to accept your hard work, but unfortunately if a feature hasn’t gone through a proper design process, your PR will be closed. - Please use the PR message template and provide detailed context for quicker review. PRs without clear problem statements will be closed. diff --git a/apps/docs/components/Feedback/Feedback.tsx b/apps/docs/components/Feedback/Feedback.tsx index 0359b20e360..4694e338e20 100644 --- a/apps/docs/components/Feedback/Feedback.tsx +++ b/apps/docs/components/Feedback/Feedback.tsx @@ -16,6 +16,7 @@ import { Button, cn } from 'ui' import { useSendFeedbackMutation } from '~/lib/fetch/feedback' import { useSendTelemetryEvent } from '~/lib/telemetry' import { FeedbackModal, type FeedbackFields } from './FeedbackModal' +import { getSanitizedTabParams } from './Feedback.utils' const FeedbackButton = forwardRef< HTMLButtonElement, @@ -89,7 +90,7 @@ function Feedback() { vote: response, page: pathname, metadata: { - query: Object.fromEntries(new URLSearchParams(window.location.search).entries()), + query: getSanitizedTabParams(), }, }) if (error) console.error(error) diff --git a/apps/docs/components/Feedback/Feedback.utils.ts b/apps/docs/components/Feedback/Feedback.utils.ts new file mode 100644 index 00000000000..bc3683490f0 --- /dev/null +++ b/apps/docs/components/Feedback/Feedback.utils.ts @@ -0,0 +1,16 @@ +import { pick } from 'lodash' + +/** + * Gets the tab selection state from the URL search params. + * + * Sanitizes by including only those search params that are explicitly marked + * as query groups. + */ +const getSanitizedTabParams = () => { + const searchParams = new URLSearchParams(window.location.search) + const queryGroups = searchParams.getAll('queryGroups') + + return pick(Object.fromEntries(searchParams.entries()), queryGroups) +} + +export { getSanitizedTabParams } diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 62ed67d0656..bf1e5bda20e 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -1139,6 +1139,10 @@ export const functions: NavMenuConstant = { name: 'Handling Routing in Functions', url: '/guides/functions/routing', }, + { + name: 'Running AI Models', + url: '/guides/functions/ai-models', + }, { name: 'Regional invocations', url: '/guides/functions/regional-invocation', @@ -1187,6 +1191,10 @@ export const functions: NavMenuConstant = { name: 'Generating OG images ', url: '/guides/functions/examples/og-image', }, + { + name: 'Semantic AI Search', + url: '/guides/functions/examples/semantic-search', + }, { name: 'CAPTCHA support with Cloudflare Turnstile', url: '/guides/functions/examples/cloudflare-turnstile', diff --git a/apps/docs/content/guides/ai/semantic-search.mdx b/apps/docs/content/guides/ai/semantic-search.mdx index a9a219f7213..44744441840 100644 --- a/apps/docs/content/guides/ai/semantic-search.mdx +++ b/apps/docs/content/guides/ai/semantic-search.mdx @@ -26,7 +26,7 @@ Embeddings are generated using a language model, and embeddings are compared to ## Embedding models -There are many embedding models available today. Some can be accessed through third-party APIs like [OpenAI](https://platform.openai.com/docs/guides/embeddings), where you send your text in the request and receive an embedding vector in the response. Others can run locally on your own compute, such as through Transformers.js for JavaScript implementations. For more information on local implementation, see [Generate embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings). +There are many embedding models available today. Supabase Edge Functions has [built in support](/docs/guides/functions/examples/semantic-search) for the `gte-small` model. Others can be accessed through third-party APIs like [OpenAI](https://platform.openai.com/docs/guides/embeddings), where you send your text in the request and receive an embedding vector in the response. Others can run locally on your own compute, such as through Transformers.js for JavaScript implementations. For more information on local implementation, see [Generate embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings). It's crucial to remember that when using embedding models with semantic search, you must use the same model for all embedding comparisons. Comparing embeddings created by different models will yield meaningless results. diff --git a/apps/docs/content/guides/auth/auth-anonymous.mdx b/apps/docs/content/guides/auth/auth-anonymous.mdx index 30b6346f53f..4a3b4aa0ad9 100644 --- a/apps/docs/content/guides/auth/auth-anonymous.mdx +++ b/apps/docs/content/guides/auth/auth-anonymous.mdx @@ -237,7 +237,7 @@ where is_anonymous is true and created_at < now() - interval '30 days'; ## Resources -- [Supabase Account - Free Plan OK](https://supabase.com) +- [Supabase - Get started for free](https://supabase.com) - [Supabase JS Client](https://github.com/supabase/supabase-js) - [Supabase Flutter Client](https://github.com/supabase/supabase-flutter) - [Supabase Kotlin Client](https://github.com/supabase-community/supabase-kt) diff --git a/apps/docs/content/guides/auth/auth-email.mdx b/apps/docs/content/guides/auth/auth-email.mdx index ab2636abfe6..6491a642830 100644 --- a/apps/docs/content/guides/auth/auth-email.mdx +++ b/apps/docs/content/guides/auth/auth-email.mdx @@ -207,7 +207,7 @@ suspend fun logout() { ## Resources -- [Supabase Account - Free Plan OK](https://supabase.com) +- [Supabase - Get started for free](https://supabase.com) - [Supabase JS Client](https://github.com/supabase/supabase-js) - [Supabase Flutter Client](https://github.com/supabase/supabase-flutter) - [Supabase Kotlin Client](https://github.com/supabase-community/supabase-kt) diff --git a/apps/docs/content/guides/auth/concepts/redirect-urls.mdx b/apps/docs/content/guides/auth/concepts/redirect-urls.mdx index 3f9f3aab8ad..665b94f13a0 100644 --- a/apps/docs/content/guides/auth/concepts/redirect-urls.mdx +++ b/apps/docs/content/guides/auth/concepts/redirect-urls.mdx @@ -76,6 +76,22 @@ const { data, error } = await supabase.auth.signInWithOAuth({ }) ``` +## Email templates when using `redirectTo` + +When using a `redirectTo` option, you may need to replace the `{{ .SiteURL }}` with `{{ .RedirectTo }}` in your email templates. See the [Email Templates guide](/docs/guides/auth/auth-email-templates) for more information. + +For example, change the following: + +```html + +Confirm your mail + + +Confirm your mail +``` + ## Mobile deep linking URIs For mobile applications you can use deep linking URIs. For example, for your `SITE_URL` you can specify something like `com.supabase://login-callback/` and for additional redirect URLs something like `com.supabase.staging://login-callback/` if needed. diff --git a/apps/docs/content/guides/auth/passwordless-login/auth-email-otp.mdx b/apps/docs/content/guides/auth/passwordless-login/auth-email-otp.mdx index 2605da556fc..9dc8599500a 100644 --- a/apps/docs/content/guides/auth/passwordless-login/auth-email-otp.mdx +++ b/apps/docs/content/guides/auth/passwordless-login/auth-email-otp.mdx @@ -157,7 +157,7 @@ If successful the user will now be logged in and you should receive a valid sess ## Resources -- [Supabase Account - Free Plan OK](https://supabase.com) +- [Supabase - Get started for free](https://supabase.com) - [Phone SMS & WhatsApp OTP](/docs/guides/auth/phone-login) - [Supabase JS Client](https://github.com/supabase/supabase-js) - [Supabase Flutter Client](https://github.com/supabase/supabase-flutter) diff --git a/apps/docs/content/guides/auth/passwordless-login/auth-magic-link.mdx b/apps/docs/content/guides/auth/passwordless-login/auth-magic-link.mdx index 001cab6c545..ed62d006f12 100644 --- a/apps/docs/content/guides/auth/passwordless-login/auth-magic-link.mdx +++ b/apps/docs/content/guides/auth/passwordless-login/auth-magic-link.mdx @@ -158,6 +158,6 @@ suspend fun logout() { ## Resources -- [Supabase Account - Free Plan OK](https://supabase.com) +- [Supabase - Get started for free](https://supabase.com) - [Supabase JS Client](https://github.com/supabase/supabase-js) - [Supabase Flutter Client](https://github.com/supabase/supabase-flutter) diff --git a/apps/docs/content/guides/auth/social-login/auth-bitbucket.mdx b/apps/docs/content/guides/auth/social-login/auth-bitbucket.mdx index 1cdde00d182..5b41ce54404 100644 --- a/apps/docs/content/guides/auth/social-login/auth-bitbucket.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-bitbucket.mdx @@ -112,6 +112,6 @@ suspend fun signOut() { ## Resources -- [Supabase Account - Free Plan OK](https://supabase.com) +- [Supabase - Get started for free](https://supabase.com) - [Supabase JS Client](https://github.com/supabase/supabase-js) - [Bitbucket Account](https://bitbucket.org) diff --git a/apps/docs/content/guides/auth/social-login/auth-discord.mdx b/apps/docs/content/guides/auth/social-login/auth-discord.mdx index 1c15e17249e..2154fd37eb8 100644 --- a/apps/docs/content/guides/auth/social-login/auth-discord.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-discord.mdx @@ -113,7 +113,7 @@ suspend fun signOut() { ## Resources -- [Supabase Account - Free Plan OK](https://supabase.com) +- [Supabase - Get started for free](https://supabase.com) - [Supabase JS Client](https://github.com/supabase/supabase-js) - [Discord Account](https://discord.com) - [Discord Developer Portal](https://discord.com/developers) diff --git a/apps/docs/content/guides/auth/social-login/auth-facebook.mdx b/apps/docs/content/guides/auth/social-login/auth-facebook.mdx index b195edf22d2..f15eb331425 100644 --- a/apps/docs/content/guides/auth/social-login/auth-facebook.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-facebook.mdx @@ -131,6 +131,6 @@ You can read more about App Review [here](https://developers.facebook.com/docs/a ## Resources -- [Supabase Account - Free Plan OK](https://supabase.com) +- [Supabase - Get started for free](https://supabase.com) - [Supabase JS Client](https://github.com/supabase/supabase-js) - [Facebook Developers Dashboard](https://developers.facebook.com/) diff --git a/apps/docs/content/guides/auth/social-login/auth-figma.mdx b/apps/docs/content/guides/auth/social-login/auth-figma.mdx index 0db6671df23..1f982fb668c 100644 --- a/apps/docs/content/guides/auth/social-login/auth-figma.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-figma.mdx @@ -112,6 +112,6 @@ suspend fun signOut() { ## Resources -- [Supabase Account - Free Plan OK](https://supabase.com) +- [Supabase - Get started for free](https://supabase.com) - [Supabase JS Client](https://github.com/supabase/supabase-js) - [Figma Developers page](https://www.figma.com/developers) diff --git a/apps/docs/content/guides/auth/social-login/auth-github.mdx b/apps/docs/content/guides/auth/social-login/auth-github.mdx index c0ffc5e8f04..0a2883ac3d8 100644 --- a/apps/docs/content/guides/auth/social-login/auth-github.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-github.mdx @@ -123,6 +123,6 @@ suspend fun signOut() { ## Resources -- [Supabase Account - Free Plan OK](https://supabase.com) +- [Supabase - Get started for free](https://supabase.com) - [Supabase JS Client](https://github.com/supabase/supabase-js) - [GitHub Developer Settings](https://github.com/settings/developers) diff --git a/apps/docs/content/guides/auth/social-login/auth-gitlab.mdx b/apps/docs/content/guides/auth/social-login/auth-gitlab.mdx index 6dd71afa9be..3ffcfd43354 100644 --- a/apps/docs/content/guides/auth/social-login/auth-gitlab.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-gitlab.mdx @@ -109,6 +109,6 @@ suspend fun signOut() { ## Resources -- [Supabase Account - Free Plan OK](https://supabase.com) +- [Supabase - Get started for free](https://supabase.com) - [Supabase JS Client](https://github.com/supabase/supabase-js) - [GitLab Account](https://gitlab.com) diff --git a/apps/docs/content/guides/auth/social-login/auth-linkedin.mdx b/apps/docs/content/guides/auth/social-login/auth-linkedin.mdx index f6ed6125535..865e7ba6ee5 100644 --- a/apps/docs/content/guides/auth/social-login/auth-linkedin.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-linkedin.mdx @@ -135,6 +135,6 @@ Do reach out to support if you have any concerns around this change. ## Resources -- [Supabase Account - Free Plan OK](https://supabase.com) +- [Supabase - Get started for free](https://supabase.com) - [Supabase JS Client](https://github.com/supabase/supabase-js) - [LinkedIn Developer Dashboard](https://api.LinkedIn.com/apps) diff --git a/apps/docs/content/guides/auth/social-login/auth-notion.mdx b/apps/docs/content/guides/auth/social-login/auth-notion.mdx index 0c414ab4fdb..9b7a079155d 100644 --- a/apps/docs/content/guides/auth/social-login/auth-notion.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-notion.mdx @@ -112,7 +112,7 @@ suspend fun signOut() { ## Resources -- [Supabase Account - Free Plan OK](https://supabase.com) +- [Supabase - Get started for free](https://supabase.com) - [Supabase JS Client](https://github.com/supabase/supabase-js) - [Notion Account](https://notion.so) - [Notion Developer Portal](https://www.notion.so/my-integrations) diff --git a/apps/docs/content/guides/auth/social-login/auth-slack.mdx b/apps/docs/content/guides/auth/social-login/auth-slack.mdx index 238b394b9d3..e1022ee7562 100644 --- a/apps/docs/content/guides/auth/social-login/auth-slack.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-slack.mdx @@ -130,6 +130,6 @@ suspend fun signOut() { ## Resources -- [Supabase Account - Free Plan OK](https://supabase.com) +- [Supabase - Get started for free](https://supabase.com) - [Supabase JS Client](https://github.com/supabase/supabase-js) - [Slack Developer Dashboard](https://api.slack.com/apps) diff --git a/apps/docs/content/guides/auth/social-login/auth-spotify.mdx b/apps/docs/content/guides/auth/social-login/auth-spotify.mdx index c74d07bb473..071cf0f8b52 100644 --- a/apps/docs/content/guides/auth/social-login/auth-spotify.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-spotify.mdx @@ -124,6 +124,6 @@ suspend fun signOut() { ## Resources -- [Supabase Account - Free Plan OK](https://supabase.com) +- [Supabase - Get started for free](https://supabase.com) - [Supabase JS Client](https://github.com/supabase/supabase-js) - [Spotify Developer Dashboard](https://developer.spotify.com/dashboard/) diff --git a/apps/docs/content/guides/auth/social-login/auth-twitch.mdx b/apps/docs/content/guides/auth/social-login/auth-twitch.mdx index 405477b5da4..a968201c747 100644 --- a/apps/docs/content/guides/auth/social-login/auth-twitch.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-twitch.mdx @@ -127,7 +127,7 @@ suspend fun signOut() { ## Resources -- [Supabase Account - Free Plan OK](https://supabase.com) +- [Supabase - Get started for free](https://supabase.com) - [Supabase JS Client](https://github.com/supabase/supabase-js) - [Twitch Account](https://twitch.tv) - [Twitch Developer Console](https://dev.twitch.tv/console) diff --git a/apps/docs/content/guides/auth/social-login/auth-twitter.mdx b/apps/docs/content/guides/auth/social-login/auth-twitter.mdx index 749029d3c9f..67721da5d8b 100644 --- a/apps/docs/content/guides/auth/social-login/auth-twitter.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-twitter.mdx @@ -118,6 +118,6 @@ suspend fun signOut() { ## Resources -- [Supabase Account - Free Plan OK](https://supabase.com) +- [Supabase - Get started for free](https://supabase.com) - [Supabase JS Client](https://github.com/supabase/supabase-js) - [Twitter Developer Dashboard](https://developer.twitter.com/en/portal/dashboard) diff --git a/apps/docs/content/guides/auth/social-login/auth-zoom.mdx b/apps/docs/content/guides/auth/social-login/auth-zoom.mdx index 37d6d07a8e8..ef4bf5e2d7d 100644 --- a/apps/docs/content/guides/auth/social-login/auth-zoom.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-zoom.mdx @@ -125,6 +125,6 @@ suspend fun signOut() { ## Resources -- [Supabase Account - Free Plan OK](https://supabase.com) +- [Supabase - Get started for free](https://supabase.com) - [Supabase JS Client](https://github.com/supabase/supabase-js) - [Zoom App Marketplace](https://marketplace.zoom.us/) diff --git a/apps/docs/content/guides/database/arrays.mdx b/apps/docs/content/guides/database/arrays.mdx index eb405bd293f..a7579573c17 100644 --- a/apps/docs/content/guides/database/arrays.mdx +++ b/apps/docs/content/guides/database/arrays.mdx @@ -210,5 +210,5 @@ returns: ## Resources - [Supabase JS Client](https://github.com/supabase/supabase-js) -- [Supabase Account - Free Plan OK](https://supabase.com) +- [Supabase - Get started for free](https://supabase.com) - [PostgreSQL Arrays](https://www.postgresql.org/docs/15/arrays.html) diff --git a/apps/docs/content/guides/database/sql-to-api.mdx b/apps/docs/content/guides/database/sql-to-api.mdx index 65550a1c67e..76140737c95 100644 --- a/apps/docs/content/guides/database/sql-to-api.mdx +++ b/apps/docs/content/guides/database/sql-to-api.mdx @@ -77,7 +77,7 @@ const { count, error } = await supabase ## Resources -- [Supabase Account - Free Plan OK](https://supabase.com) +- [Supabase - Get started for free](https://supabase.com) - [Postgrest Operators](https://postgrest.org/en/stable/api.html#operators) - [Supabase API: JavaScript select](/docs/reference/javascript/select) - [Supabase API: JavaScript modifiers](/docs/reference/javascript/using-modifiers) diff --git a/apps/docs/content/guides/functions/ai-models.mdx b/apps/docs/content/guides/functions/ai-models.mdx new file mode 100644 index 00000000000..e895bb5568e --- /dev/null +++ b/apps/docs/content/guides/functions/ai-models.mdx @@ -0,0 +1,171 @@ +--- +id: 'function-ai-models' +title: 'Running AI Models' +description: 'How to run AI models in Edge Functions.' +subtitle: 'How to run AI models in Edge Functions.' +--- + +[Supabase Edge Runtime](https://github.com/supabase/edge-runtime) has a built-in API for running AI models. You can use this API to generate embeddings, build conversational workflows, and do other AI related tasks in your Edge Functions. + +## Setup + +There are no external dependencies or packages to install to enable the API. + +You can create a new inference session by doing: + +```ts +const model = new Supabase.ai.Session('model-name') +``` + + + +To get type hints and checks for the API we recommend adding this triple-slash directive to the start of the Edge Function code: `/// ` + + + +## Running a model inference + +Once the session is instantiated, you can call it with inputs to perform inferences. Depending on the model you run, you may need to provide different options (discussed below). + +```ts +const output = await model.run(input, options) +``` + +## How to generate text embeddings + +Now let's see how to write an Edge Function using the `Supabase.ai` API to generate text embeddings. Currently, `Supabase.ai` API only supports the [gte-small](https://huggingface.co/Supabase/gte-small) model. + + + +`gte-small` model exclusively caters to English texts, and any lengthy texts will be truncated to a maximum of 512 tokens. While you can provide inputs longer than 512 tokens, truncation may affect the accuracy. + + + +```ts +const model = new Supabase.ai.Session('gte-small') + +Deno.serve(async (req: Request) => { + const params = new URL(req.url).searchParams + const input = params.get('input') + const output = await model.run(input, { mean_pool: true, normalize: true }) + return new Response(JSON.stringify(output), { + headers: { + 'Content-Type': 'application/json', + Connection: 'keep-alive', + }, + }) +}) +``` + +## Using Large Language Models + +Inference via larger models is supported via [Ollama](https://ollama.com/). In the first iteration, the `llama2` and `mistral` models are supported and you would need to manage the Ollama server yourself. + + + +### Running locally + +1. Install Ollama and pull the Mistral model + + ``` + ollama pull mistral + ``` + +1. Run the Ollama server locally + + ``` + ollama serve + ``` + +1. Set a function secret called AI_INFERENCE_API_HOST to point to the Ollama server + + ``` + echo "AI_INFERENCE_API_HOST=http://host.docker.internal:11434/api/generate" >> supabase/functions/.env + ``` + +1. Create a new function with the following code + + ``` + supabase functions new ollama-test + ``` + + ```ts + /// + const session = new Supabase.ai.Session('mistral') + + Deno.serve(async (req: Request) => { + const params = new URL(req.url).searchParams + const prompt = params.get('prompt') ?? '' + + // Get the output as a stream + const output = await session.run(prompt, { stream: true }) + + const headers = new Headers({ + 'Content-Type': 'text/event-stream', + Connection: 'keep-alive', + }) + + // Create a stream + const stream = new ReadableStream({ + async start(controller) { + const encoder = new TextEncoder() + + try { + for await (const chunk of output) { + controller.enqueue(encoder.encode(chunk.response ?? '')) + } + } catch (err) { + console.error('Stream error:', err) + } finally { + controller.close() + } + }, + }) + + // Return the stream to the user + return new Response(stream, { + headers, + }) + }) + ``` + +1. Execute the function + + ``` + curl --get "http://localhost:54321/functions/v1/ollama-test" \ + --data-urlencode "prompt=write a short rap song about Supabase, the Postgres Developer platform, as sung by Nicki Minaj" \ + -H "Authorization: $ANON_KEY" + ``` + +### Deploying to production + +Once the function is working locally, it's time to deploy to production. + +1. Deploy a Ollama server and set a function secret called `AI_INFERENCE_API_HOST` to point to the deployed Ollama server + + ``` + supabase secrets set AI_INFERENCE_API_HOST=https://path-to-your-ollama-server/ + ``` + +2. Deploy the Supabase function + + ``` + supabase functions deploy ollama-test + ``` + +3. Execute the function + + ``` + curl --get "https://project-ref.supabase.co/functions/v1/ollama-test" \ + --data-urlencode "prompt=write a short rap song about Supabase, the Postgres Developer platform, as sung by Nicki Minaj" \ + -H "Authorization: $ANON_KEY" + ``` + +As demonstrated in the video above, running Ollama locally is typically slower than running it in on a server with dedicated GPUs. We are collaborating with the Ollama team to improve local performance. + +In the future, a hosted Ollama API, will be provided as part of the Supabase platform. Supabase will scale and manage the API and GPUs for you. To sign up for early access, fill up [this form](https://forms.supabase.com/supabase.ai-llm-early-access). diff --git a/apps/docs/content/guides/functions/examples/semantic-search.mdx b/apps/docs/content/guides/functions/examples/semantic-search.mdx new file mode 100644 index 00000000000..b42077dd395 --- /dev/null +++ b/apps/docs/content/guides/functions/examples/semantic-search.mdx @@ -0,0 +1,129 @@ +--- +id: 'function-ai-models' +title: 'Semantic Search' +description: 'Semantic Search with pgvector and Supabase Edge Functions' +subtitle: 'Semantic Search with pgvector and Supabase Edge Functions' +tocVideo: 'w4Rr_1whU-U' +--- + +[Semantic search](/docs/guides/ai/semantic-search) interprets the meaning behind user queries rather than exact [keywords](/docs/guides/ai/keyword-search). It uses machine learning to capture the intent and context behind the query, handling language nuances like synonyms, phrasing variations, and word relationships. + +Since Supabase Edge Runtime [v1.36.0](https://github.com/supabase/edge-runtime/releases/tag/v1.36.0) you can run the [`gte-small` model](https://huggingface.co/Supabase/gte-small) natively within Supabase Edge Functions without any external dependencies! This allows you to easily generate text embeddings without calling any external APIs! + +In this tutorial you're implementing three parts: + +1. A [`generate-embedding`](https://github.com/supabase/supabase/tree/master/examples/ai/edge-functions/supabase/functions/generate-embedding/index.ts) database webhook edge function which generates embeddings when a content row is added (or updated) in the [`public.embeddings`](https://github.com/supabase/supabase/tree/master/examples/ai/edge-functions/supabase/migrations/20240408072601_embeddings.sql) table. +2. A [`query_embeddings` Postgres function](https://github.com/supabase/supabase/tree/master/examples/ai/edge-functions/supabase/migrations/20240410031515_vector-search.sql) which allows us to perform similarity search from an egde function via [Remote Procedure Call (RPC)](https://supabase.com/docs/guides/database/functions?language=js). +3. A [`search` edge function](https://github.com/supabase/supabase/tree/master/examples/ai/edge-functions/supabase/functions/search/index.ts) which generates the embedding for the search term, performs the similarity search via RPC function call, and returns the result. + +You can find the complete example code on [GitHub](https://github.com/supabase/supabase/tree/master/examples/ai/edge-functions) + +### Create the database table and webhook + +Given the [following table definition](https://github.com/supabase/supabase/blob/master/examples/ai/edge-functions/supabase/migrations/20240408072601_embeddings.sql): + +```sql +create extension if not exists vector with schema extensions; + +create table embeddings ( + id bigint primary key generated always as identity, + content text not null, + embedding vector (384) +); +alter table embeddings enable row level security; + +create index on embeddings using hnsw (embedding vector_ip_ops); +``` + +You can deploy the [following edge function](https://github.com/supabase/supabase/blob/master/examples/ai/edge-functions/supabase/functions/generate-embedding/index.ts) as a [database webhook](/docs/guides/database/webhooks) to generate the embeddings for any text content inserted into the table: + +```tsx +const model = new Supabase.ai.Session('gte-small') + +Deno.serve(async (req) => { + const payload: WebhookPayload = await req.json() + const { content, id } = payload.record + + // Generate embedding. + const embedding = await model.run(content, { + mean_pool: true, + normalize: true, + }) + + // Store in database. + const { error } = await supabase + .from('embeddings') + .update({ embedding: JSON.stringify(embedding) }) + .eq('id', id) + if (error) console.warn(error.message) + + return new Response('ok') +}) +``` + +## Create a Postgres Function and RPC + +With the embeddings now stored in your Postgres database table, you can query them from Supabase Edge Functions by utilizing [Remote Procedure Calls (RPC)](/docs/guides/database/functions?language=js). + +Given the [following Postgres Function](https://github.com/supabase/supabase/blob/master/examples/ai/edge-functions/supabase/migrations/20240410031515_vector-search.sql): + +```sql +-- Matches document sections using vector similarity search on embeddings +-- +-- Returns a setof embeddings so that we can use PostgREST resource embeddings (joins with other tables) +-- Additional filtering like limits can be chained to this function call +create or replace function query_embeddings(embedding vector(384), match_threshold float) +returns setof embeddings +language plpgsql +as $$ +begin + return query + select * + from embeddings + + -- The inner product is negative, so we negate match_threshold + where embeddings.embedding <#> embedding < -match_threshold + + -- Our embeddings are normalized to length 1, so cosine similarity + -- and inner product will produce the same query results. + -- Using inner product which can be computed faster. + -- + -- For the different distance functions, see https://github.com/pgvector/pgvector + order by embeddings.embedding <#> embedding; +end; +$$; +``` + +## Query vectors in Supabase Edge Functions + +You can use `supabase-js` to first generate the embedding for the search term and then invoke the Postgres function to find the relevant results from your stored embeddings, right from your [Supabase Edge Function](https://github.com/supabase/supabase/blob/master/examples/ai/edge-functions/supabase/functions/search/index.ts): + +```tsx +const model = new Supabase.ai.Session('gte-small') + +Deno.serve(async (req) => { + const { search } = await req.json() + if (!search) return new Response('Please provide a search param!') + // Generate embedding for search term. + const embedding = await model.run(search, { + mean_pool: true, + normalize: true, + }) + + // Query embeddings. + const { data: result, error } = await supabase + .rpc('query_embeddings', { + embedding, + match_threshold: 0.8, + }) + .select('content') + .limit(3) + if (error) { + return Response.json(error) + } + + return Response.json({ search, result }) +}) +``` + +That's it, you now have AI powered semantic search set up without any external dependencies! Just you, pgvector, and Supabase Edge Functions! diff --git a/apps/docs/content/guides/functions/routing.mdx b/apps/docs/content/guides/functions/routing.mdx index ab5496f0c41..193505e5c16 100644 --- a/apps/docs/content/guides/functions/routing.mdx +++ b/apps/docs/content/guides/functions/routing.mdx @@ -58,7 +58,7 @@ You can deploy the function to Supabase via: supabase functions deploy hello-world ``` -Once the function is deployed, you can try to call the two ednpoints using cURL (or Postman). +Once the function is deployed, you can try to call the two endpoints using cURL (or Postman). ```bash # https://supabase.com/docs/guides/functions/deploy#invoking-remote-functions diff --git a/apps/docs/hooks/useHash.ts b/apps/docs/hooks/useHash.ts index 3c6f9567dab..29824c1742b 100644 --- a/apps/docs/hooks/useHash.ts +++ b/apps/docs/hooks/useHash.ts @@ -1,12 +1,17 @@ import { useState, useCallback, useEffect } from 'react' const useHash = () => { - const [hash, setHash] = useState(() => - typeof window !== 'undefined' ? window.location.hash.split('#')[1] : undefined - ) + const [hash, setHash] = useState(() => { + if (typeof window !== 'undefined') { + const url = new URL(window.location.href) + return url.hash.substring(1) // Removes the leading '#' + } + return undefined + }) const hashChangeHandler = useCallback(() => { - setHash(window.location.hash.split('#')[1]) + const url = new URL(window.location.href) + setHash(url.hash.substring(1)) }, []) useEffect(() => { @@ -18,7 +23,9 @@ const useHash = () => { const updateHash = useCallback( (newHash) => { - if (newHash !== hash) window.location.hash = newHash + if (newHash !== hash) { + window.location.hash = newHash + } }, [hash] ) diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyEditor/PolicyRoles.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyEditor/PolicyRoles.tsx index c8373a59039..4586d246c21 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyEditor/PolicyRoles.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyEditor/PolicyRoles.tsx @@ -43,7 +43,7 @@ const PolicyRoles = ({ selectedRoles, onUpdateSelectedRoles }: PolicyRolesProps)
{isLoading && } - {isError && } + {isError && } {isSuccess && ( { @@ -54,8 +54,8 @@ const CreateRolePanel = ({ visible, onClose }: CreateRolePanelProps) => { }) const { mutate: createDatabaseRole, isLoading: isCreating } = useDatabaseRoleCreateMutation({ - onSuccess: (res) => { - toast.success(`Successfully created new role: ${res.name}`) + onSuccess: (_, vars) => { + toast.success(`Successfully created new role: ${vars.payload.name}`) handleClose() }, }) diff --git a/apps/studio/components/interfaces/Database/Roles/DeleteRoleModal.tsx b/apps/studio/components/interfaces/Database/Roles/DeleteRoleModal.tsx index efb5dfdd93c..4422024b5c2 100644 --- a/apps/studio/components/interfaces/Database/Roles/DeleteRoleModal.tsx +++ b/apps/studio/components/interfaces/Database/Roles/DeleteRoleModal.tsx @@ -27,7 +27,7 @@ const DeleteRoleModal = ({ role, visible, onClose }: DeleteRoleModalProps) => { deleteDatabaseRole({ projectRef: project.ref, connectionString: project.connectionString, - id: role.id.toString(), + id: role.id, }) } diff --git a/apps/studio/components/interfaces/Database/Roles/RoleRow.tsx b/apps/studio/components/interfaces/Database/Roles/RoleRow.tsx index a138fd08ee9..ae1f6eb55bd 100644 --- a/apps/studio/components/interfaces/Database/Roles/RoleRow.tsx +++ b/apps/studio/components/interfaces/Database/Roles/RoleRow.tsx @@ -1,5 +1,4 @@ import * as Tooltip from '@radix-ui/react-tooltip' -import type { PostgresRole } from '@supabase/postgres-meta' import { useState } from 'react' import toast from 'react-hot-toast' import { @@ -18,13 +17,14 @@ import { } from 'ui' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { PgRole } from 'data/database-roles/database-roles-query' import { useDatabaseRoleUpdateMutation } from 'data/database-roles/database-role-update-mutation' import { ROLE_PERMISSIONS } from './Roles.constants' interface RoleRowProps { - role: PostgresRole + role: PgRole disabled?: boolean - onSelectDelete: (role: PostgresRole) => void + onSelectDelete: (role: PgRole) => void } const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps) => { @@ -33,25 +33,22 @@ const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps) => { const { mutate: updateDatabaseRole, isLoading: isUpdating } = useDatabaseRoleUpdateMutation() - const { - is_superuser, - can_login, - can_create_role, - can_create_db, - is_replication_role, - can_bypass_rls, - } = role + const { isSuperuser, canLogin, canCreateRole, canCreateDb, isReplicationRole, canBypassRls } = + role - const onSaveChanges = async (values: any, { resetForm }: any) => { + const onSaveChanges = async (values: Partial, { resetForm }: any) => { if (!project) return console.error('Project is required') - const { is_superuser, is_replication_role, ...payload } = values + const changed = Object.fromEntries( + Object.entries(values).filter(([k, v]) => v !== (role as any)[k]) + ) + updateDatabaseRole( { projectRef: project.ref, connectionString: project.connectionString, id: role.id, - payload, + payload: changed, }, { onSuccess: () => { @@ -66,12 +63,12 @@ const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps) => {
{
- {role.active_connections > 0 && ( + {role.activeConnections > 0 && (
@@ -129,10 +126,10 @@ const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps) => {

0 ? 'text-foreground' : 'text-foreground-light' + role.activeConnections > 0 ? 'text-foreground' : 'text-foreground-light' }`} > - {role.active_connections} connections + {role.activeConnections} connections

{!disabled && ( diff --git a/apps/studio/components/interfaces/Database/Roles/Roles.constants.ts b/apps/studio/components/interfaces/Database/Roles/Roles.constants.ts index a8b661eec69..377dd9589c6 100644 --- a/apps/studio/components/interfaces/Database/Roles/Roles.constants.ts +++ b/apps/studio/components/interfaces/Database/Roles/Roles.constants.ts @@ -33,35 +33,35 @@ export const SYSTEM_ROLES = [ ] as const export const ROLE_PERMISSIONS = { - can_login: { + canLogin: { disabled: false, description: 'User can login', grant_by_dashboard: true, }, - can_create_role: { + canCreateRole: { disabled: false, description: 'User can create roles', grant_by_dashboard: true, }, - can_create_db: { + canCreateDb: { disabled: false, description: 'User can create databases', grant_by_dashboard: true, }, - can_bypass_rls: { - disabled: true, + canBypassRls: { + disabled: false, description: 'User bypasses every row level security policy', - grant_by_dashboard: false, + grant_by_dashboard: true, }, - is_superuser: { + isSuperuser: { disabled: true, description: 'User is a Superuser', grant_by_dashboard: false, }, - is_replication_role: { - disabled: true, + isReplicationRole: { + disabled: false, description: 'User can initiate streaming replication and put the system in and out of backup mode', - grant_by_dashboard: false, + grant_by_dashboard: true, }, } as const diff --git a/apps/studio/components/interfaces/Database/Roles/RolesList.tsx b/apps/studio/components/interfaces/Database/Roles/RolesList.tsx index 302328cac7c..089efbe930e 100644 --- a/apps/studio/components/interfaces/Database/Roles/RolesList.tsx +++ b/apps/studio/components/interfaces/Database/Roles/RolesList.tsx @@ -42,19 +42,19 @@ const RolesList = () => { const roles = sortBy(data ?? [], (r) => r.name.toLocaleLowerCase()) const filteredRoles = ( - filterType === 'active' ? roles.filter((role) => role.active_connections > 0) : roles + filterType === 'active' ? roles.filter((role) => role.activeConnections > 0) : roles ).filter((role) => role.name.includes(filterString)) const [supabaseRoles, otherRoles] = partition(filteredRoles, (role) => SUPABASE_ROLES.includes(role.name as SUPABASE_ROLE) ) const totalActiveConnections = roles - .map((role) => role.active_connections) + .map((role) => role.activeConnections) .reduce((a, b) => a + b, 0) // order the roles with active connections by number of connections, most connections first const rolesWithActiveConnections = sortBy( - roles.filter((role) => role.active_connections > 0), - (r) => -r.active_connections + roles.filter((role) => role.activeConnections > 0), + (r) => -r.activeConnections ) return ( @@ -145,7 +145,7 @@ const RolesList = () => {

Connections by roles:

{rolesWithActiveConnections.map((role) => (
- {role.name}: {role.active_connections} + {role.name}: {role.activeConnections}
))}
diff --git a/apps/studio/components/interfaces/QueryPerformanceV2/IndexAdvisorDisabledState.tsx b/apps/studio/components/interfaces/QueryPerformanceV2/IndexAdvisorDisabledState.tsx index 8f277142a18..f25c2cf93ce 100644 --- a/apps/studio/components/interfaces/QueryPerformanceV2/IndexAdvisorDisabledState.tsx +++ b/apps/studio/components/interfaces/QueryPerformanceV2/IndexAdvisorDisabledState.tsx @@ -3,23 +3,23 @@ import { ExternalLink } from 'lucide-react' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { useDatabaseExtensionEnableMutation } from 'data/database-extensions/database-extension-enable-mutation' import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' -import { useIndexAdvisorEnableMutation } from 'data/database/index-advisor-enable-mutation' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui' import { Markdown } from '../Markdown' +import Link from 'next/link' +import { useParams } from 'common' export const IndexAdvisorDisabledState = () => { + const { ref } = useParams() const { project } = useProjectContext() const { data: extensions } = useDatabaseExtensionsQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const hypopgExtension = (extensions ?? []).find((ext) => ext.name === 'hypopg') + const indexAdvisorExtension = (extensions ?? []).find((ext) => ext.name === 'index_advisor') const { mutateAsync: enableExtension, isLoading: isEnablingExtension } = useDatabaseExtensionEnableMutation() - const { mutateAsync: enableIndexAdvisor, isLoading: isEnablingIndexAdvisor } = - useIndexAdvisorEnableMutation() - const isEnabling = isEnablingExtension || isEnablingIndexAdvisor const onEnableIndexAdvisor = async () => { if (project === undefined) return console.error('Project is required') @@ -32,33 +32,56 @@ export const IndexAdvisorDisabledState = () => { version: hypopgExtension.default_version, }) } - - // [Joshen] Once index_advisor ext is ready, it'll just be enabling an extension - await enableIndexAdvisor({ - projectRef: project.ref, - connectionString: project.connectionString, - }) + if (indexAdvisorExtension?.installed_version === null) { + await enableExtension({ + projectRef: project?.ref, + connectionString: project?.connectionString, + name: indexAdvisorExtension.name, + schema: indexAdvisorExtension?.schema ?? 'extensions', + version: indexAdvisorExtension.default_version, + }) + } } return ( - + - Get index suggestions to improve your query performance + - + +
- - + ) : ( + + )} +