<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Gleb Otochkin</title>
    <description>The latest articles on DEV Community by Gleb Otochkin (@gleb_otochkin).</description>
    <link>https://dev.to/gleb_otochkin</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3599230%2F235f9bc7-2df4-45ca-ac02-f8aaf243c969.png</url>
      <title>DEV Community: Gleb Otochkin</title>
      <link>https://dev.to/gleb_otochkin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gleb_otochkin"/>
    <language>en</language>
    <item>
      <title>Activating Your Data Layer for Production-Ready AI</title>
      <dc:creator>Gleb Otochkin</dc:creator>
      <pubDate>Mon, 05 Oct 2026 19:39:51 +0000</pubDate>
      <link>https://dev.to/googleai/activating-your-data-layer-for-production-ready-ai-p3e</link>
      <guid>https://dev.to/googleai/activating-your-data-layer-for-production-ready-ai-p3e</guid>
      <description>&lt;p&gt;When discussing applications and systems using generative AI and the new opportunities they present, one component of the ecosystem is irreplaceable - data. Specifically, the data that companies gather, hold, and use daily. This data serves as the backbone for applications, analytics, knowledge bases, and much more. We use databases to store and work with this data, and most, if not all, AI-driven initiatives and new applications are going to use that data layer.&lt;/p&gt;

&lt;p&gt;But how can we start to use the data in our AI systems? Let me introduce you to some of the labs showing how to prepare and use the data with AI models in Google databases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Semantic Search: Text Embeddings in Database
&lt;/h2&gt;

&lt;p&gt;Our journey starts by preparing our data for semantic search and running first tests to augment the Gen AI model's response by grounding it with your semantic search results. The grounding data is the basis for RAG (Retrieval Augmented Generation). Then, you can improve the performance of your search by indexing your embeddings using the latest indexing techniques.&lt;/p&gt;

&lt;p&gt;One of the options is the &lt;a href="https://cloud.google.com/products/alloydb?e=48754805" rel="noopener noreferrer"&gt;&lt;strong&gt;Google AlloyDB database&lt;/strong&gt;&lt;/a&gt;, which has direct integration with AI models and supports the most demanding workloads. The following lab guides us through all the steps, starting from creating an AlloyDB cluster, loading sample data, and generating embeddings, to using those embeddings to generate an augmented response from the Gen AI model.&lt;/p&gt;


&lt;div class="crayons-card c-embed"&gt;

  
&lt;h4&gt;
  
  
  Go to the lab!
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Lab&lt;/strong&gt;: &lt;a href="https://codelabs.developers.google.com/alloydb-ai-embedding#0" rel="noopener noreferrer"&gt;Getting started with Vector Embeddings with AlloyDB AI&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Objective&lt;/strong&gt;: &lt;em&gt;Create an AlloyDB cluster, generate vector embeddings for sample data, and perform semantic search to ground a Gen AI model's response.&lt;/em&gt;&lt;/p&gt;


&lt;/div&gt;


&lt;p&gt;AI integration is not limited to AlloyDB. All Google Cloud databases have AI integration and are capable of generating and using embeddings for semantic search. For example, if you are using &lt;a href="https://cloud.google.com/sql" rel="noopener noreferrer"&gt;&lt;strong&gt;Cloud SQL&lt;/strong&gt;&lt;/a&gt;, you can also generate and use embeddings for semantic search directly within your existing &lt;a href="https://cloud.google.com/sql/postgresql" rel="noopener noreferrer"&gt;&lt;strong&gt;PostgreSQL&lt;/strong&gt;&lt;/a&gt; or &lt;a href="https://cloud.google.com/sql/mysql" rel="noopener noreferrer"&gt;&lt;strong&gt;MySQL&lt;/strong&gt;&lt;/a&gt; instances.&lt;/p&gt;

&lt;p&gt;The next two labs are very similar to the previous one, but instead of Google AlloyDB for PostgreSQL, we are using Cloud SQL for PostgreSQL and Cloud SQL for MySQL to use semantic search as the grounding engine for the model's response. Some steps are of course different due to variations in SQL language and different database engines, but the main idea stays the same: use our data to ground the model response and improve output.&lt;/p&gt;


&lt;div class="crayons-card c-embed"&gt;

  
&lt;h4&gt;
  
  
  Go to the labs!
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Lab:&lt;/strong&gt; &lt;a href="https://codelabs.developers.google.com/csql-pg-ai-embedding#0" rel="noopener noreferrer"&gt;Getting started with Vector Embeddings in Cloud SQL for PostgreSQL&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Objective:&lt;/strong&gt; &lt;em&gt;Create Cloud SQL for PostgreSQL instance with AI integration, generate vector embeddings for sample data, and perform semantic search to ground a Gen AI model's response.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lab:&lt;/strong&gt; &lt;a href="https://codelabs.developers.google.com/csql-mysql-ai-embedding#0" rel="noopener noreferrer"&gt;Getting started with Vector Embeddings in Cloud SQL for MySQL&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Objective:&lt;/strong&gt; &lt;em&gt;Create Cloud SQL for MySQL instance with AI integration, generate vector embeddings for sample data, and perform semantic search to ground a Gen AI model's response.&lt;/em&gt;&lt;/p&gt;


&lt;/div&gt;


&lt;p&gt;Semantic search using text data is one of the cornerstones and important features making responses much more reliable and useful, but Google Gen AI models can offer much more. Let's talk about multimodal search.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multimodal Embeddings: Bring Images to the Search
&lt;/h2&gt;

&lt;p&gt;In real life, of course, we use all our senses, including vision, to evaluate the world around us. The Google multimodal embedding models bring an additional layer of understanding, improving search by using embeddings not only for text but also for images.&lt;/p&gt;

&lt;p&gt;In the following lab, we use a catalog of products placed in AlloyDB and supplemented by images in Google Cloud Storage. In the lab, we show how we can use both text descriptions and images for semantic search, supplementing and replacing each other, naturally incorporating search based on image input into our response.&lt;/p&gt;


&lt;div class="crayons-card c-embed"&gt;

  
&lt;h4&gt;
  
  
  Go to the lab!
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Lab:&lt;/strong&gt; &lt;a href="https://codelabs.developers.google.com/alloydb-ai-mm-embeddings?hl=en#0" rel="noopener noreferrer"&gt;Multimodal Embeddings in AlloyDB&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Objective:&lt;/strong&gt; &lt;em&gt;Deploy an AlloyDB cluster, import data and use text and image semantic search using multimodal embeddings, try hybrid search approach.&lt;/em&gt;&lt;/p&gt;


&lt;/div&gt;


&lt;p&gt;Preparing the data and making first steps are important for a general understanding of RAG and tools available for the search, but Google has other cases when direct AI integration can help with your data analysis without any data preparations.&lt;/p&gt;

&lt;h2&gt;
  
  
  AlloyDB AI Functions and Reranking
&lt;/h2&gt;

&lt;p&gt;Google AlloyDB database comes with additional AI integrations that help you use some AI capabilities without data preparation. For example, the AI.IF function can perform semantic search on the fly, evaluating sentiment or comparing data in columns with a natural language query, returning results filtered by the query condition. Also, you can apply a ranking function to the search output, improving the final result. You can try some of the new functionality using the following lab and let us know if it can help in your use case.&lt;/p&gt;


&lt;div class="crayons-card c-embed"&gt;

  
&lt;h4&gt;
  
  
  Go to the lab!
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Lab:&lt;/strong&gt; &lt;a href="https://codelabs.developers.google.com/alloydb-ai-operators?hl=en#0" rel="noopener noreferrer"&gt;AlloyDB AI Operators and Reranking&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Objective:&lt;/strong&gt; &lt;em&gt;Deploy AlloyDB cluster and enable AI query functions, use AI query functions to filter data and ranking function to improve the search results.&lt;/em&gt;&lt;/p&gt;


&lt;/div&gt;


&lt;p&gt;But what if somebody is not particularly savvy with SQL or not familiar with the data structure in your database? The QueryData for AlloyDB can help you with that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generate SQL using QueryData for AlloyDB
&lt;/h2&gt;

&lt;p&gt;QueryData lets you interact with the data in your database using conversational language and build data agents. You provide database-specific information in the form of context sets to improve generator accuracy.&lt;/p&gt;

&lt;p&gt;The QueryData context add a layer describing your data schema and providing templates, facets and value search options to make your generated SQL predictable and production ready. The following lab helps you to start with the new features and generate your first queries based on your data schema.&lt;/p&gt;


&lt;div class="crayons-card c-embed"&gt;

  
&lt;h4&gt;
  
  
  Go to the lab!
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Lab:&lt;/strong&gt; &lt;a href="https://codelabs.developers.google.com/next26/alloydb-querydata?hl=en#0" rel="noopener noreferrer"&gt;QueryData for AlloyDB using Gemini Data Analytics&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Objective:&lt;/strong&gt; &lt;em&gt;Use QueryData for AlloyDB to generate accurate and predictable SQL statements from natural language input in agentic applications&lt;/em&gt;&lt;/p&gt;


&lt;/div&gt;


&lt;h2&gt;
  
  
  From Tests to Production
&lt;/h2&gt;

&lt;p&gt;Those labs are part of the &lt;strong&gt;From Data Foundations to Advanced RAG&lt;/strong&gt; module of our &lt;a href="https://cloud.google.com/blog/topics/developers-practitioners/production-ready-ai-with-google-cloud-learning-path" rel="noopener noreferrer"&gt;Production-Ready AI with Google Cloud&lt;/a&gt; program. Check the other modules and see if they can help you to adopt the AI capabilities provided by our Google Cloud services and tools. The end game goal is a high quality application using the full potential of modern technologies.&lt;/p&gt;

&lt;p&gt;And stay tuned on release notes for ALloyDB and Cloud SQL - the engineering team is busy working on new features and improvements. Happy testing.&lt;/p&gt;

</description>
      <category>database</category>
      <category>googlecloud</category>
      <category>ai</category>
    </item>
    <item>
      <title>Sub-second semantic filtering in PostgreSQL: Integrating the ultra-fast Jev model with AlloyDB</title>
      <dc:creator>Gleb Otochkin</dc:creator>
      <pubDate>Tue, 29 Sep 2026 17:00:53 +0000</pubDate>
      <link>https://dev.to/googleai/sub-second-semantic-filtering-in-postgresql-integrating-the-ultra-fast-jev-model-with-alloydb-m8k</link>
      <guid>https://dev.to/googleai/sub-second-semantic-filtering-in-postgresql-integrating-the-ultra-fast-jev-model-with-alloydb-m8k</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq9dbnzqqdjazzymcr8ho.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq9dbnzqqdjazzymcr8ho.png" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Introduction
&lt;/h3&gt;

&lt;p&gt;If you are working with semantic search, vector embeddings and AI models, you probably already know the process with either building embeddings using direct AI integration like in &lt;a href="https://docs.cloud.google.com/alloydb/docs/ai/work-with-embeddings?utm_campaign=CDR_0x370c34a8_default_b567422655&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;AlloyDB&lt;/a&gt; or through application layer and pipelines. Embeddings and direct access to LLM give you options to do classifications, sentiment analysis and many other things.&lt;/p&gt;

&lt;p&gt;But what if you want to run these evaluations directly on your operational data without the latency overhead of generative LLMs? In AlloyDB for PostgreSQL, thanks to &lt;a href="https://docs.cloud.google.com/alloydb/docs/ai/model-endpoint-overview?utm_campaign=CDR_0x370c34a8_default_b567422655&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;model endpoint management&lt;/a&gt;, we aren’t restricted only to standard generative models. We can connect specialized, ultra-low-latency models designed specifically for high-speed logical constraints — like &lt;a href="https://typesafe.ai/" rel="noopener noreferrer"&gt;TypeSafe AI&lt;/a&gt;’s Jev model.&lt;/p&gt;

&lt;p&gt;By integrating Jev, which is heavily optimized for returning structural evaluations (like booleans and scores) in milliseconds rather than generating conversational text, we can execute high-throughput semantic queries at database speeds using native functions like ai.if and ai.analyze_sentiment.&lt;/p&gt;

&lt;p&gt;In this post, I will walk you through a complete setup: configuring the cluster and primary instance, adding secrets, creating the necessary PL/pgSQL translation layers, and leveraging Jev’s parallel batching capabilities to see how fast AI evaluations can be without preparing the embeddings in advance. Let’s dive in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding Jev model to the cluster
&lt;/h3&gt;

&lt;p&gt;If you already have an AlloyDB cluster you can use it right away or you can create one using &lt;a href="https://docs.cloud.google.com/alloydb/docs/cluster-create?utm_campaign=CDR_0x370c34a8_default_b567422655&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;Google documentation&lt;/a&gt;. Make sure you’ve enabled the AI integration following the &lt;a href="https://docs.cloud.google.com/alloydb/docs/ai/configure-vertex-ai?utm_campaign=CDR_0x370c34a8_default_b567422655&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;guide&lt;/a&gt; and enabled the following database parameters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;google_ml_integration.enable_model_support=on&lt;/li&gt;
&lt;li&gt;google_ml_integration.enable_ai_query_engine=on&lt;/li&gt;
&lt;li&gt;google_ml_integration.enable_ai_function_acceleration=on&lt;/li&gt;
&lt;li&gt;google_ml_integration.enable_cost_optimized_ai_functions=on&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The combination of all those parameters is what makes AlloyDB AI function to be snappy and give the best performance.&lt;/p&gt;

&lt;p&gt;Also check your primary instance network settings. We need an outbound public IP to be able to reach the Typesafe AI model endpoint. If your instance doesn’t have it you are going to get timeout trying to reach the model.&lt;/p&gt;

&lt;p&gt;If you haven’t already got an API key from &lt;a href="https://typesafe.ai/" rel="noopener noreferrer"&gt;https://typesafe.ai/&lt;/a&gt; then you need to register there and generate the key. It will be used to access the model from AlloyDB.&lt;/p&gt;

&lt;p&gt;The next step is to add the API key to the Google Cloud secret manager. Here is a simple example how to do that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud secrets create typesafe-jev-api-key &lt;span class="nt"&gt;--replication-policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"automatic"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;YOUR_TYPESAFE_API_KEY&amp;gt;"&lt;/span&gt; | gcloud secrets versions add typesafe-jev-api-key &lt;span class="nt"&gt;--data-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;-
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now it is time to register the model in your database. You can use any of your databases with a sample dataset to test it out. I was using a database with a sample e-commerce dataset with 30 thousand different products.&lt;/p&gt;

&lt;p&gt;In the AlloyDB Studio choose your database and a user who has enough privileges to work with google_ml_integration extension and AI integration functionality. I was doing my tests using ai.if and ai.analyze_sentiment functions with the default model and with the TypeSafe AI’s Jev model.&lt;/p&gt;

&lt;p&gt;TypeSafe AI’s System One API accepts structured JSON with distinct reasoning primitives (Noul for boolean constraints, Choice for multi-class classification). You can read about the primitives in the TypeSafe AI’s &lt;a href="https://docs.typesafe.ai/introduction#typesafe-primitives" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;, but here is a very basic explanation:&lt;/p&gt;

&lt;p&gt;You have 3 different question types — Choice, Score or Noul. Depending on the type of the question you get different output. For example, for the type of question Noul you are getting 0 or 1 or, in other words, true or false.&lt;/p&gt;

&lt;p&gt;AlloyDB’s standard ai.* functions need to be translated into this specific format. We use four transformation functions to bridge the gap. Here is the scalar input transform that handles Noul and Choice types for both ai.if and ai.analyze_sentiment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plsql"&gt;&lt;code&gt;&lt;span class="kr"&gt;CREATE&lt;/span&gt; &lt;span class="kr"&gt;OR&lt;/span&gt; &lt;span class="kr"&gt;REPLACE&lt;/span&gt; &lt;span class="kr"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jev_model_input_transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model_id&lt;/span&gt; &lt;span class="kt"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;input_text&lt;/span&gt; &lt;span class="k"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;generation_config&lt;/span&gt; &lt;span class="k"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;system_instruction&lt;/span&gt; &lt;span class="k"&gt;TEXT&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;JSON&lt;/span&gt;
&lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt; &lt;span class="k"&gt;IMMUTABLE&lt;/span&gt; &lt;span class="kr"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt;
    &lt;span class="n"&gt;full_instruction&lt;/span&gt; &lt;span class="k"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;enum_json&lt;/span&gt; &lt;span class="k"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;criteria_obj&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{}&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;elem&lt;/span&gt; &lt;span class="k"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;q_obj&lt;/span&gt; &lt;span class="k"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="c1"&gt;-- Combine optional system instructions with the input prompt&lt;/span&gt;
    &lt;span class="k"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;system_instruction&lt;/span&gt; &lt;span class="ow"&gt;IS&lt;/span&gt; &lt;span class="ow"&gt;NOT&lt;/span&gt; &lt;span class="ow"&gt;NULL&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="nf"&gt;length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;system_instruction&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="kr"&gt;THEN&lt;/span&gt;
        &lt;span class="n"&gt;full_instruction&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;system_instruction&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;E&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;input_text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kr"&gt;ELSE&lt;/span&gt;
        &lt;span class="n"&gt;full_instruction&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;input_text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Inspect generation_config to determine whether caller expects a multi-class choice or boolean&lt;/span&gt;
    &lt;span class="n"&gt;enum_json&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nf"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;generation_config&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;'&lt;/span&gt;&lt;span class="s1"&gt;generationConfig&lt;/span&gt;&lt;span class="o"&gt;'-&amp;gt;'&lt;/span&gt;&lt;span class="s1"&gt;responseSchema&lt;/span&gt;&lt;span class="o"&gt;'-&amp;gt;'&lt;/span&gt;&lt;span class="s1"&gt;enum&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;generation_config&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;'&lt;/span&gt;&lt;span class="s1"&gt;responseSchema&lt;/span&gt;&lt;span class="o"&gt;'-&amp;gt;'&lt;/span&gt;&lt;span class="s1"&gt;enum&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;enum_json&lt;/span&gt; &lt;span class="ow"&gt;IS&lt;/span&gt; &lt;span class="ow"&gt;NOT&lt;/span&gt; &lt;span class="ow"&gt;NULL&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;json_typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enum_json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;array&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt; &lt;span class="kr"&gt;THEN&lt;/span&gt;
        &lt;span class="c1"&gt;-- Multi-class classification (ai.analyze_sentiment) -&amp;gt; Jev choice primitive&lt;/span&gt;
        &lt;span class="kr"&gt;FOR&lt;/span&gt; &lt;span class="n"&gt;elem&lt;/span&gt; &lt;span class="ow"&gt;IN&lt;/span&gt; &lt;span class="kr"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;json_array_elements_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enum_json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kr"&gt;LOOP&lt;/span&gt;
            &lt;span class="n"&gt;criteria_obj&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;criteria_obj&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;jsonb_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Category: &lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;elem&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="kr"&gt;LOOP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;q_obj&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;type&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;choice&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;instructions&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;full_instruction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;criteria&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;criteria_obj&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="k"&gt;JSON&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kr"&gt;ELSE&lt;/span&gt;
        &lt;span class="c1"&gt;-- Boolean filtering (ai.if) -&amp;gt; Jev noul primitive&lt;/span&gt;
        &lt;span class="n"&gt;q_obj&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;type&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;noul&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;instructions&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;full_instruction&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;json_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;model&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jev-latest&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;state&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generation_config&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;'&lt;/span&gt;&lt;span class="s1"&gt;state&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Evaluate the question accurately based on the provided text.&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;questions&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;q_1&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q_obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also need a scalar output transform to unpack Jev’s JSON response back into a PostgreSQL TEXT result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plsql"&gt;&lt;code&gt;&lt;span class="kr"&gt;CREATE&lt;/span&gt; &lt;span class="kr"&gt;OR&lt;/span&gt; &lt;span class="kr"&gt;REPLACE&lt;/span&gt; &lt;span class="kr"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jev_model_output_transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model_id&lt;/span&gt; &lt;span class="kt"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;response_json&lt;/span&gt; &lt;span class="k"&gt;JSON&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;TEXT&lt;/span&gt;
&lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt; &lt;span class="k"&gt;IMMUTABLE&lt;/span&gt; &lt;span class="kr"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt;
    &lt;span class="n"&gt;q_ans&lt;/span&gt; &lt;span class="k"&gt;JSON&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;response_json&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;'&lt;/span&gt;&lt;span class="s1"&gt;answers&lt;/span&gt;&lt;span class="o"&gt;'-&amp;gt;'&lt;/span&gt;&lt;span class="s1"&gt;q_1&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;ans_type&lt;/span&gt; &lt;span class="k"&gt;TEXT&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;q_ans&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;'&lt;/span&gt;&lt;span class="s1"&gt;type&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="k"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;ans_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;choice&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt; &lt;span class="kr"&gt;THEN&lt;/span&gt;
        &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;q_ans&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;'&lt;/span&gt;&lt;span class="s1"&gt;choice&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kr"&gt;ELSIF&lt;/span&gt; &lt;span class="n"&gt;ans_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;noul&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt; &lt;span class="kr"&gt;THEN&lt;/span&gt;
        &lt;span class="c1"&gt;-- Convert probability score (0.00 to 1.00) to boolean text&lt;/span&gt;
        &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q_ans&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;'&lt;/span&gt;&lt;span class="s1"&gt;noul&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="kt"&gt;FLOAT&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.50&lt;/span&gt; &lt;span class="kr"&gt;THEN&lt;/span&gt; &lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt; &lt;span class="kr"&gt;ELSE&lt;/span&gt; &lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;false&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kr"&gt;ELSE&lt;/span&gt;
        &lt;span class="kr"&gt;RAISE&lt;/span&gt; &lt;span class="k"&gt;EXCEPTION&lt;/span&gt; &lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unexpected Jev response payload: %&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_json&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="k"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a batched input and output you should use a modified set of functions which understand input and output as an array of values. All the functions are available in &lt;a href="https://codelabs.developers.google.com/alloydb-ai-jev?utm_campaign=CDR_0x370c34a8_default_b567422655&amp;amp;utm_medium=external&amp;amp;utm_source=blog#5" rel="noopener noreferrer"&gt;the codelab&lt;/a&gt; created by my colleague Paul Ramsey.&lt;/p&gt;

&lt;p&gt;Now we bind the transformation functions, secret credential, and the external REST endpoint together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;google_ml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model_id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'jev-model'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model_request_url&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'https://api.typesafe.ai/v1/systemone'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model_provider&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'custom'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model_type&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'llm'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model_qualified_name&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'jev-latest'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model_auth_type&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'secret_manager'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model_auth_id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'jev_api_key'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model_in_transform_fn&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'jev_model_input_transform'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model_out_transform_fn&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'jev_model_output_transform'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model_batch_in_transform_fn&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'jev_model_batch_input_transform'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model_batch_out_transform_fn&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'jev_model_batch_output_transform'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You’ve noticed the model is using the Google secret manager as authentication provider with the auth key as the secret name.&lt;/p&gt;

&lt;h3&gt;
  
  
  Basic Semantic Testing
&lt;/h3&gt;

&lt;p&gt;With jev-model registered, we can execute semantic operations directly inside standard PostgreSQL queries.&lt;/p&gt;

&lt;p&gt;First, let’s test a simple boolean constraint using ai.if:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Is the product "North Face Waterproof Gore-Tex Hiking Jacket ($249)" suitable for rainy outdoor conditions?'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model_id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'jev-model'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;is_suitable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And getting expected result in just few millliseconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; is_suitable
-------------
 true
(1 row)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, let’s test multi-class classification using ai.analyze_sentiment on a customer review:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;analyze_sentiment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'The stitching on this jacket tore on the second day and the zipper jammed completely.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model_id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'jev-model'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;sentiment&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the result in ms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; sentiment
-----------
 negative
(1 row)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  High-Throughput Array Batching on Tables
&lt;/h3&gt;

&lt;p&gt;Usually, executing an HTTP call for thousands of rows row-by-row is painfully slow. However, by using PostgreSQL’s row_number() window function and array batching, we can evaluate dozens of items in a single HTTP round-trip without generative degradation.&lt;br&gt;&lt;br&gt;
Here is an example evaluating a 50-row batch of real catalog items to find cold-weather gear:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;numbered&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; 
        &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product_description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;row_number&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;batch_id&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ecomm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;
    &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
    &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;batched&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; 
        &lt;span class="n"&gt;batch_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;array_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;array_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;names&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;prompts&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;array_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Is "'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'" designed for cold weather winter outerwear?'&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;model_id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'jev-model'&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;decisions&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;numbered&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;batch_id&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;unrolled&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; 
        &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;batch_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decision&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cold_weather_gear&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;batched&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
    &lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="k"&gt;LATERAL&lt;/span&gt; &lt;span class="k"&gt;unnest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;names&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decisions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cold_weather_gear&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;unrolled&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;cold_weather_gear&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here are the results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  id | name | cold_weather_gear
-------+--------------------------------------------------------------+-------------------
 29071 | Winter Striped Beanie with Pom | true
 29077 | Knit Collegiate Rugby Stripe Winter Scarf &amp;amp; Beanie Hat Set | true
 29083 | Junction Finds Men's Muscle Wool Blend Trapper Hat | true
 29086 | Mens Soft Thermal Insulated Wrist Length Leather Gloves | true
 29090 | Long Beanie-Red W16S24E | true
(5 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here is the execution plan for the query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                                                                              QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Limit (cost=6515.00..506515.19 rows=5 width=41) (actual time=369.386..369.391 rows=5.00 loops=1)
   Buffers: shared hit=2479
   -&amp;gt; Nested Loop (cost=6515.00..25006524.75 rows=250 width=41) (actual time=369.385..369.389 rows=5.00 loops=1)
         Buffers: shared hit=2479
         -&amp;gt; GroupAggregate (cost=6514.99..25006516.74 rows=50 width=104) (actual time=369.368..369.370 rows=1.00 loops=1)
               Group Key: numbered.batch_id
               Buffers: shared hit=2479
               -&amp;gt; Sort (cost=6514.99..6515.11 rows=50 width=67) (actual time=17.838..17.840 rows=31.00 loops=1)
                     Sort Key: numbered.batch_id, numbered.id
                     Sort Method: quicksort Memory: 29kB
                     Buffers: shared hit=2440
                     -&amp;gt; Subquery Scan on numbered (cost=6512.95..6513.58 rows=50 width=67) (actual time=17.819..17.826 rows=50.00 loops=1)
                           Buffers: shared hit=2440
                           -&amp;gt; Limit (cost=6512.95..6513.08 rows=50 width=615) (actual time=17.816..17.819 rows=50.00 loops=1)
                                 Buffers: shared hit=2440
                                 -&amp;gt; Sort (cost=6512.95..6585.75 rows=29120 width=615) (actual time=17.815..17.817 rows=50.00 loops=1)
                                       Sort Key: products.id DESC
                                       Sort Method: top-N heapsort Memory: 37kB
                                       Buffers: shared hit=2440
                                       -&amp;gt; WindowAgg (cost=4890.43..5545.61 rows=29120 width=615) (actual time=8.715..14.386 rows=29120.00 loops=1)
                                             Window: w1 AS (ORDER BY products.id ROWS UNBOUNDED PRECEDING)
                                             Storage: Memory Maximum Storage: 17kB
                                             Buffers: shared hit=2440
                                             -&amp;gt; Sort (cost=4890.41..4963.21 rows=29120 width=59) (actual time=8.700..9.204 rows=29120.00 loops=1)
                                                   Sort Key: products.id
                                                   Sort Method: quicksort Memory: 3014kB
                                                   Buffers: shared hit=2440
                                                   -&amp;gt; Seq Scan on products (cost=0.00..2731.20 rows=29120 width=59) (actual time=0.014..3.791 rows=29120.00 loops=1)
                                                         Buffers: shared hit=2440
         -&amp;gt; Function Scan on u (cost=0.01..0.11 rows=5 width=41) (actual time=0.013..0.015 rows=5.00 loops=1)
               Filter: decision
               Rows Removed by Filter: 15
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The execution plan shows that PostgreSQL sorts and caps the dataset to 50 rows, aggregates their prompt strings into an array within GroupAggregate, and invokes ai.if once. This consolidates 50 model evaluations into a single network payload — taking ~351 ms — before unnesting and filtering the results, drastically reducing HTTP roundtrips and connection overhead compared to row-by-row execution.&lt;/p&gt;

&lt;p&gt;The execution time was consistently around 180 ms. You can compare it with a default model and non-batched approach. My tests for the non-batched approach showed around 5600 ms using the same Jev model and 19500 ms with the default model. So the batching improves performance more than 30 times.&lt;/p&gt;

&lt;p&gt;The same approach worked for the ai.analyze_sentiment function where I was taking user’s reviews and passing multiple reviews as arguments to the function capturing the output.&lt;/p&gt;

&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;Comparing the traditional ETL-to-AI pipeline with AlloyDB’s native Model Endpoint Management shows massive improvements in architectural simplicity and performance. It worked really well with incredible speed.&lt;/p&gt;

&lt;p&gt;Here are some thoughts after testing the TypeSafe AI’s Jev directly in AlloyDB:&lt;/p&gt;

&lt;p&gt;Model Speed: Generative LLMs are relatively slow because they stream text token-by-token. Jev bypasses this entirely, returning structured logical primitives (like noul and score) in milliseconds, making it uniquely suited for synchronous database operations.&lt;/p&gt;

&lt;p&gt;Flexibility: You are not locked into one provider. You can securely authenticate with Secret Manager and use custom SQL transforms to adapt standard ai.* functions to any external REST API format.&lt;/p&gt;

&lt;p&gt;Database Performance: Using PostgreSQL window functions and array batching maps perfectly to Jev’s parallel API design, allowing you to process dozens of questions simultaneously inside a single HTTP round-trip. The database optimizer combines data before calling the AI model, minimizing overhead.&lt;/p&gt;

&lt;p&gt;You can test the presented scenario with an e-commerce dataset using the &lt;a href="https://codelabs.developers.google.com/alloydb-ai-jev?utm_campaign=CDR_0x370c34a8_default_b567422655&amp;amp;utm_medium=external&amp;amp;utm_source=blog#0" rel="noopener noreferrer"&gt;published codelab&lt;/a&gt; and maybe expand your testing comparing the performance and doing sentiment scoring.&lt;/p&gt;

&lt;p&gt;Happy testing!&lt;/p&gt;




</description>
      <category>typesafeai</category>
      <category>alloydbai</category>
      <category>performance</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Inline vs. Separate Tables for Vectors in Postgres: Measuring the Join Overhead</title>
      <dc:creator>Gleb Otochkin</dc:creator>
      <pubDate>Fri, 25 Sep 2026 04:29:18 +0000</pubDate>
      <link>https://dev.to/googleai/inline-vs-separate-tables-for-vectors-in-postgres-measuring-the-join-overhead-4f2b</link>
      <guid>https://dev.to/googleai/inline-vs-separate-tables-for-vectors-in-postgres-measuring-the-join-overhead-4f2b</guid>
      <description>&lt;p&gt;Testing semantic search, filtered queries, and multi-table joins in AlloyDB to measure the true cost of decoupling your vectors.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjfnktytr1a3cf2jql78s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjfnktytr1a3cf2jql78s.png" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Introduction
&lt;/h3&gt;

&lt;p&gt;If you are working with vector embeddings you probably already know the embedding models are evolving and you need to refresh your embeddings with a new version from time to time. In the &lt;a href="https://medium.com/google-cloud/ba6edb33e280" rel="noopener noreferrer"&gt;last post&lt;/a&gt; we discussed the bloating problem in the tables, TOAST segments and indexes related to the embeddings refresh. As an alternative to storing embeddings in the same table with your data I proposed a different layout where the embeddings would be placed in a dedicated table. In this post I show what performance impact it might have in comparison with storing embeddings in the same table.&lt;/p&gt;

&lt;h3&gt;
  
  
  How I tested it
&lt;/h3&gt;

&lt;p&gt;To test queries I needed sample data with real embeddings. The reasons were explained in one of my &lt;a href="https://dev.to/googleai/how-your-sample-data-impact-vector-tests-in-postgresql-and-alloydb-211l"&gt;previous posts&lt;/a&gt; — it could have a visible impact on results when an ANN index is used. I prepared a dataset with 30k rows of sample products, built embeddings on product descriptions, and used them to fill a dedicated embedding table. For all my tests I was using an &lt;a href="https://cloud.google.com/alloydb/omni?utm_campaign=CDR_0x370c34a8_default_b561997538&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;AlloyDB Omni&lt;/a&gt; database. It was coming with AI integration out of the box and that helped me to &lt;a href="https://docs.cloud.google.com/alloydb/omni/containers/current/docs/ai/work-with-embeddings?utm_campaign=CDR_0x370c34a8_default_b561997538&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;build the embeddings&lt;/a&gt;, vector indexes, and generate search embeddings by converting search phrases to the vector variables.&lt;/p&gt;

&lt;p&gt;Here are the tables used in my tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ecomm.products table&lt;/span&gt;

&lt;span class="n"&gt;demodb&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="n"&gt;ecomm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;
                              &lt;span class="k"&gt;Table&lt;/span&gt; &lt;span class="nv"&gt;"ecomm.products"&lt;/span&gt;
         &lt;span class="k"&gt;Column&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Type&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Collation&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Nullable&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Default&lt;/span&gt;
&lt;span class="c1"&gt;------------------------+------------------------+-----------+----------+---------&lt;/span&gt;
 &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;character&lt;/span&gt; &lt;span class="nb"&gt;varying&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;character&lt;/span&gt; &lt;span class="nb"&gt;varying&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="n"&gt;brand&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;character&lt;/span&gt; &lt;span class="nb"&gt;varying&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="n"&gt;retail_price&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="n"&gt;department&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;character&lt;/span&gt; &lt;span class="nb"&gt;varying&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="n"&gt;sku&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;character&lt;/span&gt; &lt;span class="nb"&gt;varying&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="n"&gt;distribution_center_id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="n"&gt;product_description&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="n"&gt;product_image_uri&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="n"&gt;Indexes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nv"&gt;"products_pkey"&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;btree&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;"fk_products_distribution_center_23"&lt;/span&gt; &lt;span class="n"&gt;btree&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;distribution_center_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;"idx_products_brand"&lt;/span&gt; &lt;span class="n"&gt;btree&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;"idx_products_category"&lt;/span&gt; &lt;span class="n"&gt;btree&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;"idx_products_retail_price"&lt;/span&gt; &lt;span class="n"&gt;btree&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retail_price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;"idx_products_sku"&lt;/span&gt; &lt;span class="n"&gt;btree&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;"idx_products_vector"&lt;/span&gt; &lt;span class="n"&gt;hnsw&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;vector_cosine_ops&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;Foreign&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;constraints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nv"&gt;"fk_products_distribution_center"&lt;/span&gt; &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;distribution_center_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;ecomm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;distribution_centers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;Referenced&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="nv"&gt;"ecomm.inventory_items"&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="nv"&gt;"fk_inventory_items_product"&lt;/span&gt; &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;ecomm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="nv"&gt;"ecomm.order_items"&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="nv"&gt;"fk_order_items_product"&lt;/span&gt; &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;ecomm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="nv"&gt;"ecomm.product_reviews"&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="nv"&gt;"fk_reviews_product"&lt;/span&gt; &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;ecomm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;

&lt;span class="c1"&gt;-- ecomm.product_embeddings table&lt;/span&gt;
&lt;span class="n"&gt;demodb&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="n"&gt;ecomm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_embeddings&lt;/span&gt;
             &lt;span class="k"&gt;Table&lt;/span&gt; &lt;span class="nv"&gt;"ecomm.product_embeddings"&lt;/span&gt;
   &lt;span class="k"&gt;Column&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Type&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Collation&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Nullable&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Default&lt;/span&gt;
&lt;span class="c1"&gt;------------+-------------+-----------+----------+---------&lt;/span&gt;
 &lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="n"&gt;Indexes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nv"&gt;"product_embeddings_pkey"&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;btree&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;"idx_product_embeddings_vector"&lt;/span&gt; &lt;span class="n"&gt;hnsw&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;vector_cosine_ops&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- ecomm.inventory_items table &lt;/span&gt;
&lt;span class="n"&gt;demodb&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="n"&gt;ecomm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inventory_items&lt;/span&gt;
                                 &lt;span class="k"&gt;Table&lt;/span&gt; &lt;span class="nv"&gt;"ecomm.inventory_items"&lt;/span&gt;
             &lt;span class="k"&gt;Column&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Type&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Collation&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Nullable&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Default&lt;/span&gt;
&lt;span class="c1"&gt;--------------------------------+-----------------------------+-----------+----------+---------&lt;/span&gt;
 &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;without&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="k"&gt;zone&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="n"&gt;sold_at&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;without&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="k"&gt;zone&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="n"&gt;product_category&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;character&lt;/span&gt; &lt;span class="nb"&gt;varying&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="n"&gt;product_name&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;character&lt;/span&gt; &lt;span class="nb"&gt;varying&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="n"&gt;product_brand&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;character&lt;/span&gt; &lt;span class="nb"&gt;varying&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="n"&gt;product_retail_price&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="n"&gt;product_department&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;character&lt;/span&gt; &lt;span class="nb"&gt;varying&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="n"&gt;product_sku&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;character&lt;/span&gt; &lt;span class="nb"&gt;varying&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="n"&gt;product_distribution_center_id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="n"&gt;Indexes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nv"&gt;"inventory_items_pkey"&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;btree&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;"fk_inventory_items_distribution_center_8"&lt;/span&gt; &lt;span class="n"&gt;btree&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_distribution_center_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;"fk_inventory_items_product_7"&lt;/span&gt; &lt;span class="n"&gt;btree&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;Foreign&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;constraints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nv"&gt;"fk_inventory_items_distribution_center"&lt;/span&gt; &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_distribution_center_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;ecomm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;distribution_centers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;"fk_inventory_items_product"&lt;/span&gt; &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;ecomm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;Referenced&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="nv"&gt;"ecomm.order_items"&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="nv"&gt;"fk_order_items_inventory_item"&lt;/span&gt; &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inventory_item_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;ecomm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inventory_items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- ecomm.product_reviews table&lt;/span&gt;
&lt;span class="n"&gt;demodb&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="n"&gt;ecomm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_reviews&lt;/span&gt;
                                &lt;span class="k"&gt;Table&lt;/span&gt; &lt;span class="nv"&gt;"ecomm.product_reviews"&lt;/span&gt;
   &lt;span class="k"&gt;Column&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Type&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Collation&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Nullable&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Default&lt;/span&gt;
&lt;span class="c1"&gt;-------------+--------------------------+-----------+----------+------------------------------&lt;/span&gt;
 &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;generated&lt;/span&gt; &lt;span class="n"&gt;always&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;identity&lt;/span&gt;
 &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="n"&gt;rating&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="n"&gt;review_text&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="k"&gt;zone&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;
&lt;span class="n"&gt;Indexes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nv"&gt;"product_reviews_pkey"&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;btree&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;"idx_product_reviews_created_at"&lt;/span&gt; &lt;span class="n"&gt;btree&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;"idx_product_reviews_product_id"&lt;/span&gt; &lt;span class="n"&gt;btree&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;"idx_product_reviews_user_id"&lt;/span&gt; &lt;span class="n"&gt;btree&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;Check&lt;/span&gt; &lt;span class="k"&gt;constraints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nv"&gt;"product_reviews_rating_check"&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rating&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;rating&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;Foreign&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;constraints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nv"&gt;"fk_reviews_product"&lt;/span&gt; &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;ecomm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;
    &lt;span class="nv"&gt;"fk_reviews_user"&lt;/span&gt; &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;ecomm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ecomm.products and ecomm.product_embeddings are connected by the primary key and both tables have HNSW index built on top of the embedding vectors.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test products semantic search
&lt;/h3&gt;

&lt;p&gt;I started the tests from a simple case with a cosine semantic search using only the products table and then comparing it with the join of the products and product_embeddings table. For the search I converted a phrase ‘lightweight waterproof high quality jacket’ to a vector embedding and passed it as test_vec variable. That removed the unpredictable model response time and allowed me to compare the query execution times more precisely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Set the test_vec variable in psql&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;google_ml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model_id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'text-embedding-005'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'lightweight waterproof high quality jacket'&lt;/span&gt;
&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;))::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;test_vec&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;gset&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the same psql session I ran the search with EXPLAIN ANALYZE to see the execution plan and timings. Every test was repeated multiple times.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Embeddings in ecomm.products&lt;/span&gt;
&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retail_price&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ecomm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;'test_vec'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is the execution plan for “inline” embeddings vector search:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; Limit (cost=1213.90..1240.56 rows=10 width=86) (actual time=1.112..1.200 rows=10.00 loops=1)
   Buffers: shared hit=792
   -&amp;gt; Index Scan using idx_products_vector on products p (cost=1213.90..78826.40 rows=29120 width=86) (actual time=1.110..1.197 rows=10.00 loops=1)
         Order By: (embedding &amp;lt;=&amp;gt; '[-0.01255631,...,-0.008388235]'::vector(768))
         Index Searches: 1
         Buffers: shared hit=792
 Planning:
   Buffers: shared hit=1
 Planning Time: 0.107 ms
 Execution Time: 1.224 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the plan it was visible the query was using the HNSW index to search through the embeddings and order them by similarity. The response time in total to return the results was around 2.1 ms. The response time was bigger than the pure execution time from the execution plan because it added overhead like planning, network time and client software time to show the results.&lt;/p&gt;

&lt;p&gt;Then I put embeddings to the ecomm.product_embeddings table and did the same search:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Embeddings in ecomm.product_embeddings&lt;/span&gt;
&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retail_price&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ecomm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_embeddings&lt;/span&gt; &lt;span class="n"&gt;pe&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;ecomm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;'test_vec'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is the execution plan with join of products and product_embeddings tables using primary keys:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; Limit (cost=219.82..250.39 rows=10 width=86) (actual time=1.188..1.351 rows=10.00 loops=1)
   Buffers: shared hit=852
   -&amp;gt; Nested Loop (cost=219.82..89223.27 rows=29120 width=86) (actual time=1.187..1.348 rows=10.00 loops=1)
         Buffers: shared hit=852
         -&amp;gt; Index Scan using idx_product_embeddings_vector on product_embeddings pe (cost=219.53..59613.60 rows=29120 width=26) (actual time=1.126..1.141 rows=10.00 loops=1)
               Order By: (embedding &amp;lt;=&amp;gt; '[-0.01255631,...,-0.008388235]'::vector(768))
               Index Searches: 1
               Buffers: shared hit=752
         -&amp;gt; Index Scan using products_pkey on products p (cost=0.29..1.01 rows=1 width=78) (actual time=0.007..0.007 rows=1.00 loops=10)
               Index Cond: (id = pe.product_id)
               Index Searches: 10
               Buffers: shared hit=50
 Planning:
   Buffers: shared hit=21
 Planning Time: 0.274 ms
 Execution Time: 1.381 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I was getting top 10 vectors using the HNSW index on ecomm.product_embeddings and joining that with the ecomm.products table using the primary key. The execution time was consistently around 2.2 ms. Yes, the execution plan is a bit more complicated but the execution itself is taking only 0.1 ms longer. When I increased limit from the top 10 to top 1000 it increased the response time only by around 0.3 ms. PostgreSQL joins with primary keys are fast.&lt;/p&gt;

&lt;h3&gt;
  
  
  Filtered semantic search
&lt;/h3&gt;

&lt;p&gt;In real life we rarely see queries using only one search condition. In most cases it involves additional parameters. I tested it adding filter on category and retail_price columns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- With two filters and embeddings in ecomm.products&lt;/span&gt;
&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retail_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ecomm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Outerwear &amp;amp; Coats'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retail_price&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;'test_vec'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The execution plan had changed adding the filter on results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; Limit (cost=1213.90..1760.50 rows=10 width=97) (actual time=6.353..6.522 rows=10.00 loops=1)
   Buffers: shared hit=817
   -&amp;gt; Index Scan using idx_products_vector on products p (cost=1213.90..78829.95 rows=1420 width=97) (actual time=6.342..6.509 rows=10.00 loops=1)
         Order By: (embedding &amp;lt;=&amp;gt; '[-0.01255631,...,-0.008388235]'::vector(768))
         Filter: ((retail_price &amp;gt;= '30'::numeric) AND (retail_price &amp;lt;= '150'::numeric) AND ((category)::text = 'Outerwear &amp;amp; Coats'::text))
         Rows Removed by Filter: 7
         Index Searches: 1
         Buffers: shared hit=799
 Planning:
   Buffers: shared hit=1
 Planning Time: 0.157 ms
 Execution Time: 1.308 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response time was consistently around 2.2 ms — almost the same as without the filter.&lt;/p&gt;

&lt;p&gt;Then I executed it with embeddings in a separate table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retail_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ecomm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_embeddings&lt;/span&gt; &lt;span class="n"&gt;pe&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;ecomm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Outerwear &amp;amp; Coats'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retail_price&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;'test_vec'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I got one extra step with the filter in the execution plan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; Limit (cost=219.82..1346.89 rows=10 width=97) (actual time=1.167..1.370 rows=10.00 loops=1)
   Buffers: shared hit=894
   -&amp;gt; Nested Loop (cost=219.82..89370.85 rows=791 width=97) (actual time=1.166..1.367 rows=10.00 loops=1)
         Buffers: shared hit=894
         -&amp;gt; Index Scan using idx_product_embeddings_vector on product_embeddings pe (cost=219.53..59613.60 rows=29120 width=26) (actual time=1.110..1.153 rows=17.00 loops=1)
               Order By: (embedding &amp;lt;=&amp;gt; '[-0.01255631,...,-0.008388235]'::vector(768))
               Index Searches: 1
               Buffers: shared hit=759
         -&amp;gt; Index Scan using products_pkey on products p (cost=0.29..1.02 rows=1 width=89) (actual time=0.006..0.006 rows=0.59 loops=17)
               Index Cond: (id = pe.product_id)
               Filter: ((retail_price &amp;gt;= '30'::numeric) AND (retail_price &amp;lt;= '150'::numeric) AND ((category)::text = 'Outerwear &amp;amp; Coats'::text))
               Rows Removed by Filter: 0
               Index Searches: 17
               Buffers: shared hit=85
 Planning:
   Buffers: shared hit=21
 Planning Time: 0.347 ms
 Execution Time: 1.406 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response time for the query was all the time between 2.3 and 2.4 ms. Again it was slower than the query with embeddings in the same table but the difference was not significant.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-tables join
&lt;/h3&gt;

&lt;p&gt;If you have a data schema where different parts and attributes of your business information are stored in different tables you use joins of multiple tables to get what you need. For tests I used a join with ecomm.product_reviews with aggregations for product ratings.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retail_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;review_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rating&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_rating&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; 
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retail_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;'test_vec'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;dist&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ecomm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Outerwear &amp;amp; Coats'&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retail_price&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;
    &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;'test_vec'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;ecomm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_reviews&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retail_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dist&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dist&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The query was consistently returning results in 2.6–2.7 ms. Then I modified the query to use embeddings stored in the ecomm.product_embeddings table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retail_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;review_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rating&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_rating&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; 
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retail_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;'test_vec'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;dist&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ecomm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_embeddings&lt;/span&gt; &lt;span class="n"&gt;pe&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;ecomm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Outerwear &amp;amp; Coats'&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retail_price&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;
    &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;'test_vec'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;ecomm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_reviews&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retail_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dist&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dist&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference was again about 0.2 ms. The second query was returning results in 2.8–2.9 ms. I am skipping the individual execution plans for this case — they are longer and more difficult to read in a short blog post like this one, but here is a comparison diagram:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuf6cxov2xbjl7kn3vcic.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuf6cxov2xbjl7kn3vcic.png" width="800" height="944"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the second query I had an extra join for our product_embeddings and products tables (which was expected), but apart from that it showed the similar execution path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;Here is summary table with the test results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+--------------------------+-----------------------+-----------------------+---------------------+
| Test Scenario | Inline Embeddings | Dedicated Table | Overhead / Delta |
+--------------------------+-----------------------+-----------------------+---------------------+
| 1. Pure Semantic Search | Execution: ~1.22 ms | Execution: ~1.38 ms | +0.16 ms (Exec) |
| (LIMIT 10) | Total: ~2.10 ms | Total: ~2.20 ms | +0.10 ms (Total) |
+--------------------------+-----------------------+-----------------------+---------------------+
| 2. Filtered Search | Execution: ~1.31 ms | Execution: ~1.41 ms | +0.10 ms (Exec) |
| (Category + Price) | Total: ~2.20 ms | Total: 2.3 - 2.4 ms | +0.10 - 0.20 ms |
+--------------------------+-----------------------+-----------------------+---------------------+
| 3. Multi-Table Join | | | |
| (Reviews Aggregation) | Total: 2.6 - 2.7 ms | Total: 2.8 - 2.9 ms | +0.20 ms (Total) |
+--------------------------+-----------------------+-----------------------+---------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Comparing queries performance for embeddings stored in the table along with the source data and in the decoupled dedicated table showed relatively minor difference in performance. In most of the cases response time didn’t exceed 0.2 ms. Of course it might be different depending on table structures, data and individual queries.&lt;/p&gt;

&lt;p&gt;I would recommend testing and considering the layout with dedicated embedding tables. It might help you in the future models maintenance and model evaluations when you just add another table with your future model and run your queries using the newly generated embeddings. With a good schema design the overhead potentially can be minimal. In my experience other factors like the model response time introduces much more variations to the response time than a join with an embedding table based on primary keys.&lt;/p&gt;




</description>
      <category>performance</category>
      <category>alloydb</category>
      <category>vectordatabase</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Embedding versions management, TOAST and bloating in PostgreSQL</title>
      <dc:creator>Gleb Otochkin</dc:creator>
      <pubDate>Sat, 12 Sep 2026 00:05:49 +0000</pubDate>
      <link>https://dev.to/googleai/embedding-versions-management-toast-and-bloating-in-postgresql-2g2k</link>
      <guid>https://dev.to/googleai/embedding-versions-management-toast-and-bloating-in-postgresql-2g2k</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4xhl0bc7cqusaitfbsq1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4xhl0bc7cqusaitfbsq1.png" width="800" height="359"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you are working with vector embeddings you probably already know the AI models including embedding models have their own lifecycle. When a new model is released by a service provider the old version will be eventually deprecated and removed from the service. You might have better control with models deployed on your own infrastructure like &lt;a href="https://codelabs.developers.google.com/alloydb-omni-gke-embeddings?utm_campaign=CDR_0x370c34a8_default_b559734139&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;EmbeddingGemma in this codelab&lt;/a&gt; but eventually you might want the new model just because it works better.&lt;/p&gt;

&lt;p&gt;What is the impact of refreshing all the vector embedding in your AlloyDB or PostgreSQL database? Let’s dive in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prepare the tests data
&lt;/h3&gt;

&lt;p&gt;Let us prepare a test environment. In my tests I am going to use AlloyDB Omni since it is fully PostgreSQL compatible and in this case behaves exactly like any PostgreSQL database.&lt;br&gt;&lt;br&gt;
We create a test table with the vector data type, add some indexes, and generate 20k rows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Install extensions&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;pgstattuple&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Create a function building random vectors&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;random_vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dim&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;
&lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="k"&gt;sql&lt;/span&gt; &lt;span class="k"&gt;VOLATILE&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;array_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()::&lt;/span&gt;&lt;span class="nb"&gt;real&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Create a demo table&lt;/span&gt;
&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;demo_documents&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;demo_documents&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;category_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="c1"&gt;-- Large text column (&amp;gt; 2.5 KB) to demonstrate TOAST interaction with uncompressed/compressed text&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;-- 768-dim float vector (~3.08 KB) -&amp;gt; pushed to TOAST&lt;/span&gt;
    &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Pause autovacuum temporarily&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;demo_documents&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;autovacuum_enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Add a few B-tree indexes&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_docs_category_created&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;demo_documents&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;category_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_docs_status&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;demo_documents&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_docs_title&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;demo_documents&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Create HNSQ index on the vectores&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_docs_embedding_hnsw&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;demo_documents&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;hnsw&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;vector_cosine_ops&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ef_construction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Generate some data&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;demo_documents&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;category_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'draft'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'published'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'archived'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'review'&lt;/span&gt;&lt;span class="p"&gt;])[(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;'Document title #'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;' minutes'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;-- ~3,000 bytes of repetitive text (&amp;gt; 2 KB TOAST threshold)&lt;/span&gt;
    &lt;span class="n"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'PostgreSQL TOAST and vector storage internals demo '&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c1"&gt;-- 768-dim vector&lt;/span&gt;
    &lt;span class="n"&gt;random_vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Check the size of all segments
&lt;/h3&gt;

&lt;p&gt;Let’s establish a clear baseline for comparison. We will run an initial vacuum analyze and check all our relations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Run an initial vacuum to start with clean baseline metrics&lt;/span&gt;
&lt;span class="k"&gt;VACUUM&lt;/span&gt; &lt;span class="k"&gt;ANALYZE&lt;/span&gt; &lt;span class="n"&gt;demo_documents&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Check all the segments&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relname&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;object_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relkind&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'r'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'table'&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'t'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'toast table'&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'i'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'index'&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;pg_size_pretty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pg_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;size&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_class&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'demo_documents'&lt;/span&gt;
   &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;indexrelid&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_index&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;indrelid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'demo_documents'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;reltoastrelid&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_class&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;relname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'demo_documents'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;pg_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is what we get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        object_name | kind | size
---------------------------+-------------+---------
 idx_docs_embedding_hnsw | index | 78 MB
 pg_toast_3177242 | toast table | 81 MB
 demo_documents | table | 4336 kB
 idx_docs_category_created | index | 1024 kB
 idx_docs_title | index | 1024 kB
 demo_documents_pkey | index | 512 kB
 idx_docs_status | index | 256 kB
(7 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our demo_documents table with indexes and TOAST occupies about 170 MB. It might be helpful also to check tuples (physical rows):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Dead/Live tuple stats&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="s1"&gt;'Main Table'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;tuple_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;tuple_percent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;dead_tuple_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;free_percent&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pgstattuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'demo_documents'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- TOAST Table Dead/Live tuple stats&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="s1"&gt;'TOAST Table'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;tuple_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;tuple_percent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;dead_tuple_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;free_percent&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pgstattuple&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;reltoastrelid&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_class&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;relname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'demo_documents'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We see the main table and the TOAST have zero dead tuples and reasonable small free space percentage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; component | table_len | tuple_count | tuple_percent | dead_tuple_count | free_percent
------------+-----------+-------------+---------------+------------------+--------------
 Main Table | 4440064 | 20000 | 90.92 | 0 | 1.57

  component | table_len | tuple_count | tuple_percent | dead_tuple_count | free_percent
-------------+-----------+-------------+---------------+------------------+--------------
 TOAST Table | 84492288 | 40000 | 74.52 | 0 | 21.82
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is a basic graph to illustrate the layout:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1apm1e3eydisxdewanhg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1apm1e3eydisxdewanhg.png" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Updating the vectors
&lt;/h3&gt;

&lt;p&gt;We emulate the process of refreshing embeddings by updating all the vectors in the demo_documents table. For example we had all our embeddings created using Google text-embedding-004 model and we updated all of them using gemini-embedding-2 model. Google publishing life cycle for all its models in the &lt;a href="https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/model-versions?utm_campaign=CDR_0x370c34a8_default_b559734139&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; where you can see it and plan your maintenance in advance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Update all vector embeddings&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;demo_documents&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random_vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After updating all the rows we can check the sizes of all objects again and see how they’ve changed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        object_name | kind | size before update | size after update
---------------------------+-------------+--------------------+-------------------
 idx_docs_embedding_hnsw | index | 78 MB | 156 MB
 pg_toast_3177242 | toast table | 81 MB | 159 MB
 demo_documents | table | 4336 kB | 7376 kB
 idx_docs_category_created | index | 1024 kB | 2048 kB
 idx_docs_title | index | 1024 kB | 2048 kB
 demo_documents_pkey | index | 512 kB | 1024 kB
 idx_docs_status | index | 256 kB | 512 kB
(7 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Almost all objects including table, TOAST and indexes doubled in size. The impact is most visible for the HNSW vector index and the demo_documents table itself. Here is information about dead tuples and space allocation for the main table and TOAST:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; component | table_len | tuple_count | tuple_percent | dead_tuple_count | free_percent
------------+-----------+-------------+---------------+------------------+--------------
 Main Table | 7585792 | 20000 | 47.84 | 20000 | 1.2

  component | table_len | tuple_count | tuple_percent | dead_tuple_count | free_percent
-------------+-----------+-------------+---------------+------------------+--------------
 TOAST Table | 166387712 | 40000 | 37.84 | 40000 | 22.21
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is a diagram:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyyw2urvmdvjicwwhgosy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyyw2urvmdvjicwwhgosy.png" width="800" height="523"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So, roughly half of the space is occupied by dead tuples. When I discuss it with some developers they mention the vacuuming process and at least some of them had expectations that it should fix the bloating. Let’s do some vacuuming and see if it changes it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;VACUUM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;VERBOSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;demo_documents&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The verbose output provides enough information about what was done during the vacuum process and everybody who is curious can check it in detail. We can have a look into the main stats about tuples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; component | table_len | tuple_count | tuple_percent | dead_tuple_count | free_percent
------------+-----------+-------------+---------------+------------------+--------------
 Main Table | 7585792 | 20000 | 47.84 | 0 | 50.16

  component | table_len | tuple_count | tuple_percent | dead_tuple_count | free_percent
-------------+-----------+-------------+---------------+------------------+--------------
 TOAST Table | 166387712 | 40000 | 37.84 | 0 | 60.17

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The vacuum process cleared up the dead tuples leaving the space allocation intact and it was exactly what we expected. We have free space which can be used by the new rows but the allocation on the disk didn’t change.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        object_name | kind | size before vacuum | size after vacuum
---------------------------+-------------+--------------------+-------------------
 pg_toast_3177242 | toast table | 159 MB | 159 MB
 idx_docs_embedding_hnsw | index | 156 MB | 156 MB
 demo_documents | table | 7408 kB | 7408 kB
 idx_docs_category_created | index | 2048 kB | 2048 kB
 idx_docs_title | index | 2048 kB | 2048 kB
 demo_documents_pkey | index | 1024 kB | 1024 kB
 idx_docs_status | index | 512 kB | 512 kB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Alternative layout
&lt;/h3&gt;

&lt;p&gt;What if the bloating is a significant issue for your workload? In such a case you might consider a different layout and place the vectors in a separate table connecting to the main table using primary keys. Then you can use either a view or simple table join to work with the data. And when the vectors are required to be updated — create a new table with a new vector version.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg1jxo9dbfv979hla3for.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg1jxo9dbfv979hla3for.png" width="800" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Such an approach can save you some vacuum overhead and bloating. But of course the join of two (or more) tables might perform slightly differently than the single flat table approach. You need to test it with your data and with your vectors.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;The final layout of your table might depend on multiple factors. In real life I very rarely saw a case when the search was performed only in one single table. In most cases it was always a combination of 2,3,4, or more tables with the resulting dataset. Considering other parts like filters on non-vector columns or full-text search combined with the semantic search, the real case might be much more complicated than a demo with one flat table where all the data is stored in one segment. And for a high-loaded scalable environment, the right relational model can be a key for success and requires vigorous testing.&lt;/p&gt;

&lt;p&gt;In the next post I will try to evaluate the performance overhead of keeping the vectors in a separate table vs having them in the same table. Stay tuned!&lt;/p&gt;




</description>
      <category>vectorembeddings</category>
      <category>performance</category>
      <category>databasedesign</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Evaluating AI models with SQL in Google Cloud databases</title>
      <dc:creator>Gleb Otochkin</dc:creator>
      <pubDate>Wed, 02 Sep 2026 02:56:01 +0000</pubDate>
      <link>https://dev.to/googleai/evaluating-ai-models-with-sql-in-google-cloud-databases-hhf</link>
      <guid>https://dev.to/googleai/evaluating-ai-models-with-sql-in-google-cloud-databases-hhf</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvga5dpwqwuuy8959wrl9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvga5dpwqwuuy8959wrl9.png" width="800" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Introduction
&lt;/h3&gt;

&lt;p&gt;Do you need to evaluate AI models with your data or cases? Yes, absolutely. Should it be done using complex and elaborate pipelines — it depends. Every time a new version of a model comes out, it behaves differently. You have to be able to evaluate each case with the model and see the differences. Sometimes you might realize that a lightweight, maybe cheaper model can work for your case as good as the more expensive one. In Google Cloud we have Gemini Enterprise Agent Platform which provides &lt;a href="https://docs.cloud.google.com/gemini-enterprise-agent-platform/optimize/evaluation/agent-evaluation?utm_campaign=CDR_0x370c34a8_default_b543490897&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;agent evaluation tools&lt;/a&gt; and you might have your own workflow going through sophisticated and elaborate tests. But what if you want to quickly check the new model or compare two different models available in your Cloud SQL for PostgreSQL or AlloyDB? You can use plain SQL and create a simple, quick evaluation procedure directly in your database. Let me show you an example of a single-answer point scoring evaluation.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI Model Evaluation Components
&lt;/h3&gt;

&lt;p&gt;What do you need for this evaluation? You need sample requests, registered model endpoint(s) you want to evaluate, a scale to quantify the performance, and evaluation criteria.&lt;/p&gt;

&lt;p&gt;Here is a set of my sample requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;'Explain Model Context Protocol (MCP) in 50 words or less.',
'Explain write-ahead logging (WAL) in PostgreSQL in 50 words or less.',
'Explain database connection pooling in 50 words or less.',
'Explain the difference between primary keys and unique constraints in 50 words or less.',
'Explain database normalization and its 3NF form in 50 words or less.',
'Explain transaction isolation levels like Serializable in 50 words or less.',
'Explain vector embeddings and cosine similarity in 50 words or less.',
'Explain database indexes (B-Tree vs Hash) in 50 words or less.',
'Explain the purpose of Foreign Keys in relational schemas in 50 words or less.',
'Explain optimistic vs pessimistic locking in databases in 50 words or less.'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of course, all my questions were about databases. And since we were working in database — I put all my questions into the table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;benchmark_prompts&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;GENERATED&lt;/span&gt; &lt;span class="n"&gt;ALWAYS&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;IDENTITY&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_words&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;is_active&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;clock_timestamp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- Index for fast active prompt lookups&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;idx_benchmark_prompts_active&lt;/span&gt; 
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;benchmark_prompts&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is_active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;benchmark_prompts&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_words&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Protocols'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Explain Model Context Protocol (MCP) in 50 words or less.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Architecture'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Explain write-ahead logging (WAL) in PostgreSQL in 50 words or less.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Networking'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Explain database connection pooling in 50 words or less.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Data Modeling'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Explain the difference between primary keys and unique constraints in 50 words or less.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Data Modeling'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Explain database normalization and its 3NF form in 50 words or less.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Concurrency'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Explain transaction isolation levels like Serializable in 50 words or less.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Vector Search'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Explain vector embeddings and cosine similarity in 50 words or less.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Storage'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Explain database indexes (B-Tree vs Hash) in 50 words or less.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Data Modeling'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Explain the purpose of Foreign Keys in relational schemas in 50 words or less.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Concurrency'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Explain optimistic vs pessimistic locking in databases in 50 words or less.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, I registered three different models in my AlloyDB database. The models were Gemini 3.5 Flash and Gemini 3.5 Flash Lite. All three models were available in AlloyDB, I just needed to &lt;a href="https://docs.cloud.google.com/alloydb/docs/ai/register-model-endpoint?utm_campaign=CDR_0x370c34a8_default_b543490897&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;register them&lt;/a&gt;. Here is an example of how you can register the Gemini 3.5 Flash Lite model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;google_ml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="n"&gt;model_id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'gemini-3.5-flash-lite'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;model_request_url&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'https://aiplatform.googleapis.com/v1/projects/PROJECT_ID/locations/global/publishers/google/models/gemini-3.5-flash-lite:generateContent'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;model_provider&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'google'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;model_type&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'llm'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don’t forget to replace the PROJECT_ID placeholder with your real project id if you use the procedure to register a model in your database.&lt;/p&gt;

&lt;p&gt;The list of evaluation criteria depends on the nature of your data and how you use the AI models. In my case, I wanted to put some basic quality criteria for the model’s response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Accuracy: Is the response technically accurate regarding the subject described in the prompt?
2. Word compliance: Is the response 50 words or less?
3. Quality: How well does it answer the question?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I wanted all my evaluations to be scaled from 1 to 10, where 10 would be the best response.&lt;/p&gt;

&lt;p&gt;How did I evaluate the responses? I registered one more model in my database just for the evaluation. I used the Gemini 3.1 Pro Preview as the Pro model should provide higher-quality evaluations. But nothing stops you from using any other models. For example, you can test it with our partner models from Anthropic or Meta which are available in our &lt;a href="https://cloud.google.com/model-garden?utm_campaign=CDR_0x370c34a8_default_b543490897&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;Model Garden&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To save some tokens and time I put a limit of 50 words for each response to a question.&lt;/p&gt;

&lt;p&gt;And last but not least, I count the total tokens for each request and average them at the end because cost is important and can help to choose the right model for each case.&lt;/p&gt;

&lt;h3&gt;
  
  
  SQL Function for Evaluation
&lt;/h3&gt;

&lt;p&gt;Here is an example of a SQL function with all the evaluation conditions directly in the SQL code. The function takes three parameters: model to evaluate, model which will be doing evaluation and number of executions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;benchmark_model_quality&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;p_target_model&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;p_evaluator_model&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;p_num_runs&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;total_runs&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;avg_quality_score&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;median_quality_score&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;min_quality_score&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_quality_score&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;avg_tokens_per_prompt&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt;
    &lt;span class="n"&gt;v_all_prompts&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="n"&gt;v_selected_prompts&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;v_current_prompt&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;v_predict_result&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;v_target_response&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;v_tokens&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;v_total_tokens&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;v_eval_prompt&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;v_eval_raw&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;v_score&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;v_scores&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;v_pool_size&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="c1"&gt;-- Input validation&lt;/span&gt;
    &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;p_num_runs&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
        &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="s1"&gt;'p_num_runs must be greater than 0'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Load prompts directly from the table&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;array_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;v_all_prompts&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;benchmark_prompts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;v_pool_size&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;cardinality&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_all_prompts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;v_pool_size&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;v_pool_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
        &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="s1"&gt;'No prompts found in benchmark_prompts table'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- 1. Select exactly p_num_runs prompts WITH replacement from table pool&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;v_all_prompts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;v_pool_size&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p_num_runs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;v_selected_prompts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- 2. Loop through each of the selected subjects and execute exactly ONCE&lt;/span&gt;
    &lt;span class="n"&gt;FOREACH&lt;/span&gt; &lt;span class="n"&gt;v_current_prompt&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="n"&gt;ARRAY&lt;/span&gt; &lt;span class="n"&gt;v_selected_prompts&lt;/span&gt; &lt;span class="n"&gt;LOOP&lt;/span&gt;

        &lt;span class="c1"&gt;-- A. Query target model&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;google_ml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;predict_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;model_id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p_target_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;request_body&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;json_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'contents'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json_build_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;json_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="s1"&gt;'role'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'user'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'parts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json_build_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                            &lt;span class="n"&gt;json_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'text'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v_current_prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;v_predict_result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- B. Extract response text and tokens&lt;/span&gt;
        &lt;span class="n"&gt;v_target_response&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v_predict_result&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'candidates'&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'content'&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'parts'&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'text'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;v_tokens&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;v_predict_result&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'usageMetadata'&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'totalTokenCount'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;v_total_tokens&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v_total_tokens&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;v_tokens&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- C. Build adaptive judge prompt&lt;/span&gt;
        &lt;span class="n"&gt;v_eval_prompt&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'You are an expert technical evaluator. Evaluate the following generated response to the prompt: "%s"

Generated Response to Evaluate:
"%s"

Instructions:
Evaluate the response based on:
1. Accuracy: Is the response technically accurate regarding the subject described in the prompt?
2. Word compliance: Is the response 50 words or less?
3. Quality: How well does it answer the question?

You must respond ONLY with a raw JSON object conforming to the following schema, and no other text or Markdown formatting:
{
  "score": &amp;lt;integer from 1 to 10&amp;gt;,
  "reasoning": "&amp;lt;short description of why the score was given under 50 words&amp;gt;"
}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="n"&gt;v_current_prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="n"&gt;v_target_response&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;-- D. Query evaluator model&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;google_ml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;predict_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;model_id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p_evaluator_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;request_body&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;json_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'contents'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json_build_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;json_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="s1"&gt;'role'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'user'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'parts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json_build_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                            &lt;span class="n"&gt;json_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'text'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v_eval_prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'candidates'&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'content'&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'parts'&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'text'&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;v_eval_raw&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- E. Clean up markdown code blocks&lt;/span&gt;
        &lt;span class="n"&gt;v_eval_raw&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;regexp_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_eval_raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'^```

json&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s1"&gt;*'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'i'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;v_eval_raw&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;regexp_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_eval_raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s1"&gt;*

```$'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'i'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;v_eval_raw&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_eval_raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;-- F. Parse and store score&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="n"&gt;v_score&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_eval_raw&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'score'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;v_scores&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;array_append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_scores&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v_score&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;OTHERS&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
            &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;WARNING&lt;/span&gt; &lt;span class="s1"&gt;'Failed to parse JSON score from evaluator output: %'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v_eval_raw&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;LOOP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- 3. Compute aggregate stats over successfully parsed scores&lt;/span&gt;
    &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;QUERY&lt;/span&gt;
    &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;sorted_scores&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
               &lt;span class="n"&gt;row_number&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
               &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;total_count&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;unnest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_scores&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;stats&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; 
            &lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;v_avg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;v_min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;v_max&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;unnest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_scores&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;median_stat&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;v_median&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sorted_scores&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;total_count&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;total_count&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; 
        &lt;span class="k"&gt;cardinality&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_scores&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v_avg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;median_stat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v_median&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v_min&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v_max&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_total_tokens&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;NUMERIC&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;NULLIF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;cardinality&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_scores&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))::&lt;/span&gt;&lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;median_stat&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are some potential improvements to the procedure and the method depending on use case. For example you can expand it with “Golden” responses for each question for evaluation. It is useful when you are working with your own domain knowledge and know exactly how the best answer should sound like.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing
&lt;/h3&gt;

&lt;p&gt;I tried it on two different models and compared the results.&lt;/p&gt;

&lt;p&gt;The first test was for the Gemini 3.5 Flash model. I set the test to run 45 times:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;benchmark_model_quality&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'gemini-3.5-flash'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'gemini-3.1-pro-preview'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Gemini 3.5 Flash showed really good results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt; total_runs | avg_quality_score | median_quality_score | min_quality_score | max_quality_score | avg_tokens_per_prompt
------------+-------------------+----------------------+-------------------+-------------------+-----------------------
         45 | 9.96 | 10.00 | 8 | 10 | 916.27
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I ran the same test for Gemini 3.5 Flash Lite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt; total_runs | avg_quality_score | median_quality_score | min_quality_score | max_quality_score | avg_tokens_per_prompt
------------+-------------------+----------------------+-------------------+-------------------+-----------------------
         45 | 9.82 | 10.00 | 7 | 10 | 77.11
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For that sample set, Gemini 3.5 Flash Lite showed almost the same quality and cost much less.&lt;/p&gt;

&lt;p&gt;If you compare the tokens per request, you can see an 11x reduction in token count. And those tokens are 3.5 times cheaper than Gemini 3.5 Flash. So, you can effectively pay 38 times less for the same quality response.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;There are two things I want to say in this article. If you are working in AlloyDB or Cloud SQL and need to quickly evaluate some AI models you can try it using SQL function thanks to the direct integration with AI in Google Cloud databases.&lt;/p&gt;

&lt;p&gt;And maybe you can use a less expensive model for your workload and get acceptable results saving some money.&lt;/p&gt;

&lt;p&gt;Happy testing and let me know if you think such kind of simple recipes are useful for you.&lt;/p&gt;




</description>
      <category>aievaluation</category>
      <category>googlecloudplatform</category>
      <category>alloydb</category>
      <category>googlegemini</category>
    </item>
    <item>
      <title>7 Times Faster and Cheaper? Gemini 3.5 Flash Lite and Gemini 3.6 Flash in Google Cloud Databases</title>
      <dc:creator>Gleb Otochkin</dc:creator>
      <pubDate>Sun, 26 Jul 2026 06:11:17 +0000</pubDate>
      <link>https://dev.to/googleai/7-times-faster-and-cheaper-gemini-35-flash-lite-and-gemini-36-flash-in-google-cloud-databases-57lf</link>
      <guid>https://dev.to/googleai/7-times-faster-and-cheaper-gemini-35-flash-lite-and-gemini-36-flash-in-google-cloud-databases-57lf</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3j2kjt10xyb8rrp6luh7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3j2kjt10xyb8rrp6luh7.png" width="799" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Only a couple of months ago, I wrote a &lt;a href="https://dev.to/googleai/gemini-35-flash-in-google-cloud-databases-14kg"&gt;blog&lt;/a&gt; post describing how to enable the latest (at the time) Gemini 3.5 Flash in Google Cloud databases. Now, it is time to introduce the newest model: Gemini 3.6 Flash. This model is fresher, with a knowledge cutoff date around the end of March this year, and it costs less. Let’s look at how it performs at first glance. I am particularly curious about its response times and how it compares to the previous model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloud SQL
&lt;/h3&gt;

&lt;p&gt;For Cloud SQL for Postgres, don’t forget to enable full &lt;a href="https://docs.cloud.google.com/sql/docs/postgres/ai-overview" rel="noopener noreferrer"&gt;AI integration&lt;/a&gt; via the google_ml extension to &lt;a href="https://docs.cloud.google.com/sql/docs/postgres/invoke-online-predictions" rel="noopener noreferrer"&gt;call models&lt;/a&gt;directly from a SQL query. If you are reading this AI integration overview right after the model announcement, you might need to register the model first. It wasn’t available by default when I tested it.&lt;/p&gt;

&lt;p&gt;Here is the SQL statement to register the model. Don’t forget to replace the PROJECT_ID placeholder in the code with your actual Google Cloud project ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;google_ml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model_id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'gemini-3.6-flash'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model_request_url&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'https://aiplatform.googleapis.com/v1/projects/PROJECT_ID/locations/global/publishers/google/models/gemini-3.6-flash:generateContent'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model_provider&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'google'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model_type&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'generic'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model_auth_type&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'cloudsql_service_agent_iam'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, your model is ready to be used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;google_ml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;predict_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model_id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'gemini-3.6-flash'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;request_body&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;json_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'contents'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json_build_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;json_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'role'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'user'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'parts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json_build_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;json_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'text'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Explain MCP server for a relational database in 50 words or less.'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'candidates'&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'content'&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'parts'&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'text'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ai_response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To measure the latency, I’ve asked Google Antigravity to prepare a PL/pgSQL function to run that query a certain number of times and aggregate the results. It did a decent job, I needed only a few polishing and adjustments to make it working. Here are my benchmarking results for Gemini 3.6 Flash and Gemini 3.5 Flash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;quickstart_db=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;SELECT &lt;span class="k"&gt;*&lt;/span&gt; FROM benchmark_model_predictions&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'gemini-3.6-flash'&lt;/span&gt;, 10&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt; total_runs | avg_latency_ms | median_latency_ms | min_latency_ms | max_latency_ms 
------------+----------------+-------------------+----------------+----------------
         10 | 3694.53 | 3725.52 | 3313.01 | 4213.40
(1 row)

Time: 36947.554 ms (00:36.948)
&lt;/span&gt;&lt;span class="gp"&gt;quickstart_db=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;SELECT &lt;span class="k"&gt;*&lt;/span&gt; FROM benchmark_model_predictions&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'gemini-3.5-flash'&lt;/span&gt;, 10&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt; total_runs | avg_latency_ms | median_latency_ms | min_latency_ms | max_latency_ms 
------------+----------------+-------------------+----------------+----------------
         10 | 4918.86 | 5136.67 | 3936.69 | 5722.01
(1 row)

Time: 49190.861 ms (00:49.191)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks like, all other conditions being equal, Gemini 3.6 Flash was slightly faster than the previous model. Of course, this is a very crude measurement, and your results may vary. Since we are using a global endpoint, the routing might theoretically hit different regional endpoints. Furthermore, public model endpoints are shared services.&lt;/p&gt;

&lt;p&gt;Next, I created a function to evaluate the quality of the responses and ran it for both models. The function uses the same query, receives the response, and sends the result to a more advanced model for evaluation. In this case, I used gemini-3.1-pro-preview as the judge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;quickstart_db=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;SELECT &lt;span class="k"&gt;*&lt;/span&gt; FROM benchmark_model_quality&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'gemini-3.6-flash'&lt;/span&gt;, &lt;span class="s1"&gt;'gemini-3.1-pro-preview'&lt;/span&gt;, 5&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt; total_runs | avg_quality_score | median_quality_score | min_quality_score | max_quality_score 
------------+-------------------+----------------------+-------------------+-------------------
          5 | 10.00 | 10.00 | 10 | 10
(1 row)

&lt;/span&gt;&lt;span class="gp"&gt;quickstart_db=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;SELECT &lt;span class="k"&gt;*&lt;/span&gt; FROM benchmark_model_quality&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'gemini-3.5-flash'&lt;/span&gt;, &lt;span class="s1"&gt;'gemini-3.1-pro-preview'&lt;/span&gt;, 5&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt; total_runs | avg_quality_score | median_quality_score | min_quality_score | max_quality_score 
------------+-------------------+----------------------+-------------------+-------------------
          5 | 9.80 | 10.00 | 9 | 10
(1 row)

&lt;/span&gt;&lt;span class="gp"&gt;quickstart_db=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both models delivered excellent results, but Gemini 3.6 Flash provided slightly better responses than its predecessor — at least according to the gemini-3.1-pro-preview judge.&lt;/p&gt;

&lt;p&gt;What about AlloyDB? Let’s run the same performance benchmark on AlloyDB to see if the response times match our Cloud SQL results.&lt;/p&gt;

&lt;h3&gt;
  
  
  AlloyDB
&lt;/h3&gt;

&lt;p&gt;Registering the new model in AlloyDB follows almost the same &lt;a href="https://docs.cloud.google.com/alloydb/docs/ai/model-endpoint-overview" rel="noopener noreferrer"&gt;process&lt;/a&gt;, with the only difference being the ‘model_type’ parameter. In AlloyDB, we register it with the model type ‘llm’. Again, don’t forget to replace the PROJECT_ID placeholder with your actual project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;google_ml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="n"&gt;model_id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'gemini-3.6-flash'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;model_request_url&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'https://aiplatform.googleapis.com/v1/projects/PROJECT_ID/locations/global/publishers/google/models/gemini-3.6-flash:generateContent'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;model_provider&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'google'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;model_type&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'llm'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For these tests, I used the exact same query and function as we did for Cloud SQL. AlloyDB is fully compatible with PostgreSQL and shares the same AI integration &lt;a href="https://docs.cloud.google.com/alloydb/docs/reference/model-endpoint" rel="noopener noreferrer"&gt;extension&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;quickstart_db=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;SELECT &lt;span class="k"&gt;*&lt;/span&gt; FROM benchmark_model_predictions&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'gemini-3.5-flash'&lt;/span&gt;, 10&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt; total_runs | avg_latency_ms | median_latency_ms | min_latency_ms | max_latency_ms
------------+----------------+-------------------+----------------+----------------
         10 | 4899.00 | 4910.05 | 3631.22 | 5798.40
(1 row)

Time: 48992.149 ms (00:48.992)
&lt;/span&gt;&lt;span class="gp"&gt;quickstart_db=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;SELECT &lt;span class="k"&gt;*&lt;/span&gt; FROM benchmark_model_predictions&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'gemini-3.6-flash'&lt;/span&gt;, 10&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt; total_runs | avg_latency_ms | median_latency_ms | min_latency_ms | max_latency_ms
------------+----------------+-------------------+----------------+----------------
         10 | 4364.76 | 4368.65 | 3664.24 | 5008.42
(1 row)

Time: 43649.384 ms (00:43.649)
&lt;/span&gt;&lt;span class="gp"&gt;quickstart_db=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time, the results were much closer, and the response times were almost identical. It is hard to draw definitive conclusions here, considering we are using a shared service with global routing; there is no guarantee that consecutive requests are hitting the same physical endpoint. However, it appears that performance and response times are roughly equivalent for both models. Naturally, the quality of the responses remained consistent with our previous test, as quality depends directly on the models themselves.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gemini 3.5 Flash Lite
&lt;/h3&gt;

&lt;p&gt;But do you always need a “Pro” or “Flash” model for your queries? In many cases, you can get highly acceptable results from a “Lite” model like the newly released gemini-3.5-flash-lite. I tested its performance using the same benchmarking approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;quickstart_db=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;SELECT &lt;span class="k"&gt;*&lt;/span&gt; FROM benchmark_model_predictions&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'gemini-3.5-flash-lite'&lt;/span&gt;, 10&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt; total_runs | avg_latency_ms | median_latency_ms | min_latency_ms | max_latency_ms
------------+----------------+-------------------+----------------+----------------
         10 | 827.08 | 882.05 | 499.95 | 1034.92
(1 row)

Time: 8272.929 ms (00:08.273)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It was about seven times faster than Gemini 3.6 Flash. But what about response quality?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;quickstart_db=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;SELECT &lt;span class="k"&gt;*&lt;/span&gt; FROM benchmark_model_quality&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'gemini-3.5-flash-lite'&lt;/span&gt;, &lt;span class="s1"&gt;'gemini-3.1-pro-preview'&lt;/span&gt;, 5&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt; total_runs | avg_quality_score | median_quality_score | min_quality_score | max_quality_score
------------+-------------------+----------------------+-------------------+-------------------
          5 | 10.00 | 10.00 | 10 | 10
(1 row)

Time: 42923.480 ms (00:42.923)
&lt;/span&gt;&lt;span class="gp"&gt;quickstart_db=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;SELECT &lt;span class="k"&gt;*&lt;/span&gt; FROM benchmark_model_quality&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'gemini-3.5-flash-lite'&lt;/span&gt;, &lt;span class="s1"&gt;'gemini-3.1-pro-preview'&lt;/span&gt;, 5&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt; total_runs | avg_quality_score | median_quality_score | min_quality_score | max_quality_score
------------+-------------------+----------------------+-------------------+-------------------
          5 | 9.60 | 10.00 | 8 | 10
(1 row)

Time: 51722.997 ms (00:51.723)
&lt;/span&gt;&lt;span class="gp"&gt;quickstart_db=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;SELECT &lt;span class="k"&gt;*&lt;/span&gt; FROM benchmark_model_quality&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'gemini-3.5-flash-lite'&lt;/span&gt;, &lt;span class="s1"&gt;'gemini-3.1-pro-preview'&lt;/span&gt;, 5&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt; total_runs | avg_quality_score | median_quality_score | min_quality_score | max_quality_score
------------+-------------------+----------------------+-------------------+-------------------
          5 | 9.80 | 10.00 | 9 | 10
(1 row)

Time: 53252.325 ms (00:53.252)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran the same benchmark three times, and even the worst-performing run was highly scored. The average scores ranged from 9.6 to 10 (with 10 being a perfect score). I was impressed by both the quality and the speed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;The new Gemini 3.6 Flash model is less &lt;a href="https://ai.google.dev/gemini-api/docs/pricing" rel="noopener noreferrer"&gt;expensive&lt;/a&gt; than Gemini 3.5 Flash. Considering it offers comparable or better quality and the same or faster response times, it is definitely a win.&lt;/p&gt;

&lt;p&gt;However, Gemini 3.5 Flash Lite is maybe even more impressive, delivering excellent quality while running seven times faster at a very attractive price. This means that many simpler, everyday tasks can be delegated to the Lite model to keep costs down, reserving more advanced models for complex problems that require deep reasoning and multi-step iterations.&lt;/p&gt;




</description>
      <category>googlecloudsql</category>
      <category>performance</category>
      <category>alloydb</category>
      <category>googlegeminiai</category>
    </item>
    <item>
      <title>How your sample data impact vector tests in PostgreSQL and AlloyDB.</title>
      <dc:creator>Gleb Otochkin</dc:creator>
      <pubDate>Tue, 14 Jul 2026 04:44:34 +0000</pubDate>
      <link>https://dev.to/googleai/how-your-sample-data-impact-vector-tests-in-postgresql-and-alloydb-211l</link>
      <guid>https://dev.to/googleai/how-your-sample-data-impact-vector-tests-in-postgresql-and-alloydb-211l</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fahc6pinsn35a42xzk9no.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fahc6pinsn35a42xzk9no.png" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Introduction
&lt;/h3&gt;

&lt;p&gt;If you are using vector data in PostgreSQL on &lt;a href="https://cloud.google.com/sql?utm_campaign=CDR_0x370c34a8_default_b524180055&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;Cloud SQL&lt;/a&gt; or &lt;a href="https://cloud.google.com/products/alloydb?utm_campaign=CDR_0x370c34a8_default_b524180055&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;AlloyDB&lt;/a&gt;, you probably use some benchmark queries or functions to check your vector search performance. From time to time, I see people performing speed benchmarks or quality testing using generated, fully synthetic data. The main goal, after all, is to test, for example, performance with a particular data type and get some comparable numbers. Does it really matter what data I have in my vector column?&lt;/p&gt;

&lt;p&gt;It really depends, and things can indeed go very wrong when you try to evaluate the index’s performance. I use synthetic data all the time for my tests. But it depends on what I am testing. In some cases, real vs. synthetic data might have a significant impact on the results. Let me show you how it can change the outcome of some tests for the vector data type in PostgreSQL.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two datasets
&lt;/h3&gt;

&lt;p&gt;Some tests can be done using fully synthetic data. For example, if you test inserts, updates, or deletes for vectors, then synthetic data works for you. In such cases, I usually create a fully synthetic dataset of totally random, normalized number arrays. Here is an example of a simple function and how to generate the dataset.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Create a function &lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;generate_random_vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dimensions&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;
&lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="k"&gt;sql&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;array_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Creating the test table with vector data type 768 dimensions&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;tvectr768&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- Populate the table with 1M or fows&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;tvectr768&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;-- This function is now called for each row&lt;/span&gt;
    &lt;span class="n"&gt;generate_random_vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
    &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the end, we have a table with 1 million vectors. Each vector dimension value is between -1 and 1. So, the vector is normalized and shows more or less the same behaviour for the DML operations as normally generated embedding for a piece of content.&lt;/p&gt;

&lt;p&gt;But when I test recall quality or performance for vector search, I use one of the publicly available datasets with vectors created based on real data. You can find multiple datasets for embeddings on Hugging Face or other sources. For example, you can check the Cohere Labs datasets here: &lt;a href="https://huggingface.co/CohereLabs/datasets." rel="noopener noreferrer"&gt;https://huggingface.co/CohereLabs/datasets.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here are my 1M rows with the same dimensions and structure, but all the vectors in those rows are based on real data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;pgdata=#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\d&lt;/span&gt; tvreal768
&lt;span class="go"&gt;               Table "public.tvreal768"
 Column | Type | Collation | Nullable | Default
--------+-------------+-----------+----------+---------
 id | bigint | | |
 d | vector(768) | | |

&lt;/span&gt;&lt;span class="gp"&gt;pgdata=#&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both tables have the same size and structure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;pgdata=#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;SELECT
&lt;span class="gp"&gt;pgdata-#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;relname AS table_name,
&lt;span class="gp"&gt;pgdata-#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pg_size_pretty&lt;span class="o"&gt;(&lt;/span&gt;pg_table_size&lt;span class="o"&gt;(&lt;/span&gt;relid&lt;span class="o"&gt;))&lt;/span&gt; AS table_size
&lt;span class="gp"&gt;pgdata-#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;FROM
&lt;span class="gp"&gt;pgdata-#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pg_catalog.pg_statio_user_tables
&lt;span class="gp"&gt;pgdata-#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;WHERE
&lt;span class="gp"&gt;pgdata-#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;relname IN &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tvectr768'&lt;/span&gt;, &lt;span class="s1"&gt;'tvreal768'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt; table_name | table_size
------------+------------
 tvectr768 | 4008 MB
 tvreal768 | 4008 MB
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you go a bit deeper, you can see that both tables are about 57 MB in heap size, plus 3950 MB in TOAST where the actual vector data is stored. It makes perfect sense, since the d column with vector(768) is larger than the default 2k threshold and is stored entirely in the TOAST segment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;pgdata-#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;relname AS table_name,
&lt;span class="gp"&gt;pgdata-#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pg_size_pretty&lt;span class="o"&gt;(&lt;/span&gt;pg_relation_size&lt;span class="o"&gt;(&lt;/span&gt;oid&lt;span class="o"&gt;))&lt;/span&gt; AS heap_size,
&lt;span class="gp"&gt;pgdata-#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pg_size_pretty&lt;span class="o"&gt;(&lt;/span&gt;pg_table_size&lt;span class="o"&gt;(&lt;/span&gt;oid&lt;span class="o"&gt;)&lt;/span&gt; - pg_relation_size&lt;span class="o"&gt;(&lt;/span&gt;oid&lt;span class="o"&gt;))&lt;/span&gt; AS toast_size
&lt;span class="gp"&gt;pgdata-#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;FROM pg_class
&lt;span class="gp"&gt;pgdata-#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;WHERE relname &lt;span class="o"&gt;=&lt;/span&gt; ANY &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'{"tvectr768" ,"tvreal768"}'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;pgdata-#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;AND relkind &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'r'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt; table_name | heap_size | toast_size
------------+-----------+------------
 tvectr768 | 57 MB | 3950 MB
 tvreal768 | 57 MB | 3950 MB
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let us do some performance tests and compare the results.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building Vector Index
&lt;/h3&gt;

&lt;p&gt;For my tests, I was using Postgres 18 on a Debian VM in Google Cloud, and &lt;a href="https://cloud.google.com/alloydb/omni?utm_campaign=CDR_0x370c34a8_default_b524180055&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;AlloyDB Omni&lt;/a&gt; for &lt;a href="https://docs.cloud.google.com/alloydb/omni/containers/18.1.0/docs/ai/scann-vector-query-perf-overview?utm_campaign=CDR_0x370c34a8_default_b524180055&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;ScaNN index&lt;/a&gt; tests on a VM of the same size. We are mostly interested in the difference between “real” and synthetic vector values, but this varies depending on the index. The first test is to build an IVFFLAT index on our vectors.&lt;/p&gt;

&lt;p&gt;Here is an example for the IVFFLAT index on synthetic vector data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;drop&lt;/span&gt; &lt;span class="k"&gt;index&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="k"&gt;exists&lt;/span&gt; &lt;span class="n"&gt;tvectr768_d_idx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;tvectr768&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;ivfflat&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="n"&gt;vector_cosine_ops&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lists&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can see I was using default parameters, specifying only the number of lists. According to my tests, it takes about 271,068 ms to build the index on the synthetic tvectr768 table. And when it is built, the size of the index is roughly equal to the size of all the vectors (3912 MB), which makes total sense.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr7lvllt1cbgjixcxt0cj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr7lvllt1cbgjixcxt0cj.png" width="787" height="586"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When we run the same for the tvreal768 table with real vectors, we get similar results: 271,788 ms. So far, there is no difference. And it is probably expected if we think about how the IVFFLAT index is being built with splitting to buckets according to the number of lists and then simply moving each vector to one or another bucket.&lt;/p&gt;

&lt;p&gt;What about the &lt;a href="https://github.com/pgvector/pgvector#hnsw" rel="noopener noreferrer"&gt;HNSW&lt;/a&gt; index? Here is an example of a query used to build the HNSW index.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;drop&lt;/span&gt; &lt;span class="k"&gt;index&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="k"&gt;exists&lt;/span&gt; &lt;span class="n"&gt;tvectr768_d_idx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;tvectr768&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;hnsw&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="n"&gt;vector_cosine_ops&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here we start to see the difference in performance. The HNSW is a graph-based index and it is much harder to find node connections for a totally random array of numbers. You can play with different build parameters for the index and see how it can impact the results.&lt;/p&gt;

&lt;p&gt;For the building with default parameters it took on average 6601789 ms for synthetic data and 4174196 ms for the “real” vectors. That’s 1.5 faster for the real data and it is significant.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcuwpjuocg1zwd09hvayg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcuwpjuocg1zwd09hvayg.png" width="782" height="586"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So, why is it happening? How does the randomness in the numbers in the vector impact the performance? It probably deserves a separate article but I will try to give a short explanation. The HNSW builds its graph incrementally. When it adds another vector to the index it has to search for the right place in the graph. It means to find the closest neighbors to connect the new vector to. HNSW relies on the assumption that your data contains some underlying structure and it means clusters, patterns, and dense neighborhoods. The embedding vectors have but the random vector most likely not. Because of that the build process has to evaluate more candidates to find neighbors. That process is mathematically expensive for distances like cosine distance and takes time. That’s a short explanation but I hope it helps to understand to some extent the reasons behind the difference in built time.&lt;/p&gt;

&lt;p&gt;With the ScaNN index we have another case of striking difference between building time for index with fully synthetic vs “real” dataset. I was using the basic recommended parameters for the ScaNN index according to the &lt;a href="https://docs.cloud.google.com/alloydb/omni/containers/current/docs/ai/tune-indexes?resource=scann&amp;amp;utm_campaign=CDR_0x370c34a8_default_b524180055&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;guide&lt;/a&gt; for AlloyDB Omni.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;LOCAL&lt;/span&gt; &lt;span class="n"&gt;scann&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;num_leaves_to_search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;LOCAL&lt;/span&gt; &lt;span class="n"&gt;scann&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pre_reordering_num_neighbors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;drop&lt;/span&gt; &lt;span class="k"&gt;index&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="k"&gt;exists&lt;/span&gt; &lt;span class="n"&gt;tvreal768_d_idx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;tvreal768_d_idx&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;tvreal768&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;scann&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="n"&gt;cosine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num_leaves&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It took 237094 ms to build the index on the synthetic data vs only 62673 ms on the “real” dataset. And the size of indexes was quite different because AlloyDB ScaNN was using internal index optimization for the real dataset. We have 1562 MB for the synthetic data index vs 275 MB for the “real” one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fecnfu7z55b34k2lucbja.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fecnfu7z55b34k2lucbja.png" width="782" height="586"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So, when you start to evaluate performance in building indexes then you probably should use proper data or you can get misleading results. I would recommend using vectors built on your actual data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vector Search Performance
&lt;/h3&gt;

&lt;p&gt;Now that we have our ANN indexes built, we can evaluate how quickly you can find the top 5 similar vectors using the HNSW index. I am using a procedure that takes 100 samples from the table and uses some of those samples as predicates to find the 5 most similar vectors. I repeated this 11 times, discarding the first execution. This gives me the throughput in Queries Per Second (QPS) and the average execution time per query, along with the max and min times for all executions. You can check the procedure benchmark_vector_search in the Appendix chapter and the end of the article or &lt;a href="https://github.com/GoogleCloudPlatform/devrel-demos/tree/main/infrastructure/database-vectors-tests" rel="noopener noreferrer"&gt;download&lt;/a&gt; it from GitHub.&lt;/p&gt;

&lt;p&gt;Here is a sample output from the procedure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NOTICE: ------------------------------------------------------------------------
NOTICE: Benchmark finished successfully.
NOTICE: Total queries executed: 1000 (excluding warmup)
NOTICE: Total execution time: 2.145 seconds (excluding warmup)
NOTICE: Throughput (overall): 466.1 QPS (excluding warmup)
NOTICE: ------------------------------------------------------------------------
NOTICE: Latency Stats (excluding warmup):
NOTICE: Average: 2.125 ms
NOTICE: Minimum: 1.389 ms
NOTICE: Maximum: 3.888 ms
NOTICE: ------------------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we run our tests, it shows 4.173 ms on average for synthetic data vs. 2.102 ms for our “real” vectors using HNSW index. Interestingly enough, for AlloyDB ScaNN, the difference is not so big — only 5.958 ms vs. 5.161 ms if we use 1,000 lists. But if we use an index with only 100 lists, we can see a significant difference in performance, where the response time for synthetic data is 7.239 ms vs. 4.249 ms for the “real” ones.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpoxmz4ix3fndtbmjoiig.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpoxmz4ix3fndtbmjoiig.png" width="782" height="586"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The reasons behind the difference in performance have the same nature but slightly different consequences. Since the random vector lacks clusters and dense neighborhoods it goes through more steps to find the closest neighbours. Instead, for example going through 32 nodes to find the closest neighbour it has to traverse 256 or more and even in such a case the quality might be still low.&lt;/p&gt;

&lt;p&gt;So, if you are testing HNSW, which is widely used in the community, the 2x difference is big enough and fully justifies the effort of getting a real dataset.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vector Search Quality
&lt;/h3&gt;

&lt;p&gt;We already established that synthetic data can mislead you about build time or vector search speed for your vector indexes. What about quality?&lt;/p&gt;

&lt;p&gt;Usually, when we talk about vector search quality, we use the term “recall,” which represents the percentage of exact matches returned by an ANN index compared to the list of hits returned by a KNN exact search. For example, we do a KNN search returning the top 10 similar vectors. Then, we run exactly the same query with an ANN index and compare how many of the 10 vectors returned by the ANN search match the KNN results. If it is 9 out of 10, then it is 90% recall.&lt;/p&gt;

&lt;p&gt;In my procedure evaluate_vector_recall, I use 100 random samples from the dataset and then iterate on those samples to get the recall for each of them, averaging the results at the end. I tested it with the top 10 similar vectors (&lt;code&gt;k_param=&amp;gt;10&lt;/code&gt;). The procedure is provided in the Appendix and on &lt;a href="https://github.com/GoogleCloudPlatform/devrel-demos/tree/main/infrastructure/database-vectors-tests" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here is a sample output of the procedure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;pgdata=#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;call evaluate_vector_recall&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tvreal768'&lt;/span&gt;,768,k_param&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;10&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt;NOTICE: Starting vector similarity search recall evaluation...
NOTICE: Target table: tvreal768, Expected dimensions: 768
NOTICE: Parameters - Vector Column: d, ID Column: id, K: 10, Sample Size: 100
NOTICE: Selecting 100 random vectors for sampling from tvreal768...
NOTICE: ------------------------------------------------------------------------
NOTICE: Recall Evaluation Finished.
NOTICE: Total queries evaluated: 100
NOTICE: Average Recall@10: %91.80
NOTICE: Minimum Recall@10: %0.00
NOTICE: Maximum Recall@10: %100.00
NOTICE: ------------------------------------------------------------------------
CALL
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;According to my test results, the recall for synthetic data is quite low. On average for the 100 samples, I am getting only 3.8% recall using HNSW, while for “real” data, the recall is 91.8%. This means our search on synthetic data is not only slow but also hugely inaccurate. When we try it using the AlloyDB ScaNN index, it shows 16.4% recall for synthetic data vs. 91.4% for the “real” data. Only IVFFLAT didn’t show itself good with the real dataset giving only about 51.9% recall on average. But the quality with synthetic data was still much worse than that — only 10.6%&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq5ecvr9bmd99b09zrktt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq5ecvr9bmd99b09zrktt.png" width="787" height="586"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Of course, your test results can and most likely will be different, but the trend is clear. Even if ScaNN shows slightly better results for a synthetic vector search, it is still quite misleading and inaccurate.&lt;/p&gt;

&lt;p&gt;You also can opt-in to use the evaluate_query_recall procedure coming with the AlloyDB Omni and it gives you the option to measure recall. Read more in the official &lt;a href="https://docs.cloud.google.com/alloydb/omni/containers/current/docs/ai/measure-vector-query-recall?utm_campaign=CDR_0x370c34a8_default_b524180055&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;guide&lt;/a&gt; on how to use the procedure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;Here is my short summary for all the tests and results&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Synthetic vectors can be used for pure performance DML tests where the vectors are not used as predicates and are only used to fill up space. For example, for inserts, updates, or deletes of rows filtered by non-vector column data.&lt;/li&gt;
&lt;li&gt;Synthetic vector data shows incorrect results for index build times and can mislead testers by giving inaccurate results. So far, in most of my tests, synthetic data required more time to build the index compared to realistic vector data.&lt;/li&gt;
&lt;li&gt;An ANN vector search executed against a synthetic dataset can be 1.5 times slower than for a similar dataset with realistic vectors.&lt;/li&gt;
&lt;li&gt;Recall quality for an ANN vector search cannot be measured against synthetic vectors. In my tests, the recall quality ranged from 3.8% to 16.4%, depending on the index type and index parameters. The real dataset showed more than 90% recall for most of the tests.&lt;/li&gt;
&lt;li&gt;Get a real dataset and use it for all your performance and quality testing for ANN indexes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Appendix
&lt;/h3&gt;

&lt;p&gt;Here are some procedures I used for testing. I don’t pretend they are perfect in any way but should be clear enough for everyone who tries to use it. I used some hardcoded values to make it simpler to read. Both procedures are available on &lt;a href="https://github.com/GoogleCloudPlatform/devrel-demos/tree/main/infrastructure/database-vectors-tests" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here is the procedure to check response time for vector search. It is using cosine distance for measurement and takes the first 100 vectors from the table as a sample to be used later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;PROCEDURE&lt;/span&gt; &lt;span class="n"&gt;benchmark_vector_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;n_repetitions&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;table_name_param&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;vector_dimensions&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt;
    &lt;span class="c1"&gt;-- Array to hold the 100 vectors sampled for the benchmark run&lt;/span&gt;
    &lt;span class="n"&gt;sample_vectors&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="c1"&gt;-- Variable to hold the current vector being searched for&lt;/span&gt;
    &lt;span class="n"&gt;current_vector&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;-- Variables for timing&lt;/span&gt;
    &lt;span class="n"&gt;overall_start_time&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;overall_end_time&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;repetition_start_time&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;q_start&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;q_end&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;q_diff&lt;/span&gt; &lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="nb"&gt;precision&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- in milliseconds&lt;/span&gt;

    &lt;span class="c1"&gt;-- Metrics tracked across the entire benchmark&lt;/span&gt;
    &lt;span class="n"&gt;global_min_latency&lt;/span&gt; &lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="nb"&gt;precision&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;e9&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;global_max_latency&lt;/span&gt; &lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="nb"&gt;precision&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;global_sum_latency&lt;/span&gt; &lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="nb"&gt;precision&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;overall_total_queries&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Variable for dimension validation&lt;/span&gt;
    &lt;span class="n"&gt;actual_dimensions&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;actual_sample_size&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Loop counter&lt;/span&gt;
    &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'Starting vector similarity search benchmark...'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'Target table: %, Repetitions: %, Expected dimensions: %'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_name_param&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_repetitions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vector_dimensions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Validate that the vector column "d" exists and dimensions match&lt;/span&gt;
    &lt;span class="k"&gt;BEGIN&lt;/span&gt;
        &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SELECT vector_dims(d) FROM %I WHERE d IS NOT NULL LIMIT 1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_name_param&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;actual_dimensions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;actual_dimensions&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
            &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="s1"&gt;'Could not find any non-NULL vectors in table %. The table might be empty or column "d" contains only NULLs.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_name_param&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;actual_dimensions&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;vector_dimensions&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
            &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="s1"&gt;'Vector dimension mismatch. Expected dimension %, but found dimension % in table %.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vector_dimensions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actual_dimensions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_name_param&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;EXCEPTION&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;undefined_table&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
            &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="s1"&gt;'Table "%" does not exist.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_name_param&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;undefined_column&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
            &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="s1"&gt;'Table "%" does not have a column named "d".'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_name_param&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'Dimension validation successful.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Fetch 100 random vectors to use as search queries&lt;/span&gt;
    &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'Selecting 100 random vectors for sampling from %...'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_name_param&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SELECT array_agg(d) FROM (SELECT d FROM %I WHERE d IS NOT NULL ORDER BY random() LIMIT 100) AS random_sample'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_name_param&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;sample_vectors&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;sample_vectors&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;array_length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample_vectors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
        &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="s1"&gt;'Could not retrieve any sample vectors. The table % might be empty or all vectors are NULL.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_name_param&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;actual_sample_size&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;array_length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample_vectors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;actual_sample_size&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
        &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;WARNING&lt;/span&gt; &lt;span class="s1"&gt;'Could not retrieve 100 sample vectors. Using % available sample vectors instead.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actual_sample_size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'Starting main benchmark loop...'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;overall_start_time&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clock_timestamp&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;-- The main outer loop that repeats the entire test (warmup + N repetitions)&lt;/span&gt;
    &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="n"&gt;n_repetitions&lt;/span&gt; &lt;span class="n"&gt;LOOP&lt;/span&gt;
        &lt;span class="n"&gt;repetition_start_time&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clock_timestamp&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;-- Reset overall_start_time to start of repetition 1 to exclude warmup duration from QPS/Totals&lt;/span&gt;
        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
            &lt;span class="n"&gt;overall_start_time&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clock_timestamp&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;DECLARE&lt;/span&gt;
            &lt;span class="n"&gt;rep_min_latency&lt;/span&gt; &lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="nb"&gt;precision&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;e9&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;rep_max_latency&lt;/span&gt; &lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="nb"&gt;precision&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;rep_sum_latency&lt;/span&gt; &lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="nb"&gt;precision&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;rep_queries&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;rep_duration&lt;/span&gt; &lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="nb"&gt;precision&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="c1"&gt;-- The inner loop that iterates through each of the sampled vectors&lt;/span&gt;
            &lt;span class="n"&gt;FOREACH&lt;/span&gt; &lt;span class="n"&gt;current_vector&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="n"&gt;ARRAY&lt;/span&gt; &lt;span class="n"&gt;sample_vectors&lt;/span&gt;
            &lt;span class="n"&gt;LOOP&lt;/span&gt;
                &lt;span class="n"&gt;q_start&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clock_timestamp&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="c1"&gt;-- Perform the core operation (limiting to top 5)&lt;/span&gt;
                &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SELECT 1 FROM %I ORDER BY d &amp;lt;=&amp;gt; $1 LIMIT 5'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_name_param&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;current_vector&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="n"&gt;q_end&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clock_timestamp&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

                &lt;span class="n"&gt;q_diff&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epoch&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q_end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;q_start&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- milliseconds&lt;/span&gt;

                &lt;span class="c1"&gt;-- Accumulate local statistics&lt;/span&gt;
                &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;q_diff&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;rep_min_latency&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;rep_min_latency&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;q_diff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;q_diff&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;rep_max_latency&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;rep_max_latency&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;q_diff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="n"&gt;rep_sum_latency&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rep_sum_latency&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;q_diff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="n"&gt;rep_queries&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rep_queries&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;LOOP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="c1"&gt;-- Calculate total time for this repetition&lt;/span&gt;
            &lt;span class="n"&gt;rep_duration&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epoch&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clock_timestamp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;repetition_start_time&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

            &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
                &lt;span class="c1"&gt;-- Warmup iteration: report it but do not add to global statistics&lt;/span&gt;
                &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'Warmup Repetition: Avg Latency = % ms (Min: % ms, Max: % ms) | QPS: %'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                             &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;rep_sum_latency&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;rep_queries&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
                             &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rep_min_latency&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
                             &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rep_max_latency&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                             &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;rep_queries&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;rep_duration&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;ELSE&lt;/span&gt;
                &lt;span class="c1"&gt;-- Accumulate global statistics&lt;/span&gt;
                &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;rep_min_latency&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;global_min_latency&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;global_min_latency&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rep_min_latency&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;rep_max_latency&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;global_max_latency&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;global_max_latency&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rep_max_latency&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="n"&gt;global_sum_latency&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;global_sum_latency&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;rep_sum_latency&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="n"&gt;overall_total_queries&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;overall_total_queries&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;rep_queries&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

                &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'Repetition %/%: Avg Latency = % ms (Min: % ms, Max: % ms) | QPS: %'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                             &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_repetitions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                             &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;rep_sum_latency&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;rep_queries&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
                             &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rep_min_latency&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
                             &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rep_max_latency&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                             &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;rep_queries&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;rep_duration&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;LOOP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;overall_end_time&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clock_timestamp&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;-- Log overall summary metrics&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt;
        &lt;span class="n"&gt;total_benchmark_time&lt;/span&gt; &lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="nb"&gt;precision&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;overall_qps&lt;/span&gt; &lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="nb"&gt;precision&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;BEGIN&lt;/span&gt;
        &lt;span class="n"&gt;total_benchmark_time&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epoch&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;overall_end_time&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;overall_start_time&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="n"&gt;overall_qps&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;overall_total_queries&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total_benchmark_time&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'------------------------------------------------------------------------'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'Benchmark finished successfully.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'Total queries executed: % (excluding warmup)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;overall_total_queries&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'Total execution time: % seconds (excluding warmup)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_benchmark_time&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'Throughput (overall): % QPS (excluding warmup)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;overall_qps&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'------------------------------------------------------------------------'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'Latency Stats (excluding warmup):'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;' Average: % ms'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;global_sum_latency&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;overall_total_queries&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;' Minimum: % ms'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;global_min_latency&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;' Maximum: % ms'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;global_max_latency&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'------------------------------------------------------------------------'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here is the procedure where I measure recall quality. It takes recall from each sample and then calculates an average recall quality for the entire execution.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;PROCEDURE&lt;/span&gt; &lt;span class="n"&gt;evaluate_vector_recall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;table_name_param&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;vector_dimensions&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;vector_col_param&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'d'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;id_col_param&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;k_param&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;sample_size_param&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt;
    &lt;span class="c1"&gt;-- Array to hold the vectors sampled for the recall evaluation&lt;/span&gt;
    &lt;span class="n"&gt;sample_vectors&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="c1"&gt;-- Variable to hold the current vector being searched for&lt;/span&gt;
    &lt;span class="n"&gt;current_vector&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;-- Arrays to hold the result IDs from exact and approximate searches&lt;/span&gt;
    &lt;span class="n"&gt;exact_ids&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="n"&gt;approx_ids&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="c1"&gt;-- Metrics variables&lt;/span&gt;
    &lt;span class="n"&gt;overlap_count&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;vector_recall&lt;/span&gt; &lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="nb"&gt;precision&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;sum_recall&lt;/span&gt; &lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="nb"&gt;precision&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;min_recall&lt;/span&gt; &lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="nb"&gt;precision&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;max_recall&lt;/span&gt; &lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="nb"&gt;precision&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;actual_dimensions&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;actual_sample_size&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;total_queries&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'Starting vector similarity search recall evaluation...'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'Target table: %, Expected dimensions: %'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_name_param&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vector_dimensions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'Parameters - Vector Column: %, ID Column: %, K: %, Sample Size: %'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                 &lt;span class="n"&gt;vector_col_param&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id_col_param&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k_param&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sample_size_param&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Validate vector column exists and dimension matches&lt;/span&gt;
    &lt;span class="k"&gt;BEGIN&lt;/span&gt;
        &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SELECT vector_dims(%I) FROM %I WHERE %I IS NOT NULL LIMIT 1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                       &lt;span class="n"&gt;vector_col_param&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_name_param&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vector_col_param&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;actual_dimensions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;actual_dimensions&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
            &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="s1"&gt;'Could not find any non-NULL vectors in column % of table %. The table might be empty or contains only NULLs.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                            &lt;span class="n"&gt;vector_col_param&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_name_param&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;actual_dimensions&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;vector_dimensions&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
            &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="s1"&gt;'Vector dimension mismatch. Expected dimension %, but found dimension % in table %.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                            &lt;span class="n"&gt;vector_dimensions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actual_dimensions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_name_param&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;EXCEPTION&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;undefined_table&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
            &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="s1"&gt;'Table "%" does not exist.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_name_param&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;undefined_column&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
            &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="s1"&gt;'Column "%" does not exist in table "%".'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vector_col_param&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_name_param&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Validate ID column exists&lt;/span&gt;
    &lt;span class="k"&gt;BEGIN&lt;/span&gt;
        &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SELECT %I FROM %I LIMIT 1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id_col_param&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_name_param&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;EXCEPTION&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;undefined_column&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
            &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="s1"&gt;'ID column "%" does not exist in table "%". Cannot measure recall.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                            &lt;span class="n"&gt;id_col_param&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_name_param&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Select random sample vectors (ignoring nulls)&lt;/span&gt;
    &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'Selecting % random vectors for sampling from %...'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sample_size_param&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_name_param&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SELECT array_agg(%I) FROM (SELECT %I FROM %I WHERE %I IS NOT NULL ORDER BY random() LIMIT $1) AS random_sample'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                   &lt;span class="n"&gt;vector_col_param&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vector_col_param&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_name_param&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vector_col_param&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;sample_vectors&lt;/span&gt;
    &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;sample_size_param&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;sample_vectors&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;array_length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample_vectors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
        &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="s1"&gt;'Could not retrieve any sample vectors. The table % might be empty or all vectors are NULL.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_name_param&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;actual_sample_size&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;array_length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample_vectors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;actual_sample_size&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;sample_size_param&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
        &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;WARNING&lt;/span&gt; &lt;span class="s1"&gt;'Could not retrieve % sample vectors. Using % available sample vectors instead.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                      &lt;span class="n"&gt;sample_size_param&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actual_sample_size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Iterate through sampled vectors to calculate recall&lt;/span&gt;
    &lt;span class="n"&gt;FOREACH&lt;/span&gt; &lt;span class="n"&gt;current_vector&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="n"&gt;ARRAY&lt;/span&gt; &lt;span class="n"&gt;sample_vectors&lt;/span&gt;
    &lt;span class="n"&gt;LOOP&lt;/span&gt;
        &lt;span class="c1"&gt;-- 1. Find exact nearest neighbors (ground truth) by forcing sequential scan&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;LOCAL&lt;/span&gt; &lt;span class="n"&gt;enable_indexscan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;LOCAL&lt;/span&gt; &lt;span class="n"&gt;enable_bitmapscan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SELECT array_agg(%I::text) FROM (SELECT %I FROM %I ORDER BY %I &amp;lt;=&amp;gt; $1 LIMIT $2) AS exact_search'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                       &lt;span class="n"&gt;id_col_param&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id_col_param&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_name_param&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vector_col_param&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;exact_ids&lt;/span&gt;
        &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;current_vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k_param&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- 2. Find approximate nearest neighbors (allowing index scan)&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;LOCAL&lt;/span&gt; &lt;span class="n"&gt;enable_indexscan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;LOCAL&lt;/span&gt; &lt;span class="n"&gt;enable_bitmapscan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SELECT array_agg(%I::text) FROM (SELECT %I FROM %I ORDER BY %I &amp;lt;=&amp;gt; $1 LIMIT $2) AS approx_search'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                       &lt;span class="n"&gt;id_col_param&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id_col_param&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_name_param&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vector_col_param&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;approx_ids&lt;/span&gt;
        &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;current_vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k_param&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- 3. Calculate overlap&lt;/span&gt;
        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;exact_ids&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;approx_ids&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
            &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;overlap_count&lt;/span&gt;
            &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;unnest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;approx_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
            &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="k"&gt;unnest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exact_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="n"&gt;vector_recall&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;overlap_count&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="nb"&gt;precision&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;k_param&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;sum_recall&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sum_recall&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;vector_recall&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;vector_recall&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;min_recall&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;min_recall&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vector_recall&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;vector_recall&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;max_recall&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;max_recall&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vector_recall&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="n"&gt;total_queries&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;total_queries&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;LOOP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Print recall report&lt;/span&gt;
    &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;total_queries&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
        &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'------------------------------------------------------------------------'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'Recall Evaluation Finished.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'Total queries evaluated: %'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_queries&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'Average Recall@%: %%%'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k_param&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(((&lt;/span&gt;&lt;span class="n"&gt;sum_recall&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total_queries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'Minimum Recall@%: %%%'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k_param&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;min_recall&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'Maximum Recall@%: %%%'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k_param&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;max_recall&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'------------------------------------------------------------------------'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;ELSE&lt;/span&gt;
        &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;WARNING&lt;/span&gt; &lt;span class="s1"&gt;'No recall queries were successfully evaluated.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






</description>
      <category>data</category>
      <category>alloydb</category>
      <category>postgres</category>
      <category>vectorembeddings</category>
    </item>
    <item>
      <title>Migrating to Antigravity CLI from Gemini CLI with MCP for Google Cloud Databases</title>
      <dc:creator>Gleb Otochkin</dc:creator>
      <pubDate>Tue, 30 Jun 2026 05:31:23 +0000</pubDate>
      <link>https://dev.to/googleai/migrating-to-antigravity-cli-from-gemini-cli-with-mcp-for-google-cloud-databases-h82</link>
      <guid>https://dev.to/googleai/migrating-to-antigravity-cli-from-gemini-cli-with-mcp-for-google-cloud-databases-h82</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ccskbm3w4kkx7rdiajw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ccskbm3w4kkx7rdiajw.png" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Time to Move
&lt;/h3&gt;

&lt;p&gt;If you are following Google news closely you are probably aware about Antigravity CLI &lt;a href="https://blog.google/innovation-and-ai/technology/developers-tools/google-io-2026-developer-highlights/?utm_campaign=CDR_0x370c34a8_default_b525718731&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;announced&lt;/a&gt; on the Google I/O 2026 and gradual &lt;a href="https://developers.googleblog.com/an-important-update-transitioning-gemini-cli-to-antigravity-cli/?utm_campaign=CDR_0x370c34a8_default_b525718731&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;move&lt;/a&gt; to the new tools ecosystem from the previous generation of AI assistants. In short, it’s time to move from Gemini CLI to Antigravity CLI. In this article I’ll explain how to set up your essential MCP servers for Google Cloud in the Antigravity CLI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gemini CLI MCP Configuration
&lt;/h3&gt;

&lt;p&gt;Everybody has their own set of extensions and tools for Gemini CLI. In my case, I work extensively with Google Cloud databases and, as such, I have a few “must-have” extensions for Gemini CLI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fccuv3wn5kdbw74x45snu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fccuv3wn5kdbw74x45snu.png" width="799" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is my minimum Gemini CLI configuration for MCP servers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;gleb@db-connect:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;gemini extension list
&lt;span class="go"&gt;Ignore file not found: /home/gleb/.geminiignore, continue without it.
✓ alloydb (1.0.0)
 ID: dffab4e5e5d86ea81431cad2bf77fc027c0042d54fc4414c67145bfa255ee6cf
 name: dffab4e5e5d86ea81431cad2bf77fc027c0042d54fc4414c67145bfa255ee6cf
 Path: /home/gleb/.gemini/extensions/alloydb
 Enabled (User): true
 Enabled (Workspace): true
 MCP servers:
  AlloyDB MCP Server

✓ cloud-sql (1.0.0)
 ID: fe4d062a1349d2dddc18bd27899586f9c5a6ea524a95c44ec45257cbd18e9ed9
 name: fe4d062a1349d2dddc18bd27899586f9c5a6ea524a95c44ec45257cbd18e9ed9
 Path: /home/gleb/.gemini/extensions/cloud-sql
 Enabled (User): true
 Enabled (Workspace): true
 MCP servers:
  Cloud SQL MCP Server

✓ developer-knowledge (0.1.0)
 ID: 7b4042e11865c674ad1b7007c3040ba4a2a277c16c8d9bda9ba10ac7a70d2eec
 name: 47ba5f7123468475fe1edc973bf455cabe8eb51061f1e892d0bb9e1867db98b9
 Path: /home/gleb/.gemini/extensions/developer-knowledge
 Source: https://github.com/gemini-cli-extensions/developer-knowledge (Type: git)
 Enabled (User): true
 Enabled (Workspace): true
 Context files:
  /home/gleb/.gemini/extensions/developer-knowledge/GEMINI.md
 MCP servers:
  developer-knowledge

✓ spanner (1.0.0)
 ID: eb1ea9d72fe0c79269a8dc2b047ddfb531b004ed19afb45eaef9e9a38cccf351
 name: eb1ea9d72fe0c79269a8dc2b047ddfb531b004ed19afb45eaef9e9a38cccf351
 Path: /home/gleb/.gemini/extensions/spanner
 Enabled (User): true
 Enabled (Workspace): true
 MCP servers:
  Spanner MCP Server
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are three extensions for different Google Cloud databases and the Developer Knowledge MCP. For some projects I have additional MCP servers depending on the nature of the project, but for this tutorial, we will limit ourselves to just those four extensions. Our goal is to move everything to Antigravity CLI and get them working.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install Antigravity CLI
&lt;/h3&gt;

&lt;p&gt;The full instructions on how to install Antigravity CLI are in the &lt;a href="https://antigravity.google/docs/cli-install?utm_campaign=CDR_0x370c34a8_default_b525718731&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;, but here is a short version. I am showing this on a Debian Linux box ,but the steps are generally the same for a Mac installation and migration.&lt;/p&gt;

&lt;p&gt;Run the installation script in a terminal window on your Mac or Linux machine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://antigravity.google/cli/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the end of the installation, it should show something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg7scg0tut9edq4wpulbg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg7scg0tut9edq4wpulbg.png" width="800" height="50"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On Linux, it automatically adds an alias for the binary, so you can start the tool using the agy alias. On the first launch, it will ask you to authenticate either with your Google account or by using a specific project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F17i2niazivzf86hfghpd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F17i2niazivzf86hfghpd.png" width="800" height="239"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And if you’ve chosen a project path then after authentication it will ask to put the project ID:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feo7iz8yaplcqs59fdodo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feo7iz8yaplcqs59fdodo.png" width="800" height="170"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, it will ask about your location and then prompt you to choose a color scheme. On the color scheme screen, there’s a checkbox at the bottom that allows you to import all your existing Gemini CLI extensions into Antigravity CLI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8xl174hh7ybkqzmrl8nt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8xl174hh7ybkqzmrl8nt.png" width="800" height="567"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And as you can see, all my existing extensions are listed there. Can we wrap up here and call it a day? We could, but what if we missed the checkbox and clicked ‘Next’ without importing our extensions? As a result, we wouldn’t have any of our old MCPs in our Antigravity CLI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgb4du60828qtof8kix6z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgb4du60828qtof8kix6z.png" width="800" height="215"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Is it possible to migrate them afterward? Yes, of course, and in the next section, we’ll do exactly that.&lt;/p&gt;

&lt;h3&gt;
  
  
  Migrating Extensions to Antigravity Plugins
&lt;/h3&gt;

&lt;p&gt;The full guide for migrating Gemini CLI extensions is in the &lt;a href="https://antigravity.google/docs/gcli-migration?utm_campaign=CDR_0x370c34a8_default_b525718731&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; and you can take a look there if something isn’t working or if you want the latest information and updates. Here is my short version, along with a few fixes I had to apply.&lt;/p&gt;

&lt;p&gt;First, exit Antigravity and run the following command in the terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agy plugin import gemini
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command should find all existing extensions, skills, hooks, and MCP servers and convert them into Antigravity plugins. You should see something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ih7o59zcsfcz2bvxklz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ih7o59zcsfcz2bvxklz.png" width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now you can start the Antigravity CLI and check your MCP servers using the /mcp command. All of your MCP servers should be visible in the output.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9xs877mzzfddcmjmothl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9xs877mzzfddcmjmothl.png" width="799" height="299"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At first glance, it looks like everything is fine. But you’ll notice that only the Developer Knowledge MCP shows its tools, while none of the database plugins show any tools at all. If that happens, you might need to fix your configuration.&lt;/p&gt;

&lt;p&gt;Have a look into the tools definitions in the ~/.gemini/antigravity-cli/mcp directory — the tools don’t have parameters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;gleb@db-connect:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; .gemini/antigravity-cli/mcp/AlloyDB&lt;span class="se"&gt;\ &lt;/span&gt;MCP&lt;span class="se"&gt;\ &lt;/span&gt;Server/list_clusters.json | jq
&lt;span class="go"&gt;{
  "name": "list_clusters",
  "description": "List all clusters",
  "parameters": null
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also, if we check the Antigravity CLI log at ~/.gemini/antigravity-cli/cli.log, we can see that the database MCP servers are not authenticating correctly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;W0618 18:12:18.733858 86343 mcp_auth.go:634] OAuth setup failed for Spanner MCP Server: OAuth client ID required: server does not support dynamic client registration
W0618 18:12:18.734186 86343 mcp_auth.go:634] OAuth setup failed for AlloyDB MCP Server: OAuth client ID required: server does not support dynamic client registration
W0618 18:12:18.740350 86343 mcp_auth.go:634] OAuth setup failed for Cloud SQL MCP Server: OAuth client ID required: server does not support dynamic client registration
W0618 18:12:18.917512 86343 mcp_auth.go:634] OAuth setup failed for developer-knowledge: OAuth client ID required: server does not support dynamic client registration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And when we look into the plugin config for the imported AlloyDB MCP extension, we can see it doesn’t have an authentication section. Also, the plugin’s name consists of several words separated by spaces.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;gleb@db-connect:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; .gemini/config/plugins/alloydb/mcp_config.json 
&lt;span class="go"&gt;{
  "mcpServers": {
    "AlloyDB MCP Server": {
      "command": "",
      "args": null,
      "cwd": "",
      "env": null,
      "serverUrl": "https://alloydb.googleapis.com/mcp"
    }
  }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The authProviderType parameter is missing, and the name for the MCP server should be fixed. This is likely why it isn’t working correctly. Let’s rename the MCP server and add the missing parameter. Here is the fixed configuration for the AlloyDB MCP plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"AlloyDB"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"cwd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"serverUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://alloydb.googleapis.com/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"authProviderType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"google_credentials"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, if we start the Antigravity CLI again and run the /mcp command, we can see all the tools associated with the remote AlloyDB MCP.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy65i998avndxyg42ioc8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy65i998avndxyg42ioc8.png" width="800" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And when we test the MCP, we can confirm that it works correctly now.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F85zrub7v56m9k2mbzd8l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F85zrub7v56m9k2mbzd8l.png" width="800" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We now have a new subdirectory for the AlloyDB MCP server, and the tools have all the necessary parameters and required information there.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;gleb@db-connect:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; .gemini/antigravity-cli/mcp/AlloyDB/list_clusters.json | jq
&lt;span class="go"&gt;{
  "name": "list_clusters",
  "description": "List all clusters",
  "parameters": {
    "description": "Message for requesting list of Clusters",
    "properties": {
      "filter": {
        "description": "Optional. Filtering results",
        "type": "string"
      },
      "orderBy": {
        "description": "Optional. Hint for how to order the results",
        "type": "string"
      },
      "pageSize": {
        "description": "Optional. Requested page size. Server may return fewer items than requested. If unspecified, server will pick an appropriate default.",
        "format": "int32",
        "type": "integer"
      },
      "pageToken": {
        "description": "A token identifying a page of results the server should return.",
        "type": "string"
      },
      "parent": {
        "description": "Required. The name of the parent resource. For the required format, see the comment on the Cluster.name field. Additionally, you can perform an aggregated list operation by specifying a value with the following format: * projects/{project}/locations/-",
        "type": "string"
      }
    },
    "required": [
      "parent"
    ],
    "type": "object"
  }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The old subdirectory with the incomplete tools can now be removed from the MCP directory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;gleb@db-connect:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; .gemini/antigravity-cli/mcp/AlloyDB&lt;span class="se"&gt;\ &lt;/span&gt;MCP&lt;span class="se"&gt;\ &lt;/span&gt;Server/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can fix the rest of our MCP servers for Cloud SQL and Spanner in the same way. For the Developer Knowledge MCP, however, we only need to add the authProviderType, its name didn’t have any spaces and was processed correctly.&lt;/p&gt;

&lt;p&gt;In the end, you should see something like this when you list your MCP servers in the Antigravity CLI:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg6fg8ckgqqps1d1teloe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg6fg8ckgqqps1d1teloe.png" width="800" height="290"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And a couple of words about the &lt;a href="https://developers.google.com/knowledge/mcp?utm_campaign=CDR_0x370c34a8_default_b525718731&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;Developer Knowledge MCP&lt;/a&gt;. I think it’s one of the “must-have” MCPs in any configuration. It provides up-to-date information directly from the Google documentation and is one of the tools I use every single day.&lt;/p&gt;

&lt;p&gt;You can also test the Developer Knowledge MCP by asking it to prepare a tutorial on a specific product option. For example, you can ask: ‘Prepare a tutorial with options on how I can connect to an AlloyDB instance.’ The Developer Knowledge MCP will use the search_documents tool to find the information and then create the tutorial based on the latest version of the Google documentation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it and let us know
&lt;/h3&gt;

&lt;p&gt;I recommend Antigravity CLI to everyone who likes command-line tools and is more comfortable with a terminal than a GUI. But keep in mind that Antigravity also has a &lt;a href="https://antigravity.google/product/antigravity-2?utm_campaign=CDR_0x370c34a8_default_b525718731&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;GUI&lt;/a&gt; application and &lt;a href="https://antigravity.google/product/antigravity-ide?utm_campaign=CDR_0x370c34a8_default_b525718731&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;IDE&lt;/a&gt;, so you have a choice.&lt;/p&gt;

&lt;p&gt;If you need to customize your Antigravity setup, it supports skills, plugins, rules, hooks, and MCP servers.&lt;/p&gt;

&lt;p&gt;Happy migration, and let us know how it works for you!&lt;/p&gt;




</description>
      <category>alloydb</category>
      <category>googlecloudplatform</category>
      <category>mcpserver</category>
      <category>googleantigravity</category>
    </item>
    <item>
      <title>Gemini 3.5 Flash in Google Cloud Databases</title>
      <dc:creator>Gleb Otochkin</dc:creator>
      <pubDate>Sat, 23 May 2026 09:41:05 +0000</pubDate>
      <link>https://dev.to/googleai/gemini-35-flash-in-google-cloud-databases-14kg</link>
      <guid>https://dev.to/googleai/gemini-35-flash-in-google-cloud-databases-14kg</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3or5xf1jx86n9uybiowh.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3or5xf1jx86n9uybiowh.jpeg" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I hope you’ve been able to attend or at least watch the IO 26 &lt;a href="https://www.youtube.com/live/wYSncx9zLIU?si=telp4s_cG8Hd0O5x" rel="noopener noreferrer"&gt;keynote&lt;/a&gt;. Among other exciting announcements Google introduced the new &lt;a href="https://ai.google.dev/gemini-api/docs/models/gemini-3.5-flash?utm_campaign=CDR_0x370c34a8_default_b515026843&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;Gemini 3.5 Flash model&lt;/a&gt;, and I immediately went to test it in Cloud SQL and AlloyDB. It worked really well, returning faster and more accurate responses in my particular use case compared to Gemini 2.5 Flash or even Gemini 3 Flash Preview. I encourage you to try it out for yourself and test it with your own workloads. Let me show you how I tested it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloud SQL
&lt;/h3&gt;

&lt;p&gt;Cloud SQL has full &lt;a href="https://docs.cloud.google.com/sql/docs/postgres/ai-overview?utm_campaign=CDR_0x370c34a8_default_b515026843&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;AI integration&lt;/a&gt; via the google_ml extension and can &lt;a href="https://docs.cloud.google.com/sql/docs/postgres/invoke-online-predictions?utm_campaign=CDR_0x370c34a8_default_b515026843&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;call models&lt;/a&gt; directly from a SQL query. It has several preregistered models, but Gemini 3.5 Flash was not there yet when I tested it. To work with the model, I registered it using the following call. Replace the PROJECT_ID placeholder in the code by your Google project id.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;google_ml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model_id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'gemini-3.5-flash'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model_request_url&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'https://aiplatform.googleapis.com/v1/projects/PROJECT_ID/locations/global/publishers/google/models/gemini-3.5-flash:generateContent'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model_provider&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'google'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model_type&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'generic'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model_auth_type&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'cloudsql_service_agent_iam'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you look at the model_request_url parameter, you’ll notice I’ve put the full URL path there with the global location. And I have a reason for that. Even if Cloud SQL and AlloyDB allow you to use a short variant for the URL, it might not work with the new models. If you try to use something like publishers/google/models/gemini-3.5-flash:generateContent, you will most likely get an error, since by default it tries to use a regional path — which is not available for Gemini 3.5 models.&lt;/p&gt;

&lt;p&gt;After the registration, the model can be used in a query or in a function. I tested it with the following query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;google_ml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;predict_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model_id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'gemini-3.5-flash'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;request_body&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;json_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'contents'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json_build_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;json_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'role'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'user'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'parts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json_build_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;json_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'text'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Explain MCP server for a relational database in 50 words or less.'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'candidates'&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'content'&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'parts'&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'text'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ai_response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It took about 4.1 seconds to get the response from the Gemini 3.5 Flash model. Then I registered the Gemini 2.5 Flash and Gemini 3 Flash Preview models and repeated the same query using their endpoints. For Gemini 2.5 Flash, it took 5.3 seconds on average, and 12.3 seconds for Gemini 3 Flash Preview. I expected version 3.5 to be faster than the preview version, but it was even faster than version 2.5.&lt;/p&gt;

&lt;p&gt;Then we have to look into the quality of the response. Gemini 3.5 Flash provided an answer that aligns better with how the term “MCP” is used today when compared to the response from Gemini 2.5 Flash.&lt;/p&gt;

&lt;p&gt;Here is an example of the response from Gemini 2.5 Flash:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“MCP server isn’t a standard term in relational databases. It *could* refer to a Master Control Program in legacy systems, acting as a central process coordinating database operations. It’s not a common component in modern database architectures.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It looks like Gemini 2.5 Flash talks about an OS for mainframes. I am not sure how relevant that is for a general audience. Now, let’s compare it with the response from Gemini 3.5 Flash.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“An MCP (Model Context Protocol) server is a secure bridge connecting AI models to a relational database. It translates the AI’s natural language requests into SQL commands, allowing the model to safely inspect schemas, query data, and perform database operations in real-time.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That makes more sense and especially now when everybody uses AI and agents.&lt;/p&gt;

&lt;p&gt;Here is the summary table for the models performance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Model Version | Response Time | Response Quality
-----------------------|---------------|------------------
Gemini 3.5 Flash | 4.1 seconds | Excellent
Gemini 2.5 Flash | 5.3 seconds | Average
Gemini 3 Flash Preview | 12.3 seconds | Good
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the tests with Cloud SQL, I moved to my AlloyDB cluster to see how it worked there.&lt;/p&gt;

&lt;h3&gt;
  
  
  AlloyDB
&lt;/h3&gt;

&lt;p&gt;I used a very similar procedure to register the new model in the AlloyDB database, with the only difference being the model_type parameter. In AlloyDB, we can register it with the model type “llm”. Again — don’t forget to replace the PROJECT_ID placeholder by your project id.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;google_ml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="n"&gt;model_id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'gemini-3.5-flash'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;model_request_url&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'https://aiplatform.googleapis.com/v1/projects/PROJECT_ID/locations/global/publishers/google/models/gemini-3.5-flash:generateContent'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;model_provider&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'google'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;model_type&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'llm'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To test the model in AlloyDB, I used a different function — &lt;a href="https://docs.cloud.google.com/alloydb/docs/ai/evaluate-semantic-queries-ai-operators?utm_campaign=CDR_0x370c34a8_default_b515026843&amp;amp;utm_medium=external&amp;amp;utm_source=blog#text-generation-summarization" rel="noopener noreferrer"&gt;ai.generate&lt;/a&gt;. The ai.generate function is much more convenient to use — you don’t need to prepare your JSON input, and it returns the output as plain text right out of the box.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'Explain MCP server for a relational database in 50 words or less.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model_id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'gemini-3.5-flash'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ai_response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In AlloyDB, the response from Gemini 3.5 Flash took the same 4.2 seconds on average, with the same high-quality results.&lt;/p&gt;

&lt;p&gt;The Gemini 2.5 Flash model was already there out of the box, and I registered Gemini 3 Flash Preview using the same approach as above. After testing the query using ai.generate, I got 5.4 seconds for Gemini 2.5 Flash and 13.1 seconds on average for Gemini 3 Flash Preview. The quality of the response was the same as in my Cloud SQL tests. And the ai.generate on AlloyDB was performing with the same speed as google_ml.predict_row on Cloud SQL. I tested it with both regional and global endpoints getting the same performance.&lt;/p&gt;

&lt;p&gt;The new model is more &lt;a href="https://ai.google.dev/gemini-api/docs/pricing?utm_campaign=CDR_0x370c34a8_default_b515026843&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;expensive&lt;/a&gt; but considering speed and quality of the result it might pay off especially for agentic workload where low quality response can lead to more turns for an agent and more consumption in the end.&lt;/p&gt;

&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;My experience so far with the new Gemini 3.5 Flash model is faster and better responses over the previous Gemini 2.5 Flash model, and it is much faster than the Gemini 3 Flash Preview we have been testing previously. I think it is the new ‘workhorse’ for most of my workloads. Try it out for yourself, and read about the new Gemini model and all the other I/O ’26 announcements in the &lt;a href="https://cloud.google.com/blog/products/ai-machine-learning/innovations-from-google-io-26-on-google-cloud?utm_campaign=CDR_0x370c34a8_default_b515026843&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;Google blog&lt;/a&gt;.&lt;/p&gt;




</description>
      <category>data</category>
      <category>cloudsql</category>
      <category>ai</category>
      <category>googlecloudplatform</category>
    </item>
    <item>
      <title>Demystifying max connections limit in Cloud SQL for PostgreSQL</title>
      <dc:creator>Gleb Otochkin</dc:creator>
      <pubDate>Thu, 30 Apr 2026 22:43:25 +0000</pubDate>
      <link>https://dev.to/gleb_otochkin/demystifying-max-connections-limit-in-cloud-sql-for-postgresql-55o6</link>
      <guid>https://dev.to/gleb_otochkin/demystifying-max-connections-limit-in-cloud-sql-for-postgresql-55o6</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo4sgiryuatsm25t1ub4e.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo4sgiryuatsm25t1ub4e.jpeg" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Introduction
&lt;/h3&gt;

&lt;p&gt;If you’ve worked with &lt;a href="https://docs.cloud.google.com/sql/docs/postgres?utm_campaign=CDR_0x370c34a8_default_b507520740&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;Google Cloud SQL for PostgreSQL&lt;/a&gt;, you’re likely aware that it sets a maximum number of connections for your instance at creation. This number isn’t static. It depends on the size — or rather, the machine type of the instance. For example, if you choose the db-f1-micro (&lt;a href="https://docs.cloud.google.com/sql/docs/postgres/machine-series-overview?utm_campaign=CDR_0x370c34a8_default_b507520740&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;the smallest available tier&lt;/a&gt;), you are capped at 25 connections by default.&lt;/p&gt;

&lt;p&gt;And it might happen that you’ve already experienced an early wakeup call when your application suddenly ran out of connection and triggered an error like C: 53300: remaining connection slots are reserved…. That means you have run out of connections.&lt;/p&gt;

&lt;p&gt;Let’s discuss connections, database parameters, and the logic behind these limits. My goal is to clear up some of the common questions I hear in the community and in private conversations with developers. I’ve structured this post as a Q&amp;amp;A to address the most frequently asked questions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Max Connections in Cloud SQL for Postgres
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Is the connection number a hard limit?
&lt;/h4&gt;

&lt;p&gt;In chats and conversations with developers, I often hear about how people try to solve the problem when they hit the max number of connections. Quite often, there is a general assumption that 25 connections for a db-f1-micro is a hard limit, and the only ways to tackle it are to change the shape of the instance or implement connection pooling.&lt;/p&gt;

&lt;p&gt;I don’t have anything against both approaches — they might be fully justified. But it is not a hard limit, and you can change it by updating the max_connections database flag using either the Google Cloud Console or the gcloud SDK. Keep in mind that the change requires a short downtime to apply. Read more in the &lt;a href="https://docs.cloud.google.com/sql/docs/postgres/quotas#maximum_concurrent_connections?utm_campaign=CDR_0x370c34a8_default_b507520740&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Will it automatically change if the instance is resized?
&lt;/h4&gt;

&lt;p&gt;If you change your Cloud SQL instance size and haven’t manually set the max_connections database flag, the value will change automatically according to your instance size. For example, if you have a db-f1-micro instance with 25 connections and increase the size to db-g1-small, your max_connections will increase to 50.&lt;/p&gt;

&lt;h4&gt;
  
  
  What if I set up a custom max_connections flag?
&lt;/h4&gt;

&lt;p&gt;If you define your preferred max_connections as a database flag, it will stay the same even through instance reconfigurations like changing the size. This can play a trick on someone who upgrades the instance size in anticipation of a higher max_connections value, only to find that it hasn’t changed.&lt;/p&gt;

&lt;h4&gt;
  
  
  What are the factors impacting the max_connections?
&lt;/h4&gt;

&lt;p&gt;The main factor is the instance memory. In PostgreSQL, you have shared memory for the data pages you work with, and each session also has its own individual memory. This is a bit of a simplified description, but it is probably enough to explain the memory impact. Let’s go down to the memory allocations.&lt;/p&gt;

&lt;p&gt;You need shared buffers to work with your data — each page of data from a table or index is copied to this area so it can be accessed by your session. The more data you have and the more you need to work with, the more shared buffers you’ll need for better performance. Otherwise, you will be constantly moving data to and from the disk.&lt;/p&gt;

&lt;p&gt;When you connect to the instance, your session allocates roughly 2 MB of memory. Then, as you work with data, the memory allocation depends on your operations and the value of the work_mem parameter. By default, it is 4 MB. If you have a sorting or hashing operation in your query (like an ORDER BY), your session will allocate those 4 MB for that operation. However, that is per operation — a single query could potentially run multiple sorting or hashing tasks, multiplying the memory allocation.&lt;/p&gt;

&lt;p&gt;Additionally, background processes like vacuuming and logical replication require memory too.&lt;/p&gt;

&lt;p&gt;I will live out some other details like temporary buffers and others, operation system processes and caches. You can check all that in the PostgreSQL &lt;a href="https://www.postgresql.org/docs/current/runtime-config-resource.html" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;. But considering all of this, the 25 max_connections limit for a db-f1-micro instance with only 600 MB of memory doesn’t look so small anymore.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6f54rjhpor9mu4j4q6e3.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6f54rjhpor9mu4j4q6e3.jpeg" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And if we look at a graph of default max_connections values relative to memory, we can see it isn’t linear. Different factors have different impacts as the instance grows in size and the number of potential connections increases.&lt;/p&gt;

&lt;h4&gt;
  
  
  Can I set up max_connections to a higher or lower value?
&lt;/h4&gt;

&lt;p&gt;Yes, you can, but you must consider all factors regarding potential memory allocation. If you run out of available memory, your connection will be terminated by an Out of Memory (OOM) error. Additionally, keep in mind that once you set max_connections as a database flag, it will no longer change dynamically based on the instance size; you will need to update the value manually if you decide to resize the instance.&lt;/p&gt;

&lt;h4&gt;
  
  
  How can I handle thousands of connections?
&lt;/h4&gt;

&lt;p&gt;If your application requires hundreds or thousands of concurrent connections, the best approach is not to increase the max_connections flag.&lt;/p&gt;

&lt;p&gt;Instead, I recommend using connection pooling. Cloud SQL offers Managed Connection Pooling (MCP), which is available in the Enterprise Plus edition. Alternatively, you can set up your own using tools like PgBouncer. Connection pooling solutions act as a proxy between your application and the database. They allow thousands of lightweight client connections- from sources like serverless scripts — to share a small, fixed number of heavy backend server connections. Read more about managed connection pooling in &lt;a href="https://docs.cloud.google.com/sql/docs/postgres/managed-connection-pooling?utm_campaign=CDR_0x370c34a8_default_b507520740&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;The default max_connections value is tied to the instance memory and adjusts automatically as you resize the instance — unless you have manually defined it as a database flag.&lt;br&gt;&lt;br&gt;
It’s important to remember that the default value is based on best practices and serves as guidance, not a hard limit. However, if you choose to set a custom value, you are responsible for managing it. You’ll need to remember to adjust it manually if you ever resize your instance.&lt;br&gt;&lt;br&gt;
If your application is designed to use a high volume of connections, your best bet might be to implement a connection pooling solution.&lt;/p&gt;




</description>
      <category>database</category>
      <category>googlecloudsql</category>
      <category>googlecloudplatform</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Setting up an AlloyDB Instance with a Public IP in Minutes</title>
      <dc:creator>Gleb Otochkin</dc:creator>
      <pubDate>Tue, 14 Apr 2026 18:35:08 +0000</pubDate>
      <link>https://dev.to/gleb_otochkin/setting-up-an-alloydb-instance-with-a-public-ip-in-minutes-3c4c</link>
      <guid>https://dev.to/gleb_otochkin/setting-up-an-alloydb-instance-with-a-public-ip-in-minutes-3c4c</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fab4alm0j2zilm0tmsnfu.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fab4alm0j2zilm0tmsnfu.jpeg" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Introduction
&lt;/h3&gt;

&lt;p&gt;Sometimes when you’re building a proof of concept or a quick demo, you just need a simple database backend with a public IP address. What if I told you that you can create an AlloyDB instance with a public IP without having to dive into private IP network configuration? Let me show you how to do exactly that.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using gcloud CLI
&lt;/h3&gt;

&lt;p&gt;AlloyDB is an enterprise-grade, fully PostgreSQL-compatible database with tons of &lt;a href="https://cloud.google.com/products/alloydb?utm_campaign=CDR_0x370c34a8_default_b502539762&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;unique features&lt;/a&gt;, making it a Swiss Army knife of a data backend for any kind of application. As such, it comes with only a private IP by default. This makes it more secure, but at the same time, requires additional actions at the network level. However, as I mentioned in the intro, sometimes you just want something quick and dirty to verify functionality or run a demo. So, how do you create an AlloyDB instance with a public IP enabled in a few quick steps?&lt;/p&gt;

&lt;p&gt;I am going to use a command-line approach and show you how to create the smallest possible AlloyDB instance with a public IP without configuring a private network. I am using a brand-new project with a default network.&lt;/p&gt;

&lt;p&gt;If this is a brand-new project, we still need to enable the minimum required APIs. Run the following command in Google Cloud Shell or from your Mac terminal. I am assuming you already have all the &lt;a href="https://docs.cloud.google.com/alloydb/docs/cluster-create#roles?utm_campaign=CDR_0x370c34a8_default_b502539762&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;required privileges&lt;/a&gt; in the project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud services &lt;span class="nb"&gt;enable &lt;/span&gt;alloydb.googleapis.com &lt;span class="se"&gt;\&lt;/span&gt;
                       compute.googleapis.com &lt;span class="se"&gt;\&lt;/span&gt;
                       servicenetworking.googleapis.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then run the command to create your AlloyDB cluster.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PGPASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"MyVeryStrictPassword123+"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$PGPASSWORD&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-central1
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ADBCLUSTER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;alloydb-aip-01
gcloud alloydb clusters create &lt;span class="nv"&gt;$ADBCLUSTER&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$REGION&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$PASSWORD&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--enable-private-service-connect&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Did you notice the --enable-private-service-connect parameter? This creates a &lt;a href="https://docs.cloud.google.com/vpc/docs/private-service-connect?utm_campaign=CDR_0x370c34a8_default_b502539762&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;Private Service Connect&lt;/a&gt; (PSC) enabled AlloyDB cluster. Once the cluster is created, run the following command to create the primary instance. For my tests, when they don’t require a large cache or heavy CPU power, I usually opt for the C4A &lt;a href="https://docs.cloud.google.com/alloydb/docs/choose-machine-type?utm_campaign=CDR_0x370c34a8_default_b502539762&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;machine type&lt;/a&gt; with a single CPU — it is enough to demonstrate functionality and costs less than other configurations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud alloydb instances create &lt;span class="nv"&gt;$ADBCLUSTER&lt;/span&gt;&lt;span class="nt"&gt;-pr&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--instance-type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;PRIMARY &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--machine-type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;c4a-highmem-1 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--cpu-count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--availability-type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ZONAL &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--database-flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;password.enforce_complexity&lt;span class="o"&gt;=&lt;/span&gt;on &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--assign-inbound-public-ip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ASSIGN_IPV4 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$REGION&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--cluster&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$ADBCLUSTER&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that’s it — after 5–8 minutes, you will have an AlloyDB primary instance running with only a public IP enabled. From there, you can use gcloud to connect to the instance, create a database, and run your queries. For more details on that step, take a look at one of my &lt;a href="https://dev.to/gleb_otochkin/alloydb-easy-connection-using-gcloud-54mo-temp-slug-1112489"&gt;previous posts&lt;/a&gt; where I explain how to use gcloud to connect to AlloyDB.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud beta alloydb connect &lt;span class="nv"&gt;$ADBCLUSTER&lt;/span&gt;&lt;span class="nt"&gt;-pr&lt;/span&gt; &lt;span class="nt"&gt;--cluster&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$ADBCLUSTER&lt;/span&gt; &lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$REGION&lt;/span&gt; &lt;span class="nt"&gt;--public-ip&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Using Terraform
&lt;/h3&gt;

&lt;p&gt;Of course, you can also use &lt;a href="https://registry.terraform.io/providers/hashicorp/google/latest/docs" rel="noopener noreferrer"&gt;Terraform Google provider&lt;/a&gt; to automate this process. Below is the alloydb-poc.tf configuration file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;google&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/google"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;gt;= 7.0.0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"google"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;project&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_id&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"google_alloydb_cluster"&lt;/span&gt; &lt;span class="s2"&gt;"default"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cluster_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cluster_id&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;
  &lt;span class="nx"&gt;psc_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;psc_enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;initial_user&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"postgres"&lt;/span&gt;
    &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db_password&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"google_alloydb_instance"&lt;/span&gt; &lt;span class="s2"&gt;"primary"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cluster&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;google_alloydb_cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;instance_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance_id&lt;/span&gt;
  &lt;span class="nx"&gt;instance_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PRIMARY"&lt;/span&gt;
  &lt;span class="nx"&gt;availability_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ZONAL"&lt;/span&gt;
  &lt;span class="nx"&gt;machine_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;machine_type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"c4a-highmem-1"&lt;/span&gt;
    &lt;span class="nx"&gt;cpu_count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;database_flags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"password.enforce_complexity"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"on"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;network_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;enable_public_ip&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"project_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The GCP Project ID"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"region"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The GCP Region (e.g., us-central1)"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-central1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"cluster_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The name of the AlloyDB cluster"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"alloydb-aip-01"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"instance_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The name of the primary instance"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"alloydb-aip-01-pr"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"db_password"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Password for the default postgres user (must meet complexity requirements!)"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;sensitive&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"alloydb_public_ip"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The Public IP address assigned to the AlloyDB primary instance"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;google_alloydb_instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public_ip_address&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run your Terraform deployment using the commands below, but remember to replace the YOUR_PROJECT_ID placeholder with your actual Google Cloud project ID and put your own password:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform init
terraform apply &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-var&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"project_id=YOUR_PROJECT_ID"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-var&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"db_password=MyVeryStrictPassword123+"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In just a few minutes, you’ll have an AlloyDB instance up and running with a public IP.&lt;/p&gt;

&lt;p&gt;I hope this makes your life easier and serves as a handy ‘hack’ for your daily workflow, whether you’re building a quick proof of concept or jumping into a vibe coding session. By the way, you can use this method for some codelabs like &lt;a href="https://codelabs.developers.google.com/alloydb-ai-embedding?hl=en#0&amp;amp;utm_campaign=CDR_0x370c34a8_default_b502539762&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;this one&lt;/a&gt; when you prefer to use your local machine instead of the Google Cloud shell.&lt;/p&gt;




</description>
      <category>googlecloudplatform</category>
      <category>terraform</category>
      <category>commandline</category>
      <category>alloydb</category>
    </item>
    <item>
      <title>AlloyDB easy connection using gcloud</title>
      <dc:creator>Gleb Otochkin</dc:creator>
      <pubDate>Sat, 11 Apr 2026 05:17:47 +0000</pubDate>
      <link>https://dev.to/gleb_otochkin/alloydb-easy-connection-using-gcloud-2kdi</link>
      <guid>https://dev.to/gleb_otochkin/alloydb-easy-connection-using-gcloud-2kdi</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo4gaew03o35iansii96r.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo4gaew03o35iansii96r.jpeg" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Introduction
&lt;/h3&gt;

&lt;p&gt;For those who work with Google AlloyDB for PostgreSQL on a daily basis, connectivity is likely not an issue. As a developer or administrator, you probably already have a preferred set of tools and connection methods, or you use AlloyDB Studio to run quick queries.&lt;br&gt;&lt;br&gt;
However, when starting a new project or cluster — or if you are new to AlloyDB and looking for a straightforward way to connect via the command line with proper mTLS encryption — there is a new method available. You can now connect to an AlloyDB instance using the Google Cloud SDK (gcloud). Let me introduce you to how it works.&lt;/p&gt;
&lt;h3&gt;
  
  
  Setting up
&lt;/h3&gt;

&lt;p&gt;The gcloud CLI is available out of the box on &lt;a href="https://docs.cloud.google.com/shell/docs?utm_campaign=CDR_0x370c34a8_default_b501397982&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;Google Cloud Shell&lt;/a&gt; and on Google Cloud Compute Engine VMs created with Google-provided templates. But, if you are running it from your laptop or a custom VM, you may need to install it by following the official &lt;a href="https://docs.cloud.google.com/sdk/docs/install-sdk?utm_campaign=CDR_0x370c34a8_default_b501397982&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You also need two additional components to get everything working: a PostgreSQL client and the AlloyDB Auth Proxy. If you are using Google Cloud Shell, both components come preinstalled. However, if you are using your own laptop or VM, you will need to add them manually.&lt;/p&gt;

&lt;p&gt;Let’s assume you’ve already installed the gcloud CLI. But before moving forward check the version using the versioncommand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It should return version 563 or higher.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;my-mac:~ $&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;gcloud &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="go"&gt;Google Cloud SDK 564.0.0
&lt;/span&gt;&lt;span class="c"&gt;...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the version is lower then you need to update it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud components update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The next step is to get the PostgreSQL client software. The latest PostgreSQL client software can be downloaded from the official website, or installed via a package manager for Linux or the brew utility for macOS. Detailed instructions can be found in our &lt;a href="https://docs.cloud.google.com/alloydb/docs/install-psql?utm_campaign=CDR_0x370c34a8_default_b501397982&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;. Here is how to install version 18 using the &lt;a href="https://brew.sh/" rel="noopener noreferrer"&gt;Homebrew&lt;/a&gt; utility on a Mac:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;postgresql@18
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After installing you can verify it by checking the version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;psql &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final component is the AlloyDB Auth Proxy. The gcloud CLI uses this proxy to create an mTLS-encrypted connection and establish a link to the instance. On a Mac, run the following commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://storage.googleapis.com/alloydb-auth-proxy/v1.14.2"&lt;/span&gt;
curl &lt;span class="nt"&gt;-o&lt;/span&gt; alloydb-auth-proxy &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;/alloydb-auth-proxy.darwin.arm64"&lt;/span&gt;
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x alloydb-auth-proxy
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nv"&gt;$HOME&lt;/span&gt;/bin
&lt;span class="nb"&gt;mv &lt;/span&gt;alloydb-auth-proxy &lt;span class="nv"&gt;$HOME&lt;/span&gt;/bin/
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/bin:&lt;/span&gt;&lt;span class="nv"&gt;$PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See the AlloyDB Auth proxy &lt;a href="https://docs.cloud.google.com/alloydb/docs/auth-proxy/connect?utm_campaign=CDR_0x370c34a8_default_b501397982&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; on how to install it for other platforms. By the way if your gcloud CLI cannot find the AlloyDB Auth Proxy in your system’s PATH, it will automatically provide instructions on how to install it.&lt;/p&gt;

&lt;p&gt;Once all components are installed, you can run the gcloud beta alloydb instances connect command to access your database via the mTLS encryption provided by the proxy. Additionally, if you are connecting using an AlloyDB public IP, you do not need to add your personal public IP to the authorized networks; the proxy handles this automatically. Here is how I connect to my AlloyDB instance while testing a codelab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-central1
&lt;span class="nv"&gt;CLUSTER_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;alloydb-aip-01
&lt;span class="nv"&gt;INSTANCE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;alloydb-aip-01-pr
gcloud beta alloydb connect &lt;span class="nv"&gt;$INSTANCE_NAME&lt;/span&gt; &lt;span class="nt"&gt;--cluster&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$CLUSTER_NAME&lt;/span&gt; &lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$REGION&lt;/span&gt; &lt;span class="nt"&gt;--public-ip&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you type your password for the user postgres and you are in.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;my-mac:~ $&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-central1
&lt;span class="gp"&gt;my-mac:~ $&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;CLUSTER_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;alloydb-aip-01
&lt;span class="gp"&gt;my-mac:~ $&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;INSTANCE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;alloydb-aip-01-pr
&lt;span class="gp"&gt;my-mac:~ $&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;gcloud beta alloydb connect &lt;span class="nv"&gt;$INSTANCE_NAME&lt;/span&gt; &lt;span class="nt"&gt;--cluster&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$CLUSTER_NAME&lt;/span&gt; &lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$REGION&lt;/span&gt; &lt;span class="nt"&gt;--public-ip&lt;/span&gt;
&lt;span class="go"&gt;Starting the AlloyDB Auth Proxy...
Running command:
 alloydb-auth-proxy projects/gleb-genai-002/locations/us-central1/clusters/alloydb-aip-01/instances/alloydb-aip-01-pr --port 9471 --public-ip

Connecting to the AlloyDB Auth Proxy...
Running command:
 psql -h 127.0.0.1 -p 9471 -U postgres -d postgres
Password for user postgres:
psql (18.0 (Postgres.app), server 16.11)
Type "help" for help.

&lt;/span&gt;&lt;span class="gp"&gt;postgres=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few final notes: You must be authenticated with an account that has the proper permissions to connect to AlloyDB instances, specifically the roles/alloydb.cloent and roless/serviceUsageConsumer roles. Additionally, if you want to use IAM authentication, ensure it is enabled on your AlloyDB cluster, that you have the roles/alloydb.databaseUser role, and that you have created the IAM user within the cluster itself.&lt;/p&gt;

&lt;p&gt;As of the time of writing, this feature is still in Preview. And once more — you may need to update your Google Cloud SDK to version 563.0.0 or higher. For more details on available flags and configurations, refer to the official &lt;a href="https://docs.cloud.google.com/alloydb/docs/connect-gcloud?utm_campaign=CDR_0x370c34a8_default_b501397982&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; regarding connecting via the gcloud CLI.&lt;/p&gt;

&lt;p&gt;Happy testing! You can try it with some of our latest AlloyDB &lt;a href="https://codelabs.developers.google.com/?product=alloydbforpostgresql&amp;amp;utm_campaign=CDR_0x370c34a8_default_b501397982&amp;amp;utm_medium=external&amp;amp;utm_source=blog" rel="noopener noreferrer"&gt;codelabs&lt;/a&gt;.&lt;/p&gt;




</description>
      <category>alloydb</category>
      <category>commandline</category>
      <category>googlecloudplatform</category>
      <category>data</category>
    </item>
  </channel>
</rss>
