<?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 09:51:30 GMT</lastBuildDate>
        <atom:link href="https://www.tigerdata.com/blog" rel="self" type="application/rss+xml"/>
        <ttl>60</ttl>
        <item>
            <title><![CDATA[Build Search and RAG Systems on PostgreSQL Using Cohere and Pgai]]></title>
            <description><![CDATA[Enterprise-ready LLMs from Cohere, now available in the pgai PostgreSQL extension.]]></description>
            <link>https://www.tigerdata.com/blog/build-search-and-rag-systems-on-postgresql-using-cohere-and-pgai</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/build-search-and-rag-systems-on-postgresql-using-cohere-and-pgai</guid>
            <category><![CDATA[AI]]></category>
            <category><![CDATA[PostgreSQL]]></category>
            <dc:creator><![CDATA[Avthar Sewrathan]]></dc:creator>
            <pubDate>Fri, 09 Aug 2024 18:01:55 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2024/08/Cohere_pgai.png">
            </media:content>
            <content:encoded><![CDATA[<p><em>Enterprise-ready LLMs from Cohere—now available in the pgai PostgreSQL extension.</em></p><p><a href="https://cohere.com" rel="noreferrer"><strong>Cohere</strong></a> is a leading generative AI company in the field of large language models (LLMs) and retrieval-augmented generation (RAG) systems. What sets Cohere apart from other model developers is its unwavering focus on enterprise needs and support for multiple languages.&nbsp;</p><h3 id="cohere-models">Cohere models</h3><p>Cohere has quickly gained traction among businesses and developers alike, thanks to its suite of models that cater to key steps of the AI application-building process: text embedding, result reranking, and reasoning. Here’s an overview of Cohere models:</p><ul><li><a href="https://cohere.com/embed"><strong><u>Cohere Embed</u></strong></a><strong>:</strong> A leading text representation model supporting over 100 languages, with the latest version (Embed v3) capable of evaluating document quality and relevance to queries. Cohere Embed is ideal for <a href="https://www.tigerdata.com/learn/vector-search-vs-semantic-search" rel="noreferrer">semantic search</a>, retrieval-augmented generation (RAG), clustering, and classification tasks.</li><li><a href="https://cohere.com/rerank"><strong><u>Cohere Rerank</u></strong></a><strong>:</strong> A model that significantly improves search quality for any keyword or vector search system with minimal code changes. It’s optimized for high throughput and reduced compute requirements while leveraging Cohere's embedding performance for accurate reranking. Cohere rerank is system agnostic, meaning it can be used with any vector search system, including PostgreSQL.</li><li><a href="https://cohere.com/command"><strong><u>Cohere Command</u></strong></a>: A family of highly scalable language models optimized for enterprise use. Command supports RAG, multi-language use, tool use, and citations. Command R+ is the most advanced model, as it’s optimized for conversational interaction and long-context tasks. Command R is well suited for simpler RAG and single-step tool use tasks and is the most cost-effective choice out of the Command family.&nbsp;</li></ul><div class="kg-card kg-callout-card kg-callout-card-purple"><div class="kg-callout-emoji">🎉</div><div class="kg-callout-text">Today, we’re thrilled to announce that PostgreSQL developers can now harness the power of Cohere's enterprise-grade language models directly on PostgreSQL data using the pgai PostgreSQL extension.&nbsp;</div></div><h3 id="what-is-pgai">What is pgai?</h3><p><a href="https://github.com/timescale/pgai"><u>Pgai</u></a> is an open-source <a href="https://www.tigerdata.com/blog/top-8-postgresql-extensions" rel="noreferrer">PostgreSQL extension</a> that brings AI models closer to your data, simplifying tasks such as embedding creation, text classification, semantic search, and retrieval-augmented generation on data stored in PostgreSQL.</p><p><strong>Pgai supports the entire suite of Cohere Embed, Rerank, and Command models.</strong> This means that developers can now:</p><ul><li>Create embeddings using Cohere’s Embed model for data inside PostgreSQL tables, all without having pipe data out and back into the database.</li><li>Build hybrid search systems for higher quality results in search and RAG applications using Cohere Rerank, combining vector search in pgvector and PostgreSQL full-text search.</li><li>Perform tasks like classification, summarization, <a href="https://www.tigerdata.com/blog/automating-data-enrichment-in-postgresql-with-openai" rel="noreferrer">data enrichment</a>, and other reasoning tasks on data in PostgreSQL tables using Cohere Command models.</li><li>Build highly accurate RAG systems completely in SQL, leveraging the Cohere Embed, Rerank, and Command models altogether.</li></ul><p>We built pgai to <a href="https://timescale.ghost.io/blog/pgai-giving-postgresql-developers-ai-engineering-superpowers/"><u>give more PostgreSQL developers AI Engineering superpowers</u></a>. Pgai makes it easier for database developers familiar with PostgreSQL to become "AI engineers" by providing familiar SQL-based interfaces to AI functionalities like embedding, creating, and model usage.</p><p>While Command is the flagship model for building scalable, production-ready AI applications, <a href="https://www.tigerdata.com/blog/pgai-giving-postgresql-developers-ai-engineering-superpowers" rel="noreferrer">pgai</a> supports a variety of models in the Cohere lineup. This includes specialized embedding models that support over 100 languages, enabling developers to select the most suitable model for their specific enterprise use case. </p><p>Whether it's building internal AI knowledge assistants, security documentation AI, customer feedback analysis, or employee support systems, Cohere's models integrated with pgai and PostgreSQL offer powerful solutions.</p><p><strong>Getting started with Cohere models in pgai</strong></p><p>Ready to start building with Cohere's suite of powerful language models and PostgreSQL? Pgai is open source under the PostgreSQL License and is available for immediate use in your AI projects. You can find installation instructions on the pgai <a href="https://github.com/timescale/pgai/?ref=timescale.com"><u>GitHub repository</u></a>. You can also access pgai (alongside pgvector and <a href="https://timescale.ghost.io/blog/pgvector-is-now-as-fast-as-pinecone-at-75-less-cost/"><u>pgvectorscale</u></a>) on any database service on <a href="https://console.cloud.timescale.com/signup?ref=timescale.com"><u>Timescale’s Cloud PostgreSQL platform</u></a>. If you’re new to Timescale, you can get started with a <a href="https://console.cloud.timescale.com/signup?ref=timescale.com"><u>free cloud PostgreSQL database here</u></a>.</p><p>Once connected to your database, create the pgai extension by running:</p><pre><code class="language-postgresql">CREATE EXTENSION IF NOT EXISTS ai CASCADE;</code></pre><p><strong>Join the Postgres AI Community</strong></p><p>Have questions about using Cohere models with pgai? Join the <a href="https://discord.gg/KRdHVXAmkp?ref=timescale.com"><u>Postgres for AI Discord</u></a>,&nbsp; where you can share your projects, seek help, and collaborate with a community of peers.&nbsp; You can also <a href="https://github.com/timescale/pgai/issues?ref=timescale.com"><u>open an issue on the pgai GitHub</u></a> (and while you’re there, stars are always appreciated ⭐).</p><p>Next, let's explore the benefits of using Cohere models for building enterprise AI applications. And we’ll close with a real-world example of using Cohere models to build a hybrid search system, combining pgvector semantic search with PostgreSQL full-text search.&nbsp;</p><p>Let's dive in...</p><h2 id="why-use-cohere-models-for-rag-and-search-applications">Why Use Cohere Models for RAG and Search Applications?</h2><p>Cohere's suite of models offers several advantages for enterprise-grade RAG and search applications:</p><p><strong>High-quality results</strong>: Cohere models excel in understanding business language, providing relevant content, and tackling complex enterprise challenges.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXcxGJoF6tCFIRf-wLKvkqUKCRp7S7WekDS6qQuySntrZp68pBzl8VxVeIjelY54kcjpJd9wmBvBS9TjMdWkXAeceM6V40w42zsR7IbWlZCT1OOyePr0wehPrJCa0DxilDhcrbM4iXDMElXNxl8NYymFew?key=6JbOGbzT9OJGF_xgkuNFdg" class="kg-image" alt="" loading="lazy" width="1600" height="704"><figcaption><i><b><strong class="italic" style="white-space: pre-wrap;">Impact of Cohere Rerank:</strong></b></i><i><em class="italic" style="white-space: pre-wrap;"> Semi-structured retrieval accuracy based Recall@5 on TMDB-5k-Movies, WikiSQL, nq-tables, and Cohere annotated datasets (higher is better). Source: </em></i><a href="https://cohere.com/blog/rerank-3"><u><i><em class="italic underline" style="white-space: pre-wrap;">Cohere blog</em></i></u></a><i><em class="italic" style="white-space: pre-wrap;">.</em></i></figcaption></figure><p><strong>Multi-language support:</strong> With support for over 100 languages, Cohere models are ideal for global enterprises. For example, Command R+ is optimized to perform well in the following languages: English, French, Spanish, Italian, German, Brazilian Portuguese, Japanese, Korean, Simplified Chinese, and Arabic.&nbsp; Additionally, pre-training data has been included for the following 13 languages: Russian, Polish, Turkish, Vietnamese, Dutch, Czech, Indonesian, Ukrainian, Romanian, Greek, Hindi, Hebrew, and Persian.</p><p><strong>Flexible deployment:</strong> Cohere’s flexible deployment options allow businesses to bring Cohere's models to their own data, either on their servers or in private/commercial clouds, addressing critical security and compliance concerns. This flexibility, combined with Cohere's robust API and partnerships with major cloud providers like Amazon Web Services (through the Bedrock AI platform), ensures seamless integration and improved scalability for enterprise applications.</p><p><strong>Enterprise focus:</strong> Strategic partnerships with industry leaders like Fujitsu, Oracle, and McKinsey &amp; Company underscore Cohere's enterprise-centric approach.</p><p>Cohere models enable sophisticated enterprise AI applications such as:</p><ul><li>Investment research assistants</li><li>Support chatbots and Intelligent support copilots</li><li>Executive AI assistants</li><li>Document summarization tools</li><li>Knowledge and project staffing assistants</li><li>Regulatory compliance monitoring</li><li>Sentiment analysis for brand management</li><li>Research and development assistance</li></ul><p>Cohere's models offer a powerful, scalable, and easily implementable solution for enterprise search and RAG applications, balancing high accuracy with efficient performance.</p><h2 id="tutorial-build-search-and-rag-systems-on-postgresql-using-cohere-and-pgai">Tutorial: Build Search and RAG Systems on PostgreSQL Using Cohere and Pgai</h2><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXcDydTpU8JCBmeEgJZFg_NMrdqAUeWGAoM8bfvt_iSVgWHcaDZNYx-ePIOV3i1nLg2ht_NBAaMJd02A6gD4Ao2xtZ12AbzYzBa_jD2La5VaRJmdn6DlWEF7VzIBP_jI32CEdMhMo47uv41RXM1IzT-xyZhH?key=6JbOGbzT9OJGF_xgkuNFdg" class="kg-image" alt="" loading="lazy" width="1600" height="1048"><figcaption><i><em class="italic" style="white-space: pre-wrap;">Process diagram showing how pgai combines full text, semantic search</em></i><span style="white-space: pre-wrap;">,</span><i><em class="italic" style="white-space: pre-wrap;"> and Cohere’s models to find the most relevant results to a user query using hybrid search.</em></i></figcaption></figure><p>Let’s look at an example of why Cohere's support in pgai is such a game-changer. We’ll perform a hybrid search over a corpus of news articles, combining PostgreSQL full-text search with pgvector’s semantic search, leveraging Cohere Embed and Rerank models in the process.</p><p>Here's what we'll cover:</p><ul><li><strong>Setting up your environment: </strong>Creating a Python virtual environment, installing necessary libraries, and setting up a PostgreSQL database with the pgai extension.</li><li><strong>Preparing your data: </strong>Creating a table to store news articles and loading sample data from the CNN/Daily Mail news dataset.</li><li><strong>Generating vector embeddings using pgai:</strong> Using Cohere's Embed model to create vector representations of text.</li><li><strong>Implementing full text and semantic search capabilities:</strong> Setting up PostgreSQL's full-text search for keyword matching and creating a vector search index for semantic search.</li><li><strong>Performing searches:</strong> Executing keyword searches using PostgreSQL's full-text search and semantic searches on embeddings using pgvector <a href="https://www.tigerdata.com/learn/hnsw-vs-diskann" rel="noreferrer">HNSW</a>.</li><li><strong>Performing hybrid search: </strong>Combining keyword and semantic search results by using Cohere's Rerank model to improve result relevance.</li></ul><p>By the end of this tutorial, you'll have a powerful hybrid search system that provides highly relevant search results, leveraging both traditional keyword search and semantic search techniques.</p><h3 id="setting-up-your-environment">Setting up your environment</h3><p>To begin, we'll set up a Python virtual environment and install the necessary libraries. This ensures a clean, isolated environment for our project.</p><pre><code class="language-bash">mkdir cohere
cd cohere
python -m venv venv
source venv/bin/activate # On Windows, use `venv\Scripts\activate`

pip install datasets "psycopg[binary]" python-dotenv
</code></pre><p><br>This setup:</p><ul><li>Creates a new directory for our project</li><li>Sets up a Python virtual environment</li><li>Activates the virtual environment</li><li>Installs the required Python packages:<ul><li><code>datasets</code>: For loading our sample dataset</li><li><code>psycopg</code>: PostgreSQL adapter for Python</li><li><code>python-dotenv</code>: For managing environment variables</li></ul></li></ul><p>Next, create a <code>.env</code> file in your project directory to store your database connection string and Cohere API key:</p><pre><code class="language-bash">DB_URL=postgres://username:password@localhost:5432/your_database
COHERE_API_KEY=your_cohere_api_key_here
</code></pre><p>Replace the placeholders with your actual database credentials and Cohere API key.</p><h3 id="setting-up-your-postgresql-database">Setting up your PostgreSQL database</h3><p>Now, let's set up our PostgreSQL database with the necessary extensions and table structure. We’ll create a table to hold our news articles. The <code>embedding</code> column will hold our embeddings which we’ll create using the Cohere Embed model. The <code>tsv</code> column will hold our full-text-search vectors.</p><p>Connect to your PostgreSQL database and run the following SQL commands:</p><pre><code class="language-postgresql">-- create the pgai extension
create extension if not exists ai cascade;

-- create a table for the news articles
-- the embedding column will store embeddings from cohere
-- the tsv column will store the full-text-search vector
create table cnn_daily_mail
( id bigint not null primary key generated by default as identity
, highlights text
, article text
, embedding vector(1024)
, tsv tsvector generated always as (to_tsvector('english', article)) stored
);

-- index the full-text-search vector
create index on cnn_daily_mail using gin (tsv);
</code></pre><p>This SQL script:</p><ul><li>Creates the <code>pgai</code> extension</li><li>Sets up a table to store our news articles, including columns for the article text, embeddings, and a full-text <a href="https://www.tigerdata.com/learn/understanding-vector-search" rel="noreferrer">search vector</a></li><li>Creates an index on the full-text search vector for efficient keyword searches</li></ul><h3 id="load-the-dataset">Load the dataset</h3><p>Now that our database is set up, we'll load sample data from the CNN/Daily Mail dataset. We'll use Python to download the dataset and efficiently insert it into our PostgreSQL table using the <code>copy</code> command with the <code>binary</code> format.</p><p>Create a new file called <code>load_data.py</code> in your project directory and add the following code:</p><pre><code class="language-python">import os
from dotenv import load_dotenv
from datasets import load_dataset
import psycopg


# Load environment variables
load_dotenv()
DB_URL = os.environ["DB_URL"]
COHERE_API_KEY = os.environ["COHERE_API_KEY"]

# Load and prepare the dataset
dataset = load_dataset("cnn_dailymail", "3.0.0")
test = dataset["test"]
test = test.shuffle(seed=42).select(range(0,1000))

# Insert data into PostgreSQL
with psycopg.connect(DB_URL) as con:
   with con.cursor(binary=True) as cur:
       with cur.copy("copy public.cnn_daily_mail (highlights, article) from stdin (format binary)") as cpy:
           cpy.set_types(['text', 'text'])
           for row in test:
               cpy.write_row((row["highlights"], row["article"]))

print("Data loading complete!")
</code></pre><p>This script:</p><ol><li>Loads environment variables from the .env file</li><li>Downloads the CNN/Daily Mail dataset</li><li>Selects a random subset of 1,000 articles from the test set</li><li>Efficiently inserts the data into our PostgreSQL table using the binary COPY command</li></ol><p>To run the script and load the data:</p><pre><code class="language-bash">python load_data.py</code></pre><p>This approach is much faster than inserting rows one by one, especially for larger datasets. After running the script, you should have 1000 news articles loaded into your cnn_daily_mail table, ready for the next steps in our tutorial.</p><p><strong>Note</strong>: Before running this script, make sure your .env file is properly set up with the correct database URL.</p><h3 id="create-vector-embeddings-with-cohere-embed-and-pgai"><br>Create vector embeddings with Cohere Embed and pgai</h3><p>In this step, we'll use Cohere's Embed model to generate <a href="https://www.tigerdata.com/blog/a-beginners-guide-to-vector-embeddings" rel="noreferrer">vector embeddings</a> for our news articles. These embeddings will enable semantic search capabilities.</p><p>First, set your Cohere API key in your PostgreSQL session:</p><pre><code class="language-postgresql">select set_config('ai.cohere_api_key', '&lt;YOUR-API-KEY&gt;', false) is not null as set_cohere_api_key</code></pre><p>The pgai extension will use this key by default for the duration of your database session.</p><p>We'll explore two methods to generate embeddings: a simple approach and a more production-ready solution. Choose method 1 if you’re just experimenting and method 2 if you want to see how you’d run this in production.<br></p><p><strong>Method 1: Easy mode (for small datasets)</strong></p><p>This is the "easy-mode" approach to generating embeddings for each news article.&nbsp;</p><pre><code class="language-postgresql">-- this is the easy way to create the embeddings
-- but it's one LONG-running update statement
update cnn_daily_mail set embedding = ai.cohere_embed('embed-english-v3.0', article, input_type=&gt;'search_document');
</code></pre><p>This method is straightforward but can be slow for large datasets. The downside to this is that each call to Cohere is relatively slow. This makes the whole statement long-running.</p><p><br><strong>Method 2: Production mode (recommended for larger datasets)</strong></p><p>This method processes rows one at a time, allowing for better concurrency and error handling. We select and lock only a single row at a time, and we commit our work as we go along.</p><pre><code class="language-postgresql">-- this is a more production-appropriate way to generate the embeddings
-- we only lock one row at a time and we commit each row immediately
do $$
declare
    _id bigint;
    _article text;
    _embedding vector(1024);
begin
    loop
        select id, article into _id, _article
        from cnn_daily_mail
        where embedding is null
        for update skip locked
        limit 1;

        if not found then
            exit;
        end if;

        _embedding = ai.cohere_embed('embed-english-v3.0', _article, input_type=&gt;'search_document');
        update cnn_daily_mail set embedding = _embedding where id = _id;
        commit;
    end loop;
end;
$$;
</code></pre><p>Note that both methods above use the <a href="https://github.com/timescale/pgai/blob/main/docs/cohere.md#cohere_embed"><u>cohere_embed() function</u></a> provided by the pgai extension to generate embeddings for each article in the cnn_daily_mail table using the Cohere <a href="https://docs.cohere.com/docs/cohere-embed" rel="noreferrer">Embed v3 embedding model</a>.</p><p>After running one of the above methods, verify that all rows have embeddings:</p><pre><code class="language-postgresql">-- this should return 0
select count(*) from cnn_daily_mail where embedding is null;
</code></pre><p><strong>Create a vector search index</strong></p><p>To optimize vector similarity searches, create an index on the embedding column:</p><pre><code class="language-postgresql">-- index the embeddings
create index on cnn_daily_mail using hnsw (embedding vector_cosine_ops);
</code></pre><p>This index uses the pgvector <a href="https://www.tigerdata.com/blog/vector-database-basics-hnsw" rel="noreferrer">HNSW</a> (<a href="https://www.tigerdata.com/blog/vector-database-basics-hnsw" rel="noreferrer">hierarchical navigable small world</a>) algorithm, which is efficient for nearest-neighbor searches in high-dimensional spaces.</p><p>With these steps completed, your database is now set up for both keyword-based and semantic searches. In the next section, we'll explore how to perform these searches using <a href="https://www.tigerdata.com/blog/pgai-giving-postgresql-developers-ai-engineering-superpowers" rel="noreferrer">pgai</a> and Cohere's models.</p><h2 id="perform-a-keyword-search-in-postgresql"><br>Perform a Keyword Search in PostgreSQL</h2><div class="kg-card kg-callout-card kg-callout-card-purple"><div class="kg-callout-emoji">⚠️</div><div class="kg-callout-text">Note to reader: Unfortunately, the news dataset is quite depressing, so please forgive the negativity in the keywords and prompts below. All terms are chosen purely for illustrative and educational purposes.</div></div><p>PostgreSQL's full-text search capabilities allow us to perform complex keyword searches efficiently. We'll use the <code>tsv</code> column we created earlier, which contains the tsvector representation of each article.</p><pre><code class="language-postgresql">-- a full-text-search
-- must include "death" OR "kill"
-- must include "police" AND "car" AND "dog"
select article
from cnn_daily_mail
where article @@ to_tsquery('english', '(death | kill) &amp; police &amp; car &amp; dog')
;
</code></pre><p>Let's break down this query:</p><ul><li><code>@@</code> is the text search match operator in PostgreSQL.</li><li><code>to_tsquery('english', '...')</code> converts our search terms into a tsquery type.</li><li>The search logic is: (death OR kill) AND police AND car AND dog.</li></ul><p>This query will return articles that contain:</p><ol><li>Either "death" or "kill" (or their variations)</li><li>AND "police" (or variations)</li><li>AND "car" (or variations)</li><li>AND "dog" (or variations)</li></ol><p>This method of searching is fast and efficient, especially for exact keyword matches inside the article text. However, it doesn't capture semantic meaning or handle synonyms well. In the next section, we'll explore how to perform semantic searches using the embeddings we created earlier.</p><h2 id="perform-a-semantic-search-in-postgresql-with-pgvector-pgai-and-cohere-embed">Perform a Semantic Search in PostgreSQL With pgvector, pgai, and Cohere Embed </h2><p>Semantic search allows us to find relevant articles based on the meaning of a query rather than just matching keywords. We'll use Cohere's Embed model to convert our search query into a <a href="https://www.tigerdata.com/blog/a-beginners-guide-to-vector-embeddings" rel="noreferrer">vector embedding</a>, then find the most similar article embeddings using pgvector.</p><pre><code class="language-postgresql">-- get the 15 most relevant stories to our question
with q as
(
   select ai.cohere_embed
   ('embed-english-v3.0'
   , 'Show me stories about police reports of deadly happenings involving cars and dogs.'
   , input_type=&gt;'search_query'
   ) as q
)
select article
from cnn_daily_mail
order by embedding &lt;=&gt; (select q from q limit 1)
limit 15
;
</code></pre><p>Let's break this query down:</p><ol><li>We use a <a href="https://www.timescale.com/learn/how-to-use-common-table-expression-sql" rel="noreferrer">CTE (Common Table Expression) </a>named <code>q</code> to generate the embedding for our search query using Cohere's model (also named <code>q</code>).</li><li><code>ai.cohere_embed()</code> is a function provided by pgai that interfaces with Cohere's API to create embeddings. See more in the <a href="https://github.com/timescale/pgai/blob/main/docs/cohere.md#cohere_embed" rel="noreferrer">pgai docs</a>.</li><li>We specify <code>input_type =&gt; 'search_query'</code> to optimize the embedding for search queries.</li><li>In the main query, we order the results by the cosine distance (<code>&lt;=&gt;</code>) between each article's embedding and our query embedding.</li><li>The <code>LIMIT 15</code> clause returns the top 15 most semantically similar articles.</li></ol><p>This semantic search can find relevant articles even if they don't contain the exact words used in the query. It understands the context and meaning of the search query and matches it with semantically similar content.</p><h2 id="performing-hybrid-search-using-pgai-and-cohere-rerank">Performing Hybrid Search Using Pgai and Cohere Rerank&nbsp;</h2><p>Hybrid search combines the strengths of both keyword and semantic searches and then uses Cohere's Rerank model to improve the relevance of the results further. This approach can provide more accurate and contextually relevant results than either method alone.</p><p>Here's how to perform a hybrid search with reranking:</p><pre><code class="language-postgresql">with full_text_search as
(
   select article
   from cnn_daily_mail
   where article @@ to_tsquery('english', '(death | kill) &amp; police &amp; car &amp; dog')
   limit 15
)
, vector_query as
(
   select ai.cohere_embed
   ('embed-english-v3.0'
   , 'Show me stories about police reports of deadly happenings involving cars and dogs.'
   , input_type=&gt;'search_query'
   ) as query_embedding
)
, vector_search as
(
   select article
   from cnn_daily_mail
   order by embedding &lt;=&gt; (select query_embedding from vector_query limit 1)
   limit 15
)
, rerank as
(
   select ai.cohere_rerank
   ( 'rerank-english-v3.0'
   , 'Show me stories about police reports of deadly happenings involving cars and dogs.'
   , (
       select jsonb_agg(x.article)
       from
       (
           select *
           from full_text_search
           union
           select * from vector_search
       ) x
     )
   , top_n =&gt; 5
   , return_documents =&gt; true
   ) as response
)
select
  x.index
, x.document-&gt;&gt;'text' as article
, x.relevance_score
from rerank
cross join lateral jsonb_to_recordset(rerank.response-&gt;'results') x(document jsonb, index int, relevance_score float8)
order by relevance_score desc
;
</code></pre><p>Let's break down this query:</p><ol><li><code>full_text_search</code>: Performs a keyword search using PostgreSQL's full-text search capabilities.</li><li><code>vector_query</code>: Creates an embedding for our search query using Cohere's Embed model.</li><li><code>vector_search</code>: Performs a semantic search using the query embedding.</li><li><code>combined_results</code>: Combines the results from both keyword and semantic searches.</li><li><code>reranked_results</code>: Uses Cohere's Rerank model to reorder the combined results based on relevance to the query.</li><li>The final <code>SELECT</code> statement extracts and formats the reranked results.</li></ol><p>This hybrid approach offers several advantages:</p><ul><li>It captures both exact keyword matches and semantically similar content.</li><li>The reranking step helps to prioritize the most relevant results.</li><li>It can handle cases where either keyword or semantic search alone might miss relevant articles.</li></ul><p>To use this query effectively:</p><ol><li>Adjust the <code>to_tsquery</code> parameters to match your specific keyword search needs.</li><li>Modify the natural language query in both the <code>ai.cohere_embed</code> and <code>ai.cohere_rerank</code> functions to match your search intent.</li><li>Experiment with the <code>LIMIT</code> values and the <code>top_n</code> parameter to balance between recall and precision.</li></ol><p>Remember, while this approach can provide highly relevant results, it does involve multiple API calls to Cohere (for embedding and reranking), which may impact performance for large result sets or high-volume applications.&nbsp;</p><p>This hybrid search method demonstrates the power of combining traditional database search techniques with advanced AI models, all within your PostgreSQL database using pgai.</p><h2 id="get-started-with-cohere-and-pgai-today">Get Started With Cohere and Pgai Today</h2><p>The integration of Cohere’s Command, Embed, and Rerank model into pgai marks a significant milestone in our vision to help PostgreSQL evolve into an <a href="https://timescale.ghost.io/blog/making-postgresql-a-better-ai-database/"><u>AI database</u></a>. By bringing these state-of-the-art language models directly into your database environment, you can unlock new levels of efficiency, intelligence, and innovation in your projects.</p><p>Pgai is open source under the PostgreSQL License and is available for you to use in your AI projects today. You can find installation instructions on the <a href="https://github.com/timescale/pgai/?ref=timescale.com"><u>pgai GitHub repository</u></a>. You can also access pgai on any database service on <a href="https://console.cloud.timescale.com/signup?ref=timescale.com"><u>Timescale’s cloud PostgreSQL platform</u></a>. If you’re new to Timescale, you can get started with a <a href="https://console.cloud.timescale.com/signup?ref=timescale.com"><u>free cloud PostgreSQL database here</u></a>.<br></p><p>Pgai is an effort to enrich the PostgreSQL ecosystem for AI. If you’d like to help, here’s how you can get involved:</p><ul><li>Got questions about using Cohere models in pgai? Join the <a href="https://discord.gg/Q8TajvAPRN?ref=timescale.com"><u>Postgres for AI Discord</u></a>, a community of developers building AI applications with PostgreSQL. Share what you’re working on, and help or get helped by a community of peers.</li><li>Share the news with your friends and colleagues: Share our posts about pgai on <a href="https://x.com/TimescaleDB?ref=timescale.com"><u>X/Twitter</u></a>, <a href="https://www.linkedin.com/company/timescaledb/?ref=timescale.com"><u>LinkedIn</u></a>, and Threads. We promise to RT back.</li><li>Submit issues and feature requests: 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 pgai. Head over to the <a href="https://github.com/timescale/pgai/?ref=timescale.com"><u>pgai GitHub repo</u></a> to share your ideas.</li><li>Make a contribution: We welcome community contributions for pgai. Pgai is written in Python and PL/Python. Let us know which models you want to see supported, particularly for open-source embedding and generation models. See the <a href="https://github.com/timescale/pgai/blob/main/CONTRIBUTING.md?ref=timescale.com"><u>pgai GitHub for instructions to contribute</u></a>.</li><li>Offer the pgai extension on your PostgreSQL cloud: Pgai is an open-source project under the <a href="https://github.com/timescale/pgvectorscale/blob/main/LICENSE?ref=timescale.com"><u>PostgreSQL License</u></a>. We encourage you to offer pgai on your managed PostgreSQL database-as-a-service platform and can even help you spread the word. Get in touch via our <a href="https://www.timescale.com/contact?ref=timescale.com"><u>Contact Us form</u></a> and mention pgai to discuss further.</li></ul><p>We're excited to see what you'll build with PostgreSQL and pgai!</p><p><br></p><p><br><br></p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Refining Vector Search Queries With Time Filters in Pgvector: A Tutorial]]></title>
            <description><![CDATA[Learn how to do both time-based filtering and vector search—semantic similarity search to be more precise—in a single SQL query.]]></description>
            <link>https://www.tigerdata.com/blog/refining-vector-search-queries-with-time-filters-in-pgvector-a-tutorial</link>
            <guid isPermaLink="true">https://www.tigerdata.com/blog/refining-vector-search-queries-with-time-filters-in-pgvector-a-tutorial</guid>
            <category><![CDATA[AI]]></category>
            <category><![CDATA[PostgreSQL]]></category>
            <category><![CDATA[Engineering]]></category>
            <dc:creator><![CDATA[John Pruitt]]></dc:creator>
            <pubDate>Mon, 01 Apr 2024 15:15:31 GMT</pubDate>
            <media:content medium="image" href="https://timescale.ghost.io/blog/content/images/2024/04/refining-vector-queries-with-time-filters-in-pgvector.png">
            </media:content>
            <content:encoded><![CDATA[<p><a href="https://timescale.ghost.io/blog/postgresql-as-a-vector-database-create-store-and-query-openai-embeddings-with-pgvector/" rel="noreferrer">Vector databases have emerged as a powerful solution for working with high-dimensional data</a>. These databases store vectors and perform vector searches. A vector is an array of numbers representing a data point in a multi-dimensional space, and vector search finds the most similar vectors to a query vector in this space. Vector search capabilities are powerful in machine learning and artificial intelligence, where traditional text-based search falls short.</p><p>Refining vector search queries becomes increasingly essential as data grows in volume. This is where time filters come into play. Time-based filtering allows users to narrow search results based on temporal criteria, such as creation date, modification date, or any time-related attribute associated with the data. Whether finding the most recent documents or documents from a specific time range, time-based filtering provides a crucial capability that complements similarity search.</p><p>Not all vector databases support both vector search and time-based filtering. The ability to perform both in the database unlocks efficiencies not otherwise possible. PostgreSQL with the <a href="https://github.com/pgvector/pgvector"><u>pgvector</u></a> and <a href="https://github.com/timescale/timescaledb"><u>timescaledb</u></a> extensions provides a potent platform for combining vector search with time-based filtering using SQL. In this post, we will explore vector search, time-based search, and several approaches to how one might combine the two. We will evaluate the speed and efficiency of these options.</p><h2 id="vector-search-queries-with-time-filters-the-roadmap">Vector Search Queries With Time Filters: The Roadmap</h2><p>In this hands-on orientation, we will load a sample dataset into a table and run time-filtered vector search queries against it. We can understand how Postgres executes each vector search query by examining the query plans. Furthermore, we can evaluate the performance of each query.</p><p>Here’s an overview of what we’ll cover:</p><p><strong>Query plans: </strong><a href="#query-plans" rel="noreferrer"><u>First</u></a>, we will provide a brief description of query plans in Postgres. If you are a seasoned query tuner, skip ahead to the setup.</p><p><strong>Setup: </strong>In <a href="#the-setup" rel="noreferrer"><u>the second section</u></a>, we will do all our prep work. We will create a Timescale service, create a table, and load it with our sample dataset.</p><p><strong>Filtering documents by time: </strong>In <a href="#filtering-documents-by-time" rel="noreferrer"><u>the next section</u></a>, we will look at time-based filtering in isolation. We will explore the power of TimescaleDB’s hypertables (which automatically partition Postgres tables into smaller data partitions or chunks) and compare them to plain Postgres tables. Feel free to skip ahead if you are already familiar with hypertables and time-based filtering.</p><p><strong>Vector search:</strong> After looking at time-based filtering in isolation, we will look at vector search in isolation. <a href="#vector-search" rel="noreferrer"><u>This section</u></a> will introduce the features the <a href="https://github.com/pgvector/pgvector"><u>pgvector</u></a> and <a href="https://www.tigerdata.com/blog/pgai-giving-postgresql-developers-ai-engineering-superpowers" rel="noreferrer"><u>pgai on Timescale</u></a> extensions provide. You can safely jump past this if you know how to do vector search in Postgres.</p><p><strong>Vector search and querying by time: </strong>In <a href="#vector-search-and-filtering-by-time" rel="noreferrer"><u>this section</u></a>, we will learn how to do both time-based filtering and similarity search in a single SQL query. (When the vectors contain a large language model’s embedding of textual content, vector search is a search of semantic similarity.) We will play with a few different versions of the query and see how Postgres decides to execute them.</p><p><strong>Using vector search and time filters in AI applications: </strong>Once we’ve covered the techniques for combining vector similarity search and time filters, we’ll briefly explore <a href="#using-vector-search-and-time-filters-in-ai-applications" rel="noreferrer">how to apply what we learned to build GenAI applications</a>.</p><h2 id="query-plans">Query Plans</h2><p>When discussing query tuning in PostgreSQL, an essential concept to grasp is a "query plan." <a href="https://www.postgresql.org/docs/current/using-explain.html"><u>A query plan is a set of steps</u></a> the PostgreSQL query planner generates describing how PostgreSQL will execute a SQL query. Various factors, including the query structure, the database schema, and the statistical information about the data distribution in the tables, influence the chosen query plan.</p><p>To evaluate a query plan in PostgreSQL, you can use the <a href="https://www.postgresql.org/docs/current/sql-explain.html"><u><code>EXPLAIN</code></u></a> command followed by your query. This command returns the planner's chosen execution plan without executing the query. It will show the plan's overall cost and each step's cost. Cost is a unitless measure but can be used to compare the relative performance of one query to another. For a more detailed analysis, including the execution times, you can use <code>EXPLAIN ANALYZE</code>, which executes the query. However, be cautious with <code>EXPLAIN ANALYZE</code> on production systems, as it can affect performance.&nbsp;</p><p>Don’t worry! We won’t get too bogged down in the minutia of query plans. We will use <code>EXPLAIN ANALYZE</code> to compare the speed and efficiency of various queries, but we will keep it light.</p><p><strong>Note</strong>: Because many variables influence query plans, you may see query plans that are not exactly like the ones depicted; however, they should be similar enough to draw the same conclusions.</p><p><strong>Another note</strong>: If you want to see a visual representation of query plans, there is an excellent, free tool for this at <a href="https://explain.dalibo.com/"><u>https://explain.dalibo.com/</u></a></p><h2 id="the-setup">The Setup</h2><p>We will load a table with a sample dataset and run queries against it to explore time-based filtering and <a href="https://www.tigerdata.com/learn/vector-search-vs-semantic-search" rel="noreferrer">semantic search</a>. We will use a modified version of the Cohere <a href="https://huggingface.co/datasets/Cohere/wikipedia-22-12-simple-embeddings"><u>wikipedia-22-12-simple-embeddings</u></a> dataset hosted on <a href="https://huggingface.co/"><u>Huggingface</u></a>. It contains embeddings of <a href="https://simple.wikipedia.org/wiki/Main_Page"><u>Simple English Wikipedia</u></a> entries. </p><p>We added synthetic data: a time column, category, and tags. We loaded the data into a Postgres table and exported it to a CSV file; therefore, the format has changed. The original dataset on Huggingface is available under the Apache 2.0 license, and thus, our modified version is also subject to the Apache 2.0 license.</p><p>You will need a database if you want to follow along. Head to the <a href="https://console.cloud.timescale.com/signup?utm_campaign=vectorlaunch&amp;utm_source=timescale-blog&amp;utm_medium=direct&amp;utm_content=filtering-blog"><u>Timescale console</u></a> (you can try pgai on Timescale for free with a 90-day extended trial) and create a “<a href="https://www.tigerdata.com/blog/time-series-introduction" rel="noreferrer">Time Series</a> and Analytics” service. Building vector indexes can be a compute-hungry activity, so choose 2 CPU / 8 GiB Memory compute. Choose a region close to you for the best experience.</p><figure class="kg-card kg-image-card"><img src="https://timescale.ghost.io/blog/content/images/2024/10/Refining-Vector-Search-Queries-With-Time-Filters-in-Pgvector_Timescale-UI.png" class="kg-image" alt="The Configure your service page in the Timescale Cloud UI" loading="lazy" width="645" height="759" srcset="https://timescale.ghost.io/blog/content/images/size/w600/2024/10/Refining-Vector-Search-Queries-With-Time-Filters-in-Pgvector_Timescale-UI.png 600w, https://timescale.ghost.io/blog/content/images/2024/10/Refining-Vector-Search-Queries-With-Time-Filters-in-Pgvector_Timescale-UI.png 645w"></figure><p>We need some sample data to load into our database. The dataset we will use is <a href="https://huggingface.co/datasets/timescale/wikipedia-22-12-simple-embeddings"><u>available</u></a> on Huggingface. <a href="https://huggingface.co/datasets/timescale/wikipedia-22-12-simple-embeddings/resolve/main/wiki.csv?download=true"><u>Use this link to download the CSV file</u></a>.</p><p>Make sure you have the <a href="https:/www.tigerdata.com/blog/how-to-install-psql-on-mac-ubuntu-debian-windows/" rel="noreferrer"><u><code>psql</code> client installed locally</u></a>. You will need <code>psql</code> version 16 or greater. Run <code>psql --version</code> to see what version you have.</p><p>Connect to your Timescale service using the <code>psql</code> client. Pass the connection URL as an argument. Make sure the <code>wiki.csv</code> file you downloaded is in the same directory where you launch <code>psql</code>.</p><pre><code class="language-bash">psql "&lt;connection url&gt;"</code></pre><p>We will use three extensions, including timescaledb, pgvector, and timescale_vector. We need to ensure these extensions exist before creating our table.</p><pre><code class="language-PostgreSQL">create extension if not exists timescaledb;
create extension if not exists vector;
create extension if not exists timescale_vector;</code></pre><p>Run the following statement to create a table named <code>wiki</code>. The <code>embedding</code>column is of type <code>vector(768)</code> and will contain our <a href="https://platform.openai.com/docs/guides/embeddings/what-are-embeddings?ref=timescale.com"><u>embeddings</u></a>. The <a href="https://www.tigerdata.com/learn/postgresql-extensions-pgvector" rel="noreferrer">pgvector extension</a> provides the <code>vector</code> type.</p><pre><code class="language-SQL">create table public.wiki
( id int not null
, time timestamptz not null
, contents text
, meta jsonb
, embedding vector(768)
);</code></pre><p>Next, run the following command to convert the plain Postgres table into a TimescaleDB hypertable. The hypertable divides the dataset into day-sized sub-tables called chunks based on the values in the <code>time</code> column.</p><pre><code class="language-SQL">select create_hypertable
( 'public.wiki'::regclass
, 'time'::name
, chunk_time_interval=&gt;'1d'::interval
);</code></pre><p>Now that we have a place to put our data, we can load our data. Run this meta-command to load the data from the CSV file into the <code>wiki</code> table. Expect this to take a few minutes.</p><pre><code class="language-SQL">\copy wiki from 'wiki.csv' with (format csv, header on)
</code></pre><p>Make sure Postgres has accurate statistics for our new table. Statistics will influence the query plans.</p><pre><code class="language-SQL">analyze wiki;</code></pre><p>You should find 485,859 rows in the <code>wiki</code> table if you have done this correctly.</p><pre><code class="language-SQL">select count(*) from wiki;</code></pre><p>Finally, let’s turn off parallelism to keep our query plans simple.</p><pre><code class="language-SQL">set max_parallel_workers_per_gather = 0;</code></pre><p>You are ready to go!</p><h2 id="filtering-documents-by-time">Filtering Documents by Time</h2><p>Our <code>wiki</code> table contains a <code>timestamptz</code> column named <code>time</code>. The data in this column is fake. We have added these timestamps so that we have something to illustrate filtering by time. You would use the timestamp associated with a web page or document in real-world use.</p><p>We will use the power of TimescaleDB's <a href="https://docs.timescale.com/use-timescale/latest/hypertables/about-hypertables/"><u>hypertables</u></a> to facilitate filtering by time. A hypertable is a logical table broken up into smaller physical tables called chunks. <a href="https://docs.timescale.com/use-timescale/latest/hypertables/change-chunk-intervals/"><u>Each chunk contains data falling within a time range, and the time ranges of the chunks do not overlap.</u></a> If you are familiar with <a href="https://www.postgresql.org/docs/current/ddl-partitioning.html"><u>partitioning</u></a> or <a href="https://www.postgresql.org/docs/current/ddl-inherit.html"><u>inheritance</u></a> in Postgres, you can think of hypertables in these terms.</p><p>Why is this helpful? When querying a hypertable and filtering on a given time range, TimescaleDB can do chunk exclusion: it ignores any chunks whose time ranges fall outside the bounds of the time range filter, and Postgres expends no compute resources to process the rows in the excluded chunks. Eliminating large chunks of the table through chunk exclusion speeds up time-filtered search.</p><p>There are 485,859 rows in the dataset. As we loaded the data, we assigned a date to each row and incremented the date each time we loaded 50,000 rows, giving us 10 chunks. Nine chunks have 50,000 rows, and the last chunk has 35,859. Below are the time range bounds and row counts for the 10 chunks.</p><pre><code class="language-text">┌─────────────┬────────────┬───────┐
│ range_start │ range_end  │ count │
├─────────────┼────────────┼───────┤
│ 2000-01-01  │ 2000-01-02 │ 50000 │
│ 2000-01-02  │ 2000-01-03 │ 50000 │
│ 2000-01-03  │ 2000-01-04 │ 50000 │
│ 2000-01-04  │ 2000-01-05 │ 50000 │
│ 2000-01-05  │ 2000-01-06 │ 50000 │
│ 2000-01-06  │ 2000-01-07 │ 50000 │
│ 2000-01-07  │ 2000-01-08 │ 50000 │
│ 2000-01-08  │ 2000-01-09 │ 50000 │
│ 2000-01-09  │ 2000-01-10 │ 50000 │
│ 2000-01-10  │ 2000-01-11 │ 35859 │
└─────────────┴────────────┴───────┘
(10 rows)</code></pre><p>The query below filters the <code>wiki</code> table on a time range. We use <code>explain analyze</code> to get Postgres to run the query and tell us what it did.</p><pre><code class="language-SQL">explain analyze
select id
from wiki
where '2000-01-04'::timestamptz &lt;= time and time &lt; '2000-01-06'::timestamptz
;</code></pre><p>The query plan shows that Postgres did a sequential scan on two chunks of the hypertable. Postgres returned 100,000 rows in total. Postgres excluded the other eight chunks and did not have to expend resources to process the rows in those chunks. Again, chunk exclusion is a hypertable’s superpower; it allows for faster and more efficient queries when filtering on a time range.</p><pre><code class="language-text">QUERY PLAN
Append&nbsp; (cost=0.00..10064.00 rows=100000 width=4) (actual time=0.005..30.934 rows=100000 loops=1)
&nbsp;&nbsp;-&gt;&nbsp; Seq Scan on _hyper_1_4_chunk&nbsp; (cost=0.00..4846.00 rows=50000 width=4) (actual time=0.005..12.453 rows=50000 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Filter: (('2000-01-04 00:00:00+00'::timestamp with time zone &lt;= "time") AND ("time" &lt; '2000-01-06 00:00:00+00'::timestamp with time zone))
&nbsp;&nbsp;-&gt;&nbsp; Seq Scan on _hyper_1_5_chunk&nbsp; (cost=0.00..4718.00 rows=50000 width=4) (actual time=0.006..12.622 rows=50000 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Filter: (('2000-01-04 00:00:00+00'::timestamp with time zone &lt;= "time") AND ("time" &lt; '2000-01-06 00:00:00+00'::timestamp with time zone))
Planning Time: 0.197 ms
Execution Time: 33.925 ms</code></pre><p>To illustrate the savings associated with the chunk exclusion, we can create a <code>wiki2</code> table exactly like <code>wiki</code> except <code>wiki2</code> is a plain table—not a hypertable.</p><p><strong>Note</strong>: the insert will take a minute or two. Be patient.</p><pre><code class="language-SQL">create table public.wiki2
( id int not null
, time timestamptz not null
, contents text
, meta jsonb
, embedding vector(768)
);

insert into wiki2 select * from wiki;

analyze wiki2;

explain analyze
select id
from wiki2
where '2000-01-04'::timestamptz &lt;= time and time &lt; '2000-01-06'::timestamptz
;</code></pre><p>Running the same query against <code>wiki2</code> gives us the following query plan. Using the plain table is more expensive and slower than using the hypertable. In this case, Postgres had to “look at” all 485,859 rows in the table to find the 100,000 that match the time filter.&nbsp;&nbsp;</p><pre><code class="language-text">QUERY PLAN
Seq Scan on wiki2&nbsp; (cost=0.00..44853.14 rows=102261 width=4) (actual time=31.259..105.586 rows=100000 loops=1)
&nbsp;&nbsp;Filter: (('2000-01-04 00:00:00+00'::timestamp with time zone &lt;= "time") AND ("time" &lt; '2000-01-06 00:00:00+00'::timestamp with time zone))
&nbsp;&nbsp;Rows Removed by Filter: 385859
Planning Time: 0.631 ms
Execution Time: 108.655 ms</code></pre><p>You may say this is not a fair comparison because any reasonable developer would put an index on <code>time</code>. Let's try that.</p><pre><code class="language-SQL">create index on wiki2 (time);</code></pre><pre><code class="language-text">QUERY PLAN
Index Scan using wiki2_time_idx on wiki2&nbsp; (cost=0.42..10049.55 rows=102257 width=4) (actual time=0.013..26.817 rows=100000 loops=1)
&nbsp;&nbsp;Index Cond: (("time" &gt;= '2000-01-04 00:00:00+00'::timestamp with time zone) AND ("time" &lt; '2000-01-06 00:00:00+00'::timestamp with time zone))
Planning Time: 0.086 ms
Execution Time: 29.917 ms</code></pre><pre><code class="language-text">┌────────────┬──────────┬────────────┐
│&nbsp; &nbsp; type&nbsp; &nbsp; │ &nbsp; cost &nbsp; │&nbsp; &nbsp; time&nbsp; &nbsp; │
├────────────┼──────────┼────────────┤
│ hypertable │ 10064.00 │&nbsp; 33.925 ms │
│ plain&nbsp; &nbsp; &nbsp; │ 44853.14 │ 108.655 ms │
│ indexed&nbsp; &nbsp; │ 10049.55 │&nbsp; 29.917 ms │
└────────────┴──────────┴────────────┘</code></pre><p>At least for this dataset on this machine type, an indexed plain table is slightly cheaper and faster than a hypertable. So, why would we use a hypertable? There are a couple of reasons. Firstly, as this dataset gets larger, it becomes more likely that the hypertable will outperform the plain table. Secondly, Postgres will use only one index per table per query in most situations. We want to use a hypertable because we are not finished filtering, and we want to use a different index altogether.</p><h2 id="vector-search">Vector Search</h2><p>As a reminder, we ultimately want to combine time filtering and vector search. Let's explore vector search in isolation first. If you are already a confident user of similarity search with pgvector, feel free to <a href="https://www.tigerdata.com/blog/refining-vector-search-queries-with-time-filters-in-pgvector-a-tutorial/#vector-search-and-filtering-by-time" rel="noreferrer"><u>jump ahead</u></a>.</p><p>Our <code>wiki</code> table has a column named <code>embedding</code> of type <code>vector(768)</code>. The pgvector extension provides the <code>vector</code> datatype. This column contains the embeddings of the wiki content.</p><p>We will grab a random vector to use as a search parameter. The <code>psql</code> client makes this easy. We can select the <code>embedding</code> column from a row and use the <a href="https://www.postgresql.org/docs/current/app-psql.html#APP-PSQL-META-COMMAND-GSET"><u><code>\gset</code> meta-command</u></a> to create a variable in the psql client named <code>emb</code> containing the vector value. We can use this variable as a parameter for future queries. In this case, we will use the vector from the row with id 65272. In subsequent queries, we will search for rows where the embedding is semantically similar to the row with id 65272.</p><pre><code class="language-SQL">select embedding as emb
from wiki
where id = 65272
\gset</code></pre><p>Next, we query the <code>wiki</code> table and use <a href="https://docs.timescale.com/ai/latest/key-vector-database-concepts-for-understanding-pgvector/#vector-distance-types"><u>the <code>&lt;=&gt;</code> (cosine) distance operator </u></a>provided by pgvector and timescale_vector. This operator computes the distance between two vectors, and this distance is a representation of how semantically similar the two vectors are. If we order by the similarity distance and limit the results to 10, our query will return the 10 rows most semantically similar to the content from row 65272.</p><p><strong>Note</strong>: The <code>$1</code> in the query below denotes a query parameter, and the <a href="https://www.postgresql.org/docs/current/app-psql.html#APP-PSQL-META-COMMAND-BIND"><u><code>\bind</code> meta-command</u></a> assigns the value from our <code>emb</code> psql variable to this parameter.</p><pre><code class="language-SQL">select id, embedding &lt;=&gt; $1::vector as dist
from wiki
order by dist
limit 10
\bind :emb
;</code></pre><pre><code class="language-text">┌────────┬─────────────────────┐
│ &nbsp; id &nbsp; │&nbsp; &nbsp; &nbsp; &nbsp; dist &nbsp; &nbsp; &nbsp; &nbsp; │
├────────┼─────────────────────┤
│&nbsp; 65272 │ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 0 │
│&nbsp; 16295 │&nbsp; 0.1115766717013853 │
│&nbsp; 16302 │ 0.11360477414644143 │
│&nbsp; 16297 │ 0.12387882580027065 │
│&nbsp; 16298 │&nbsp; 0.1299166956232466 │
│&nbsp; 34598 │ 0.13121629452715455 │
│&nbsp; 16292 │ 0.13307570937651947 │
│&nbsp; 61078 │ 0.13427528064358674 │
│ 215163 │ 0.13759063950532036 │
│&nbsp; 79614 │ 0.13770102915039784 │
└────────┴─────────────────────┘
(10 rows)</code></pre><p>As expected, row 65272 is the top result with exactly the vector we borrowed for the query. Nine more results follow with increasing distance.</p><p>Looking at the query plan for this, we find that Postgres scanned the entire table and performed the distance calculation on the fly for each of the 485,859 rows. This execution took 3,441 milliseconds, considerably longer than any time-based filtering approaches we had previously explored.</p><pre><code class="language-SQL">explain analyze
select id, embedding &lt;=&gt; $1::vector as dist
from wiki
order by dist
limit 10
\bind :emb
;</code></pre><p></p><pre><code class="language-text">QUERY PLAN
Limit&nbsp; (cost=62004.36..62004.39 rows=10 width=12) (actual time=3440.996..3441.002 rows=10 loops=1)
&nbsp;&nbsp;-&gt;&nbsp; Sort&nbsp; (cost=62004.36..63219.01 rows=485859 width=12) (actual time=3440.994..3441.000 rows=10 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Sort Key: ((_hyper_1_1_chunk.embedding &lt;=&gt; '[...]'::vector))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Sort Method: top-N heapsort&nbsp; Memory: 25kB
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Result&nbsp; (cost=0.00..51505.12 rows=485859 width=12) (actual time=0.030..3357.861 rows=485859 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Append&nbsp; (cost=0.00..45431.88 rows=485859 width=22) (actual time=0.015..329.158 rows=485859 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Seq Scan on _hyper_1_1_chunk&nbsp; (cost=0.00..4788.00 rows=50000 width=22) (actual time=0.015..30.596 rows=50000 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Seq Scan on _hyper_1_2_chunk&nbsp; (cost=0.00..4724.00 rows=50000 width=22) (actual time=0.013..31.533 rows=50000 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Seq Scan on _hyper_1_3_chunk&nbsp; (cost=0.00..4660.00 rows=50000 width=22) (actual time=0.012..31.167 rows=50000 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Seq Scan on _hyper_1_4_chunk&nbsp; (cost=0.00..4596.00 rows=50000 width=22) (actual time=0.012..30.917 rows=50000 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Seq Scan on _hyper_1_5_chunk&nbsp; (cost=0.00..4468.00 rows=50000 width=22) (actual time=0.013..29.304 rows=50000 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Seq Scan on _hyper_1_6_chunk&nbsp; (cost=0.00..4340.00 rows=50000 width=22) (actual time=0.012..28.831 rows=50000 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Seq Scan on _hyper_1_7_chunk&nbsp; (cost=0.00..4276.00 rows=50000 width=22) (actual time=0.012..29.065 rows=50000 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Seq Scan on _hyper_1_8_chunk&nbsp; (cost=0.00..4148.00 rows=50000 width=22) (actual time=0.013..28.186 rows=50000 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Seq Scan on _hyper_1_9_chunk&nbsp; (cost=0.00..3956.00 rows=50000 width=22) (actual time=0.012..27.490 rows=50000 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Seq Scan on _hyper_1_10_chunk&nbsp; (cost=0.00..3046.59 rows=35859 width=22) (actual time=0.011..20.635 rows=35859 loops=1)
Planning Time: 0.231 ms
Execution Time: 3441.034 ms</code></pre><p>Can we make vector search faster and cheaper? Yes, we can do it by <a href="https://docs.timescale.com/ai/latest/sql-interface-for-pgvector-and-timescale-vector/#indexing-the-vector-data-using-indexes-provided-by-pgvector-and-timescale-vector"><u>using an index</u></a>. The pgvector extension provides two types of indexes: <code>ivfflat</code> and <code>hnsw</code>. The <a href="https://timescale.ghost.io/blog/how-we-made-postgresql-the-best-vector-database/"><u>timescale_vector extension</u></a> provides a third: <code>tsv</code>. All three index types implement <a href="https://docs.timescale.com/ai/latest/key-vector-database-concepts-for-understanding-pgvector/#vector-search-indexing-approximate-nearest-neighbor-search"><u>approximate nearest-neighbor search algorithms</u></a>. None of them will produce exact results, but they will make our searches considerably more efficient. As the number of vectors grows past a certain scale, exact searches become impractical.</p><p>This tutorial will use the <code>tsv</code> <a href="https://docs.timescale.com/ai/latest/sql-interface-for-pgvector-and-timescale-vector/#timescale-vector-index"><u>index type and its default settings</u></a>.</p><p><strong>Note</strong>: Expect creating the index to take 10 minutes or more.</p><p></p><pre><code class="language-SQL">create index on wiki using tsv (embedding);</code></pre><p>You will note that we get slightly different results when we execute the same query. Without the index, Postgres performs an exhaustive and exact search. With the index, Postgres performs an approximate search. We trade some measure of accuracy for speed and efficiency. We can see how fast and efficient it is by looking at the query plan.</p><pre><code class="language-SQL">select id, embedding &lt;=&gt; $1::vector as dist
from wiki
order by dist
limit 10
\bind :emb
;</code></pre><pre><code class="language-text">┌───────┬─────────────────────┐
│&nbsp; id &nbsp; │&nbsp; &nbsp; &nbsp; &nbsp; dist &nbsp; &nbsp; &nbsp; &nbsp; │
├───────┼─────────────────────┤
│ 65272 │ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 0 │
│ 16302 │ 0.11360477414644143 │
│ 16295 │&nbsp; 0.1115766717013853 │
│ 16297 │ 0.12387882580027065 │
│ 16298 │&nbsp; 0.1299166956232466 │
│ 34598 │ 0.13121629452715455 │
│ 16292 │ 0.13307570937651947 │
│ 61078 │ 0.13427528064358674 │
│ 79614 │ 0.13770102915039784 │
│ 16307 │&nbsp; 0.1408329687219958 │
└───────┴─────────────────────┘
(10 rows)</code></pre><p>The query plan shows that the query cost 2720.81 and took 24.642 milliseconds to execute. The version of the query that did not use the index cost 62004.39 and took 3441.034 milliseconds. The index made a huge difference! Instead of doing a sequential scan on all the chunks, Postgres did an index scan on each chunk.</p><pre><code class="language-text">QUERY PLAN
Limit&nbsp; (cost=2719.51..2720.81 rows=10 width=12) (actual time=24.317..24.578 rows=10 loops=1)
&nbsp;&nbsp;-&gt;&nbsp; Result&nbsp; (cost=2719.51..66108.97 rows=485859 width=12) (actual time=24.316..24.575 rows=10 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Merge Append&nbsp; (cost=2719.51..60035.73 rows=485859 width=22) (actual time=24.315..24.572 rows=10 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Sort Key: ((_hyper_1_1_chunk.embedding &lt;=&gt; '[...]'::vector))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Index Scan using _hyper_1_1_chunk_wiki_embedding_idx on _hyper_1_1_chunk&nbsp; (cost=279.85..5496.65 rows=50000 width=22) (actual time=1.846..1.982 rows=7 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Order By: (embedding &lt;=&gt; '[...]'::vector)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Index Scan using _hyper_1_2_chunk_wiki_embedding_idx on _hyper_1_2_chunk&nbsp; (cost=279.85..5426.25 rows=50000 width=22) (actual time=2.247..2.361 rows=4 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Order By: (embedding &lt;=&gt; '[...]'::vector)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Index Scan using _hyper_1_3_chunk_wiki_embedding_idx on _hyper_1_3_chunk&nbsp; (cost=279.85..5355.85 rows=50000 width=22) (actual time=2.327..2.327 rows=1 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Order By: (embedding &lt;=&gt; '[...]'::vector)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Index Scan using _hyper_1_4_chunk_wiki_embedding_idx on _hyper_1_4_chunk&nbsp; (cost=279.85..5285.45 rows=50000 width=22) (actual time=2.265..2.265 rows=1 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Order By: (embedding &lt;=&gt; '[...]'::vector)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Index Scan using _hyper_1_5_chunk_wiki_embedding_idx on _hyper_1_5_chunk&nbsp; (cost=279.85..5144.65 rows=50000 width=22) (actual time=2.849..2.849 rows=1 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Order By: (embedding &lt;=&gt; '[...]'::vector)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Index Scan using _hyper_1_6_chunk_wiki_embedding_idx on _hyper_1_6_chunk&nbsp; (cost=279.85..5003.85 rows=50000 width=22) (actual time=2.625..2.625 rows=1 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Order By: (embedding &lt;=&gt; '[...]'::vector)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Index Scan using _hyper_1_7_chunk_wiki_embedding_idx on _hyper_1_7_chunk&nbsp; (cost=279.85..4933.45 rows=50000 width=22) (actual time=2.805..2.805 rows=1 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Order By: (embedding &lt;=&gt; '[...]'::vector)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Index Scan using _hyper_1_8_chunk_wiki_embedding_idx on _hyper_1_8_chunk&nbsp; (cost=279.85..4792.65 rows=50000 width=22) (actual time=2.496..2.497 rows=1 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Order By: (embedding &lt;=&gt; '[...]'::vector)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Index Scan using _hyper_1_9_chunk_wiki_embedding_idx on _hyper_1_9_chunk&nbsp; (cost=279.85..4581.45 rows=50000 width=22) (actual time=2.160..2.161 rows=1 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Order By: (embedding &lt;=&gt; '[...]'::vector)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Index Scan using _hyper_1_10_chunk_wiki_embedding_idx on _hyper_1_10_chunk&nbsp; (cost=200.69..3516.08 rows=35859 width=22) (actual time=2.689..2.689 rows=1 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Order By: (embedding &lt;=&gt; '[...]'::vector)
Planning Time: 0.288 ms
Execution Time: 24.642 ms
</code></pre><p>Notice that we got different results from the two queries. The version without the index had to calculate the exact distances on the fly—a big part of its slowness. The indexed version used an approximate nearest-neighbor algorithm. Its results were not exact, but they were delivered faster and cheaper.</p><h2 id="vector-search-and-filtering-by-time">Vector Search and Filtering by Time</h2><p>Now that we have explored filtering by time and vector search in isolation, can we combine the two in a single efficient query?</p><pre><code class="language-SQL">select id, embedding &lt;=&gt; $1::vector as dist
from wiki
where '2000-01-02'::timestamptz &lt;= time and time &lt; '2000-01-04'::timestamptz
order by dist
limit 10
\bind :emb
;</code></pre><p><strong>Note</strong>: Notice that our results are "worse" in terms of semantic similarity because our time filter has eliminated some of the most similar rows. This outcome is expected. Instead, we have more <em>relevant</em> rows as we excluded rows with high similarity outside our time period of interest.</p><pre><code class="language-text">┌────────┬─────────────────────┐
│ &nbsp; id &nbsp; │&nbsp; &nbsp; &nbsp; &nbsp; dist &nbsp; &nbsp; &nbsp; &nbsp; │
├────────┼─────────────────────┤
│&nbsp; 65272 │ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 0 │
│&nbsp; 61078 │ 0.13427528064358674 │
│&nbsp; 79614 │ 0.13770102915039784 │
│&nbsp; 79612 │ 0.14383225467703376 │
│&nbsp; 61080 │&nbsp; 0.1444315737261198 │
│&nbsp; 52634 │&nbsp; 0.1436081460507892 │
│&nbsp; 64473 │ 0.14383654656162292 │
│&nbsp; 61079 │&nbsp; 0.1474735568613179 │
│ 131920 │ 0.15066526846960182 │
│&nbsp; 64475 │ &nbsp; 0.154294958183531 │
└────────┴─────────────────────┘
(10 rows)</code></pre><p>The query plan reveals that Postgres only considers the two chunks that match the time filter and uses the <code>tsv</code> vector indexes associated with those chunks. We have the best of both worlds!</p><pre><code class="language-text">QUERY PLAN
Limit&nbsp; (cost=559.71..561.01 rows=10 width=12) (actual time=4.631..5.025 rows=10 loops=1)
&nbsp;&nbsp;-&gt;&nbsp; Result&nbsp; (cost=559.71..13532.11 rows=100000 width=12) (actual time=4.630..5.023 rows=10 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Merge Append&nbsp; (cost=559.71..12282.11 rows=100000 width=22) (actual time=4.628..5.019 rows=10 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Sort Key: ((_hyper_1_2_chunk.embedding &lt;=&gt; '[...]'::vector))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Index Scan using _hyper_1_2_chunk_wiki_embedding_idx on _hyper_1_2_chunk&nbsp; (cost=279.85..5676.25 rows=50000 width=22) (actual time=2.349..2.682 rows=9 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Order By: (embedding &lt;=&gt; '[...]'::vector)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Filter: (('2000-01-02 00:00:00+00'::timestamp with time zone &lt;= "time") AND ("time" &lt; '2000-01-04 00:00:00+00'::timestamp with time zone))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Index Scan using _hyper_1_3_chunk_wiki_embedding_idx on _hyper_1_3_chunk&nbsp; (cost=279.85..5605.85 rows=50000 width=22) (actual time=2.277..2.334 rows=2 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Order By: (embedding &lt;=&gt; '[...]'::vector)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Filter: (('2000-01-02 00:00:00+00'::timestamp with time zone &lt;= "time") AND ("time" &lt; '2000-01-04 00:00:00+00'::timestamp with time zone))
Planning Time: 0.257 ms
Execution Time: 5.056 ms</code></pre><p>Could we have achieved the same with the indexed plain table? Let's add a <code>tsv</code> index to <code>wiki2</code> and find out.</p><p><strong>Note</strong>: Expect creating the index to take 15 minutes or more.</p><pre><code class="language-SQL">create index on wiki2 using tsv (embedding);</code></pre><pre><code class="language-SQL">explain analyze
select id, embedding &lt;=&gt; $1::vector as dist
from wiki2
where '2000-01-04'::timestamptz &lt;= time and time &lt; '2000-01-06'::timestamptz
order by dist
limit 10
\bind :emb
;</code></pre><p>In most situations, Postgres can only use one index per table at a time. At least in this specific example on this machine, it chooses to use the vector index on <code>embedding</code>. It then filters the time on the fly. If you run this, you may get the same result, or Postgres may choose to do an index scan on the <code>time</code> column to filter by time and then do on-the-fly distance calculations on the vectors.&nbsp;</p><p>In this case, the query is as fast as the <a href="https://www.tigerdata.com/blog/database-indexes-in-postgresql-and-timescale-cloud-your-questions-answered" rel="noreferrer">hypertable</a> approach but nearly five times more expensive. Crucially, as the dataset grows, the hypertable version will increasingly outperform the plain-table version because the hypertable will use chunk exclusion.</p><pre><code class="language-text">QUERY PLAN
Limit&nbsp; (cost=2709.44..2714.22 rows=10 width=12) (actual time=3.029..5.078 rows=10 loops=1)
&nbsp;&nbsp;-&gt;&nbsp; Index Scan using wiki2_embedding_idx on wiki2&nbsp; (cost=2709.44..51574.47 rows=102257 width=12) (actual time=3.028..5.074 rows=10 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Order By: (embedding &lt;=&gt; '[...]'::vector)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Filter: (('2000-01-04 00:00:00+00'::timestamp with time zone &lt;= "time") AND ("time" &lt; '2000-01-06 00:00:00+00'::timestamp with time zone))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Rows Removed by Filter: 73
Planning Time: 0.093 ms
Execution Time: 5.096 ms</code></pre><h3 id="other-approaches-that-did-not-perform-as-well">Other approaches that did not perform as well.</h3><p>Can we force Postgres to use both indexes on one plain table? Yes! We can use a subquery. We will order by the vector distance using the vector index in the subquery. In the outer query, we will filter on time using the index on the <code>time</code> column. By joining the two, we will only get results that match both criteria. In other words, querying this way creates two resultsets—one via each index—and these resultsets are then evaluated for intersection.</p><p><strong>Note</strong>: Technically, we cannot force Postgres to choose to use indexes. Postgres does not have query hints. However, the structure of a query influences the plan. By restructuring the query, we can politely suggest the plan we want.</p><pre><code class="language-SQL">explain analyze
select w.id, x.dist
from wiki2 w
inner join
(
  select id, embedding &lt;=&gt; $1::vector as dist
  from wiki2
  order by dist
  limit 10
) x on (w.id = x.id)
where '2000-01-02'::timestamptz &lt;= time and time &lt; '2000-01-04'::timestamptz
order by x.dist
\bind :emb
;</code></pre><pre><code class="language-text">QUERY PLAN
Sort&nbsp; (cost=12868.26..12868.27 rows=2 width=12) (actual time=36.700..36.703 rows=3 loops=1)
&nbsp;&nbsp;Sort Key: x.dist
&nbsp;&nbsp;Sort Method: quicksort&nbsp; Memory: 25kB
&nbsp;&nbsp;-&gt;&nbsp; Hash Join&nbsp; (cost=2711.06..12868.25 rows=2 width=12) (actual time=5.720..36.693 rows=3 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Hash Cond: (w.id = x.id)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Index Scan using wiki2_time_idx on wiki2 w&nbsp; (cost=0.42..9784.27 rows=99553 width=4) (actual time=0.013..25.961 rows=100000 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Index Cond: (("time" &gt;= '2000-01-02 00:00:00+00'::timestamp with time zone) AND ("time" &lt; '2000-01-04 00:00:00+00'::timestamp with time zone))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Hash&nbsp; (cost=2710.51..2710.51 rows=10 width=12) (actual time=3.111..3.112 rows=10 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Buckets: 1024&nbsp; Batches: 1&nbsp; Memory Usage: 9kB
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Subquery Scan on x&nbsp; (cost=2709.44..2710.51 rows=10 width=12) (actual time=2.693..3.096 rows=10 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Limit&nbsp; (cost=2709.44..2710.41 rows=10 width=12) (actual time=2.692..3.092 rows=10 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Index Scan using wiki2_embedding_idx on wiki2&nbsp; (cost=2709.44..50104.18 rows=485859 width=12) (actual time=2.690..3.089 rows=10 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Order By: (embedding &lt;=&gt; '[...]'::vector)
Planning Time: 0.258 ms
Execution Time: 36.737 ms</code></pre><p>The query plan shows us that Postgres used both indexes. Surprisingly, having Postgres use both indexes is slower and more costly than both previous approaches. While the subquery only returns 10 rows, the outer query returns 100,000. Postgres then has to evaluate each of the 100,000 rows against each of the 10 rows using a hash join. Returning the 100,000 and comparing them to the 10 is where the bulk of the execution time takes place.</p><pre><code class="language-text">┌───────┬─────────────────────┐
│&nbsp; id &nbsp; │&nbsp; &nbsp; &nbsp; &nbsp; dist &nbsp; &nbsp; &nbsp; &nbsp; │
├───────┼─────────────────────┤
│ 65272 │ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 0 │
│ 61078 │ 0.13427528064358674 │
│ 79614 │ 0.13770102915039784 │
└───────┴─────────────────────┘
(3 rows)</code></pre><p>More importantly, we only get three results using the two-index approach. Why is this the case? The vector search subquery considers all the rows regardless of time. It returns the 10 most similar rows from the whole dataset. Unfortunately, seven of these 10 fall outside the time range we are filtering on and, therefore, get excluded from the results. This effect illustrates the classic post-filtering problem common in vector search use cases. Filtering by time after a similarity search can leave you with fewer results than expected or even none at all.</p><p>What happens if we turn this query “inside out”? We can put the time filtering in a subquery and do the vector search in the outer query like so:</p><pre><code class="language-SQL">explain analyze
select w.id, w.embedding &lt;=&gt; $1::vector as dist
from wiki2 w
where exists
(
  select 1
  from wiki2 x
  where '2000-01-02'::timestamptz &lt;= x.time and x.time &lt; '2000-01-04'::timestamptz
  and w.id = x.id
)
order by dist
limit 10
\bind :emb
;</code></pre><pre><code class="language-text">┌────────┬─────────────────────┐
│ &nbsp; id &nbsp; │&nbsp; &nbsp; &nbsp; &nbsp; dist &nbsp; &nbsp; &nbsp; &nbsp; │
├────────┼─────────────────────┤
│&nbsp; 65272 │ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 0 │
│&nbsp; 61078 │ 0.13427528064358674 │
│&nbsp; 79614 │ 0.13770102915039784 │
│&nbsp; 52634 │&nbsp; 0.1436081460507892 │
│&nbsp; 79612 │ 0.14383225467703376 │
│&nbsp; 64473 │ 0.14383654656162292 │
│&nbsp; 61080 │&nbsp; 0.1444315737261198 │
│&nbsp; 61079 │&nbsp; 0.1474735568613179 │
│ 131920 │ 0.15066526846960182 │
│&nbsp; 64475 │ &nbsp; 0.154294958183531 │
└────────┴─────────────────────┘
(10 rows)
</code></pre><pre><code class="language-text">QUERY PLAN
Limit&nbsp; (cost=58235.37..58235.39 rows=10 width=12) (actual time=1394.904..1394.909 rows=10 loops=1)
&nbsp;&nbsp;-&gt;&nbsp; Sort&nbsp; (cost=58235.37..58484.25 rows=99553 width=12) (actual time=1394.902..1394.906 rows=10 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Sort Key: ((w.embedding &lt;=&gt; '[...]'::vector))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Sort Method: top-N heapsort&nbsp; Memory: 25kB
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Hash Semi Join&nbsp; (cost=11028.68..56084.06 rows=99553 width=12) (actual time=294.332..1380.233 rows=100000 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Hash Cond: (w.id = x.id)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Seq Scan on wiki2 w&nbsp; (cost=0.00..42423.59 rows=485859 width=22) (actual time=0.034..616.130 rows=485859 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Hash&nbsp; (cost=9784.27..9784.27 rows=99553 width=4) (actual time=40.376..40.378 rows=100000 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Buckets: 131072&nbsp; Batches: 1&nbsp; Memory Usage: 4540kB
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;&nbsp; Index Scan using wiki2_time_idx on wiki2 x&nbsp; (cost=0.42..9784.27 rows=99553 width=4) (actual time=0.016..28.235 rows=100000 loops=1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Index Cond: (("time" &gt;= '2000-01-02 00:00:00+00'::timestamp with time zone) AND ("time" &lt; '2000-01-04 00:00:00+00'::timestamp with time zone))
Planning Time: 0.164 ms
Execution Time: 1394.960 ms
</code></pre><p>Unfortunately, Postgres used the index on time but chose to ignore the vector index. It computed the exact distances on the fly instead of using an approximate nearest-neighbor search. Unsurprisingly, this approach is slower and more expensive than the single index query plan on the plain table. It is also much slower and more costly than the hypertable approach.</p><h2 id="using-vector-search-and-time-filters-in-ai-applications">Using Vector Search and Time Filters in AI Applications</h2><p>Incorporating time-based filtering with semantic search allows AI applications to offer users a more nuanced, context-aware interaction. This approach ensures that the information, products, or content presented is not just relevant but also timely, providing a more valuable user experience.</p><ol><li><strong>Content discovery platforms</strong>: Imagine a streaming service that not only recommends movies based on your viewing history but also prioritizes new releases or timely content, such as holiday-themed movies. By applying time filters to semantic search, these platforms can dynamically adjust recommendations, ensuring users are presented with content that's not just relevant to their interests but also their recent viewing history or seasonal trends.</li><li><strong>News aggregation</strong>: Time-based filtering can revolutionize how readers engage with information in news and content aggregation. Time filters can prioritize the latest news, ensuring readers can access the most current information and trends relevant to their query, while semantic search simultaneously sifts through the articles to find ones related to a user's interests.</li><li><strong>E-commerce</strong>: For e-commerce platforms, combining semantic search with time-based filters can enhance shopping experiences by promoting products related to a user’s recent purchases, upcoming events, or seasonal trends. For instance, suggesting winter sports gear as the season approaches or highlighting flash sale items within a limited time frame makes the shopping experience more relevant and timely.</li><li><strong>Academic research</strong>: Researchers often seek their field's most recent studies or articles. By integrating time filters with semantic search, academic databases can provide search results that are contextually relevant and prioritize the latest research, facilitating access to cutting-edge knowledge and discoveries.</li></ol><h2 id="next-steps">Next Steps</h2><p>So, what have we learned? We can combine vector search with time-based filtering in Postgres to retrieve more temporally relevant vectors. While we can achieve this with a plain table, we can write simpler, faster, and more efficient queries that continue to perform as your dataset grows using TimescaleDB’s hypertables.</p><p>Use pgvector on Timescale today! <a href="https://console.cloud.timescale.com/signup?utm_campaign=vectorlaunch&amp;utm_source=timescale-blog&amp;utm_medium=direct&amp;utm_content=filtering-blog"><u>Follow this link</u></a> to get a 90-day free trial!</p><p>Read more:</p><ul><li><a href="https://docs.timescale.com/ai/latest/"><u>pgai on Timescale documentation</u></a></li><li><a href="https://youtu.be/JDVU0k30cGA?si=_xoIDzbtQQ8WJen9"><u>pgvector and Timescale Vector Up and Running</u></a></li><li><a href="https://youtu.be/EYMZVfKcRzM?si=o2BHV6fEqpaxjufO"><u>LlamaIndex Webinar: Time-based retrieval for RAG (with Timescale)</u></a></li></ul>]]></content:encoded>
        </item>
    </channel>
</rss>