Skip to content
Postgres extensions

pgvector

What it is

pgvector adds vector columns, distance operators, and approximate-nearest-neighbor indexes to PostgreSQL. It lets relational records and machine-learning embeddings live and transact together.

What it provides

  • Vector, half-vector, sparse-vector, and binary-vector storage
  • Exact L2, inner-product, cosine, L1, Hamming, and Jaccard search
  • HNSW and IVFFlat indexes for approximate nearest-neighbor queries

Use it for semantic search, recommendation systems, duplicate detection, image similarity, retrieval-augmented generation, or hybrid search that combines embeddings with ordinary filters.

Use it with Rent

extension vector {
  name   = "vector"
  schema = "embeddings"
}

model Post {
  id    BigInt @id
  title String
  embedding pgvector.Vector? @db.Vector(1536)
}

Run rent generate, then review and apply the migration with rent migrate dev --name add_embeddings --schemas public,embeddings. Add rent-ext-pgvector to your application's dependencies and enable Rent's postgres and extensions features. The generated model holds Option<Vector>, not a string or JSON proxy.

use rent_ext_pgvector::Vector;

let query = Vector::new(embedding_service_output)?;

let posts = client
    .post()
    .order_by_embedding_cosine(&query)?
    .limit(10)
    .all()
    .await?;

Generated setters, filters, and distance ordering use native binary values and qualify extension operators with the installation schema. The field's dimensions are checked before a write or filter reaches PostgreSQL. Use .embedding(query) to set a value, .clear_embedding() to clear a nullable value, and .select_embedding() to retrieve typed vectors. Vector distance ordering composes with ordinary filters, eager loading, limits, and multi-field projections. A cosine query must have nonzero norm to define a useful distance. Choose L2 when zero vectors are meaningful in your application.

Query filters and distance ordering accept owned or borrowed vectors. Borrowing avoids copying the original component buffer. Rent encodes owned query parameters before returning the query builder, so the builder does not borrow your embedding across an await. Collection filters accept borrowed slices as well:

let candidates = [first_embedding, second_embedding];
let posts = client
    .post()
    .embedding_in(&candidates)?
    .all()
    .await?;

Model field setters accept an owned or borrowed vector and store an owned copy. The generated native field type represents pgvector's full-precision vector. Other storage formats require explicit SQL and their own codecs.

Run the application

Add the extension declaration above to your application's schema. Confirm that your PostgreSQL provider offers the extension and that your migration role can install it, then apply the migration and run your application:

rent migrate dev --name enable_pgvector
cargo run

Complete example

Download this project, or copy the files below into an empty directory.

Replace RENT_REPOSITORY_URL in Cargo.toml with the URL supplied with your preview access. Use the matching Rent CLI release.

Use an empty PostgreSQL database with the extension binaries available on its server. Set DATABASE_URL to that database and follow any server setup requirements described above.

rent extension check --provider self-hosted
rent generate
rent migrate dev --name initial --schemas public,embeddings
cargo run

The vector example uses three-dimensional sample values for readability. Use your embedding model's dimensions in an application.

The schema places the extension in embeddings. Include that schema when Rent inspects the database.

If your host preinstalls other extensions, declare those in RSL as well. For a database with existing application tables, baseline it first.

Cargo.toml
[package]
name = "rent-extension-pgvector"
version = "0.1.0"
edition = "2024"
[workspace]
[[bin]]
name = "rent-extension-pgvector"
path = "main.rs"
[dependencies]
rent = { git = "RENT_REPOSITORY_URL", tag = "v0.8.6", default-features = false, features = ["postgres", "extensions", "migrate"] }
rent-ext-pgvector = { git = "RENT_REPOSITORY_URL", tag = "v0.8.6" }
anyhow = "1.0.104"
tokio = { version = "1.53.1", features = ["full"] }
rent.toml
schema = "schema.rsl"
generated = "generated"
migrations = "migrations"

[database]
dialect = "postgres"
backends = ["postgres"]
schema.rsl
datasource db {
  provider = "postgresql"
}

extension vector {
  name   = "vector"
  schema = "embeddings"
}

model Post {
  id        BigInt            @id
  title     String
  embedding pgvector.Vector?  @db.Vector(3)
}
main.rs
use anyhow::Result;
use rent::driver::DatabasePool;
#[allow(dead_code, unused_imports)]
#[rustfmt::skip]
#[path = "generated/mod.rs"]
mod generated;
#[tokio::main]
async fn main() -> Result<()> {
    let pool = DatabasePool::connect_url(&std::env::var("DATABASE_URL")?).await?;
    let client = generated::client::Client::new(pool.clone());
    use rent_ext_pgvector::Vector;
    let query = Vector::new(vec![0.1, 0.2, 0.3])?;
    client
        .upsert_post(generated::post::unique::id)
        .id(1)
        .title("Launch notes")
        .embedding(&query)
        .save()
        .await?;
    let nearby = client.post().order_by_embedding_cosine(&query)?.limit(10).all().await?;
    println!("Nearest post: {}", nearby.first().map_or("none", |post| post.title.as_str()));
    Ok(())
}