From 09253f9ed30c981dbd87ebb69ad1df0c61bbf2f0 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 9 Feb 2025 01:48:00 -0800 Subject: [PATCH 01/28] Added Citus example [skip ci] --- README.md | 1 + examples/citus/Cargo.toml | 13 +++++ examples/citus/src/main.rs | 98 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 examples/citus/Cargo.toml create mode 100644 examples/citus/src/main.rs diff --git a/README.md b/README.md index 10aa0e3..9ce1520 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Or check out some examples: - [Sentence embeddings](https://github.com/pgvector/pgvector-rust/blob/master/examples/candle/src/main.rs) with Candle - [Hybrid search](https://github.com/pgvector/pgvector-rust/blob/master/examples/hybrid_search/src/main.rs) with Candle (Reciprocal Rank Fusion) - [Recommendations](https://github.com/pgvector/pgvector-rust/blob/master/examples/disco/src/main.rs) with Disco +- [Horizontal scaling](https://github.com/pgvector/pgvector-rust/blob/master/examples/citus/src/main.rs) with Citus - [Bulk loading](https://github.com/pgvector/pgvector-rust/blob/master/examples/loading/src/main.rs) with `COPY` ## Rust-Postgres diff --git a/examples/citus/Cargo.toml b/examples/citus/Cargo.toml new file mode 100644 index 0000000..48b2c99 --- /dev/null +++ b/examples/citus/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "example" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +pgvector = { path = "../..", features = ["postgres"] } +postgres = "0.19" +rand = "0.8" + +[profile.dev] +opt-level = 1 diff --git a/examples/citus/src/main.rs b/examples/citus/src/main.rs new file mode 100644 index 0000000..79249bf --- /dev/null +++ b/examples/citus/src/main.rs @@ -0,0 +1,98 @@ +use pgvector::Vector; +use postgres::binary_copy::BinaryCopyInWriter; +use postgres::types::{Kind, Type}; +use postgres::{Client, NoTls}; +use rand::Rng; +use std::error::Error; + +fn main() -> Result<(), Box> { + // generate random data + let rows = 100000; + let dimensions = 128; + let mut rng = rand::thread_rng(); + let embeddings: Vec> = (0..rows) + .map(|_| (0..dimensions).map(|_| rng.gen()).collect()) + .collect(); + let categories: Vec = (0..rows).map(|_| rng.gen_range(1..=100)).collect(); + let queries: Vec> = (0..10) + .map(|_| (0..dimensions).map(|_| rng.gen()).collect()) + .collect(); + + // enable extensions + let mut client = Client::configure() + .host("localhost") + .dbname("pgvector_citus") + .user(std::env::var("USER")?.as_str()) + .connect(NoTls)?; + client.execute("CREATE EXTENSION IF NOT EXISTS citus", &[])?; + client.execute("CREATE EXTENSION IF NOT EXISTS vector", &[])?; + + // GUC variables set on the session do not propagate to Citus workers + // https://github.com/citusdata/citus/issues/462 + // you can either: + // 1. set them on the system, user, or database and reconnect + // 2. set them for a transaction with SET LOCAL + client.execute( + "ALTER DATABASE pgvector_citus SET maintenance_work_mem = '512MB'", + &[], + )?; + client.execute("ALTER DATABASE pgvector_citus SET hnsw.ef_search = 20", &[])?; + client.close()?; + + // reconnect for updated GUC variables to take effect + let mut client = Client::configure() + .host("localhost") + .dbname("pgvector_citus") + .user(std::env::var("USER")?.as_str()) + .connect(NoTls)?; + + println!("Creating distributed table"); + client.execute("DROP TABLE IF EXISTS items", &[])?; + client.execute( + &format!("CREATE TABLE items (id bigserial, embedding vector({dimensions}), category_id bigint, PRIMARY KEY (id, category_id))"), + &[], + )?; + client.execute("SET citus.shard_count = 4", &[])?; + client.execute( + "SELECT create_distributed_table('items', 'category_id')", + &[], + )?; + + println!("Loading data in parallel"); + let vector_type = get_type(&mut client, "vector")?; + let writer = + client.copy_in("COPY items (embedding, category_id) FROM STDIN WITH (FORMAT BINARY)")?; + let mut writer = BinaryCopyInWriter::new(writer, &[vector_type, Type::INT8]); + for (embedding, category) in embeddings.into_iter().zip(categories) { + writer.write(&[&Vector::from(embedding), &category])?; + } + writer.finish()?; + + println!("Creating index in parallel"); + client.execute( + "CREATE INDEX ON items USING hnsw (embedding vector_l2_ops)", + &[], + )?; + + println!("Running distributed queries"); + for query in queries { + let rows = client.query( + "SELECT id FROM items ORDER BY embedding <-> $1 LIMIT 10", + &[&Vector::from(query)], + )?; + let ids: Vec = rows.into_iter().map(|row| row.get(0)).collect(); + println!("{:?}", ids); + } + + Ok(()) +} + +fn get_type(client: &mut Client, name: &str) -> Result> { + let row = client.query_one("SELECT pg_type.oid, nspname AS schema FROM pg_type INNER JOIN pg_namespace ON pg_namespace.oid = pg_type.typnamespace WHERE typname = $1", &[&name])?; + Ok(Type::new( + name.into(), + row.get("oid"), + Kind::Simple, + row.get("schema"), + )) +} From 7d66e30f382eea31c306fb593262b58e4b5dabf3 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 9 Feb 2025 01:51:43 -0800 Subject: [PATCH 02/28] Improved examples [skip ci] --- examples/citus/Cargo.toml | 2 +- examples/citus/src/main.rs | 8 ++++---- examples/loading/Cargo.toml | 2 +- examples/loading/src/main.rs | 12 ++++++------ 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/examples/citus/Cargo.toml b/examples/citus/Cargo.toml index 48b2c99..419f9a0 100644 --- a/examples/citus/Cargo.toml +++ b/examples/citus/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] pgvector = { path = "../..", features = ["postgres"] } postgres = "0.19" -rand = "0.8" +rand = "0.9" [profile.dev] opt-level = 1 diff --git a/examples/citus/src/main.rs b/examples/citus/src/main.rs index 79249bf..c1acda0 100644 --- a/examples/citus/src/main.rs +++ b/examples/citus/src/main.rs @@ -9,13 +9,13 @@ fn main() -> Result<(), Box> { // generate random data let rows = 100000; let dimensions = 128; - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); let embeddings: Vec> = (0..rows) - .map(|_| (0..dimensions).map(|_| rng.gen()).collect()) + .map(|_| (0..dimensions).map(|_| rng.random()).collect()) .collect(); - let categories: Vec = (0..rows).map(|_| rng.gen_range(1..=100)).collect(); + let categories: Vec = (0..rows).map(|_| rng.random_range(1..=100)).collect(); let queries: Vec> = (0..10) - .map(|_| (0..dimensions).map(|_| rng.gen()).collect()) + .map(|_| (0..dimensions).map(|_| rng.random()).collect()) .collect(); // enable extensions diff --git a/examples/loading/Cargo.toml b/examples/loading/Cargo.toml index 48b2c99..419f9a0 100644 --- a/examples/loading/Cargo.toml +++ b/examples/loading/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] pgvector = { path = "../..", features = ["postgres"] } postgres = "0.19" -rand = "0.8" +rand = "0.9" [profile.dev] opt-level = 1 diff --git a/examples/loading/src/main.rs b/examples/loading/src/main.rs index 2d5ba88..880a21e 100644 --- a/examples/loading/src/main.rs +++ b/examples/loading/src/main.rs @@ -10,9 +10,9 @@ fn main() -> Result<(), Box> { // generate random data let rows = 1000000; let dimensions = 128; - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); let embeddings: Vec> = (0..rows) - .map(|_| (0..dimensions).map(|_| rng.gen()).collect()) + .map(|_| (0..dimensions).map(|_| rng.random()).collect()) .collect(); // enable extension @@ -32,7 +32,7 @@ fn main() -> Result<(), Box> { // load data println!("Loading {} rows", embeddings.len()); - let vector_type = vector_type(&mut client)?; + let vector_type = get_type(&mut client, "vector")?; let writer = client.copy_in("COPY items (embedding) FROM STDIN WITH (FORMAT BINARY)")?; let mut writer = BinaryCopyInWriter::new(writer, &[vector_type]); for (i, embedding) in embeddings.into_iter().enumerate() { @@ -64,10 +64,10 @@ fn main() -> Result<(), Box> { Ok(()) } -fn vector_type(client: &mut Client) -> Result> { - let row = client.query_one("SELECT pg_type.oid, nspname AS schema FROM pg_type INNER JOIN pg_namespace ON pg_namespace.oid = pg_type.typnamespace WHERE typname = 'vector'", &[])?; +fn get_type(client: &mut Client, name: &str) -> Result> { + let row = client.query_one("SELECT pg_type.oid, nspname AS schema FROM pg_type INNER JOIN pg_namespace ON pg_namespace.oid = pg_type.typnamespace WHERE typname = $1", &[&name])?; Ok(Type::new( - "vector".into(), + name.into(), row.get("oid"), Kind::Simple, row.get("schema"), From 277e400879192a8379f5957fab77d43d35ea1792 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 9 Feb 2025 01:52:43 -0800 Subject: [PATCH 03/28] Updated license year [skip ci] --- LICENSE-MIT | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE-MIT b/LICENSE-MIT index d205f4e..b612d6d 100644 --- a/LICENSE-MIT +++ b/LICENSE-MIT @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2021-2024 Andrew Kane +Copyright (c) 2021-2025 Andrew Kane Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From e38f23e39f1277cd2353b303483ae0458a8f25ea Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 9 Feb 2025 01:56:20 -0800 Subject: [PATCH 04/28] Updated dependencies for example [skip ci] --- examples/candle/Cargo.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/candle/Cargo.toml b/examples/candle/Cargo.toml index 10ceca9..9c2728f 100644 --- a/examples/candle/Cargo.toml +++ b/examples/candle/Cargo.toml @@ -5,11 +5,11 @@ edition = "2021" publish = false [dependencies] -candle-core = "0.7" -candle-nn = "0.7" -candle-transformers = "0.7" -hf-hub = "0.3" +candle-core = "0.8" +candle-nn = "0.8" +candle-transformers = "0.8" +hf-hub = "0.4" pgvector = { path = "../..", features = ["postgres"] } postgres = "0.19" serde_json = "1" -tokenizers = "0.20" +tokenizers = "0.21" From 6a7d69a62684abb6d14f1e08906de62867911c3b Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 9 Feb 2025 02:01:27 -0800 Subject: [PATCH 05/28] Updated example [skip ci] --- examples/cohere/Cargo.toml | 2 +- examples/cohere/src/main.rs | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/cohere/Cargo.toml b/examples/cohere/Cargo.toml index 1a21a16..d436309 100644 --- a/examples/cohere/Cargo.toml +++ b/examples/cohere/Cargo.toml @@ -8,4 +8,4 @@ publish = false pgvector = { path = "../..", features = ["postgres"] } postgres = "0.19" serde_json = "1" -ureq = { version = "2", features = ["json"] } +ureq = { version = "3", features = ["json"] } diff --git a/examples/cohere/src/main.rs b/examples/cohere/src/main.rs index 8d8da50..644bf8e 100644 --- a/examples/cohere/src/main.rs +++ b/examples/cohere/src/main.rs @@ -39,14 +39,15 @@ fn fetch_embeddings(texts: &[&str], input_type: &str) -> Result>, Bo let api_key = std::env::var("CO_API_KEY").or(Err("Set CO_API_KEY"))?; let response: Value = ureq::post("https://api.cohere.com/v1/embed") - .set("Authorization", &format!("Bearer {}", api_key)) - .send_json(ureq::json!({ + .header("Authorization", &format!("Bearer {}", api_key)) + .send_json(serde_json::json!({ "texts": texts, "model": "embed-english-v3.0", "input_type": input_type, "embedding_types": &["ubinary"], }))? - .into_json()?; + .body_mut() + .read_json()?; let embeddings = response["embeddings"]["ubinary"] .as_array() From 4a95ca0d250ab2cef107d2d6fb62d65d7c509333 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 9 Feb 2025 02:05:16 -0800 Subject: [PATCH 06/28] Updated example [skip ci] --- examples/hybrid_search/Cargo.toml | 10 +++++----- examples/hybrid_search/src/main.rs | 5 ++--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/examples/hybrid_search/Cargo.toml b/examples/hybrid_search/Cargo.toml index e38ef39..9c2728f 100644 --- a/examples/hybrid_search/Cargo.toml +++ b/examples/hybrid_search/Cargo.toml @@ -5,11 +5,11 @@ edition = "2021" publish = false [dependencies] -candle-core = "0.6" -candle-nn = "0.6" -candle-transformers = "0.6" -hf-hub = "0.3" +candle-core = "0.8" +candle-nn = "0.8" +candle-transformers = "0.8" +hf-hub = "0.4" pgvector = { path = "../..", features = ["postgres"] } postgres = "0.19" serde_json = "1" -tokenizers = "0.19" +tokenizers = "0.21" diff --git a/examples/hybrid_search/src/main.rs b/examples/hybrid_search/src/main.rs index 6b480fd..e047e42 100644 --- a/examples/hybrid_search/src/main.rs +++ b/examples/hybrid_search/src/main.rs @@ -113,13 +113,12 @@ impl EmbeddingModel { Ok(Self { tokenizer, model }) } - // embed one at a time since BertModel does not support attention mask - // https://github.com/huggingface/candle/issues/1798 + // TODO support multiple texts fn embed(&self, text: &str) -> Result, Box> { let tokens = self.tokenizer.encode(text, true)?; let token_ids = Tensor::new(vec![tokens.get_ids().to_vec()], &self.model.device)?; let token_type_ids = token_ids.zeros_like()?; - let embeddings = self.model.forward(&token_ids, &token_type_ids)?; + let embeddings = self.model.forward(&token_ids, &token_type_ids, None)?; let embeddings = (embeddings.sum(1)? / (embeddings.dim(1)? as f64))?; let embeddings = embeddings.broadcast_div(&embeddings.sqr()?.sum_keepdim(1)?.sqrt()?)?; Ok(embeddings.squeeze(0)?.to_vec1::()?) From a33c4a643365029329f1301a3770c36f1684a90a Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 9 Feb 2025 02:06:30 -0800 Subject: [PATCH 07/28] Updated example [skip ci] --- examples/openai/Cargo.toml | 2 +- examples/openai/src/main.rs | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/examples/openai/Cargo.toml b/examples/openai/Cargo.toml index 1a21a16..d436309 100644 --- a/examples/openai/Cargo.toml +++ b/examples/openai/Cargo.toml @@ -8,4 +8,4 @@ publish = false pgvector = { path = "../..", features = ["postgres"] } postgres = "0.19" serde_json = "1" -ureq = { version = "2", features = ["json"] } +ureq = { version = "3", features = ["json"] } diff --git a/examples/openai/src/main.rs b/examples/openai/src/main.rs index e937f0a..41ebf6b 100644 --- a/examples/openai/src/main.rs +++ b/examples/openai/src/main.rs @@ -12,7 +12,10 @@ fn main() -> Result<(), Box> { client.execute("CREATE EXTENSION IF NOT EXISTS vector", &[])?; client.execute("DROP TABLE IF EXISTS documents", &[])?; - client.execute("CREATE TABLE documents (id serial PRIMARY KEY, content text, embedding vector(1536))", &[])?; + client.execute( + "CREATE TABLE documents (id serial PRIMARY KEY, content text, embedding vector(1536))", + &[], + )?; let input = [ "The dog is barking", @@ -23,7 +26,10 @@ fn main() -> Result<(), Box> { for (content, embedding) in input.iter().zip(embeddings) { let embedding = Vector::from(embedding); - client.execute("INSERT INTO documents (content, embedding) VALUES ($1, $2)", &[&content, &embedding])?; + client.execute( + "INSERT INTO documents (content, embedding) VALUES ($1, $2)", + &[&content, &embedding], + )?; } let document_id = 2; @@ -39,12 +45,13 @@ fn fetch_embeddings(input: &[&str]) -> Result>, Box> { let api_key = std::env::var("OPENAI_API_KEY").or(Err("Set OPENAI_API_KEY"))?; let response: Value = ureq::post("https://api.openai.com/v1/embeddings") - .set("Authorization", &format!("Bearer {}", api_key)) - .send_json(ureq::json!({ + .header("Authorization", &format!("Bearer {}", api_key)) + .send_json(serde_json::json!({ "input": input, "model": "text-embedding-3-small", }))? - .into_json()?; + .body_mut() + .read_json()?; let embeddings = response["data"] .as_array() From 8e1d6ca41276a38f8e5baa1e25860df4d46d9195 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 9 Feb 2025 02:07:24 -0800 Subject: [PATCH 08/28] Updated examples [skip ci] --- examples/cohere/src/main.rs | 15 ++++++++++++--- examples/disco/src/main.rs | 20 ++++++++++++++++---- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/examples/cohere/src/main.rs b/examples/cohere/src/main.rs index 644bf8e..30d5d48 100644 --- a/examples/cohere/src/main.rs +++ b/examples/cohere/src/main.rs @@ -12,7 +12,10 @@ fn main() -> Result<(), Box> { client.execute("CREATE EXTENSION IF NOT EXISTS vector", &[])?; client.execute("DROP TABLE IF EXISTS documents", &[])?; - client.execute("CREATE TABLE documents (id serial PRIMARY KEY, content text, embedding bit(1024))", &[])?; + client.execute( + "CREATE TABLE documents (id serial PRIMARY KEY, content text, embedding bit(1024))", + &[], + )?; let input = [ "The dog is barking", @@ -22,12 +25,18 @@ fn main() -> Result<(), Box> { let embeddings = fetch_embeddings(&input, "search_document")?; for (content, embedding) in input.iter().zip(embeddings) { let embedding = Bit::from_bytes(&embedding); - client.execute("INSERT INTO documents (content, embedding) VALUES ($1, $2)", &[&content, &embedding])?; + client.execute( + "INSERT INTO documents (content, embedding) VALUES ($1, $2)", + &[&content, &embedding], + )?; } let query = "forest"; let query_embedding = fetch_embeddings(&[query], "search_query")?; - for row in client.query("SELECT content FROM documents ORDER BY embedding <~> $1 LIMIT 5", &[&Bit::from_bytes(&query_embedding[0])])? { + for row in client.query( + "SELECT content FROM documents ORDER BY embedding <~> $1 LIMIT 5", + &[&Bit::from_bytes(&query_embedding[0])], + )? { let content: &str = row.get(0); println!("{}", content); } diff --git a/examples/disco/src/main.rs b/examples/disco/src/main.rs index 614defd..c677e8a 100644 --- a/examples/disco/src/main.rs +++ b/examples/disco/src/main.rs @@ -21,20 +21,32 @@ fn main() -> Result<(), Box> { client.execute("CREATE EXTENSION IF NOT EXISTS vector", &[])?; client.execute("DROP TABLE IF EXISTS users", &[])?; client.execute("DROP TABLE IF EXISTS movies", &[])?; - client.execute("CREATE TABLE users (id integer PRIMARY KEY, factors vector(20))", &[])?; - client.execute("CREATE TABLE movies (name text PRIMARY KEY, factors vector(20))", &[])?; + client.execute( + "CREATE TABLE users (id integer PRIMARY KEY, factors vector(20))", + &[], + )?; + client.execute( + "CREATE TABLE movies (name text PRIMARY KEY, factors vector(20))", + &[], + )?; let data = load_movielens(Path::new(&movielens_path)); let recommender = RecommenderBuilder::new().factors(20).fit_explicit(&data); for user_id in recommender.user_ids() { let factors = Vector::from(recommender.user_factors(user_id).unwrap().to_vec()); - client.execute("INSERT INTO users (id, factors) VALUES ($1, $2)", &[&user_id, &factors])?; + client.execute( + "INSERT INTO users (id, factors) VALUES ($1, $2)", + &[&user_id, &factors], + )?; } for item_id in recommender.item_ids() { let factors = Vector::from(recommender.item_factors(item_id).unwrap().to_vec()); - client.execute("INSERT INTO movies (name, factors) VALUES ($1, $2)", &[&item_id, &factors])?; + client.execute( + "INSERT INTO movies (name, factors) VALUES ($1, $2)", + &[&item_id, &factors], + )?; } let movie = "Star Wars (1977)"; From 02c07cc789ec30fdc51b3793e9f1fccd6c29cd2e Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 13 Feb 2025 21:45:43 -0800 Subject: [PATCH 09/28] Improved reference section [skip ci] --- README.md | 109 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 105 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9ce1520..739ad9e 100644 --- a/README.md +++ b/README.md @@ -259,13 +259,19 @@ Use `vector_ip_ops` for inner product and `vector_cosine_ops` for cosine distanc Use the `serde` feature to enable serialization -## Half Vectors +## Reference -Use the `halfvec` feature to enable half vectors +### Vectors -## Reference +Create a vector -Convert a vector to a `Vec` +```rust +use pgvector::Vector; + +let vec = Vector::from(vec![1.0, 2.0, 3.0]); +``` + +Convert to a `Vec` ```rust let f32_vec: Vec = vec.into(); @@ -277,6 +283,101 @@ Get a slice let slice = vec.as_slice(); ``` +### Half Vectors + +Note: Use the `halfvec` feature to enable half vectors + +Create a half vector + +```rust +use pgvector::HalfVector; + +let vec = HalfVector::from(vec![f16::from_f32(1.0), f16::from_f32(2.0), f16::from_f32(3.0)]); +``` + +Convert to a `Vec` + +```rust +let f16_vec: Vec = vec.into(); +``` + +Get a slice + +```rust +let slice = vec.as_slice(); +``` + +### Binary Vectors + +Create a binary vector from a slice of bits + +```rust +use pgvector::Bit; + +let vec = Bit::new(&[true, false, true]); +``` + +or a slice of bytes + +```rust +let vec = Bit::from_bytes(&[0b00000000, 0b11111111]); +``` + +Get the number of bits + +```rust +let len = vec.len(); +``` + +Get a slice of bytes + +```rust +let bytes = vec.as_bytes(); +``` + +### Sparse Vectors + +Create a sparse vector from a dense vector + +```rust +use pgvector::SparseVector; + +let vec = SparseVector::from_dense(vec![1.0, 0.0, 2.0, 0.0, 3.0, 0.0]); +``` + +or a map of non-zero elements + +```rust +let map = HashMap::from([(0, 1.0), (2, 2.0), (4, 3.0)]); +let vec = SparseVector::from_map(&map, 6); +``` + +Note: Indices start at 0 + +Get the number of dimensions + +```rust +let dim = vec.dimensions(); +``` + +Get the indices of non-zero elements + +```rust +let indices = vec.indices(); +``` + +Get the values of non-zero elements + +```rust +let values = vec.values(); +``` + +Get a dense vector + +```rust +let f32_vec = vec.to_vec(); +``` + ## History View the [changelog](https://github.com/pgvector/pgvector-rust/blob/master/CHANGELOG.md) From 84103ca3132bfa7dbcf1176fbe679950570913fd Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 13 Feb 2025 21:46:47 -0800 Subject: [PATCH 10/28] Updated readme [skip ci] --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 739ad9e..176c40d 100644 --- a/README.md +++ b/README.md @@ -290,6 +290,7 @@ Note: Use the `halfvec` feature to enable half vectors Create a half vector ```rust +use half::f16; use pgvector::HalfVector; let vec = HalfVector::from(vec![f16::from_f32(1.0), f16::from_f32(2.0), f16::from_f32(3.0)]); From 5cdc28b3d7ad5c5d3db90114793e530769af347a Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 16 Feb 2025 20:48:32 -0800 Subject: [PATCH 11/28] Improved examples [skip ci] --- examples/cohere/src/main.rs | 6 +++--- examples/openai/src/main.rs | 16 +++++++++------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/examples/cohere/src/main.rs b/examples/cohere/src/main.rs index 30d5d48..c4b6bd7 100644 --- a/examples/cohere/src/main.rs +++ b/examples/cohere/src/main.rs @@ -22,7 +22,7 @@ fn main() -> Result<(), Box> { "The cat is purring", "The bear is growling", ]; - let embeddings = fetch_embeddings(&input, "search_document")?; + let embeddings = embed(&input, "search_document")?; for (content, embedding) in input.iter().zip(embeddings) { let embedding = Bit::from_bytes(&embedding); client.execute( @@ -32,7 +32,7 @@ fn main() -> Result<(), Box> { } let query = "forest"; - let query_embedding = fetch_embeddings(&[query], "search_query")?; + let query_embedding = embed(&[query], "search_query")?; for row in client.query( "SELECT content FROM documents ORDER BY embedding <~> $1 LIMIT 5", &[&Bit::from_bytes(&query_embedding[0])], @@ -44,7 +44,7 @@ fn main() -> Result<(), Box> { Ok(()) } -fn fetch_embeddings(texts: &[&str], input_type: &str) -> Result>, Box> { +fn embed(texts: &[&str], input_type: &str) -> Result>, Box> { let api_key = std::env::var("CO_API_KEY").or(Err("Set CO_API_KEY"))?; let response: Value = ureq::post("https://api.cohere.com/v1/embed") diff --git a/examples/openai/src/main.rs b/examples/openai/src/main.rs index 41ebf6b..eb063d4 100644 --- a/examples/openai/src/main.rs +++ b/examples/openai/src/main.rs @@ -22,18 +22,20 @@ fn main() -> Result<(), Box> { "The cat is purring", "The bear is growling", ]; - let embeddings = fetch_embeddings(&input)?; - + let embeddings = embed(&input)?; for (content, embedding) in input.iter().zip(embeddings) { - let embedding = Vector::from(embedding); client.execute( "INSERT INTO documents (content, embedding) VALUES ($1, $2)", - &[&content, &embedding], + &[&content, &Vector::from(embedding)], )?; } - let document_id = 2; - for row in client.query("SELECT content FROM documents WHERE id != $1 ORDER BY embedding <=> (SELECT embedding FROM documents WHERE id = $1) LIMIT 5", &[&document_id])? { + let query = "forest"; + let query_embedding = embed(&[query])?.drain(..).next().unwrap(); + for row in client.query( + "SELECT content FROM documents ORDER BY embedding <=> $1 LIMIT 5", + &[&Vector::from(query_embedding)], + )? { let content: &str = row.get(0); println!("{}", content); } @@ -41,7 +43,7 @@ fn main() -> Result<(), Box> { Ok(()) } -fn fetch_embeddings(input: &[&str]) -> Result>, Box> { +fn embed(input: &[&str]) -> Result>, Box> { let api_key = std::env::var("OPENAI_API_KEY").or(Err("Set OPENAI_API_KEY"))?; let response: Value = ureq::post("https://api.openai.com/v1/embeddings") From 22afb2de3d367e24ef00512b9b5d16df7e41bdcb Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 5 Mar 2025 13:58:06 -0800 Subject: [PATCH 12/28] Added from_f32_slice function to HalfVector --- CHANGELOG.md | 4 +++ README.md | 8 +++++- src/diesel_ext/halfvec.rs | 57 +++++++------------------------------ src/halfvec.rs | 17 +++++------ src/postgres_ext/halfvec.rs | 18 ++---------- src/sqlx_ext/halfvec.rs | 18 ++---------- 6 files changed, 35 insertions(+), 87 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8834638..4e4bb3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.4.1 (unreleased) + +- Added `from_f32_slice` function to `HalfVector` + ## 0.4.0 (2024-07-28) - Added support for SQLx 0.8 diff --git a/README.md b/README.md index 176c40d..de74de7 100644 --- a/README.md +++ b/README.md @@ -287,7 +287,7 @@ let slice = vec.as_slice(); Note: Use the `halfvec` feature to enable half vectors -Create a half vector +Create a half vector from a `Vec` ```rust use half::f16; @@ -296,6 +296,12 @@ use pgvector::HalfVector; let vec = HalfVector::from(vec![f16::from_f32(1.0), f16::from_f32(2.0), f16::from_f32(3.0)]); ``` +or a `f32` slice [unreleased] + +```rust +let vec = HalfVector::from_f32_slice(&[1.0, 2.0, 3.0]); +``` + Convert to a `Vec` ```rust diff --git a/src/diesel_ext/halfvec.rs b/src/diesel_ext/halfvec.rs index 1bff526..8fb17b5 100644 --- a/src/diesel_ext/halfvec.rs +++ b/src/diesel_ext/halfvec.rs @@ -36,7 +36,6 @@ impl FromSql for HalfVector { mod tests { use crate::{HalfVector, VectorExpressionMethods}; use diesel::prelude::*; - use half::f16; table! { use diesel::sql_types::*; @@ -74,25 +73,13 @@ mod tests { let new_items = vec![ NewItem { - embedding: Some(HalfVector::from(vec![ - f16::from_f32(1.0), - f16::from_f32(1.0), - f16::from_f32(1.0), - ])), + embedding: Some(HalfVector::from_f32_slice(&[1.0, 1.0, 1.0])), }, NewItem { - embedding: Some(HalfVector::from(vec![ - f16::from_f32(2.0), - f16::from_f32(2.0), - f16::from_f32(2.0), - ])), + embedding: Some(HalfVector::from_f32_slice(&[2.0, 2.0, 2.0])), }, NewItem { - embedding: Some(HalfVector::from(vec![ - f16::from_f32(1.0), - f16::from_f32(1.0), - f16::from_f32(2.0), - ])), + embedding: Some(HalfVector::from_f32_slice(&[1.0, 1.0, 2.0])), }, NewItem { embedding: None }, ]; @@ -105,11 +92,7 @@ mod tests { assert_eq!(4, all.len()); let neighbors = items::table - .order(items::embedding.l2_distance(HalfVector::from(vec![ - f16::from_f32(1.0), - f16::from_f32(1.0), - f16::from_f32(1.0), - ]))) + .order(items::embedding.l2_distance(HalfVector::from_f32_slice(&[1.0, 1.0, 1.0]))) .limit(5) .load::(&mut conn)?; assert_eq!( @@ -117,20 +100,12 @@ mod tests { neighbors.iter().map(|v| v.id).collect::>() ); assert_eq!( - Some(HalfVector::from(vec![ - f16::from_f32(1.0), - f16::from_f32(1.0), - f16::from_f32(1.0) - ])), + Some(HalfVector::from_f32_slice(&[1.0, 1.0, 1.0])), neighbors.first().unwrap().embedding ); let neighbors = items::table - .order(items::embedding.max_inner_product(HalfVector::from(vec![ - f16::from_f32(1.0), - f16::from_f32(1.0), - f16::from_f32(1.0), - ]))) + .order(items::embedding.max_inner_product(HalfVector::from_f32_slice(&[1.0, 1.0, 1.0]))) .limit(5) .load::(&mut conn)?; assert_eq!( @@ -139,11 +114,7 @@ mod tests { ); let neighbors = items::table - .order(items::embedding.cosine_distance(HalfVector::from(vec![ - f16::from_f32(1.0), - f16::from_f32(1.0), - f16::from_f32(1.0), - ]))) + .order(items::embedding.cosine_distance(HalfVector::from_f32_slice(&[1.0, 1.0, 1.0]))) .limit(5) .load::(&mut conn)?; assert_eq!( @@ -152,11 +123,7 @@ mod tests { ); let neighbors = items::table - .order(items::embedding.l1_distance(HalfVector::from(vec![ - f16::from_f32(1.0), - f16::from_f32(1.0), - f16::from_f32(1.0), - ]))) + .order(items::embedding.l1_distance(HalfVector::from_f32_slice(&[1.0, 1.0, 1.0]))) .limit(5) .load::(&mut conn)?; assert_eq!( @@ -165,11 +132,9 @@ mod tests { ); let distances = items::table - .select(items::embedding.max_inner_product(HalfVector::from(vec![ - f16::from_f32(1.0), - f16::from_f32(1.0), - f16::from_f32(1.0), - ]))) + .select( + items::embedding.max_inner_product(HalfVector::from_f32_slice(&[1.0, 1.0, 1.0])), + ) .order(items::id) .load::>(&mut conn)?; assert_eq!(vec![Some(-3.0), Some(-6.0), Some(-4.0), None], distances); diff --git a/src/halfvec.rs b/src/halfvec.rs index 28eedd8..27b675d 100644 --- a/src/halfvec.rs +++ b/src/halfvec.rs @@ -25,6 +25,11 @@ impl From for Vec { } impl HalfVector { + /// Creates a half vector from a `f32` slice. + pub fn from_f32_slice(slice: &[f32]) -> HalfVector { + HalfVector(slice.iter().map(|v| f16::from_f32(*v)).collect()) + } + /// Returns a copy of the half vector as a `Vec`. pub fn to_vec(&self) -> Vec { self.0.clone() @@ -76,11 +81,7 @@ mod tests { #[test] fn test_to_vec() { - let vec = HalfVector::from(vec![ - f16::from_f32(1.0), - f16::from_f32(2.0), - f16::from_f32(3.0), - ]); + let vec = HalfVector::from_f32_slice(&[1.0, 2.0, 3.0]); assert_eq!( vec.to_vec(), vec![f16::from_f32(1.0), f16::from_f32(2.0), f16::from_f32(3.0)] @@ -89,11 +90,7 @@ mod tests { #[test] fn test_as_slice() { - let vec = HalfVector::from(vec![ - f16::from_f32(1.0), - f16::from_f32(2.0), - f16::from_f32(3.0), - ]); + let vec = HalfVector::from_f32_slice(&[1.0, 2.0, 3.0]); assert_eq!( vec.as_slice(), &[f16::from_f32(1.0), f16::from_f32(2.0), f16::from_f32(3.0)] diff --git a/src/postgres_ext/halfvec.rs b/src/postgres_ext/halfvec.rs index 43897bf..5ec2574 100644 --- a/src/postgres_ext/halfvec.rs +++ b/src/postgres_ext/halfvec.rs @@ -59,26 +59,14 @@ mod tests { &[], )?; - let vec = HalfVector::from(vec![ - f16::from_f32(1.0), - f16::from_f32(2.0), - f16::from_f32(3.0), - ]); - let vec2 = HalfVector::from(vec![ - f16::from_f32(4.0), - f16::from_f32(5.0), - f16::from_f32(6.0), - ]); + let vec = HalfVector::from_f32_slice(&[1.0, 2.0, 3.0]); + let vec2 = HalfVector::from_f32_slice(&[4.0, 5.0, 6.0]); client.execute( "INSERT INTO postgres_half_items (embedding) VALUES ($1), ($2), (NULL)", &[&vec, &vec2], )?; - let query_vec = HalfVector::from(vec![ - f16::from_f32(3.0), - f16::from_f32(1.0), - f16::from_f32(2.0), - ]); + let query_vec = HalfVector::from_f32_slice(&[3.0, 1.0, 2.0]); let row = client.query_one( "SELECT embedding FROM postgres_half_items ORDER BY embedding <-> $1 LIMIT 1", &[&query_vec], diff --git a/src/sqlx_ext/halfvec.rs b/src/sqlx_ext/halfvec.rs index e505f56..0a5b807 100644 --- a/src/sqlx_ext/halfvec.rs +++ b/src/sqlx_ext/halfvec.rs @@ -65,27 +65,15 @@ mod tests { .execute(&pool) .await?; - let vec = HalfVector::from(vec![ - f16::from_f32(1.0), - f16::from_f32(2.0), - f16::from_f32(3.0), - ]); - let vec2 = HalfVector::from(vec![ - f16::from_f32(4.0), - f16::from_f32(5.0), - f16::from_f32(6.0), - ]); + let vec = HalfVector::from_f32_slice(&[1.0, 2.0, 3.0]); + let vec2 = HalfVector::from_f32_slice(&[4.0, 5.0, 6.0]); sqlx::query("INSERT INTO sqlx_half_items (embedding) VALUES ($1), ($2), (NULL)") .bind(&vec) .bind(&vec2) .execute(&pool) .await?; - let query_vec = HalfVector::from(vec![ - f16::from_f32(3.0), - f16::from_f32(1.0), - f16::from_f32(2.0), - ]); + let query_vec = HalfVector::from_f32_slice(&[3.0, 1.0, 2.0]); let row = sqlx::query("SELECT embedding FROM sqlx_half_items ORDER BY embedding <-> $1 LIMIT 1") .bind(query_vec) From bf0be98b2e0b8fc25b1f371ff83065b344047848 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 5 Mar 2025 14:00:46 -0800 Subject: [PATCH 13/28] Updated readme [skip ci] --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index de74de7..4522f37 100644 --- a/README.md +++ b/README.md @@ -296,7 +296,7 @@ use pgvector::HalfVector; let vec = HalfVector::from(vec![f16::from_f32(1.0), f16::from_f32(2.0), f16::from_f32(3.0)]); ``` -or a `f32` slice [unreleased] +Or a `f32` slice [unreleased] ```rust let vec = HalfVector::from_f32_slice(&[1.0, 2.0, 3.0]); @@ -324,7 +324,7 @@ use pgvector::Bit; let vec = Bit::new(&[true, false, true]); ``` -or a slice of bytes +Or a slice of bytes ```rust let vec = Bit::from_bytes(&[0b00000000, 0b11111111]); @@ -352,7 +352,7 @@ use pgvector::SparseVector; let vec = SparseVector::from_dense(vec![1.0, 0.0, 2.0, 0.0, 3.0, 0.0]); ``` -or a map of non-zero elements +Or a map of non-zero elements ```rust let map = HashMap::from([(0, 1.0), (2, 2.0), (4, 3.0)]); From 0b6bf971c43c6d3688de536dae5e606af677bc14 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 15 Apr 2025 10:09:33 -0700 Subject: [PATCH 14/28] Updated Cohere example [skip ci] --- examples/cohere/src/main.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/cohere/src/main.rs b/examples/cohere/src/main.rs index c4b6bd7..7d7cbab 100644 --- a/examples/cohere/src/main.rs +++ b/examples/cohere/src/main.rs @@ -13,7 +13,7 @@ fn main() -> Result<(), Box> { client.execute("CREATE EXTENSION IF NOT EXISTS vector", &[])?; client.execute("DROP TABLE IF EXISTS documents", &[])?; client.execute( - "CREATE TABLE documents (id serial PRIMARY KEY, content text, embedding bit(1024))", + "CREATE TABLE documents (id serial PRIMARY KEY, content text, embedding bit(1536))", &[], )?; @@ -47,11 +47,11 @@ fn main() -> Result<(), Box> { fn embed(texts: &[&str], input_type: &str) -> Result>, Box> { let api_key = std::env::var("CO_API_KEY").or(Err("Set CO_API_KEY"))?; - let response: Value = ureq::post("https://api.cohere.com/v1/embed") + let response: Value = ureq::post("https://api.cohere.com/v2/embed") .header("Authorization", &format!("Bearer {}", api_key)) .send_json(serde_json::json!({ "texts": texts, - "model": "embed-english-v3.0", + "model": "embed-v4.0", "input_type": input_type, "embedding_types": &["ubinary"], }))? From 0d95dc172b1e3890fbfc40a6f304a23ee3ff64d8 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 11 May 2025 20:30:23 -0700 Subject: [PATCH 15/28] Improved example [skip ci] --- examples/disco/src/main.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/disco/src/main.rs b/examples/disco/src/main.rs index c677e8a..58f070f 100644 --- a/examples/disco/src/main.rs +++ b/examples/disco/src/main.rs @@ -30,7 +30,7 @@ fn main() -> Result<(), Box> { &[], )?; - let data = load_movielens(Path::new(&movielens_path)); + let data = load_movielens(Path::new(&movielens_path))?; let recommender = RecommenderBuilder::new().factors(20).fit_explicit(&data); for user_id in recommender.user_ids() { @@ -66,37 +66,37 @@ fn main() -> Result<(), Box> { Ok(()) } -fn load_movielens(path: &Path) -> Dataset { +fn load_movielens(path: &Path) -> Result, Box> { // read movies, removing invalid UTF-8 bytes let mut movies = HashMap::new(); - let mut movies_file = File::open(path.join("u.item")).unwrap(); + let mut movies_file = File::open(path.join("u.item"))?; let mut buf = Vec::new(); - movies_file.read_to_end(&mut buf).unwrap(); + movies_file.read_to_end(&mut buf)?; let movies_data = String::from_utf8_lossy(&buf); let mut rdr = ReaderBuilder::new() .has_headers(false) .delimiter(b'|') .from_reader(movies_data.as_bytes()); for record in rdr.records() { - let row = record.unwrap(); + let row = record?; movies.insert(row[0].to_string(), row[1].to_string()); } // read ratings and create dataset let mut data = Dataset::new(); - let ratings_file = File::open(path.join("u.data")).unwrap(); + let ratings_file = File::open(path.join("u.data"))?; let mut rdr = ReaderBuilder::new() .has_headers(false) .delimiter(b'\t') .from_reader(ratings_file); for record in rdr.records() { - let row = record.unwrap(); + let row = record?; data.push( - row[0].parse::().unwrap(), + row[0].parse::()?, movies.get(&row[1]).unwrap().to_string(), - row[2].parse().unwrap(), + row[2].parse()?, ); } - data + Ok(data) } From 8c9d559e665177b0e38c266ae31e38a665534e95 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 11 May 2025 20:35:05 -0700 Subject: [PATCH 16/28] Improved example [skip ci] --- examples/disco/Cargo.toml | 1 - examples/disco/src/main.rs | 25 ++++++++++--------------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/examples/disco/Cargo.toml b/examples/disco/Cargo.toml index 79479e3..d8781e1 100644 --- a/examples/disco/Cargo.toml +++ b/examples/disco/Cargo.toml @@ -5,7 +5,6 @@ edition = "2021" publish = false [dependencies] -csv = "1" discorec = "0.2" pgvector = { path = "../..", features = ["postgres"] } postgres = "0.19" diff --git a/examples/disco/src/main.rs b/examples/disco/src/main.rs index 58f070f..cc58ca4 100644 --- a/examples/disco/src/main.rs +++ b/examples/disco/src/main.rs @@ -1,11 +1,10 @@ -use csv::ReaderBuilder; use discorec::{Dataset, RecommenderBuilder}; use pgvector::Vector; use postgres::{Client, NoTls}; use std::collections::HashMap; use std::error::Error; use std::fs::File; -use std::io::Read; +use std::io::{BufRead, BufReader, Read}; use std::path::Path; fn main() -> Result<(), Box> { @@ -73,27 +72,23 @@ fn load_movielens(path: &Path) -> Result, Box> { let mut buf = Vec::new(); movies_file.read_to_end(&mut buf)?; let movies_data = String::from_utf8_lossy(&buf); - let mut rdr = ReaderBuilder::new() - .has_headers(false) - .delimiter(b'|') - .from_reader(movies_data.as_bytes()); - for record in rdr.records() { - let row = record?; + let rdr = BufReader::new(movies_data.as_bytes()); + for line in rdr.lines() { + let line = line?; + let row: Vec<_> = line.split('|').collect(); movies.insert(row[0].to_string(), row[1].to_string()); } // read ratings and create dataset let mut data = Dataset::new(); let ratings_file = File::open(path.join("u.data"))?; - let mut rdr = ReaderBuilder::new() - .has_headers(false) - .delimiter(b'\t') - .from_reader(ratings_file); - for record in rdr.records() { - let row = record?; + let rdr = BufReader::new(ratings_file); + for line in rdr.lines() { + let line = line?; + let row: Vec<_> = line.split('\t').collect(); data.push( row[0].parse::()?, - movies.get(&row[1]).unwrap().to_string(), + movies.get(row[1]).unwrap().to_string(), row[2].parse()?, ); } From 8994a09902d451d57a444bae8a5de0f4a30f8a12 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 20 May 2025 15:33:25 -0700 Subject: [PATCH 17/28] Version bump to 0.4.1 [skip ci] --- CHANGELOG.md | 2 +- Cargo.toml | 2 +- README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e4bb3b..e313441 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.4.1 (unreleased) +## 0.4.1 (2025-05-20) - Added `from_f32_slice` function to `HalfVector` diff --git a/Cargo.toml b/Cargo.toml index c1406b8..81d298c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pgvector" -version = "0.4.0" +version = "0.4.1" description = "pgvector support for Rust" repository = "https://github.com/pgvector/pgvector-rust" license = "MIT OR Apache-2.0" diff --git a/README.md b/README.md index 4522f37..ddce2f4 100644 --- a/README.md +++ b/README.md @@ -296,7 +296,7 @@ use pgvector::HalfVector; let vec = HalfVector::from(vec![f16::from_f32(1.0), f16::from_f32(2.0), f16::from_f32(3.0)]); ``` -Or a `f32` slice [unreleased] +Or a `f32` slice ```rust let vec = HalfVector::from_f32_slice(&[1.0, 2.0, 3.0]); From bd39598c7d06bbe948749ea400adc5fd99762483 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 21 May 2025 18:17:04 -0700 Subject: [PATCH 18/28] Improved example [skip ci] --- examples/loading/src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/loading/src/main.rs b/examples/loading/src/main.rs index 880a21e..8c9ca6e 100644 --- a/examples/loading/src/main.rs +++ b/examples/loading/src/main.rs @@ -36,13 +36,13 @@ fn main() -> Result<(), Box> { let writer = client.copy_in("COPY items (embedding) FROM STDIN WITH (FORMAT BINARY)")?; let mut writer = BinaryCopyInWriter::new(writer, &[vector_type]); for (i, embedding) in embeddings.into_iter().enumerate() { + writer.write(&[&Vector::from(embedding)])?; + // show progress if i % 10000 == 0 { print!("."); io::stdout().flush()?; } - - writer.write(&[&Vector::from(embedding)])?; } writer.finish()?; println!("\nSuccess!"); From 40b1c1160d66b9240158ccd4ef11da3d0e78cf63 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 21 May 2025 21:52:47 -0700 Subject: [PATCH 19/28] Improved example [skip ci] --- examples/disco/src/main.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/examples/disco/src/main.rs b/examples/disco/src/main.rs index cc58ca4..a7363b3 100644 --- a/examples/disco/src/main.rs +++ b/examples/disco/src/main.rs @@ -75,8 +75,10 @@ fn load_movielens(path: &Path) -> Result, Box> { let rdr = BufReader::new(movies_data.as_bytes()); for line in rdr.lines() { let line = line?; - let row: Vec<_> = line.split('|').collect(); - movies.insert(row[0].to_string(), row[1].to_string()); + let mut row = line.split('|'); + let id = row.next().unwrap().to_string(); + let name = row.next().unwrap().to_string(); + movies.insert(id, name); } // read ratings and create dataset @@ -85,12 +87,11 @@ fn load_movielens(path: &Path) -> Result, Box> { let rdr = BufReader::new(ratings_file); for line in rdr.lines() { let line = line?; - let row: Vec<_> = line.split('\t').collect(); - data.push( - row[0].parse::()?, - movies.get(row[1]).unwrap().to_string(), - row[2].parse()?, - ); + let mut row = line.split('\t'); + let user_id: i32 = row.next().unwrap().parse()?; + let item_id = movies.get(row.next().unwrap()).unwrap().to_string(); + let rating: f32 = row.next().unwrap().parse()?; + data.push(user_id, item_id, rating); } Ok(data) From 6b41d7ed73c9bff0443e9debf096c88cdee23fda Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 21 May 2025 21:55:14 -0700 Subject: [PATCH 20/28] Improved example [skip ci] --- examples/disco/src/main.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/disco/src/main.rs b/examples/disco/src/main.rs index a7363b3..d36e5e4 100644 --- a/examples/disco/src/main.rs +++ b/examples/disco/src/main.rs @@ -67,7 +67,7 @@ fn main() -> Result<(), Box> { fn load_movielens(path: &Path) -> Result, Box> { // read movies, removing invalid UTF-8 bytes - let mut movies = HashMap::new(); + let mut movies = HashMap::with_capacity(2000); let mut movies_file = File::open(path.join("u.item"))?; let mut buf = Vec::new(); movies_file.read_to_end(&mut buf)?; @@ -82,15 +82,15 @@ fn load_movielens(path: &Path) -> Result, Box> { } // read ratings and create dataset - let mut data = Dataset::new(); + let mut data = Dataset::with_capacity(100000); let ratings_file = File::open(path.join("u.data"))?; let rdr = BufReader::new(ratings_file); for line in rdr.lines() { let line = line?; let mut row = line.split('\t'); - let user_id: i32 = row.next().unwrap().parse()?; + let user_id = row.next().unwrap().parse()?; let item_id = movies.get(row.next().unwrap()).unwrap().to_string(); - let rating: f32 = row.next().unwrap().parse()?; + let rating = row.next().unwrap().parse()?; data.push(user_id, item_id, rating); } From 017ebf58152545cdee0b7524aa72ae72a34792e9 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 21 May 2025 22:38:00 -0700 Subject: [PATCH 21/28] Improved example [skip ci] --- examples/disco/src/main.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/examples/disco/src/main.rs b/examples/disco/src/main.rs index d36e5e4..8cc97ce 100644 --- a/examples/disco/src/main.rs +++ b/examples/disco/src/main.rs @@ -4,7 +4,7 @@ use postgres::{Client, NoTls}; use std::collections::HashMap; use std::error::Error; use std::fs::File; -use std::io::{BufRead, BufReader, Read}; +use std::io::{BufRead, BufReader}; use std::path::Path; fn main() -> Result<(), Box> { @@ -68,17 +68,19 @@ fn main() -> Result<(), Box> { fn load_movielens(path: &Path) -> Result, Box> { // read movies, removing invalid UTF-8 bytes let mut movies = HashMap::with_capacity(2000); - let mut movies_file = File::open(path.join("u.item"))?; + let movies_file = File::open(path.join("u.item"))?; + let mut rdr = BufReader::new(movies_file); let mut buf = Vec::new(); - movies_file.read_to_end(&mut buf)?; - let movies_data = String::from_utf8_lossy(&buf); - let rdr = BufReader::new(movies_data.as_bytes()); - for line in rdr.lines() { - let line = line?; + while let Ok(len) = rdr.read_until(b'\n', &mut buf) { + if len == 0 { + break; + } + let line = String::from_utf8_lossy(&buf); let mut row = line.split('|'); let id = row.next().unwrap().to_string(); let name = row.next().unwrap().to_string(); movies.insert(id, name); + buf.clear(); } // read ratings and create dataset From 39bf1010e24bc44f752303465734f7671f7cc45a Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 21 May 2025 22:53:03 -0700 Subject: [PATCH 22/28] Improved naming [skip ci] --- examples/disco/src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/disco/src/main.rs b/examples/disco/src/main.rs index 8cc97ce..56fd83a 100644 --- a/examples/disco/src/main.rs +++ b/examples/disco/src/main.rs @@ -71,8 +71,8 @@ fn load_movielens(path: &Path) -> Result, Box> { let movies_file = File::open(path.join("u.item"))?; let mut rdr = BufReader::new(movies_file); let mut buf = Vec::new(); - while let Ok(len) = rdr.read_until(b'\n', &mut buf) { - if len == 0 { + while let Ok(bytes_read) = rdr.read_until(b'\n', &mut buf) { + if bytes_read == 0 { break; } let line = String::from_utf8_lossy(&buf); From 8e4be148ec405a7a3fa5026590443aa4999cfdcd Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 21 May 2025 22:55:54 -0700 Subject: [PATCH 23/28] Improved example [skip ci] --- examples/disco/src/main.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/disco/src/main.rs b/examples/disco/src/main.rs index 56fd83a..ea0a7df 100644 --- a/examples/disco/src/main.rs +++ b/examples/disco/src/main.rs @@ -71,7 +71,8 @@ fn load_movielens(path: &Path) -> Result, Box> { let movies_file = File::open(path.join("u.item"))?; let mut rdr = BufReader::new(movies_file); let mut buf = Vec::new(); - while let Ok(bytes_read) = rdr.read_until(b'\n', &mut buf) { + loop { + let bytes_read = rdr.read_until(b'\n', &mut buf)?; if bytes_read == 0 { break; } From 123af34a73c5a8ab3beed6f4fdab52bcb5dfdf67 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 21 May 2025 23:02:26 -0700 Subject: [PATCH 24/28] Updated example [skip ci] --- examples/candle/Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/candle/Cargo.toml b/examples/candle/Cargo.toml index 9c2728f..de30618 100644 --- a/examples/candle/Cargo.toml +++ b/examples/candle/Cargo.toml @@ -5,9 +5,9 @@ edition = "2021" publish = false [dependencies] -candle-core = "0.8" -candle-nn = "0.8" -candle-transformers = "0.8" +candle-core = "0.9" +candle-nn = "0.9" +candle-transformers = "0.9" hf-hub = "0.4" pgvector = { path = "../..", features = ["postgres"] } postgres = "0.19" From 5f51d5cf0ac02606b414e40f9c93ff98baa0d7ee Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 21 May 2025 23:04:46 -0700 Subject: [PATCH 25/28] Updated example [skip ci] --- examples/hybrid_search/Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/hybrid_search/Cargo.toml b/examples/hybrid_search/Cargo.toml index 9c2728f..de30618 100644 --- a/examples/hybrid_search/Cargo.toml +++ b/examples/hybrid_search/Cargo.toml @@ -5,9 +5,9 @@ edition = "2021" publish = false [dependencies] -candle-core = "0.8" -candle-nn = "0.8" -candle-transformers = "0.8" +candle-core = "0.9" +candle-nn = "0.9" +candle-transformers = "0.9" hf-hub = "0.4" pgvector = { path = "../..", features = ["postgres"] } postgres = "0.19" From 0c26f0e0c1343e8dd3d74408ec99956fcb3f3b2c Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 21 May 2025 23:32:58 -0700 Subject: [PATCH 26/28] Improved example [skip ci] --- examples/disco/src/main.rs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/examples/disco/src/main.rs b/examples/disco/src/main.rs index ea0a7df..1dd698c 100644 --- a/examples/disco/src/main.rs +++ b/examples/disco/src/main.rs @@ -69,19 +69,14 @@ fn load_movielens(path: &Path) -> Result, Box> { // read movies, removing invalid UTF-8 bytes let mut movies = HashMap::with_capacity(2000); let movies_file = File::open(path.join("u.item"))?; - let mut rdr = BufReader::new(movies_file); - let mut buf = Vec::new(); - loop { - let bytes_read = rdr.read_until(b'\n', &mut buf)?; - if bytes_read == 0 { - break; - } - let line = String::from_utf8_lossy(&buf); + let rdr = BufReader::new(movies_file); + for line in rdr.split(b'\n') { + let line = line?; + let line = String::from_utf8_lossy(&line); let mut row = line.split('|'); let id = row.next().unwrap().to_string(); let name = row.next().unwrap().to_string(); movies.insert(id, name); - buf.clear(); } // read ratings and create dataset From ed1a1a7b5e19488b5f204216e46298c34bd22955 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 21 May 2025 23:45:32 -0700 Subject: [PATCH 27/28] Improved example [skip ci] --- examples/disco/src/main.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/examples/disco/src/main.rs b/examples/disco/src/main.rs index 1dd698c..7bead2c 100644 --- a/examples/disco/src/main.rs +++ b/examples/disco/src/main.rs @@ -66,13 +66,18 @@ fn main() -> Result<(), Box> { } fn load_movielens(path: &Path) -> Result, Box> { - // read movies, removing invalid UTF-8 bytes + // read movies let mut movies = HashMap::with_capacity(2000); let movies_file = File::open(path.join("u.item"))?; let rdr = BufReader::new(movies_file); for line in rdr.split(b'\n') { let line = line?; - let line = String::from_utf8_lossy(&line); + // convert encoding to UTF-8 + let line = String::from_utf8( + line.into_iter() + .flat_map(|v| if v < 128 { vec![v] } else { vec![195, v - 64] }) + .collect(), + )?; let mut row = line.split('|'); let id = row.next().unwrap().to_string(); let name = row.next().unwrap().to_string(); From 6daed207429cd73975fa7bee02bf3af53abff472 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 22 May 2025 00:14:06 -0700 Subject: [PATCH 28/28] Improved example [skip ci] --- examples/disco/src/main.rs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/examples/disco/src/main.rs b/examples/disco/src/main.rs index 7bead2c..62f654a 100644 --- a/examples/disco/src/main.rs +++ b/examples/disco/src/main.rs @@ -6,6 +6,7 @@ use std::error::Error; use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::Path; +use std::string::FromUtf8Error; fn main() -> Result<(), Box> { // https://grouplens.org/datasets/movielens/100k/ @@ -72,12 +73,7 @@ fn load_movielens(path: &Path) -> Result, Box> { let rdr = BufReader::new(movies_file); for line in rdr.split(b'\n') { let line = line?; - // convert encoding to UTF-8 - let line = String::from_utf8( - line.into_iter() - .flat_map(|v| if v < 128 { vec![v] } else { vec![195, v - 64] }) - .collect(), - )?; + let line = convert_to_utf8(&line)?; let mut row = line.split('|'); let id = row.next().unwrap().to_string(); let name = row.next().unwrap().to_string(); @@ -99,3 +95,17 @@ fn load_movielens(path: &Path) -> Result, Box> { Ok(data) } + +// ISO-8859-1 to UTF-8 +fn convert_to_utf8(s: &[u8]) -> Result { + let mut buf = Vec::with_capacity(s.len() + 10); + for v in s { + if *v < 128 { + buf.push(*v); + } else { + buf.push(195); + buf.push(*v - 64); + } + } + String::from_utf8(buf) +} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy