<?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>Vibe Coding Forem: Anthony Lee</title>
    <description>The latest articles on Vibe Coding Forem by Anthony Lee (@anthony_lee_63e96408d7573).</description>
    <link>https://vibe.forem.com/anthony_lee_63e96408d7573</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3605723%2F08a5ac12-368f-4e52-ab95-1f55ec66e4ea.png</url>
      <title>Vibe Coding Forem: Anthony Lee</title>
      <link>https://vibe.forem.com/anthony_lee_63e96408d7573</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://vibe.forem.com/feed/anthony_lee_63e96408d7573"/>
    <language>en</language>
    <item>
      <title>How to Set Up Google Analytics as a Claude Code Skill</title>
      <dc:creator>Anthony Lee</dc:creator>
      <pubDate>Wed, 11 Feb 2026 11:43:31 +0000</pubDate>
      <link>https://vibe.forem.com/anthony_lee_63e96408d7573/how-to-set-up-google-analytics-as-a-claude-code-skill-1</link>
      <guid>https://vibe.forem.com/anthony_lee_63e96408d7573/how-to-set-up-google-analytics-as-a-claude-code-skill-1</guid>
      <description>&lt;p&gt;A step-by-step guide for connecting your Google Analytics data to Claude Code, so you can ask questions about your website traffic in plain English.&lt;/p&gt;




&lt;h2&gt;
  
  
  What You'll End Up With
&lt;/h2&gt;

&lt;p&gt;Once this is set up, you'll be able to open Claude Code (in VS Code or the terminal) and ask things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How much traffic did my site get this week?"&lt;/li&gt;
&lt;li&gt;"What are my top pages in the last 30 days?"&lt;/li&gt;
&lt;li&gt;"Where is my traffic coming from?"&lt;/li&gt;
&lt;li&gt;"Show me a daily breakdown of visitors this month"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Claude will run a script behind the scenes, pull your real Google Analytics data, and give you the answer.&lt;/p&gt;




&lt;h2&gt;
  
  
  What You'll Need Before Starting
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A Google Analytics 4 (GA4) property (the newer version of Google Analytics)&lt;/li&gt;
&lt;li&gt;A Google account with access to that GA4 property&lt;/li&gt;
&lt;li&gt;Claude Code installed (either the VS Code extension or the CLI)&lt;/li&gt;
&lt;li&gt;Python installed on your computer&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Part 1: Set Up Google Cloud
&lt;/h2&gt;

&lt;p&gt;Google Analytics data is accessed through Google's cloud platform. You need to create a "service account" - think of it as a robot employee that has read-only access to your analytics data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Go to Google Cloud Console
&lt;/h3&gt;

&lt;p&gt;Open your browser and go to: &lt;strong&gt;&lt;a href="https://console.cloud.google.com" rel="noopener noreferrer"&gt;https://console.cloud.google.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sign in with the same Google account that has access to your Google Analytics.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Create a Project (or select an existing one)
&lt;/h3&gt;

&lt;p&gt;If you've never used Google Cloud before:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click the project dropdown at the top of the page (it might say "Select a project")&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;New Project&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Give it a name (something like "My Analytics" or your business name)&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Wait a moment, then select your new project from the dropdown&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you already have a project, just make sure it's selected.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Enable the Google Analytics API
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;In the search bar at the top of Google Cloud Console, type: &lt;strong&gt;Google Analytics Data API&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click on &lt;strong&gt;Google Analytics Data API&lt;/strong&gt; in the results&lt;/li&gt;
&lt;li&gt;Click the big blue &lt;strong&gt;Enable&lt;/strong&gt; button&lt;/li&gt;
&lt;li&gt;Wait for it to activate (takes a few seconds)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 4: Create a Service Account
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;In the left sidebar, click &lt;strong&gt;IAM &amp;amp; Admin&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Service Accounts&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;+ Create Service Account&lt;/strong&gt; at the top&lt;/li&gt;
&lt;li&gt;Fill in the details:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Service account name&lt;/strong&gt;: &lt;code&gt;claude-ga4-reader&lt;/code&gt; (or whatever you like)&lt;/li&gt;
&lt;li&gt;The email will auto-generate - it will look something like: &lt;code&gt;claude-ga4-reader@your-project.iam.gserviceaccount.com&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create and Continue&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Skip the "Grant this service account access" step - click &lt;strong&gt;Continue&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Skip the "Grant users access" step - click &lt;strong&gt;Done&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 5: Download the Credentials Key
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;You should now see your new service account in the list - click on it&lt;/li&gt;
&lt;li&gt;Go to the &lt;strong&gt;Keys&lt;/strong&gt; tab&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add Key&lt;/strong&gt; → &lt;strong&gt;Create new key&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;JSON&lt;/strong&gt; and click &lt;strong&gt;Create&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;.json&lt;/code&gt; file will download to your computer - &lt;strong&gt;this is important, don't lose it&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Move this file somewhere safe and permanent on your computer (for example: &lt;code&gt;C:\Users\YourName\keys\&lt;/code&gt; or your Desktop)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Write down the full file path&lt;/strong&gt; - you'll need it later. It will look something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C:\Users\YourName\Downloads\your-project-name-abc123.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also &lt;strong&gt;write down the service account email&lt;/strong&gt; from step 4 - you'll need it in the next section.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2: Give the Service Account Access to Your Analytics
&lt;/h2&gt;

&lt;p&gt;The service account exists, but it doesn't have permission to see your analytics data yet. You need to invite it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Open Google Analytics
&lt;/h3&gt;

&lt;p&gt;Go to: &lt;strong&gt;&lt;a href="https://analytics.google.com" rel="noopener noreferrer"&gt;https://analytics.google.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7: Add the Service Account as a Viewer
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Click the &lt;strong&gt;gear icon&lt;/strong&gt; (⚙️) in the bottom-left corner to open Admin&lt;/li&gt;
&lt;li&gt;In the &lt;strong&gt;Property&lt;/strong&gt; column (the middle column), click &lt;strong&gt;Property Access Management&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;+&lt;/strong&gt; button in the top-right → &lt;strong&gt;Add users&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;In the email field, paste your &lt;strong&gt;service account email&lt;/strong&gt; (the one that looks like &lt;code&gt;claude-ga4-reader@your-project.iam.gserviceaccount.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Set the role to &lt;strong&gt;Viewer&lt;/strong&gt; (this is read-only — it can't change anything)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uncheck&lt;/strong&gt; "Notify new users by email" (it's not a real email address)&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 8: Find Your GA4 Property ID
&lt;/h3&gt;

&lt;p&gt;While you're still in the Admin area:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the &lt;strong&gt;Property&lt;/strong&gt; column, click &lt;strong&gt;Property Settings&lt;/strong&gt; (or &lt;strong&gt;Property details&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;Look for &lt;strong&gt;Property ID&lt;/strong&gt; - it's a number like &lt;code&gt;363186564&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write this number down&lt;/strong&gt; - you'll need it soon&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Part 3: Install the Python Package
&lt;/h2&gt;

&lt;p&gt;The skill uses a Python library to talk to Google's servers. You need to install it once.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 9: Open a Terminal
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On Windows&lt;/strong&gt;: Press &lt;code&gt;Win + R&lt;/code&gt;, type &lt;code&gt;cmd&lt;/code&gt;, press Enter&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On Mac&lt;/strong&gt;: Open the Terminal app (search for "Terminal" in Spotlight)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 10: Install the Package
&lt;/h3&gt;

&lt;p&gt;Type this command and press Enter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pip install google-analytics-data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait for it to finish. You should see "Successfully installed" at the end.&lt;/p&gt;

&lt;p&gt;If you get an error about &lt;code&gt;pip&lt;/code&gt; not being found, try:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pip3 install google-analytics-data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 4: Create the Skill Files
&lt;/h2&gt;

&lt;p&gt;A Claude Code skill is just a folder with specific files in it. You need three files.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 11: Create the Skill Folder
&lt;/h3&gt;

&lt;p&gt;Navigate to your Claude configuration folder and create the skill directory:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On Windows&lt;/strong&gt;: &lt;code&gt;C:\Users\YourName\.claude\skills\google-analytics\&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On Mac/Linux&lt;/strong&gt;: &lt;code&gt;~/.claude/skills/google-analytics/&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the &lt;code&gt;skills&lt;/code&gt; folder doesn't exist yet, create it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 12: Create SKILL.md
&lt;/h3&gt;

&lt;p&gt;Inside the &lt;code&gt;google-analytics&lt;/code&gt; folder, create a file called &lt;code&gt;SKILL.md&lt;/code&gt; and paste the following content. This file tells Claude what the skill does and how to use it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;google-analytics&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Query Google Analytics 4 data. Use when the user asks about website traffic, page views, sessions, user counts, conversions, top pages, traffic sources, or any analytics/metrics questions. Trigger on keywords like "analytics", "traffic", "visitors", "page views", "sessions", "GA4", "bounce rate", "conversions", "top pages", "referrals".&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Google Analytics 4 Skill&lt;/span&gt;

Query GA4 property data using the Google Analytics Data API v1.

&lt;span class="gu"&gt;## Setup&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="gs"&gt;**Credentials**&lt;/span&gt;: Service account JSON key at &lt;span class="sb"&gt;`YOUR_CREDENTIALS_PATH_HERE`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Property ID**&lt;/span&gt;: &lt;span class="sb"&gt;`YOUR_PROPERTY_ID_HERE`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Python dependency**&lt;/span&gt;: &lt;span class="sb"&gt;`google-analytics-data`&lt;/span&gt; (install if needed: &lt;span class="sb"&gt;`pip install google-analytics-data`&lt;/span&gt;)

&lt;span class="gu"&gt;## How to Use&lt;/span&gt;

Run the query script from this skill's directory:

&lt;span class="p"&gt;~~~&lt;/span&gt;&lt;span class="nl"&gt;bash
&lt;/span&gt;python PATH_TO_SKILL/ga_query.py &lt;span class="nt"&gt;--report&lt;/span&gt; &amp;lt;report_type&amp;gt; &lt;span class="o"&gt;[&lt;/span&gt;options]
&lt;span class="p"&gt;~~~&lt;/span&gt;

&lt;span class="gu"&gt;## Available Reports&lt;/span&gt;

&lt;span class="gu"&gt;### 1. `overview` - High-level summary&lt;/span&gt;
&lt;span class="p"&gt;~~~&lt;/span&gt;&lt;span class="nl"&gt;bash
&lt;/span&gt;python ga_query.py &lt;span class="nt"&gt;--report&lt;/span&gt; overview &lt;span class="nt"&gt;--days&lt;/span&gt; 30
&lt;span class="p"&gt;~~~&lt;/span&gt;
Returns: total users, sessions, page views, avg session duration, bounce rate, new vs returning users.

&lt;span class="gu"&gt;### 2. `pages` - Top pages by views&lt;/span&gt;
&lt;span class="p"&gt;~~~&lt;/span&gt;&lt;span class="nl"&gt;bash
&lt;/span&gt;python ga_query.py &lt;span class="nt"&gt;--report&lt;/span&gt; pages &lt;span class="nt"&gt;--days&lt;/span&gt; 30 &lt;span class="nt"&gt;--limit&lt;/span&gt; 20
&lt;span class="p"&gt;~~~&lt;/span&gt;
Returns: page path, title, views, users, avg engagement time.

&lt;span class="gu"&gt;### 3. `sources` - Traffic sources&lt;/span&gt;
&lt;span class="p"&gt;~~~&lt;/span&gt;&lt;span class="nl"&gt;bash
&lt;/span&gt;python ga_query.py &lt;span class="nt"&gt;--report&lt;/span&gt; sources &lt;span class="nt"&gt;--days&lt;/span&gt; 30 &lt;span class="nt"&gt;--limit&lt;/span&gt; 20
&lt;span class="p"&gt;~~~&lt;/span&gt;
Returns: source, medium, sessions, users, conversions.

&lt;span class="gu"&gt;### 4. `countries` - Geographic breakdown&lt;/span&gt;
&lt;span class="p"&gt;~~~&lt;/span&gt;&lt;span class="nl"&gt;bash
&lt;/span&gt;python ga_query.py &lt;span class="nt"&gt;--report&lt;/span&gt; countries &lt;span class="nt"&gt;--days&lt;/span&gt; 30 &lt;span class="nt"&gt;--limit&lt;/span&gt; 20
&lt;span class="p"&gt;~~~&lt;/span&gt;
Returns: country, sessions, users, engagement rate.

&lt;span class="gu"&gt;### 5. `devices` - Device category breakdown&lt;/span&gt;
&lt;span class="p"&gt;~~~&lt;/span&gt;&lt;span class="nl"&gt;bash
&lt;/span&gt;python ga_query.py &lt;span class="nt"&gt;--report&lt;/span&gt; devices &lt;span class="nt"&gt;--days&lt;/span&gt; 30
&lt;span class="p"&gt;~~~&lt;/span&gt;
Returns: device category (desktop/mobile/tablet), sessions, users.

&lt;span class="gu"&gt;### 6. `daily` - Day-by-day trend&lt;/span&gt;
&lt;span class="p"&gt;~~~&lt;/span&gt;&lt;span class="nl"&gt;bash
&lt;/span&gt;python ga_query.py &lt;span class="nt"&gt;--report&lt;/span&gt; daily &lt;span class="nt"&gt;--days&lt;/span&gt; 30
&lt;span class="p"&gt;~~~&lt;/span&gt;
Returns: date, users, sessions, page views per day.

&lt;span class="gu"&gt;### 7. `realtime` - Active users right now&lt;/span&gt;
&lt;span class="p"&gt;~~~&lt;/span&gt;&lt;span class="nl"&gt;bash
&lt;/span&gt;python ga_query.py &lt;span class="nt"&gt;--report&lt;/span&gt; realtime
&lt;span class="p"&gt;~~~&lt;/span&gt;
Returns: active users in last 30 minutes by source.

&lt;span class="gu"&gt;### 8. `custom` - Custom query (advanced)&lt;/span&gt;
&lt;span class="p"&gt;~~~&lt;/span&gt;&lt;span class="nl"&gt;bash
&lt;/span&gt;python ga_query.py &lt;span class="nt"&gt;--report&lt;/span&gt; custom &lt;span class="nt"&gt;--metrics&lt;/span&gt; &lt;span class="s2"&gt;"sessions,totalUsers"&lt;/span&gt; &lt;span class="nt"&gt;--dimensions&lt;/span&gt; &lt;span class="s2"&gt;"city"&lt;/span&gt; &lt;span class="nt"&gt;--days&lt;/span&gt; 7 &lt;span class="nt"&gt;--limit&lt;/span&gt; 10
&lt;span class="p"&gt;~~~&lt;/span&gt;
Pass any valid GA4 API metric/dimension names as comma-separated values.

&lt;span class="gu"&gt;## Common Options&lt;/span&gt;

| Option | Default | Description |
|--------|---------|-------------|
| &lt;span class="sb"&gt;`--days`&lt;/span&gt; | &lt;span class="sb"&gt;`30`&lt;/span&gt; | Lookback period in days |
| &lt;span class="sb"&gt;`--limit`&lt;/span&gt; | &lt;span class="sb"&gt;`10`&lt;/span&gt; | Max rows returned |
| &lt;span class="sb"&gt;`--start`&lt;/span&gt; | — | Explicit start date (YYYY-MM-DD), overrides --days |
| &lt;span class="sb"&gt;`--end`&lt;/span&gt; | — | Explicit end date (YYYY-MM-DD), defaults to today |
| &lt;span class="sb"&gt;`--output`&lt;/span&gt; | &lt;span class="sb"&gt;`table`&lt;/span&gt; | Output format: &lt;span class="sb"&gt;`table`&lt;/span&gt;, &lt;span class="sb"&gt;`json`&lt;/span&gt;, or &lt;span class="sb"&gt;`csv`&lt;/span&gt; |

&lt;span class="gu"&gt;## GA4 Metric and Dimension Reference (for custom queries)&lt;/span&gt;

&lt;span class="gs"&gt;**Common Metrics**&lt;/span&gt;: totalUsers, newUsers, sessions, screenPageViews, averageSessionDuration, bounceRate, engagementRate, conversions, eventCount, activeUsers

&lt;span class="gs"&gt;**Common Dimensions**&lt;/span&gt;: date, pagePath, pageTitle, sessionSource, sessionMedium, country, city, deviceCategory, browser, operatingSystem, landingPage, sessionDefaultChannelGroup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;IMPORTANT - Replace the placeholders:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replace &lt;code&gt;YOUR_CREDENTIALS_PATH_HERE&lt;/code&gt; with the full path to your JSON key file from Step 5&lt;/li&gt;
&lt;li&gt;Replace &lt;code&gt;YOUR_PROPERTY_ID_HERE&lt;/code&gt; with your GA4 Property ID from Step 8&lt;/li&gt;
&lt;li&gt;Replace &lt;code&gt;PATH_TO_SKILL&lt;/code&gt; with the full path to your skill folder&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 13: Create ga_query.py
&lt;/h3&gt;

&lt;p&gt;In the same &lt;code&gt;google-analytics&lt;/code&gt; folder, create a file called &lt;code&gt;ga_query.py&lt;/code&gt; and paste the following Python script.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before pasting&lt;/strong&gt;, you need to update two values at the top of the file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CREDENTIALS_PATH&lt;/code&gt; - the full path to your JSON key file from Step 5&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PROPERTY_ID&lt;/code&gt; - your GA4 Property ID from Step 8
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
Google Analytics 4 Data API query tool.
Queries GA4 property data using a service account.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;

&lt;span class="c1"&gt;# ============================================================
# CONFIGURATION - UPDATE THESE TWO VALUES
# ============================================================
&lt;/span&gt;&lt;span class="n"&gt;CREDENTIALS_PATH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;C:\Users\YourName\path\to\your-credentials.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;PROPERTY_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;000000000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="c1"&gt;# ============================================================
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_client&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Create and return a GA4 BetaAnalyticsDataClient.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GOOGLE_APPLICATION_CREDENTIALS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CREDENTIALS_PATH&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.analytics.data_v1beta&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BetaAnalyticsDataClient&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;BetaAnalyticsDataClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metrics&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="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&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;order_by_metric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Build a RunReportRequest.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.analytics.data_v1beta.types&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;RunReportRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Metric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dimension&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DateRange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderBy&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;end_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d&lt;/span&gt;&lt;span class="sh"&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;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;start_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;start_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RunReportRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;property&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PROPERTY_ID&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Dimension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dimensions&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;dimensions&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
        &lt;span class="n"&gt;date_ranges&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;DateRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;start_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;end_date&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
        &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&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;order_by_metric&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_bys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="nc"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MetricOrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metric_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;order_by_metric&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;desc&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;return&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;format_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Format the API response into the desired output format.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;h&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;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dimension_headers&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;h&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;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metric_headers&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;dv&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dimension_values&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;mv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;mv&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metric_values&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&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;output&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&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&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&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="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# table
&lt;/span&gt;        &lt;span class="n"&gt;col_widths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;for&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;val&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;col_widths&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="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;col_widths&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="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;

        &lt;span class="n"&gt;separator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&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;w&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;col_widths&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;header_row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;col_widths&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="si"&gt;}}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&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;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

        &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;separator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;header_row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;separator&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;col_widths&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="si"&gt;}}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&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;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;separator&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;row_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Total rows: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;row_count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;report_overview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;High-level site overview.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;totalUsers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;newUsers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sessions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;screenPageViews&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;averageSessionDuration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;engagementRate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bounceRate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
        &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;format_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;report_pages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Top pages by views.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;screenPageViews&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;totalUsers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;averageSessionDuration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pagePath&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pageTitle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;order_by_metric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;screenPageViews&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;format_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;report_sources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Traffic sources.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sessions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;totalUsers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;engagementRate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sessionSource&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sessionMedium&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;order_by_metric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sessions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;format_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;report_countries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Geographic breakdown.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sessions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;totalUsers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;engagementRate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;country&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;order_by_metric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sessions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;format_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;report_devices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Device category breakdown.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sessions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;totalUsers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;engagementRate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deviceCategory&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;order_by_metric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sessions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;format_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;report_daily&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Day-by-day trend.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;totalUsers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sessions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;screenPageViews&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.analytics.data_v1beta.types&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OrderBy&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_bys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dimension&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DimensionOrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dimension_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;format_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;report_realtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Realtime active users.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.analytics.data_v1beta.types&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;RunRealtimeReportRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Metric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dimension&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RunRealtimeReportRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;property&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PROPERTY_ID&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;activeUsers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
        &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Dimension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unifiedScreenName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
        &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_realtime_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;h&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;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dimension_headers&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;h&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;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metric_headers&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;dv&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dimension_values&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;mv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;mv&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metric_values&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No active users right now.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&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&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&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="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;col_widths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;for&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;val&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;col_widths&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="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;col_widths&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="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="n"&gt;separator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&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;w&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;col_widths&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;header_row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;col_widths&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="si"&gt;}}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&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;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;separator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;header_row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;separator&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;col_widths&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="si"&gt;}}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&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;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;separator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;report_custom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Custom query with user-specified metrics and dimensions.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error: --metrics required for custom report (comma-separated)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;metrics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;dimensions&lt;/span&gt; &lt;span class="o"&gt;=&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="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;args&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="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&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;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dimensions&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;order_by_metric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;metrics&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="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;format_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;REPORTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;overview&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;report_overview&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;report_pages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sources&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;report_sources&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;countries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;report_countries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;devices&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;report_devices&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;daily&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;report_daily&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;realtime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;report_realtime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;custom&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;report_custom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Query Google Analytics 4 data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--report&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;REPORTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Report type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--days&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&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;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Lookback period in days (default: 30)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Start date (YYYY-MM-DD), overrides --days&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--end&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;End date (YYYY-MM-DD), defaults to today&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&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;default&lt;/span&gt;&lt;span class="o"&gt;=&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;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Max rows (default: 10)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Output format&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--metrics&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Comma-separated metrics (for custom report)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--dimensions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Comma-separated dimensions (for custom report)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;REPORTS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&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;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 14: Create requirements.txt
&lt;/h3&gt;

&lt;p&gt;In the same folder, create a file called &lt;code&gt;requirements.txt&lt;/code&gt; with just this one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;google-analytics-data&amp;gt;=0.18.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This file is just for reference - it documents what Python package the skill needs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 5: Verify Your Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 15: Check Your Folder Structure
&lt;/h3&gt;

&lt;p&gt;Your skill folder should now look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.claude/
  skills/
    google-analytics/
      SKILL.md
      ga_query.py
      requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Windows, the full path would be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C:\Users\YourName\.claude\skills\google-analytics\SKILL.md
C:\Users\YourName\.claude\skills\google-analytics\ga_query.py
C:\Users\YourName\.claude\skills\google-analytics\requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 16: Test the Script Manually (Optional but Recommended)
&lt;/h3&gt;

&lt;p&gt;Before trying it in Claude Code, you can test the script directly to make sure everything is connected:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open a terminal&lt;/li&gt;
&lt;li&gt;Navigate to the skill folder:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   cd C:\Users\YourName\.claude\skills\google-analytics
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Run:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   python ga_query.py --report overview --days 7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If everything is set up correctly, you should see a table with your analytics data. If you get an error, check that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The credentials path in &lt;code&gt;ga_query.py&lt;/code&gt; is correct&lt;/li&gt;
&lt;li&gt;The property ID is correct&lt;/li&gt;
&lt;li&gt;You added the service account email as a Viewer in Google Analytics (Step 7)&lt;/li&gt;
&lt;li&gt;You installed the Python package (Step 10)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 17: Test in Claude Code
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open Claude Code (restart it if it was already running)&lt;/li&gt;
&lt;li&gt;Ask something like: &lt;strong&gt;"How much traffic did my site get in the last 7 days?"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Claude should recognize the analytics-related question, load the skill, and run the script&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If Claude doesn't pick it up automatically, try being explicit: &lt;strong&gt;"Use the google-analytics skill to show me a traffic overview for the last 30 days."&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"ModuleNotFoundError: No module named 'google'"&lt;/strong&gt;&lt;br&gt;
→ The Python package isn't installed. Run: &lt;code&gt;pip install google-analytics-data&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Permission denied" or "403" errors&lt;/strong&gt;&lt;br&gt;
→ The service account doesn't have access to your GA4 property. Go back to Step 7 and make sure you added the service account email as a Viewer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"File not found" error for credentials&lt;/strong&gt;&lt;br&gt;
→ The path to your JSON key file is wrong in &lt;code&gt;ga_query.py&lt;/code&gt;. Double-check the &lt;code&gt;CREDENTIALS_PATH&lt;/code&gt; value. On Windows, use a raw string: &lt;code&gt;r"C:\Users\..."&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"API not enabled" error&lt;/strong&gt;&lt;br&gt;
→ The Google Analytics Data API isn't turned on. Go back to Step 3.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude Code doesn't use the skill&lt;/strong&gt;&lt;br&gt;
→ Try invoking it directly with &lt;code&gt;/google-analytics&lt;/code&gt;. If that doesn't work, add this line to your global CLAUDE.md file (at &lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt;): "When asked about analytics or website traffic, use the google-analytics skill."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Property not found" error&lt;/strong&gt;&lt;br&gt;
→ Double-check your Property ID. It should be just the number (like &lt;code&gt;363186564&lt;/code&gt;), not the full "properties/363186564" string.&lt;/p&gt;




&lt;h2&gt;
  
  
  Important Notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;This skill only works in &lt;strong&gt;local&lt;/strong&gt; Claude Code (VS Code extension or terminal CLI). It does &lt;strong&gt;not&lt;/strong&gt; work in the browser-based Claude Code on claude.ai, because that runs in a cloud sandbox without access to your local files.&lt;/li&gt;
&lt;li&gt;The service account has &lt;strong&gt;read-only&lt;/strong&gt; access — it cannot modify your Google Analytics settings.&lt;/li&gt;
&lt;li&gt;Keep your credentials JSON file safe. Don't share it or commit it to a public code repository.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;pip install&lt;/code&gt; only needs to be done once. The package stays installed on your computer.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>python</category>
      <category>googleanalytics</category>
    </item>
    <item>
      <title>Context Mesh Lite: Hybrid Vector Search + SQL Search + Graph Search Fused (for Super Accurate RAG)</title>
      <dc:creator>Anthony Lee</dc:creator>
      <pubDate>Tue, 23 Dec 2025 23:39:10 +0000</pubDate>
      <link>https://vibe.forem.com/anthony_lee_63e96408d7573/context-mesh-lite-hybrid-vector-search-sql-search-graph-search-fused-for-super-accurate-rag-25kn</link>
      <guid>https://vibe.forem.com/anthony_lee_63e96408d7573/context-mesh-lite-hybrid-vector-search-sql-search-graph-search-fused-for-super-accurate-rag-25kn</guid>
      <description>&lt;p&gt;I spent WAYYY too long trying to build a more accurate RAG retrieval system.&lt;br&gt;&lt;br&gt;
With Context Mesh Lite, I managed to combine hybrid vector search with SQL search (agentic text-to-sql) with graph search (shallow graph using dependent tables).&lt;/p&gt;

&lt;p&gt;The results were a significantly more accurate (albeit slower) RAG system.&lt;/p&gt;

&lt;p&gt;How does it work?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SQL Functions do most of the heavy lifting, creating tables and table dependencies.&lt;/li&gt;
&lt;li&gt;Then Edge Functions call Gemini (embeddings 001 and 2.5 flash) to create vector embeddings and graph entity/predicate extraction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;REQUIREMENTS: This system was built to exist within a Supabase instance. It also requires a Gemini API key (set in your Edge Functions window).&lt;/p&gt;

&lt;p&gt;I also connected the system to n8n workflows and it works like a charm. Anyway, I'm gonna give it to you. Maybe it'll be useful. Maybe you can improve on it.&lt;/p&gt;

&lt;p&gt;So, first, go to your Supabase (the entire end-to-end system exists there...only the interface for document upsert and chat are external).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1&lt;/strong&gt;. Go to the SQL editor and paste this master query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
    -- ===============================================================
    -- CONTEXT MESH V9.0: GOLDEN MASTER (COMPOSITE RETRIEVAL)
    -- UPDATED: Nov 25, 2025
    -- ===============================================================
    -- PART 1:  EXTENSIONS
    -- PART 2:  STORAGE CONFIGURATION
    -- PART 3:  CORE TABLES (Docs, Graph, Queue, Config, Registry)
    -- PART 4:  INDEXES
    -- PART 5:  HELPER FUNCTIONS &amp;amp; TRIGGERS
    -- PART 6:  INGESTION FUNCTIONS (Universal V8 Logic)
    -- PART 7:  SEARCH FUNCTIONS (V9: FTS, Enrichment, Detection, Peek)
    -- PART 8:  CONFIGURATION LOGIC (V9 Weights)
    -- PART 9:  GRAPH CONTEXT RERANKER
    -- PART 10: WORKER SETUP
    -- PART 11: PERMISSIONS &amp;amp; REPAIRS
    -- ===============================================================


    -- ===============================================================
    -- PART 1: EXTENSIONS
    -- ===============================================================
    CREATE EXTENSION IF NOT EXISTS vector;
    CREATE EXTENSION IF NOT EXISTS pg_trgm;
    CREATE EXTENSION IF NOT EXISTS pg_net; -- Required for Async Worker


    -- ===============================================================
    -- PART 2: STORAGE CONFIGURATION
    -- ===============================================================
    INSERT INTO storage.buckets (id, name, public)
    VALUES ('raw_uploads', 'raw_uploads', false)
    ON CONFLICT (id) DO NOTHING;


    DROP POLICY IF EXISTS "Service Role Full Access" ON storage.objects;


    CREATE POLICY "Service Role Full Access"
    ON storage.objects FOR ALL
    USING ( auth.role() = 'service_role' )
    WITH CHECK ( auth.role() = 'service_role' );


    -- ===============================================================
    -- PART 3: CORE TABLES
    -- ===============================================================


    -- 3a. The Async Queue
    CREATE TABLE IF NOT EXISTS public.ingestion_queue (
        id uuid default gen_random_uuid() primary key,
        uri text not null,
        title text not null,
        chunk_index int not null,
        chunk_text text not null,
        status text default 'pending',
        error_log text,
        created_at timestamptz default now()
    );


    -- 3b. RAG Tables
    CREATE TABLE IF NOT EXISTS public.document (
        id         BIGSERIAL PRIMARY KEY,
        uri        TEXT NOT NULL UNIQUE,
        title      TEXT NOT NULL,
        doc_type   TEXT NOT NULL DEFAULT 'document',
        meta       JSONB NOT NULL DEFAULT '{}'::jsonb,
        created_at TIMESTAMPTZ NOT NULL DEFAULT now()
    );


    CREATE TABLE IF NOT EXISTS public.chunk (
        id          BIGSERIAL PRIMARY KEY,
        document_id BIGINT NOT NULL REFERENCES public.document(id) ON DELETE CASCADE,
        ordinal     INT    NOT NULL,
        text        TEXT   NOT NULL,
        tsv         TSVECTOR,
        UNIQUE (document_id, ordinal)
    );


    -- PERFORMANCE: Using halfvec(768) for Gemini embeddings
    CREATE TABLE IF NOT EXISTS public.chunk_embedding (
        chunk_id  BIGINT PRIMARY KEY REFERENCES public.chunk(id) ON DELETE CASCADE,
        embedding halfvec(768) NOT NULL 
    );


    -- 3c. Graph Tables
    CREATE TABLE IF NOT EXISTS public.node (
        id     BIGSERIAL PRIMARY KEY,
        key    TEXT UNIQUE NOT NULL,
        labels TEXT[] NOT NULL DEFAULT '{}',
        props  JSONB  NOT NULL DEFAULT '{}'::jsonb
    );


    CREATE TABLE IF NOT EXISTS public.edge (
        src   BIGINT NOT NULL REFERENCES public.node(id) ON DELETE CASCADE,
        dst   BIGINT NOT NULL REFERENCES public.node(id) ON DELETE CASCADE,
        type  TEXT   NOT NULL,
        props JSONB  NOT NULL DEFAULT '{}'::jsonb,
        PRIMARY KEY (src, dst, type)
    );


    CREATE TABLE IF NOT EXISTS public.chunk_node (
        chunk_id BIGINT NOT NULL REFERENCES public.chunk(id) ON DELETE CASCADE,
        node_id  BIGINT NOT NULL REFERENCES public.node(id)  ON DELETE CASCADE,
        rel      TEXT   NOT NULL DEFAULT 'MENTIONS',
        PRIMARY KEY (chunk_id, node_id, rel)
    );


    -- 3d. Structured Data Registry (V8 Updated)
    CREATE TABLE IF NOT EXISTS public.structured_table (
        id            BIGSERIAL PRIMARY KEY,
        table_name    TEXT NOT NULL UNIQUE,
        document_id   BIGINT REFERENCES public.document(id) ON DELETE CASCADE,
        schema_def    JSONB NOT NULL,
        row_count     INT DEFAULT 0,
        -- V8 Metadata Columns
        description   TEXT,
        column_semantics JSONB DEFAULT '{}'::jsonb,
        graph_hints   JSONB DEFAULT '[]'::jsonb,
        sample_row    JSONB,
        created_at    TIMESTAMPTZ NOT NULL DEFAULT now(),
        updated_at    TIMESTAMPTZ NOT NULL DEFAULT now()
    );


    -- 3e. Configuration Table
    CREATE TABLE IF NOT EXISTS public.app_config (
        id INT PRIMARY KEY DEFAULT 1,
        settings JSONB NOT NULL,
        updated_at TIMESTAMPTZ DEFAULT now(),
        CONSTRAINT single_row CHECK (id = 1)
    );


    -- ===============================================================
    -- PART 4: INDEXES
    -- ===============================================================
    CREATE INDEX IF NOT EXISTS idx_queue_status ON public.ingestion_queue(status);
    CREATE INDEX IF NOT EXISTS document_type_idx ON public.document(doc_type);
    CREATE INDEX IF NOT EXISTS document_uri_idx ON public.document(uri);
    CREATE INDEX IF NOT EXISTS chunk_tsv_gin ON public.chunk USING GIN (tsv);
    CREATE INDEX IF NOT EXISTS chunk_doc_idx ON public.chunk(document_id);


    -- Embedding Index (HNSW)
    CREATE INDEX IF NOT EXISTS emb_hnsw_cos ON public.chunk_embedding USING HNSW (embedding halfvec_cosine_ops);


    -- Graph Indexes
    CREATE INDEX IF NOT EXISTS edge_src_idx ON public.edge (src);
    CREATE INDEX IF NOT EXISTS edge_dst_idx ON public.edge (dst);
    CREATE INDEX IF NOT EXISTS node_labels_gin ON public.node USING GIN (labels);
    CREATE INDEX IF NOT EXISTS node_props_gin ON public.node USING GIN (props);
    CREATE INDEX IF NOT EXISTS chunknode_node_idx ON public.chunk_node (node_id);
    CREATE INDEX IF NOT EXISTS chunknode_chunk_idx ON public.chunk_node (chunk_id);


    -- Registry Index
    CREATE INDEX IF NOT EXISTS idx_structured_table_active ON public.structured_table(table_name) WHERE row_count &amp;gt; 0;


    -- ===============================================================
    -- PART 5: HELPER FUNCTIONS &amp;amp; TRIGGERS
    -- ===============================================================


    -- 5a. Full Text Search Update Trigger
    CREATE OR REPLACE FUNCTION public.chunk_tsv_update()
    RETURNS trigger LANGUAGE plpgsql AS $$
    DECLARE doc_title text;
    BEGIN
      SELECT d.title INTO doc_title FROM public.document d WHERE d.id = NEW.document_id;
      NEW.tsv := 
        setweight(to_tsvector('english', coalesce(doc_title, '')), 'D') || 
        setweight(to_tsvector('english', coalesce(NEW.text, '')), 'A');
      RETURN NEW;
    END $$;


    DROP TRIGGER IF EXISTS chunk_tsv_trg ON public.chunk;
    CREATE TRIGGER chunk_tsv_trg
    BEFORE INSERT OR UPDATE OF text, document_id ON public.chunk
    FOR EACH ROW EXECUTE FUNCTION public.chunk_tsv_update();


    -- 5b. Sanitize Table Names
    CREATE OR REPLACE FUNCTION public.sanitize_table_name(name TEXT)
    RETURNS TEXT LANGUAGE sql IMMUTABLE AS $$
      SELECT 'tbl_' || regexp_replace(lower(trim(name)), '[^a-z0-9_]', '_', 'g');
    $$;


    -- 5c. Data Extraction Helpers
    CREATE OR REPLACE FUNCTION public.extract_numeric(text TEXT, key TEXT)
    RETURNS NUMERIC LANGUAGE sql IMMUTABLE AS $$
      SELECT (regexp_match(text, key || '\s*:\s*\$?([0-9,]+\.?[0-9]*)', 'i'))[1]::text::numeric;
    $$;


    -- 5d. Polymorphic Date Extraction (Supports Excel, ISO, US formats)
    -- 1. Text
    CREATE OR REPLACE FUNCTION public.extract_date(text TEXT)
    RETURNS DATE LANGUAGE plpgsql IMMUTABLE AS $$
    BEGIN
      IF text ~ '^\d{5}$' THEN RETURN '1899-12-30'::date + (text::int); END IF;
      IF text ~ '\d{4}-\d{2}-\d{2}' THEN RETURN (regexp_match(text, '(\d{4}-\d{2}-\d{2})'))[1]::date; END IF;
      IF text ~ '\d{1,2}/\d{1,2}/\d{4}' THEN RETURN to_date((regexp_match(text, '(\d{1,2}/\d{1,2}/\d{4})'))[1], 'MM/DD/YYYY'); END IF;
      RETURN NULL;
    EXCEPTION WHEN OTHERS THEN RETURN NULL;
    END $$;


    -- 2. Numeric (Excel Serial)
    CREATE OR REPLACE FUNCTION public.extract_date(val NUMERIC)
    RETURNS DATE LANGUAGE plpgsql IMMUTABLE AS $$
    BEGIN RETURN '1899-12-30'::date + (val::int); EXCEPTION WHEN OTHERS THEN RETURN NULL; END $$;


    -- 3. Integer
    CREATE OR REPLACE FUNCTION public.extract_date(val INTEGER)
    RETURNS DATE LANGUAGE plpgsql IMMUTABLE AS $$
    BEGIN RETURN '1899-12-30'::date + val; EXCEPTION WHEN OTHERS THEN RETURN NULL; END $$;


    -- 4. Date (Pass-through)
    CREATE OR REPLACE FUNCTION public.extract_date(val DATE)
    RETURNS DATE LANGUAGE sql IMMUTABLE AS $$ SELECT val; $$;


    CREATE OR REPLACE FUNCTION public.extract_keywords(p_text TEXT)
    RETURNS TEXT[] LANGUAGE sql IMMUTABLE AS $$
      SELECT array_agg(DISTINCT word) FROM (
        SELECT unnest(tsvector_to_array(to_tsvector('english', p_text))) AS word
      ) words WHERE length(word) &amp;gt; 2 LIMIT 10;
    $$;


    -- 5e. Column Type Inference
    CREATE OR REPLACE FUNCTION public.infer_column_type(sample_values TEXT[])
    RETURNS TEXT LANGUAGE plpgsql IMMUTABLE AS $$
    DECLARE
      val TEXT;
      numeric_count INT := 0;
      date_count INT := 0;
      boolean_count INT := 0;
      total_non_null INT := 0;
    BEGIN
      FOR val IN SELECT unnest(sample_values) LOOP
        IF val IS NOT NULL AND val != '' THEN
          total_non_null := total_non_null + 1;
          IF lower(val) IN ('true', 'false', 'yes', 'no', 't', 'f', 'y', 'n', '1', '0') THEN boolean_count := boolean_count + 1; END IF;
          IF val ~ '\d+\s*x\s*\d+' THEN RETURN 'TEXT'; END IF;
          IF val ~ '\d+\s*(cm|mm|m|km|in|ft|yd|kg|g|mg|lb|oz|ml|l|gal)' THEN RETURN 'TEXT'; END IF;
          IF val ~ '^\$?-?[0-9,]+\.?[0-9]*$' THEN numeric_count := numeric_count + 1; END IF;
          IF val ~ '^\d{4}-\d{2}-\d{2}' OR val ~ '^\d{1,2}/\d{1,2}/\d{4}' OR val ~ '^\d{5}$' THEN date_count := date_count + 1; END IF;
        END IF;
      END LOOP;
      
      IF total_non_null = 0 THEN RETURN 'TEXT'; END IF;
      IF boolean_count::float / total_non_null &amp;gt; 0.8 THEN RETURN 'BOOLEAN'; END IF;
      IF numeric_count::float / total_non_null &amp;gt; 0.8 THEN RETURN 'NUMERIC'; END IF;
      IF date_count::float / total_non_null &amp;gt; 0.8 THEN RETURN 'DATE'; END IF;
      RETURN 'TEXT';
    END $$;


    -- ===============================================================
    -- PART 6: INGESTION FUNCTIONS (RPC)
    -- ===============================================================


    -- 6a. Document Ingest (Standard)
    CREATE OR REPLACE FUNCTION public.ingest_document_chunk(
      p_uri TEXT, p_title TEXT, p_doc_meta JSONB,
      p_chunk JSONB, p_nodes JSONB, p_edges JSONB, p_mentions JSONB
    )
    RETURNS JSONB LANGUAGE plpgsql SECURITY DEFINER SET search_path = public, pg_temp AS $$
    DECLARE
      v_doc_id BIGINT; v_chunk_id BIGINT; v_node JSONB; v_edge JSONB; v_mention JSONB;
      v_src_id BIGINT; v_dst_id BIGINT;
    BEGIN
      INSERT INTO public.document(uri, title, doc_type, meta)
      VALUES (p_uri, p_title, 'document', COALESCE(p_doc_meta, '{}'::jsonb))
      ON CONFLICT (uri) DO UPDATE SET title = EXCLUDED.title, meta = public.document.meta || EXCLUDED.meta
      RETURNING id INTO v_doc_id;


      INSERT INTO public.chunk(document_id, ordinal, text)
      VALUES (v_doc_id, (p_chunk-&amp;gt;&amp;gt;'ordinal')::INT, p_chunk-&amp;gt;&amp;gt;'text')
      ON CONFLICT (document_id, ordinal) DO UPDATE SET text = EXCLUDED.text
      RETURNING id INTO v_chunk_id;


      IF (p_chunk ? 'embedding') THEN
        INSERT INTO public.chunk_embedding(chunk_id, embedding)
        VALUES (v_chunk_id, (SELECT array_agg((e)::float4 ORDER BY ord) FROM jsonb_array_elements_text(p_chunk-&amp;gt;'embedding') WITH ORDINALITY t(e, ord))::halfvec(768))
        ON CONFLICT (chunk_id) DO UPDATE SET embedding = EXCLUDED.embedding;
      END IF;


      FOR v_node IN SELECT * FROM jsonb_array_elements(COALESCE(p_nodes, '[]'::jsonb)) LOOP
        INSERT INTO public.node(key, labels, props)
        VALUES (v_node-&amp;gt;&amp;gt;'key', COALESCE((SELECT array_agg(l::TEXT) FROM jsonb_array_elements_text(v_node-&amp;gt;'labels') l), '{}'), COALESCE(v_node-&amp;gt;'props', '{}'::jsonb))
        ON CONFLICT (key) DO UPDATE SET props = public.node.props || EXCLUDED.props;
      END LOOP;


      FOR v_edge IN SELECT * FROM jsonb_array_elements(COALESCE(p_edges, '[]'::jsonb)) LOOP
        SELECT id INTO v_src_id FROM public.node WHERE key = v_edge-&amp;gt;&amp;gt;'src_key';
        SELECT id INTO v_dst_id FROM public.node WHERE key = v_edge-&amp;gt;&amp;gt;'dst_key';
        IF v_src_id IS NOT NULL AND v_dst_id IS NOT NULL THEN
          INSERT INTO public.edge(src, dst, type, props)
          VALUES (v_src_id, v_dst_id, v_edge-&amp;gt;&amp;gt;'type', COALESCE(v_edge-&amp;gt;'props', '{}'::jsonb))
          ON CONFLICT (src, dst, type) DO UPDATE SET props = public.edge.props || EXCLUDED.props;
        END IF;
      END LOOP;


      FOR v_mention IN SELECT * FROM jsonb_array_elements(COALESCE(p_mentions, '[]'::jsonb)) LOOP
        SELECT id INTO v_src_id FROM public.node WHERE key = v_mention-&amp;gt;&amp;gt;'node_key';
        IF v_chunk_id IS NOT NULL AND v_src_id IS NOT NULL THEN
          INSERT INTO public.chunk_node(chunk_id, node_id, rel)
          VALUES (v_chunk_id, v_src_id, COALESCE(v_mention-&amp;gt;&amp;gt;'rel', 'MENTIONS'))
          ON CONFLICT (chunk_id, node_id, rel) DO NOTHING;
        END IF;
      END LOOP;


      RETURN jsonb_build_object('ok', true);
    END $$;


    -- 6b. Universal Spreadsheet Ingest (V8 Updated: Description Support)
    DROP FUNCTION IF EXISTS public.ingest_spreadsheet(text, text, text, jsonb, jsonb, jsonb, jsonb);


    CREATE OR REPLACE FUNCTION public.ingest_spreadsheet(
      p_uri TEXT, p_title TEXT, p_table_name TEXT, 
      p_description TEXT, -- V8 Addition
      p_rows JSONB, p_schema JSONB, p_nodes JSONB, p_edges JSONB
    )
    RETURNS JSONB LANGUAGE plpgsql SECURITY DEFINER SET search_path = public, pg_temp AS $$
    DECLARE
      v_doc_id BIGINT; v_safe_name TEXT; v_col_name TEXT; v_inferred_type TEXT;
      v_cols TEXT[]; v_sample_values TEXT[]; v_row JSONB; v_node JSONB; v_edge JSONB;
      v_src_id BIGINT; v_dst_id BIGINT; v_table_exists BOOLEAN; v_all_columns TEXT[];
      v_schema_def JSONB;
    BEGIN
      INSERT INTO public.document(uri, title, doc_type, meta)
      VALUES (p_uri, p_title, 'spreadsheet', jsonb_build_object('table_name', p_table_name))
      ON CONFLICT (uri) DO UPDATE SET title = EXCLUDED.title RETURNING id INTO v_doc_id;


      v_safe_name := public.sanitize_table_name(p_table_name);
      SELECT EXISTS (SELECT FROM pg_tables WHERE schemaname = 'public' AND tablename = v_safe_name) INTO v_table_exists;


      IF NOT v_table_exists THEN
        -- Table Creation Logic
        SELECT array_agg(DISTINCT key ORDER BY key) INTO v_all_columns FROM jsonb_array_elements(p_rows) AS r, jsonb_object_keys(r) AS key;
        v_cols := ARRAY['id BIGSERIAL PRIMARY KEY'];
        FOREACH v_col_name IN ARRAY v_all_columns LOOP
          SELECT array_agg(kv.value::text) INTO v_sample_values FROM jsonb_array_elements(p_rows) r, jsonb_each_text(r) kv WHERE kv.key = v_col_name LIMIT 100;
          v_inferred_type := public.infer_column_type(COALESCE(v_sample_values, ARRAY[]::TEXT[]));
          v_cols := v_cols || format('%I %s', v_col_name, v_inferred_type);
        END LOOP;
        
        EXECUTE format('CREATE TABLE public.%I (%s)', v_safe_name, array_to_string(v_cols, ', '));
        
        -- Permissions
        EXECUTE format('GRANT ALL ON TABLE public.%I TO service_role', v_safe_name);
        EXECUTE format('GRANT SELECT ON TABLE public.%I TO authenticated, anon', v_safe_name);
        
        SELECT jsonb_object_agg(col_name, 'TEXT') INTO v_schema_def FROM unnest(v_all_columns) AS col_name;
      END IF;


      -- Insert Rows
      FOR v_row IN SELECT * FROM jsonb_array_elements(p_rows) LOOP
        DECLARE v_k TEXT; v_v TEXT; v_cl TEXT[] := ARRAY[]::TEXT[]; v_vl TEXT[] := ARRAY[]::TEXT[];
        BEGIN
          FOR v_k, v_v IN SELECT * FROM jsonb_each_text(v_row) LOOP v_cl := v_cl || quote_ident(v_k); v_vl := v_vl || quote_literal(v_v); END LOOP;
          IF array_length(v_cl, 1) &amp;gt; 0 THEN EXECUTE format('INSERT INTO public.%I (%s) VALUES (%s)', v_safe_name, array_to_string(v_cl, ', '), array_to_string(v_vl, ', ')); END IF;
        END;
      END LOOP;


      -- Upsert Registry (V8 with Description)
      INSERT INTO public.structured_table(table_name, document_id, schema_def, row_count, description)
      VALUES (v_safe_name, v_doc_id, COALESCE(v_schema_def, '{}'::jsonb), jsonb_array_length(p_rows), p_description)
      ON CONFLICT (table_name) DO UPDATE SET updated_at = now(), description = EXCLUDED.description;


      -- Upsert Graph Nodes
      FOR v_node IN SELECT * FROM jsonb_array_elements(COALESCE(p_nodes, '[]'::jsonb)) LOOP
        INSERT INTO public.node(key, labels, props)
        VALUES (v_node-&amp;gt;&amp;gt;'key', COALESCE((SELECT array_agg(l::TEXT) FROM jsonb_array_elements_text(v_node-&amp;gt;'labels') l), '{}'), COALESCE(v_node-&amp;gt;'props', '{}'::jsonb))
        ON CONFLICT (key) DO UPDATE SET props = public.node.props || EXCLUDED.props;
      END LOOP;


      -- Upsert Graph Edges
      FOR v_edge IN SELECT * FROM jsonb_array_elements(COALESCE(p_edges, '[]'::jsonb)) LOOP
        SELECT id INTO v_src_id FROM public.node WHERE key = v_edge-&amp;gt;&amp;gt;'src_key';
        SELECT id INTO v_dst_id FROM public.node WHERE key = v_edge-&amp;gt;&amp;gt;'dst_key';
        IF v_src_id IS NOT NULL AND v_dst_id IS NOT NULL THEN
          INSERT INTO public.edge(src, dst, type, props)
          VALUES (v_src_id, v_dst_id, v_edge-&amp;gt;&amp;gt;'type', COALESCE(v_edge-&amp;gt;'props', '{}'::jsonb))
          ON CONFLICT (src, dst, type) DO UPDATE SET props = public.edge.props || EXCLUDED.props;
        END IF;
      END LOOP;


      RETURN jsonb_build_object('ok', true, 'table_name', v_safe_name);
    END $$;


    -- ===============================================================
    -- PART 7: SEARCH FUNCTIONS (UPDATED V9 - COMPOSITE RETRIEVAL)
    -- ===============================================================


    -- 7a. Vector Search
    CREATE OR REPLACE FUNCTION public.search_vector(
        p_embedding VECTOR(768), 
        p_limit INT,
        p_threshold FLOAT8 DEFAULT 0.65
    )
    RETURNS TABLE(chunk_id BIGINT, content TEXT, score FLOAT8)
    LANGUAGE sql STABLE AS $$
      SELECT 
        ce.chunk_id, 
        c.text as content,
        1.0 / (1.0 + (ce.embedding &amp;lt;=&amp;gt; p_embedding)) AS score
      FROM public.chunk_embedding ce
      JOIN public.chunk c ON c.id = ce.chunk_id
      WHERE (1.0 / (1.0 + (ce.embedding &amp;lt;=&amp;gt; p_embedding))) &amp;gt;= p_threshold
      ORDER BY score DESC
      LIMIT p_limit;
    $$;


    -- 7b. Multi-Strategy Full-Text Search (V9)
    CREATE OR REPLACE FUNCTION public.search_fulltext(
      p_query text, 
      p_limit integer
    )
    RETURNS TABLE(
      chunk_id bigint, 
      content text, 
      score double precision
    )
    LANGUAGE sql STABLE AS $$
      WITH query_variants AS (
        SELECT 
          websearch_to_tsquery('english', p_query) AS tsq_websearch,
          plainto_tsquery('english', p_query) AS tsq_plain,
          to_tsquery('english', regexp_replace(p_query, '\s+', ' | ', 'g')) AS tsq_or
      ),
      results AS (
        -- Strategy 1: Websearch (most precise)
        SELECT 
          c.id AS chunk_id, 
          c.text AS content,
          ts_rank_cd(c.tsv, q.tsq_websearch)::float8 AS score,
          1 as strategy
        FROM public.chunk c 
        CROSS JOIN query_variants q 
        WHERE c.tsv @@ q.tsq_websearch
        
        UNION ALL
        
        -- Strategy 2: Plain text (more flexible)
        SELECT 
          c.id AS chunk_id, 
          c.text AS content,
          ts_rank_cd(c.tsv, q.tsq_plain)::float8 * 0.8 AS score,
          2 as strategy
        FROM public.chunk c 
        CROSS JOIN query_variants q 
        WHERE c.tsv @@ q.tsq_plain
          AND NOT EXISTS (
            SELECT 1 FROM public.chunk c2 
            CROSS JOIN query_variants q2
            WHERE c2.id = c.id AND c2.tsv @@ q2.tsq_websearch
          )
        
        UNION ALL
        
        -- Strategy 3: OR query (most flexible)
        SELECT 
          c.id AS chunk_id, 
          c.text AS content,
          ts_rank_cd(c.tsv, q.tsq_or)::float8 * 0.6 AS score,
          3 as strategy
        FROM public.chunk c 
        CROSS JOIN query_variants q 
        WHERE c.tsv @@ q.tsq_or
          AND NOT EXISTS (
            SELECT 1 FROM public.chunk c2 
            CROSS JOIN query_variants q2
            WHERE c2.id = c.id 
              AND (c2.tsv @@ q2.tsq_websearch OR c2.tsv @@ q2.tsq_plain)
          )
      )
      SELECT chunk_id, content, score
      FROM results
      ORDER BY score DESC
      LIMIT p_limit;
    $$;
    GRANT EXECUTE ON FUNCTION public.search_fulltext TO anon, authenticated, service_role;


    -- 7c. Table Peeking (V9: GENERIC FK DETECTION + REVERSE LOOKUP)


    CREATE OR REPLACE FUNCTION public.get_table_context(p_table_name TEXT)
    RETURNS JSONB
    LANGUAGE plpgsql 
    SECURITY DEFINER 
    SET search_path = public, pg_temp
    AS $$
    DECLARE
      v_safe_name TEXT;
      v_columns TEXT;
      v_sample_row JSONB;
      v_description TEXT;
      v_semantics JSONB;
      v_categorical_values JSONB := '{}'::jsonb;
      v_related_tables JSONB := '[]'::jsonb;
      v_cat_col TEXT;
      v_cat_values JSONB;
      v_distinct_count INT;
      v_fk_col TEXT;
      v_ref_table TEXT;
      v_ref_table_exists BOOLEAN;
      v_join_col TEXT;
      v_ref_has_id BOOLEAN;
      v_ref_has_name BOOLEAN;
      v_reverse_rec RECORD;
    BEGIN
      v_safe_name := quote_ident(p_table_name);
      
      -- Get schema
      SELECT string_agg(column_name || ' (' || data_type || ')', ', ')
      INTO v_columns
      FROM information_schema.columns
      WHERE table_name = p_table_name AND table_schema = 'public';


      -- Get sample row
      EXECUTE format('SELECT to_jsonb(t) FROM (SELECT * FROM public.%I LIMIT 1) t', v_safe_name)
      INTO v_sample_row;
      
      -- Get semantic metadata from structured_table
      SELECT 
        description,
        column_semantics
      INTO v_description, v_semantics
      FROM public.structured_table
      WHERE table_name = p_table_name;
      
      -- Get categorical values for columns with status/type/category in name
      FOR v_cat_col IN 
        SELECT column_name
        FROM information_schema.columns
        WHERE table_schema = 'public' 
          AND table_name = p_table_name
          AND (
            column_name LIKE '%status%' OR
            column_name LIKE '%type%' OR
            column_name LIKE '%category%' OR
            column_name LIKE '%state%' OR
            column_name LIKE '%priority%' OR
            column_name LIKE '%level%'
          )
      LOOP
        BEGIN
          -- Check if column has reasonable number of distinct values
          EXECUTE format('SELECT COUNT(DISTINCT %I) FROM public.%I', v_cat_col, v_safe_name)
          INTO v_distinct_count;
          
          -- Only include if 20 or fewer distinct values
          IF v_distinct_count &amp;lt;= 20 THEN
            EXECUTE format('SELECT jsonb_agg(DISTINCT %I ORDER BY %I) FROM public.%I', 
                          v_cat_col, v_cat_col, v_safe_name)
            INTO v_cat_values;
            
            -- Add to categorical_values object
            v_categorical_values := v_categorical_values || jsonb_build_object(v_cat_col, v_cat_values);
          END IF;
        EXCEPTION WHEN OTHERS THEN
          -- Skip this column if any error
          CONTINUE;
        END;
      END LOOP;
      
      -- ========================================================================
      -- PART 1: FORWARD FK DETECTION (this table references other tables)
      -- ========================================================================
      FOR v_fk_col IN 
        SELECT column_name
        FROM information_schema.columns
        WHERE table_schema = 'public' 
          AND table_name = p_table_name
          AND (
            column_name LIKE '%\_id' OR           -- customer_id, warehouse_id, manager_id
            column_name LIKE '%\_name'            -- manager_name, customer_name
          )
          AND column_name != 'id'                 -- Skip primary key
      LOOP
        -- Infer referenced table name
        v_ref_table := regexp_replace(v_fk_col, '_(id|name)$', '');
        
        -- Handle pluralization
        -- carriers → carrier, employees → employee, warehouses → warehouse
        IF v_ref_table ~ '(ss|us|ch|sh|x|z)es$' THEN
          v_ref_table := regexp_replace(v_ref_table, 'es$', '');
        ELSIF v_ref_table ~ 'ies$' THEN
          v_ref_table := regexp_replace(v_ref_table, 'ies$', 'y');
        ELSIF v_ref_table ~ 's$' THEN
          v_ref_table := regexp_replace(v_ref_table, 's$', '');
        END IF;
        
        -- Add tbl_ prefix
        v_ref_table := 'tbl_' || v_ref_table;
        
        -- Check if referenced table exists
        SELECT EXISTS (
          SELECT FROM pg_tables 
          WHERE schemaname = 'public' AND tablename = v_ref_table
        ) INTO v_ref_table_exists;
        
        IF v_ref_table_exists THEN
          -- Determine join column in referenced table
          v_join_col := NULL;
          v_ref_has_id := FALSE;
          v_ref_has_name := FALSE;
          
          -- Check what columns the referenced table has
          IF v_fk_col LIKE '%\_id' THEN
            -- For FK columns ending in _id, look for matching ID column in ref table
            -- customer_id → look for customer_id in tbl_customers
            -- manager_id → look for employee_id in tbl_employees (special case)
            
            SELECT bool_or(
              column_name = v_fk_col OR 
              column_name = regexp_replace(v_ref_table, '^tbl_', '') || '_id'
            )
            INTO v_ref_has_id
            FROM information_schema.columns
            WHERE table_schema = 'public' AND table_name = v_ref_table;
            
            IF v_ref_has_id THEN
              -- Find the actual ID column name
              SELECT column_name INTO v_join_col
              FROM information_schema.columns
              WHERE table_schema = 'public' AND table_name = v_ref_table
                AND (column_name = v_fk_col OR 
                     column_name = regexp_replace(v_ref_table, '^tbl_', '') || '_id')
              LIMIT 1;
            END IF;
          END IF;
          
          IF v_fk_col LIKE '%\_name' THEN
            -- For FK columns ending in _name, look for 'name' column in ref table
            SELECT bool_or(column_name = 'name')
            INTO v_ref_has_name
            FROM information_schema.columns
            WHERE table_schema = 'public' AND table_name = v_ref_table;
            
            IF v_ref_has_name THEN
              v_join_col := 'name';
            END IF;
          END IF;
          
          -- If we found a valid join column, add to related tables
          IF v_join_col IS NOT NULL THEN
            v_related_tables := v_related_tables || jsonb_build_array(
              jsonb_build_object(
                'table', v_ref_table,
                'fk_column', v_fk_col,
                'join_on', format('%I.%I = %I.%I', 
                                 p_table_name, v_fk_col,
                                 v_ref_table, v_join_col),
                'useful_columns', 'Details from ' || v_ref_table,
                'use_when', format('Query mentions %s or asks about %s details', 
                                  regexp_replace(v_ref_table, '^tbl_', ''),
                                  regexp_replace(v_fk_col, '_(id|name)$', ''))
              )
            );
          END IF;
        END IF;
      END LOOP;


      -- ========================================================================
      -- PART 2: REVERSE FK DETECTION (other tables reference this table)
      -- ========================================================================
      -- Example: tbl_employees should know that tbl_warehouses.manager_name references it
      
      FOR v_reverse_rec IN
        SELECT DISTINCT 
          c.table_name,
          c.column_name,
          c.data_type
        FROM information_schema.columns c
        WHERE c.table_schema = 'public' 
          AND c.table_name LIKE 'tbl_%'
          AND c.table_name != p_table_name
          AND (
            c.column_name LIKE '%\_id' OR
            c.column_name LIKE '%\_name'
          )
      LOOP
        v_ref_table := v_reverse_rec.table_name;
        v_fk_col := v_reverse_rec.column_name;
        v_join_col := NULL;
        
        -- Extract the base entity name from the foreign key column
        -- manager_id → manager, customer_name → customer
        DECLARE
          v_base_entity TEXT;
          v_current_table_entity TEXT;
        BEGIN
          v_base_entity := regexp_replace(v_fk_col, '_(id|name)$', '');
          v_current_table_entity := regexp_replace(p_table_name, '^tbl_', '');
          
          -- Normalize pluralization for comparison
          IF v_current_table_entity ~ 's$' THEN
            v_current_table_entity := regexp_replace(v_current_table_entity, 's$', '');
          END IF;
          
          -- Check if this FK might reference the current table
          -- Examples:
          --   manager → employee (tbl_warehouses.manager_name → tbl_employees.name)
          --   customer → customer (tbl_orders.customer_id → tbl_customers.customer_id)
          --   employee → employee (tbl_employees.manager_id → tbl_employees.employee_id)
          
          IF v_base_entity = v_current_table_entity OR
             (v_base_entity = 'manager' AND v_current_table_entity = 'employee') OR
             (v_base_entity = 'employee' AND v_current_table_entity = 'employee') THEN
            
            -- Determine what column in current table this FK should join to
            IF v_fk_col LIKE '%\_id' THEN
              -- Look for matching ID column in current table
              SELECT column_name INTO v_join_col
              FROM information_schema.columns
              WHERE table_schema = 'public' 
                AND table_name = p_table_name
                AND (
                  column_name = v_fk_col OR
                  column_name = p_table_name || '_id' OR
                  column_name = regexp_replace(p_table_name, '^tbl_', '') || '_id'
                )
              LIMIT 1;
            ELSIF v_fk_col LIKE '%\_name' THEN
              -- Look for 'name' column in current table
              SELECT column_name INTO v_join_col
              FROM information_schema.columns
              WHERE table_schema = 'public' 
                AND table_name = p_table_name
                AND column_name = 'name'
              LIMIT 1;
            END IF;
            
            -- If we found a matching join column, add to related tables
            IF v_join_col IS NOT NULL THEN
              -- Check if this relationship already exists (avoid duplicates from forward pass)
              IF NOT EXISTS (
                SELECT 1 FROM jsonb_array_elements(v_related_tables) elem
                WHERE elem-&amp;gt;&amp;gt;'table' = v_ref_table
                  AND elem-&amp;gt;&amp;gt;'fk_column' = v_fk_col
              ) THEN
                v_related_tables := v_related_tables || jsonb_build_array(
                  jsonb_build_object(
                    'table', v_ref_table,
                    'fk_column', v_fk_col,
                    'join_on', format('%I.%I = %I.%I', 
                                     p_table_name, v_join_col,
                                     v_ref_table, v_fk_col),
                    'useful_columns', 'Details from ' || v_ref_table,
                    'use_when', format('Query asks about %s that reference %s', 
                                      regexp_replace(v_ref_table, '^tbl_', ''),
                                      regexp_replace(p_table_name, '^tbl_', ''))
                  )
                );
              END IF;
            END IF;
          END IF;
        END;
      END LOOP;


      RETURN jsonb_build_object(
        'table', p_table_name,
        'schema', v_columns,
        'sample', COALESCE(v_sample_row, '{}'::jsonb),
        'description', v_description,
        'column_semantics', COALESCE(v_semantics, '{}'::jsonb),
        'categorical_values', v_categorical_values,
        'related_tables', v_related_tables
      );
      
    EXCEPTION WHEN OTHERS THEN
      RETURN jsonb_build_object('error', SQLERRM);
    END $$;


    GRANT EXECUTE ON FUNCTION public.get_table_context(text) TO service_role, authenticated, anon;


    -- 7d. Hybrid Graph Search
    CREATE OR REPLACE FUNCTION public.search_graph_hybrid(
      p_entities TEXT[],
      p_actions TEXT[],
      p_limit INT DEFAULT 20
    )
    RETURNS TABLE(chunk_id BIGINT, content TEXT, score FLOAT8, strategy TEXT)
    LANGUAGE plpgsql STABLE AS $$
    DECLARE
      v_has_actions BOOLEAN;
    BEGIN
      v_has_actions := (array_length(p_actions, 1) &amp;gt; 0);


      RETURN QUERY
      WITH relevant_nodes AS (
        SELECT id FROM public.node
        WHERE EXISTS (
          SELECT 1 FROM unnest(p_entities) entity
          WHERE public.node.key ILIKE '%' || entity || '%'
             OR public.node.props-&amp;gt;&amp;gt;'name' ILIKE '%' || entity || '%'
        )
      ),
      relevant_edges AS (
        SELECT e.src, e.dst, e.type, 
               CASE 
                 WHEN s.id IS NOT NULL AND d.id IS NOT NULL THEN 'entity-entity'
                 ELSE 'entity-action'
               END as match_strategy,
               CASE 
                 WHEN s.id IS NOT NULL AND d.id IS NOT NULL THEN 2.0
                 ELSE 1.5
               END as base_score
        FROM public.edge e
        LEFT JOIN relevant_nodes s ON e.src = s.id
        LEFT JOIN relevant_nodes d ON e.dst = d.id
        WHERE 
          (s.id IS NOT NULL AND d.id IS NOT NULL)
          OR
          (v_has_actions AND (s.id IS NOT NULL OR d.id IS NOT NULL) AND 
            EXISTS (SELECT 1 FROM unnest(p_actions) act WHERE e.type ILIKE act || '%')
          )
      ),
      hits AS (
        SELECT 
            cn.chunk_id,
            count(*) as mention_count,
            max(base_score) as max_strategy_score,
            string_agg(DISTINCT match_strategy, ', ') as strategies
        FROM relevant_edges re
        JOIN public.chunk_node cn ON cn.node_id = re.src
        GROUP BY cn.chunk_id
      )
      SELECT 
        h.chunk_id, 
        c.text as content,
        (log(h.mention_count + 1) * h.max_strategy_score)::float8 AS score,
        h.strategies::text as strategy
      FROM hits h
      JOIN public.chunk c ON c.id = h.chunk_id
      ORDER BY score DESC
      LIMIT p_limit;
    END $$;


    -- 7e. Legacy Targeted Graph Search (RESTORED FOR COMPLETENESS)
    CREATE OR REPLACE FUNCTION public.search_graph_targeted(
      p_entities TEXT[],
      p_actions TEXT[],
      p_limit INT DEFAULT 20
    )
    RETURNS TABLE(chunk_id BIGINT, content TEXT, score FLOAT8)
    LANGUAGE plpgsql STABLE AS $$
    DECLARE
      v_has_actions BOOLEAN;
    BEGIN
      v_has_actions := (array_length(p_actions, 1) &amp;gt; 0);


      RETURN QUERY
      WITH relevant_nodes AS (
        SELECT id, props-&amp;gt;&amp;gt;'name' as name
        FROM public.node
        WHERE EXISTS (
          SELECT 1 FROM unnest(p_entities) entity
          WHERE public.node.key ILIKE '%' || entity || '%'
             OR public.node.props-&amp;gt;&amp;gt;'name' ILIKE '%' || entity || '%'
        )
      ),
      relevant_edges AS (
        SELECT e.src, e.dst, e.type
        FROM public.edge e
        JOIN relevant_nodes rn ON e.src = rn.id
      ),
      hits AS (
        SELECT 
            cn.chunk_id,
            count(*) as mention_count
        FROM relevant_edges re
        JOIN public.chunk_node cn ON cn.node_id = re.src
        GROUP BY cn.chunk_id
      )
      SELECT 
        h.chunk_id, 
        c.text as content,
        (log(h.mention_count + 1) * 1.5)::float8 AS score
      FROM hits h
      JOIN public.chunk c ON c.id = h.chunk_id
      ORDER BY score DESC
      LIMIT p_limit;
    END $$;


    -- 7f. Graph Neighborhood (Update 4: Security Definer / Case Insensitive)
    DROP FUNCTION IF EXISTS public.get_graph_neighborhood(text[]);
    CREATE OR REPLACE FUNCTION public.get_graph_neighborhood(
      p_entity_names TEXT[]
    )
    RETURNS TABLE(subject TEXT, action TEXT, object TEXT, context JSONB)
    LANGUAGE plpgsql 
    SECURITY DEFINER 
    SET search_path = public, pg_temp
    AS $$
    BEGIN
      RETURN QUERY
      WITH target_nodes AS (
        SELECT id, key, props-&amp;gt;&amp;gt;'name' as name 
        FROM public.node
        WHERE 
           -- 1. Direct Key Match
           key = ANY(p_entity_names)
           -- 2. Name Match (Case Insensitive, Trimmed)
           OR lower(trim(props-&amp;gt;&amp;gt;'name')) = ANY(
              SELECT lower(trim(x)) FROM unnest(p_entity_names) x
           )
           -- 3. Fuzzy Match (Substring)
           OR EXISTS (
             SELECT 1 FROM unnest(p_entity_names) term
             WHERE length(term) &amp;gt; 3 
             AND public.node.props-&amp;gt;&amp;gt;'name' ILIKE '%' || term || '%'
           )
      )
      -- Outgoing Edges
      SELECT n1.props-&amp;gt;&amp;gt;'name', e.type, n2.props-&amp;gt;&amp;gt;'name', e.props
      FROM target_nodes tn
      JOIN public.edge e ON tn.id = e.src
      JOIN public.node n1 ON e.src = n1.id
      JOIN public.node n2 ON e.dst = n2.id
      
      UNION ALL
      
      -- Incoming Edges
      SELECT n1.props-&amp;gt;&amp;gt;'name', e.type, n2.props-&amp;gt;&amp;gt;'name', e.props
      FROM target_nodes tn
      JOIN public.edge e ON tn.id = e.dst
      JOIN public.node n1 ON e.src = n1.id
      JOIN public.node n2 ON e.dst = n2.id;
    END $$;
    GRANT EXECUTE ON FUNCTION public.get_graph_neighborhood(text[]) TO service_role, authenticated, anon;


    -- 7g. Structured Search (Safe V6.1)
    DROP FUNCTION IF EXISTS public.search_structured(text, int);
    CREATE OR REPLACE FUNCTION public.search_structured(p_query_sql TEXT, p_limit INT DEFAULT 20)
    RETURNS TABLE(table_name TEXT, row_data JSONB, score FLOAT8, rank INT)
    LANGUAGE plpgsql 
    SECURITY DEFINER 
    SET search_path = public, pg_temp
    AS $$
    DECLARE 
      v_sql TEXT;
    BEGIN
      IF p_query_sql IS NULL OR length(trim(p_query_sql)) = 0 THEN RETURN; END IF;
      v_sql := p_query_sql;
      
      -- Sanitization
      v_sql := regexp_replace(v_sql, '(\W)to\.([a-zA-Z0-9_]+)', '\1t_orders.\2', 'g');
      v_sql := regexp_replace(v_sql, '\s+to\s+ON\s+', ' t_orders ON ', 'gi');
      v_sql := regexp_replace(v_sql, '\s+AS\s+to\s+', ' AS t_orders ', 'gi');
      v_sql := regexp_replace(v_sql, 'tbl_orders\s+to\s+', 'tbl_orders t_orders ', 'gi');
      v_sql := regexp_replace(v_sql, '[;\s]+$', '');


      RETURN QUERY EXECUTE format(
        'WITH user_query AS (%s) 
         SELECT 
           ''result''::text AS table_name, 
           to_jsonb(user_query.*) AS row_data, 
           1.0::float8 AS score, 
           (row_number() OVER ())::int AS rank 
         FROM user_query LIMIT %s',
        v_sql, p_limit
      );


    EXCEPTION WHEN OTHERS THEN
      RETURN QUERY SELECT 'ERROR'::text, jsonb_build_object('msg', SQLERRM, 'sql', v_sql), 1.0, 1;
    END $$;
    GRANT EXECUTE ON FUNCTION public.search_structured(text, int) TO service_role, authenticated, anon;


    -- 7h. Smart Entity Detection (V9 - Composite Retrieval)
    CREATE OR REPLACE FUNCTION public.detect_query_entities(
      p_query TEXT
    )
    RETURNS TABLE(
      entity_type TEXT,
      table_name TEXT,
      key_column TEXT,
      key_value TEXT
    )
    LANGUAGE plpgsql
    AS $$
    BEGIN
      -- Detect ORDER IDs (O followed by 5 digits)
      IF p_query ~* 'O\d{5}' THEN
        RETURN QUERY
        SELECT 
          'order'::TEXT,
          'tbl_orders'::TEXT,
          'order_id'::TEXT,
          (regexp_match(p_query, '(O\d{5})', 'i'))[1]::TEXT;
      END IF;
      
      -- Detect CUSTOMER IDs (CU followed by 3 digits)
      IF p_query ~* 'CU\d{3}' THEN
        RETURN QUERY
        SELECT 
          'customer'::TEXT,
          'tbl_customers'::TEXT,
          'customer_id'::TEXT,
          (regexp_match(p_query, '(CU\d{3})', 'i'))[1]::TEXT;
      END IF;
      
      -- Detect EMPLOYEE IDs (E followed by 3 digits)
      IF p_query ~* 'E\d{3}' THEN
        RETURN QUERY
        SELECT 
          'employee'::TEXT,
          'tbl_employees'::TEXT,
          'employee_id'::TEXT,
          (regexp_match(p_query, '(E\d{3})', 'i'))[1]::TEXT;
      END IF;
      
      -- Detect WAREHOUSE IDs (WH followed by 3 digits)
      IF p_query ~* 'WH\d{3}' THEN
        RETURN QUERY
        SELECT 
          'warehouse'::TEXT,
          'tbl_warehouses'::TEXT,
          'warehouse_id'::TEXT,
          (regexp_match(p_query, '(WH\d{3})', 'i'))[1]::TEXT;
      END IF;
      
      -- Detect CARRIER IDs (CR followed by 3 digits)
      IF p_query ~* 'CR\d{3}' THEN
        RETURN QUERY
        SELECT 
          'carrier'::TEXT,
          'tbl_carriers'::TEXT,
          'carrier_id'::TEXT,
          (regexp_match(p_query, '(CR\d{3})', 'i'))[1]::TEXT;
      END IF;
      
      RETURN;
    END;
    $$;
    GRANT EXECUTE ON FUNCTION public.detect_query_entities TO anon, authenticated, service_role;


    -- 7i. Context Enrichment (V9 - Composite Retrieval)
    CREATE OR REPLACE FUNCTION public.enrich_query_context(
      p_primary_table TEXT,
      p_primary_key TEXT,
      p_primary_value TEXT
    )
    RETURNS TABLE(
      enrichment_type TEXT,
      table_name TEXT,
      row_data JSONB,
      relationship TEXT
    )
    LANGUAGE plpgsql
    AS $$
    DECLARE
      v_customer_id TEXT;
      v_warehouse_id TEXT;
      v_employee_id TEXT;
      v_carrier_id TEXT;
      v_primary_row JSONB;
    BEGIN
      -- ====================================================
      -- STEP 1: GET PRIMARY ROW
      -- ====================================================
      EXECUTE format(
        'SELECT to_jsonb(t.*) FROM public.%I t WHERE %I = $1 LIMIT 1',
        p_primary_table,
        p_primary_key
      ) INTO v_primary_row USING p_primary_value;
      
      IF v_primary_row IS NULL THEN
        RETURN;
      END IF;
      
      RETURN QUERY SELECT 
        'primary'::TEXT,
        p_primary_table,
        v_primary_row,
        'direct_match'::TEXT;
      
      -- ====================================================
      -- STEP 2: FOLLOW FOREIGN KEYS (ENRICH!)
      -- ====================================================
      
      -- ORDERS TABLE
      IF p_primary_table = 'tbl_orders' THEN
        v_customer_id := v_primary_row-&amp;gt;&amp;gt;'customer_id';
        v_warehouse_id := v_primary_row-&amp;gt;&amp;gt;'warehouse_id';
        v_carrier_id := v_primary_row-&amp;gt;&amp;gt;'carrier_id';
        
        -- Customer
        IF v_customer_id IS NOT NULL THEN
          RETURN QUERY
          SELECT 'enrichment'::TEXT, 'tbl_customers'::TEXT, to_jsonb(c.*), 'order_customer'::TEXT
          FROM public.tbl_customers c WHERE c.customer_id = v_customer_id;
          
          -- Customer History
          RETURN QUERY
          SELECT 'related_orders'::TEXT, 'tbl_orders'::TEXT, to_jsonb(o.*), 'customer_history'::TEXT
          FROM public.tbl_orders o WHERE o.customer_id = v_customer_id AND o.order_id != p_primary_value
          ORDER BY o.order_date DESC LIMIT 5;
        END IF;
        
        -- Warehouse
        IF v_warehouse_id IS NOT NULL THEN
          RETURN QUERY
          SELECT 'enrichment'::TEXT, 'tbl_warehouses'::TEXT, to_jsonb(w.*), 'order_warehouse'::TEXT
          FROM public.tbl_warehouses w WHERE w.warehouse_id = v_warehouse_id;
        END IF;
        
        -- Carrier
        IF v_carrier_id IS NOT NULL THEN
          RETURN QUERY
          SELECT 'enrichment'::TEXT, 'tbl_carriers'::TEXT, to_jsonb(cr.*), 'order_carrier'::TEXT
          FROM public.tbl_carriers cr WHERE cr.carrier_id = v_carrier_id;
        END IF;
      END IF;
      
      -- CUSTOMERS TABLE
      IF p_primary_table = 'tbl_customers' THEN
        RETURN QUERY
        SELECT 'enrichment'::TEXT, 'tbl_orders'::TEXT, to_jsonb(o.*), 'customer_orders'::TEXT
        FROM public.tbl_orders o WHERE o.customer_id = p_primary_value
        ORDER BY o.order_date DESC LIMIT 10;
      END IF;
      
      -- EMPLOYEES TABLE
      IF p_primary_table = 'tbl_employees' THEN
        v_employee_id := v_primary_row-&amp;gt;&amp;gt;'employee_id';
        RETURN QUERY
        SELECT 'enrichment'::TEXT, 'tbl_employees'::TEXT, to_jsonb(e.*), 'direct_reports'::TEXT
        FROM public.tbl_employees e WHERE e.manager_id = v_employee_id;
        
        RETURN QUERY
        SELECT 'enrichment'::TEXT, 'tbl_employees'::TEXT, to_jsonb(m.*), 'manager'::TEXT
        FROM public.tbl_employees m WHERE m.employee_id = (v_primary_row-&amp;gt;&amp;gt;'manager_id');
      END IF;
      
      -- WAREHOUSES TABLE
      IF p_primary_table = 'tbl_warehouses' THEN
        RETURN QUERY
        SELECT 'enrichment'::TEXT, 'tbl_orders'::TEXT, to_jsonb(o.*), 'warehouse_orders'::TEXT
        FROM public.tbl_orders o WHERE o.warehouse_id = p_primary_value
        ORDER BY o.order_date DESC LIMIT 10;
      END IF;
      
      RETURN;
    END;
    $$;
    GRANT EXECUTE ON FUNCTION public.enrich_query_context TO anon, authenticated, service_role;


    -- ===============================================================
    -- PART 8: CONFIGURATION SYSTEM
    -- ===============================================================


    INSERT INTO public.app_config (id, settings)
    VALUES (1, '{
        "chunk_size": 500,
        "chunk_overlap": 100,
        "graph_sample_rate": 5,
        "worker_batch_size": 5,
        "model_router": "gemini-2.5-flash",
        "model_reranker": "gemini-2.5-flash-lite",
        "model_sql": "gemini-2.5-flash",
        "model_extraction": "gemini-2.5-flash",
        "rrf_weight_enrichment": 15.0,
        "rrf_weight_sql": 10.0,
        "rrf_weight_graph": 5.0,
        "rrf_weight_fts": 3.0,
        "rrf_weight_vector": 1.0,
        "rerank_depth": 15,
        "min_vector_score": 0.01,
        "search_limit": 10
    }'::jsonb)
    ON CONFLICT (id) DO NOTHING;


    CREATE OR REPLACE FUNCTION public.configure_system(
        p_chunk_size INT DEFAULT NULL,
        p_chunk_overlap INT DEFAULT NULL,
        p_graph_sample_rate INT DEFAULT NULL,
        p_worker_batch_size INT DEFAULT NULL,
        p_model_router TEXT DEFAULT NULL,
        p_model_reranker TEXT DEFAULT NULL,
        p_model_extraction TEXT DEFAULT NULL,
        p_search_limit INT DEFAULT NULL,
        p_rerank_depth INT DEFAULT NULL,
        p_rrf_weight_vector NUMERIC DEFAULT NULL,
        p_rrf_weight_graph NUMERIC DEFAULT NULL,
        p_rrf_weight_sql NUMERIC DEFAULT NULL
    )
    RETURNS TEXT LANGUAGE plpgsql SECURITY DEFINER AS $$
    DECLARE
        current_settings JSONB;
        new_settings JSONB;
    BEGIN
        SELECT settings INTO current_settings FROM public.app_config WHERE id = 1;
        new_settings := current_settings;
        
        IF p_chunk_size IS NOT NULL THEN new_settings := jsonb_set(new_settings, '{chunk_size}', to_jsonb(p_chunk_size)); END IF;
        IF p_chunk_overlap IS NOT NULL THEN new_settings := jsonb_set(new_settings, '{chunk_overlap}', to_jsonb(p_chunk_overlap)); END IF;
        IF p_graph_sample_rate IS NOT NULL THEN new_settings := jsonb_set(new_settings, '{graph_sample_rate}', to_jsonb(p_graph_sample_rate)); END IF;
        IF p_worker_batch_size IS NOT NULL THEN new_settings := jsonb_set(new_settings, '{worker_batch_size}', to_jsonb(p_worker_batch_size)); END IF;
        IF p_model_router IS NOT NULL THEN new_settings := jsonb_set(new_settings, '{model_router}', to_jsonb(p_model_router)); END IF;
        IF p_model_reranker IS NOT NULL THEN new_settings := jsonb_set(new_settings, '{model_reranker}', to_jsonb(p_model_reranker)); END IF;
        IF p_model_extraction IS NOT NULL THEN new_settings := jsonb_set(new_settings, '{model_extraction}', to_jsonb(p_model_extraction)); END IF;
        IF p_search_limit IS NOT NULL THEN new_settings := jsonb_set(new_settings, '{search_limit}', to_jsonb(p_search_limit)); END IF;
        IF p_rerank_depth IS NOT NULL THEN new_settings := jsonb_set(new_settings, '{rerank_depth}', to_jsonb(p_rerank_depth)); END IF;
        IF p_rrf_weight_vector IS NOT NULL THEN new_settings := jsonb_set(new_settings, '{rrf_weight_vector}', to_jsonb(p_rrf_weight_vector)); END IF;
        IF p_rrf_weight_graph IS NOT NULL THEN new_settings := jsonb_set(new_settings, '{rrf_weight_graph}', to_jsonb(p_rrf_weight_graph)); END IF;
        IF p_rrf_weight_sql IS NOT NULL THEN new_settings := jsonb_set(new_settings, '{rrf_weight_sql}', to_jsonb(p_rrf_weight_sql)); END IF;


        UPDATE public.app_config SET settings = new_settings, updated_at = now() WHERE id = 1;
        RETURN 'System configuration updated successfully.';
    END;
    $$;


    -- ===============================================================
    -- PART 9: GRAPH CONTEXT RERANKER
    -- ===============================================================
    DROP FUNCTION IF EXISTS public.get_graph_context(bigint[], text[]);


    CREATE OR REPLACE FUNCTION public.get_graph_context(
      p_chunk_ids BIGINT[],
      p_keywords TEXT[] DEFAULT '{}',
      p_actions TEXT[] DEFAULT '{}'
    )
    RETURNS TABLE (chunk_id BIGINT, graph_data JSONB)
    LANGUAGE sql STABLE AS $$
      WITH raw_edges AS (
        SELECT 
          cn_src.chunk_id,
          jsonb_build_object(
            'subject', n1.props-&amp;gt;&amp;gt;'name',
            'action', e.type,
            'object', n2.props-&amp;gt;&amp;gt;'name',
            'context', e.props
          ) as edge_json,
          (
            0 
            + (SELECT COALESCE(MAX(CASE WHEN n1.props-&amp;gt;&amp;gt;'name' ILIKE '%' || kw || '%' THEN 10 ELSE 0 END),0) FROM unnest(p_keywords) kw)
            + (SELECT COALESCE(MAX(CASE WHEN n2.props-&amp;gt;&amp;gt;'name' ILIKE '%' || kw || '%' THEN 10 ELSE 0 END),0) FROM unnest(p_keywords) kw)
            + (SELECT COALESCE(MAX(CASE WHEN array_length(p_actions, 1) &amp;gt; 0 AND e.type ILIKE act || '%' THEN 20 ELSE 0 END),0) FROM unnest(p_actions) act)
          ) as relevance_score
        FROM public.chunk_node cn_src
        JOIN public.edge e ON cn_src.node_id = e.src
        JOIN public.chunk_node cn_tgt ON e.dst = cn_tgt.node_id AND cn_src.chunk_id = cn_tgt.chunk_id
        JOIN public.node n1 ON e.src = n1.id
        JOIN public.node n2 ON e.dst = n2.id
        WHERE cn_src.chunk_id = ANY(p_chunk_ids)
      ),
      ranked_edges AS (
        SELECT 
          chunk_id, 
          edge_json,
          relevance_score,
          ROW_NUMBER() OVER (PARTITION BY chunk_id ORDER BY relevance_score DESC, length(edge_json::text) ASC) as rn
        FROM raw_edges
      )
      SELECT 
        chunk_id,
        jsonb_agg(edge_json) as graph_data
      FROM ranked_edges
      WHERE rn &amp;lt;= 5
      GROUP BY chunk_id;
    $$;


    -- ===============================================================
    -- PART 10: WORKER SETUP
    -- ===============================================================


    CREATE OR REPLACE FUNCTION public.setup_worker(project_url TEXT, service_role_key TEXT)
    RETURNS TEXT LANGUAGE plpgsql SECURITY DEFINER AS $$
    BEGIN
      EXECUTE format(
        $f$
        CREATE OR REPLACE FUNCTION public.trigger_ingest_worker()
        RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER AS $func$
        BEGIN
          PERFORM net.http_post(
            url := '%s/functions/v1/ingest-worker',
            headers := jsonb_build_object('Content-Type', 'application/json', 'Authorization', 'Bearer %s'),
            body := jsonb_build_object('type', 'INSERT', 'table', 'objects', 'schema', 'storage', 'record', row_to_json(NEW))
          );
          RETURN NEW;
        END;
        $func$;
        $f$,
        project_url, service_role_key
      );


      DROP TRIGGER IF EXISTS "trigger-ingest-worker" ON storage.objects;
      CREATE TRIGGER "trigger-ingest-worker"
      AFTER INSERT ON storage.objects
      FOR EACH ROW
      WHEN (NEW.bucket_id = 'raw_uploads')
      EXECUTE FUNCTION public.trigger_ingest_worker();


      RETURN 'Worker configured successfully!';
    END;
    $$;


    -- ===============================================================
    -- PART 11: PERMISSIONS &amp;amp; REPAIRS
    -- ===============================================================
    GRANT USAGE ON SCHEMA public TO service_role, authenticated, anon;
    GRANT ALL ON ALL TABLES IN SCHEMA public TO service_role, authenticated;
    GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO service_role, authenticated;
    GRANT ALL ON TABLE public.ingestion_queue TO service_role, postgres, anon, authenticated;
    GRANT SELECT ON TABLE public.app_config TO anon, authenticated, service_role;
    GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO authenticated, service_role;


    -- 🚨 REPAIR SCRIPT: Fix permissions for any EXISTING "tbl_" spreadsheets
    DO $$ 
    DECLARE 
        r RECORD;
    BEGIN 
        FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND tablename LIKE 'tbl_%') 
        LOOP 
            EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE public.%I TO service_role', r.tablename);
            EXECUTE format('GRANT SELECT ON TABLE public.%I TO anon, authenticated', r.tablename);
        END LOOP; 
    END $$;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2&lt;/strong&gt;. This is minor but important. If you upload a BIG file, it needs to be able to process in the background. So in order to enable background functions you need to set up a worker. In a new query, paste this and then fill in your information before running it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    -- insert your url and service worker secret
    SELECT setup_worker(
      'https://YOUR URL.supabase.co', 
      'ey**YOUR SERVICE WORKER SERET***Tc'
    );

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3&lt;/strong&gt;. If you want to adjust weighting and other parameters, run any of these queries with your desired adjustments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    -- ===============================================================
    -- CONTEXT MESH: SIMPLE CONFIGURATION SCRIPT
    -- ===============================================================
    -- Instructions: Replace the default values below and run any line
    -- Each parameter can be updated independently
    -- ===============================================================


    -- ============================================================
    -- INGESTION PARAMETERS
    -- ============================================================


    SELECT public.configure_system(p_chunk_size =&amp;gt; 500);
    -- What it does: Number of characters per document chunk
    -- Suggestions: 
    --   Higher (800-1000) = Better context, slower processing
    --   Lower (300-400) = Faster processing, more precise retrieval
    --   Default (500) = Balanced performance


    SELECT public.configure_system(p_chunk_overlap =&amp;gt; 100);
    -- What it does: Character overlap between adjacent chunks
    -- Suggestions:
    --   Higher (150-200) = Better continuity, more redundancy
    --   Lower (50-75) = Less redundancy, faster processing
    --   Default (100) = Balanced overlap


    SELECT public.configure_system(p_graph_sample_rate =&amp;gt; 5);
    -- What it does: Extract graph relationships from every Nth chunk
    -- Suggestions:
    --   Higher (10-15) = Faster ingestion, sparser graph
    --   Lower (1-3) = Dense graph, slower ingestion
    --   Default (5) = Balanced graph density


    SELECT public.configure_system(p_worker_batch_size =&amp;gt; 5);
    -- What it does: Queue items processed per worker batch
    -- Suggestions:
    --   Higher (10-20) = Faster bulk ingestion, higher memory
    --   Lower (1-3) = Slower ingestion, lower memory
    --   Default (5) = Balanced throughput


    -- ============================================================
    -- MODEL SELECTION
    -- ============================================================


    SELECT public.configure_system(p_model_router =&amp;gt; 'gemini-2.5-flash');
    -- What it does: LLM for query routing and entity extraction
    -- Suggestions:
    --   'gemini-2.5-flash' = Fast, cost-effective (default)
    --   'gemini-2.0-flash-exp' = Experimental, free tier
    --   'gemini-2.5-pro' = Most accurate, expensive


    SELECT public.configure_system(p_model_reranker =&amp;gt; 'gemini-2.5-flash-lite');
    -- What it does: LLM for reranking documents by relevance
    -- Suggestions:
    --   'gemini-2.5-flash-lite' = Ultra-fast, cheap (default)
    --   'gemini-2.5-flash' = More accurate, slightly slower
    --   'gemini-2.0-flash-exp' = Free tier option


    SELECT public.configure_system(p_model_sql =&amp;gt; 'gemini-2.5-flash');
    -- What it does: LLM for generating SQL queries
    -- Suggestions:
    --   'gemini-2.5-flash' = Good balance (default)
    --   'gemini-2.5-pro' = Complex queries, better accuracy
    --   'gemini-2.0-flash-exp' = Free tier option


    -- ============================================================
    -- SEARCH WEIGHTS (RRF Fusion)
    -- ============================================================
    -- Higher weight = More influence in final ranking
    -- ============================================================


    SELECT public.configure_system(p_rrf_weight_enrichment =&amp;gt; 15.0);
    -- What it does: Weight for composite enrichment (SQL + FK relationships)
    -- Suggestions:
    --   Higher (20-30) = Prioritize structured data context
    --   Lower (10-12) = Balance with documents
    --   Default (15.0) = Highest priority (recommended)


    SELECT public.configure_system(p_rrf_weight_sql =&amp;gt; 10.0);
    -- What it does: Weight for direct SQL query results
    -- Suggestions:
    --   Higher (15-20) = Prioritize exact matches
    --   Lower (5-8) = More exploratory results
    --   Default (10.0) = Strong influence


    SELECT public.configure_system(p_rrf_weight_graph =&amp;gt; 5.0);
    -- What it does: Weight for knowledge graph relationships
    -- Suggestions:
    --   Higher (8-12) = More relationship context
    --   Lower (2-4) = Focus on direct matches
    --   Default (5.0) = Moderate context


    SELECT public.configure_system(p_rrf_weight_fts =&amp;gt; 3.0);
    -- What it does: Weight for full-text keyword search
    -- Suggestions:
    --   Higher (5-7) = Better keyword matching
    --   Lower (1-2) = Favor semantic search
    --   Default (3.0) = Balanced keyword influence


    SELECT public.configure_system(p_rrf_weight_vector =&amp;gt; 1.0);
    -- What it does: Weight for vector similarity search
    -- Suggestions:
    --   Higher (2-5) = More semantic similarity
    --   Lower (0.5-0.8) = Favor exact matches
    --   Default (1.0) = Lowest priority (as designed)


    -- ============================================================
    -- SEARCH THRESHOLDS
    -- ============================================================


    SELECT public.configure_system(p_rerank_depth =&amp;gt; 15);
    -- What it does: Number of documents sent to LLM reranker
    -- Suggestions:
    --   Higher (20-30) = Better accuracy, slower, more expensive
    --   Lower (5-10) = Faster, cheaper, less accurate
    --   Default (15) = Good balance


    SELECT public.configure_system(p_search_limit =&amp;gt; 10);
    -- What it does: Maximum results returned to user
    -- Suggestions:
    --   Higher (20-30) = More comprehensive results
    --   Lower (5-8) = Faster response, focused results
    --   Default (10) = Standard result count


    -- ============================================================
    -- BATCH UPDATE (Update multiple at once)
    -- ============================================================


    -- Example: Update all weights at once
    SELECT public.configure_system(
        p_rrf_weight_enrichment =&amp;gt; 15.0,
        p_rrf_weight_sql =&amp;gt; 10.0,
        p_rrf_weight_graph =&amp;gt; 5.0,
        p_rrf_weight_fts =&amp;gt; 3.0,
        p_rrf_weight_vector =&amp;gt; 1.0
    );


    -- Example: Update all models at once
    SELECT public.configure_system(
        p_model_router =&amp;gt; 'gemini-2.5-flash',
        p_model_reranker =&amp;gt; 'gemini-2.5-flash-lite',
        p_model_sql =&amp;gt; 'gemini-2.5-flash',
        p_model_extraction =&amp;gt; 'gemini-2.5-flash'
    );


    -- ============================================================
    -- VIEW CURRENT SETTINGS
    -- ============================================================


    SELECT jsonb_pretty(settings) FROM public.app_config WHERE id = 1;


    -- ============================================================
    -- RESET TO DEFAULTS
    -- ============================================================


    UPDATE public.app_config 
    SET settings = '{
        "chunk_size": 500,
        "chunk_overlap": 100,
        "graph_sample_rate": 5,
        "worker_batch_size": 5,
        "model_router": "gemini-2.5-flash",
        "model_reranker": "gemini-2.5-flash-lite",
        "model_sql": "gemini-2.5-flash",
        "model_extraction": "gemini-2.5-flash",
        "rrf_weight_enrichment": 15.0,
        "rrf_weight_sql": 10.0,
        "rrf_weight_graph": 5.0,
        "rrf_weight_fts": 3.0,
        "rrf_weight_vector": 1.0,
        "rerank_depth": 15,
        "min_vector_score": 0.01,
        "search_limit": 10
    }'::jsonb
    WHERE id = 1;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4&lt;/strong&gt;. Now go over to Edge Functions. Create the first one. Make sure you name it 'ingest-intelligent'...PLEASE NOTE, YOU MUST SET A SECRET FOR GOOGLE_API_KEY WITH YOUR GEMINI API KEY FOR THESE EDGE FUNCTIONS TO WORK :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
    import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
    const corsHeaders = {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type'
    };
    // --- CONFIG LOADER ---
    async function getConfig(supabase) {
      const { data } = await supabase.from('app_config').select('settings').single();
      const defaults = {
        chunk_size: 600,
        chunk_overlap: 100
      };
      return {
        ...defaults,
        ...data &amp;amp;&amp;amp; data.settings ? data.settings : {}
      };
    }
    // --- HELPER 1: SEMANTIC CHUNKER ---
    function semanticChunker(text, maxSize = 600, overlap = 50) {
      const cleanText = text.replace(/\r\n/g, "\n");
      const separators = [
        "\n\n",
        "\n",
        ". ",
        "? ",
        "! ",
        " "
      ];
      function splitRecursive(input) {
        if (input.length &amp;lt;= maxSize) return [
          input
        ];
        let splitBy = "";
        for (const sep of separators) {
          if (input.includes(sep)) {
            splitBy = sep;
            break;
          }
        }
        if (!splitBy) {
          const chunks = [];
          for (let i = 0; i &amp;lt; input.length; i += maxSize) {
            chunks.push(input.slice(i, i + maxSize));
          }
          return chunks;
        }
        const parts = input.split(splitBy);
        const finalChunks = [];
        let current = "";
        for (const part of parts) {
          const p = splitBy.trim() === "" ? part : part + splitBy;
          if (current.length + p.length &amp;gt; maxSize) {
            if (current.trim()) finalChunks.push(current.trim());
            if (p.length &amp;gt; maxSize) finalChunks.push(...splitRecursive(p));
            else current = p;
          } else {
            current += p;
          }
        }
        if (current.trim()) finalChunks.push(current.trim());
        return finalChunks;
      }
      let chunks = splitRecursive(cleanText);
      if (overlap &amp;gt; 0 &amp;amp;&amp;amp; chunks.length &amp;gt; 1) {
        const overlapped = [
          chunks[0]
        ];
        for (let i = 1; i &amp;lt; chunks.length; i++) {
          const prev = chunks[i - 1];
          const tail = prev.length &amp;gt; overlap ? prev.slice(-overlap) : prev;
          const snap = tail.indexOf(" ");
          const cleanTail = snap &amp;gt; -1 ? tail.slice(snap + 1) : tail;
          overlapped.push(cleanTail + " ... " + chunks[i]);
        }
        return overlapped;
      }
      return chunks;
    }
    // --- GEMINI CALLER ---
    async function callGemini(prompt, apiKey, model = "gemini-2.5-flash") {
      try {
        const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`, {
          method: "POST",
          headers: {
            "Content-Type": "application/json"
          },
          body: JSON.stringify({
            contents: [
              {
                parts: [
                  {
                    text: prompt
                  }
                ]
              }
            ],
            generationConfig: {
              temperature: 0.1,
              responseMimeType: "application/json"
            }
          })
        });
        const data = await response.json();
        const text = data.candidates &amp;amp;&amp;amp; data.candidates[0] &amp;amp;&amp;amp; data.candidates[0].content &amp;amp;&amp;amp; data.candidates[0].content.parts &amp;amp;&amp;amp; data.candidates[0].content.parts[0] &amp;amp;&amp;amp; data.candidates[0].content.parts[0].text || "{}";
        return JSON.parse(text);
      } catch (e) {
        console.error("Gemini Error:", e);
        return {
          nodes: [],
          edges: []
        };
      }
    }
    // --- SMART GRAPH BUILDER ---
    async function buildGraphGeneric(rows, tableName, apiKey) {
      console.log(`[Graph] Building graph for ${tableName} (${rows.length} rows)`);
      const hints = await analyzeRelationships(rows, tableName, apiKey);
      if (hints &amp;amp;&amp;amp; hints.length &amp;gt; 0) {
        console.log(`[Graph] Using AI hints (${hints.length} relationships found)`);
        return buildFromHints(rows, hints);
      }
      console.log(`[Graph] No AI hints, using generic heuristics`);
      return buildFromHeuristics(rows, tableName);
    }
    // --- AI Analysis ---
    async function analyzeRelationships(rows, tableName, apiKey) {
      const sample = rows.slice(0, 5);
      const headers = Object.keys(sample[0] || {});
      const prompt = `You are a data relationship analyzer.


      Table: ${tableName}
      Columns: ${headers.join(', ')}
      Sample Data: ${JSON.stringify(sample, null, 2)}


      TASK: Identify HIGH-VALUE relationships ONLY.


      ✅ PRIORITIZE (High-Value Relationships):
      1. **Person-to-Person**: Manager-employee, mentor-mentee, colleague relationships
        - employee_id → manager_id = "REPORTS_TO"
        - manager_id → employee_id = "MANAGES"
        
      2. **Business Process**: Order-customer, shipment-warehouse, payment-account
        - order_id → customer_id = "PLACED_BY"
        - order_id → warehouse_id = "FULFILLED_FROM"
        - shipment_id → carrier_id = "SHIPPED_BY"
        
      3. **Ownership/Assignment**: Asset-owner, project-lead, task-assignee
        - warehouse_id → manager_name = "MANAGED_BY"
        - project_id → owner_id = "OWNED_BY"


      ❌ IGNORE (Low-Value Relationships):
      1. **Generic Attributes**: HAS_STATUS, HAS_TYPE, HAS_CATEGORY
        - order_id → order_status (this is an attribute, not a relationship)
        - item_id → item_type (this is classification, not a relationship)
        
      2. **Carrier/Infrastructure**: Unless directly person-related
        - order_id → carrier_id (weak relationship, often just logistics)
        
      3. **Self-References**: Same entity on both sides
        - employee_id → employee_id (invalid)


      RELATIONSHIP QUALITY CRITERIA:
      - **High**: Connects two different entities with meaningful business relationship
      - **Medium**: Connects entities but relationship is transactional
      - **Low**: Just describes an attribute or status


      OUTPUT RULES:
      - Only return relationships with confidence "high" or "medium"
      - Skip any relationship that just describes an attribute
      - Focus on relationships between ENTITIES, not entity-to-attribute


      Output ONLY valid JSON array:
      [
        {
          "from_col": "source_column",
          "to_col": "target_column", 
          "relationship": "VERB_DESCRIBING_RELATIONSHIP",
          "confidence": "high",
          "explanation": "Why this relationship is valuable"
        }
      ]


      If no HIGH-VALUE relationships exist, return empty array [].`;
      try {
        const result = await callGemini(prompt, apiKey, "gemini-2.5-flash");
        if (Array.isArray(result)) return result;
        if (result.relationships) return result.relationships;
        return [];
      } catch (e) {
        console.error("[Graph] AI analysis failed:", e);
        return [];
      }
    }
    // --- V10: GRAPH EDGE QUALITY VALIDATOR ---
    function validateGraphEdge(srcNode, dstNode, edgeType, context) {
      const validation = {
        valid: true,
        reason: null,
        priority: 'normal'
      };
      // Get clean names for comparison
      const srcName = srcNode.props?.name || srcNode.key || '';
      const dstName = dstNode.props?.name || dstNode.key || '';
      // Rule 1: Reject self-referential edges
      if (srcName === dstName) {
        validation.valid = false;
        validation.reason = `Self-referential: ${srcName} → ${srcName}`;
        return validation;
      }
      // Rule 2: Reject if both nodes have same key prefix (duplicates)
      const srcPrefix = srcNode.key?.split(':')[0];
      const dstPrefix = dstNode.key?.split(':')[0];
      if (srcPrefix === dstPrefix &amp;amp;&amp;amp; srcName === dstName) {
        validation.valid = false;
        validation.reason = `Duplicate nodes: ${srcNode.key} → ${dstNode.key}`;
        return validation;
      }
      // Rule 3: Define relationship priorities
      const VALUABLE_RELATIONSHIPS = new Set([
        'REPORTS_TO',
        'MANAGES',
        'WORKS_WITH',
        'ASSIGNED_TO',
        'PLACED_BY',
        'FULFILLED_FROM',
        'SHIPPED_BY'
      ]);
      const LOW_VALUE_RELATIONSHIPS = new Set([
        'HAS_CARRIER',
        'HAS_STATUS',
        'HAS_TYPE',
        'HAS_CATEGORY'
      ]);
      // Rule 4: Reject generic "HAS_*" relationships unless high priority
      if (edgeType.startsWith('HAS_') &amp;amp;&amp;amp; !VALUABLE_RELATIONSHIPS.has(edgeType)) {
        if (LOW_VALUE_RELATIONSHIPS.has(edgeType)) {
          validation.valid = false;
          validation.reason = `Low-value relationship: ${edgeType}`;
          return validation;
        }
      }
      // Rule 5: Reject if context suggests it's inferred from ID column only
      const contextStr = context?.context || context?.explanation || '';
      if (contextStr.includes('Inferred from carrier_id') || contextStr.includes('Inferred from status') || contextStr.includes('Inferred from type')) {
        validation.valid = false;
        validation.reason = `Low-confidence inference: ${contextStr}`;
        return validation;
      }
      // Rule 6: Boost person-to-person relationships
      const isPersonToPerson = (srcNode.labels?.includes('Person') || srcNode.labels?.includes('Employee')) &amp;amp;&amp;amp; (dstNode.labels?.includes('Person') || dstNode.labels?.includes('Employee'));
      if (isPersonToPerson &amp;amp;&amp;amp; VALUABLE_RELATIONSHIPS.has(edgeType)) {
        validation.priority = 'high';
      }
      // Rule 7: Reject edges with missing names
      if (!srcName || !dstName || srcName === 'undefined' || dstName === 'undefined') {
        validation.valid = false;
        validation.reason = `Missing names: src="${srcName}", dst="${dstName}"`;
        return validation;
      }
      return validation;
    }
    // --- BUILD FROM HINTS (DEDUPLICATED) ---
    function buildFromHints(rows, hints) {
      const nodeMap = new Map();
      const edgeMap = new Map();
      const clean = (s) =&amp;gt; String(s).toLowerCase().trim().replace(/[^a-z0-9]/g, "_");
      // Build ID maps
      const idMaps = {};
      for (const hint of hints) {
        if (hint.from_col.includes('_id') || hint.to_col.includes('_id')) {
          const idCol = hint.from_col.includes('_id') ? hint.from_col : hint.to_col;
          const nameCol = findNameColumn(rows[0], idCol);
          if (nameCol) {
            idMaps[idCol] = {};
            rows.forEach((r) =&amp;gt; {
              if (r[idCol] &amp;amp;&amp;amp; r[nameCol]) {
                idMaps[idCol][r[idCol]] = r[nameCol];
              }
            });
          }
        }
      }
      // Process each row
      for (const row of rows) {
        for (const hint of hints) {
          const fromVal = row[hint.from_col];
          const toVal = row[hint.to_col];
          if (!fromVal || !toVal) continue;
          // ✅ FIXED: Removed optional chaining
          const fromIdMap = idMaps[hint.from_col];
          const toIdMap = idMaps[hint.to_col];
          const resolvedFrom = fromIdMap &amp;amp;&amp;amp; fromIdMap[fromVal] || fromVal;
          const resolvedTo = toIdMap &amp;amp;&amp;amp; toIdMap[toVal] || toVal;
          const fromKey = `entity:${clean(String(resolvedFrom))}`;
          const toKey = `entity:${clean(String(resolvedTo))}`;
          if (!nodeMap.has(fromKey)) {
            nodeMap.set(fromKey, {
              key: fromKey,
              labels: [
                inferEntityType(hint.from_col)
              ],
              props: {
                name: String(resolvedFrom)
              }
            });
          }
          if (!nodeMap.has(toKey)) {
            nodeMap.set(toKey, {
              key: toKey,
              labels: [
                inferEntityType(hint.to_col)
              ],
              props: {
                name: String(resolvedTo)
              }
            });
          }
          // ✅ V10: VALIDATE EDGE BEFORE ADDING
          const srcNode = nodeMap.get(fromKey);
          const dstNode = nodeMap.get(toKey);
          const edgeType = hint.relationship || 'RELATES_TO';
          const edgeContext = {
            context: hint.explanation || `${hint.from_col} → ${hint.to_col}`,
            confidence: hint.confidence || 'medium'
          };
          const validation = validateGraphEdge(srcNode, dstNode, edgeType, edgeContext);
          if (validation.valid) {
            const edgeKey = `${fromKey}-${edgeType}-${toKey}`;
            if (!edgeMap.has(edgeKey)) {
              edgeMap.set(edgeKey, {
                src_key: fromKey,
                dst_key: toKey,
                type: edgeType,
                props: edgeContext
              });
            }
          } else {
            console.log(`[Graph Quality] REJECTED edge: ${validation.reason}`);
          }
        }
      }
      const nodes = Array.from(nodeMap.values());
      const edges = Array.from(edgeMap.values());
      console.log(`[Graph] Deduplicated: ${nodes.length} unique nodes, ${edges.length} unique edges`);
      return {
        nodes,
        edges
      };
    }
    // --- V10: NODE KEY NORMALIZER (PREVENTS DUPLICATES) ---
    function normalizeNodeKey(tableName, entityName) {
      const clean = (s) =&amp;gt; String(s).toLowerCase().trim().replace(/[^a-z0-9]/g, "_");
      // Always use tbl_ prefix for consistency
      let normalizedTable = tableName;
      if (!normalizedTable.startsWith('tbl_')) {
        normalizedTable = `tbl_${tableName}`;
      }
      return `${normalizedTable}:${clean(String(entityName))}`;
    }
    // --- BUILD FROM HEURISTICS (DEDUPLICATED) ---
    function buildFromHeuristics(rows, tableName) {
      const nodeMap = new Map();
      const edgeMap = new Map();
      const entityRegistry = new Map();
      const clean = (s) =&amp;gt; String(s).toLowerCase().trim().replace(/[^a-z0-9]/g, "_");
      const firstRow = rows[0] || {};
      const columns = Object.keys(firstRow);
      const idColumns = columns.filter((c) =&amp;gt; {
        return c.endsWith('_id') || c === 'id' || c.includes('identifier');
      });
      const nameColumns = columns.filter((c) =&amp;gt; {
        return c.includes('name') || c === 'title' || c === 'label';
      });
      if (nameColumns.length &amp;gt; 0) {
        const primaryName = nameColumns[0];
        rows.forEach((row) =&amp;gt; {
          const entityName = row[primaryName];
          if (entityName) {
            // ✅ V10: Use normalized key and check entity registry
            const normalizedName = clean(String(entityName));
            const key = normalizeNodeKey(tableName, entityName);
            if (!entityRegistry.has(normalizedName)) {
              entityRegistry.set(normalizedName, key); // Track this entity
              nodeMap.set(key, {
                key,
                labels: [
                  tableName
                ],
                props: {
                  name: String(entityName),
                  source: tableName
                }
              });
              console.log(`[Graph Dedup] Created primary node: ${key}`);
            } else {
              console.log(`[Graph Dedup] Skipped duplicate primary: ${entityName} (already exists as ${entityRegistry.get(normalizedName)})`);
            }
          }
        });
      }
      for (const col of idColumns) {
        if (col === 'id') continue;
        const referencedTable = col.replace(/_id$/, '');
        const correspondingName = findNameColumn(firstRow, col);
        if (correspondingName) {
          rows.forEach((row) =&amp;gt; {
            const fkValue = row[col];
            const fkName = row[correspondingName];
            if (fkValue &amp;amp;&amp;amp; fkName) {
              // ✅ V10: Check if entity already exists before creating node
              const normalizedFkName = clean(String(fkName));
              let fkKey;
              if (entityRegistry.has(normalizedFkName)) {
                // Entity already exists, use existing key
                fkKey = entityRegistry.get(normalizedFkName);
                console.log(`[Graph Dedup] Reusing existing node: ${fkKey} for FK ${fkName}`);
              } else {
                // Create new node with normalized key
                fkKey = normalizeNodeKey(referencedTable, fkName);
                entityRegistry.set(normalizedFkName, fkKey);
                nodeMap.set(fkKey, {
                  key: fkKey,
                  labels: [
                    referencedTable
                  ],
                  props: {
                    name: String(fkName)
                  }
                });
                console.log(`[Graph Dedup] Created FK node: ${fkKey}`);
              }
              if (nameColumns.length &amp;gt; 0) {
                const primaryName = row[nameColumns[0]];
                if (primaryName) {
                  const primaryKey = `${tableName}:${clean(String(primaryName))}`;
                  // ✅ V10: VALIDATE HEURISTIC EDGE
                  const edgeType = `HAS_${referencedTable.toUpperCase()}`;
                  const srcNode = nodeMap.get(primaryKey);
                  const dstNode = nodeMap.get(fkKey);
                  const edgeContext = {
                    context: `Inferred from ${col}`
                  };
                  const validation = validateGraphEdge(srcNode, dstNode, edgeType, edgeContext);
                  if (validation.valid) {
                    const edgeKey = `${primaryKey}-${edgeType}-${fkKey}`;
                    if (!edgeMap.has(edgeKey)) {
                      edgeMap.set(edgeKey, {
                        src_key: primaryKey,
                        dst_key: fkKey,
                        type: edgeType,
                        props: edgeContext
                      });
                    }
                  } else {
                    console.log(`[Graph Quality] REJECTED heuristic edge: ${validation.reason}`);
                  }
                }
              }
            }
          });
        }
      }
      const nodes = Array.from(nodeMap.values());
      const edges = Array.from(edgeMap.values());
      console.log(`[Graph] Heuristic mode: ${nodes.length} unique nodes, ${edges.length} unique edges`);
      return {
        nodes,
        edges
      };
    }
    // --- HELPER FUNCTIONS ---
    function findNameColumn(row, idColumn) {
      const keys = Object.keys(row);
      const baseName = idColumn.replace(/_id$/, '');
      const candidates = [
        `${baseName}_name`,
        `${baseName}_title`,
        `${baseName}`,
        'name',
        'title',
        'label'
      ];
      for (const candidate of candidates) {
        if (keys.includes(candidate)) return candidate;
      }
      return null;
    }
    function inferEntityType(columnName) {
      if (columnName.includes('employee') || columnName.includes('person')) return 'Person';
      if (columnName.includes('customer') || columnName.includes('client')) return 'Customer';
      if (columnName.includes('warehouse') || columnName.includes('location')) return 'Location';
      if (columnName.includes('product') || columnName.includes('item')) return 'Product';
      if (columnName.includes('order')) return 'Order';
      if (columnName.includes('company') || columnName.includes('organization')) return 'Organization';
      return 'Entity';
    }
    async function analyzeTableSchema(tableName, rows, apiKey) {
      const sample = rows.slice(0, 3);
      const headers = Object.keys(sample[0] || {});
      const prompt = `You are a data schema analyst. Analyze this table:


    Table Name: ${tableName}
    Columns: ${headers.join(', ')}
    Sample Data: ${JSON.stringify(sample, null, 2)}


    Provide:
    1. description: One sentence explaining what this data represents
    2. semantics: For each column, identify its semantic type. Options:
       - person_name, company_name, location_name
       - currency_amount, percentage, count
       - date, datetime, duration
       - identifier, category, description, status
    3. graph_hints: Relationships that could form knowledge graph edges. Format:
       [{"from_col": "manager_id", "to_col": "employee_id", "edge_type": "MANAGES", "confidence": "high"}]


    Output ONLY valid JSON:
    {
      "description": "...",
      "semantics": {"col1": "type", ...},
      "graph_hints": [...]
    }`;
      try {
        const result = await callGemini(prompt, apiKey, "gemini-2.5-flash");
        return {
          description: result.description || `Data table: ${tableName}`,
          semantics: result.semantics || {},
          graphHints: result.graph_hints || []
        };
      } catch (e) {
        console.error("Schema analysis failed:", e);
        return {
          description: `Data table: ${tableName}`,
          semantics: {},
          graphHints: []
        };
      }
    }
    // --- MAIN LOGIC: SPREADSHEET ---
    async function processSpreadsheetCore(uri, title, rows, env) {
      const supabase = createClient(env.SUPABASE_URL, env.SUPABASE_SERVICE_ROLE_KEY);
      console.log(`[Spreadsheet] Processing ${rows.length} rows for: ${title}`);
      // 1. Clean Rows
      const cleanRows = rows.map((row) =&amp;gt; {
        const newRow = {};
        Object.keys(row).forEach((k) =&amp;gt; {
          const cleanKey = k.toLowerCase().trim().replace(/[^a-z0-9]/g, '_');
          newRow[cleanKey] = row[k];
        });
        return newRow;
      });
      // 2. Schema Inference
      const firstRow = cleanRows[0] || {};
      const schema = {};
      Object.keys(firstRow).forEach((k) =&amp;gt; schema[k] = typeof firstRow[k]);
      const safeName = title.toLowerCase().replace(/[^a-z0-9]/g, '_');
      const tableName = `tbl_${safeName}`;
      // 3. BUILD ID MAP
      const idMap = {};
      cleanRows.forEach((r) =&amp;gt; {
        if (r.employee_id &amp;amp;&amp;amp; r.name) {
          idMap[r.employee_id] = r.name;
        }
      });
      // 4. ANALYZE SCHEMA
      console.log(`[V8] Analyzing schema for: ${title}`);
      const schemaAnalysis = await analyzeTableSchema(title, cleanRows, env.GOOGLE_API_KEY);
      console.log(`[V8] Analysis complete:`, schemaAnalysis);
      // 5. GENERATE GRAPH
      console.log(`[Graph] Building graph using generic approach...`);
      const { nodes: allNodes, edges: allEdges } = await buildGraphGeneric(cleanRows, tableName, env.GOOGLE_API_KEY);
      console.log(`[Graph] Generated ${allNodes.length} nodes and ${allEdges.length} edges.`);
      // 6. DB Insert
      const BATCH_SIZE = 500;
      for (let i = 0; i &amp;lt; cleanRows.length; i += BATCH_SIZE) {
        const rowBatch = cleanRows.slice(i, i + BATCH_SIZE);
        const nodesBatch = i === 0 ? allNodes : [];
        const edgesBatch = i === 0 ? allEdges : [];
        const { error } = await supabase.rpc('ingest_spreadsheet', {
          p_uri: uri,
          p_title: title,
          p_table_name: safeName,
          p_description: null,
          p_rows: rowBatch,
          p_schema: schema,
          p_nodes: nodesBatch,
          p_edges: edgesBatch
        });
        if (error) throw new Error(`Batch ${i} error: ${error.message}`);
      }
      // 7. SAVE METADATA
      console.log(`[V8] Saving metadata for ${tableName}...`);
      const { error: metaError } = await supabase.from('structured_table').update({
        description: schemaAnalysis.description,
        column_semantics: schemaAnalysis.semantics,
        graph_hints: schemaAnalysis.graphHints,
        sample_row: cleanRows[0]
      }).eq('table_name', tableName);
      if (metaError) {
        console.error(`[V8] Metadata save failed:`, metaError);
      } else {
        console.log(`[V8] Metadata saved successfully`);
      }
      return {
        success: true,
        rows: cleanRows.length,
        graph_nodes: allNodes.length,
        metadata: schemaAnalysis
      };
    }
    // --- MAIN HANDLER ---
    serve(async (req) =&amp;gt; {
      if (req.method === 'OPTIONS') {
        return new Response('ok', {
          headers: corsHeaders
        });
      }
      try {
        const env = Deno.env.toObject();
        const supabase = createClient(env.SUPABASE_URL, env.SUPABASE_SERVICE_ROLE_KEY);
        const body = await req.json();
        const { uri, title, text, data } = body;
        if (!uri || !title) throw new Error("Missing 'uri' or 'title'");
        if (!text &amp;amp;&amp;amp; !data) throw new Error("Must provide 'text' or 'data'");
        // DOC PATH
        if (text) {
          const config = await getConfig(supabase);
          const chunks = semanticChunker(text, config.chunk_size, config.chunk_overlap);
          const rows = chunks.map((chunk, idx) =&amp;gt; ({
            uri,
            title,
            chunk_index: idx,
            chunk_text: chunk,
            status: 'pending'
          }));
          for (let i = 0; i &amp;lt; rows.length; i += 100) {
            const { error } = await supabase.from('ingestion_queue').insert(rows.slice(i, i + 100));
            if (error) throw error;
          }
          fetch(`${env.SUPABASE_URL}/functions/v1/ingest-worker`, {
            method: 'POST',
            headers: {
              'Authorization': `Bearer ${env.SUPABASE_SERVICE_ROLE_KEY}`,
              'Content-Type': 'application/json'
            },
            body: JSON.stringify({
              action: 'start_processing'
            })
          }).catch((e) =&amp;gt; console.error("Worker trigger failed", e));
          return new Response(JSON.stringify({
            success: true,
            message: `Queued ${chunks.length} chunks.`
          }), {
            headers: {
              ...corsHeaders,
              'Content-Type': 'application/json'
            }
          });
        }
        // DATA PATH
        if (data) {
          const payloadSize = JSON.stringify(data).length;
          if (payloadSize &amp;lt; 40000) {
            const result = await processSpreadsheetCore(uri, title, data, env);
            return new Response(JSON.stringify(result), {
              headers: {
                ...corsHeaders,
                'Content-Type': 'application/json'
              }
            });
          } else {
            const fileName = `${Date.now()}_${uri.replace(/[^a-z0-9]/gi, '_')}.json`;
            const { error } = await supabase.storage.from('raw_uploads').upload(fileName, JSON.stringify(body), {
              contentType: 'application/json'
            });
            if (error) throw error;
            return new Response(JSON.stringify({
              status: "queued",
              message: "Large file uploaded to background queue."
            }), {
              status: 202,
              headers: {
                ...corsHeaders,
                'Content-Type': 'application/json'
              }
            });
          }
        }
      } catch (error) {
        return new Response(JSON.stringify({
          error: error.message
        }), {
          status: 500,
          headers: {
            ...corsHeaders,
            'Content-Type': 'application/json'
          }
        });
      }
    });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 5&lt;/strong&gt;. Create another Edge Function. Name this one 'ingest-worker':&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
    import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
    const corsHeaders = {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type'
    };
    // --- CONFIG LOADER ---
    async function getConfig(supabase) {
      const { data } = await supabase.from('app_config').select('settings').single();
      const defaults = {
        graph_sample_rate: 5,
        worker_batch_size: 5,
        model_extraction: "gemini-2.5-flash"
      };
      return {
        ...defaults,
        ...data?.settings || {}
      };
    }
    // --- GEMINI CALLER ---
    async function callGemini(prompt, apiKey, model) {
      console.log(`[Gemini] Calling ${model}...`);
      try {
        const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`, {
          method: "POST",
          headers: {
            "Content-Type": "application/json"
          },
          body: JSON.stringify({
            contents: [
              {
                parts: [
                  {
                    text: prompt
                  }
                ]
              }
            ],
            generationConfig: {
              temperature: 0.1,
              responseMimeType: "application/json"
            }
          })
        });
        if (!response.ok) {
          console.error(`[Gemini] HTTP ${response.status}: ${response.statusText}`);
          return {
            description: "Analysis unavailable",
            semantics: {},
            graph_hints: []
          };
        }
        const data = await response.json();
        const text = data.candidates?.[0]?.content?.parts?.[0]?.text || "{}";
        console.log(`[Gemini] Response received: ${text.substring(0, 100)}...`);
        return JSON.parse(text);
      } catch (e) {
        console.error("[Gemini] Error:", e);
        return {
          description: "Analysis failed",
          semantics: {},
          graph_hints: []
        };
      }
    }
    // --- EMBEDDING HELPER ---
    async function getEmbedding(text, apiKey) {
      const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:embedContent?key=${apiKey}`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json"
        },
        body: JSON.stringify({
          model: "models/gemini-embedding-001",
          content: {
            parts: [
              {
                text
              }
            ]
          },
          outputDimensionality: 768
        })
      });
      const data = await response.json();
      return data.embedding?.values || [];
    }
    // --- HELPER: SMART GRAPH BUILDER (LEGACY - FALLBACK) ---
    async function buildGraphGeneric(rows, tableName, apiKey) {
      console.log(`[Graph] Building graph for ${tableName} (${rows.length} rows)`);
      // Step 1: Try AI Analysis
      const hints = await analyzeRelationships(rows, tableName, apiKey);
      if (hints &amp;amp;&amp;amp; hints.length &amp;gt; 0) {
        console.log(`[Graph] Using AI hints (${hints.length} relationships found)`);
        return buildFromHints(rows, hints);
      }
      // Step 2: Generic Heuristic Fallback
      console.log(`[Graph] No AI hints, using generic heuristics`);
      return buildFromHeuristics(rows, tableName);
    }
    // --- AI Analysis (Enhanced Prompt) ---
    async function analyzeRelationships(rows, tableName, apiKey) {
      const sample = rows.slice(0, 5); // More samples = better analysis
      const headers = Object.keys(sample[0] || {});
      const prompt = `You are a data relationship analyzer.


      Table: ${tableName}
      Columns: ${headers.join(', ')}
      Sample Data: ${JSON.stringify(sample, null, 2)}


      TASK: Identify HIGH-VALUE relationships ONLY.


      ✅ PRIORITIZE (High-Value Relationships):
      1. **Person-to-Person**: Manager-employee, mentor-mentee, colleague relationships
        - employee_id → manager_id = "REPORTS_TO"
        - manager_id → employee_id = "MANAGES"
        
      2. **Business Process**: Order-customer, shipment-warehouse, payment-account
        - order_id → customer_id = "PLACED_BY"
        - order_id → warehouse_id = "FULFILLED_FROM"
        - shipment_id → carrier_id = "SHIPPED_BY"
        
      3. **Ownership/Assignment**: Asset-owner, project-lead, task-assignee
        - warehouse_id → manager_name = "MANAGED_BY"
        - project_id → owner_id = "OWNED_BY"


      ❌ IGNORE (Low-Value Relationships):
      1. **Generic Attributes**: HAS_STATUS, HAS_TYPE, HAS_CATEGORY
        - order_id → order_status (this is an attribute, not a relationship)
        - item_id → item_type (this is classification, not a relationship)
        
      2. **Carrier/Infrastructure**: Unless directly person-related
        - order_id → carrier_id (weak relationship, often just logistics)
        
      3. **Self-References**: Same entity on both sides
        - employee_id → employee_id (invalid)


      RELATIONSHIP QUALITY CRITERIA:
      - **High**: Connects two different entities with meaningful business relationship
      - **Medium**: Connects entities but relationship is transactional
      - **Low**: Just describes an attribute or status


      OUTPUT RULES:
      - Only return relationships with confidence "high" or "medium"
      - Skip any relationship that just describes an attribute
      - Focus on relationships between ENTITIES, not entity-to-attribute


      Output ONLY valid JSON array:
      [
        {
          "from_col": "source_column",
          "to_col": "target_column", 
          "relationship": "VERB_DESCRIBING_RELATIONSHIP",
          "confidence": "high",
          "explanation": "Why this relationship is valuable"
        }
      ]


      If no HIGH-VALUE relationships exist, return empty array [].`;
      try {
        const result = await callGemini(prompt, apiKey, "gemini-2.5-flash");
        // Handle both array and object responses
        if (Array.isArray(result)) return result;
        if (result.relationships) return result.relationships;
        return [];
      } catch (e) {
        console.error("[Graph] AI analysis failed:", e);
        return [];
      }
    }
    // --- V10: GRAPH EDGE QUALITY VALIDATOR ---
    function validateGraphEdge(srcNode, dstNode, edgeType, context) {
      const validation = {
        valid: true,
        reason: null,
        priority: 'normal'
      };
      // Get clean names for comparison
      const srcName = srcNode.props?.name || srcNode.key || '';
      const dstName = dstNode.props?.name || dstNode.key || '';
      // Rule 1: Reject self-referential edges
      if (srcName === dstName) {
        validation.valid = false;
        validation.reason = `Self-referential: ${srcName} → ${srcName}`;
        return validation;
      }
      // Rule 2: Reject if both nodes have same key prefix (duplicates)
      const srcPrefix = srcNode.key?.split(':')[0];
      const dstPrefix = dstNode.key?.split(':')[0];
      if (srcPrefix === dstPrefix &amp;amp;&amp;amp; srcName === dstName) {
        validation.valid = false;
        validation.reason = `Duplicate nodes: ${srcNode.key} → ${dstNode.key}`;
        return validation;
      }
      // Rule 3: Define relationship priorities
      const VALUABLE_RELATIONSHIPS = new Set([
        'REPORTS_TO',
        'MANAGES',
        'WORKS_WITH',
        'ASSIGNED_TO',
        'PLACED_BY',
        'FULFILLED_FROM',
        'SHIPPED_BY'
      ]);
      const LOW_VALUE_RELATIONSHIPS = new Set([
        'HAS_CARRIER',
        'HAS_STATUS',
        'HAS_TYPE',
        'HAS_CATEGORY'
      ]);
      // Rule 4: Reject generic "HAS_*" relationships unless high priority
      if (edgeType.startsWith('HAS_') &amp;amp;&amp;amp; !VALUABLE_RELATIONSHIPS.has(edgeType)) {
        if (LOW_VALUE_RELATIONSHIPS.has(edgeType)) {
          validation.valid = false;
          validation.reason = `Low-value relationship: ${edgeType}`;
          return validation;
        }
      }
      // Rule 5: Reject if context suggests it's inferred from ID column only
      const contextStr = context?.context || context?.explanation || '';
      if (contextStr.includes('Inferred from carrier_id') || contextStr.includes('Inferred from status') || contextStr.includes('Inferred from type')) {
        validation.valid = false;
        validation.reason = `Low-confidence inference: ${contextStr}`;
        return validation;
      }
      // Rule 6: Boost person-to-person relationships
      const isPersonToPerson = (srcNode.labels?.includes('Person') || srcNode.labels?.includes('Employee')) &amp;amp;&amp;amp; (dstNode.labels?.includes('Person') || dstNode.labels?.includes('Employee'));
      if (isPersonToPerson &amp;amp;&amp;amp; VALUABLE_RELATIONSHIPS.has(edgeType)) {
        validation.priority = 'high';
      }
      // Rule 7: Reject edges with missing names
      if (!srcName || !dstName || srcName === 'undefined' || dstName === 'undefined') {
        validation.valid = false;
        validation.reason = `Missing names: src="${srcName}", dst="${dstName}"`;
        return validation;
      }
      return validation;
    }
    // --- BUILD FROM HINTS (DEDUPLICATED) ---
    function buildFromHints(rows, hints) {
      const nodeMap = new Map();
      const edgeMap = new Map();
      const clean = (s) =&amp;gt; String(s).toLowerCase().trim().replace(/[^a-z0-9]/g, "_");
      // Build ID maps
      const idMaps = {};
      for (const hint of hints) {
        if (hint.from_col.includes('_id') || hint.to_col.includes('_id')) {
          const idCol = hint.from_col.includes('_id') ? hint.from_col : hint.to_col;
          const nameCol = findNameColumn(rows[0], idCol);
          if (nameCol) {
            idMaps[idCol] = {};
            rows.forEach((r) =&amp;gt; {
              if (r[idCol] &amp;amp;&amp;amp; r[nameCol]) {
                idMaps[idCol][r[idCol]] = r[nameCol];
              }
            });
          }
        }
      }
      // Process each row
      for (const row of rows) {
        for (const hint of hints) {
          const fromVal = row[hint.from_col];
          const toVal = row[hint.to_col];
          if (!fromVal || !toVal) continue;
          // ✅ FIXED: Removed optional chaining
          const fromIdMap = idMaps[hint.from_col];
          const toIdMap = idMaps[hint.to_col];
          const resolvedFrom = fromIdMap &amp;amp;&amp;amp; fromIdMap[fromVal] || fromVal;
          const resolvedTo = toIdMap &amp;amp;&amp;amp; toIdMap[toVal] || toVal;
          const fromKey = `entity:${clean(String(resolvedFrom))}`;
          const toKey = `entity:${clean(String(resolvedTo))}`;
          if (!nodeMap.has(fromKey)) {
            nodeMap.set(fromKey, {
              key: fromKey,
              labels: [
                inferEntityType(hint.from_col)
              ],
              props: {
                name: String(resolvedFrom)
              }
            });
          }
          if (!nodeMap.has(toKey)) {
            nodeMap.set(toKey, {
              key: toKey,
              labels: [
                inferEntityType(hint.to_col)
              ],
              props: {
                name: String(resolvedTo)
              }
            });
          }
          // ✅ V10: VALIDATE EDGE BEFORE ADDING
          const srcNode = nodeMap.get(fromKey);
          const dstNode = nodeMap.get(toKey);
          const edgeType = hint.relationship || 'RELATES_TO';
          const edgeContext = {
            context: hint.explanation || `${hint.from_col} → ${hint.to_col}`,
            confidence: hint.confidence || 'medium'
          };
          const validation = validateGraphEdge(srcNode, dstNode, edgeType, edgeContext);
          if (validation.valid) {
            const edgeKey = `${fromKey}-${edgeType}-${toKey}`;
            if (!edgeMap.has(edgeKey)) {
              edgeMap.set(edgeKey, {
                src_key: fromKey,
                dst_key: toKey,
                type: edgeType,
                props: edgeContext
              });
            }
          } else {
            console.log(`[Graph Quality] REJECTED edge: ${validation.reason}`);
          }
        }
      }
      const nodes = Array.from(nodeMap.values());
      const edges = Array.from(edgeMap.values());
      console.log(`[Graph] Deduplicated: ${nodes.length} unique nodes, ${edges.length} unique edges`);
      return {
        nodes,
        edges
      };
    }
    // --- V10: NODE KEY NORMALIZER (PREVENTS DUPLICATES) ---
    function normalizeNodeKey(tableName, entityName) {
      const clean = (s) =&amp;gt; String(s).toLowerCase().trim().replace(/[^a-z0-9]/g, "_");
      // Always use tbl_ prefix for consistency
      let normalizedTable = tableName;
      if (!normalizedTable.startsWith('tbl_')) {
        normalizedTable = `tbl_${tableName}`;
      }
      return `${normalizedTable}:${clean(String(entityName))}`;
    }
    // --- BUILD FROM HEURISTICS (DEDUPLICATED) ---
    function buildFromHeuristics(rows, tableName) {
      const nodeMap = new Map();
      const edgeMap = new Map();
      const entityRegistry = new Map();
      const clean = (s) =&amp;gt; String(s).toLowerCase().trim().replace(/[^a-z0-9]/g, "_");
      const firstRow = rows[0] || {};
      const columns = Object.keys(firstRow);
      const idColumns = columns.filter((c) =&amp;gt; {
        return c.endsWith('_id') || c === 'id' || c.includes('identifier');
      });
      const nameColumns = columns.filter((c) =&amp;gt; {
        return c.includes('name') || c === 'title' || c === 'label';
      });
      if (nameColumns.length &amp;gt; 0) {
        const primaryName = nameColumns[0];
        rows.forEach((row) =&amp;gt; {
          const entityName = row[primaryName];
          if (entityName) {
            // ✅ V10: Use normalized key and check entity registry
            const normalizedName = clean(String(entityName));
            const key = normalizeNodeKey(tableName, entityName);
            if (!entityRegistry.has(normalizedName)) {
              entityRegistry.set(normalizedName, key); // Track this entity
              nodeMap.set(key, {
                key,
                labels: [
                  tableName
                ],
                props: {
                  name: String(entityName),
                  source: tableName
                }
              });
              console.log(`[Graph Dedup] Created primary node: ${key}`);
            } else {
              console.log(`[Graph Dedup] Skipped duplicate primary: ${entityName} (already exists as ${entityRegistry.get(normalizedName)})`);
            }
          }
        });
      }
      for (const col of idColumns) {
        if (col === 'id') continue;
        const referencedTable = col.replace(/_id$/, '');
        const correspondingName = findNameColumn(firstRow, col);
        if (correspondingName) {
          rows.forEach((row) =&amp;gt; {
            const fkValue = row[col];
            const fkName = row[correspondingName];
            if (fkValue &amp;amp;&amp;amp; fkName) {
              // ✅ V10: Check if entity already exists before creating node
              const normalizedFkName = clean(String(fkName));
              let fkKey;
              if (entityRegistry.has(normalizedFkName)) {
                // Entity already exists, use existing key
                fkKey = entityRegistry.get(normalizedFkName);
                console.log(`[Graph Dedup] Reusing existing node: ${fkKey} for FK ${fkName}`);
              } else {
                // Create new node with normalized key
                fkKey = normalizeNodeKey(referencedTable, fkName);
                entityRegistry.set(normalizedFkName, fkKey);
                nodeMap.set(fkKey, {
                  key: fkKey,
                  labels: [
                    referencedTable
                  ],
                  props: {
                    name: String(fkName)
                  }
                });
                console.log(`[Graph Dedup] Created FK node: ${fkKey}`);
              }
              if (nameColumns.length &amp;gt; 0) {
                const primaryName = row[nameColumns[0]];
                if (primaryName) {
                  const primaryKey = `${tableName}:${clean(String(primaryName))}`;
                  // ✅ V10: VALIDATE HEURISTIC EDGE
                  const edgeType = `HAS_${referencedTable.toUpperCase()}`;
                  const srcNode = nodeMap.get(primaryKey);
                  const dstNode = nodeMap.get(fkKey);
                  const edgeContext = {
                    context: `Inferred from ${col}`
                  };
                  const validation = validateGraphEdge(srcNode, dstNode, edgeType, edgeContext);
                  if (validation.valid) {
                    const edgeKey = `${primaryKey}-${edgeType}-${fkKey}`;
                    if (!edgeMap.has(edgeKey)) {
                      edgeMap.set(edgeKey, {
                        src_key: primaryKey,
                        dst_key: fkKey,
                        type: edgeType,
                        props: edgeContext
                      });
                    }
                  } else {
                    console.log(`[Graph Quality] REJECTED heuristic edge: ${validation.reason}`);
                  }
                }
              }
            }
          });
        }
      }
      const nodes = Array.from(nodeMap.values());
      const edges = Array.from(edgeMap.values());
      console.log(`[Graph] Heuristic mode: ${nodes.length} unique nodes, ${edges.length} unique edges`);
      return {
        nodes,
        edges
      };
    }
    // --- Helper Functions ---
    function findNameColumn(row, idColumn) {
      const keys = Object.keys(row);
      // Try exact match: employee_id → employee_name
      const baseName = idColumn.replace(/_id$/, '');
      const candidates = [
        `${baseName}_name`,
        `${baseName}_title`,
        `${baseName}`,
        'name',
        'title',
        'label'
      ];
      for (const candidate of candidates) {
        if (keys.includes(candidate)) return candidate;
      }
      return null;
    }
    function inferEntityType(columnName) {
      // Infer entity type from column name
      if (columnName.includes('employee') || columnName.includes('person')) return 'Person';
      if (columnName.includes('customer') || columnName.includes('client')) return 'Customer';
      if (columnName.includes('warehouse') || columnName.includes('location')) return 'Location';
      if (columnName.includes('product') || columnName.includes('item')) return 'Product';
      if (columnName.includes('order')) return 'Order';
      if (columnName.includes('company') || columnName.includes('organization')) return 'Organization';
      return 'Entity'; // Generic fallback
    }
    async function analyzeTableSchema(tableName, rows, apiKey) {
      console.log(`[V8] Starting schema analysis for: ${tableName}`);
      const sample = rows.slice(0, 3);
      const headers = Object.keys(sample[0] || {});
      const prompt = `You are a data schema analyst. Analyze this table:


    Table Name: ${tableName}
    Columns: ${headers.join(', ')}
    Sample Data: ${JSON.stringify(sample, null, 2)}


    Provide:
    1. description: One sentence explaining what this data represents
    2. semantics: For each column, identify its semantic type. Options:
       - person_name, company_name, location_name
       - currency_amount, percentage, count
       - date, datetime, duration
       - identifier, category, description, status
    3. graph_hints: Relationships that could form knowledge graph edges. Format:
       [{"from_col": "manager_id", "to_col": "employee_id", "edge_type": "MANAGES", "confidence": "high"}]


    Output ONLY valid JSON:
    {
      "description": "...",
      "semantics": {"col1": "type", ...},
      "graph_hints": [...]
    }`;
      try {
        const result = await callGemini(prompt, apiKey, "gemini-2.5-flash");
        console.log(`[V8] Analysis complete for ${tableName}`);
        return {
          description: result.description || `Data table: ${tableName}`,
          semantics: result.semantics || {},
          graphHints: result.graph_hints || []
        };
      } catch (e) {
        console.error("[V8] Schema analysis failed:", e);
        return {
          description: `Data table: ${tableName}`,
          semantics: {},
          graphHints: []
        };
      }
    }
    // --- SPREADSHEET PROCESSOR ---
    async function processSpreadsheetCore(uri, title, rows, env) {
      console.log(`[Spreadsheet] Starting processing: ${rows.length} rows for ${title}`);
      const supabase = createClient(env.SUPABASE_URL, env.SUPABASE_SERVICE_ROLE_KEY);
      // 1. Clean Keys
      console.log(`[Spreadsheet] Cleaning row keys...`);
      const cleanRows = rows.map((row) =&amp;gt; {
        const newRow = {};
        Object.keys(row).forEach((k) =&amp;gt; {
          const cleanKey = k.toLowerCase().trim().replace(/[^a-z0-9]/g, '_');
          newRow[cleanKey] = row[k];
        });
        return newRow;
      });
      // 2. Infer Schema
      const firstRow = cleanRows[0] || {};
      const schema = {};
      Object.keys(firstRow).forEach((k) =&amp;gt; schema[k] = typeof firstRow[k]);
      console.log(`[Spreadsheet] Schema inferred: ${Object.keys(schema).length} columns`);
      // *** V8: Generate table name early ***
      const safeName = title.toLowerCase().replace(/[^a-z0-9]/g, '_');
      const tableName = `tbl_${safeName}`;
      console.log(`[Spreadsheet] Table name: ${tableName}`);
      // 3. PRE-SCAN FOR ID MAP
      const idMap = {};
      cleanRows.forEach((r) =&amp;gt; {
        if (r.employee_id &amp;amp;&amp;amp; r.name) {
          idMap[r.employee_id] = r.name;
        }
      });
      console.log(`[Spreadsheet] ID map built: ${Object.keys(idMap).length} entries`);
      // *** V8: ANALYZE SCHEMA ***
      console.log(`[V8] Analyzing schema for: ${title}`);
      const schemaAnalysis = await analyzeTableSchema(title, cleanRows, env.GOOGLE_API_KEY);
      console.log(`[V8] Analysis complete:`, schemaAnalysis);
      // 4. GENERATE GRAPH (V8: Use Generic Builder)
      console.log(`[Graph] Building graph using generic approach...`);
      const { nodes: allNodes, edges: allEdges } = await buildGraphGeneric(cleanRows, tableName, env.GOOGLE_API_KEY);
      console.log(`[Graph] Generated ${allNodes.length} nodes and ${allEdges.length} edges.`);
      // 5. BATCH INSERT
      console.log(`[DB] Starting batch insert...`);
      const BATCH_SIZE = 500;
      for (let i = 0; i &amp;lt; cleanRows.length; i += BATCH_SIZE) {
        const rowBatch = cleanRows.slice(i, i + BATCH_SIZE);
        const nodesBatch = i === 0 ? allNodes : [];
        const edgesBatch = i === 0 ? allEdges : [];
        console.log(`[DB] Inserting batch ${i / BATCH_SIZE + 1}: ${rowBatch.length} rows`);
        const { error } = await supabase.rpc('ingest_spreadsheet', {
          p_uri: uri,
          p_title: title,
          p_table_name: safeName,
          p_description: null,
          p_rows: rowBatch,
          p_schema: schema,
          p_nodes: nodesBatch,
          p_edges: edgesBatch
        });
        if (error) {
          console.error(`[DB] Batch ${i} ERROR:`, error);
          throw new Error(`Batch ${i} error: ${error.message}`);
        }
        console.log(`[DB] Batch ${i / BATCH_SIZE + 1} completed successfully`);
      }
      // *** V8: SAVE METADATA (SKIP FOR NOW) ***
      console.log(`[V8] SKIPPING metadata save to test basic ingestion`);
      console.log(`[Spreadsheet] Processing complete!`);
      return {
        success: true,
        rows: cleanRows.length,
        graph_nodes: allNodes.length,
        metadata: schemaAnalysis
      };
    }
    // --- MAIN WORKER HANDLER ---
    serve(async (req) =&amp;gt; {
      if (req.method === 'OPTIONS') return new Response('ok', {
        headers: corsHeaders
      });
      console.log(`[Worker] Request received`);
      try {
        const env = Deno.env.toObject();
        const supabase = createClient(env.SUPABASE_URL, env.SUPABASE_SERVICE_ROLE_KEY);
        const payload = await req.json();
        console.log(`[Worker] Payload parsed`);
        // PATH A: STORAGE TRIGGER (Large Spreadsheets)
        if (payload.record &amp;amp;&amp;amp; payload.record.bucket_id === 'raw_uploads') {
          console.log(`[Worker] Storage Trigger: ${payload.record.name}`);
          console.log(`[Worker] Downloading file...`);
          const { data: fileData, error: dlError } = await supabase.storage.from('raw_uploads').download(payload.record.name);
          if (dlError) {
            console.error(`[Worker] Download error:`, dlError);
            throw dlError;
          }
          console.log(`[Worker] File downloaded, parsing JSON...`);
          const contentStr = await fileData.text();
          console.log(`[Worker] Content length: ${contentStr.length} bytes`);
          const parsed = JSON.parse(contentStr);
          const { uri, title, data } = parsed;
          console.log(`[Worker] Parsed: uri=${uri}, title=${title}, rows=${data?.length || 0}`);
          if (!data || data.length === 0) {
            console.error(`[Worker] ERROR: No data in payload!`);
            throw new Error("No data found in uploaded file");
          }
          console.log(`[Worker] Calling processSpreadsheetCore...`);
          await processSpreadsheetCore(uri, title, data, env);
          console.log(`[Worker] Processing complete, deleting file...`);
          await supabase.storage.from('raw_uploads').remove([
            payload.record.name
          ]);
          console.log(`[Worker] SUCCESS!`);
          return new Response(JSON.stringify({
            success: true
          }), {
            headers: {
              ...corsHeaders,
              'Content-Type': 'application/json'
            }
          });
        }
        // PATH B: QUEUE TRIGGER (Text Documents)
        console.log(`[Worker] Queue trigger path...`);
        const config = await getConfig(supabase);
        const { data: batch, error } = await supabase.from('ingestion_queue').select('*').eq('status', 'pending').limit(config.worker_batch_size);
        if (error || !batch || batch.length === 0) {
          console.log(`[Worker] Queue empty or error:`, error);
          return new Response(JSON.stringify({
            msg: "Queue empty"
          }), {
            headers: {
              ...corsHeaders,
              'Content-Type': 'application/json'
            }
          });
        }
        console.log(`[Worker] Processing ${batch.length} chunks...`);
        await supabase.from('ingestion_queue').update({
          status: 'processing'
        }).in('id', batch.map((r) =&amp;gt; r.id));
        for (const row of batch) {
          try {
            const embedding = await getEmbedding(row.chunk_text, env.GOOGLE_API_KEY);
            // GRAPH EXTRACTION FOR TEXT (Every Nth chunk)
            let nodes = [], edges = [], mentions = [];
            if (row.chunk_index % config.graph_sample_rate === 0) {
              const extractPrompt = `
    You are a Knowledge Graph Extractor.
    Analyze this text. Identify:
    1. **ROLES**: Job titles (e.g., "Customer Support Manager").
    2. **RESPONSIBILITIES**: Key duties (e.g., "Refunds", "OSHA").
    3. **SYSTEMS**: Tools (e.g., "OMS", "Chatbots").


    Output JSON: { 
        "nodes": [{"key": "Role:Name", "labels": ["Role"], "props": {"name": "Name"}}], 
        "edges": [{"src_key": "...", "dst_key": "...", "type": "OWNS", "props": {"context": "..."}}] 
    }


    Text: ${row.chunk_text}`;
              const graphData = await callGemini(extractPrompt, env.GOOGLE_API_KEY, config.model_extraction);
              nodes = graphData.nodes || [];
              edges = graphData.edges || [];
              mentions = nodes.map((n) =&amp;gt; ({
                node_key: n.key,
                rel: "MENTIONS"
              }));
            }
            await supabase.rpc('ingest_document_chunk', {
              p_uri: row.uri,
              p_title: row.title,
              p_doc_meta: {
                processed_at: new Date().toISOString()
              },
              p_chunk: {
                ordinal: row.chunk_index,
                text: row.chunk_text,
                embedding
              },
              p_nodes: nodes,
              p_edges: edges,
              p_mentions: mentions
            });
            await supabase.from('ingestion_queue').delete().eq('id', row.id);
          } catch (err) {
            console.error(`Error processing chunk ${row.id}:`, err);
            await supabase.from('ingestion_queue').update({
              status: 'failed',
              error_log: err.message
            }).eq('id', row.id);
          }
        }
        // RECURSION (Process next batch)
        const { count } = await supabase.from('ingestion_queue').select('*', {
          count: 'exact',
          head: true
        }).eq('status', 'pending');
        if (count &amp;amp;&amp;amp; count &amp;gt; 0) {
          fetch(`${env.SUPABASE_URL}/functions/v1/ingest-worker`, {
            method: 'POST',
            headers: {
              'Authorization': `Bearer ${env.SUPABASE_SERVICE_ROLE_KEY}`,
              'Content-Type': 'application/json'
            },
            body: JSON.stringify({
              action: 'continue'
            })
          }).catch((e) =&amp;gt; console.error("Daisy chain failed", e));
        }
        return new Response(JSON.stringify({
          success: true,
          processed: batch.length
        }), {
          headers: {
            ...corsHeaders,
            'Content-Type': 'application/json'
          }
        });
      } catch (e) {
        console.error("[Worker] FATAL ERROR:", e);
        return new Response(JSON.stringify({
          error: e.message,
          stack: e.stack
        }), {
          status: 500,
          headers: {
            ...corsHeaders,
            'Content-Type': 'application/json'
          }
        });
      }
    });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 6&lt;/strong&gt;. Create one last Edge Function. Name this one, simply 'search':&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
    import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
    const CORS = {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type"
    };
    // --- CONFIG LOADER ---
    async function getConfig(supabase) {
      const { data } = await supabase.from('app_config').select('settings').single();
      return {
        model_router: "gemini-2.5-flash",
        model_reranker: "gemini-2.5-flash-lite",
        model_sql: "gemini-2.5-flash",
        rrf_weight_enrichment: 15.0,
        rrf_weight_sql: 10.0,
        rrf_weight_graph: 2.0,
        rrf_weight_fts: 4.0,
        rrf_weight_vector: 2.5,
        rerank_depth: 15,
        min_vector_score: 0.01,
        ...data &amp;amp;&amp;amp; data.settings || {}
      };
    }
    // --- V8: DYNAMIC SCHEMA LOADER ---
    async function getAvailableSchemas(supabase) {
      const { data, error } = await supabase.from('structured_table').select('table_name, description, schema_def, column_semantics').gt('row_count', 0);
      if (error || !data || data.length === 0) {
        return "No structured data available.";
      }
      return data.map((t)=&amp;gt;{
        const cols = Object.keys(t.schema_def || {}).join(', ');
        const desc = t.description || 'No description';
        return `- ${t.table_name}: ${desc}\n  Columns: ${cols}`;
      }).join('\n');
    }
    // --- V9: COMPOSITE ENRICHMENT ---
    async function enrichQueryContext(query, supabase, log) {
      console.log("[V9] Detecting entities in query...");
      const { data: detected, error: detectError } = await supabase.rpc('detect_query_entities', {
        p_query: query
      });
      if (detectError || !detected || detected.length === 0) {
        log("ENTITY_DETECTION", {
          found: false
        });
        return [];
      }
      log("ENTITY_DETECTION", {
        found: true,
        entities: detected
      });
      const enrichments = [];
      for (const entity of detected){
        console.log(`[V9] Enriching ${entity.entity_type}: ${entity.key_value}`);
        const { data: enriched, error: enrichError } = await supabase.rpc('enrich_query_context', {
          p_primary_table: entity.table_name,
          p_primary_key: entity.key_column,
          p_primary_value: entity.key_value
        });
        if (enrichError) {
          log("ENRICHMENT_ERROR", {
            entity,
            error: enrichError
          });
          continue;
        }
        if (enriched &amp;amp;&amp;amp; enriched.length &amp;gt; 0) {
          enrichments.push(...enriched.map((e)=&amp;gt;({
              ...e,
              _source: `enrichment (${e.enrichment_type})`,
              content: `[${e.enrichment_type.toUpperCase()}] ${e.table_name}: ${JSON.stringify(e.row_data)}`
            })));
        }
      }
      log("ENRICHMENT_RESULTS", {
        count: enrichments.length
      });
      return enrichments;
    }
    // --- GEMINI LLM RERANKER (FROM OLD SYSTEM) ---
    async function rerankWithGemini(query, docs, apiKey, model, depth) {
      if (!docs || docs.length === 0) return [];
      const candidates = docs.slice(0, depth);
      const docList = candidates.map((d)=&amp;gt;({
          id: d.chunk_id,
          text: (d.content || "").substring(0, 350)
        }));
      const prompt = `Role: Relevance Filter.
      Task: Evaluate if chunks are RELEVANT to the User Query.


      User Query: "${query}"


      KEEP RULES:
      ✅ KEEP if chunk directly answers the query
      ✅ KEEP if chunk provides important context (definitions, procedures, policies)
      ✅ KEEP if chunk mentions key entities from the query (names, IDs, locations)
      ✅ KEEP if unsure - err on the side of inclusion


      DISCARD RULES:
      ❌ ONLY discard if completely unrelated (different topic entirely)
      ❌ Discard "Table of Contents", "Index", or navigation elements
      ❌ Discard if chunk is just metadata without substance


      EXAMPLES:
      - Query: "return policy" → KEEP: "Customer Support: returns processing", "30-day return window", "refund procedures"
      - Query: "Order O00062" → KEEP: order details, customer info, warehouse data, shipping info
      - Query: "Who founded company?" → KEEP: company history, founder bio, origin story


      Return JSON: { "kept_ids": [list of chunk IDs to keep] }


      Docs: ${JSON.stringify(docList)}`;
      try {
        const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`, {
          method: "POST",
          headers: {
            "Content-Type": "application/json"
          },
          body: JSON.stringify({
            contents: [
              {
                parts: [
                  {
                    text: prompt
                  }
                ]
              }
            ],
            generationConfig: {
              responseMimeType: "application/json"
            }
          })
        });
        const data = await response.json();
        const text = data.candidates &amp;amp;&amp;amp; data.candidates[0] &amp;amp;&amp;amp; data.candidates[0].content &amp;amp;&amp;amp; data.candidates[0].content.parts &amp;amp;&amp;amp; data.candidates[0].content.parts[0] &amp;amp;&amp;amp; data.candidates[0].content.parts[0].text || "{}";
        const result = JSON.parse(text);
        if (result.kept_ids &amp;amp;&amp;amp; Array.isArray(result.kept_ids)) {
          const keptDocs = [];
          result.kept_ids.forEach((id)=&amp;gt;{
            const doc = docs.find((d)=&amp;gt;d.chunk_id === id);
            if (doc) keptDocs.push(doc);
          });
          // Safety net: If LLM discards everything, return top 1
          if (keptDocs.length === 0 &amp;amp;&amp;amp; docs.length &amp;gt; 0) {
            return [
              docs[0]
            ];
          }
          const MIN_KEPT = 10;
          if (keptDocs.length &amp;lt; MIN_KEPT &amp;amp;&amp;amp; docs.length &amp;gt;= MIN_KEPT) {
            console.log(`[Rerank] Only kept ${keptDocs.length}, adding top ${MIN_KEPT - keptDocs.length} from original set`);
            const keptIds = new Set(keptDocs.map((d)=&amp;gt;d.chunk_id));
            const remaining = docs.filter((d)=&amp;gt;!keptIds.has(d.chunk_id));
            const toAdd = remaining.slice(0, MIN_KEPT - keptDocs.length);
            return [
              ...keptDocs,
              ...toAdd
            ];
          }
          return keptDocs;
        }
        return candidates;
      } catch (e) {
        console.error("[Rerank] Error:", e);
        return candidates;
      }
    }
    // --- V10: GRAPH RELEVANCE FILTER ---
    function filterGraphByRelevance(graphEdges, query) {
      if (!graphEdges || graphEdges.length === 0) return [];
      const queryLower = query.toLowerCase();
      const queryWords = queryLower.split(/\s+/).filter((w)=&amp;gt;w.length &amp;gt; 2);
      const priorityRelations = [
        'REPORTS_TO',
        'MANAGES',
        'PLACED_BY',
        'FULFILLED_FROM',
        'SHIPPED_BY',
        'ASSIGNED_TO',
        'WORKS_WITH'
      ];
      const relevantEdges = [];
      for (const edge of graphEdges){
        const edgeText = `${edge.subject || ''} ${edge.action || ''} ${edge.object || ''}`.toLowerCase();
        // Calculate keyword overlap
        const matchedWords = queryWords.filter((w)=&amp;gt;edgeText.includes(w));
        const overlapRatio = matchedWords.length / queryWords.length;
        // Check for exact phrase matches (e.g., "customer onboarding" as a phrase)
        const hasPhraseMatch = queryWords.some((word, idx)=&amp;gt;{
          if (idx &amp;lt; queryWords.length - 1) {
            const phrase = `${word} ${queryWords[idx + 1]}`;
            return edgeText.includes(phrase);
          }
          return false;
        });
        // Keep if:
        // 1. High keyword overlap (&amp;gt;40% of query words present in edge)
        // 2. OR at least 2 keywords match
        // 3. OR exact phrase match found
        // 4. OR it's a high-priority relationship type
        if (overlapRatio &amp;gt; 0.4 || matchedWords.length &amp;gt;= 2 || hasPhraseMatch || priorityRelations.includes(edge.action)) {
          relevantEdges.push(edge);
        }
      }
      console.log(`[Graph Filter] ${graphEdges.length} → ${relevantEdges.length} relevant edges`);
      return relevantEdges;
    }
    // --- RRF FUSION (SIMPLIFIED) ---
    async function performRRFFusion(rerankedDocs, graphResults, sqlResults, config) {
      const K = 60;
      const scores = {};
      const addScore = (item, rank, weight, type)=&amp;gt;{
        let key = item.chunk_id ? `chunk_${item.chunk_id}` : `sql_${JSON.stringify(item.row_data)}`;
        if (!scores[key]) {
          scores[key] = {
            item: {
              ...item,
              _source: type
            },
            score: 0
          };
        }
        scores[key].score += 1.0 / (K + rank) * weight;
        if (!scores[key].item._source.includes(type)) {
          scores[key].item._source += `, ${type}`;
        }
      };
      // Apply weights
      rerankedDocs.forEach((item, idx)=&amp;gt;addScore(item, idx, config.rrf_weight_vector, 'vector/fts'));
      if (graphResults) graphResults.forEach((item, idx)=&amp;gt;addScore(item, idx, config.rrf_weight_graph, 'graph'));
      if (sqlResults) sqlResults.forEach((item, idx)=&amp;gt;addScore(item, idx, config.rrf_weight_sql, 'structured'));
      // Convert to array &amp;amp; sort
      let results = Object.values(scores).sort((a, b)=&amp;gt;b.score - a.score);
      // ✅ NO CUTOFF - Let ranking + limit handle quality
      // Reranking already filtered noise, cutoff was too aggressive
      console.log(`[RRF_FUSION] Total results: ${results.length}`);
      return results.map((s)=&amp;gt;s.item);
    }
    // --- V10: STRICTER ROUTER PROMPT (LINE ~238) ---
    function buildRouterPrompt(schemas, query) {
      return `You are the Context Mesh Router.
    User Query: "${query}"


    Available Tables:
    ${schemas}


    DECISION PROTOCOL:


    1. **ENTITY EXTRACTION** - Extract ONLY specific identifiers and proper names:


       âœ… ALWAYS EXTRACT (ID Patterns):
       - Order IDs: O00001, O00062, O\\d{5}
       - Customer IDs: CU006, CU008, CU\\d{3}
       - Employee IDs: E001, E002, E\\d{3}
       - Warehouse IDs: WH001, WH002, WH\\d{3}
       - Carrier IDs: CR001, CR005, CR\\d{3}
       
       âœ… ALWAYS EXTRACT (Proper Names):
       - Full person names: "Nicholas Cooper", "Sarah Brooks", "Emily Chen"
       - Company names: "CommerceFlow Solutions", "Brand 06"
       - Specific location names: "California", "Texas", "New Jersey"
       
       âŒ NEVER EXTRACT:
       - Action verbs: list, show, get, find, tell, display, give, provide
       - Question words: what, who, where, when, how, why, which
       - Generic nouns: employees, customers, orders, warehouses, people, items
       - Plural table names: customers, orders, employees (these trigger SQL, not graph)
       - Departments: Operations, Sales, Marketing (these are WHERE clauses, not entities)
       - Roles/titles: manager, CEO, analyst (these are WHERE clauses, not entities)
       - Determiners: the, a, an, this, that, these, those
       - Prepositions: in, at, from, to, with, by
       - Status words: active, inactive, pending, shipped
       
       âš ï¸ CRITICAL VALIDATION:
       - If word appears in common English dictionary → NOT an entity
       - If word is lowercase in query → NOT an entity (unless it's an ID)
       - If word describes a category/group → NOT an entity
       - If word is a verb in any tense → NOT an entity


       EXAMPLES:
       Query: "List orders placed this year"
       ❌ BAD: entities: ["list", "orders", "placed", "year"]
       âœ… GOOD: entities: []
       Reason: All are generic terms, no specific identifiers
       
       Query: "Show me order O00062"
       ❌ BAD: entities: ["show", "me", "order", "O00062"]
       âœ… GOOD: entities: ["O00062"]
       Reason: Only O00062 is a specific identifier
       
       Query: "Who does Sarah Brooks report to?"
       ❌ BAD: entities: ["who", "Sarah", "Brooks", "report", "to"]
       âœ… GOOD: entities: ["Sarah Brooks"]
       Reason: Full name is proper noun, rest are grammar/verbs
       
       Query: "List employees in Operations department"
       ❌ BAD: entities: ["employees", "Operations", "department"]
       âœ… GOOD: entities: []
       Reason: All are generic category terms, no specific names
       
       Query: "Show orders for Brand 06"
       ❌ BAD: entities: ["show", "orders", "Brand", "06"]
       âœ… GOOD: entities: ["Brand 06"]
       Reason: "Brand 06" is a specific customer name
       
       Query: "Which carrier shipped order O00123?"
       ❌ BAD: entities: ["which", "carrier", "shipped", "order", "O00123"]
       âœ… GOOD: entities: ["O00123"]
       Reason: Only O00123 is a specific identifier


    2. **SQL TABLE DETECTION**:
       - Use sql_tables for: counts, sums, averages, lists, filters
       - Keywords: "how many", "total", "average", "list", "show all"
       
    3. **KEYWORD EXTRACTION** (for FTS):
       - Extract nouns ONLY (no verbs, no determiners)
       - Max 3-5 keywords
       - Focus on domain-specific terms


    VALIDATION CHECKLIST (before returning entities):
    1. Is it an ID pattern (letters + numbers)? → YES = keep, NO = next check
    2. Is it a capitalized proper name (2+ words)? → YES = keep, NO = next check  
    3. Is it a common English word? → YES = REMOVE, NO = keep
    4. Is it a verb (ends in -ing, -ed, -s)? → YES = REMOVE, NO = keep
    5. Is it a plural noun (employees, orders)? → YES = REMOVE, NO = keep


    DEFAULT BEHAVIOR:
    - When in doubt → DO NOT extract as entity
    - Empty entities array is CORRECT for most queries
    - Entities should be rare (only 20-30% of queries have them)


    Output JSON: { "sql_tables": [], "entities": [], "keywords": [] }`;
    }
    // --- V10: ENTITY VALIDATION FILTER (ADD AFTER buildRouterPrompt) ---
    function validateRouterEntities(entities, query) {
      if (!entities || entities.length === 0) return [];
      const validated = [];
      const queryLower = query.toLowerCase();
      // Common English verbs and nouns to reject
      const REJECT_PATTERNS = {
        // Action verbs
        verbs: new Set([
          'list',
          'show',
          'get',
          'find',
          'tell',
          'display',
          'give',
          'provide',
          'placed',
          'hired',
          'shipped',
          'ordered',
          'delivered',
          'returned',
          'create',
          'update',
          'delete',
          'search',
          'filter',
          'sort'
        ]),
        // Question words
        questions: new Set([
          'what',
          'who',
          'where',
          'when',
          'why',
          'how',
          'which',
          'whose'
        ]),
        // Generic nouns (plural forms)
        plurals: new Set([
          'employees',
          'customers',
          'orders',
          'warehouses',
          'carriers',
          'products',
          'items',
          'people',
          'users',
          'companies',
          'brands'
        ]),
        // Generic nouns (singular forms)
        singulars: new Set([
          'employee',
          'customer',
          'order',
          'warehouse',
          'carrier',
          'product',
          'item',
          'person',
          'user',
          'company',
          'brand',
          'manager',
          'analyst',
          'director',
          'supervisor',
          'coordinator'
        ]),
        // Departments and categories
        departments: new Set([
          'operations',
          'sales',
          'marketing',
          'finance',
          'logistics',
          'hr',
          'it',
          'support',
          'management',
          'administration'
        ]),
        // Status and descriptors
        descriptors: new Set([
          'active',
          'inactive',
          'pending',
          'shipped',
          'delivered',
          'returned',
          'new',
          'old',
          'current',
          'previous',
          'next',
          'last',
          'first'
        ])
      };
      for (const entity of entities){
        const entityLower = entity.toLowerCase().trim();
        // Rule 1: Reject if empty or too short
        if (!entityLower || entityLower.length &amp;lt; 2) {
          console.log(`[Router Validation] REJECTED (too short): "${entity}"`);
          continue;
        }
        // Rule 2: Keep if matches ID pattern (letters + numbers)
        // O00062, CU006, E001, WH003, CR005
        if (entity.match(/^[A-Z]{1,3}\d{3,5}$/)) {
          console.log(`[Router Validation] ACCEPTED (ID pattern): "${entity}"`);
          validated.push(entity);
          continue;
        }
        // Rule 3: Keep if proper name (2+ capitalized words)
        // "Nicholas Cooper", "Sarah Brooks", "Brand 06"
        if (entity.match(/^[A-Z][a-z]+(\s+[A-Z0-9][a-z0-9]*)+$/)) {
          console.log(`[Router Validation] ACCEPTED (proper name): "${entity}"`);
          validated.push(entity);
          continue;
        }
        // Rule 4: Keep if single capitalized word (potential company/location name)
        // "CommerceFlow", "California", "Texas"
        if (entity.match(/^[A-Z][a-z]{2,}$/) &amp;amp;&amp;amp; entityLower.length &amp;gt; 4) {
          // But reject if it's a known category word
          if (REJECT_PATTERNS.departments.has(entityLower) || REJECT_PATTERNS.singulars.has(entityLower) || REJECT_PATTERNS.descriptors.has(entityLower)) {
            console.log(`[Router Validation] REJECTED (category word): "${entity}"`);
            continue;
          }
          console.log(`[Router Validation] ACCEPTED (capitalized term): "${entity}"`);
          validated.push(entity);
          continue;
        }
        // Rule 5: Reject if it's a known verb
        if (REJECT_PATTERNS.verbs.has(entityLower)) {
          console.log(`[Router Validation] REJECTED (verb): "${entity}"`);
          continue;
        }
        // Rule 6: Reject if it's a question word
        if (REJECT_PATTERNS.questions.has(entityLower)) {
          console.log(`[Router Validation] REJECTED (question word): "${entity}"`);
          continue;
        }
        // Rule 7: Reject if it's a plural generic noun
        if (REJECT_PATTERNS.plurals.has(entityLower)) {
          console.log(`[Router Validation] REJECTED (plural noun): "${entity}"`);
          continue;
        }
        // Rule 8: Reject if it's a singular generic noun
        if (REJECT_PATTERNS.singulars.has(entityLower)) {
          console.log(`[Router Validation] REJECTED (singular noun): "${entity}"`);
          continue;
        }
        // Rule 9: Reject if it's a department/category
        if (REJECT_PATTERNS.departments.has(entityLower)) {
          console.log(`[Router Validation] REJECTED (department): "${entity}"`);
          continue;
        }
        // Rule 10: Reject if it's a status/descriptor
        if (REJECT_PATTERNS.descriptors.has(entityLower)) {
          console.log(`[Router Validation] REJECTED (descriptor): "${entity}"`);
          continue;
        }
        // Rule 11: Reject if word appears as-is in query (likely a query word)
        // Exception: proper nouns (capitalized in both)
        if (queryLower.includes(entityLower) &amp;amp;&amp;amp; entity === entityLower) {
          console.log(`[Router Validation] REJECTED (uncapitalized query word): "${entity}"`);
          continue;
        }
        // If we got here, it passed all checks - keep it with warning
        console.log(`[Router Validation] ACCEPTED (passed all checks): "${entity}"`);
        validated.push(entity);
      }
      console.log(`[Router Validation] Final: ${entities.length} → ${validated.length}`);
      return validated;
    }
    // --- HELPERS ---
    async function callGemini(prompt, apiKey, model = "gemini-2.5-flash") {
      try {
        const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`, {
          method: "POST",
          headers: {
            "Content-Type": "application/json"
          },
          body: JSON.stringify({
            contents: [
              {
                parts: [
                  {
                    text: prompt
                  }
                ]
              }
            ]
          })
        });
        const data = await response.json();
        if (!data.candidates) return {};
        const txt = data.candidates[0].content.parts[0].text;
        return JSON.parse(txt.replace(/```

json/g, "").replace(/

```/g, "").trim());
      } catch (e) {
        console.error("Gemini error:", e);
        return {};
      }
    }
    async function getEmbedding(text, apiKey) {
      const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:embedContent?key=${apiKey}`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json"
        },
        body: JSON.stringify({
          model: "models/gemini-embedding-001",
          content: {
            parts: [
              {
                text
              }
            ]
          },
          outputDimensionality: 768
        })
      });
      const data = await response.json();
      return data.embedding &amp;amp;&amp;amp; data.embedding.values || [];
    }
    function extractFastKeywords(query) {
      const stopWords = new Set([
        'the',
        'and',
        'for',
        'with',
        'that',
        'this',
        'what',
        'where',
        'when',
        'who',
        'how',
        'show',
        'list',
        'tell'
      ]);
      return query.replace(/[^\w\s]/g, '').split(/\s+/).filter((w)=&amp;gt;w.length &amp;gt; 3 &amp;amp;&amp;amp; !stopWords.has(w.toLowerCase()));
    }
    // --- MAIN HANDLER ---
    serve(async (req)=&amp;gt;{
      if (req.method === 'OPTIONS') {
        return new Response('ok', {
          headers: CORS
        });
      }
      // NEW: toggle debug output from request body
      let debugEnabled = false;
      let debugInfo = {
        logs: []
      };
      const log = (msg, data)=&amp;gt;{
        if (!debugEnabled) return; // no-op when debug is off
        debugInfo.logs.push({
          msg,
          data
        });
      };
      let resultLimit = 20;
      try {
        const body = await req.json();
        const query = body.query;
        // You can make this stricter/looser if you want
        debugEnabled = body.debug === true;
        if (body.limit) resultLimit = body.limit;
        const env = Deno.env.toObject();
        const supabase = createClient(env.SUPABASE_URL, env.SUPABASE_SERVICE_ROLE_KEY);
        const config = await getConfig(supabase);
        // V8: DYNAMIC SCHEMA LOADING
        console.log("[V8] Loading available schemas...");
        const schemas = await getAvailableSchemas(supabase);
        log("AVAILABLE_SCHEMAS", schemas);
        // V9: COMPOSITE ENRICHMENT
        const enrichmentPromise = enrichQueryContext(query, supabase, log);
        // ROUTER
        const routerPrompt = buildRouterPrompt(schemas, query);
        const embeddingPromise = getEmbedding(query, env.GOOGLE_API_KEY);
        const routerPromise = callGemini(routerPrompt, env.GOOGLE_API_KEY, config.model_router);
        const fastKeywords = extractFastKeywords(query);
        const [embedding, routerRes, enrichedData] = await Promise.all([
          embeddingPromise,
          routerPromise,
          enrichmentPromise
        ]);
        // ✅ V10: VALIDATE ROUTER ENTITIES
        const originalEntities = routerRes.entities || [];
        const validatedEntities = validateRouterEntities(originalEntities, query);
        // Update router response with validated entities
        routerRes.entities = validatedEntities;
        debugInfo.router = {
          ...routerRes,
          entity_validation: {
            before: originalEntities,
            after: validatedEntities,
            rejected: originalEntities.filter((e)=&amp;gt;!validatedEntities.includes(e))
          }
        };
        log("ROUTER_DECISION", debugInfo.router);
        // ✅ MERGE: Combine entity detection + router entities
        const allEntities = [];
        // Add entities from detection (O00062, CU006, etc.)
        if (enrichedData &amp;amp;&amp;amp; enrichedData.length &amp;gt; 0) {
          // Entity detection already found these
          const detectedIds = enrichedData.filter((e)=&amp;gt;e.enrichment_type === 'primary').map((e)=&amp;gt;e.row_data.order_id || e.row_data.customer_id || e.row_data.employee_id).filter(Boolean);
          allEntities.push(...detectedIds);
        }
        // Add entities from router (person names, roles, etc.)
        if (routerRes.entities &amp;amp;&amp;amp; routerRes.entities.length &amp;gt; 0) {
          allEntities.push(...routerRes.entities);
        }
        // Add keywords as fallback (for queries like "employees in warehouse")
        if (allEntities.length === 0 &amp;amp;&amp;amp; routerRes.keywords &amp;amp;&amp;amp; routerRes.keywords.length &amp;gt; 0) {
          allEntities.push(...routerRes.keywords.slice(0, 3)); // Top 3 keywords
        }
        log("MERGED_ENTITIES", {
          count: allEntities.length,
          entities: allEntities
        });
        // ✅ ENTITY VALIDATION: Filter out generic terms
        const specificEntities = allEntities.filter((entity)=&amp;gt;{
          const entityLower = entity.toLowerCase().trim();
          // 1. Keep if matches ID pattern (O00062, CU006, E001, WH001, CR001)
          if (entity.match(/^[A-Z]{1,3}\d{3,5}$/)) {
            return true;
          }
          // 2. Keep if proper name (has space + capitalized words)
          // "Sarah Brooks", "Nicholas Cooper", "Brand 06"
          if (entity.match(/^[A-Z][a-z]+(\s+[A-Z][a-z0-9]+)+$/)) {
            return true;
          }
          // 3. Reject if generic table name
          const genericTableNames = [
            'employee',
            'employees',
            'customer',
            'customers',
            'order',
            'orders',
            'warehouse',
            'warehouses',
            'carrier',
            'carriers',
            'product',
            'products',
            'item',
            'items',
            'user',
            'users'
          ];
          if (genericTableNames.includes(entityLower)) {
            return false;
          }
          // 4. Reject if generic concept
          const genericConcepts = [
            'process',
            'procedure',
            'system',
            'policy',
            'onboarding',
            'training',
            'support',
            'service',
            'department',
            'role',
            'location',
            'region',
            'manager',
            'staff',
            'team',
            'people'
          ];
          if (genericConcepts.includes(entityLower)) {
            return false;
          }
          // 5. Keep if single capitalized word (might be company name)
          // "CommerceFlow", "California", "Texas"
          if (entity.match(/^[A-Z][a-z]+$/) &amp;amp;&amp;amp; entity.length &amp;gt; 3) {
            return true;
          }
          // 6. Default: reject
          return false;
        });
        log("ENTITY_VALIDATION", {
          before: allEntities,
          after: specificEntities,
          filtered_out: allEntities.filter((e)=&amp;gt;!specificEntities.includes(e)),
          count_before: allEntities.length,
          count_after: specificEntities.length
        });
        const searchTasks = [];
        // TASK A: VECTOR
        if (embedding &amp;amp;&amp;amp; embedding.length &amp;gt; 0) {
          searchTasks.push(supabase.rpc('search_vector', {
            p_embedding: embedding,
            p_limit: 20,
            p_threshold: config.min_vector_score
          }));
        } else {
          searchTasks.push(Promise.resolve([]));
        }
        // TASK B: FTS
        const ftsQuery = routerRes.keywords &amp;amp;&amp;amp; routerRes.keywords.length &amp;gt; 0 ? routerRes.keywords.join(' ') : query;
        console.log(`[V8] FTS search for: "${ftsQuery}"`);
        searchTasks.push(supabase.rpc('search_fulltext', {
          p_query: ftsQuery,
          p_limit: 20
        }));
        // TASK C: GRAPH (Entity Neighborhood)
        if (specificEntities.length &amp;gt; 0) {
          searchTasks.push((async ()=&amp;gt;{
            console.log(`[V8] Searching graph for entities:`, allEntities);
            const { data, error } = await supabase.rpc('get_graph_neighborhood', {
              p_entity_names: specificEntities
            });
            log("GRAPH_RAW", {
              count: data &amp;amp;&amp;amp; data.length || 0,
              sample: data &amp;amp;&amp;amp; data[0],
              error: error &amp;amp;&amp;amp; error.message
            });
            if (error) {
              log("GRAPH_ERROR", error);
              return [];
            }
            // ✅ FILTER OUT GARBAGE EDGES
            const cleanEdges = (data || []).filter((r)=&amp;gt;{
              // Remove self-referential edges (subject == object)
              if (r.subject &amp;amp;&amp;amp; r.object &amp;amp;&amp;amp; r.subject === r.object) {
                console.log(`[GRAPH_FILTER] Removed self-referential: ${r.subject} ${r.action} ${r.object}`);
                return false;
              }
              // Remove edges with "Inferred from carrier_id" context (low quality)
              if (r.context &amp;amp;&amp;amp; r.context.context &amp;amp;&amp;amp; r.context.context.includes('Inferred from carrier_id')) {
                console.log(`[GRAPH_FILTER] Removed low-quality inferred edge: ${r.subject} ${r.action} ${r.object}`);
                return false;
              }
              return true;
            });
            log("GRAPH_FILTERED", {
              before: data &amp;amp;&amp;amp; data.length || 0,
              after: cleanEdges.length,
              removed: (data &amp;amp;&amp;amp; data.length || 0) - cleanEdges.length
            });
            return cleanEdges.map((r)=&amp;gt;({
                ...r,
                _source: `graph (${r.action})`,
                content: `[GRAPH] ${r.subject} ${r.action} ${r.object || ''}. Context: ${r.context &amp;amp;&amp;amp; r.context.context || ''}`
              }));
          })());
        } else {
          searchTasks.push(Promise.resolve([]));
        }
        // TASK D: SQL
        let sqlEntityNames = []; // Track entity names for graph (generic)
        let sqlEntityType = ''; // Track what type of entities
        if (routerRes.sql_tables &amp;amp;&amp;amp; routerRes.sql_tables.length &amp;gt; 0) {
          searchTasks.push((async ()=&amp;gt;{
            const targetTable = routerRes.sql_tables[0];
            console.log(`[V8] Peeking at table: ${targetTable}`);
            const allTableNames = [
              targetTable,
              ...routerRes.sql_tables.filter((t)=&amp;gt;t !== targetTable)
            ];
            const allContexts = await Promise.all(allTableNames.map((tableName)=&amp;gt;supabase.rpc('get_table_context', {
                p_table_name: tableName
              })));
            const context = allContexts[0].data; // Primary table
            const relatedSchemas = allContexts.slice(1).map((c)=&amp;gt;c.data).filter(Boolean);
            if (!context || context.error) {
              log("PEEK_ERROR", context?.error || "Failed to fetch table context");
              return [];
            }
            log("TABLE_CONTEXT", context);
            const sqlPrompt = `PostgreSQL Query Generator


            Query: "${query}"


            PRIMARY TABLE: ${context.table}
            Schema: ${context.schema}
            ${context.description ? `Purpose: ${context.description}` : ''}


            ${relatedSchemas.length &amp;gt; 0 ? `
            RELATED TABLES:
            ${relatedSchemas.map((s)=&amp;gt;`${s.table}: ${s.schema}`).join('\n')}
            ` : ''}


            ${context.related_tables &amp;amp;&amp;amp; context.related_tables.length &amp;gt; 0 ? `
            JOINS (copy exact syntax):
            ${context.related_tables.map((rt)=&amp;gt;`${rt.join_on}`).join('\n')}
            ` : ''}


            🚨 MANDATORY RULES:


            1. DATE CONVERSION (start_date, order_date are Excel serials):
              ✅ CORRECT: WHERE EXTRACT(YEAR FROM (DATE '1899-12-30' + start_date::int)) = 2022
              ❌ WRONG: WHERE LEFT(start_date::text, 4) = '2022'
              ❌ WRONG: WHERE TO_TIMESTAMP(order_date) &amp;gt;= '2024-01-01'
              ❌ WRONG: WHERE start_date &amp;lt; 2015


            2. TYPE CASTING (all aggregates):
              - SUM(col::numeric)::double precision
              - AVG(col::numeric)::double precision  
              - COUNT(*)::double precision
              - MIN/MAX(col::numeric)::double precision


            3. EXACT COLUMN NAMES (from schemas above):
              - order_status, warehouse_id, order_value_usd
              - NOT: status, id, value


            4. LIST QUERIES (contains "list", "show", "find", "display"):
              ✅ CORRECT: SELECT name, role, department, location, email, employee_id FROM...
              ❌ WRONG: SELECT name FROM...
              - Return ALL identifying + descriptive columns
              - For employees: name, role, department, location, email, employee_id
              - For customers: customer_id, customer_name, status, industry, location
              - For orders: order_id, customer_name, order_status, order_value_usd, order_date


            5. INCLUDE FILTER COLUMNS (CRITICAL):
              If query filters by a column, you MUST include it (or computed version) in SELECT.
              
              ✅ CORRECT Examples:
              Query: "employees who started before 2015"
              SELECT name, role, department, location, email, employee_id,
                     EXTRACT(YEAR FROM (DATE '1899-12-30' + start_date::int)) as start_year
              FROM tbl_employees
              WHERE EXTRACT(YEAR FROM (DATE '1899-12-30' + start_date::int)) &amp;lt; 2015
              
              Query: "orders over $1000"
              SELECT order_id, customer_name, order_status, order_value_usd
              FROM tbl_orders
              WHERE order_value_usd &amp;gt; 1000
              
              Query: "customers in California"
              SELECT customer_id, customer_name, status, location
              FROM tbl_customers
              WHERE location ILIKE '%California%'
              
              WHY: Users need to verify results match the filter criteria.
              RULE: WHERE clause column → must appear in SELECT


            Output: {"sql": "..."}`;
            const sqlGen = await callGemini(sqlPrompt, env.GOOGLE_API_KEY, config.model_sql);
            log("SQL_GENERATED", sqlGen);
            log("SQL_PROMPT_LENGTH", {
              chars: sqlPrompt.length,
              lines: sqlPrompt.split('\n').length
            });
            if (!sqlGen || !sqlGen.sql) {
              log("SQL_GENERATION_FAILED", {
                reason: "Gemini returned empty or invalid response",
                response: sqlGen
              });
            }
            if (sqlGen.sql) {
              const cleanSql = sqlGen.sql.replace(/```

sql/g, "").replace(/

```/g, "").trim();
              const { data, error } = await supabase.rpc('search_structured', {
                p_query_sql: cleanSql,
                p_limit: 20
              });
              if (error || data &amp;amp;&amp;amp; data[0] &amp;amp;&amp;amp; data[0].table_name === 'ERROR') {
                log("SQL_ERROR", error || data &amp;amp;&amp;amp; data[0]);
                return [
                  {
                    _source: "structured_error",
                    error: error || data &amp;amp;&amp;amp; data[0]
                  }
                ];
              }
              // ✅ V10: Validate SQL results have expected fields
              if (data &amp;amp;&amp;amp; data.length &amp;gt; 0 &amp;amp;&amp;amp; data[0].row_data) {
                const firstRow = data[0].row_data;
                const columns = Object.keys(firstRow);
                const queryLower = query.toLowerCase();
                // Check for date fields in temporal queries
                if (queryLower.match(/\b(hired|year|month|date|when|2022|2023|2024)\b/)) {
                  const hasDateField = columns.some((col)=&amp;gt;col.includes('date') || col.includes('hire') || col.includes('start'));
                  if (!hasDateField) {
                    log("SQL_VALIDATION_WARNING", {
                      issue: "temporal_query_missing_date",
                      query: query,
                      columns: columns,
                      suggestion: "SQL should include converted date field"
                    });
                  }
                }
                // Check for name fields in employee queries
                if (targetTable.includes('employee') &amp;amp;&amp;amp; !columns.includes('name')) {
                  log("SQL_VALIDATION_WARNING", {
                    issue: "employee_query_missing_name",
                    query: query,
                    columns: columns
                  });
                }
                // Check for status/value fields in order queries
                if (targetTable.includes('order') &amp;amp;&amp;amp; queryLower.match(/\b(status|value|price|amount)\b/)) {
                  const hasStatusOrValue = columns.some((col)=&amp;gt;col.includes('status') || col.includes('value') || col.includes('amount'));
                  if (!hasStatusOrValue) {
                    log("SQL_VALIDATION_WARNING", {
                      issue: "order_query_missing_status_or_value",
                      query: query,
                      columns: columns
                    });
                  }
                }
                log("SQL_VALIDATION_PASSED", {
                  columns: columns,
                  row_count: data.length
                });
              }
              // ✅ GENERIC ENTITY EXTRACTION (works for ANY table/columns)
              if (data &amp;amp;&amp;amp; data.length &amp;gt; 0) {
                const firstRow = data[0].row_data || {};
                const columns = Object.keys(firstRow);
                // Try common identifier columns in priority order
                const identifierColumns = [
                  'name',
                  'customer_name',
                  'brand_name',
                  'order_id',
                  'customer_id',
                  'employee_id',
                  'product_id',
                  'warehouse_id',
                  'title',
                  'project_name' // Generic identifiers
                ];
                // Find first column that exists
                const identifierColumn = identifierColumns.find((col)=&amp;gt;columns.includes(col));
                if (identifierColumn) {
                  sqlEntityNames = data.map((row)=&amp;gt;row.row_data &amp;amp;&amp;amp; row.row_data[identifierColumn]).filter(Boolean);
                  sqlEntityType = identifierColumn.replace(/_id$/, '').replace(/_name$/, '');
                  log("SQL_ENTITY_NAMES", {
                    column: identifierColumn,
                    type: sqlEntityType,
                    count: sqlEntityNames.length,
                    sample: sqlEntityNames.slice(0, 3)
                  });
                }
              }
              return data || [];
            }
            return [];
          })());
        } else {
          searchTasks.push(Promise.resolve([]));
        }
        // WAIT FOR ALL SEARCHES
        const [vectorRes, ftsRes, graphRes, sqlResults] = await Promise.all(searchTasks);
        // ✅ SECOND GRAPH PASS: Use SQL-derived entity names
        let entityGraphRes = [];
        if (sqlEntityNames.length &amp;gt; 0) {
          // Validate these are real names, not generic terms
          const validSqlNames = sqlEntityNames.filter((name)=&amp;gt;{
            return name &amp;amp;&amp;amp; name.length &amp;gt; 2 &amp;amp;&amp;amp; ![
              'employee',
              'customer',
              'order',
              'warehouse'
            ].includes(name.toLowerCase());
          });
          if (validSqlNames.length &amp;gt; 0) {
            console.log(`[V8] Second graph pass with SQL ${sqlEntityType}:`, validSqlNames);
            const { data: entityGraphData, error: entityGraphError } = await supabase.rpc('get_graph_neighborhood', {
              p_entity_names: validSqlNames
            });
            if (!entityGraphError &amp;amp;&amp;amp; entityGraphData &amp;amp;&amp;amp; entityGraphData.length &amp;gt; 0) {
              // Filter out garbage edges
              const cleanEntityEdges = entityGraphData.filter((r)=&amp;gt;{
                if (r.subject &amp;amp;&amp;amp; r.object &amp;amp;&amp;amp; r.subject === r.object) return false;
                if (r.context &amp;amp;&amp;amp; r.context.context &amp;amp;&amp;amp; r.context.context.includes('Inferred from carrier_id')) return false;
                return true;
              });
              entityGraphRes = cleanEntityEdges.map((r)=&amp;gt;({
                  ...r,
                  _source: `graph (${r.action})`,
                  content: `[GRAPH] ${r.subject} ${r.action} ${r.object || ''}. Context: ${r.context &amp;amp;&amp;amp; r.context.context || ''}`
                }));
              log("ENTITY_GRAPH", {
                type: sqlEntityType,
                count: entityGraphRes.length,
                sample: entityGraphRes[0]
              });
            }
          } else {
            log("ENTITY_GRAPH_SKIPPED", {
              reason: "no_valid_sql_names",
              filtered_out: sqlEntityNames
            });
          }
        }
        // Log all search results
        log("VECTOR_RAW", {
          count: Array.isArray(vectorRes) ? vectorRes.length : vectorRes &amp;amp;&amp;amp; vectorRes.data &amp;amp;&amp;amp; vectorRes.data.length || 0
        });
        log("FTS_RAW", {
          count: Array.isArray(ftsRes) ? ftsRes.length : ftsRes &amp;amp;&amp;amp; ftsRes.data &amp;amp;&amp;amp; ftsRes.data.length || 0
        });
        log("GRAPH_EDGES", {
          count: Array.isArray(graphRes) ? graphRes.length : graphRes &amp;amp;&amp;amp; graphRes.data &amp;amp;&amp;amp; graphRes.data.length || 0,
          sample: Array.isArray(graphRes) ? graphRes[0] : graphRes &amp;amp;&amp;amp; graphRes.data &amp;amp;&amp;amp; graphRes.data[0]
        });
        log("SQL_RAW", {
          count: Array.isArray(sqlResults) ? sqlResults.length : 0
        });
        // ✅ COMBINE VECTOR + FTS &amp;amp; DEDUPLICATE
        const vectorItems = Array.isArray(vectorRes) ? vectorRes : vectorRes &amp;amp;&amp;amp; vectorRes.data || [];
        const ftsItems = Array.isArray(ftsRes) ? ftsRes : ftsRes &amp;amp;&amp;amp; ftsRes.data || [];
        const combinedDocs = [
          ...vectorItems,
          ...ftsItems
        ];
        const uniqueDocsMap = new Map();
        combinedDocs.forEach((doc)=&amp;gt;{
          if (doc.chunk_id &amp;amp;&amp;amp; !uniqueDocsMap.has(doc.chunk_id)) {
            uniqueDocsMap.set(doc.chunk_id, doc);
          }
        });
        log("BEFORE_RERANK", {
          count: uniqueDocsMap.size
        });
        log("BEFORE_RERANK_IDS", {
          chunk_ids: Array.from(uniqueDocsMap.keys())
        });
        // ✅ V10: SELECTIVE RERANKING - Only rerank for policy/document queries
        // Skip reranking when we have structured results (SQL/entities/enrichment)
        const hasStructuredResults = routerRes.sql_tables &amp;amp;&amp;amp; routerRes.sql_tables.length &amp;gt; 0 || enrichedData.length &amp;gt; 0 || Array.isArray(sqlResults) &amp;amp;&amp;amp; sqlResults.length &amp;gt; 0;
        let rerankedDocs;
        if (hasStructuredResults) {
          // Structured query: Keep top 15 docs without reranking (reranker often removes critical context)
          rerankedDocs = Array.from(uniqueDocsMap.values()).slice(0, 15);
          log("RERANK_SKIPPED", {
            reason: "structured_query_detected",
            keeping: rerankedDocs.length
          });
        } else {
          // Policy/document query: Use reranker with safety net
          // SAFETY NET: Always preserve top 5 by original score
          const allDocs = Array.from(uniqueDocsMap.values());
          const topByScore = allDocs.slice(0, 5);
          // Send up to 30 docs to reranker
          const rerankerResults = await rerankWithGemini(query, allDocs, env.GOOGLE_API_KEY, config.model_reranker, Math.min(uniqueDocsMap.size, 30));
          // Merge: reranker results + top 5 safety net (deduplicated)
          const merged = [
            ...rerankerResults
          ];
          let safetyNetAdded = 0;
          topByScore.forEach((doc)=&amp;gt;{
            if (!merged.find((d)=&amp;gt;d.chunk_id === doc.chunk_id)) {
              merged.push(doc);
              safetyNetAdded++;
            }
          });
          // Keep top 15 from merged results
          rerankedDocs = merged.slice(0, 15);
          log("RERANK_EXECUTED", {
            reason: "policy_query_detected",
            reranker_kept: rerankerResults.length,
            safety_net_added: safetyNetAdded,
            final_count: rerankedDocs.length,
            fallback_used: rerankerResults.length === 0
          });
        }
        log("AFTER_RERANK", {
          count: rerankedDocs.length
        });
        // ✅ CONDITIONAL GRAPH: Use entity-specific OR generic, NOT both
        const graphItems = Array.isArray(graphRes) ? graphRes : graphRes &amp;amp;&amp;amp; graphRes.data || [];
        // If we have entity-specific edges, DROP generic graph entirely
        const allGraphItems = entityGraphRes.length &amp;gt; 0 ? entityGraphRes // ✅ Use ONLY entity-specific edges (high quality)
         : graphItems; // Fallback to generic (if no entities)
        log("GRAPH_STRATEGY", {
          strategy: entityGraphRes.length &amp;gt; 0 ? "entity-specific" : "generic",
          entity_type: sqlEntityType || 'none',
          entity_count: entityGraphRes.length,
          generic_count: graphItems.length,
          using: allGraphItems.length
        });
        // ✅ V10: Filter graph edges for relevance BEFORE fusion
        const filteredGraphItems = allGraphItems;
        log("GRAPH_RELEVANCE_FILTER", {
          before: allGraphItems.length,
          after: filteredGraphItems.length,
          removed: allGraphItems.length - filteredGraphItems.length
        });
        const fusedResults = await performRRFFusion(rerankedDocs, filteredGraphItems, sqlResults, config);
        log("AFTER_FUSION", {
          count: fusedResults.length
        });
        // ✅ FILTER &amp;amp; DIVERSIFY GRAPH EDGES
        // 1. Filter redundant attributes (don't repeat what SQL already shows)
        // 2. Deduplicate by entity (show diverse entities, not 4 facts about 1 person)
        const RELATIONSHIP_ACTIONS = [
          'REPORTS_TO',
          'MANAGES',
          'ASSIGNED_TO',
          'WORKS_WITH',
          'PLACED_BY',
          'FULFILLED_FROM',
          'SHIPPED_BY'
        ];
        const ATTRIBUTE_ACTIONS = [
          'HAS_ROLE',
          'BELONGS_TO',
          'WORKS_AT',
          'HAS_STATUS'
        ];
        // Check what SQL already has
        const sqlHasRole = sqlResults.some((r)=&amp;gt;r.row_data &amp;amp;&amp;amp; r.row_data.role);
        const sqlHasDepartment = sqlResults.some((r)=&amp;gt;r.row_data &amp;amp;&amp;amp; (r.row_data.department || r.row_data.dept));
        const sqlHasLocation = sqlResults.some((r)=&amp;gt;r.row_data &amp;amp;&amp;amp; r.row_data.location);
        // Filter redundant edges
        const valuableEdges = filteredGraphItems.filter((edge)=&amp;gt;{
          // Keep all relationship edges (these ADD new info)
          if (RELATIONSHIP_ACTIONS.includes(edge.action)) return true;
          // Filter redundant attributes
          if (edge.action === 'HAS_ROLE' &amp;amp;&amp;amp; sqlHasRole) return false;
          if (edge.action === 'BELONGS_TO' &amp;amp;&amp;amp; sqlHasDepartment) return false;
          if (edge.action === 'WORKS_AT' &amp;amp;&amp;amp; sqlHasLocation) return false;
          // Keep everything else
          return true;
        });
        log("FILTERED_REDUNDANT", {
          before: filteredGraphItems.length,
          after: valuableEdges.length,
          removed: filteredGraphItems.length - valuableEdges.length,
          sql_has: {
            role: sqlHasRole,
            department: sqlHasDepartment,
            location: sqlHasLocation
          }
        });
        // Group by entity (subject)
        const edgesByEntity = new Map();
        valuableEdges.forEach((edge)=&amp;gt;{
          const subject = edge.subject || 'unknown';
          if (!edgesByEntity.has(subject)) {
            edgesByEntity.set(subject, []);
          }
          edgesByEntity.get(subject).push(edge);
        });
        // Take 1 edge per entity (prioritize relationships)
        const diverseGraphEdges = [];
        for (const [subject, edges] of edgesByEntity){
          // Prioritize relationship edges over attributes
          const sorted = edges.sort((a, b)=&amp;gt;{
            const relPriority = {
              'REPORTS_TO': 10,
              'MANAGES': 9,
              'ASSIGNED_TO': 8,
              'PLACED_BY': 7,
              'FULFILLED_FROM': 6,
              'SHIPPED_BY': 5,
              'BELONGS_TO': 3,
              'WORKS_AT': 2,
              'HAS_ROLE': 1
            };
            return (relPriority[b.action] || 0) - (relPriority[a.action] || 0);
          });
          diverseGraphEdges.push(sorted[0]); // Take best edge for this entity
          if (diverseGraphEdges.length &amp;gt;= 10) break; // Max 10 diverse entities
        }
        // Score them for proper ranking
        const scoredGraphEdges = diverseGraphEdges.map((e, idx)=&amp;gt;({
            ...e,
            score: config.rrf_weight_graph + (10 - idx) * 0.05
          }));
        log("DIVERSIFIED_GRAPH", {
          total_edges: filteredGraphItems.length,
          after_filter: valuableEdges.length,
          unique_entities: edgesByEntity.size,
          selected: scoredGraphEdges.length,
          sample: scoredGraphEdges[0]
        });
        // ✅ ADD ENRICHED DATA AT TOP
        const finalResults = [
          ...enrichedData.map((e)=&amp;gt;({
              ...e,
              score: config.rrf_weight_enrichment
            })),
          ...scoredGraphEdges,
          ...fusedResults
        ];
        // ✅ DEDUPLICATE by content (avoid showing same edge/doc twice)
        const seenKeys = new Set();
        const dedupedResults = finalResults.filter((item)=&amp;gt;{
          // Generate unique key
          const key = item.chunk_id ? `chunk_${item.chunk_id}` : item.subject &amp;amp;&amp;amp; item.action &amp;amp;&amp;amp; item.object ? `graph_${item.subject}_${item.action}_${item.object}` : JSON.stringify(item.row_data || item);
          if (seenKeys.has(key)) return false;
          seenKeys.add(key);
          return true;
        });
        dedupedResults.sort((a, b)=&amp;gt;b.score - a.score);
        const payload = {
          success: true,
          results: dedupedResults.slice(0, resultLimit),
          enrichment_count: enrichedData.length
        };
        // Only include diagnostics when explicitly requested
        if (debugEnabled) {
          payload.debug = debugInfo;
        }
        return new Response(JSON.stringify(payload), {
          headers: {
            ...CORS,
            'Content-Type': 'application/json'
          }
        });
      } catch (error) {
        console.error("[V9] Search error:", error);
        const errorPayload = {
          error: error?.message || "Unknown error"
        };
        if (debugEnabled) {
          errorPayload.debug = debugInfo;
        }
        return new Response(JSON.stringify(errorPayload), {
          status: 500,
          headers: CORS
        });
      }
    });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 7&lt;/strong&gt;. Now you can interact with your Supabase functions using the api endpoints ingest-intelligent and search (ingest-worker is called by ingest-intelligent). You can do that with any REST API call. I've created a couple of n8n workflows that facilitate this. First, here's two workflows for ingestion (one for documents and one for spreadsheets). If you have n8n, copy and paste this json into a canvas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    {
      "name": "Context Mesh V2 - File Uploader",
      "nodes": [
        {
          "parameters": {
            "path": "doc-upload-v2-robust",
            "formTitle": "Upload Document",
            "formDescription": "Upload text files (PDF, TXT, or MD) to add to the knowledge base",
            "formFields": {
              "values": [
                {
                  "fieldLabel": "data",
                  "fieldType": "file",
                  "requiredField": true
                },
                {
                  "fieldLabel": "Title (use logical title name)"
                }
              ]
            },
            "options": {}
          },
          "type": "n8n-nodes-base.formTrigger",
          "typeVersion": 2,
          "position": [
            1232,
            -80
          ],
          "id": "da526682-dde5-4245-ac50-2a337c71dad6",
          "name": "Document Upload Form",
          "webhookId": "doc-upload-v2-robust"
        },
        {
          "parameters": {
            "operation": "text",
            "options": {}
          },
          "type": "n8n-nodes-base.extractFromFile",
          "typeVersion": 1,
          "position": [
            1456,
            -80
          ],
          "id": "8b526c4c-6773-43cd-b19a-ce70fb75f518",
          "name": "Extract Text"
        },
        {
          "parameters": {
            "method": "POST",
            "url": "=https://zbtqpvkaycnonaslwqfq.supabase.co/functions/v1/ingest-intelligent",
            "authentication": "predefinedCredentialType",
            "nodeCredentialType": "supabaseApi",
            "sendBody": true,
            "bodyParameters": {
              "parameters": [
                {
                  "name": "uri",
                  "value": "={{ $('Document Upload Form').item.json.data[0].filename }}"
                },
                {
                  "name": "title",
                  "value": "={{ $('Document Upload Form').item.json['Title (optional)'] }}"
                },
                {
                  "name": "text",
                  "value": "={{ $json.data }}"
                }
              ]
            },
            "options": {}
          },
          "type": "n8n-nodes-base.httpRequest",
          "typeVersion": 4,
          "position": [
            1680,
            -80
          ],
          "id": "5e2c69f6-4312-4b23-8881-37ee9ad0c360",
          "name": "Ingest Document Chunk",
          "retryOnFail": true,
          "waitBetweenTries": 5000,
          "credentials": {
            "supabaseApi": {
              "id": "L1c6TGVJOHc8wt9H",
              "name": "infoSupa_contentMesh"
            }
          }
        },
        {
          "parameters": {
            "content": "## This is for documents (txt, pdf, md) \n",
            "width": 192
          },
          "type": "n8n-nodes-base.stickyNote",
          "position": [
            1120,
            -192
          ],
          "typeVersion": 1,
          "id": "3e1eeb6f-fe73-4676-b714-e35008d2b27a",
          "name": "Sticky Note"
        },
        {
          "parameters": {
            "content": "## This node extracts from txt files\n**Change this to extract from whatever file type you wish to upload. 'Extract from Text File' for .txt or .md. 'Extract from PDF' for .pdf.**\n",
            "height": 208,
            "color": 3
          },
          "type": "n8n-nodes-base.stickyNote",
          "position": [
            1376,
            -256
          ],
          "typeVersion": 1,
          "id": "b82b51c2-aa31-4ec3-b262-4c175da20e26",
          "name": "Sticky Note1"
        },
        {
          "parameters": {
            "content": "## Make sure your Supabase Credentials are saved in n8n\n",
            "height": 176,
            "color": 6
          },
          "type": "n8n-nodes-base.stickyNote",
          "position": [
            1856,
            48
          ],
          "typeVersion": 1,
          "id": "58314655-fd7a-48d5-921d-11aba1b11333",
          "name": "Sticky Note2"
        },
        {
          "parameters": {
            "content": "## This is for spreadsheets (csv, xls, xlsx) \n",
            "width": 192
          },
          "type": "n8n-nodes-base.stickyNote",
          "position": [
            1088,
            192
          ],
          "typeVersion": 1,
          "id": "04447f73-111d-45c7-a37b-d81e08cb5189",
          "name": "Sticky Note3"
        },
        {
          "parameters": {
            "content": "## This node extracts from spreadsheet files\n**Change this to extract from whatever file type you wish to upload. 'Extract from CSV', 'Extract from XLS', etc**\n",
            "height": 208,
            "color": 3
          },
          "type": "n8n-nodes-base.stickyNote",
          "position": [
            1376,
            112
          ],
          "typeVersion": 1,
          "id": "d23b51ce-557b-4811-82f4-137b6592f0ff",
          "name": "Sticky Note4"
        },
        {
          "parameters": {
            "path": "sheet-upload-v3",
            "formTitle": "Upload Spreadsheet (V3)",
            "formDescription": "Upload CSV or Excel files. The system handles large files automatically.",
            "formFields": {
              "values": [
                {
                  "fieldLabel": "data",
                  "fieldType": "file",
                  "requiredField": true
                },
                {
                  "fieldLabel": "Table Name (e.g. sales_data)",
                  "requiredField": true
                }
              ]
            },
            "options": {}
          },
          "type": "n8n-nodes-base.formTrigger",
          "typeVersion": 2,
          "position": [
            1216,
            304
          ],
          "id": "d6e7a240-e165-4ed4-9310-07d1a59944e1",
          "name": "Spreadsheet Upload Form",
          "webhookId": "sheet-upload-v3"
        },
        {
          "parameters": {
            "operation": "xlsx",
            "options": {}
          },
          "type": "n8n-nodes-base.extractFromFile",
          "typeVersion": 1,
          "position": [
            1440,
            304
          ],
          "id": "8e4447c8-22db-4cc3-a6a5-8faa8f245f4f",
          "name": "Extract Spreadsheet"
        },
        {
          "parameters": {
            "method": "POST",
            "url": "https://zbtqpvkaycnonaslwqfq.supabase.co/functions/v1/ingest-intelligent",
            "authentication": "predefinedCredentialType",
            "nodeCredentialType": "supabaseApi",
            "sendBody": true,
            "bodyParameters": {
              "parameters": [
                {
                  "name": "uri",
                  "value": "={{ $('Spreadsheet Upload Form').item.json.data[0].filename }}"
                },
                {
                  "name": "title",
                  "value": "={{ $('Spreadsheet Upload Form').item.json['Table Name (e.g. sales_data)'] }}"
                },
                {
                  "name": "data",
                  "value": "={{ $json.data }}"
                }
              ]
            },
            "options": {}
          },
          "type": "n8n-nodes-base.httpRequest",
          "typeVersion": 4,
          "position": [
            1888,
            304
          ],
          "id": "c4130f58-b1a7-4d58-a338-7f481b89a695",
          "name": "Send to Context Mesh",
          "retryOnFail": true,
          "waitBetweenTries": 5000,
          "credentials": {
            "supabaseApi": {
              "id": "L1c6TGVJOHc8wt9H",
              "name": "infoSupa_contentMesh"
            }
          }
        },
        {
          "parameters": {
            "aggregate": "aggregateAllItemData",
            "options": {}
          },
          "type": "n8n-nodes-base.aggregate",
          "typeVersion": 1,
          "position": [
            1664,
            304
          ],
          "id": "5840c145-0511-4cec-8b70-3d5d373e2556",
          "name": "Aggregate"
        }
      ],
      "pinData": {},
      "connections": {
        "Document Upload Form": {
          "main": [
            [
              {
                "node": "Extract Text",
                "type": "main",
                "index": 0
              }
            ]
          ]
        },
        "Extract Text": {
          "main": [
            [
              {
                "node": "Ingest Document Chunk",
                "type": "main",
                "index": 0
              }
            ]
          ]
        },
        "Ingest Document Chunk": {
          "main": [
            []
          ]
        },
        "Spreadsheet Upload Form": {
          "main": [
            [
              {
                "node": "Extract Spreadsheet",
                "type": "main",
                "index": 0
              }
            ]
          ]
        },
        "Extract Spreadsheet": {
          "main": [
            [
              {
                "node": "Aggregate",
                "type": "main",
                "index": 0
              }
            ]
          ]
        },
        "Aggregate": {
          "main": [
            [
              {
                "node": "Send to Context Mesh",
                "type": "main",
                "index": 0
              }
            ]
          ]
        }
      },
      "active": false,
      "settings": {
        "executionOrder": "v1"
      },
      "versionId": "dc8d82a5-eb54-446f-ba8a-9274469bb70e",
      "meta": {
        "templateCredsSetupCompleted": true,
        "instanceId": "1dbf32ab27f7926a258ac270fe5e9e15871cfb01059a55b25aa401186050b9b5"
      },
      "id": "P9zYEohLKCCgjkym",
      "tags": []
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 8&lt;/strong&gt;. Here's a workflow for the 'search' endpoint. This one is the retrieval. I connected it as a tool to an A.I. agent, so you can just start chatting and reference your data directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    {
      "name": "Context Mesh V2 - Chat Interface",
      "nodes": [
        {
          "parameters": {
            "options": {}
          },
          "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
          "typeVersion": 1,
          "position": [
            304,
            528
          ],
          "id": "9da9a603-da31-460f-9f1a-96e0bb3e9e23",
          "name": "Google Gemini Chat Model",
          "credentials": {
            "googlePalmApi": {
              "id": "YEyGAyg7bHXHutrf",
              "name": "sb_projects"
            }
          }
        },
        {
          "parameters": {
            "toolDescription": "composite_query: query Supabase using edge function that retrieves hybrid vector search, SQL, and knowledge graph all at once.",
            "method": "POST",
            "url": "https://zbtqpvkaycnonaslwqfq.supabase.co/functions/v1/search",
            "authentication": "predefinedCredentialType",
            "nodeCredentialType": "supabaseApi",
            "sendHeaders": true,
            "headerParameters": {
              "parameters": [
                {
                  "name": "Content-Type",
                  "value": "application/json"
                }
              ]
            },
            "sendBody": true,
            "bodyParameters": {
              "parameters": [
                {
                  "name": "query",
                  "value": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('parameters0_Value', ``, 'string') }}"
                },
                {
                  "name": "limit",
                  "value": "20"
                }
              ]
            },
            "options": {}
          },
          "type": "n8n-nodes-base.httpRequestTool",
          "typeVersion": 4.2,
          "position": [
            560,
            528
          ],
          "id": "f02b140b-aaad-48b2-be6c-42ae55a1209f",
          "name": "composite_query",
          "credentials": {
            "supabaseApi": {
              "id": "L1c6TGVJOHc8wt9H",
              "name": "infoSupa_contentMesh"
            }
          }
        },
        {
          "parameters": {
            "options": {
              "systemMessage": "=You have access to a powerful search tool called `composite_query` that searches through a knowledge base using three search methods simultaneously:\n1. **Vector search** - semantic/meaning-based search\n2. **Graph search** - entity and relationship traversal  \n3. **Structured search** - full-text filtering\n\n**When to use this tool:**\n- Whenever the user asks any question\n- When you need factual information to answer questions accurately\n- When the user requests specific filtering or analysis\n\n**How to use this tool:**\nOutput a query_text:\n\n**Required:**\n- `query_text` (string) - The user's question or search terms in natural language\n\n\n**What you'll receive:**\n- `context_block` - Formatted text with source, content, entities, and relationships\n- `entities` - JSON array of relevant entities (people, products, companies, etc.)\n- `relationships` - JSON array showing how entities are connected\n- `relevance` - Indicates which search methods found this result\n\n**Important:** Always use this tool before answering questions. Use the returned context to provide accurate, grounded answers. Reference entities and relationships when relevant.\n"
            }
          },
          "type": "@n8n/n8n-nodes-langchain.agent",
          "typeVersion": 2.2,
          "position": [
            352,
            304
          ],
          "id": "c32fa3a4-d62f-47dc-b0a2-a4c418a30a35",
          "name": "AI Agent"
        },
        {
          "parameters": {
            "options": {}
          },
          "type": "@n8n/n8n-nodes-langchain.chatTrigger",
          "typeVersion": 1.3,
          "position": [
            128,
            304
          ],
          "id": "350f2dcc-4f8b-4dc7-861a-fe661b06348f",
          "name": "When chat message received",
          "webhookId": "873bddf5-f2ee-4ead-afa3-0a09463389ea"
        }
      ],
      "pinData": {},
      "connections": {
        "Google Gemini Chat Model": {
          "ai_languageModel": [
            [
              {
                "node": "AI Agent",
                "type": "ai_languageModel",
                "index": 0
              }
            ]
          ]
        },
        "composite_query": {
          "ai_tool": [
            [
              {
                "node": "AI Agent",
                "type": "ai_tool",
                "index": 0
              }
            ]
          ]
        },
        "When chat message received": {
          "main": [
            [
              {
                "node": "AI Agent",
                "type": "main",
                "index": 0
              }
            ]
          ]
        },
        "AI Agent": {
          "main": [
            []
          ]
        }
      },
      "active": false,
      "settings": {
        "executionOrder": "v1"
      },
      "versionId": "16316f80-7cac-4bd4-b05e-b0c4230a9e85",
      "meta": {
        "templateCredsSetupCompleted": true,
        "instanceId": "1dbf32ab27f7926a258ac270fe5e9e15871cfb01059a55b25aa401186050b9b5"
      },
      "id": "4lKXwzK514XEOuiY",
      "tags": []
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. That's the full Context Mesh Lite. Cheers!&lt;/p&gt;

&lt;p&gt;P.S. if you want these in managable files, use this form here to give me your email and I'll send them to you in a nice zip file:&lt;br&gt;
&lt;a href="https://vmat.fillout.com/context-mesh-lite" rel="noopener noreferrer"&gt;https://vmat.fillout.com/context-mesh-lite&lt;/a&gt;&lt;/p&gt;

</description>
      <category>gemini</category>
      <category>serverless</category>
      <category>database</category>
      <category>ai</category>
    </item>
    <item>
      <title>Beyond Basic RAG: 3 Advanced Architectures I Built to Fix AI Retrieval</title>
      <dc:creator>Anthony Lee</dc:creator>
      <pubDate>Sun, 07 Dec 2025 10:07:11 +0000</pubDate>
      <link>https://vibe.forem.com/anthony_lee_63e96408d7573/beyond-basic-rag-3-advanced-architectures-i-built-to-fix-ai-retrieval-4e5b</link>
      <guid>https://vibe.forem.com/anthony_lee_63e96408d7573/beyond-basic-rag-3-advanced-architectures-i-built-to-fix-ai-retrieval-4e5b</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Everyone builds a "Chat with your Data" bot eventually. But standard RAG fails when data is static (latency), exact (SQL table names), or noisy (Slack logs). Here are the three specific architectural patterns I used to solve those problems across three different products: &lt;strong&gt;Client-side Vector Search&lt;/strong&gt;, &lt;strong&gt;Temporal Graphs&lt;/strong&gt;, and &lt;strong&gt;Heuristic Signal Filtering&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Story
&lt;/h2&gt;

&lt;p&gt;I’ve been building AI-driven tools for a while now. I started in the no-code space, building “A.I. Agents” in n8n. Over the last several months I pivoted to coding solutions, many of which involve or revolve around RAG.&lt;/p&gt;

&lt;p&gt;And like many, I hit the wall.&lt;/p&gt;

&lt;p&gt;The "Hello World" of RAG is easy. But when you try to put it into production—where users want instant answers inside Excel, or need complex context about "when" something happened, or want to query a messy Slack history—the standard pattern breaks down.&lt;/p&gt;

&lt;p&gt;I’ve built three distinct projects recently, each with unique constraints that forced me to abandon the "default" RAG architecture. Here is exactly how I architected them and the specific strategies I used to make them work.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Formula AI (The "Mini" RAG)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Build:&lt;/strong&gt; An add-in for Google Sheets/Excel. The user opens a chat widget, describes what they want to do with their data, and the AI tells them which formula to use and where, writes it for them, and places the formula at the click of a button.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt; Latency and Privacy. Sending every user query to a cloud vector database (like Pinecone or Weaviate) to search a static dictionary of Excel functions is overkill. It introduces network lag and unnecessary costs for a dataset that rarely changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Strategy: Client-Side Vector Search&lt;/strong&gt; I realized the "knowledge base" (the dictionary of Excel/Google functions) is finite. It’s not petabytes of data; it’s a few hundred rows.&lt;/p&gt;

&lt;p&gt;Instead of a remote database, I turned the dataset into a &lt;strong&gt;portable vector search engine&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I took the entire function dictionary.
&lt;/li&gt;
&lt;li&gt;I generated vector embeddings and full-text indexes (tsvector) for every function description.
&lt;/li&gt;
&lt;li&gt;I exported this as a static JSON/binary object.
&lt;/li&gt;
&lt;li&gt;I host that file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When the add-in loads, it fetches this "Mini-DB" once. Now, when the user types, the retrieval happens &lt;strong&gt;locally in the browser&lt;/strong&gt; (or via a super-lightweight edge worker). The LLM receives the relevant formula context instantly without a heavy database query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 60-second mental model:&lt;/strong&gt; &lt;code&gt;[Static Data] -&amp;gt; [Pre-computed Embeddings] -&amp;gt; [JSON File] -&amp;gt; [Client Memory]&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Takeaway:&lt;/strong&gt; &lt;strong&gt;You don't always need a Vector Database.&lt;/strong&gt; If your domain data is under 50MB and static (like documentation, syntax, or FAQs), compute your embeddings beforehand and ship them as a file. It’s faster, cheaper, and privacy-friendly.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Context Mesh (The "Hybrid" Graph)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Build:&lt;/strong&gt; A hybrid retrieval system that combines vector search, full-text retrieval, SQL, and graph search into a single answer. It allows LLMs to query databases intelligently while understanding the relationships between data points.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt; Vector search is terrible at &lt;strong&gt;exactness&lt;/strong&gt; and &lt;strong&gt;time&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If you search for "Order table", vectors might give you "shipping logs" (semantically similar) rather than the actual SQL table &lt;code&gt;tbl_orders_001&lt;/code&gt;.
&lt;/li&gt;
&lt;li&gt;If you search "Why did the server crash?", vectors give you the &lt;em&gt;fact&lt;/em&gt; of the crash, but not the &lt;em&gt;sequence&lt;/em&gt; of events leading up to it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The Strategy: Trigrams + Temporal Graphs&lt;/strong&gt; I approached this with a two-pronged solution:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part A: Trigrams for Structure&lt;/strong&gt; To solve the SQL schema problem, I use &lt;strong&gt;Trigram Similarity&lt;/strong&gt; (specifically &lt;code&gt;pg_trgm&lt;/code&gt; in Postgres). Vectors understand &lt;em&gt;meaning&lt;/em&gt;, but Trigrams understand &lt;em&gt;spelling&lt;/em&gt;. If the LLM needs a table name, we use Trigrams/&lt;code&gt;ilike&lt;/code&gt; to find the exact match, and only use vectors to find the relevant SQL syntax.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part B: The Temporal Graph&lt;/strong&gt; Data isn't just &lt;em&gt;what&lt;/em&gt; happened, but &lt;em&gt;when&lt;/em&gt; and &lt;em&gt;in relation to what&lt;/em&gt;. In a standard vector store, "Server Crash" from 2020 looks the same as "Server Crash" from today. I implemented a lightweight graph where &lt;strong&gt;Time&lt;/strong&gt; and &lt;strong&gt;Events&lt;/strong&gt; are nodes.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[User] --(commented)--&amp;gt; [Ticket] --(happened_at)--&amp;gt; [Event Node: Tuesday 10am]&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;When retrieving, even if the vector match is imperfect, the graph provides "relevant adjacency." We can see that the crash coincided with "Deployment 001" because they share a temporal node in the graph.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Takeaway:&lt;/strong&gt; &lt;strong&gt;Context is relational.&lt;/strong&gt; Don't just chuck text into a vector store. Even a shallow graph (linking Users, Orders, and Time) provides the "connective tissue" that pure vector search misses.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Slack Brain (The "Noise" Filter)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Build:&lt;/strong&gt; A connected knowledge hub inside Slack. It ingests files (PDFs, Videos, CSVs) and chat history, turning them into a queryable brain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt; &lt;strong&gt;Signal to Noise Ratio.&lt;/strong&gt; Slack is 90% noise. "Good morning," "Lunch?", "lol." If you blindly feed all this into an LLM or vector store, you dilute your signal and bankrupt your API credits. Additionally, unstructured data (videos) and structured data (CSVs) need different treatment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Strategy: Heuristic Filtering &amp;amp; Normalization&lt;/strong&gt; I realized we can't rely on the AI to decide what is important—that's too expensive. We need to filter &lt;em&gt;before&lt;/em&gt; we embed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step A: The Heuristic Gate&lt;/strong&gt; We identify "Important Threads" programmatically using a set of rigid rules—&lt;strong&gt;No AI involved yet.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the thread inactive for X hours? (It's finished).
&lt;/li&gt;
&lt;li&gt;Does it have &amp;gt; 1 participant? (It's a conversation, not a monologue).
&lt;/li&gt;
&lt;li&gt;Does it follow a Q&amp;amp;A pattern? (e.g., ends with "Thanks" or "Fixed").
&lt;/li&gt;
&lt;li&gt;Does it contain specific keywords indicating a solution?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only if a thread passes these gates do we pass it to the LLM to summarize and embed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step B: Aggressive Normalization&lt;/strong&gt; To make the LLM's life easier, we reduce all file types to the lowest common denominator:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Documents/Transcripts&lt;/strong&gt; → &lt;code&gt;.md&lt;/code&gt; files (ideal for dense retrieval).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured Data&lt;/strong&gt; → &lt;code&gt;.csv&lt;/code&gt; rows (ideal for code interpreter/analysis).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Takeaway:&lt;/strong&gt; &lt;strong&gt;Don't use AI to filter noise.&lt;/strong&gt; Use code. Simple logical heuristics are free, fast, and surprisingly effective at curating high-quality training data from messy chat logs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;We are moving past the phase of "I sent a prompt to OpenAI and got an answer." The next generation of AI apps requires composite architectures.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Formula AI&lt;/strong&gt; taught me that sometimes the best database is a JSON file in memory.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context Mesh&lt;/strong&gt; taught me that "time" and "spelling" are just as important as semantic meaning.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slack Brain&lt;/strong&gt; taught me that heuristics save your wallet, and strict normalization saves your context.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't be afraid to mix and match. The best retrieval systems aren't pure; they are pragmatic.&lt;/p&gt;

&lt;p&gt;Be well and build good systems.  &lt;/p&gt;

</description>
      <category>ai</category>
      <category>database</category>
      <category>rag</category>
    </item>
    <item>
      <title>How I Created Superior RAG Retrieval With 3 Files in Supabase</title>
      <dc:creator>Anthony Lee</dc:creator>
      <pubDate>Wed, 12 Nov 2025 11:30:35 +0000</pubDate>
      <link>https://vibe.forem.com/anthony_lee_63e96408d7573/how-i-created-superior-rag-retrieval-with-3-files-in-supabase-4o25</link>
      <guid>https://vibe.forem.com/anthony_lee_63e96408d7573/how-i-created-superior-rag-retrieval-with-3-files-in-supabase-4o25</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;br&gt;
Plain RAG (vector + full-text) is great at fetching &lt;em&gt;facts in passages&lt;/em&gt;, but it struggles with &lt;em&gt;relationship answers&lt;/em&gt; (e.g., “How many times has this customer ordered?”). &lt;strong&gt;Context Mesh&lt;/strong&gt; adds a lightweight knowledge graph inside Supabase—so semantic, lexical, and &lt;strong&gt;relational&lt;/strong&gt; context get fused into one ranked result set (via RRF). It’s an opinionated pattern that lives mostly in SQL + Supabase RPCs. If hybrid search hasn’t closed the gap for you, add the graph.&lt;/p&gt;


&lt;h2&gt;
  
  
  The story
&lt;/h2&gt;

&lt;p&gt;I've been somewhat obsessed with RAG and A.I. powered document retrieval for some time. When I first figured out how to set up a vector DB using no-code, I did. When I learned how to set up hybrid retrieval I did. When I taught my A.I. agents how to generate SQL queries, I added that too. Despite those being INCREDIBLY USEFUL when combined, for most business cases it was still missing...something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;br&gt;
Let's say you have a pipeline into your RAG system that updates new order and logistics info (if not...you really should). Now let's say your customer support rep wants to query order #889. What they'll get back is likely all the information for that line-item; person who ordered, their contact info, product, shipping details, etc. &lt;/p&gt;

&lt;p&gt;What you &lt;strong&gt;don’t&lt;/strong&gt; get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;total number of orders by that buyer,&lt;/li&gt;
&lt;li&gt;when they first became a customer,&lt;/li&gt;
&lt;li&gt;lifetime value,&lt;/li&gt;
&lt;li&gt;number of support interactions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can SQL-join your way there—but that’s brittle and time-consuming. A &lt;strong&gt;knowledge graph&lt;/strong&gt; naturally keeps those relationships.&lt;/p&gt;

&lt;p&gt;That's why I've been building what I call the Context Mesh. On the journey I've created a lite version, which exists almost entirely in Supabase and requires only three files to implement (within Supabase, plus additional UI means of interacting with the system).&lt;/p&gt;

&lt;p&gt;Those elements are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an &lt;strong&gt;ingestion path&lt;/strong&gt; that standardizes content and writes to SQL + graph,&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;retrieval path&lt;/strong&gt; that runs vector + FTS + graph and fuses results,&lt;/li&gt;
&lt;li&gt;a single &lt;strong&gt;SQL migration&lt;/strong&gt; that creates tables, functions, and indexes.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Before vs. after
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;User asks:&lt;/strong&gt; “Show me order #889 and customer context.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plain RAG (before):&lt;/strong&gt;&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;"order_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;889&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Alexis Chen"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"alexis@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"items"&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="s2"&gt;"Ethiopia Natural 2x"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ship_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Delivered 2024-03-11"&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;&lt;strong&gt;Context Mesh (after):&lt;/strong&gt;&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;"order_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;889&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Alexis Chen"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lifetime_orders"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"first_order_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2022-08-19"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lifetime_value_eur"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;642.80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"support_tickets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"last_ticket_disposition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Carrier delay - resolved"&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;&lt;strong&gt;Why this happens:&lt;/strong&gt; the system links &lt;code&gt;node(customer: Alexis Chen)&lt;/code&gt; ⇄ &lt;code&gt;orders&lt;/code&gt; ⇄ &lt;code&gt;tickets&lt;/code&gt; and stores those edges. Retrieval calls &lt;code&gt;search_vector&lt;/code&gt;, &lt;code&gt;search_fulltext&lt;/code&gt;, &lt;strong&gt;and&lt;/strong&gt; &lt;code&gt;search_graph&lt;/code&gt;, then unifies with RRF so top answers include the &lt;em&gt;relational&lt;/em&gt; context.&lt;/p&gt;




&lt;h2&gt;
  
  
  60-second mental model
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Files / CSVs] ──&amp;gt; [document] ──&amp;gt; [chunk] ─┬─&amp;gt; [chunk_embedding]  (vector)
                                          │
                                          ├─&amp;gt; [chunk.tsv]        (FTS)
                                          │
                                          └─&amp;gt; [chunk_node] ─&amp;gt; [node] &amp;lt;─&amp;gt; [edge]  (graph)

vector/full-text/graph ──&amp;gt; search_unified (RRF) ──&amp;gt; ranked, mixed results (chunks + rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What’s inside Context Mesh Lite (Supabase)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Documents &amp;amp; chunks&lt;/strong&gt; with embeddings and FTS (&lt;code&gt;tsvector&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lightweight graph&lt;/strong&gt;: &lt;code&gt;node&lt;/code&gt;, &lt;code&gt;edge&lt;/code&gt;, plus &lt;code&gt;chunk_node&lt;/code&gt; mentions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured registry&lt;/strong&gt; for spreadsheet-to-SQL tables&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search functions&lt;/strong&gt;: vector, FTS, graph, and &lt;strong&gt;unified fusion&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guarded SQL execution&lt;/strong&gt; for safe read-only structured queries&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The SQL migration (collapsed for readability)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1) Extensions&lt;/strong&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;-- 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;pg_trgm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Enables vector embeddings and trigram text similarity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2) Core tables&lt;/strong&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="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;document&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;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="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;tsv&lt;/span&gt; &lt;span class="n"&gt;TSVECTOR&lt;/span&gt;&lt;span class="p"&gt;,&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;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="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk_embedding&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;chunk_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="k"&gt;REFERENCES&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;chunk&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="p"&gt;,&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;1536&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="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="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node&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;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="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;edge&lt;/span&gt; &lt;span class="p"&gt;(...&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;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;type&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;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="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk_node&lt;/span&gt; &lt;span class="p"&gt;(...&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;chunk_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;node_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rel&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;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="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;structured_table&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt; &lt;span class="n"&gt;schema_def&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;row_count&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Documents + chunks; embeddings; a minimal graph; and a registry for spreadsheet-derived tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3) Indexes for speed&lt;/strong&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;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;chunk_tsv_gin&lt;/span&gt; &lt;span class="k"&gt;ON&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;chunk&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tsv&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;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;emb_hnsw_cos&lt;/span&gt; &lt;span class="k"&gt;ON&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;chunk_embedding&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;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;edge_src_idx&lt;/span&gt; &lt;span class="k"&gt;ON&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;edge&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&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;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;edge_dst_idx&lt;/span&gt; &lt;span class="k"&gt;ON&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;edge&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dst&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;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;node_labels_gin&lt;/span&gt; &lt;span class="k"&gt;ON&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;node&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;labels&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;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;node_props_gin&lt;/span&gt; &lt;span class="k"&gt;ON&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;node&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;FTS GIN + vector HNSW + graph helpers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4) Triggers &amp;amp; helpers&lt;/strong&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="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk_tsv_update&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;trigger&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;BEGIN&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;doc_title&lt;/span&gt; &lt;span class="k"&gt;FROM&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;document&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;d&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="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tsv&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;setweight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&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;doc_title&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;'A'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;setweight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&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="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;text&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;'B'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="err"&gt;$$&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;TRIGGER&lt;/span&gt; &lt;span class="n"&gt;chunk_tsv_trg&lt;/span&gt;
&lt;span class="k"&gt;BEFORE&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;OF&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;document_id&lt;/span&gt; &lt;span class="k"&gt;ON&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;chunk&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;EACH&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&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;chunk_tsv_update&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;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="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sanitize_table_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="nb"&gt;TEXT&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="s1"&gt;'tbl_'&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="k"&gt;lower&lt;/span&gt;&lt;span class="p"&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;name&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="s1"&gt;'[^a-z0-9_]'&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;'g'&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="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="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;infer_column_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample_values&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="c1"&gt;-- counts booleans/numerics/dates and returns BOOLEAN/NUMERIC/DATE/TEXT&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;Keeps FTS up-to-date; normalizes spreadsheet table names; infers column types.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5) Ingest documents (chunks + embeddings + graph)&lt;/strong&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="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ingest_document_chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;p_uri&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_title&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_doc_meta&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;p_chunk&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;p_nodes&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;p_edges&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;p_mentions&lt;/span&gt; &lt;span class="n"&gt;JSONB&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;JSONB&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;BEGIN&lt;/span&gt;
  &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&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;document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&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;doc_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;v_doc_id&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="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ordinal&lt;/span&gt;&lt;span class="p"&gt;,&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="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ordinal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;v_chunk_id&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;p_chunk&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s1"&gt;'embedding'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
    &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&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;chunk_embedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk_id&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="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;UPDATE&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;-- Upsert nodes/edges and link mentions chunk↔node&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;jsonb_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ok'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'document_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v_doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'chunk_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v_chunk_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;END&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;&lt;strong&gt;6) Ingest spreadsheets → SQL tables&lt;/strong&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="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ingest_spreadsheet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;p_uri&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_title&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_table_name&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_rows&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;p_schema&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;p_nodes&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;p_edges&lt;/span&gt; &lt;span class="n"&gt;JSONB&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;JSONB&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;BEGIN&lt;/span&gt;
  &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&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;document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&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;doc_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="s1"&gt;'spreadsheet'&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="n"&gt;v_safe_name&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&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;sanitize_table_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p_table_name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;-- CREATE MODE: infer columns &amp;amp;amp; types, then CREATE TABLE public.%I (...)&lt;/span&gt;
  &lt;span class="c1"&gt;-- APPEND MODE: reuse existing columns and INSERT rows&lt;/span&gt;
  &lt;span class="c1"&gt;-- Update structured_table(schema_def,row_count)&lt;/span&gt;
  &lt;span class="c1"&gt;-- Optional: upsert nodes/edges from the data&lt;/span&gt;
  &lt;span class="k"&gt;RETURN&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="s1"&gt;'ok'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'table_name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v_safe_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'rows_inserted'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v_row_count&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="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;&lt;strong&gt;7) Search primitives (vector, FTS, graph)&lt;/strong&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="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;search_vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p_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;1536&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;p_limit&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;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;chunk_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="n"&gt;FLOAT8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rank&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;LANGUAGE&lt;/span&gt; &lt;span class="k"&gt;sql&lt;/span&gt; &lt;span class="k"&gt;STABLE&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;ce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk_id&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="o"&gt;/&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="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ce&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;amp;&lt;/span&gt;&lt;span class="n"&gt;lt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;p_embedding&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;score&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;ce&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;amp;&lt;/span&gt;&lt;span class="n"&gt;lt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;p_embedding&lt;/span&gt;&lt;span class="p"&gt;))::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;
  &lt;span class="k"&gt;FROM&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;chunk_embedding&lt;/span&gt; &lt;span class="n"&gt;ce&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="n"&gt;p_limit&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="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="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;search_fulltext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p_query&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_limit&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;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;chunk_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="n"&gt;FLOAT8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rank&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;LANGUAGE&lt;/span&gt; &lt;span class="k"&gt;sql&lt;/span&gt; &lt;span class="k"&gt;STABLE&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;WITH&lt;/span&gt; &lt;span class="n"&gt;query&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;websearch_to_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p_query&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;tsq&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;c&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;ts_rank_cd&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;tsv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tsq&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;float8&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;FROM&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;chunk&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="n"&gt;q&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;tsv&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tsq&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="n"&gt;p_limit&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="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="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;search_graph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p_keywords&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_limit&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;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;chunk_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="n"&gt;FLOAT8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rank&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;LANGUAGE&lt;/span&gt; &lt;span class="k"&gt;sql&lt;/span&gt; &lt;span class="k"&gt;STABLE&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;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="n"&gt;seeds&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(...),&lt;/span&gt; &lt;span class="n"&gt;walk&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(...),&lt;/span&gt; &lt;span class="n"&gt;hits&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;chunk_id&lt;/span&gt;&lt;span class="p"&gt;,&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="o"&gt;/&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="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;min_depth&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;float8&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="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="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mention_count&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;float8&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;score&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;AS&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="n"&gt;p_limit&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;&lt;strong&gt;8) Safe read-only SQL for structured data&lt;/strong&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="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;search_structured&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p_query_sql&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_limit&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;20&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="k"&gt;table_name&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;row_data&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;score&lt;/span&gt; &lt;span class="n"&gt;FLOAT8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rank&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;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt; &lt;span class="k"&gt;STABLE&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;BEGIN&lt;/span&gt;
  &lt;span class="c1"&gt;-- Reject dangerous statements and trailing semicolons&lt;/span&gt;
  &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;p_query_sql&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="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;p_query_sql&lt;/span&gt; &lt;span class="o"&gt;~*&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="s1"&gt;(insert|update|delete|drop|alter|grant|revoke|truncate)&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;RETURN&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;v_sql&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;'WITH user_query AS (%s)
     SELECT &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;result&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; AS table_name, to_jsonb(user_query.*) AS row_data, 1.0::float8 AS score,
            (row_number() OVER ())::int AS rank FROM user_query LIMIT %s'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;p_query_sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p_limit&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;QUERY&lt;/span&gt; &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="n"&gt;v_sql&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="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;RETURN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&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;&lt;strong&gt;9) Unified search with RRF fusion&lt;/strong&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="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;search_unified&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;p_query_text&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_query_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;1536&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;p_keywords&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_query_sql&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_limit&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;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p_rrf_constant&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;60&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;final_score&lt;/span&gt; &lt;span class="n"&gt;FLOAT8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vector_rank&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;fts_rank&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;graph_rank&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;struct_rank&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;LANGUAGE&lt;/span&gt; &lt;span class="k"&gt;sql&lt;/span&gt; &lt;span class="k"&gt;STABLE&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;WITH&lt;/span&gt;
  &lt;span class="n"&gt;vector_results&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;chunk_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="k"&gt;FROM&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;search_vector&lt;/span&gt;&lt;span class="p"&gt;(...)),&lt;/span&gt;
  &lt;span class="n"&gt;fts_results&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;chunk_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="k"&gt;FROM&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;search_fulltext&lt;/span&gt;&lt;span class="p"&gt;(...)),&lt;/span&gt;
  &lt;span class="n"&gt;graph_results&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;chunk_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="k"&gt;FROM&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;search_graph&lt;/span&gt;&lt;span class="p"&gt;(...)),&lt;/span&gt;
  &lt;span class="n"&gt;unstructured_fusion&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;c&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;AS&lt;/span&gt; &lt;span class="n"&gt;chunk_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="n"&gt;uri&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="n"&gt;title&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="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;sum&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="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="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p_rrf_constant&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;vr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rank&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;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&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="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="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p_rrf_constant&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rank&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;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&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="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="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p_rrf_constant&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;gr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rank&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;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="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rrf_score&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;vr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rank&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;vector_rank&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;fr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rank&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;fts_rank&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;gr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rank&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;graph_rank&lt;/span&gt;
    &lt;span class="k"&gt;FROM&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;chunk&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;JOIN&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;document&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;d&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="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;document_id&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;vector_results&lt;/span&gt; &lt;span class="n"&gt;vr&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;vr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk_id&lt;/span&gt;&lt;span class="o"&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;id&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;fts_results&lt;/span&gt; &lt;span class="n"&gt;fr&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk_id&lt;/span&gt;&lt;span class="o"&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;id&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;graph_results&lt;/span&gt; &lt;span class="n"&gt;gr&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;gr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk_id&lt;/span&gt;&lt;span class="o"&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;id&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;vr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk_id&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;OR&lt;/span&gt; &lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk_id&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;OR&lt;/span&gt; &lt;span class="n"&gt;gr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk_id&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;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&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;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="n"&gt;uri&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="n"&gt;title&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="nb"&gt;text&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;structured_results&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;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="k"&gt;FROM&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;search_structured&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p_query_sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p_limit&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="c1"&gt;-- graph-aware boost for structured rows by matching entity names&lt;/span&gt;
  &lt;span class="n"&gt;structured_with_graph&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(...),&lt;/span&gt;
  &lt;span class="n"&gt;structured_ranked&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(...),&lt;/span&gt;
  &lt;span class="n"&gt;structured_normalized&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(...),&lt;/span&gt;
  &lt;span class="n"&gt;combined&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="s1"&gt;'chunk'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;result_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uri&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;content&lt;/span&gt;&lt;span class="p"&gt;,&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;jsonb&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;structured_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rrf_score&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;final_score&lt;/span&gt;&lt;span class="p"&gt;,&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;unstructured_fusion&lt;/span&gt;
    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="s1"&gt;'structured'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&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;row_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rrf_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&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;NULL&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;graph_rank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;struct_rank&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;structured_normalized&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&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;combined&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;final_score&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="n"&gt;p_limit&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;&lt;strong&gt;10) Grants&lt;/strong&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;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;service_role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="n"&gt;TABLES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;service_role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="n"&gt;SEQUENCES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;service_role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="n"&gt;FUNCTIONS&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;service_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Security &amp;amp; cost notes (the honest bits)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Guardrails&lt;/strong&gt;: &lt;code&gt;search_structured&lt;/code&gt; blocks DDL/DML—keep it that way. If you expose custom SQL, add allowlists and parse checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PII&lt;/strong&gt;: if nodes contain emails/phones, consider hashing or using RLS policies keyed by tenant/account.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cost drivers&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;embedding generation (per chunk),&lt;/li&gt;
&lt;li&gt;HNSW maintenance (inserts/updates),&lt;/li&gt;
&lt;li&gt;storage growth for &lt;code&gt;chunk&lt;/code&gt;, &lt;code&gt;chunk_embedding&lt;/code&gt;, and the graph.
Track these; consider tiered retention (hot vs warm).&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h2&gt;
  
  
  Limitations &amp;amp; edge cases
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Graph drift&lt;/strong&gt;: entity IDs and names change—keep stable IDs, use alias nodes for renames.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Temporal truth&lt;/strong&gt;: add &lt;code&gt;effective_from&lt;/code&gt;/&lt;code&gt;to&lt;/code&gt; on edges if you need time-aware answers (“as of March 2024”).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema evolution&lt;/strong&gt;: spreadsheet ingestion may need migrations (or shadow tables) when types change.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  A tiny, honest benchmark (illustrative)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query type&lt;/th&gt;
&lt;th&gt;Plain RAG&lt;/th&gt;
&lt;th&gt;Context Mesh&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Exact order lookup&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customer 360 roll-up&lt;/td&gt;
&lt;td&gt;😬&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;“First purchase when?”&lt;/td&gt;
&lt;td&gt;😬&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;“Top related tickets?”&lt;/td&gt;
&lt;td&gt;😬&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The win isn’t fancy math; it’s &lt;em&gt;capturing relationships&lt;/em&gt; and letting retrieval use them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create a Supabase project; enable &lt;code&gt;vector&lt;/code&gt; and &lt;code&gt;pg_trgm&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run the single SQL migration (tables, functions, indexes, grants).&lt;/li&gt;
&lt;li&gt;Wire up your ingestion path to call the &lt;strong&gt;document&lt;/strong&gt; and &lt;strong&gt;spreadsheet&lt;/strong&gt; RPCs.&lt;/li&gt;
&lt;li&gt;Wire up retrieval to call &lt;strong&gt;unified search&lt;/strong&gt; with:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;natural-language text,&lt;/li&gt;
&lt;li&gt;an embedding (optional but recommended),&lt;/li&gt;
&lt;li&gt;a keyword set (for graph seeding),&lt;/li&gt;
&lt;li&gt;a safe, read-only SQL snippet (for structured lookups).

&lt;ol&gt;
&lt;li&gt;Add lightweight logging so you can see fusion behavior and adjust weights.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(I built a couple of n8n workflows to easily interact with the Context Mesh; workflows for ingestion calling the ingest edge function, and a workflow chat UI that interacts with the search edge function.)&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is this overkill for simple Q&amp;amp;A?&lt;/strong&gt;&lt;br&gt;
If your queries never need rollups, joins, or cross-entity context, plain hybrid RAG is fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need a giant knowledge graph?&lt;/strong&gt;&lt;br&gt;
No. Start small: Customers, Orders, Tickets—then add edges as you see repeated questions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about multilingual content?&lt;/strong&gt;&lt;br&gt;
Set FTS configuration per language and keep embeddings in a multilingual model; the pattern stays the same.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;After upserting the same documents into Context Mesh-enabled Supabase as well as a traditional vector store, I connected both to the chat agent. Context Mesh consistently outperforms regular RAG. &lt;/p&gt;

&lt;p&gt;That's because it has more access to structured data, temporal reasoning, relationship context, etc. All because of the additional context provided by nodes and edges from a knowledge graph. Hopefully this helps you down the path of superior retrieval as well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Be well and build good systems.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>supabase</category>
      <category>database</category>
    </item>
  </channel>
</rss>
