<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/">
    <channel>
        <title><![CDATA[Tiger Data Blog]]></title>
        <description><![CDATA[Insights, product updates, and tips from TigerData (Creators of TimescaleDB) engineers on Postgres, time series & AI. IoT, crypto, and analytics tutorials & use cases.]]></description>
        <link>https://www.tigerdata.com/blog</link>
        <image>
            <url>https://www.tigerdata.com/icon.ico</url>
            <title>Tiger Data Blog</title>
            <link>https://www.tigerdata.com/blog</link>
        </image>
        <generator>RSS for Node</generator>
        <lastBuildDate>Tue, 07 Apr 2026 11:15:24 GMT</lastBuildDate>
        <atom:link href="https://www.tigerdata.com/blog" rel="self" type="application/rss+xml"/>
        <ttl>60</ttl>
        <item>
            <title><![CDATA[We Taught AI to Write Real Postgres Code (And Open Sourced It)]]></title>
            <description><![CDATA[pg-aiguide teaches AI to write production-ready Postgres code with curated skills, semantic search, and version-aware docs. Open source and free to use.]]></description>
            <link>https://www.tigerdata.com/blog/we-taught-ai-to-write-real-postgres-code-open-sourced-it</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/we-taught-ai-to-write-real-postgres-code-open-sourced-it</guid>
            <category><![CDATA[Announcements & Releases]]></category>
            <category><![CDATA[AI]]></category>
            <category><![CDATA[PostgreSQL]]></category>
            <dc:creator><![CDATA[Matvey Arye]]></dc:creator>
            <pubDate>Mon, 24 Nov 2025 15:00:09 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2025/11/2025-nov-21-thumbnail-open-source-mcp-server.png">
            </media:content>
            <content:encoded><![CDATA[<p>You open Claude Code or Cursor, describe your tables, and in seconds the AI hands you a Postgres schema that looks… fine. It runs. Your tests pass. You ship.</p><p>What you don’t see are the quiet little disasters tucked inside: money for prices, a BRIN index on random data, SERIAL and UUID mixed like a cocktail, timestamp without time zone because some tutorial said it was “easier”.</p><p>Fast-forward six months. You’re debugging currency-conversion bugs, chasing timezone ghosts, rewriting migrations, and adding the index that should have existed since day one. The code the AI agent wrote worked; it just wasn’t good. It was copying whatever examples it scraped from the entire internet.</p><p>And that’s the problem. It learned SQL from everywhere: Postgres, MySQL, SQLite, SQL Server, Oracle, random tutorials, and a decade of Stack Overflow answers. In all that noise, the nuances of idiomatic, high-quality Postgres<strong> </strong>get buried under the good, the bad, and the MySQL.</p><p>So we built something to fix that.</p><h2 id="giving-ai-the-postgres-judgment-it%E2%80%99s-missing">Giving AI the Postgres Judgment It’s Missing</h2><p>pg-aiguide gives AI coding agents the Postgres-specific judgment they’re missing.It does this with three things working together:</p><ol><li><strong>AI-optimized “skills”</strong>— curated, opinionated Postgres best practices that Claude Code and other agents can apply automatically.</li><li><strong>Semantic search across official documentation</strong> — version-aware retrieval for Postgres 15–18.</li><li><strong>Extension ecosystem docs</strong>, starting with TimescaleDB and expanding quickly</li></ol><p>You can connect it to any AI coding agent via our public <strong>Model Context Protocol (MCP) server</strong> or with the <strong>Claude Code plugin</strong> built to take advantage of Claude’s native skill support. No accounts. No usage limits. Completely free.</p><p>The goal is simple:<strong>Make AI write correct, production-ready Postgres by default.</strong></p><p>You shouldn’t have to paste docs, correct outputs, or rely on prompt hacks. The AI should just generate better SQL the first time.</p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">💡</div><div class="kg-callout-text"><b><strong style="white-space: pre-wrap;">Try it now </strong></b><br>You can start using pg-aiguide in less than a minute. It works with Claude Code, Codex, Cursor, Gemini CLI, Visual Studio, VS Code, Windsurf, and any other MCP compatible editors. See our <a href="https://github.com/timescale/pg-aiguide/tree/main?tab=readme-ov-file#-quickstart"><u>quickstart guide</u></a> for installation instructions.</div></div><h2 id="why-database-code-quality-is-so-important">Why Database Code Quality Is So Important&nbsp;</h2><p>AI adoption among Postgres developers has exploded: jumping from 37% to 55% in a single year according to the <a href="https://www.tigerdata.com/state-of-postgres/2024"><u>2024 State of Postgres</u></a> survey. But as LLMs get more general, the training data has become broad instead of specialized and the LLMs become <em>less</em> precise.</p><p>This creates a surprising problem: A study by METR recently found that experienced developers using AI actually took<strong> 19 percent longer</strong> to complete tasks. Not because the AI failed, but because the output was almost right—the kind of subtle mistakes that take the longest to spot, debug, and unwind.</p><p>With databases, “almost right” compounds quickly.&nbsp;</p><p>Schemas codify decisions that are painful to change later. Indexing mistakes hide until your tables grow. Datatype choices that take an hour to fix in development can take days of migrations and careful planning in production.</p><p>You can refactor application code. You can’t refactor state without consequences.</p><p>pg-aiguide stops these mistakes at the source, before they get baked into production.</p><h2 id="generic-vs-guided-sql">Generic vs. Guided SQL</h2><p>Here’s what happens when you ask a coding assistant to design a simple bookstore schema.</p><h3 id="without-pg-aiguide">Without pg-aiguide</h3><pre><code class="language-SQL">CREATE TABLE customers (
    customer_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    email VARCHAR(255) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    first_name VARCHAR(100) NOT NULL,
    last_name VARCHAR(100) NOT NULL,
    phone VARCHAR(20),
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    is_active BOOLEAN DEFAULT TRUE
);

CREATE INDEX idx_customers_email ON customers(email);

CREATE TABLE authors (
    author_id SERIAL PRIMARY KEY,
    first_name VARCHAR(100) NOT NULL,
    last_name VARCHAR(100) NOT NULL,
    biography TEXT,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
</code></pre><p>This schema works, but has classic issues:</p><ul><li>mixed identity strategies</li><li>arbitrary <code>VARCHAR</code> limits</li><li>case-sensitive email uniqueness</li><li>timestamp defaults that behave inconsistently across systems</li></ul><h3 id="with-pg-aiguide-enabled">With pg-aiguide enabled</h3><pre><code class="language-SQL">CREATE TABLE authors (
  author_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name TEXT NOT NULL,
  bio TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX ON authors (name);

CREATE TABLE users (
  user_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  email TEXT NOT NULL UNIQUE,
  password_hash TEXT NOT NULL,
  first_name TEXT NOT NULL,
  last_name TEXT NOT NULL,
  phone TEXT,
  is_active BOOLEAN NOT NULL DEFAULT true,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX ON users (LOWER(email));
CREATE INDEX ON users (created_at);</code></pre><p>This version uses:</p><ul><li>consistent identity columns (<code>BIGINT GENERATED ALWAYS AS IDENTITY</code>)</li><li><code>TEXT</code> instead of unnecessary <code>VARCHAR</code></li><li>correct timestamp handling (<code>timestamptz + now()</code>)</li><li>case-insensitive uniqueness enforced properly</li></ul><p>Behind the scenes, the AI used the <a href="https://github.com/timescale/pg-aiguide/blob/main/skills/design-postgres-tables/SKILL.md"><u>design_postgres_table skill</u></a> either through the <a href="https://github.com/timescale/pg-aiguide/blob/main/API.md#view_skill"><u>view_skill </u></a>MCP tool or <a href="https://www.claude.com/blog/skills"><u>Claude’s native skills framework</u></a>. In both cases, the agent automatically discovered and applied the Postgres-optimized guidance without human intervention.</p><p>You didn’t have to prompt differently. </p><p>You didn’t have to paste in docs.</p><p><strong>pg-aiguide automatically shifts AI from “SQL that works” to “SQL you’d actually want in production.”</strong></p><h2 id="the-skills-are-the-secret-sauce">The Skills Are the Secret Sauce&nbsp;</h2><p>If you want AI to generate high-quality SQL, it is not enough to let it search the manual. A manual tells you what you can do, not what you should do. Skills fill that gap. They give the model judgment, not just facts.</p><p>Our skills are not trying to reteach the LLM syntax or capabilities. Instead they give the model the context it needs to make better choices. Here is an excerpt from a real skill.</p><pre><code class="language-markup">## Postgres "Gotchas"

- **FK indexes**: Postgres **does not** auto-index FK columns. Add them.
- **No silent coercions**: length/precision overflows error out (no truncation). 
  Example: inserting 999 into `NUMERIC(2,0)` fails with error, unlike some 
  databases that silently truncate or round.
- **Heap storage**: no clustered PK by default (unlike SQL Server/MySQL InnoDB); 
  row order on disk is insertion order unless explicitly clustered.</code></pre><p>These are the kinds of details you only know once you have lived in Postgres for a while. They trip up LLMs for the same reason they trip up developers who are new (and not so new) to the database. Yet these details are exactly what allow the model to produce better SQL.</p><p>In our evaluations (currently human-vibes-driven, soon LLM-judged), schema quality improves consistently when we compare a system with just semantic search to one that includes both semantic search and skills:</p><ul><li>more appropriate data types</li><li>correct timestamp semantics</li><li>stronger indexing strategies</li><li>fewer migration pitfalls</li><li>fewer long-term performance surprises</li></ul><p>This is what “AI coding tools actually understanding Postgres” looks like.</p><h2 id="the-tools-we-provide-the-llm">The Tools We Provide The LLM</h2><p>pg-aiguide provides two core capabilities that map cleanly to how AI coding tools operate.</p><h3 id="1-skills-complete-opinionated-postgres-guidance">1. Skills: Complete, Opinionated Postgres Guidance</h3><p><code>view_skill</code> returns full, AI-optimized best practices.These aren’t tutorials and they aren’t vague prompts. They’re machine-targeted, dense, token-efficient guidance that the AI can reliably use.</p><p>For example:</p><ul><li>prefer <code>BIGINT GENERATED ALWAYS AS IDENTITY</code></li><li>don’t use <code>money</code></li><li>don’t use <code>timestamp</code> without timezone</li><li>index your foreign keys</li><li>expect errors on precision overflows</li></ul><p>Skills don’t need to be chunked—they are written so that each skill fits in context as a single complete unit.</p><p>Claude Code even supports skills natively, so the MCP server’s <code>view_skill</code> tool is disabled automatically when running as a plugin.</p><h3 id="2-semantic-search-version-aware-vector-retrieval-across-docs">2. Semantic Search: Version-Aware Vector Retrieval Across Docs</h3><p>The MCP tools <code>semantic_search_postgres_docs</code> and <code>semantic_search_tiger_docs</code> allow the AI to pull in the <strong>correct</strong> documentation for the Postgres version you’re targeting.</p><p>This matters because Postgres versions evolve meaningfully:</p><ul><li>Postgres 15: <code>UNIQUE NULLS NOT DISTINCT</code></li><li>Postgres 16: major changes to parallel query behavior</li><li>Postgres 17: COPY error-handling improvements</li></ul><p>Without version awareness, an AI can (and does) hallucinate features or syntax that will break your actual environment.</p><p>All of this knowledge of Postgres is chunked, embedded, and stored in Postgres itself.</p><p>We scrape official HTML docs, preserve header context, attach source URLs, and use character-bounded chunking with H1→H2→H3 breadcrumbs so each piece retains meaning of how it fits into the broader puzzle.</p><h2 id="help-us-build-the-world%E2%80%99s-best-postgres-guide-for-ai">Help Us Build the World’s Best Postgres Guide for AI</h2><p>Postgres has 35 years of engineering, craft, and hard-won lessons behind it. No single team can capture all of that. The community built the patterns, extensions, and production wisdom that make Postgres what it is. AI coding tools should reflect that depth, not spit out generic SQL lifted from outdated tutorials and old Stack Overflow posts.</p><p>pg-aiguide is our first step toward making Postgres the best database to use with AI coding assistants on purpose, not by accident. We are expanding the skill library with richer indexing guidance, full-text search skills, and documentation for essential extensions like PostGIS and pgvector. We are also adding keyword BM25 search to pair with semantic search for more accurate retrieval. <strong>But we need your help.</strong></p><h3 id="how-you-can-contribute">How You Can Contribute</h3><p>You can make an immediate impact:</p><ul><li>add documentation for your Postgres extension</li><li>contribute new skills that encode real, battle-tested expertise</li><li>help evaluate, refine, and stress-test existing skills</li><li>request features or report issues</li><li>improve semantic search chunking or propose new areas to index</li><li>share deep knowledge on partitioning, replication, security, or performance tuning</li></ul><p>Skills matter most. They turn years of experience into guidance the AI can use instantly. Our schema-design skill went through multiple iterations before it felt right, and we learned a ton in the process. We would love to partner with you to build skills in your area of expertise.</p><p>pg-aiguide is fully open source at github.com/timescale/pg-aiguide.&nbsp;</p><p><strong>Help us teach AI to write Postgres like an expert.</strong></p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to Train Your Agent to Be a Postgres Expert]]></title>
            <description><![CDATA[Turn AI into a Postgres expert with our MCP server. Get 35 years of best practices, versioned docs, and prompt templates for production-ready schemas.]]></description>
            <link>https://www.tigerdata.com/blog/free-postgres-mcp-prompt-templates</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/free-postgres-mcp-prompt-templates</guid>
            <category><![CDATA[Announcements & Releases]]></category>
            <category><![CDATA[AI]]></category>
            <category><![CDATA[PostgreSQL]]></category>
            <dc:creator><![CDATA[Matty Stratton]]></dc:creator>
            <pubDate>Wed, 22 Oct 2025 14:02:12 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2025/10/2025-Oct-21-Prompt-Template-Thumbnail.png">
            </media:content>
            <content:encoded><![CDATA[<h3 id="with-prompt-templates-and-versioned-docs-we-turn-35-years-of-postgres-wisdom-into-structured-knowledge-your-agent-can-reason-with">With prompt templates and versioned docs, we turn 35 years of Postgres wisdom into structured knowledge your Agent can reason with.</h3><p>Agents are the <a href="https://www.tigerdata.com/blog/postgres-for-agents" rel="noreferrer">new developer</a>. But they’re generalists.&nbsp;</p><p>What happens when they design your Postgres database? Your schema runs, your tests pass… and six months later your queries crawl and your costs skyrocket.&nbsp;</p><p>AI-generated SQL and database schemas are almost right. And that’s the problem. Fixing schema design mistakes is costlier than refactoring code. It often means multi-week migrations, downtime windows, rollback plans, and your CEO asking why the site is in maintenance mode. The root issue? LLMs don’t have the depth of Postgres and database expertise to let them build scalable systems. And when agents try to learn, they find documentation written for humans, not for them.&nbsp;</p><p>But agents don’t need more data, they need better context. They need to know what “good Postgres” actually looks like. The good news is given the right context and tools, agents can become instant experts. Even with Postgres.&nbsp;</p><p>That’s why we built an MCP server that provides 35 years of Postgres wisdom, and full access Postgres docs, all in a format that agents can easily process.&nbsp;</p><p>And we think this just might be the best database MCP server ever built. While Neon, Supabase, and other Postgres companies created MCP servers as thin API wrappers, ours teaches AI how to think in Postgres. The Tiger MCP server gives AI tools that work automatically: no prompt engineering or manual lookups needed. You just ask. And it provides correct, idiomatic Postgres. </p><p>Our new MCP server ships with detailed prompt templates written by our senior engineers, plus versioned Postgres (15-18) docs, and <a href="https://docs.tigerdata.com/" rel="noreferrer">TimescaleDB docs</a>.&nbsp;When your AI needs to design a schema, it automatically pulls the right template and searches the docs (using hybrid search) to generate code that actually lasts. And of course, it handles the basics: start, stop, <a href="https://www.tigerdata.com/blog/fast-zero-copy-database-forks" rel="noreferrer">fork a database</a>. </p><p>But the real magic happens when you ask AI to design your schema. So let’s see what happens when AI designs a simple schema. First without the MCP server, then with it.</p><h2 id="without-the-mcp-server">Without the MCP Server</h2><pre><code class="language-SQL">CREATE TABLE customers (
    customer_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    email VARCHAR(255) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    first_name VARCHAR(100) NOT NULL,
    last_name VARCHAR(100) NOT NULL,
    phone VARCHAR(20),
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    is_active BOOLEAN DEFAULT TRUE
);

CREATE INDEX idx_customers_email ON customers(email);

CREATE TABLE authors (
    author_id SERIAL PRIMARY KEY,
    first_name VARCHAR(100) NOT NULL,
    last_name VARCHAR(100) NOT NULL,
    biography TEXT,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
</code></pre><p>What’s wrong here:&nbsp;</p><ul><li>Mixed ID types (SERIAL and UUID) with no clear reason</li><li>Arbitrary VARCHAR limits instead of TEXT</li><li>Case-sensitive email index → duplicate accounts</li><li>CURRENT_TIMESTAMP instead of now(), leading to inconsistent timestamps</li></ul><p>Someone new to Postgres may not notice these problems. Until it’s too late.</p><p>Now let’s see what AI generates with our new MCP server.</p><h2 id="with-the-mcp-server">With the MCP Server</h2><pre><code class="language-SQL">CREATE TABLE authors (
  author_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name TEXT NOT NULL,
  bio TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX ON authors (name);

CREATE TABLE users (
  user_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  email TEXT NOT NULL UNIQUE,
  password_hash TEXT NOT NULL,
  first_name TEXT NOT NULL,
  last_name TEXT NOT NULL,
  phone TEXT,
  is_active BOOLEAN NOT NULL DEFAULT true,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX ON users (LOWER(email));
CREATE INDEX ON users (created_at);</code></pre><p>What’s better about this?</p><ul><li>Consistent ID strategy with BIGINT GENERATED ALWAYS AS IDENTITY</li><li>TEXT instead of arbitrary VARCHAR limits</li><li>Case-insensitive email lookups</li><li>Modern timestamp handling</li></ul><p>But why does this matter?</p><p>Each of these differences creates a compounding problem. Changing datatypes in the future will require full table rewrites. Missing lowercase email handling means duplicate accounts and confused users. And time zones? Every senior developer gets the thousand-yard stare when you mention UTC conversions.</p><p>This is just with a small example; imagine what would happen with more complex schemas.</p><p>And if you don’t believe us, here’s what Claude has to say:</p><pre><code class="language-markdown">&gt; Please describe the schema you would create for an e-commerce website two times, first with the tiger mcp server disabled, then with the tiger mcp server enabled. For each time, write the schema to its own file in the current working directory. Then compare the two files and let me know which approach generated the better schema, using both qualitative and quantitative reasons. For this example, only use standard Postgres.</code></pre><figure class="kg-card kg-video-card kg-width-regular" data-kg-thumbnail="https://timescale.ghost.io/blog/content/media/2025/10/how-to-train-your-agent_thumb.jpg" data-kg-custom-thumbnail="">
            <div class="kg-video-container">
                <video src="https://storage.ghost.io/c/6b/cb/6bcb39cf-9421-4bd1-9c9d-fa7b6755ba0e/content/media/2025/10/how-to-train-your-agent.mp4" poster="https://img.spacergif.org/v1/1280x720/0a/spacer.png" width="1280" height="720" loop="" autoplay="" muted="" playsinline="" preload="metadata" style="background: transparent url('https://storage.ghost.io/c/6b/cb/6bcb39cf-9421-4bd1-9c9d-fa7b6755ba0e/content/media/2025/10/how-to-train-your-agent_thumb.jpg') 50% 50% / cover no-repeat;"></video>
                <div class="kg-video-overlay">
                    <button class="kg-video-large-play-icon" aria-label="Play video">
                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
                            <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"></path>
                        </svg>
                    </button>
                </div>
                <div class="kg-video-player-container kg-video-hide">
                    <div class="kg-video-player">
                        <button class="kg-video-play-icon" aria-label="Play video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
                                <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"></path>
                            </svg>
                        </button>
                        <button class="kg-video-pause-icon kg-video-hide" aria-label="Pause video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
                                <rect x="3" y="1" width="7" height="22" rx="1.5" ry="1.5"></rect>
                                <rect x="14" y="1" width="7" height="22" rx="1.5" ry="1.5"></rect>
                            </svg>
                        </button>
                        <span class="kg-video-current-time">0:00</span>
                        <div class="kg-video-time">
                            /<span class="kg-video-duration">0:50</span>
                        </div>
                        <input type="range" class="kg-video-seek-slider" max="100" value="0">
                        <button class="kg-video-playback-rate" aria-label="Adjust playback speed">1×</button>
                        <button class="kg-video-unmute-icon" aria-label="Unmute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
                                <path d="M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z"></path>
                            </svg>
                        </button>
                        <button class="kg-video-mute-icon kg-video-hide" aria-label="Mute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
                                <path d="M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z"></path>
                            </svg>
                        </button>
                        <input type="range" class="kg-video-volume-slider" max="100" value="100">
                    </div>
                </div>
            </div>
            
        </figure><pre><code class="language-markdown">Verdict:

The Tiger MCP-enabled schema is objectively superior because:

1. Type Safety: Uses modern PostgreSQL types that prevent common errors
2. Data Integrity: 4x more constraints ensure data quality at the database level
3. Performance: Better indexing strategy with 55% more indexes, including partial and expression indexes
4. Maintainability: Better documentation, clear comments, proper naming
5. Features: More complete e-commerce functionality (discounts, full-text search, audit trails)
6. PostgreSQL Best Practices: Follows official PG17 recommendations from the documentation
7. Future-Proof: Uses modern features like GENERATED ALWAYS AS IDENTITY and NULLS NOT DISTINCT

The Tiger MCP server provided access to PostgreSQL-specific best practices documentation and design patterns that resulted in a more robust, performant, and maintainable schema.
</code></pre><h2 id="how-prompt-templates-make-it-all-possible">How Prompt Templates Make It All Possible</h2><p>Behind the scenes, AI uses the MCP server to call <code>get_prompt_template(‘design_postgres’)</code> to load schema design guidance. No pasted docs. No corrections. Just better code.</p><p>Prompt templates turn production wisdom into reusable guidance for AI. Instead of hunting through documentation written for humans, AI gets the information it needs in a format built for machines.</p><p>It comes down to the fact that humans and LLMs have opposite needs. Humans need narratives and memorable examples (and yes, even cat memes) to help them retain information. LLMs need to preserve context window space. That’s why prompt templates make terrible blog posts, but perfect AI guidance.</p><p>Our philosophy is: don't re-teach what the model already knows. LLMs have seen millions of lines of SQL. They know how to write CREATE TABLE. What they don’t know is the 35 years of Postgres wisdom about what works well and what doesn’t.</p><p>It's like your senior DBA whispering advice in the model's ear.</p><p>Our schema design template <code>(design_postgres_tables)</code> doesn’t explain what a primary key is. It jumps straight to guidance:</p><p>“Prefer <code>BIGINT GENERATED ALWAYS AS IDENTITY</code>; use <code>UUID</code> only when global uniqueness is needed.”</p><p>For data types, it doesn’t teach from scratch. It just tells you what works:</p><p>“DO NOT use <code>money</code> type; DO use <code>numeric</code> instead.”</p><p>Here’s a real snippet from the template:</p><pre><code class="language-markdown">## Postgres "Gotchas"

- **FK indexes**: Postgres **does not** auto-index FK columns. Add them.
- **No silent coercions**: length/precision overflows error out (no truncation). 
  Example: inserting 999 into `NUMERIC(2,0)` fails with error, unlike some 
  databases that silently truncate or round.
- **Heap storage**: no clustered PK by default (unlike SQL Server/MySQL InnoDB); 
  row order on disk is insertion order unless explicitly clustered.</code></pre><p>These gotchas trip up LLMs the same way they trip up developers new to Postgres. We optimized these templates for machines: short, factual, and precise, packing maximum guidance into minimum tokens.&nbsp;</p><p>We tested the same approach on a real IoT schema design task. Without templates, the AI added forbidden configurations and missed critical optimizations. <em>With</em> templates, it generated production-ready code with compression, continuous aggregates, and tuned performance.</p><p>That’s how prompt templates work. Now let’s see how the MCP server makes it all happen.</p><h2 id="how-this-mcp-server-is-smarter-than-others">How This MCP Server is Smarter Than Others</h2><p>While Neon, Supabase, and other Postgres companies created MCP servers as thin API wrappers, ours teaches AI how to think in Postgres.The Tiger MCP server gives AI tools that work automatically: no prompt engineering or manual lookups needed. You just ask. And it provides correct, idiomatic Postgres.</p><p><strong><code>get_prompt_template</code> provides auto-discovered expertise. </strong>Instead of having to call a template explicitly, you just say “I want to make a schema for IoT devices…” and the MCP server figures it out. </p><p>With self-discoverable templates, the AI can detect intent and load the right recipe, applying 35 years of Postgres best practices behind the scenes. </p><p><strong>The templates have real depth. </strong>No scraped snippets or boilerplate. The templates are written by senior Postgres engineers, and provide opinionated, production-tested guidance that is tuned to avoid every trap that seasoned DBAs know to avoid.</p><p><strong>Postgres-native vector retrieval adds the right context.</strong> When the AI needs more information, the MCP server searches the versioned Postgres (15-18) and TimescaleDB docs. And it uses Postgres itself for storage and vector search.</p><p>Versioning is critical. For example, Postgres 15 introduced UNIQUE NULLS NOT DISTINCT, while 16 improved parallel queries, and 17 changed COPY error handling. The MCP keeps AIs grounded in correct syntax every time, avoiding broken code from the wrong version.</p><p>The Tiger MCP doesn’t just wire up APIs. It teaches AI to think like a real Postgres engineer. </p><p>You don’t have to craft the perfect prompt. You just ask, and it does the right thing.</p><h2 id="see-it-for-yourself">See It For Yourself</h2><p>Install the Tiger CLI and MCP server:</p><pre><code class="language-shell">curl -fsSL https://cli.tigerdata.com | sh
tiger auth login
tiger mcp install</code></pre><p>(We also have alternative <a href="https://github.com/timescale/tiger-cli"><u>installation instructions</u></a> for the CLI tool.)</p><p>Then select your AI assistant (Claude Code, Cursor, VS Code, Windsurf, etc.) and immediately get real Postgres knowledge flowing into your AI.</p><p>This is how Postgres becomes the best database to use with AI coding tools: not by accident, not because someone pasted docs into a chat, but because the tooling now teaches AI how to think in Postgres.&nbsp;</p><p>Try the MCP server. Break it. <a href="https://timescaledb.slack.com/join/shared_invite/zt-38c4rrt9t-eR8I4hnb4qeGLUrL6hM3mA#/shared-invite/email"><u>Improve it</u></a>. Help us teach every AI to write real Postgres.</p><hr><p><strong>About the authors</strong></p><p><strong>Matty Stratton</strong></p><p>Matty Stratton is the Head of Developer Advocacy and Docs at Tiger Data, a well-known member of the DevOps community, founder and co-host of the popular <a href="https://www.arresteddevops.com/"><u>Arrested DevOps</u></a> podcast, and a global organizer of the <a href="https://devopsdays.org"><u>DevOpsDays</u></a> set of conferences.</p><p>Matty has over 20 years of experience in IT operations and is a sought-after speaker internationally, presenting at Agile, DevOps, and cloud engineering focused events worldwide. Demonstrating his keen insight into the changing landscape of technology, he recently changed his license plate from DEVOPS to KUBECTL.</p><p>He lives in the Chicagoland area and has three awesome kids and two Australian Shepherds, whom he loves just a little bit more than he loves Diet Coke.</p><p><strong>Matvey Arye</strong></p><p><a href="https://www.linkedin.com/in/matvey-arye/"><u>Matvey Arye</u></a> is a founding engineering leader at Tiger Data (creators of TimescaleDB), the premiere provider of relational database technology for time-series data and AI. Currently, he manages the team at Tiger Data responsible for building the go-to developer platform for AI applications.&nbsp;</p><p>Under his leadership, the Tiger Data engineering team has introduced partitioning, compression, and incremental materialized views for time-series data, plus cutting-edge indexing and performance innovations for AI.&nbsp;</p><p>Matvey earned a Bachelor degree in Engineering at The Cooper Union. He earned a Doctorate in Computer Science at Princeton University where his research focused on cross-continental data analysis covering issues such as networking, approximate algorithms, and performant data processing.&nbsp;</p><p><strong>Jacky Liang</strong></p><p><a href="https://www.linkedin.com/in/jjackyliang/"><u>Jacky Liang</u></a> is a developer advocate at Tiger Data with an AI and LLMs obsession. He's worked at Pinecone, Oracle Cloud, and Looker Data as both a software developer and product manager which has shaped the way he thinks about software.&nbsp;</p><p>He cuts through AI hype to focus on what actually works. How can we use AI to solve real problems? What tools are worth your time? How will this technology actually change how we work?&nbsp;</p><p>When he's not writing or speaking about AI, Jacky builds side projects and tries to keep up with the endless stream of new AI tools and research—an impossible task, but he keeps trying anyway. His model of choice is Claude Sonnet 4 and his favorite coding tool is Claude Code.</p><p></p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The Database Has a New User—LLMs—and They Need a Different Database]]></title>
            <description><![CDATA[Tiger Data experiments with self-describing Postgres using semantic catalogs. Early tests show 27% better SQL generation accuracy when AI agents understand schemas.
]]></description>
            <link>https://www.tigerdata.com/blog/the-database-new-user-llms-need-a-different-database</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/the-database-new-user-llms-need-a-different-database</guid>
            <category><![CDATA[AI agents]]></category>
            <category><![CDATA[PostgreSQL]]></category>
            <category><![CDATA[AI]]></category>
            <dc:creator><![CDATA[Matvey Arye]]></dc:creator>
            <pubDate>Thu, 21 Aug 2025 12:59:10 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2025/08/experimenting-with-a-self-describing-postgresql-database-for-the-agentic-era.png">
            </media:content>
            <content:encoded><![CDATA[<h3 id="we%E2%80%99re-experimenting-with-a-database-that-can-self-describe-and-we%E2%80%99re-starting-with-the-most-popular-one-in-the-world-postgres">We’re experimenting with a database that can self-describe. And we’re starting with the most popular one in the world: Postgres.</h3><p><strong>TL;DR:</strong> We’re experimenting with turning Postgres into a <strong>self-describing database</strong>, embedding meaning as part of the schema. By providing natural language explanations of PostgreSQL structures and logic, agents can more accurately query data, and answer a broader set of questions. In our early tests, using an LLM-generated semantic catalog improved SQL generation accuracy by up to 27%. Here's the <a href="https://github.com/timescale/pgai/tree/main/docs/semantic_catalog" rel="noreferrer">repo link</a> for reference.</p><h2 id="databases-lack-context-about-their-structures">Databases Lack Context About Their Structures</h2><p>As any developer who has ever had the (mis)fortune of working on a legacy database knows quite well, a database is not self-describing. You can’t look at a database schema and tell what’s going on. That <code>orders1</code> table can be for a purchase order or a customer order or it’s an experimental table someone created and forgot to drop 5 years ago. This is a long-standing problem with databases that people solve by talking to each other, looking at code that interacts with the database, examining git history, and screaming at the wall.&nbsp;&nbsp;</p><p>But, LLMs need to answer questions about the data just from the context provided by the database. No wonder they get confused.&nbsp;</p><p>Like our fearless leader <a href="https://www.tigerdata.com/blog/author/ajay"><u>Ajay Kulkarni</u></a> once said—<em>“LLMs crave (accurate) context the same way GPUs crave power.”</em></p><p>In order for agents to extract insights from data in the database it needs to understand which columns and tables to query. For example, if it doesn’t understand which table contains the customer vs purchase orders, it can’t understand when a customer’s order has shipped.</p><p>In fact, in internal experiments we conducted at Tiger Data, we found that 42% of context-less LLM-generated SQL queries missed critical filters or misunderstood relationships, silently returning misleading data.&nbsp;</p><p>The larger (and thus probably more important) the database, the worse things get.</p><p>It doesn’t take a senior engineer to know that this is unacceptable.</p><h2 id="adding-context-with-semantic-catalogs">Adding Context with Semantic Catalogs</h2><p>The solution is incredibly simple: allow developers to add context about the database in the form of natural language descriptions of the schemas and business logic within Postgres, injecting much-needed factual meaning into schemas.&nbsp;</p><p>We arrived at this by putting ourselves in the place of the LLM — asking, how would we generate the query if we were in its shoes? The answer was clear: without additional context, we wouldn’t know enough to make the right call. Along the way, we tried different prompts, techniques, and even alternative LLMs, but the core issue was always the same: the model simply lacked the context necessary to generate the query. That realization led us to build a way for developers to provide that missing context directly.</p><p>That's what we’ve been experimenting with and are excited to share today.&nbsp;&nbsp;</p><p>We set out to test what happens when SQL generation is powered by our LLM-generated semantic catalog. The outcome was a 27% boost in accuracy compared to the control.</p><p>Let's show you how we created this self-describing Postgres database.</p><h2 id="building-a-self-describing-database">Building a Self-Describing Database</h2><p>Our thoughts for this self-describing Postgres are built around four core ideas:&nbsp;</p><ol><li><strong>Embed semantics alongside schema: </strong><br>Every table, column, function, and business rule should be described clearly—in natural language.</li><li><strong>Versioned and governed descriptions: </strong><br>Metadata should live alongside application code, version-controlled, reviewed, and governed with the same rigor.</li><li><strong>Self-correcting querying: </strong><br>Postgres itself should expose safety hints, telling agents which queries are expensive or unsafe, and provide deterministic verification mechanisms (<code>EXPLAIN</code>) to catch errors before queries run.</li><li><strong>Measure and iterate transparently: </strong><br>Developers should be able to build an evaluation suite for agent interactions and determine how well the system performed. Critically, the developers should see what errors are due to bad schema descriptions and lack of context (retrieval errors) or incorrect reasoning (logical errors).</li></ol><h2 id="what-we%E2%80%99re-experimenting-with-today">What We’re Experimenting With Today</h2><p>We’re exploring this approach through two building blocks. This post primarily covers the Semantic Catalog. Next week, we’ll publish a post about the evaluation harness.</p>
<!--kg-card-begin: html-->
<table>
        <thead>
            <tr>
                <th>Component</th>
                <th>What It Does</th>
                <th>Repo</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>Semantic Catalog</td>
                <td>Stores natural-language descriptions for schema elements. Supports vector search to retrieve relevant context dynamically.</td>
                <td><a href="https://github.com/timescale/pgai/tree/main/docs/semantic_catalog" target="_blank" style="text-decoration: underline">Link</a></td>
            </tr>
            <tr>
                <td>Evaluation Harness</td>
                <td>Measures query accuracy in any agentic text-to-sql system and transparently reveals errors in retrieval (bad metadata/context) versus reasoning (bad SQL generation). Helps track accuracy over time.</td>
                <td>TBD</td>
            </tr>
        </tbody>
    </table>
<!--kg-card-end: html-->
<h3 id="why-isnt-the-problem-just-retrieval">Why isn't the problem just retrieval?</h3><p>Retrieval using the schema element names alone does not cut it simply because the names lack enough meaning. It doesn’t matter if you use keyword search, semantic search, or this week’s newest shiny retrieval algorithm—the semantic meaning of <code>payment_aborts</code> versus&nbsp; <code>refunds</code> is too close for any retrieval mechanism to make heads or tails of it.&nbsp;&nbsp;</p><p>Even if you injected the entire schema into the context of your prompt (burning through cash and increasing response latency in the process), it’s doubtful the model would be able to reason about&nbsp;<code>payment_aborts</code> versus&nbsp;<code>refunds</code> either.</p><p>The semantic catalog solves this by offering a structured, natural language representation of your database's metadata and business logic. In this experimental version, it supports documentation and descriptions for:</p><ul><li>Tables and views</li><li>Columns</li><li>Functions and procedures</li><li>Example queries</li><li>Facts: business logic rules or freeform context</li></ul><p>Initial descriptions can be LLM-generated and stored in human-readable YAML files designed for version control, peer review, and governance. Once reviewed, this metadata can be imported into the semantic catalog and indexed for semantic search, simplifying the process of retrieving and reasoning about data structures.</p><h3 id="performance-enhancement-through-semantic-context">Performance enhancement through semantic context</h3><p>In our early tests, using an LLM-generated semantic catalog improved SQL generation accuracy by up to 27%. Effective SQL generation involves two key steps:</p><ol><li><strong>Retrieving Relevant Context</strong>: Identifying necessary database components</li><li><strong>SQL Generation Reasoning</strong>: Formulating accurate SQL queries based on the retrieved context</li></ol><p>Our evaluation revealed substantial improvements from using semantic descriptions for SQL generation accuracy (from 58% to 86%) for certain schemas. A smaller improvement was noted when using semantic descriptions in retrieval recall (from 52% to 58% on one of our tests).</p><p>Results varied significantly across datasets, schemas, and models. Our current evaluations were conducted using a more cost-effective model (OpenAI's gpt-4.1-nano). We're particularly interested in insights from users experimenting with diverse datasets and more advanced models.</p><p>With these results, let’s walk through how the agent actually interacts with the database.</p><h3 id="how-queries-work-the-minimal-agent-loop">How queries work: the minimal agent loop</h3><p>Every semantic query follows this four-step flow:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2025/08/self-describing-postgres-diagram-vertical.png" class="kg-image" alt="Developers and agents alike gain visibility and control at each step." loading="lazy" width="2000" height="581" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2025/08/self-describing-postgres-diagram-vertical.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2025/08/self-describing-postgres-diagram-vertical.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2025/08/self-describing-postgres-diagram-vertical.png 1600w, https://timescale.ghost.io/blog/content/images/size/w2400/2025/08/self-describing-postgres-diagram-vertical.png 2400w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Developers and agents alike gain visibility and control at each step.</span></figcaption></figure><p>Walking through step-by-step:</p><p><strong>Step 1: Describe your database</strong> <strong>using an LLM</strong></p><p>Generate initial YAML descriptions with an LLM:</p><pre><code class="language-Shell">pgai semantic-catalog describe -f descriptions.yaml</code></pre><p>Review and refine these descriptions, storing them with the same rigor as your application code.</p><p>The resulting structure looks like this:</p><pre><code class="language-YAML">schema: public
name: restaurant
type: table
description: Stores core information about each restaurant, including its cuisine,
  city, and rating.
columns:
- name: id
  description: Primary key identifier for the restaurant.
- name: name
  description: Lowercased name of the restaurant.
- name: food_type
  description: Lowercased name of the type of cuisine or food style offered by the restaurant.
- name: city_name
  description: Lowercased name of the city where the restaurant is located.
- name: rating
  description: Numeric rating score assigned to the restaurant, where higher than 2.5 is considered good.
...
---
type: sql_example
sql: SELECT t2.house_number, t1.name FROM LOCATION AS t2 JOIN restaurant AS t1 ON t1.id = t2.restaurant_id WHERE t1.city_name IN (SELECT city_name FROM geographic WHERE region = 'bay area') AND t1.rating &gt; 2.5
description: give me some good restaurants in the bay area ?
...
---
type: fact
description: When asking for a restaurant, provide its name (restaurant.name) and its house number (location.house_number).
...</code></pre><p><strong>Step 2: Have a human review the description and add context</strong></p><p>A developer would then review the descriptions and add additional context. Crucially, business logic that is central to database operations is often not encoded into the schema, and thus cannot be derived by the LLM. A developer needs to provide that information.</p><p>After the descriptions are reviewed, they should be treated as a core part of application code and thus should be stored in version control, be reviewed in pull-requests, etc.</p><p><strong>Step 3: Import into the catalog</strong>&nbsp;</p><p>Make descriptions available:</p><pre><code class="language-Shell">pgai semantic-catalog import -f descriptions.yaml</code></pre><p>This supports declarative configuration and continuous deployment practices.</p><p><strong>Step 4: Generate SQL</strong>&nbsp;</p><p>Generate SQL from natural language queries:</p><p>CLI:</p><pre><code class="language-Shell">pgai semantic-catalog generate-sql -p "Which passengers have experienced the most flight delays in 2024?"</code></pre><p>Python:</p><pre><code class="language-Python">response = await catalog.generate_sql(
    con,
    con,
    "openai:gpt-4.1",
    "Which passengers have experienced the most flight delays in 2024?",
)
</code></pre><h2 id="key-lessons-so-far">Key Lessons So Far</h2><h3 id="semantic-context-matters">Semantic context matters&nbsp;</h3><p>Models are far better at generating correct SQL when they have access to rich semantic information, not just schema names, but natural-language descriptions. Even a small amount of context (e.g. "the order table tracks transactions for customers buying subscription plans") can dramatically improve reliability.</p><h3 id="narrow-interfaces-build-confidence">Narrow interfaces build confidence&nbsp;</h3><p>What we’ve noticed with agentic text-to-sql systems is that to get good accuracy you need to tighten the scope of what kind of information the system has access to. The relationship looks something like this:</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2025/08/data-scope-generality-vs-accuracy.png" class="kg-image" alt="" loading="lazy" width="528" height="432"></figure><p>Successful agentic systems start with tight scopes. We found it valuable to restrict agents to function-level or view-level access at first, and only expand access once correctness is proven.</p><p>Postgres allows you to balance control and flexibility through three main interfaces:</p><ul><li><strong>Functions</strong>: Highly controlled but narrow scope</li><li><strong>Views</strong>: Moderate control with broader access</li><li><strong>Raw Tables</strong>: Most general but least constrained and more prone to failure</li></ul><p>We suggest beginning with tightly scoped functions, then expanding access as confidence grows.</p><h2 id="self-correcting-using-explain">Self-Correcting Using EXPLAIN</h2><p>To further improve reliability, we employ Postgres's deterministic EXPLAIN command. This preemptively catches query errors such as incorrect column or table names, allowing agents to self-correct and substantially increasing accuracy.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2025/08/Explain-plan.png" class="kg-image" alt="" loading="lazy" width="949" height="265" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2025/08/Explain-plan.png 600w, https://timescale.ghost.io/blog/content/images/2025/08/Explain-plan.png 949w" sizes="(min-width: 720px) 720px"></figure><h2 id="store-the-semantic-catalog-anywhere">Store the Semantic Catalog Anywhere</h2><p>For many deployments, integrating the semantic catalog directly into your existing database provides the simplest path. Recognizing, however, the risks and complexities of altering production environments, we've also enabled the semantic catalog to be hosted independently in a separate database. This approach offers greater deployment flexibility and ensures accessibility even when you only have read-only access to your primary database.</p><h2 id="what%E2%80%99s-next-from-self-describing-to-self-learning">What’s Next: From Self-Describing to Self-Learning</h2><p>While this semantic foundation is already improving agentic querying, we're just beginning. Our roadmap focuses on:</p><ul><li><strong>Self-learning catalog:</strong> Automatically enrich metadata by analyzing queries from production environments. Let databases learn from usage to continuously enhance accuracy.</li><li><strong>Dynamic policy management:</strong> Express complex access rules—like row-level privacy policies—in natural language, automatically enforced by the database.</li></ul><p>We’ve open-sourced everything we’ve built so far. Check out the <a href="https://github.com/timescale/pgai/blob/main/docs/semantic_catalog/README.md"><u>README with a Quickstart</u></a> to dive in, and we warmly invite your contributions, whether it's running evaluations on your schemas, proposing datasets, or challenging our assumptions. The best databases aren't just designed; they evolve through community effort.</p><hr><p><strong>About the author</strong></p><p><a href="https://www.linkedin.com/in/matvey-arye/"><u>Matvey Arye</u></a> is a founding engineering leader at Tiger Data (creators of TimescaleDB), the premiere provider of relational database technology for time-series data and AI. Currently, he manages the team at Tiger Data responsible for building the go-to developer platform for AI applications.&nbsp; Under his leadership, the Tiger Data engineering team has introduced partitioning, compression, and incremental materialized views for time-series data, plus cutting-edge indexing and performance innovations for AI.&nbsp;</p><p>Matvey earned a Bachelor degree in Engineering at The Cooper Union. He earned a Doctorate in Computer Science at Princeton University where his research focused on cross-continental data analysis covering issues such as networking, approximate algorithms, and performant data processing.&nbsp;</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How We Made PostgreSQL as Fast as Pinecone for Vector Data]]></title>
            <description><![CDATA[Read how we equipped PostgreSQL with advanced indexing techniques, making it as fast as other specialized vector databases, like Pinecone.]]></description>
            <link>https://www.tigerdata.com/blog/how-we-made-postgresql-as-fast-as-pinecone-for-vector-data</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/how-we-made-postgresql-as-fast-as-pinecone-for-vector-data</guid>
            <category><![CDATA[AI]]></category>
            <category><![CDATA[PostgreSQL]]></category>
            <category><![CDATA[pgvector]]></category>
            <dc:creator><![CDATA[Matvey Arye]]></dc:creator>
            <pubDate>Tue, 11 Jun 2024 12:05:45 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2024/06/How-We-Made-PostgreSQL-as-Fast-as-Pinecone-for-Vector-Data_Binary-Quantization-1.png">
            </media:content>
            <content:encoded><![CDATA[<p>We’ve <a href="https://timescale.ghost.io/blog/pgvector-is-now-as-fast-as-pinecone-at-75-less-cost" rel="noreferrer">recently announced the open-sourcing of pgvectorscale</a>, a new <a href="https://www.tigerdata.com/blog/top-8-postgresql-extensions" rel="noreferrer">PostgreSQL extension</a> that provides advanced indexing techniques for vector data. Pgvectorscale provides a new index method for pgvector data, significantly improving the search performance of approximate nearest neighbor (ANN) queries. These queries are key for leveraging modern <a href="https://www.tigerdata.com/blog/a-beginners-guide-to-vector-embeddings" rel="noreferrer">vector embedding</a> techniques to facilitate <a href="https://www.tigerdata.com/learn/vector-search-vs-semantic-search" rel="noreferrer"><u>semantic search</u></a>, which allows for finding things similar to a query's <em>meaning. </em>That, in turn, enables applications like retrieval-augmented generation (RAG), summarization, clustering, or general search.</p><p>In our announcement post, we described how our new StreamingDiskANN vector index allows us to <a href="https://www.tigerdata.com/search" rel="noreferrer">perform vector search faster than bespoke purpose-built databases created for this purpose—like Pinecone</a>. We also observed that if bespoke databases aren’t faster, then there is no reason to use them because they can’t possibly compete with the rich feature set and ecosystem of general-purpose databases like PostgreSQL.</p><p>In this article we’ll go into the technical contributions that allowed us to “break the speed barrier” and create a fast vector index in PostgreSQL. We’ll cover three technical improvements we made:</p><ul><li><strong>Implementing the DiskANN algorithm</strong> to allow the index to be stored on SSDs instead of having to reside in memory. This vastly decreases the cost of storing large amounts of vectors since SSDs are much cheaper than RAM.&nbsp;</li><li><strong>Supporting streaming post-filtering,</strong> which allows for accurate retrieval even when secondary filters are applied. In contrast, the <a href="https://www.tigerdata.com/blog/vector-database-basics-hnsw" rel="noreferrer">HNSW</a> (hierarchical navigable small world) index fails to accurately retrieve data if the filters exclude the first <code>ef_search</code> vectors. Pinecone had previously complained about this problem when comparing itself to pgvector. Guess what; through the power of open source, this issue has been resolved.&nbsp;</li><li><strong>Developing a completely new vector quantization algorithm </strong>we call SBQ (statistical binary quantization). This algorithm provides a better accuracy vs. performance trade-off compared to existing ones like BQ (binary quantization) and PQ (product quantization).</li></ul><h2 id="enhancing-postgresql-for-vector-data">Enhancing PostgreSQL for Vector Data</h2><h3 id="implementing-the-diskann-algorithm-to-optimize-for-ssd-storage"><br>Implementing the DiskANN algorithm to optimize for SSD storage</h3><p>The <a href="https://www.tigerdata.com/learn/understanding-diskann" rel="noreferrer"><u>DiskANN</u></a> algorithm was developed by work coming out of Microsoft. Its goal was to store a very large number of vectors (think Microsoft scale). At that scale, it was simply uneconomical to store everything in RAM. Thus, the algorithm is geared towards enabling storing vectors on SSDs and using less RAM. Its details are described very well in the <a href="https://proceedings.neurips.cc/paper_files/paper/2019/file/09853c7fb1d3f8ee67a61b6bf4a7f8e6-Paper.pdf"><u>paper</u></a>, so I’ll only give a bit of intuition below.&nbsp;</p><p>The DiskANN algorithm is a graph-based search algorithm like <a href="https://www.tigerdata.com/learn/hnsw-vs-diskann" rel="noreferrer">HNSW</a>. Graph-based algorithms in this space have a well-known problem: finding an item that’s “very far” from the starting position is expensive because it requires a lot of hops.&nbsp;</p><p>HNSW solves this problem by introducing a system of layers where the first (top) layer only has “long-range” edges that quickly get you into the right vicinity and have pointers to nodes into lower levels that allow you to traverse the graph in a more fine-grained way. This solves the long-range problem but introduces more indirection through the layering system, which requires more random-access that forces the graph into RAM for good performance.&nbsp;</p><p>In contrast, DiskANN uses a single-layer graph and solves the long-range problem during graph construction by allowing for neighbor edges that refer to far-away nodes. The single-layer construction simplifies the algorithm and decreases the random access necessary during search, allowing SSDs to be used effectively.</p><h3 id="support-for-streaming-retrieval-for-accurate-metadata-filtering">Support for streaming retrieval for accurate metadata filtering</h3><p>Oftentimes, when searching for semantically similar items, you want to constrain your search with additional filters. For example, documents are often associated with a set of tags and you may want to constrain your search by requiring a match of the tags as well as vector similarity.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2024/06/How-We-Made-PostgreSQL-as-Fast-as-Pinecone-for-Vector-Data_two-stage-filtering.png" class="kg-image" alt="A diagram representing two-stage filtering" loading="lazy" width="2000" height="1657" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2024/06/How-We-Made-PostgreSQL-as-Fast-as-Pinecone-for-Vector-Data_two-stage-filtering.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2024/06/How-We-Made-PostgreSQL-as-Fast-as-Pinecone-for-Vector-Data_two-stage-filtering.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2024/06/How-We-Made-PostgreSQL-as-Fast-as-Pinecone-for-Vector-Data_two-stage-filtering.png 1600w, https://timescale.ghost.io/blog/content/images/2024/06/How-We-Made-PostgreSQL-as-Fast-as-Pinecone-for-Vector-Data_two-stage-filtering.png 2400w" sizes="(min-width: 720px) 720px"><figcaption><i><em class="italic" style="white-space: pre-wrap;">Figure 1: The problem with two-stage post-filtering is that if the matching records aren’t located in the set before the cutoff of the first stage, the final answer will be incorrect.</em></i></figcaption></figure><p>This is challenging for many HNSW-based indexes (including pgvector’s implementation) because the index retrieves a pre-set number of records from the index (set by the <code>hnsw.ef_search</code> parameter, often set to 1,000 or less) <em>before</em> applying secondary filters. If not enough items in the retrieved set (e.g., first 1,000 items) match the secondary filters, you will miss those results.&nbsp;</p><p>Figure 1 illustrates this problem when you use <code>hnsw.ef_search=5</code> to find the top two vectors closest to a given query <strong>and</strong> matching the tag “department=engineering”. In this scenario, the first item with the correct tag is the seventh vector closest to the query.&nbsp;</p><p>Since the vector search returns only the closest five items and none matches the tag filter, no results will be returned! This is an extreme example where no results are left over, but there will be some accuracy loss any time the retrieved set has less than k items matching the filter.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2024/06/How-We-Made-PostgreSQL-as-Fast-as-Pinecone-for-Vector-Data_streaming-filtering.png" class="kg-image" alt="A diagram representing streaming filtering" loading="lazy" width="2000" height="1657" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2024/06/How-We-Made-PostgreSQL-as-Fast-as-Pinecone-for-Vector-Data_streaming-filtering.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2024/06/How-We-Made-PostgreSQL-as-Fast-as-Pinecone-for-Vector-Data_streaming-filtering.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2024/06/How-We-Made-PostgreSQL-as-Fast-as-Pinecone-for-Vector-Data_streaming-filtering.png 1600w, https://timescale.ghost.io/blog/content/images/2024/06/How-We-Made-PostgreSQL-as-Fast-as-Pinecone-for-Vector-Data_streaming-filtering.png 2400w" sizes="(min-width: 720px) 720px"><figcaption><i><em class="italic" style="white-space: pre-wrap;">Figure 2: Streaming filtering produces the correct result by exposing a </em></i><i><code spellcheck="false" style="white-space: pre-wrap;"><em class="italic">get_next()</em></code></i><i><em class="italic" style="white-space: pre-wrap;"> function that can be called continuously until the right number of records are found. </em></i></figcaption></figure><p>In contrast, our StreamingDiskANN index has no “ef_search” type cutoff. Instead, as shown in Figure 2, it uses a streaming model that allows the index to continuously retrieve the “next closest” item for a given query, potentially even traversing the entire graph! The Postgres execution system will continuously ask for the “next closet” item until it has matched the <code>LIMIT N</code> items that satisfy the additional filters. This is a form of post-filtering that suffers absolutely no accuracy degradation.</p><p>As a side note, Pinecone made a big deal of the “ef_search” type limitation to deposition pgvector in <a href="https://www.pinecone.io/blog/pinecone-vs-pgvector/"><u>their comparison</u></a>. But, with the introduction of StreamingDiskANN, this criticism no longer applies. This just shows the power of open-source projects to move quickly to mitigate limitations.</p><h3 id="statistical-binary-quantization-sbq-a-new-quantization-algorithm">Statistical binary quantization (SBQ): A new quantization algorithm</h3><p>Many vector indexes use compression to reduce the space needed for vector storage and make index traversal faster at the cost of some loss in accuracy. The common algorithms are product quantization (PQ) and binary quantization (BQ). In fact, pgvector’s <a href="https://www.tigerdata.com/learn/vector-database-basics-hnsw" rel="noreferrer">HNSW index</a> just added BQ in their <a href="https://github.com/pgvector/pgvector/blob/master/CHANGELOG.md?ref=timescale.com#070-2024-04-29" rel="noreferrer">latest 0.7.0 release</a> (hooray!).&nbsp;</p><p>The way most vector databases work to retrieve K results is as follows. The system first retrieves N results (N&gt;K) using the approximate quantized differences, then “corrects” for the error by rescoring. It calculates the full distance for the N results, sorts the list by the full distance, and returns the K items with the smallest distance. Yet, even with rescoring, accuracy is important because it allows you to decrease N (and thus query faster) and improve the chances that the accurate result will be in the set of N pre-fetched results.</p><p>We took a look at the BQ algorithm and were unhappy with the amount of accuracy loss it produced. We also immediately saw some low-hanging fruit to improve it. In tinkering with the algorithm, we developed a new compression algorithm we are calling statistical binary quantization (SBQ).&nbsp;</p><p>The BQ compression algorithm transforms a floating-point vector into a binary vector in a surprisingly simple way: for each element in the vector, if the value is greater than 0.0, make the binary value 1; otherwise, set the binary value to 0. Then, the distance function simply becomes the <code>XOR</code> function. Why <code>XOR</code>? Well, you’ll find many mathematical explanations (none of which we quite like) but the intuition we use is that the binary vector divides the space into quadrants as seen in Figure 3, and the <code>XOR</code> function is simply a count of how many planes you have to cross to get from one quadrant to another.&nbsp;</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2024/06/How-We-Made-PostgreSQL-as-Fast-as-Pinecone-for-Vector-Data_Binary-Quantization.png" class="kg-image" alt="A diagram representing BQ" loading="lazy" width="2000" height="1460" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2024/06/How-We-Made-PostgreSQL-as-Fast-as-Pinecone-for-Vector-Data_Binary-Quantization.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2024/06/How-We-Made-PostgreSQL-as-Fast-as-Pinecone-for-Vector-Data_Binary-Quantization.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2024/06/How-We-Made-PostgreSQL-as-Fast-as-Pinecone-for-Vector-Data_Binary-Quantization.png 1600w, https://timescale.ghost.io/blog/content/images/2024/06/How-We-Made-PostgreSQL-as-Fast-as-Pinecone-for-Vector-Data_Binary-Quantization.png 2400w" sizes="(min-width: 720px) 720px"><figcaption><i><em class="italic" style="white-space: pre-wrap;">Figure 3: BQ for three dimensions. Quadrant 1 is represented by the binary vector [1,1,1] and any vector falling into that quadrant will have a distance of 0. The distance with vectors in other quadrants increases with the number of dimensions that are different.</em></i></figcaption></figure><p>One of the immediate things that struck us as odd is that the cutoff for each dimension is always 0.0. This was odd because in analyzing real embeddings we’ve previously found that the mean for each dimension is not even approximately 0.0. That means the quadrants we are defining in BQ are not dividing the space of points in half and thus missing out on opportunities for differentiation.&nbsp;</p><p>Intuitively, you want the “origin” of your cutting plane in the middle of all the action, but in BQ, it’s off to the side. The solution was quite simple: we used a learning pass to derive the mean value for each dimension and then set the float-value cutoff to the mean instead of 0.0. Thus, we set the binary value for an element to 1 if and only if the float value is greater than the mean for the dimension.</p><p>But then we noticed yet another odd thing: the compression algorithm worked better for 1,536 dimensions than for 768 dimensions. This made little sense to use because the literature strongly implies that problems with higher dimensions are harder than lower dimensions (the so-called “curse of dimensionality”). But here, the opposite is true.&nbsp;</p><p>However, thinking about the quadrant analogy, this kind of made sense—we’d have fewer quadrants with 768 dimensions, and each quadrant would be bigger and thus less differentiated. So we asked ourselves, could we create more quadrants with 768 dimensions?&nbsp;</p><p>Our approach was to convert each floating-point dimension into two bits (which we later generalized). The idea was to use the mean and standard deviations to derive a z-score (a value’s distance from the mean normalized by standard deviation) and then divide the z-score into three regions. We then encode the three regions into two bits so that adjacent regions have a <code>XOR</code> distance of 1, and the distance increases with the z-score distance. In the two-bit case with three regions the encoding is 00, 01, 11.&nbsp;</p><p>Experimentally, we found that two-bit encoding really helps accuracy with the 768-dimension case. Thus, by default, we use two-bit encoding for any data with less than about 900 dimensions and one-bit encoding otherwise. In one representative example on a dataset with 768 dimensions, the recall improved from 96.5&nbsp;% to 98.6&nbsp;% when switching from the one-bit to two-bit encoding, a significant improvement at such high recall levels.</p><p>In sum, these techniques help us achieve a better accuracy/performance trade-off.</p><h2 id="a-better-postgresql-for-vector-data">A Better PostgreSQL for Vector Data</h2><p>The three techniques we covered in this post allow us to develop a best-in-class index for vector data in PostgreSQL that rivals the performance of bespoke databases like Pinecone. We were able to achieve this with a small team by harnessing much of the infrastructure that PostgreSQL provides, including caching, WAL (write-ahead logging), and the associated recovery infrastructure, and a rock-solid disk writing system.&nbsp;</p><p>We wrote this in Rust using the <a href="https://github.com/pgcentralfoundation/pgrx"><u>PGRX</u></a> framework for writing Rust extensions for PostgreSQL. This further sped up development because we could rely on some of the safety guarantees that Rust and PGRX provide while developing our own safe wrappers for tricky parts of the code (like disk I/O). We think that this combination of tools is really useful and powerful for developing database features and extending the reach of PostgreSQL.&nbsp;</p><h3 id="next-steps">Next steps</h3><p>Our team has been working tirelessly in the last few months to equip PostgreSQL with these new advanced indexing techniques for vector data. Our goal is to help PostgreSQL developers become AI developers. But for that, we need your feedback.</p><p>Here’s how you can get involved:&nbsp;</p><ul><li><strong>Share the news with your friends and colleagues</strong>: Share our posts announcing <a href="https://www.tigerdata.com/blog/pgai-giving-postgresql-developers-ai-engineering-superpowers" rel="noreferrer">pgai</a> and pgvectorscale on <a href="https://x.com/TimescaleDB"><u>X/Twitter</u></a>, <a href="https://www.linkedin.com/company/timescaledb/"><u>LinkedIn</u></a>, and Threads. We promise to RT back.</li><li><strong>Submit issues and feature requests</strong>: We encourage you to submit issues and feature requests for functionality you’d like to see, bugs you find, and suggestions you think would improve both projects.</li><li><strong>Make a contribution</strong>: We welcome community contributions for both pgvectorscale and pgai. Pgvectorscale is written in Rust, while pgai uses Python and PL/Python. For pgai specifically, let us know which models you want to see supported, particularly for open-source embedding and generation models. <a href="https://github.com/timescale/pgai/" rel="noreferrer">See the pgai GitHub</a> for more.</li><li><strong>Offer pgvectorscale and pgai extensions on your PostgreSQL cloud</strong>: Pgvectorscale and pgai are open-source projects under the <a href="https://github.com/timescale/pgvectorscale/blob/main/LICENSE" rel="noreferrer"><u>PostgreSQL License</u></a>. We encourage you to offer pgvectorscale and pgai on your managed PostgreSQL database-as-a-service platform, and we can even help you spread the word. Get in touch via our <a href="https://www.timescale.com/contact"><u>Contact Us form</u></a> and mention pgai and pgvectorscale to discuss further.</li><li><strong>Use pgai and pgvectorscale today</strong>: You can find installation instructions on the <a href="https://github.com/timescale/pgai/" rel="noreferrer">pgai GitHub</a> and <a href="https://github.com/timescale/pgvectorscale/" rel="noreferrer">pgvectorscale GitHub</a> repositories, respectively. <a href="https://www.tigerdata.com/search" rel="noreferrer">You can also access both pgai and pgvectorscale on any database service on Tiger Data’s cloud PostgreSQL platform</a>. For production vector workloads, we’re offering private beta access to vector-optimized databases with pgvector and pgvectorscale on Timescale. <a href="https://timescale.typeform.com/to/H7lQ10eQ"><u>Sign up here for priority access</u></a>.</li></ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How We Designed a Resilient Vector Embedding Creation System for PostgreSQL Data]]></title>
            <description><![CDATA[Learn the design decisions and trade-offs behind our system for creating and storing vector embeddings for data in PostgreSQL, PgVectorizer.]]></description>
            <link>https://www.tigerdata.com/blog/how-we-designed-a-resilient-vector-embedding-creation-system-for-postgresql-data</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/how-we-designed-a-resilient-vector-embedding-creation-system-for-postgresql-data</guid>
            <category><![CDATA[PostgreSQL]]></category>
            <category><![CDATA[AI]]></category>
            <category><![CDATA[Engineering]]></category>
            <dc:creator><![CDATA[Matvey Arye]]></dc:creator>
            <pubDate>Fri, 10 Nov 2023 13:33:39 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2023/11/How-we-designed-a-resilient-embedding-system-for-PostgreSQL-Data.png">
            </media:content>
            <content:encoded><![CDATA[<p>Embedding data stored in a PostgreSQL table is undoubtedly useful—with applications ranging from semantic search and recommendation systems to generative AI applications and retrieval augmented generation. But creating and managing embeddings for data in PostgreSQL tables can be tricky, with many considerations and edge cases to take into account, such as keeping embeddings up to date with table updates and deletes, ensuring resilience against failures, and impact to existing systems dependent on the table.</p><p><a href="https://timescale.ghost.io/blog/a-complete-guide-to-creating-and-storing-embeddings-for-postgresql-data/" rel="noreferrer">In a previous blog post, we detailed a step-by-step guide on the process of creating and managing embeddings</a> for data residing in PostgreSQL using <a href="https://docs.timescale.com/ai/latest/pgvectorizer/" rel="noreferrer">PgVectorizer</a>—our simple and resilient embedding creation system for data residing in PostgreSQL. Using a blog application with data stored in a PostgreSQL database as an example, we covered how to create and keep up-to-date vector embeddings using <a href="https://timescale.ghost.io/blog/a-python-library-for-using-postgresql-as-a-vector-database-in-ai-applications/">Python</a>, <a href="https://python.langchain.com/docs/integrations/vectorstores/timescalevector">LangChain</a>, and <a href="https://www.timescale.com/ai">pgai on Timescale</a>.</p><p>In this blog post, we’ll discuss the technical design decisions and the trade-offs we made while building <a href="https://docs.timescale.com/ai/latest/pgvectorizer/" rel="noreferrer">PgVectorizer</a> to ensure simplicity, resilience, and high performance. We’ll also discuss alternative designs if you want to roll your own.</p><p>Let’s jump into it.</p><h2 id="design-of-a-high-performance-vectorizer-for-postgresql-data-pgvectorizer">Design of a High-Performance Vectorizer for PostgreSQL Data (PgVectorizer)</h2><p>First, let’s describe how the system we are building will work. Feel free to skip this section if you already read the <a href="https://timescale.ghost.io/blog/a-complete-guide-to-creating-and-storing-embeddings-for-postgresql-data/" rel="noreferrer">PgVectorizer post</a>.</p><h3 id="system-overview">System overview</h3><p>As an illustrative example, we’ll use a simple blog application storing data in PostgreSQL using a table defined as follows:</p><pre><code class="language-SQL">CREATE TABLE blog (
  id              SERIAL PRIMARY KEY NOT NULL,
  title           TEXT NOT NULL, 
  author          TEXT NOT NULL,
  contents        TEXT NOT NULL,
  category        TEXT NOT NULL,
  published_time  TIMESTAMPTZ NULL --NULL if not yet published
);
</code></pre>
<p>We want to create embeddings on the contents of the blog post so we can later use it for semantic search and power retrieval augmented generation. Embeddings should only exist and be searchable for blogs that have been published (where the <code>published_time</code> is <code>NOT NULL</code>).&nbsp;</p><p>While building this embeddings system, we were able to identify a number of goals that any straightforward and resilient system that creates embeddings should have:</p><ul><li><strong>No modifications to the original table. </strong>This allows systems and applications that already use this table not to be impacted by changes to the embedding system. This is especially important for legacy systems.</li><li><strong>No modification to the applications that interact with the table. </strong>Having to modify the code that alters the table may not be possible for legacy systems. It’s also poor software design because it couples systems that don’t use embeddings with code that generates the embedding.</li><li><strong>Automatically update embeddings</strong> when rows in the source table change (in this case, the blog table). This lessens the maintenance burden and contributes to worry-free software. At the same time, this update need not be instantaneous or within the same commit. For most systems, “eventual consistency” is just fine.</li><li><strong>Ensure resilience against network and service failures: </strong>Most systems generate embeddings via a call to an external system, such as the OpenAI API. In scenarios where the external system is down, or a network malfunction occurs, it's imperative that the remainder of your database system continues working.</li></ul><p>These guidelines were the basis of a robust architecture that we implemented using the <a href="https://github.com/timescale/python-vector">Python Vector library</a>, a library for <a href="https://timescale.ghost.io/blog/a-python-library-for-using-postgresql-as-a-vector-database-in-ai-applications/">working with vector data using PostgreSQL</a>. To complete the job successfully, added new functionality to this library—<a href="https://docs.timescale.com/ai/latest/pgvectorizer/" rel="noreferrer">PgVectorizer</a>—to make embedding PostgreSQL data as simple as possible.</p><p>Here’s the architecture we settled on:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2023/11/Create-Embedding-for-PostgreSQL-Data-Architecture_Diagram-1.png" class="kg-image" alt="Reference architecture for a simple and resilient system for embedding data in an existing PostgreSQL table. We use the example use case of a blogging application, hence the table names above." loading="lazy" width="2000" height="1282" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/11/Create-Embedding-for-PostgreSQL-Data-Architecture_Diagram-1.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/11/Create-Embedding-for-PostgreSQL-Data-Architecture_Diagram-1.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2023/11/Create-Embedding-for-PostgreSQL-Data-Architecture_Diagram-1.png 1600w, https://timescale.ghost.io/blog/content/images/2023/11/Create-Embedding-for-PostgreSQL-Data-Architecture_Diagram-1.png 2400w" sizes="(min-width: 720px) 720px"><figcaption><i><em class="italic" style="white-space: pre-wrap;">Reference architecture for a simple and resilient system for embedding data in an existing PostgreSQL table. We use the example use case of a blogging application, hence the table names above</em></i><span style="white-space: pre-wrap;">.</span></figcaption></figure><p>In this design, we first add a trigger to the blog table that monitors for changes and, upon seeing a modification, inserts a job into the blog_work_queue table that indicates that a row in the blog table is out-of-date with its embedding.</p><p>On a fixed schedule, an embeddings creator job will poll the blog_work_queue table, and if it finds work to do, will do the following in a loop:</p><ol><li>Read and lock a row in the blog_work_queue table&nbsp;</li><li>Read the corresponding row in the blog table</li><li>Create an embedding for the data in the blog row</li><li>Write the embedding to the blog_embedding table</li><li>Delete the locked row in the&nbsp; blog_work_queue table</li></ol><p>To see this system in action, see an example of usage to <a href="https://timescale.ghost.io/blog/a-complete-guide-to-creating-and-storing-embeddings-for-postgresql-data/" rel="noreferrer">create and maintain embeddings in a PostgreSQL table using OpenAI, LangChain, and pgai on Timescale in this blog post</a>.</p><p>Going back to the example of our blog application table, on a high level, <a href="https://docs.timescale.com/ai/latest/pgvectorizer/" rel="noreferrer">PgVectorizer</a> has to do two things:</p><ol><li>Track changes to the blog rows to know which rows have changed.</li><li>Provide a method to process the changes to create embeddings.</li></ol><p>Both of these have to be highly concurrent and performant. Let’s see how it works.</p><h3 id="track-change-to-the-blog-table-with-the-blogworkqueue-table">Track change to the blog table with the blog_work_queue table</h3><p>You can create a simple work queue table with the following:</p><pre><code class="language-SQL">CREATE TABLE blog_embedding_work_queue (
  id  INT 
);

CREATE INDEX ON blog_embedding_work_queue(id);
</code></pre>
<p>This is a very simple table, but there is one item of note: this table has no unique key. This was done to avoid locking issues when processing the queue, but it does mean that we may have duplicates. We discuss the trade-off later in Alternative 1 below.</p><p>Then you create a trigger to track any changes made to <code>blog</code>:</p><pre><code class="language-SQL">CREATE OR REPLACE FUNCTION blog_wq_for_embedding() RETURNS TRIGGER LANGUAGE PLPGSQL AS $$ 
BEGIN 
  IF (TG_OP = 'DELETE') THEN
    INSERT INTO blog_embedding_work_queue 
      VALUES (OLD.id);
  ELSE
    INSERT INTO blog_embedding_work_queue 
      VALUES (NEW.id);
  END IF;
  RETURN NULL;
END; 
$$;

CREATE TRIGGER track_changes_for_embedding 
AFTER INSERT OR UPDATE OR DELETE
ON blog 
FOR EACH ROW EXECUTE PROCEDURE blog_wq_for_embedding();

INSERT INTO blog_embedding_work_queue 
  SELECT id FROM blog WHERE published_time is NOT NULL;
</code></pre>
<p>The trigger inserts the ID of the blog that has changed into blog_work_queue. We install the trigger and then insert any existing blogs into the work_queue. This ordering is important to make sure that no IDs get dropped.</p><p>Now, let’s describe some alternative designs and why we rejected them.</p><h3 id="alternative-1-implement-a-primary-or-unique-key-for-the-blogworkqueue-table"><strong>Alternative 1: </strong>Implement a primary or unique key for the blog_work_queue table.</h3><p>Introducing this key would eliminate the problem of duplicate entries. However, it's not without its challenges, particularly because such a key would force us to use the <code>INSERT…ON CONFLICT DO NOTHING</code> clause to insert new IDs into the table, and that clause takes a lock on the ID in the B-tree.&nbsp;</p><p>Here's the dilemma: during the processing phase, it's necessary to delete the rows being worked on to prevent simultaneous processing. Yet, committing this deletion can only be done after the corresponding embedding has been placed into blog_embeddings. This ensures no IDs are lost if there's a disruption midway—say, if the embedding creation crashes post-deletion but before the embedding is written.</p><p>Now, if we create a unique or primary key, the transaction overseeing the deletion stays open. Consequently, this acts as a lock on those specific IDs, preventing their insertion back into the blog_work_queue for the entire duration of the embedding creation job. Given that it takes longer to create embeddings than your typical database transaction, this spells trouble. The lock would stall the trigger for the main 'blog' table, leading to a dip in the primary application's performance. Making things worse, if processing multiple rows in a batch, deadlocks become a potential problem as well.</p><p>However, the potential issues arising from occasional duplicate entries can be managed during the processing stage, as illustrated later. A sporadic duplicate here and there isn't a problem as it only marginally increases the amount of work the embedding job performs. This is certainly more palatable than grappling with the above-mentioned locking challenges.</p><h3 id="alternative-2-track-the-work-that-needs-to-be-done-by-adding-a-column-to-the-blog-table-to-track-whether-an-up-to-date-embedding-exists"><strong>Alternative 2:</strong> Track the work that needs to be done by adding a column to the <code>blog</code> table to track whether an up-to-date embedding exists.</h3><p>For example, we could add an <code>embedded</code> boolean column set to false on modification and flipped to true when the embedding is created. There are three reasons to reject this design:&nbsp;</p><ol><li>We don’t want to modify the <code>blog</code> table for the reasons we already mentioned above.</li><li>Efficiently getting a list of non-embedded blogs would require an additional index (or partial index) on the blog table. This would slow down other operations.</li><li>This increases churn on the table because every modification would now be written twice (once with embedding=false and once with embedding=true) due to the MVCC nature of PostgreSQL.</li></ol><p>A separate work_queue_table solves these issues.</p><h3 id="alternative-3-create-the-embeddings-directly-in-the-trigger">Alternative 3: Create the embeddings directly in the trigger.</h3><p>This approach has several issues:</p><ol><li>If the embedding service is down, either the trigger needs to fail (aborting your transaction), or you need to create a backup code path that … stores the IDs that couldn’t be embedded in a queue. The latter solution gets us back to our proposed design but with more complexity bolted on top.</li><li>This trigger will probably be much slower than the rest of the database operations because of the latency required to contact an external service. This will slow down the rest of your database operations on the table.</li><li>It forces the user to write the creation embedding code directly in the database. Given that the lingua franca of AI is Python and that embedding creation often requires many other libraries, this isn’t always easy or even possible (especially if running within a hosted PostgreSQL cloud environment). It’s much better to have a design where you have a choice to create embeddings inside or outside of the database.</li></ol><p>Now we have a list of blogs that need to be embedded, let’s process the list!</p><h3 id="create-the-embeddings">Create the embeddings</h3><p>There are many ways to create embeddings. We recommend using an external Python script. This script will scan the work queue and the related blog posts, invoke an external service to craft the embeddings, and then store these embeddings back into the database. Our reasoning for this strategy is as follows:</p><ul><li><strong>Choice of Python</strong>: We recommend<a href="https://timescale.ghost.io/blog/postgresql-as-a-vector-database-create-store-and-query-openai-embeddings-with-pgvector/"> Python</a> because it offers a rich, unmatched ecosystem for AI data tasks, highlighted by powerful LLM development and data libraries like <a href="https://blog.langchain.dev/timescale-vector-x-langchain-making-postgresql-a-better-vector-database-for-ai-applications/">LangChain</a> and <a href="https://timescale.ghost.io/blog/timescale-vector-x-llamaindex-making-postgresql-a-better-vector-database-for-ai-applications/">LlamaIndex</a>.</li><li><strong>Opting for an external script instead of PL/Python</strong>: We wanted users to have control over how they embed their data. Yet, at the same time, many Postgres cloud providers don’t allow the execution of arbitrary Python code inside the database because of security concerns. So, to allow users to have flexibility in both their embedding scripts as well as where they host their database, we went with a design that used external Python scripts.</li></ul><p>The jobs must be both performant and concurrency-safe. Concurrency guarantees that if jobs start running behind, the schedulers can start more jobs to help the system catch up and handle the load.</p><p>We’ll go through how to set up each of those methods later, but first, let’s see what the Python script would look like. Fundamentally, the script has three parts:</p><ol><li>Read the work queue and the blog post</li><li>Create an embedding for the blog post</li><li>Write the embedding to the blog_embedding table</li></ol><p>Steps 2 and 3 are performed by an&nbsp;<code>embed_and_write</code> callback that we define in the <a href="https://timescale.ghost.io/blog/a-complete-guide-to-creating-and-storing-embeddings-for-postgresql-data/" rel="noreferrer">PgVectorizer blog post</a>. So, let’s look more deeply at how we process the work queue.</p><h3 id="process-the-work-queue">Process the work queue</h3><p>We’ll first show you the code and then highlight the key elements at play:</p><pre><code class="language-SQL">def process_queue(embed_and_write_cb, batch_size:int=10):            
    with psycopg2.connect(TIMESCALE_SERVICE_URL) as conn:
        with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cursor:
            cursor.execute(f"""
                SELECT to_regclass('blog_embedding_work_queue')::oid; 
                """)
            table_oid = cursor.fetchone()[0]
            
            cursor.execute(f"""
                WITH selected_rows AS (
                    SELECT id
                    FROM blog_embedding_work_queue                         
                    LIMIT {int(batch_size)}
                    FOR UPDATE SKIP LOCKED
                ), 
                locked_items AS (
                    SELECT id, 
                           pg_try_advisory_xact_lock(
                                {int(table_oid)}, id) AS locked
                    FROM (
                        SELECT DISTINCT id 
                        FROM selected_rows 
                        ORDER BY id
                     ) as ids
                ),
                deleted_rows AS (
                    DELETE FROM blog_embedding_work_queue
                    WHERE id IN (
                        SELECT id 
                        FROM locked_items 
                        WHERE locked = true ORDER BY id
                   )
                )
                SELECT locked_items.id as locked_id, {self.table_name}.*
                FROM locked_items
                LEFT JOIN blog ON blog.id = locked_items.id
                WHERE locked = true
                ORDER BY locked_items.id
            """)
            res = cursor.fetchall()
            if len(res) &gt; 0:
                embed_and_write_cb(res)
            return len(res)

process_queue(embed_and_write)
</code></pre>
<p>The SQL code in the above snippet is subtle because it is designed to be both performant and concurrency-safe, so let’s go through it:</p><ul><li><strong>Getting items off the work queue</strong>: Initially, the system retrieves a specified number of entries from the work queue, determined by the batch queue size parameter. A FOR UPDATE lock is taken to ensure that concurrently executing scripts don’t try processing the same queue items. The SKIP LOCKED directive ensures that if any entry is currently being handled by another script, the system will skip it instead of waiting, avoiding unnecessary delays.<br></li><li><strong>Locking blog IDs</strong>: Due to the possibility of duplicate entries for the same blog_id within the work-queue table, simply locking said table is insufficient. Concurrent processing of the same ID by different jobs would be detrimental. Consider the following potential race-condition:</li></ul><ol><li>Job 1 initiates and accesses a blog, retrieving version 1.</li><li>An external update to the blog occurs.</li><li>Subsequently, Job 2 begins, obtaining version 2.</li><li>Both jobs commence the embedding generation process.</li><li>Job 2 concludes, storing the embedding corresponding to blog version 2.</li><li>Job 1, upon conclusion, erroneously overwrites the version 2 embedding with the outdated version 1.</li></ol><p>While one could counter this issue by introducing explicit version tracking, it introduces considerable complexity without performance benefit. The strategy we opted for not only mitigates this issue but also prevents redundant operations and wasted work by concurrently executing scripts. </p><p>A Postgres advisory lock, prefixed with the table identifier to avoid potential overlaps with other such locks, is employed. The <code>try</code> variant, analogous to the earlier application of SKIP LOCKED, ensures the system avoids waiting on locks. The inclusion of the ORDER BY blog_id clause helps prevent potential deadlocks. We’ll cover some alternatives below.</p><ul><li><strong>Cleaning up the work queue</strong>: The script then deletes all the work queue items for blogs we have successfully locked. If these queue items are visible via Multi-Version Concurrency Control (MVCC), their updates are manifested in the retrieved blog row. Note that we delete all items with the given blog ID, not only the items read when selecting the rows: this effectively handles duplicate entries for the same blog ID. It's crucial to note that this deletion only commits after invoking the embed_and_write() function and the subsequent storage of the updated embedding. This sequence ensures we don’t lose any updates even if the script fails during the embedding generation phase.</li><li><strong>Getting the blogs to process: </strong>In the last step, we fetch the blogs to process. Note the use of the left join: that allows us to retrieve the blog IDs for deleted items that won’t have a blog row. We need to track those items to delete their embeddings. In the <code>embed_and_write</code> callback, we use published_time being NULL as a sentinel for the blog being deleted (or unpublished, in which case we also want to delete the embedding).</li></ul><h3 id="alternative-4-avoid-using-advisory-locks-by-using-another-table"><strong>Alternative 4</strong>: Avoid using advisory locks by using another table.&nbsp;</h3><p>If the system already uses advisory locks and you are worried about collisions, it’s possible to use a table with a blog ID as the primary key and lock the rows. In fact, this can be the blog table itself if you are sure these locks won’t slow down any other system (remember, these locks have to be held throughout the embedding process, which can take a while). </p><p>Alternatively, you can have a blog_embedding_locks table just for this purpose. We didn’t suggest creating that table because we think it can get quite wasteful in terms of space, and using advisory locks avoids this overhead.&nbsp;</p><h2 id="conclusion-and-next-steps">Conclusion and Next Steps</h2><p><a href="https://timescale.ghost.io/blog/a-complete-guide-to-creating-and-storing-embeddings-for-postgresql-data/" rel="noreferrer">We introduced PgVectorizer and outlined a system adept at generating vector embeddings from data stored in PostgreSQL</a> and automatically keeping them up to date. This architecture ensures the embeddings remain synchronized with the perpetually evolving data, responding seamlessly to insertions, modifications, and deletions. </p><p>In this blog post, we gave you a behind-the-scenes look at how we created a system that boasts resilience, effectively handling potential downtimes of the embedding-generation service. Its design is adept at managing a high rate of data modifications and can seamlessly use concurrent embedding-generation processes to accommodate heightened loads.</p><p>Moreover, the paradigm of committing data to PostgreSQL and using the database to manage embedding generation in the background emerges as an easy mechanism to supervise embedding upkeep amidst data modifications. A myriad of demos and tutorials in the AI space focus singularly on the initial creation of data from documents, overlooking the intricate nuances associated with preserving data synchronization as it evolves.&nbsp;</p><p>However, in real production environments, data invariably changes, and grappling with the complexities of tracking and synchronizing these shifts is no trivial endeavor. But that’s what a database is designed to do! Why not just use it?</p><p>Here are some resources to continue your learning journey:</p><ul><li><a href="https://console.cloud.timescale.com/signup?utm_campaign=vectorlaunch&amp;utm_source=timescale-blog&amp;utm_medium=direct&amp;utm_content=pgvectorizer-how-we-built"><strong>Try pgai on Timescale free for 90 days</strong></a>: Store the embeddings generated from your PostgreSQL data in a fast and scalable vector database built on PostgreSQL. Learn more <a href="https://www.timescale.com/ai">about pgai on Timescale</a> and <a href="https://timescale.ghost.io/blog/how-we-made-postgresql-the-best-vector-database/">how it performs</a>.</li><li><a href="https://docs.timescale.com/ai/latest/pgvectorizer/" rel="noreferrer"><strong>Read the docs</strong></a>: Learn more about PgVectorizer and how to use it via the <a href="https://timescale.ghost.io/blog/a-python-library-for-using-postgresql-as-a-vector-database-in-ai-applications/">Python Vector library</a>.</li><li><a href="https://github.com/timescale/vector-cookbook/tree/main/pgvectorizer"><strong>Tutorial: Embedding blog data in PostgreSQL</strong></a>:<strong> </strong>Follow this step-by-step tutorial on how to create, embed, and store blog post data from a PostgreSQL table using the methods discussed in this blog post.</li><li><a href="https://timescale.ghost.io/blog/refining-vector-search-queries-with-time-filters-in-pgvector-a-tutorial/" rel="noreferrer"><strong>Tutorial: Refining Vector Search Queries With Time Filters in pgvector</strong></a><strong>: </strong>Learn how to do time-based filtering and semantic similarity search in a single SQL query.</li><li><a href="https://python.langchain.com/docs/integrations/vectorstores/timescalevector"><strong>LangChain and pgai on Timescale</strong></a><strong>: </strong>We used LangChain to illustrate document parsing and embedding creation in our examples above. Learn more about how to use it with pgai on Timescale for vector storage, similarity search, and hybrid search.</li></ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A Complete Guide to Creating and Storing Embeddings for PostgreSQL Data]]></title>
            <description><![CDATA[Explore the power of vector embeddings and learn how to create and store them for PostgreSQL data using Python, LangChain, and pgai on Timescale.]]></description>
            <link>https://www.tigerdata.com/blog/a-complete-guide-to-creating-and-storing-embeddings-for-postgresql-data</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/a-complete-guide-to-creating-and-storing-embeddings-for-postgresql-data</guid>
            <category><![CDATA[PostgreSQL]]></category>
            <category><![CDATA[AI]]></category>
            <category><![CDATA[Engineering]]></category>
            <dc:creator><![CDATA[Matvey Arye]]></dc:creator>
            <pubDate>Fri, 10 Nov 2023 13:33:28 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2023/11/Create-and-Store-Embeddings-PostgreSQL-data.png">
            </media:content>
            <content:encoded><![CDATA[<h3 id="why-use-embeddings-for-your-postgresql-data">Why use embeddings for your PostgreSQL data</h3><p><a href="https://timescale.ghost.io/blog/a-beginners-guide-to-vector-embeddings/" rel="noreferrer">Vector embeddings</a> provide a mathematical representation of data, encapsulating its semantic essence in a form that machines can readily process. While commonly associated with text, images, and audio, virtually any binary data can be converted into this format.&nbsp;</p><p>Generating embeddings from data already stored in your PostgreSQL database unlocks a multitude of applications:</p><p>Embeddings enable <a href="https://www.timescale.com/learn/vector-search-vs-semantic-search" rel="noreferrer"><strong>semantic search</strong></a>, which transcends the limitations of traditional keyword-driven methods. It doesn't just seek exact word matches; it grasps the deeper intent behind a user's query. The result? Even if search terms differ in phrasing, relevant results are surfaced. Taking advantage of <strong>hybrid search</strong>, which marries lexical and semantic search methodologies, offers users a search experience that's both rich and accurate. It's not just about finding direct matches anymore; it's about tapping into contextually and conceptually similar content to meet user needs.</p><p><strong>Recommendation systems</strong> benefit immensely from embeddings. Imagine a user who has shown interest in several articles on a singular topic. With embeddings, the recommendation engine can delve deep into the semantic essence of those articles, surfacing other database items that resonate with the same theme. Recommendations, thus, move beyond just the superficial layers like tags or categories and dive into the very heart of the content.</p><p>Generative AI, particularly <strong>retrieval-augmented generation (RAG)</strong>, can be powered using the data stored in a PostgreSQL database. This turns your data into more than just tabular information; it becomes context for Large Language Models (LLMs) like OpenAI’s<a href="https://openai.com/blog/new-models-and-developer-products-announced-at-devday"> GPT-4 Turbo</a>, Anthropic’s <a href="https://www.anthropic.com/index/claude-2">Claude 2</a>, and open-source modes like <a href="https://ai.meta.com/llama/">Llama 2</a>. When a user poses a query, relevant database content is fetched and used to supplement the query as additional context for the LLM. This helps reduce LLM hallucinations, as it ensures the model's output is more grounded in specific and relevant information, even if it wasn't part of the original training data.</p><p>Furthermore, embeddings offer a robust solution for <strong>clustering</strong> data in PostgreSQL. Transforming data into these vectorized forms enables nuanced comparisons between data points in a high-dimensional space. Through algorithms like <a href="https://en.wikipedia.org/wiki/K-means_clustering">K-means</a> or <a href="https://en.wikipedia.org/wiki/Hierarchical_clustering">hierarchical clustering</a>, data can be categorized into semantic clusters, offering insights that surface-level attributes might miss. This deepens our grasp of inherent data patterns, enriching both exploration and decision-making processes.</p><p>This guide delves into the process of creating and managing embeddings for data residing in PostgreSQL using PgVectorizer, a library we developed to make managing embeddings simple. PgVectorizer both creates embedding from your data and keeps your relational and embedding data in sync as your data changes. </p><p>We'll navigate through architectural considerations, set up the library, perform a sync between your relational and embedding data, and query your embeddings. To learn more about how we built this, in this article, <a href="https://timescale.ghost.io/blog/how-we-designed-a-resilient-vector-embedding-creation-system-for-postgresql-data/" rel="noreferrer">we go under the hood and explore how PgVectorizer works</a>: we’ll cover schema layout, how we designed the system for performance, concurrency, and resilience, and explore a few alternative design decisions.</p><p>Let’s get started!&nbsp;</p><h2 id="creating-embeddings-for-data-in-postgresql-and-keeping-them-up-to-date-with-your-tables">Creating Embeddings for Data in PostgreSQL (and Keeping Them Up-To-Date With Your Tables)</h2><p>As a running example, we’ll use a simple blog application storing data in PostgreSQL using a table defined as:</p><pre><code class="language-SQL">CREATE TABLE blog (
  id              SERIAL PRIMARY KEY NOT NULL,
  title           TEXT NOT NULL, 
  author          TEXT NOT NULL,
  contents        TEXT NOT NULL,
  category        TEXT NOT NULL,
  published_time  TIMESTAMPTZ NULL --NULL if not yet published
);

</code></pre>
<p>We want to create embeddings on the contents of the blog post so we can later use it for semantic search. Embeddings should only exist and be searchable for blogs that have been published (where the <code>published_time</code> is <code>NOT NULL</code>).&nbsp;</p><p>To make working with embeddings simple and resilient, any system that creates embeddings should have the following goals:</p><ul><li><strong>No modifications to the original table. </strong>This allows systems and applications that already use this table not to be impacted by changes to the embedding system. This is especially important for legacy systems.</li><li><strong>No modification to the applications that interact with the table. </strong>Having to modify the code that alters the table may not be possible for legacy systems. It’s also poor software design because it couples systems that don’t use embeddings with code that generates the embedding.</li><li><strong>Automatically update embeddings</strong> when rows in the source table change (in this case, the blog table). This lessens the maintenance burden and contributes to worry-free software. At the same time, this update need not be instantaneous or within the same commit. For most systems, “eventual consistency” is just fine.</li><li><strong>Ensure resilience against network and service failures: </strong>Most systems generate embeddings via a call to an external system, such as the OpenAI API. In scenarios where the external system is down or a network malfunction occurs, it's imperative that the remainder of your database system continues working.</li></ul><p>These guidelines act as a robust framework for the following architecture:&nbsp;</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2023/11/Create-Embedding-for-PostgreSQL-Data-Architecture_Diagram.png" class="kg-image" alt="Reference architecture for a simple and resilient system for embedding data in an existing PostgreSQL table. We use the example use case of a blogging application, hence the names above." loading="lazy" width="2000" height="1282" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/11/Create-Embedding-for-PostgreSQL-Data-Architecture_Diagram.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/11/Create-Embedding-for-PostgreSQL-Data-Architecture_Diagram.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2023/11/Create-Embedding-for-PostgreSQL-Data-Architecture_Diagram.png 1600w, https://timescale.ghost.io/blog/content/images/2023/11/Create-Embedding-for-PostgreSQL-Data-Architecture_Diagram.png 2400w" sizes="(min-width: 720px) 720px"><figcaption><i><em class="italic" style="white-space: pre-wrap;">Reference architecture for a simple and resilient system for embedding data in an existing PostgreSQL table. We use the example use case of a blogging application, hence the names above.</em></i></figcaption></figure><p>In this design, we first add a trigger to the blog table that monitors for changes and, upon seeing a modification, inserts a job into the blog_work_queue table that indicates that a row in the blog table is out-of-date with its embedding.</p><p>On a fixed schedule, an embeddings creator job will poll the blog_work_queue table, and if it finds work to do, will do the following in a loop:</p><ol><li>Read and lock a row in the blog_work_queue table&nbsp;</li><li>Read the corresponding row in the blog table</li><li>Create an embedding for the data in the blog row</li><li>Write the embedding to the blog_embedding table</li><li>Delete the locked row in the&nbsp; blog_work_queue table</li></ol><p>Next, we’ll discuss how to implement this simply by using the <a href="https://github.com/timescale/python-vector">Python Vector library</a>, a library for <a href="https://timescale.ghost.io/blog/a-python-library-for-using-postgresql-as-a-vector-database-in-ai-applications/">working with vector data using PostgreSQL</a>.&nbsp;</p><h3 id="easily-manage-embedding-postgresql-data-using-langchain-and-the-python-vector-library">Easily manage embedding PostgreSQL data using LangChain and the Python Vector library</h3><p>We’ve added functionality to our library to make embedding PostgreSQL data as simple as possible. We call this functionality <a href="https://docs.timescale.com/ai/latest/pgvectorizer/" rel="noreferrer">PgVectorizer</a>.</p><h3 id="define-your-embedding-creation-function">Define your embedding creation function</h3><p>There are myriad ways to embed your data. We don’t want to force you to use just one pre-defined method, so we ask users to define how to embed their data. Thus, we ask you to provide us with a Python function callback to create and store embeddings from database data. We call this the <code>embed_and_write</code> function, and it’s best to illustrate how to write it with an example.&nbsp;</p><p>Using the blog example above, it could look like the following when embedding using <a href="https://python.langchain.com/docs/get_started/introduction">LangChain</a>, a popular framework to work with LLM applications:</p><pre><code class="language-Python">from langchain.docstore.document import Document
from langchain.text_splitter import CharacterTextSplitter
from timescale_vector import client, pgvectorizer
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores.timescalevector import TimescaleVector
from datetime import timedelta


import os
from dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv(), override=True)

TIMESCALE_SERVICE_URL = os.environ["TIMESCALE_SERVICE_URL"]

def get_document(blog):
    text_splitter = CharacterTextSplitter(
        chunk_size=1000,
        chunk_overlap=200,
    )
    docs = []
    for chunk in text_splitter.split_text(blog['contents']):
        content = f"Author {blog['author']}, title: {blog['title']}, contents:{chunk}"
        metadata = {
            "id": str(client.uuid_from_time(blog['published_time'])),
            "blog_id": blog['id'], 
            "author": blog['author'], 
            "category": blog['category'],
            "published_time": blog['published_time'].isoformat(),
        }
        docs.append(Document(page_content=content, metadata=metadata))
    return docs

def embed_and_write(blog_instances, vectorizer):
    # Note: the vectorizer argument isn’t used in this example but it
    # provides a way to get the name of the table being embedded,
    # along with other metadata.


    embedding = OpenAIEmbeddings()
    vector_store = TimescaleVector(
        collection_name="blog_embedding",
        service_url=TIMESCALE_SERVICE_URL,
        embedding=embedding,
        time_partition_interval=timedelta(days=30),
    )

    # delete old embeddings for all ids in the work queue. locked_id is a special column that is set to the primary key of the table being
    # embedded. For items that are deleted, it is the only key that is set.
    metadata_for_delete = [{"blog_id": blog['locked_id']} for blog in blog_instances]
    vector_store.delete_by_metadata(metadata_for_delete)

    documents = []
    for blog in blog_instances:
        # skip blogs that are not published yet, or are deleted (in which case it will be NULL)
        if blog['published_time'] != None:
            documents.extend(get_document(blog))

    if len(documents) == 0:
        return
    
    texts = [d.page_content for d in documents]
    metadatas = [d.metadata for d in documents]
    ids = [d.metadata["id"] for d in documents]
    vector_store.add_texts(texts, metadatas, ids)
</code></pre>
<p>The <code>embed_and_write()</code> function gets a list of blogs that have either been created, updated, or deleted. Its job is to update the vector store with the new blogs. We do this in two steps:</p><ol><li>Delete all existing vectors already in the vector store for items with the same primary key. The primary key is passed in via a special “locked_id” attribute. This is necessary if rows are deleted or updated.</li><li>Create embeddings for all items that were updated or deleted. Deleted items will have all attributes other than “locked_id” set to <code>None</code> so that any attribute can be used as a sentinel. In the example above, we use “published_time” because we also want to skip embedding documents where “published_time” is NULL in the database.</li></ol><p>The <code>get_document()</code> function is very use-case-specific, and you’ll have to adjust this code to suit your needs. Because of the context length limitations in LLM completion queries and token length limitations in embedding generation models, you will likely need some way to split long text up into smaller chunks. Here, we use a simple <a href="https://python.langchain.com/docs/modules/data_connection/document_transformers/text_splitters/character_text_splitter"><code>CharacterTextSplitter</code> in LangChain</a>, but much more complex approaches are possible. In the code above, we use a simple but effective trick: add some semantic context to each chunk by prepending the author and category. The only real requirement for the metadata generation portion is including the blog_id, which we can later use to delete old embeddings for a given blog.</p><p>In the code snippet above, we use <a href="https://python.langchain.com/docs/integrations/vectorstores/timescalevector#2-similarity-search-with-time-based-filtering">time-based partitioning</a> based on published_time. This type of partitioning drastically <strong>speeds up hybrid search on time and embedding similarity</strong>. We partition by time by setting the UUID based on the timestamp using <a href="https://timescale.github.io/python-vector/vector.html#uuid_from_time"><code>client.uuid_from_time()</code> function</a> and by specifying <code>time_partition_interval=timedelta(days=30)</code> when creating the <a href="https://python.langchain.com/docs/integrations/vectorstores/timescalevector" rel="noreferrer">pgai on Timescale vector store</a>. This type of partitioning really speeds up search when filtering by both published_time and vector similarity. <a href="https://youtu.be/EYMZVfKcRzM?si=iW-ySVYaGCDYrZkS">See this explainer video</a> for more on how time-based partitioning works in pgai on Timescale (previously known as Timescale Vector).</p><p>Once this is written, all you have to do is call the following code on a schedule:</p><pre><code class="language-Python">vectorizer = pgvectorizer.Vectorize(service_url, 'blog')
while vectorizer.process(embed_and_write) &gt; 0:
    pass
</code></pre>
<p>This is the embedding creator job, which will sync your PostgreSQL data with a vector store. You can run this Python script on a schedule from practically anywhere:</p><ul><li>A scheduled <a href="https://timescale.ghost.io/blog/aws-lambda-for-beginners-overcoming-the-most-common-challenges/">AWS Lambda function</a></li><li>A scheduled <a href="https://developers.cloudflare.com/workers/">Cloudflare worker</a></li><li>A<a href="https://modal.com/use-cases/job-queues"> Modal function</a></li><li>A <a href="https://robocorp.com/docs/python">Robocorp automation task</a></li><li>A cron job on an EC2 instance or even on your local machine</li></ul><p>It will automatically track which rows within the blog table have changed and call the embed_and_write function on batches of changed rows. It is performant, resilient to failures, and can be run in parallel when you have a backlog of things that need to be embedded. <a href="https://timescale.ghost.io/blog/how-we-designed-a-resilient-vector-embedding-creation-system-for-postgresql-data/" rel="noreferrer">Designing such a system to perform well is harder than it sounds</a>. But we’ve done it for you in <a href="https://docs.timescale.com/ai/latest/pgvectorizer/" rel="noreferrer">PgVectorizer</a> as part of the <a href="https://docs.timescale.com/ai/latest/python-interface-for-pgvector-and-timescale-vector/" rel="noreferrer">Python Vector library</a>!&nbsp;</p><h3 id="searching-through-your-embeddings">Searching through your embeddings</h3><p>Use of the embeddings depends on how the embeddings were generated as well as the use case. We will illustrate some simple search applications to work with the <a href="https://python.langchain.com/docs/integrations/vectorstores/timescalevector#1-similarity-search-with-euclidean-distance-default">LangChain example</a> we gave above and also provide references for more advanced applications, such as hybrid search on metadata and time.</p><pre><code class="language-Python">TABLE_NAME = "blog_embedding"
embedding = OpenAIEmbeddings()
vector_store = TimescaleVector(
        collection_name=TABLE_NAME,
        service_url=TIMESCALE_SERVICE_URL,
        embedding=embedding,
        time_partition_interval=timedelta(days=30)
)
# find closest item
res = vector_store.similarity_search_with_score("Tell me about Travel to Istanbul", 1);

#hybrid search with time
start_dt = datetime(2021, 1, 1, 0, 0, 0) 
end_dt = datetime(2024, 1, 1, 0, 0, 0)
res = vector_store.similarity_search_with_score("Tell me about Travel to Istanbul", 1, start_date=start_dt, end_date=end_dt);
</code></pre>
<p>There are, of course, a lot more options for search, including filters and predicates on metadata, self-query retriever options, integrations with chat and RAG, and more! We recommend reading this <a href="https://python.langchain.com/docs/integrations/vectorstores/timescalevector">LangChain tutorial</a> for more info about the above-mentioned methods!&nbsp;</p><p><strong>Note: </strong>While the above example uses LangChain, you can also swap in frameworks like <a href="https://gpt-index.readthedocs.io/en/stable/index.html">LlamaIndex</a>, a popular LLM data framework that <a href="https://gpt-index.readthedocs.io/en/stable/examples/vector_stores/Timescalevector.html">integrates well with pgai on Timescale</a>, or do DIY document parsing and embedding in Python using OpenAI’s <a href="https://platform.openai.com/docs/guides/embeddings/what-are-embeddings">text-embedding-ada-002</a> model, or an open-source embedding model like <a href="https://huggingface.co/sentence-transformers">sentence-transformers</a>, while using the <a href="https://timescale.ghost.io/blog/a-python-library-for-using-postgresql-as-a-vector-database-in-ai-applications/">Python Vector client</a> for vector search.&nbsp;</p><h2 id="conclusion-and-next-steps">Conclusion and Next Steps</h2><p>In this blog post, we have outlined a system adept at generating vector embeddings from data stored in PostgreSQL and automatically keeping them up to date. This architecture ensures the embeddings remain synchronized with the perpetually evolving data, responding seamlessly to insertions, modifications, and deletions.&nbsp;</p><p>Using PostgreSQL to handle both data storage and background embedding generation offers an interesting new paradigm for maintaining embeddings as data changes. Many AI demonstrations and tutorials tend to concentrate only on initial data creation from documents, often missing the complexities of keeping data and embeddings synchronized as it evolves.</p><p>If your goal is to embed and keep up-to-date data in a PostgreSQL table, then you are done! Just <a href="https://docs.timescale.com/ai/latest/pgvectorizer/" rel="noreferrer">use the PgVectorizer class</a> in the <a href="https://github.com/timescale/python-vector">Python Vector library</a> and the code above to start embedding your PostgreSQL data and leveraging semantic and hybrid search in your applications.&nbsp;</p><p>If you are curious about how the PgVectorizer library works “under the hood” and how we designed the system for high performance, <a href="https://timescale.ghost.io/blog/how-we-designed-a-resilient-vector-embedding-creation-system-for-postgresql-data/" rel="noreferrer">see our companion blog post about how we designed a resilient embeddings system for PostgreSQL data</a>, which discusses the system design decisions and trade-offs we made while building PgVectorizer above.&nbsp;</p><p>If you’d like to go straight to applying what you learned to your own data in PostgreSQL, here are some resources to continue your learning journey:<br></p><ul><li><a href="https://console.cloud.timescale.com/signup?utm_campaign=vectorlaunch&amp;utm_source=timescale-blog&amp;utm_medium=direct&amp;utm_content=pgvectorizer-guide"><strong>Try pgai on Timescale free for 90 days</strong></a>: Store the embeddings generated from your PostgreSQL data in a fast and scalable vector database built on PostgreSQL. Learn more <a href="https://www.timescale.com/ai">about pgai on Timescale</a>, and <a href="https://timescale.ghost.io/blog/how-we-made-postgresql-the-best-vector-database/">how it performs</a>.</li><li><a href="https://docs.timescale.com/ai/latest/pgvectorizer/" rel="noreferrer"><strong>Read the docs</strong></a>: Learn more about PgVectorizer and how to use it via the <a href="https://timescale.ghost.io/blog/a-python-library-for-using-postgresql-as-a-vector-database-in-ai-applications/">Python Vector library</a> to embed data stored in PostgreSQL tables.</li><li><a href="https://github.com/timescale/vector-cookbook/tree/main/pgvectorizer"><strong>Tutorial–Embedding blog data in PostgreSQL</strong></a>:<strong> </strong>Follow this step-by-step tutorial on creating, embedding, and storing blog post data from a PostgreSQL table using the methods discussed in this blog post.</li><li><a href="https://python.langchain.com/docs/integrations/vectorstores/timescalevector"><strong>LangChain and pgai on Timescale</strong></a><strong> (previously known as Timescale Vector): </strong>We used LangChain to illustrate document parsing and embedding creation in our examples above. Learn more about how to use it with pgai on Timescale for vector storage, similarity search, and hybrid search.</li></ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Nearest Neighbor Indexes: What Are IVFFlat Indexes in Pgvector and How Do They Work]]></title>
            <description><![CDATA[A primer on the pgvector’s Inverted File Flat (ivfflat) algorithm for approximate nearest neighbor search. ]]></description>
            <link>https://www.tigerdata.com/blog/nearest-neighbor-indexes-what-are-ivfflat-indexes-in-pgvector-and-how-do-they-work</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/nearest-neighbor-indexes-what-are-ivfflat-indexes-in-pgvector-and-how-do-they-work</guid>
            <category><![CDATA[PostgreSQL]]></category>
            <category><![CDATA[pgvector]]></category>
            <category><![CDATA[AI]]></category>
            <dc:creator><![CDATA[Matvey Arye]]></dc:creator>
            <pubDate>Fri, 30 Jun 2023 13:03:10 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2023/06/nearest-neighbor_hero_4.png">
            </media:content>
            <content:encoded><![CDATA[<p>The rising popularity of ChatGPT, OpenAI, and applications of Large Language Models (LLMs) has brought the concept of approximate nearest neighbor search (ANN) to the forefront and sparked a renewed interest in vector databases due to the use of embeddings. <a href="https://platform.openai.com/docs/guides/embeddings">Embeddings</a> are mathematical representations of phrases that capture the semantic meaning as a vector of numerical values. </p><p>What makes this representation fascinating—and useful—is that phrases with similar meanings will have similar vector representations, meaning the distance between their respective vectors will be small. We recently discussed one application of these embeddings, <a href="https://timescale.ghost.io/blog/postgresql-as-a-vector-database-create-store-and-query-openai-embeddings-with-pgvector/">retrieval-augmented generation</a>—augmenting base LLMs with knowledge that it wasn’t trained on—but there are numerous other applications as well.</p><h3 id="semantic-similarity-search">Semantic similarity search</h3><p>One common application of embeddings is precisely <a href="https://en.wikipedia.org/wiki/Semantic_search">semantic similarity search</a>. The basic concept behind this approach is that if I have a knowledge library consisting of various phrases and I receive a question from a user, I can locate the most relevant information in my library by finding the data that is most similar to the user's query. <br></p><p>This is in contrast to lexical or full-text search, which only returns exact matches for the query. The remarkable aspect of this technique is that, since the embeddings represent the semantics of the phrase rather than its specific wording, I can find pertinent information even if it is expressed using completely different words!<br></p><h3 id="the-challenge-of-speed-at-scale">The challenge of speed at scale</h3><p>Semantic similarity search involves calculating an embedding for the user's question and then searching through my library to find the K most relevant items related to that question—these are the K items whose embeddings are closest to that of the question. However, when dealing with a large library, it becomes crucial to perform this search efficiently and swiftly. In the realm of vector databases, this problem is referred to as "Finding the k nearest neighbors" (<a href="https://en.wikipedia.org/wiki/K-nearest_neighbors_algorithm">KNN</a>).<br></p><p>This post discusses a method to enhance the speed of this search when utilizing PostgreSQL and <a href="https://www.timescale.com/ai">pgvector</a> for storing <a href="https://www.tigerdata.com/blog/a-beginners-guide-to-vector-embeddings" rel="noreferrer">vector embeddings</a>: the <a href="https://github.com/pgvector/pgvector#indexing">Inverted File Flat (IVFFlat)</a> algorithm for approximate nearest neighbor search. We’ll cover why IVFFlat is useful, how it works, and best practices for using it in pgvector for fast similarity search over embeddings vectors. </p><p>Let’s go!</p><p><strong>P.S. </strong>If you’re looking for the fastest vector search index on PostgreSQL, <a href="https://timescale.ghost.io/blog/how-we-made-postgresql-as-fast-as-pinecone-for-vector-data/" rel="noreferrer"><u>check out pgvectorscale</u></a>.</p><h2 id="what-are-ivfflat-indexes">What Are IVFFlat Indexes?</h2><p>IVFFlat indexes, short for Inverted File with Flat Compression, are a type of vector index used in PostgreSQL's <a href="https://www.tigerdata.com/learn/postgresql-extensions-pgvector" rel="noreferrer">pgvector extension</a> to speed up similarity searches to find vectors that are close to a given query. This index type uses approximate nearest neighbor search (ANNS) to provide fast searches.&nbsp;&nbsp;</p><p>These indexes work by dividing the vectors into multiple lists, known as clusters. Each cluster represents a region of similar vectors, and an inverted index is built to map each region to its corresponding vectors. When a query comes in, the nearest clusters to the query are identified and only the vectors in those clusters are searched. Thus, this approach significantly reduces the scope of similarity searches by excluding all the vectors that are not in the clusters that are close to the query.</p><p></p><h2 id="why-use-the-ivfflat-index-in-pgvector">Why Use the IVFFlat Index in Pgvector</h2><p>Searching for the k-nearest neighbors is not a novel problem for PostgreSQL. <a href="https://docs.timescale.com/use-timescale/latest/extensions/postgis/">PostGIS</a>, a <a href="https://www.tigerdata.com/blog/top-8-postgresql-extensions" rel="noreferrer">PostgreSQL extension</a> for handling location data, stores its data points as two-dimensional vectors (longitude and latitude). Locating nearby locations is a crucial query in that domain. <br></p><p>PostGIS tackles this challenge by employing an index known as an R-Tree, which yields precise results for k-nearest neighbor queries. Similar techniques, such as KD-Trees and Ball Trees, are also employed for this type of search in other databases.<br></p><h3 id="the-curse-of-dimensionality">"The curse of dimensionality"</h3><p>However, there's a catch. These approaches cease to be effective when dealing with data larger than approximately 10 dimensions due to the "curse of dimensionality." Cue the ominous music! Essentially, as you add more dimensions, the available space increases exponentially, resulting in exponentially sparser data. This reduced density renders existing indexing techniques, like the aforementioned R-Tree, KD-Trees, and Ball Trees, which rely on partitioning the space, ineffective. (To learn more, I suggest these two videos: <a href="https://www.youtube.com/watch?v=BbYV8UfMJSA">1</a>, <a href="https://www.youtube.com/watch?v=E1_WCdUAtyE">2</a>). <br></p><p>Given that embeddings often consist of more than a thousand dimensions—OpenAI’s are 1,536—new techniques had to be developed. There are no known exact algorithms for efficiently searching in such high-dimensional spaces. Nevertheless, there are excellent <em>approximate</em> algorithms that fall into the category of approximate nearest neighbor algorithms. Numerous such algorithms exist, but in this article, we will delve into the Inverted File Flat or IVFFlat algorithm, which is provided by pgvector.</p><h2 id="how-the-ivfflat-index-works-in-pgvector">How the IVFFlat Index Works in pgvector</h2><p></p><h3 id="how-ivfflat-divides-the-space">How IVFFlat divides the space</h3><p>To gain an intuitive understanding of how IVFFlat works, let's consider a set of vectors represented in a two-dimensional space as the following points:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2023/06/nearest-neighbor-pgvector-diagram---1.png" class="kg-image" alt="A set of vectors represented as points in two dimensions" loading="lazy" width="1640" height="1040" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/06/nearest-neighbor-pgvector-diagram---1.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/06/nearest-neighbor-pgvector-diagram---1.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2023/06/nearest-neighbor-pgvector-diagram---1.png 1600w, https://timescale.ghost.io/blog/content/images/2023/06/nearest-neighbor-pgvector-diagram---1.png 1640w" sizes="(min-width: 720px) 720px"><figcaption><i><em class="italic" style="white-space: pre-wrap;">A set of vectors represented as points in two dimensions</em></i></figcaption></figure><p>In the IVFFlat algorithm, the first step involves applying k-means clustering to the vectors to find cluster centroids. In the case of the given vectors, let's assume we perform k-means clustering and identify four clusters with the following centroids.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2023/06/nearest-neighbor-pgvector-diagram---2-1.png" class="kg-image" alt="After k-means clustering, we identify four clusters indicated by the colored triangles." loading="lazy" width="1640" height="1040" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/06/nearest-neighbor-pgvector-diagram---2-1.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/06/nearest-neighbor-pgvector-diagram---2-1.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2023/06/nearest-neighbor-pgvector-diagram---2-1.png 1600w, https://timescale.ghost.io/blog/content/images/2023/06/nearest-neighbor-pgvector-diagram---2-1.png 1640w" sizes="(min-width: 720px) 720px"><figcaption><i><em class="italic" style="white-space: pre-wrap;">After k-means clustering, we identify four clusters indicated by the colored triangles</em></i></figcaption></figure><p>After computing the centroids, the next step is to assign each vector to its nearest centroid. This is accomplished by calculating the distance between the vector and each centroid and selecting the centroid with the smallest distance as the closest one. This process conceptually maps each point in space to the closest centroid based on proximity.<br></p><p>By establishing this mapping, the space becomes divided into distinct regions surrounding each centroid (technically, this kind of division is called a <a href="https://en.wikipedia.org/wiki/Voronoi_diagram">Voronoi Diagram</a>). Each region represents a cluster of vectors that exhibit similar characteristics or are close in semantic meaning. <br></p><p>This division enables efficient organization and retrieval of approximate nearest neighbors during subsequent search operations, as vectors within the same region are likely to be more similar to each other than those in different regions.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2023/06/nearest-neighbor-pgvector-diagram---3-1.png" class="kg-image" alt=" The process of assigning each vector to its closest centroid conceptually divides the space into distinct regions that surround each centroid" loading="lazy" width="1640" height="1040" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/06/nearest-neighbor-pgvector-diagram---3-1.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/06/nearest-neighbor-pgvector-diagram---3-1.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2023/06/nearest-neighbor-pgvector-diagram---3-1.png 1600w, https://timescale.ghost.io/blog/content/images/2023/06/nearest-neighbor-pgvector-diagram---3-1.png 1640w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">The process of assigning each vector to its closest centroid conceptually divides the space into distinct regions that surround each centroid</span></figcaption></figure><h3 id="building-the-ivfflat-index-in-pgvector"><br>Building the IVFFlat index in pgvector</h3><p>IVFFlat proceeds to create an <a href="https://en.wikipedia.org/wiki/Inverted_index">inverted index</a> that maps each centroid to the set of vectors within the corresponding region. In pseudocode, the index can be represented as follows:</p><pre><code>inverted_index = {
  centroid_1: [vector_1, vector_2, ...],
  centroid_2: [vector_3, vector_4, ...],
  centroid_3: [vector_5, vector_6, ...],
  ...
}
</code></pre>
<p>Here, each centroid serves as a key in the inverted index, and the corresponding value is a list of vectors that belong to the region associated with that centroid. This index structure allows for efficient retrieval of vectors in a region when performing similarity searches.</p><h3 id="searching-the-ivfflat-index-in-pgvector">Searching the IVFFlat index in pgvector</h3><p>Let's imagine we have a query for the nearest neighbors to a vector represented by a question mark, as shown below:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2023/06/nearest-neighbor-pgvector-diagram---4-1.png" class="kg-image" alt="We want to find nearest neighbors to the vector represented by the question mark" loading="lazy" width="1640" height="1040" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/06/nearest-neighbor-pgvector-diagram---4-1.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/06/nearest-neighbor-pgvector-diagram---4-1.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2023/06/nearest-neighbor-pgvector-diagram---4-1.png 1600w, https://timescale.ghost.io/blog/content/images/2023/06/nearest-neighbor-pgvector-diagram---4-1.png 1640w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">We want to find nearest neighbors to the vector represented by the question mark</span></figcaption></figure><p>To find the approximate nearest neighbors using IVFFlat, the algorithm operates under the assumption that the nearest vectors will be located in the same region as the query vector. Based on this assumption, IVFFlat employs the following steps:</p><ol><li>Calculate the distance between the query vector (red question mark) and each centroid in the index.</li><li>Select the centroid with the smallest distance as the closest centroid to the query (the blue centroid in this example).</li><li>Retrieve the vectors associated with the region corresponding to the closest centroid from the inverted index.</li><li>Compute the distances between the query vector and each of the vectors in the retrieved set.</li><li>Select the K vectors with the smallest distances as the approximate nearest neighbors to the query.<br></li></ol><p>The use of the index in IVFFlat accelerates the search process by restricting the search to the region associated with the closest centroid. This results in a significant reduction in the number of vectors that need to be examined during the search. Specifically, if we have C clusters (centroids), on average, we can reduce the number of vectors to search by a factor of 1/C.</p><h3 id="searching-at-the-edge">Searching at the edge</h3><p>The assumption that the nearest vectors will be found in the same region as the query vector can introduce recall errors in IVFFlat. Consider the following query:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2023/06/nearest-neighbor-pgvector-diagram---5-1.png" class="kg-image" alt=" ivfflat can sometimes make errors when searching for nearest neighbors to a point at the edge of two regions of the vector space" loading="lazy" width="1640" height="1040" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/06/nearest-neighbor-pgvector-diagram---5-1.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/06/nearest-neighbor-pgvector-diagram---5-1.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2023/06/nearest-neighbor-pgvector-diagram---5-1.png 1600w, https://timescale.ghost.io/blog/content/images/2023/06/nearest-neighbor-pgvector-diagram---5-1.png 1640w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">IVFFlat</span><i><em class="italic" style="white-space: pre-wrap;"> can sometimes make errors when searching for nearest neighbors to a point at the edge of two regions of the vector space</em></i></figcaption></figure><p>From visual inspection, it becomes apparent that one of the light-blue vectors is closer to the query vector than any of the dark-blue vectors, despite the query vector falling within the dark-blue region. This illustrates a potential error in assuming that the nearest vectors will always be found within the same region as the query vector.<br></p><p>To mitigate this type of error, one approach is to search not only the region of the closest centroid but also the regions of the next closest R centroids. This approach expands the search scope and improves the chances of finding the true nearest neighbors. <br></p><p>In pgvector, this functionality is implemented through the `probes` parameter, which specifies the number of centroids to consider during the search, as described below.</p><h2 id="parameters-for-pgvector%E2%80%99s-ivfflat-implementation"><br>Parameters for Pgvector’s IVFFlat Implementation</h2><p>In the implementation of IVFFlat in pgvector, two key parameters are exposed: lists and probes.</p><h3 id="lists-parameter-in-pgvector">Lists parameter in pgvector</h3><p>The <code>lists</code> parameter determines the number of clusters created during index building (It’s called lists because each centroid has a list of vectors in its region). Increasing this parameter reduces the number of vectors in each list and results in smaller regions.<br></p><p>It offers the following trade-offs to consider:</p><ul><li>Higher <code>lists</code> value speeds up queries by reducing the search space during query time.</li><li>However, it also decreases the region size, which can lead to more recall errors by excluding some points.</li><li>Additionally, more distance comparisons are required to find the closest centroid during step one of the query process.<br></li></ul><p>Here are some recommendations for setting the <code>lists</code> parameter:</p><ul><li>For datasets with less than one million rows, use <code>lists =  rows / 1000</code>.</li><li>For datasets with more than one million rows, use <code>lists = sqrt(rows)</code>.</li><li>It is generally advisable to have at least 10 clusters.</li></ul><h3 id="probes-parameter-in-pgvector">Probes parameter in pgvector</h3><p>The probes parameter is a query-time parameter that determines the number of regions to consider during a query. By default, only the region corresponding to the closest centroid is searched. By increasing the probes parameter, more regions can be searched to improve recall at the cost of query speed. <br></p><p>The recommended value for the probes parameter is <code>probes = sqrt(lists)</code>.</p><h2 id="using-ivfflat-in-pgvector">Using IVFFlat in Pgvector</h2><p></p><h3 id="creating-an-index">Creating an index<br></h3><p>When creating an index, it is advisable to have existing data in the table, as it will be utilized by k-means to derive the centroids of the clusters.<br></p><p>The index in pgvector offers three different methods to calculate the distance between vectors: L2, inner product, and cosine. It is essential to select the same method for both the index creation and query operations. The following table illustrates the query operators and their corresponding index methods:</p>
<!--kg-card-begin: html-->
<table style="border:none;border-collapse:collapse;"><colgroup><col width="208"><col width="209"><col width="207"></colgroup><tbody><tr style="height:0pt"><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Distance type</span></p></td><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Query operator</span></p></td><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Index method</span></p></td></tr><tr style="height:0pt"><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">L2 / Euclidean</span></p></td><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">&lt;-&gt;</span></p></td><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">vector_l2_ops</span></p></td></tr><tr style="height:0pt"><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Negative Inner product</span></p></td><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">&lt;#&gt;</span></p></td><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">vector_ip_ops</span></p></td></tr><tr style="height:0pt"><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Cosine</span></p></td><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">&lt;=&gt;</span></p></td><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">vector_cosine_ops</span></p></td></tr></tbody></table>
<!--kg-card-end: html-->
<p><strong>Note</strong>: OpenAI <a href="https://platform.openai.com/docs/guides/embeddings/limitations-risks">recommends</a> cosine distance for its embeddings.</p><p>To create an index in pgvector using IVFFlat, you can use a statement using the following form:</p><pre><code class="language-SQL">CREATE INDEX ON &lt;table name&gt; USING ivfflat (&lt;column name&gt; &lt;index method&gt;) WITH (lists = &lt;lists parameter&gt;);
</code></pre>
<p>Replace <code>&lt;table name&gt;</code> with the name of your table and <code>&lt;column name&gt;</code> with the name of the column that contains the vector type.</p><p>For example, if our table is named <code>embeddings</code> and our embedding vectors are in a column named <code>embedding</code>, we can create an IVFFlat index as follows:</p><pre><code class="language-SQL">CREATE INDEX ON embeddings USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);

</code></pre>
<p>Here’s a simple Python function that you can use to create an IVFFlat index with the correct parameters for <code>lists</code> and <code>probes</code> as discussed above:</p><pre><code class="language-Python">def create_ivfflat_index(conn, table_name, column_name, query_operator="&lt;=&gt;"): 
    index_method = "invalid"
    if query_operator == "&lt;-&gt;":
        index_method = "vector_l2_ops"
    elif query_operator == "&lt;#&gt;":
        index_method = "vector_ip_ops"
    elif query_operator == "&lt;=&gt;":
        index_method = "vector_cosine_ops"
    else:
        raise ValueError(f"unrecognized operator {query_operator}")

    with conn.cursor() as cur:
        cur.execute(f"SELECT COUNT(*) as cnt FROM {table_name};")
        num_records = cur.fetchone()[0]

        num_lists = num_records / 1000
        if num_lists &lt; 10:
            num_lists = 10
        if num_records &gt; 1000000:
            num_lists = math.sqrt(num_records)

        cur.execute(f'CREATE INDEX ON {table_name} USING ivfflat ({column_name} {index_method}) WITH (lists = {num_lists});')
        conn.commit()
</code></pre>
<h3 id="querying">Querying</h3><p>An index can be used whenever there is an ORDER BY of the form <code>column &lt;query operator&gt; &lt;some pseudo-constant vector&gt;</code> along with a LIMIT k;<br></p><p><strong>Some examples</strong><br><br>Get the closest two vectors to a constant vector:</p><pre><code class="language-SQL">SELECT * FROM my_table ORDER BY embedding_column &lt;=&gt; '[1,2]' LIMIT 2;
</code></pre>
<p>This is a common usage pattern in retrieval augmented generation using LLMs, where we find the embedding vectors that are closest in semantic meaning to the user’s query. In that case, the constant vector would be the embedding vector representing the user’s query. </p><p>You can see an example of this in our guide to <a href="https://timescale.ghost.io/blog/postgresql-as-a-vector-database-create-store-and-query-openai-embeddings-with-pgvector/">creating, storing, and querying OpenAI embeddings with pgvector</a>, where we use this Python function to find the three most similar documents to a given user query from our database:</p><pre><code class="language-Python"># Helper function: Get top 3 most similar documents from the database
def get_top3_similar_docs(query_embedding, conn):
    embedding_array = np.array(query_embedding)
    # Register pgvector extension
    register_vector(conn)
    cur = conn.cursor()
    # Get the top 3 most similar documents using the KNN &lt;=&gt; operator
    cur.execute("SELECT content FROM embeddings ORDER BY embedding &lt;=&gt; %s LIMIT 3", (embedding_array,))
    top3_docs = cur.fetchall()
    return top3_docs
</code></pre>
<p>Get the closest vector to some row:</p><pre><code class="language-SQL">SELECT * FROM my_table WHERE id != 1 ORDER BY embedding_column &lt;=&gt; (SELECT embedding_column FROM my_table WHERE id = 1) LIMIT 2;
</code></pre>
<p><strong>Tip:</strong> PostgreSQL's ability to use an index does not guarantee its usage! The cost-based planner evaluates query plans and may determine that a sequential scan or a different index is more efficient for a specific query. You can use the EXPLAIN command to see the chosen execution plan. To test the viability of using an index, you can modify planner costing parameters until you achieve the desired plan. For small datasets, setting <code>enable_seqscan = 0</code> can be especially advantageous for testing viability as it avoids sequential scans.<br></p><p>To adjust the probes parameter, you can set the <code>ivfflat.probes</code> variable. For instance, to set it to '5', execute the following statement before running the query:</p><pre><code class="language-sql">SET ivfflat.probes = 5;
</code></pre>
<h3 id="dealing-with-data-changes">Dealing with data changes</h3><p>As your data evolves with inserts, updates, and deletes, the IVFFlat index will be updated accordingly. New vectors will be added to the index, while no longer-used vectors will be removed. </p><p><strong>However, the clustering centroids will not be updated</strong>. Over time, this can result in a situation where the initial clustering, established during index creation, no longer accurately represents the data. This can be visualized as follows:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://timescale.ghost.io/blog/content/images/2023/06/nearest-neighbor-pgvector-diagram---6.png" class="kg-image" alt="As data gets inserted or deleted from the index, if the index is not rebuilt, the ivfflat index in pgvector can return incorrect approximate nearest neighbors due to clustering centroids no longer fitting the data well" loading="lazy" width="1640" height="1040" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2023/06/nearest-neighbor-pgvector-diagram---6.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2023/06/nearest-neighbor-pgvector-diagram---6.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2023/06/nearest-neighbor-pgvector-diagram---6.png 1600w, https://timescale.ghost.io/blog/content/images/2023/06/nearest-neighbor-pgvector-diagram---6.png 1640w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">As data gets inserted or deleted from the index, if the index is not rebuilt, the IVFFlat index in pgvector can return incorrect approximate nearest neighbors due to clustering centroids no longer fitting the data well</span></figcaption></figure><p>To address this issue, the only solution is to rebuild the index.<br></p><p>Here are two important takeaways from this issue:</p><ul><li>Build the index once you have all the representative data you want to reference in it. This is unlike most indexes, which can be built on an empty table.</li><li>It is advisable to periodically rebuild the index.<br></li></ul><p>When rebuilding the index, it is highly recommended to use the <code>CONCURRENTLY</code>option to avoid interfering with ongoing operations.<br></p><p>Thus, to rebuild the index run the following in a cron job:</p><pre><code class="language-SQL">REINDEX INDEX CONCURRENTLY &lt;index name&gt;;
</code></pre>
<h2 id="summing-it-up">Summing It Up</h2><p>The IVFFlat algorithm in pgvector provides an efficient solution for approximate nearest neighbor search over high-dimensional data like embeddings. It works by clustering similar vectors into regions and building an inverted index to map each region to its vectors. This allows queries to focus on a subset of the data, enabling fast search. By tuning the lists and probes parameters, IVFFlat can balance speed and accuracy for a dataset.  </p><p></p><p>Overall, IVFFlat gives PostgreSQL the ability to perform fast semantic similarity search over complex data. With simple queries, applications can find the nearest neighbors to a query vector among millions of high-dimensional vectors. For natural language processing, information retrieval, and more, IVFFlat is a compelling solution. By understanding how IVFFlat divides the vector space into regions and builds its inverted index, you can optimize its performance for your needs and build powerful applications on top of it.</p><p>✨<strong>Resources for further learning:</strong> Now that you know more about the IVFFlat index in pgvector, here are some resources to further your learning journey:&nbsp;</p><ul><li>Learn about other PostgreSQL indexes for vector search, like <a href="https://timescale.ghost.io/blog/vector-database-basics-hnsw/" rel="noreferrer">HNSW</a>.</li><li>Learn how we made <a href="https://timescale.ghost.io/blog/how-we-made-postgresql-as-fast-as-pinecone-for-vector-data/" rel="noreferrer">PostgreSQL as fast as Pinecone for vector data</a>.</li><li>Follow our tutorial on <a href="https://timescale.ghost.io/blog/postgresql-as-a-vector-database-create-store-and-query-openai-embeddings-with-pgvector/" rel="noreferrer">creating, storing, and querying OpenAI embeddings using PostgreSQL as a vector database</a>. <a href="https://timescale.ghost.io/blog/how-to-build-llm-applications-with-pgvector-vector-store-in-langchain/" rel="noreferrer"><u>Learn how </u>to use pgvector as a vector store in LangChain</a>. <a href="https://timescale.ghost.io/blog/refining-vector-search-queries-with-time-filters-in-pgvector-a-tutorial/" rel="noreferrer">Or see how you can refine vector search queries using time filters in pgvector with a single SQL query</a>.</li></ul><p>And if you’re looking for a production-ready PostgreSQL database for your AI application’s vector, relational, and time-series data, <a href="https://www.timescale.com/ai" rel="noreferrer"><u>try Timescale Cloud</u></a>.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[PostgreSQL as a Vector Database: A Pgvector Tutorial]]></title>
            <description><![CDATA[Vector databases add organizational intelligence to AI. Learn how to use PostgreSQL as a vector database for retrieval-augmented generation with pgvector.]]></description>
            <link>https://www.tigerdata.com/blog/postgresql-as-a-vector-database-using-pgvector</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/postgresql-as-a-vector-database-using-pgvector</guid>
            <category><![CDATA[PostgreSQL]]></category>
            <category><![CDATA[pgvector]]></category>
            <category><![CDATA[AI]]></category>
            <category><![CDATA[OpenAI]]></category>
            <dc:creator><![CDATA[Avthar Sewrathan]]></dc:creator>
            <pubDate>Wed, 21 Jun 2023 18:22:10 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2023/06/Postgres-vector-database-and-OpenAI-embeddings-blog--1-.png">
            </media:content>
            <content:encoded><![CDATA[<p>Vector databases enable efficient storage and searching of vector data. They are essential for developing and maintaining AI applications using large language models (LLMs).</p><p>With some help from the <a href="https://www.tigerdata.com/learn/postgresql-extensions-pgvector" rel="noreferrer"><u>pgvector extension</u></a>, you can leverage PostgreSQL as a vector database to store and query<a href="https://platform.openai.com/docs/guides/embeddings/what-are-embeddings?ref=timescale.com"> <u>OpenAI embeddings</u></a>. OpenAI embeddings are a type of data representation (in the shape of vectors, i.e., lists of numbers) used to measure the similarity of text strings for OpenAI’s models.</p><p>In this article, we work through the example of creating a chatbot to answer questions about Tiger Data (creators of TimescaleDB). The chatbot will be trained on content from the <a href="https://timescale.ghost.io/blog/tag/dev-q-a/"><u>Tiger Data Developer Q&amp;A blog posts</u></a>. This example will illustrate the key concepts for creating, storing, and querying OpenAI embeddings with PostgreSQL and pgvector.</p><p>This example has three parts:</p><ul><li>Part 1: How to create embeddings from content using the<a href="https://platform.openai.com/docs/api-reference?ref=timescale.com"> <u>OpenAI API</u></a>.</li><li>Part 2: How to use PostgreSQL as a vector database and store OpenAI embedding vectors using pgvector.</li><li>Part 3: How to use embeddings retrieved from a vector database to augment LLM generation.</li></ul><p>One could think of this as a “hello world” tutorial for building a chatbot that can reference a company knowledge base or developer docs.</p><div class="kg-card kg-callout-card kg-callout-card-grey"><div class="kg-callout-emoji">✨</div><div class="kg-callout-text"><b><strong style="white-space: pre-wrap;">Jupyter Notebook and Code:</strong></b> You can find all the code used in this tutorial in a Jupyter Notebook, as well as sample content and embeddings on the Tiger Data GitHub: <a href="https://github.com/timescale/vector-cookbook/tree/main/openai_pgvector_helloworld">timescale/vector-cookbook</a>. We recommend cloning the repo and following along by executing the code cells as you read through the tutorial.</div></div><h2 id="the-big-picture-openai-embeddings">The Big Picture: OpenAI Embeddings</h2><p>Foundational models of AI (e.g., GPT-3 or GPT-4) may be missing some information needed to provide accurate answers to certain specific questions. That’s because relevant information was not in the dataset used to train the model. (For example, the information is stored in private documents or only became available recently.) This lack of data may make these models unsuitable for use as a chatbot in specific information banks.</p><p><a href="https://www.promptingguide.ai/techniques/rag?ref=timescale.com"><u>Retrieval-augmented generation</u></a> (RAG) gives a simple solution; it provides additional context to the foundational model in the prompt. </p><p>This technique is powerful—it allows you to “teach” foundational models about things only you know about and use that to create a ChatGPT++ experience for your users!</p><p>But what context should you provide to the model? If you have a library of information, how can you determine what’s relevant to a given question? That is what <a href="https://www.timescale.com/blog/a-beginners-guide-to-vector-embeddings" rel="noreferrer">embeddings</a> are for. <a href="https://platform.openai.com/docs/guides/embeddings/what-are-embeddings?ref=timescale.com"><u>OpenAI embeddings</u></a> are a mathematical representation of the semantic meaning of a piece of text that allows for <em>similarity search</em>.</p><p>With this representation, when you get a user question and calculate its embedding, you can use a similarity search against data embeddings in your library to find the most relevant information. But that requires having an embedding representation of your library.&nbsp;&nbsp;</p><h3 id="what-is-a-vector-database">What is a vector database?</h3><p>A <a href="https://www.timescale.com/blog/how-to-choose-a-vector-database"><u>vector database</u></a> is a database that can handle vector data. Vector databases are useful for:</p><ul><li><a href="https://www.tigerdata.com/learn/vector-search-vs-semantic-search" rel="noreferrer"><strong>Semantic search</strong></a><strong>:</strong> Vector databases facilitate semantic search, which considers the context or meaning of search terms rather than just exact matches. They are useful for recommendation systems, content discovery, and question-answering systems.</li><li><strong>Efficient similarity search:</strong> Vector databases are designed for efficient high-dimensional nearest neighbor search, a task where traditional relational databases struggle.</li><li><strong>Machine learning:</strong> Vector databases store and search embeddings created by machine-learning models. This feature aids in finding items semantically similar to a given item.</li><li><strong>Multimedia data handling:</strong> Vector databases also excel in working with multimedia data (images, audio, video) by converting them into high-dimensional vectors for efficient similarity search.</li><li><strong>NLP and data combination:</strong> In natural language processing (NLP), vector databases store high-dimensional vectors representing words, sentences, or documents. They also allow a combination of traditional SQL queries with similarity searches, accommodating both structured and unstructured data.</li></ul><p>We’ll use PostgreSQL with the <a href="https://github.com/pgvector/pgvector"><u>pgvector extension</u></a> installed as our vector database. Pgvector extends PostgreSQL to handle vector data types and vector similarity search, like <a href="https://en.wikipedia.org/wiki/Nearest_neighbor_search"><u>nearest neighbor search</u></a>, which we’ll use to find the k most related embeddings in our database for a given user prompt.</p><h2 id="using-pgvector-for-a-postgresql-vector-database">Using Pgvector for a PostgreSQL Vector Database</h2><p><a href="https://www.timescale.com/learn/using-pgvector-with-python"><u>Pgvector</u></a> is an open-source extension for PostgreSQL that enables storing and searching over machine learning-generated embeddings. It provides different capabilities that allow users to identify exact and approximate nearest neighbors. Pgvector is designed to work seamlessly with other PostgreSQL features, including indexing and querying.</p><p>Now we’re ready to start building our chatbot!</p><h3 id="why-use-pgvector-as-a-vector-database">Why use pgvector as a vector database?</h3><p>Here are five reasons <a href="https://www.tigerdata.com/blog/postgres-for-everything" rel="noreferrer">why <u>PostgreSQL</u></a> is a good choice for storing and handling vector data:</p><ul><li><strong>Integrated solution:</strong> By using PostgreSQL as a vector database, you keep your data in one place. This can simplify your architecture by reducing the need for multiple databases or additional services.</li><li><strong>Enterprise-level robustness and operations:</strong> With a 30-year pedigree, PostgreSQL provides world-class data integrity, operations, and robustness. This includes backups, streaming replication, role-based and row-level security, and ACID compliance.</li><li><strong>Full-featured SQL:</strong> PostgreSQL supports a rich set of SQL features, including joins, subqueries, window functions, and more. This allows for powerful and complex queries that can include both traditional relational data and vector data. It also integrates with a plethora of existing data science and data analysis tools.</li><li><strong>Scalability and performance:</strong> PostgreSQL is known for its robustness and ability to handle large datasets. Using it as a vector database allows you to leverage these characteristics for vector data as well.</li><li><strong>Open source:</strong> PostgreSQL is open source, which means it's free to download and use, and you can modify it to suit your needs. It also means that it benefits from the collective input of developers all over the world, which often results in high-quality, secure, and up-to-date software. PostgreSQL has a large and active community, so help is readily available. There are many resources, including documentation, tutorials, forums, and more, to help you troubleshoot and optimize your PostgreSQL database.</li></ul><h2 id="setting">setting</h2><ul><li>Install Python.</li><li>Install and configure a Python virtual environment. We recommend <a href="https://github.com/pyenv/pyenv">Pyenv</a>.</li><li>Install the requirements for this notebook using the following command:</li></ul><pre><code class="language-Python">pip install -r requirements.txt
</code></pre>
<p>Import all the packages we will be using:</p><pre><code class="language-Python">import openai
import os
import pandas as pd
import numpy as np
import json
import tiktoken
import psycopg2
import ast
import pgvector
import math
from psycopg2.extras import execute_values
from pgvector.psycopg2 import register_vector
</code></pre>
<p>You’ll need to <a href="https://platform.openai.com/overview">sign up for an OpenAI Developer Account</a> and create an OpenAI API Key – we recommend getting a paid account to avoid rate limiting and setting a spending cap so that you avoid any surprises with bills.</p><p>Once you have an OpenAI API key, it’s a <a href="https://help.openai.com/en/articles/5112595-best-practices-for-api-key-safety">best practice</a> to store it as an environment variable and then have your Python program read it.</p><pre><code class="language-Python">#First, run export OPENAI_API_KEY=sk-YOUR_OPENAI_API_KEY...


# Get openAI api key by reading local .env file
from dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv()) 
openai.api_key  = os.environ['OPENAI_API_KEY'] 
</code></pre>
<h2 id="part-1-create-embeddings-for-your-postgresql-vector-database">Part 1: Create Embeddings for Your PostgreSQL Vector Database</h2><p><a href="https://platform.openai.com/docs/guides/embeddings/what-are-embeddings">Embeddings</a> measure how related text strings are. First, we'll create embeddings using the OpenAI API on some text we want the LLM to answer questions on.</p><p>In this example, we'll use content from the Tiger Data blog, specifically from the <a href="https://timescale.ghost.io/blog/tag/dev-q-a/">Developer Q&amp;A section</a>, which features posts by Tiger Data users talking about their real-world use cases.</p><p>You can replace this blog data with any text you want to embed, such as your own company blog, developer documentation, internal knowledge base, or any other information you’d like to have a “ChatGPT-like” experience over.</p><pre><code class="language-Python"># Load your CSV file into a pandas DataFrame
df = pd.read_csv('blog_posts_data.csv')
df.head()
</code></pre>
<p>The output looks like this:</p>
<!--kg-card-begin: html-->
<table style="border:none;border-collapse:collapse;"><colgroup><col width="23"><col width="162"><col width="146"><col width="262"></colgroup><tbody><tr style="height:16.5pt"><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:middle;padding:3pt 6pt 3pt 6pt;overflow:hidden;overflow-wrap:break-word;"><br></td><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:middle;padding:3pt 6pt 3pt 6pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Title</span></p></td><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:middle;padding:3pt 6pt 3pt 6pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Content</span></p></td><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:middle;padding:3pt 6pt 3pt 6pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">URL</span></p></td></tr><tr style="height:38.25pt"><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:middle;padding:3pt 6pt 3pt 6pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">0</span></p></td><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:3pt 6pt 3pt 6pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">How to Build a Weather Station With Elixir, Ne...</span></p></td><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:3pt 6pt 3pt 6pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">This is an installment of our “Community Membe...</span></p></td><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:3pt 6pt 3pt 6pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">https://www.timescale.com/blog/how-to-build-a-...</span></p></td></tr><tr style="height:38.25pt"><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:middle;padding:3pt 6pt 3pt 6pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">1</span></p></td><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:3pt 6pt 3pt 6pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">CloudQuery on Using PostgreSQL for Cloud Asset...</span></p></td><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:3pt 6pt 3pt 6pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">This is an installment of our “Community Membe...</span></p></td><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:3pt 6pt 3pt 6pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">https://www.timescale.com/blog/cloudquery-on-u...</span></p></td></tr><tr style="height:38.25pt"><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:middle;padding:3pt 6pt 3pt 6pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">2</span></p></td><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:3pt 6pt 3pt 6pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">How a Data Scientist Is Building a Time-Series...</span></p></td><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:3pt 6pt 3pt 6pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">This is an installment of our “Community Membe...</span></p></td><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:3pt 6pt 3pt 6pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">https://www.timescale.com/blog/how-a-data-scie...</span></p></td></tr><tr style="height:38.25pt"><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:middle;padding:3pt 6pt 3pt 6pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">3</span></p></td><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:3pt 6pt 3pt 6pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">How Conserv Safeguards History: Building an En...</span></p></td><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:3pt 6pt 3pt 6pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">This is an installment of our “Community Membe...</span></p></td><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:3pt 6pt 3pt 6pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">https://www.timescale.com/blog/how-conserv-saf...</span></p></td></tr><tr style="height:38.25pt"><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:middle;padding:3pt 6pt 3pt 6pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">4</span></p></td><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:3pt 6pt 3pt 6pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">How Messari Uses Data to Open the Cryptoeconom...</span></p></td><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:3pt 6pt 3pt 6pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">This is an installment of our “Community Membe...</span></p></td><td style="border-left:solid #000000 0.75pt;border-right:solid #000000 0.75pt;border-bottom:solid #000000 0.75pt;border-top:solid #000000 0.75pt;vertical-align:top;padding:3pt 6pt 3pt 6pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">https://www.timescale.com/blog/how-messari-use...</span></p></td></tr></tbody></table>
<!--kg-card-end: html-->
<h3 id="11-calculate-the-cost-of-embedding-data">1.1 Calculate the cost of embedding data</h3><p>It's usually a good idea to calculate how much creating embeddings for your selected content will cost. We provide a number of helper functions to calculate a cost estimate before creating the embeddings to help us avoid surprises.</p><p>For OpenAI, you are charged on a per-token basis for embeddings created. The total cost for the blog posts we want to embed will be less than $0.01, thanks to OpenAI’s small text embedding model, <a href="https://openai.com/index/new-embedding-models-and-api-updates/"><u>text-embedding-3-small</u></a>. This model boasts not only stronger performance but also 5X cost reduction compared to its predecessor, <a href="https://openai.com/blog/new-and-improved-embedding-model?ref=timescale.com"><u>text-embedding-ada-002</u></a>.</p><pre><code class="language-Python"># Helper functions to help us create the embeddings

# Helper func: calculate number of tokens
def num_tokens_from_string(string: str, encoding_name = "cl100k_base") -&gt; int:
    if not string:
        return 0
    # Returns the number of tokens in a text string
    encoding = tiktoken.get_encoding(encoding_name)
    num_tokens = len(encoding.encode(string))
    return num_tokens

# Helper function: calculate length of essay
def get_essay_length(essay):
    word_list = essay.split()
    num_words = len(word_list)
    return num_words

# Helper function: calculate cost of embedding num_tokens
# Assumes we're using the text-embedding-ada-002 model
# See https://openai.com/pricing
def get_embedding_cost(num_tokens):
    return num_tokens/1000*0.0002

# Helper function: calculate total cost of embedding all content in the dataframe
def get_total_embeddings_cost():
    total_tokens = 0
    for i in range(len(df.index)):
        text = df['content'][i]
        token_len = num_tokens_from_string(text)
        total_tokens = total_tokens + token_len
    total_cost = get_embedding_cost(total_tokens)
    return total_cost

</code></pre>
<pre><code class="language-Python"># quick check on total token amount for price estimation
total_cost = get_total_embeddings_cost()
print("estimated price to embed this content = $" + str(total_cost))

</code></pre>
<h3 id="12-create-smaller-chunks-of-content">1.2 Create smaller chunks of content</h3><p>The OpenAI API has a maximum token <a href="https://platform.openai.com/docs/guides/embeddings/what-are-embeddings"><u>limit</u></a> that it can create an embedding for in a single request: 8,191 to be specific. To get around this limit, we'll break up our text into smaller chunks. Generally, it's a best practice to “chunk” the documents you want to create embeddings into groups of a fixed token size.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2024/10/Making-PostgreSQL-a-Vector-Database-pgvector-tutorial_embedding-models.png" class="kg-image" alt="A table with the performance eval of the OpenAI embedding models" loading="lazy" width="1748" height="546" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2024/10/Making-PostgreSQL-a-Vector-Database-pgvector-tutorial_embedding-models.png 600w, https://timescale.ghost.io/blog/content/images/size/w1000/2024/10/Making-PostgreSQL-a-Vector-Database-pgvector-tutorial_embedding-models.png 1000w, https://timescale.ghost.io/blog/content/images/size/w1600/2024/10/Making-PostgreSQL-a-Vector-Database-pgvector-tutorial_embedding-models.png 1600w, https://timescale.ghost.io/blog/content/images/2024/10/Making-PostgreSQL-a-Vector-Database-pgvector-tutorial_embedding-models.png 1748w" sizes="(min-width: 720px) 720px"></figure><p>The precise number of tokens to include in a chunk depends on your use case and your model’s context window—the number of input tokens it can handle in a prompt.</p><p>For our purposes, we'll aim for chunks of around 512 tokens each. Chunking text up is a complex topic worthy of its own blog post. We’ll illustrate a simple method we found to work well below. &nbsp;If you want to read about other approaches, we recommend <a href="https://python.langchain.com/docs/how_to/#text-splitters"><u>this section</u></a> of the LangChain docs.</p><p><strong>Note:</strong> If you prefer to skip this step, you can use the provided file: <a href="https://github.com/timescale/vector-cookbook/tree/main/openai_pgvector_helloworld">blog_data_and_embeddings.csv</a>, which contains the data and embeddings that you'll generate in this step.</p><p>The code below creates a new list of our blog content while retaining the metadata associated with the text, such as the blog title and URL that the text is associated with.</p><pre><code class="language-Python">
# Create new list with small content chunks to not hit max token limits
# Note: the maximum number of tokens for a single request is 8191
# https://platform.openai.com/docs/guides/embeddings/embedding-models

# list for chunked content and embeddings
new_list = []
# Split up the text into token sizes of around 512 tokens
for i in range(len(df.index)):
    text = df['content'][i]
    token_len = num_tokens_from_string(text)
    if token_len &lt;= 512:
        new_list.append([df['title'][i], df['content'][i], df['url'][i], token_len])
    else:
        # add content to the new list in chunks
        start = 0
        ideal_token_size = 512
        # 1 token ~ 3/4 of a word
        ideal_size = int(ideal_token_size // (4/3))
        end = ideal_size
        #split text by spaces into words
        words = text.split()

        #remove empty spaces
        words = [x for x in words if x != ' ']

        total_words = len(words)
        
        #calculate iterations
        chunks = total_words // ideal_size
        if total_words % ideal_size != 0:
            chunks += 1
        
        new_content = []
        for j in range(chunks):
            if end &gt; total_words:
                end = total_words
            new_content = words[start:end]
            new_content_string = ' '.join(new_content)
            new_content_token_len = num_tokens_from_string(new_content_string)
            if new_content_token_len &gt; 0:
                new_list.append([df['title'][i], new_content_string, df['url'][i], new_content_token_len])
            start += ideal_size
            end += ideal_size

</code></pre>
<p>Now that our text is chunked better, we can create embeddings for each chunk of text using the OpenAI API.</p><p>We’ll use this helper function to create embeddings for a piece of text:</p><pre><code class="language-Python">openai_client = openai.OpenAI()

# Helper function: get embeddings for a text
def get_embeddings(text):
    response = openai_client.embeddings.create(
        model="text-embedding-3-small",
        input = text.replace("\n"," ")
    )
    return response.data[0].embedding</code></pre>
<p><br>And then create embeddings for each chunk of content:</p><pre><code class="language-Python"># Create embeddings for each piece of content
for i in range(len(new_list)):
   text = new_list[i][1]
   embedding = get_embeddings(text)
   new_list[i].append(embedding)

# Create a new dataframe from the list
df_new = pd.DataFrame(new_list, columns=['title', 'content', 'url', 'tokens', 'embeddings'])
df_new.head()

</code></pre>
<p>The new data frame should look like this:</p>
<!--kg-card-begin: html-->
<style type="text/css">
.tg  {border-collapse:collapse;border-spacing:0;}
.tg td{border-color:black;border-style:solid;border-width:1px;font-family:Arial, sans-serif;font-size:14px;
  overflow:hidden;padding:10px 5px;word-break:normal;}
.tg th{border-color:black;border-style:solid;border-width:1px;font-family:Arial, sans-serif;font-size:14px;
  font-weight:normal;overflow:hidden;padding:10px 5px;word-break:normal;}
.tg .tg-0pky{border-color:inherit;text-align:left;vertical-align:top}
.tg .tg-0lax{text-align:left;vertical-align:top}
</style>
<table class="tg">
<thead>
  <tr>
    <th class="tg-0pky"></th>
    <th class="tg-0pky"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">Title</span></th>
    <th class="tg-0pky"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">Content</span></th>
    <th class="tg-0lax"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">URL</span></th>
    <th class="tg-0lax"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">Tokens</span></th>
    <th class="tg-0lax"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">Embeddings</span></th>
  </tr>
</thead>
<tbody>
  <tr>
    <td class="tg-0pky"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">0</span></td>
    <td class="tg-0pky"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">How to Build a Weather Station With Elixir, Ne...</span></td>
    <td class="tg-0pky"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">This is an installment of our “Community Membe...</span></td>
    <td class="tg-0lax"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">https://www.timescale.com/blog/how-to-build-a-...</span></td>
    <td class="tg-0lax"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">501</span></td>
    <td class="tg-0lax"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">[0.021440856158733368, 0.02200360782444477, -0...</span></td>
  </tr>
  <tr>
    <td class="tg-0pky"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">1</span></td>
    <td class="tg-0pky"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">How to Build a Weather Station With Elixir, Ne...</span></td>
    <td class="tg-0pky"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">capture weather and environmental data. In all...</span></td>
    <td class="tg-0lax"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">https://www.timescale.com/blog/how-to-build-a-...</span></td>
    <td class="tg-0lax"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">512</span></td>
    <td class="tg-0lax"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">[0.016165969893336296, 0.011341351084411144, 0...</span></td>
  </tr>
  <tr>
    <td class="tg-0pky"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">2</span></td>
    <td class="tg-0pky"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">How to Build a Weather Station With Elixir, Ne...</span></td>
    <td class="tg-0pky"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">command in their database migration:SELECT cre...</span></td>
    <td class="tg-0lax"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">https://www.timescale.com/blog/how-to-build-a-...</span></td>
    <td class="tg-0lax"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">374</span></td>
    <td class="tg-0lax"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">[0.022517921403050423, -0.0019158280920237303,...</span></td>
  </tr>
  <tr>
    <td class="tg-0pky"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">3</span></td>
    <td class="tg-0pky"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">CloudQuery on Using PostgreSQL for Cloud Asset...</span></td>
    <td class="tg-0pky"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">This is an installment of our “Community Membe...</span></td>
    <td class="tg-0lax"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">https://www.timescale.com/blog/cloudquery-on-u...</span></td>
    <td class="tg-0lax"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">519</span></td>
    <td class="tg-0lax"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">[0.009028822183609009, -0.005185891408473253, ...</span></td>
  </tr>
  <tr>
    <td class="tg-0pky"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">4</span></td>
    <td class="tg-0pky"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">CloudQuery on Using PostgreSQL for Cloud Asset...</span></td>
    <td class="tg-0pky"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">Architecture with CloudQuery SDK- Writing plug...</span></td>
    <td class="tg-0lax"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">https://www.timescale.com/blog/cloudquery-on-u...</span></td>
    <td class="tg-0lax"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">511</span></td>
    <td class="tg-0lax"><span style="font-weight:400;font-style:normal;text-decoration:none;color:#000;background-color:transparent">[0.02050386555492878, 0.010169642977416515, 0....</span></td>
  </tr>
</tbody>
</table>
<!--kg-card-end: html-->
<p><br>As an optional but recommended step, you can save the original blog content along with associated embeddings in a CSV file for reference later on so that you don't have to recreate embeddings if you want to reference it in another project.</p><pre><code class="language-Python"># Save the dataframe with embeddings as a CSV file
df_new.to_csv('blog_data_and_embeddings.csv', index=False)
</code></pre>
<h2 id="pro-tip-automating-embedding-creation-with-pgai-vectorizer">Pro Tip: Automating Embedding Creation with pgai Vectorizer</h2><p>In the section above, we showed how to manually create and manage embeddings in your own data pipeline – chunking content, calling the OpenAI API, and storing the results. While this approach helps you understand the fundamentals, in production, you may want to automate this process completely. Let’s look at how <a href="https://github.com/timescale/pgai/blob/main/docs/vectorizer.md"><u>pgai Vectorizer</u></a> can handle this entire pipeline for you!&nbsp;</p><p>Managing embeddings in production involves several challenges: keeping embeddings in sync with changing content, handling API failures, and optimally chunking text.&nbsp;</p><p><a href="https://www.tigerdata.com/blog/pgai-giving-postgresql-developers-ai-engineering-superpowers" rel="noreferrer">pgai</a> Vectorizer automates this entire process directly in PostgreSQL - similar to how PostgreSQL automatically <a href="https://docs.timescale.com/use-timescale/latest/schema-management/indexing?ref=timescale.com"><u>maintains indexes</u></a> for your tables.</p><h3 id="setting-up-pgai-vectorizer">Setting Up pgai Vectorizer</h3><p>The setup process differs depending on whether you’re using Tiger Cloud (formerly Timescale Cloud) or hosting PostgreSQL yourself.&nbsp;</p><p><strong>On Tiger Cloud</strong></p><pre><code class="language-python">-- 1. Store your OpenAI API key securely in Timescale Cloud
-- 2. Navigate to Project Settings &gt; AI Model API Keys in the Timescale Console
-- 3. The key is stored securely and not in your database
-- 4. Create the extensions
CREATE EXTENSION IF NOT EXISTS ai;</code></pre><p><strong>For self-hosted PostgreSQL</strong></p><pre><code class="language-bash">export OPENAI_API_KEY="your-api-key-here"

# Start the vectorizer worker
vectorizer-worker --connection="postgres://user:password@host:port/dbname"</code></pre><h3 id="creating-your-first-vectorizer">Creating Your First Vectorizer</h3><p>Instead of manually creating embeddings using Python, you can define a <em>vectorizer</em> that automatically generates and maintains embeddings for your content:</p><pre><code class="language-python">SELECT ai.create_vectorizer( 
   'blog_posts'::regclass,
    destination =&gt; 'blog_embeddings',
    embedding =&gt; ai.embedding_openai('text-embedding-3-small', 768),
    chunking =&gt; ai.chunking_recursive_character_text_splitter('content'),
    -- Pro tip: Add blog title as context to each chunk
    formatting =&gt; ai.formatting_python_template('$title: $chunk')
);</code></pre><p>This single SQL command:</p><ol><li>Automatically chunks your blog content</li><li>Creates embeddings for each chunk using OpenAI's API</li><li>Maintains embeddings as your content changes</li><li>Creates a view that joins your content with its embeddings</li></ol><h3 id="searching-with-vectorizer">Searching with Vectorizer</h3><p>You can then search your content the same way as before:</p><pre><code class="language-Python">SELECT 
   chunk,
   embedding &lt;=&gt; ai.openai_embed('text-embedding-3-small', 'How is Timescale used in IoT?') as distance
FROM blog_embeddings
ORDER BY distance
LIMIT 3;</code></pre><p>Vectorizer runs automatically every five minutes on <a href="https://console.cloud.timescale.com/signup?ref=timescale.com"><u>Tiger Cloud</u></a>, handling retries and keeping your embeddings up to date. For more details on setup and advanced features like <a href="https://github.com/timescale/pgai/blob/main/docs/vectorizer.md#monitor-a-vectorizer"><u>monitoring the Vectorizer</u></a>, see our pgai Vectorizer <a href="https://github.com/timescale/pgai/blob/main/docs/vectorizer.md"><u>documentation</u></a>.&nbsp;</p><h3 id="further-reading-on-rag">Further Reading on RAG</h3><p>The accuracy and cost of your RAG application depends heavily on implementation choices such as the embedding model selection to chunking strategies.&nbsp;</p><p>Here are more blog posts to help you build effective RAG applications with PostgreSQL:</p><ol><li><a href="https://timescale.ghost.io/blog/vector-databases-are-the-wrong-abstraction?ref=timescale.com"><u>Vector Databases Are the Wrong Abstraction</u></a> – learn why general-purpose databases with vector extensions like <a href="https://timescale.ghost.io/blog/pgvector-is-now-as-fast-as-pinecone-at-75-less-cost/"><u>pgvectorscale</u></a> often provide better solutions than specialized vector databases</li><li><a href="https://timescale.ghost.io/blog/which-rag-chunking-and-formatting-strategy-is-best?ref=timescale.com"><u>Which RAG Chunking and Formatting Strategy Is Best?</u></a> – Explore different approaches to chunking and formatting your content for optimal retrieval-augmented generation (RAG) performance</li><li><a href="https://www.tigerdata.com/blog/which-openai-embedding-model-is-best" rel="noreferrer"><u>Which OpenAI Embedding Model Is Best?</u></a> - Compare OpenAI's embedding models to choose the right one for your use case</li></ol><h2 id="part-2-store-embeddings-in-a-postgresql-vector-database-using-pgvector">Part 2: Store Embeddings in a PostgreSQL Vector Database Using Pgvector</h2><p>Now that we have created embedding vectors for our blog content, the next step is to store the embedding vectors in a vector database to help us perform a fast search over many vectors.</p><h3 id="21-create-a-postgresql-database-and-install-pgvector">2.1 Create a PostgreSQL database and <a href="https://www.tigerdata.com/learn/postgresql-extensions-pgvector" rel="noreferrer">install pgvector</a></h3><p>First, we’ll create a PostgreSQL database. You can <a href="https://docs.timescale.com/getting-started/latest/services/" rel="noreferrer">create a cloud PostgreSQL database</a> in minutes for free on <a href="https://console.cloud.timescale.com/signup">Tiger Cloud</a> or use a local PostgreSQL database for this step. </p><p>Once you’ve created your PostgreSQL database, export your connection string as an environment variable, and just like the OpenAI API key, we’ll read it into our Python program from the environment file:</p><pre><code class="language-Python"># Timescale database connection string
# Found under "Service URL" of the credential cheat-sheet or "Connection Info" in the Timescale console
# In terminal, run: export TIMESCALE_CONNECTION_STRING=postgres://&lt;fill in here&gt;

connection_string  = os.environ['TIMESCALE_CONNECTION_STRING']

</code></pre>
<p>We then connect to our database using the popular <a href="https://pypi.org/project/psycopg2/?ref=timescale.com"><u>psycopg2</u></a> Python library and install the pgvector and <a href="https://github.com/timescale/pgvectorscale?tab=readme-ov-file#installation"><u>pgvectorscale</u></a> extension (which provides powerful filtering and indexing capabilities ) as follows:</p><pre><code class="language-Python"># Connect to PostgreSQL database in Timescale using connection string
conn = psycopg2.connect(connection_string)
cur = conn.cursor()

#install pgvector
cur.execute("CREATE EXTENSION IF NOT EXISTS vector;")
conn.commit()

#install pgvectorscale
cur.execute("CREATE EXTENSION IF NOT EXISTS vectorscale CASCADE;")
conn.commit()
</code></pre>
<h3 id="22-connect-to-and-configure-your-vector-database">2.2 Connect to and configure your vector database</h3><p>Once we’ve installed pgvector, we use the <a href="https://github.com/pgvector/pgvector-python#psycopg-2">register_vector()</a> command to register the vector type with our connection:</p><pre><code class="language-Python"># Register the vector type with psycopg2
register_vector(conn)
</code></pre>
<p>Once we’ve connected to the database, let’s create a table that we’ll use to store embeddings along with metadata. Our table will look as follows:<br></p>
<!--kg-card-begin: html-->
<table style="border:none;border-collapse:collapse;"><colgroup><col width="44"><col width="48"><col width="60"><col width="87"><col width="84"><col width="112"></colgroup><tbody><tr style="height:0pt"><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">id</span></p></td><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">title&nbsp;</span></p></td><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">url</span></p></td><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">content</span></p></td><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">tokens</span></p></td><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">embedding</span></p></td></tr></tbody></table>
<!--kg-card-end: html-->
<p></p><ul><li><code>Id</code> represents the unique ID of each <a href="https://www.tigerdata.com/blog/a-beginners-guide-to-vector-embeddings" rel="noreferrer">vector embedding</a> in the table.</li><li><code>title</code> is the blog title from which the content associated with the embedding is taken.</li><li><code>url</code> is the blog URL from which the content associated with the embedding is taken.</li><li><code>content</code> is the actual blog content associated with the embedding.</li><li><code>tokens</code> is the number of tokens the embedding represents.</li><li><code>embedding</code> is the vector representation of the content.<br></li></ul><p>One advantage of using PostgreSQL as a vector database is that you can easily store metadata and embedding vectors in the same database, which is helpful for supplying the user-relevant information related to the response they receive, like links to read more or specific parts of a blog post that are relevant to them.</p><pre><code class="language-Python"># Create table to store embeddings and metadata
table_create_command = """
CREATE TABLE embeddings (
            id bigserial primary key, 
            title text,
            url text,
            content text,
            tokens integer,
            embedding vector(1536)
            );
            """

cur.execute(table_create_command)
cur.close()
conn.commit()

</code></pre>
<h3 id="23-ingest-and-store-vector-data-into-postgresql-using-pgvector">2.3 Ingest and <a href="https://www.tigerdata.com/learn/vector-store-vs-vector-database" rel="noreferrer">store vector</a> data into PostgreSQL using pgvector</h3><p>Now that we’ve created the database and created the table to house the embeddings and metadata, the final step is to insert the embedding vectors into the database. </p><p>For this step, it’s a best practice to batch insert the embeddings rather than insert them one by one.<br></p><pre><code class="language-Python">#Batch insert embeddings and metadata from dataframe into PostgreSQL database
register_vector(conn)
cur = conn.cursor()
# Prepare the list of tuples to insert
data_list = [(row['title'], row['url'], row['content'], int(row['tokens']), np.array(row['embeddings'])) for index, row in df_new.iterrows()]
# Use execute_values to perform batch insertion
execute_values(cur, "INSERT INTO embeddings (title, url, content, tokens, embedding) VALUES %s", data_list)
# Commit after we insert all embeddings
conn.commit()

</code></pre>
<p>Let’s sanity check by running some simple queries against our newly inserted data:</p><pre><code class="language-Python">cur.execute("SELECT COUNT(*) as cnt FROM embeddings;")
num_records = cur.fetchone()[0]
print("Number of vector records in table: ", num_records,"\n")
# Correct output should be 129

</code></pre>
<pre><code class="language-Python"># print the first record in the table, for sanity-checking
cur.execute("SELECT * FROM embeddings LIMIT 1;")
records = cur.fetchall()
print("First record in table: ", records)
</code></pre>
<h3 id="24-index-your-data-for-faster-retrieval">2.4 Index your data for faster retrieval</h3><p>In this example, we only have 129 embedding vectors, so searching through all of them is blazingly fast. But for larger datasets, you need to create indexes to speed up searching for similar embeddings, so we include the code to build the index for illustrative purposes. </p><p>While pgvector&nbsp;supports the <a href="https://www.tigerdata.com/blog/nearest-neighbor-indexes-what-are-ivfflat-indexes-in-pgvector-and-how-do-they-work" rel="noreferrer"><u>IVFFLAT</u></a> and <a href="https://www.tigerdata.com/learn/vector-database-basics-hnsw" rel="noreferrer"><u>HNSW</u></a> index types for approximate nearest neighbor (ANN) search, <a href="https://github.com/timescale/pgvectorscale"><u>pgvectorscale</u></a> offers a more cost-efficient and powerful index type for pgvector data: <a href="https://timescale.ghost.io/blog/pgvector-is-now-as-fast-as-pinecone-at-75-less-cost/"><u>StreamingDiskANN</u></a>, which we use here. </p><p>You always want to build this index <strong>after</strong> you have inserted the data, as the index needs to discover clusters in your data to be effective, and it does this only when first building the index. </p><p>The StreamingDiskANN index has tunable parameters depending on your goal, whether it is changing indexing operations or querying operations. In our case, we use the default values of the parameters. You can read more about <a href="https://github.com/timescale/pgvectorscale?tab=readme-ov-file#tuning"><u>tuning here</u></a>.</p><pre><code class="language-Python"># Create an index on the data for faster retrieval
cur.execute('CREATE INDEX embedding_idx ON embeddings USING diskann (embedding);')
conn.commit()
</code></pre>
<h2 id="part-3-nearest-neighbor-search-using-pgvector">Part 3: Nearest Neighbor Search Using pgvector</h2><p>Given a user question, we’ll perform the following steps to use information stored in the vector database to answer their question using Retrieval Augmented Generation:</p><ol><li>Create an embedding vector for the user question.</li><li>Use pgvector to perform a vector similarity search and retrieve the <code>k</code> nearest neighbors to the question embedding from our embedding vectors representing the blog content. In our example, we’ll use k=3, finding the three most similar embedding vectors and associated content.</li><li>Supply the content retrieved from the database as additional context to the model and ask it to perform a completion task to answer the user question.</li></ol><h3 id="31-define-a-question-you-want-to-answer">3.1 Define a question you want to answer</h3><p>First, we’ll define a sample question that a user might want to answer about the blog posts stored in the database.</p><pre><code class="language-Python"># Question about Timescale we want the model to answer
input = "How is Timescale used in IoT?"
</code></pre>
<p>Since TimescaleDB is <a href="https://timescale.ghost.io/blog/visualizing-iot-data-at-scale-with-hopara-and-timescaledb/">popular for IoT sensor data</a>, a user might want to learn specifics about how they can leverage it for that use case.</p><h3 id="32-find-the-most-relevant-content-in-the-database">3.2 Find the most relevant content in the database</h3><p>Here’s the function we use to find the three nearest neighbors to the user question. Note it uses pgvector’s <code>&lt;=&gt;</code> operator, which finds the <a href="https://en.wikipedia.org/wiki/Cosine_similarity">Cosine distance</a> (also known as <a href="https://www.tigerdata.com/learn/understanding-cosine-similarity" rel="noreferrer">Cosine similarity</a>) between two embedding vectors.  </p><pre><code class="language-Python"># Helper function: Get top 3 most similar documents from the database
def get_top3_similar_docs(query_embedding, conn):
    embedding_array = np.array(query_embedding)
    # Register pgvector extension
    register_vector(conn)
    cur = conn.cursor()
    # Get the top 3 most similar documents using the KNN &lt;=&gt; operator
    cur.execute("SELECT content FROM embeddings ORDER BY embedding &lt;=&gt; %s LIMIT 3", (embedding_array,))
    top3_docs = cur.fetchall()
    return top3_docs

</code></pre>
<h3 id="33-define-helper-functions-to-query-openai">3.3 Define helper functions to query OpenAI</h3><p>We define a helper function to get a completion response from an OpenAI model while we use the previously defined helper function, <code>get_embeddings</code>, to create an embedding for the user question. We use GPT-4o, but you can use any other model from OpenAI.</p><p>We also specify a number of parameters, such as limits of the maximum number of tokens in the model response and model temperature, which controls the randomness of the model, which you can modify to your liking:</p><pre><code class="language-Python"># Helper function: get text completion from OpenAI API
# Note we're using the latest gpt-3.5-turbo-0613 model
def get_completion_from_messages(messages, model="gpt-4o", temperature=0, max_tokens=1000):
    response = openai_client.chat.completions.create(
        model=model,
        messages=messages,
        temperature=temperature, 
        max_tokens=max_tokens, 
    )
    return response.choices[0].message.content
</code></pre>
<h3 id="33-putting-it-all-together">3.3 Putting it all together</h3><p>We’ll define a function to process the user input by retrieving the most similar documents from our database and passing the user input, along with the relevant retrieved context to the OpenAI model to provide a completion response to.</p><p>Note that we modify the system prompt as well in order to influence the tone of the model’s response.</p><p>We pass to the model the content associated with the three most similar embeddings to the user input using the assistant role. You can also append the additional context to the user message.<br></p><pre><code class="language-Python"># Function to process input with retrieval of most similar documents from the database
def process_input_with_retrieval(user_input):
    delimiter = "```"

    #Step 1: Get documents related to the user input from database
    related_docs = get_top3_similar_docs(get_embeddings(user_input), conn)

    # Step 2: Get completion from OpenAI API
    # Set system message to help set appropriate tone and context for model
    system_message = f"""
    You are a friendly chatbot. \
    You can answer questions about timescaledb, its features and its use cases. \
    You respond in a concise, technically credible tone. \
    """

    # Prepare messages to pass to model
    # We use a delimiter to help the model understand the where the user_input starts and ends
    messages = [
        {"role": "system", "content": system_message},
        {"role": "user", "content": f"{delimiter}{user_input}{delimiter}"},
        {"role": "assistant", "content": f"Relevant Timescale case studies information: \n {related_docs[0][0]} \n {related_docs[1][0]} {related_docs[2][0]}"}   
    ]

    final_response = get_completion_from_messages(messages)
    return final_response
</code></pre>
<p>Let’s see an example of the model’s output to our original input question:</p><pre><code class="language-Python">input = "How is Timescale used in IoT?"
response = process_input_with_retrieval(input)
print(input)
print(response)
</code></pre>
<p>Here’s the model’s response:</p><pre><code>User input: How is Timescale used in IoT?

Model Response:

TimescaleDB is widely used in IoT applications due to its ability to efficiently handle time-series data, which is a common requirement in IoT systems. Here are some ways TimescaleDB is utilized in IoT:

1. **Data Ingestion and Storage**: IoT devices generate large volumes of time-stamped data. TimescaleDB's architecture is optimized for ingesting and storing this data efficiently, allowing for high write throughput and efficient storage through features like data compression.

2. **Real-time Analytics**: TimescaleDB supports real-time analytics, enabling IoT applications to process and analyze data as it arrives. This is crucial for applications that require immediate insights, such as monitoring systems or predictive maintenance.

3. **Historical Data Analysis**: With its support for continuous aggregates, TimescaleDB allows for efficient querying of historical data. This is useful for trend analysis, reporting, and long-term data storage, which are common in IoT use cases.

4. **Integration with IoT Platforms**: TimescaleDB can be integrated with various IoT platforms and tools, such as AWS IoT, MQTT, and Grafana, to manage, visualize, and analyze IoT data seamlessly.

5. **Scalability**: As IoT deployments grow, the volume of data can increase significantly. TimescaleDB's scalability features, such as partitioning and distributed hypertables, help manage this growth effectively.

Overall, TimescaleDB provides a robust solution for managing the unique challenges of IoT data, combining the benefits of a relational database with time-series capabilities.

</code></pre><p>We can also ask the model questions about specific documents in the database, in this case about specific TimescaleDB users who have spoken about their experience in a blog post:</p><pre><code class="language-Python">input_2 = "Tell me about Edeva and Hopara. How do they use Timescale?"
response_2 = process_input_with_retrieval(input_2)
print(input_2)
print(response_2)

</code></pre>
<p>Here’s the model output for the input question above; notice how it uses specific details from the blog posts.</p><pre><code>User input: Tell me about Edeva and Hopara. How do they use Timescale?


Model Response:

### Edeva

**Use Case:**
Edeva leverages TimescaleDB to manage and analyze data from their smart city systems, particularly focusing on their Actibump dynamic speed bump. The data collected includes:

1. **IoT Event Data:** Information such as vehicle speeds and environmental conditions.
2. **Alarm Data:** Alerts for sensor malfunctions or other issues.
3. **Status Data:** Self-checks and statistical data from IoT devices.
4. **Administrative Data:** Metadata about devices, such as configuration details.

**Key Features Utilized:**
- **Continuous Aggregations:** To speed up queries and make dashboards responsive.
- **Percentile Aggregations:** For calculating accurate percentile values without querying raw data.
- **SQL Compatibility:** Simplifies onboarding for developers familiar with SQL.

**Benefits:**
- **Performance:** Transitioned from sluggish to lightning-fast dashboards.
- **Ease of Use:** Developers could quickly adapt due to SQL familiarity.
- **Scalability:** Efficiently handles large datasets, such as hundreds of millions of records.

### Hopara

**Use Case:**
Hopara uses TimescaleDB to manage and visualize time-series data for their geospatial analytics platform. The platform integrates various data sources to provide insights into spatial and temporal trends.

**Key Features Utilized:**
- **Time-Series Data Management:** Efficiently stores and queries large volumes of time-series data.
- **Geospatial Capabilities:** Leverages PostgreSQL’s PostGIS extension for spatial queries.
- **Continuous Aggregations:** To pre-compute and speed up complex queries.

**Benefits:**
- **Scalability:** Handles large datasets with ease.
- **Performance:** Fast query execution for real-time analytics.
- **Integration:** Seamless integration with existing PostgreSQL tools and extensions.

Both Edeva and Hopara benefit from TimescaleDB’s ability to handle large volumes of time-series data efficiently, providing fast query performance and ease of use through SQL compatibility.
</code></pre><h2 id="conclusion">Conclusion</h2><p><a href="https://www.timescale.com/blog/rag-is-more-than-just-vector-search" rel="noreferrer">Retrieval-augmented generation (RAG)</a> is a powerful method of building applications with LLMs that enables you to teach foundation models about things they were not originally trained on, like private documents or recently published information.</p><p>This project is an example of how to create, store, and perform similarity search on <a href="https://www.tigerdata.com/blog/open-source-vs-openai-embeddings-for-rag" rel="noreferrer">OpenAI embeddings</a>. We used PostgreSQL + <a href="https://github.com/pgvector/pgvector"><u>pgvector</u></a> + <a href="https://github.com/timescale/pgvectorscale"><u>pgvectorscale</u></a> as our vector database to efficiently store and query the embeddings, enabling precise and relevant responses.</p><h2 id="timescaledb-postgresql">TimescaleDB + PostgreSQL</h2><p>And if you’re looking for a production PostgreSQL database for your vector workloads, <a href="https://console.cloud.timescale.com/signup"><u>try Timescale</u></a>. It’s free for 30 days, no credit card required.</p><h3 id="further-reading">Further reading</h3><p>Here are more blog posts about RAG with PostgreSQL and different tools:</p><ul><li><a href="https://www.timescale.com/blog/rag-is-more-than-just-vector-search/"><u>RAG Is More Than Just Vector Search</u></a></li><li><a href="https://timescale.com/blog/retrieval-augmented-generation-with-claude-sonnet-3-5-and-pgvector/"><u>Retrieval-Augmented Generation With Claude Sonnet 3.5 &amp; Pgvector</u></a></li><li><a href="https://www.timescale.com/blog/build-a-fully-local-rag-app-with-postgresql-mistral-and-ollama/"><u>Build a Fully Local RAG App With PostgreSQL, Mistral, and Ollama</u></a></li><li><a href="https://www.timescale.com/blog/building-an-ai-image-gallery-advanced-rag-with-pgvector-and-claude-sonnet-3-5/"><u>Building an AI Image Gallery: Advanced RAG With Pgvector and Claude Sonnet 3.5</u></a></li></ul><p></p>]]></content:encoded>
        </item>
    </channel>
</rss>