// Copyright (c) 2023-2025 ParadeDB, Inc. // // This file is part of ParadeDB - Postgres for Search and Analytics // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . mod fixtures; use core::panic; use fixtures::*; use pretty_assertions::assert_eq; use rstest::*; use sqlx::{PgConnection, Row}; #[rstest] fn boolean_tree(mut conn: PgConnection) { SimpleProductsTable::setup().execute(&mut conn); let columns: SimpleProductsTableVec = r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ paradedb.boolean( should => ARRAY[ paradedb.parse('description:shoes'), paradedb.phrase_prefix(field => 'description', phrases => ARRAY['book']), paradedb.term(field => 'description', value => 'speaker'), paradedb.fuzzy_term(field => 'description', value => 'wolo', transposition_cost_one => false, distance => 1, prefix => true) ] ) ORDER BY id; "# .fetch_collect(&mut conn); assert_eq!(columns.id, vec![3, 4, 5, 7, 10, 32, 33, 34, 37, 39, 41]); } #[rstest] fn fuzzy_term(mut conn: PgConnection) { SimpleProductsTable::setup().execute(&mut conn); let columns: SimpleProductsTableVec = r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ paradedb.fuzzy_term(field => 'category', value => 'elector', prefix => true) ORDER BY id"# .fetch_collect(&mut conn); assert_eq!(columns.id, vec![1, 2, 12, 22, 32], "wrong results"); let columns: SimpleProductsTableVec = r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ paradedb.term(field => 'category', value => 'electornics') ORDER BY id"# .fetch_collect(&mut conn); assert!(columns.is_empty(), "without fuzzy field should be empty"); let columns: SimpleProductsTableVec = r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ paradedb.fuzzy_term( field => 'description', value => 'keybaord', transposition_cost_one => false, distance => 1, prefix => true ) ORDER BY id"# .fetch_collect(&mut conn); assert!( columns.is_empty(), "transposition_cost_one false should be empty" ); let columns: SimpleProductsTableVec = r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ paradedb.fuzzy_term( field => 'description', value => 'keybaord', transposition_cost_one => true, distance => 1, prefix => true ) ORDER BY id"# .fetch_collect(&mut conn); assert_eq!( columns.id, vec![1, 2], "incorrect transposition_cost_one true" ); let columns: SimpleProductsTableVec = r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ paradedb.fuzzy_term( field => 'description', value => 'keybaord', prefix => true ) ORDER BY id"# .fetch_collect(&mut conn); assert_eq!(columns.id, vec![1, 2], "incorrect defaults"); } #[rstest] fn single_queries(mut conn: PgConnection) { SimpleProductsTable::setup().execute(&mut conn); // All let columns: SimpleProductsTableVec = r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ paradedb.all() ORDER BY id"# .fetch_collect(&mut conn); assert_eq!(columns.len(), 41); // Boost let columns: SimpleProductsTableVec = r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ paradedb.boost(query => paradedb.all(), factor => 1.5) ORDER BY id"# .fetch_collect(&mut conn); assert_eq!(columns.len(), 41); // ConstScore let columns: SimpleProductsTableVec = r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ paradedb.const_score(query => paradedb.all(), score => 3.9) ORDER BY id"# .fetch_collect(&mut conn); assert_eq!(columns.len(), 41); // DisjunctionMax let columns: SimpleProductsTableVec = r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ paradedb.disjunction_max(disjuncts => ARRAY[paradedb.parse('description:shoes')]) ORDER BY id"# .fetch_collect(&mut conn); assert_eq!(columns.len(), 3); // Empty let columns: SimpleProductsTableVec = r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ paradedb.empty() ORDER BY id"# .fetch_collect(&mut conn); assert_eq!(columns.len(), 0); // FuzzyTerm let columns: SimpleProductsTableVec = r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ paradedb.fuzzy_term( field => 'description', value => 'wolo', transposition_cost_one => false, distance => 1, prefix => true ) ORDER BY ID"# .fetch_collect(&mut conn); assert_eq!(columns.len(), 4); // Parse let columns: SimpleProductsTableVec = r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ paradedb.parse('description:teddy') ORDER BY id"# .fetch_collect(&mut conn); assert_eq!(columns.len(), 1); // PhrasePrefix let columns: SimpleProductsTableVec = r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ paradedb.phrase_prefix(field => 'description', phrases => ARRAY['har']) ORDER BY id"# .fetch_collect(&mut conn); assert_eq!(columns.len(), 1); // Phrase with invalid term list match r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ paradedb.phrase(field => 'description', phrases => ARRAY['robot']) ORDER BY id"# .fetch_result::(&mut conn) { Err(err) => assert!(err .to_string() .contains("required to have strictly more than one term")), _ => panic!("phrase prefix query should require multiple terms"), } // Phrase let columns: SimpleProductsTableVec = r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ paradedb.phrase( field => 'description', phrases => ARRAY['robot', 'building', 'kit'] ) ORDER BY id"# .fetch_collect(&mut conn); assert_eq!(columns.len(), 1); // Range let columns: SimpleProductsTableVec = r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ paradedb.range(field => 'last_updated_date', range => '[2023-05-01,2023-05-03]'::daterange) ORDER BY id"# .fetch_collect(&mut conn); assert_eq!(columns.len(), 7); // Regex let columns: SimpleProductsTableVec = r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ paradedb.regex( field => 'description', pattern => '(hardcover|plush|leather|running|wireless)' ) ORDER BY id"# .fetch_collect(&mut conn); assert_eq!(columns.len(), 5); // Test regex anchors let columns: SimpleProductsTableVec = r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ paradedb.regex( field => 'description', pattern => '^running' ) ORDER BY id"# .fetch_collect(&mut conn); assert_eq!( columns.len(), 1, "start anchor ^ should match exactly one item" ); let columns: SimpleProductsTableVec = r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ paradedb.regex( field => 'description', pattern => 'keyboard$' ) ORDER BY id"# .fetch_collect(&mut conn); assert_eq!(columns.len(), 2, "end anchor $ should match two items"); // Regex Phrase let columns: SimpleProductsTableVec = r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ paradedb.regex_phrase( field => 'description', regexes => ARRAY['.*bot', '.*ing', 'kit'] ) ORDER BY id"# .fetch_collect(&mut conn); assert_eq!(columns.len(), 1); let columns: SimpleProductsTableVec = r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ '{ "regex_phrase": { "field": "description", "regexes": [".*eek", "shoes"], "slop": 1, "max_expansion": 10 } }'::jsonb;"# .fetch_collect(&mut conn); assert_eq!(columns.len(), 1); // Regex Phrase let columns: SimpleProductsTableVec = r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ paradedb.regex_phrase( field => 'description', regexes => ARRAY['.*bot', '.*ing', 'kit'] ) ORDER BY id"# .fetch_collect(&mut conn); assert_eq!(columns.len(), 1); let columns: SimpleProductsTableVec = r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ '{ "regex_phrase": { "field": "description", "regexes": [".*eek", "shoes"], "slop": 1, "max_expansion": 10 } }'::jsonb;"# .fetch_collect(&mut conn); assert_eq!(columns.len(), 1); // Term let columns: SimpleProductsTableVec = r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ paradedb.term(field => 'description', value => 'shoes') ORDER BY id"# .fetch_collect(&mut conn); assert_eq!(columns.len(), 3); // // NB: This once worked, but the capability was removed when the new "pdb.*" builder functions // were added. The general problem is that there's no longer a clean way to indicate // the desire to "search all column" // // // Term with no field (should search all columns) // let columns: SimpleProductsTableVec = r#" // SELECT * FROM paradedb.bm25_search // WHERE bm25_search @@@ paradedb.term(value => 'shoes') ORDER BY id"# // .fetch_collect(&mut conn); // assert_eq!(columns.len(), 3); // TermSet with invalid term list match r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ paradedb.term_set( terms => ARRAY[ paradedb.regex(field => 'description', pattern => '.+') ] ) ORDER BY id"# .fetch_result::(&mut conn) { Err(err) => assert!(err .to_string() .contains("only term queries can be passed to term_set")), _ => panic!("term set query should only accept terms"), } // TermSet let columns: SimpleProductsTableVec = r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ paradedb.term_set( terms => ARRAY[ paradedb.term(field => 'description', value => 'shoes'), paradedb.term(field => 'description', value => 'novel') ] ) ORDER BY id"# .fetch_collect(&mut conn); assert_eq!(columns.len(), 5); } #[rstest] fn exists_query(mut conn: PgConnection) { SimpleProductsTable::setup().execute(&mut conn); // Simple exists query let columns: SimpleProductsTableVec = r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ paradedb.exists('rating') "# .fetch_collect(&mut conn); assert_eq!(columns.len(), 41); // Non fast field should fail match r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ paradedb.exists('description') "# .execute_result(&mut conn) { Err(err) => assert!(err.to_string().contains("not a fast field")), _ => panic!("exists() over non-fast field should fail"), } // Exists with boolean query "INSERT INTO paradedb.bm25_search (id, description, rating) VALUES (42, 'shoes', NULL)" .execute(&mut conn); let columns: SimpleProductsTableVec = r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ paradedb.boolean( must => ARRAY[ paradedb.exists('rating'), paradedb.parse('description:shoes') ] ) "# .fetch_collect(&mut conn); assert_eq!(columns.len(), 3); } #[rstest] fn more_like_this_raw(mut conn: PgConnection) { r#" CREATE TABLE test_more_like_this_table ( id SERIAL PRIMARY KEY, flavour TEXT ); INSERT INTO test_more_like_this_table (flavour) VALUES ('apple'), ('banana'), ('cherry'), ('banana split'); "# .execute(&mut conn); r#" CREATE INDEX test_more_like_this_index on test_more_like_this_table USING bm25 (id, flavour) WITH (key_field='id'); "# .execute(&mut conn); match r#" SELECT id, flavour FROM test_more_like_this_table WHERE test_more_like_this_table @@@ paradedb.more_like_this(); "# .fetch_result::<()>(&mut conn) { Err(err) => { assert_eq!(err .to_string() , "error returned from database: more_like_this must be called with either document_id or document_fields") } _ => panic!("document_id or document_fields validation failed"), } let rows: Vec<(i32, String)> = r#" SELECT id, flavour FROM test_more_like_this_table WHERE test_more_like_this_table @@@ paradedb.more_like_this( min_doc_frequency => 0, min_term_frequency => 0, document_fields => '{"flavour": "banana"}' ) ORDER BY id; "# .fetch_collect(&mut conn); assert_eq!(rows.len(), 2); let rows: Vec<(i32, String)> = r#" SELECT id, flavour FROM test_more_like_this_table WHERE test_more_like_this_table @@@ paradedb.more_like_this( min_doc_frequency => 0, min_term_frequency => 0, document_id => 2 ) ORDER BY id; "# .fetch_collect(&mut conn); assert_eq!(rows.len(), 2); } #[rstest] fn more_like_this_empty(mut conn: PgConnection) { r#" CREATE TABLE test_more_like_this_table ( id SERIAL PRIMARY KEY, flavour TEXT ); INSERT INTO test_more_like_this_table (flavour) VALUES ('apple'), ('banana'), ('cherry'), ('banana split'); "# .execute(&mut conn); r#" CREATE INDEX test_more_like_this_index on test_more_like_this_table USING bm25 (id, flavour) WITH (key_field='id'); "# .execute(&mut conn); match r#" SELECT id, flavour FROM test_more_like_this_table WHERE test_more_like_this_table @@@ paradedb.more_like_this() ORDER BY id; "# .fetch_result::<()>(&mut conn) { Err(err) => { assert_eq!(err .to_string() , "error returned from database: more_like_this must be called with either document_id or document_fields") } _ => panic!("document_id or document_fields validation failed"), } } #[rstest] fn more_like_this_text(mut conn: PgConnection) { r#" CREATE TABLE test_more_like_this_table ( id SERIAL PRIMARY KEY, flavour TEXT ); INSERT INTO test_more_like_this_table (flavour) VALUES ('apple'), ('banana'), ('cherry'), ('banana split'); "# .execute(&mut conn); r#" CREATE INDEX test_more_like_this_index on test_more_like_this_table USING bm25 (id, flavour) WITH (key_field='id'); "# .execute(&mut conn); let rows: Vec<(i32, String)> = r#" SELECT id, flavour FROM test_more_like_this_table WHERE test_more_like_this_table @@@ paradedb.more_like_this( min_doc_frequency => 0, min_term_frequency => 0, document_fields => '{"flavour": "banana"}' ) ORDER BY id; "# .fetch_collect(&mut conn); assert_eq!(rows.len(), 2); } #[rstest] fn more_like_this_boolean_key(mut conn: PgConnection) { r#" CREATE TABLE test_more_like_this_table ( id BOOLEAN PRIMARY KEY, flavour TEXT ); INSERT INTO test_more_like_this_table (id, flavour) VALUES (true, 'apple'), (false, 'banana') "# .execute(&mut conn); r#" CREATE INDEX test_more_like_this_index on test_more_like_this_table USING bm25 (id, flavour) WITH (key_field='id'); "# .execute(&mut conn); let rows: Vec<(bool, String)> = r#" SELECT id, flavour FROM test_more_like_this_table WHERE test_more_like_this_table @@@ paradedb.more_like_this( min_doc_frequency => 0, min_term_frequency => 0, document_fields => '{"flavour": "banana"}' ) ORDER BY id; "# .fetch_collect(&mut conn); assert_eq!(rows.len(), 1); } #[rstest] fn more_like_this_uuid_key(mut conn: PgConnection) { r#" CREATE TABLE test_more_like_this_table ( id UUID PRIMARY KEY, flavour TEXT ); INSERT INTO test_more_like_this_table (id, flavour) VALUES ('f159c89e-2162-48cd-85e3-e42b71d2ecd0', 'apple'), ('38bf27a0-1aa8-42cd-9cb0-993025e0b8d0', 'banana'), ('b5faacc0-9eba-441a-81f8-820b46a3b57e', 'cherry'), ('eb833eb6-c598-4042-b84a-0045828fceea', 'banana split'); "# .execute(&mut conn); r#" CREATE INDEX test_more_like_this_index on test_more_like_this_table USING bm25 (id, flavour) WITH (key_field='id'); "# .execute(&mut conn); let rows: Vec<(uuid::Uuid, String)> = r#" SELECT id, flavour FROM test_more_like_this_table WHERE test_more_like_this_table @@@ paradedb.more_like_this( min_doc_frequency => 0, min_term_frequency => 0, document_fields => '{"flavour": "banana"}' ) ORDER BY id; "# .fetch_collect(&mut conn); assert_eq!(rows.len(), 2); } #[rstest] fn more_like_this_i64_key(mut conn: PgConnection) { r#" CREATE TABLE test_more_like_this_table ( id BIGINT PRIMARY KEY, flavour TEXT ); INSERT INTO test_more_like_this_table (id, flavour) VALUES (1, 'apple'), (2, 'banana'), (3, 'cherry'), (4, 'banana split'); "# .execute(&mut conn); r#" CREATE INDEX test_more_like_this_index on test_more_like_this_table USING bm25 (id, flavour) WITH (key_field='id'); "# .execute(&mut conn); let rows: Vec<(i64, String)> = r#" SELECT id, flavour FROM test_more_like_this_table WHERE test_more_like_this_table @@@ paradedb.more_like_this( min_doc_frequency => 0, min_term_frequency => 0, document_fields => '{"flavour": "banana"}' ) ORDER BY id; "# .fetch_collect(&mut conn); assert_eq!(rows.len(), 2); } #[rstest] fn more_like_this_i32_key(mut conn: PgConnection) { r#" CREATE TABLE test_more_like_this_table ( id INT PRIMARY KEY, flavour TEXT ); INSERT INTO test_more_like_this_table (id, flavour) VALUES (1, 'apple'), (2, 'banana'), (3, 'cherry'), (4, 'banana split'); "# .execute(&mut conn); r#" CREATE INDEX test_more_like_this_index on test_more_like_this_table USING bm25 (id, flavour) WITH (key_field='id'); "# .execute(&mut conn); let rows: Vec<(i32, String)> = r#" SELECT id, flavour FROM test_more_like_this_table WHERE test_more_like_this_table @@@ paradedb.more_like_this( min_doc_frequency => 0, min_term_frequency => 0, document_fields => '{"flavour": "banana"}' ) ORDER BY id; "# .fetch_collect(&mut conn); assert_eq!(rows.len(), 2); } #[rstest] fn more_like_this_literal_cast(mut conn: PgConnection) { r#" CREATE TABLE test_more_like_this_table ( id INT PRIMARY KEY, year INTEGER ); INSERT INTO test_more_like_this_table (id, year) VALUES (1, 2012), (2, 2013), (3, 2014), (4, 2012); "# .execute(&mut conn); r#" CREATE INDEX test_more_like_this_index on test_more_like_this_table USING bm25 (id, year) WITH (key_field='id'); "# .execute(&mut conn); let rows: Vec<(i32, i32)> = r#" SELECT id, year FROM test_more_like_this_table WHERE test_more_like_this_table @@@ paradedb.more_like_this( min_doc_frequency => 0, min_term_frequency => 0, document_fields => '{"year": 2012}' ) ORDER BY id; "# .fetch_collect(&mut conn); assert_eq!(rows.len(), 2); } #[rstest] fn more_like_this_i16_key(mut conn: PgConnection) { r#" CREATE TABLE test_more_like_this_table ( id SMALLINT PRIMARY KEY, flavour TEXT ); INSERT INTO test_more_like_this_table (id, flavour) VALUES (1, 'apple'), (2, 'banana'), (3, 'cherry'), (4, 'banana split'); "# .execute(&mut conn); r#" CREATE INDEX test_more_like_this_index on test_more_like_this_table USING bm25 (id, flavour) WITH (key_field='id'); "# .execute(&mut conn); let rows: Vec<(i16, String)> = r#" SELECT id, flavour FROM test_more_like_this_table WHERE test_more_like_this_table @@@ paradedb.more_like_this( min_doc_frequency => 0, min_term_frequency => 0, document_fields => '{"flavour": "banana"}' ) ORDER BY id; "# .fetch_collect(&mut conn); assert_eq!(rows.len(), 2); } #[rstest] fn more_like_this_f32_key(mut conn: PgConnection) { r#" CREATE TABLE test_more_like_this_table ( id FLOAT4 PRIMARY KEY, flavour TEXT ); INSERT INTO test_more_like_this_table (id, flavour) VALUES (1.1, 'apple'), (2.2, 'banana'), (3.3, 'cherry'), (4.4, 'banana split'); "# .execute(&mut conn); r#" CREATE INDEX test_more_like_this_index on test_more_like_this_table USING bm25 (id, flavour) WITH (key_field='id'); "# .execute(&mut conn); let rows: Vec<(f32, String)> = r#" SELECT id, flavour FROM test_more_like_this_table WHERE test_more_like_this_table @@@ paradedb.more_like_this( min_doc_frequency => 0, min_term_frequency => 0, document_fields => '{"flavour": "banana"}' ) ORDER BY id; "# .fetch_collect(&mut conn); assert_eq!(rows.len(), 2); } #[rstest] fn more_like_this_f64_key(mut conn: PgConnection) { r#" CREATE TABLE test_more_like_this_table ( id FLOAT8 PRIMARY KEY, flavour TEXT ); INSERT INTO test_more_like_this_table (id, flavour) VALUES (1.1, 'apple'), (2.2, 'banana'), (3.3, 'cherry'), (4.4, 'banana split'); "# .execute(&mut conn); r#" CREATE INDEX test_more_like_this_index on test_more_like_this_table USING bm25 (id, flavour) WITH (key_field='id'); "# .execute(&mut conn); let rows: Vec<(f64, String)> = r#" SELECT id, flavour FROM test_more_like_this_table WHERE test_more_like_this_table @@@ paradedb.more_like_this( min_doc_frequency => 0, min_term_frequency => 0, document_fields => '{"flavour": "banana"}' ) ORDER BY id; "# .fetch_collect(&mut conn); assert_eq!(rows.len(), 2); } #[rstest] fn more_like_this_numeric_key(mut conn: PgConnection) { r#" CREATE TABLE test_more_like_this_table ( id NUMERIC PRIMARY KEY, flavour TEXT ); INSERT INTO test_more_like_this_table (id, flavour) VALUES (1.1, 'apple'), (2.2, 'banana'), (3.3, 'cherry'), (4.4, 'banana split'); "# .execute(&mut conn); r#" CREATE INDEX test_more_like_this_index on test_more_like_this_table USING bm25 (id, flavour) WITH (key_field='id'); "# .execute(&mut conn); let rows: Vec<(f64, String)> = r#" SELECT CAST(id AS FLOAT8), flavour FROM test_more_like_this_table WHERE test_more_like_this_table @@@ paradedb.more_like_this( min_doc_frequency => 0, min_term_frequency => 0, document_fields => '{"flavour": "banana"}' ) ORDER BY id; "# .fetch_collect(&mut conn); assert_eq!(rows.len(), 2); } #[rstest] fn more_like_this_date_key(mut conn: PgConnection) { r#" CREATE TABLE test_more_like_this_table ( id DATE PRIMARY KEY, flavour TEXT ); INSERT INTO test_more_like_this_table (id, flavour) VALUES ('2023-05-03', 'apple'), ('2023-05-04', 'banana'), ('2023-05-05', 'cherry'), ('2023-05-06', 'banana split'); "# .execute(&mut conn); r#" CREATE INDEX test_more_like_this_index on test_more_like_this_table USING bm25 (id, flavour) WITH (key_field='id'); "# .execute(&mut conn); let rows: Vec<(String, String)> = r#" SELECT CAST(id AS TEXT), flavour FROM test_more_like_this_table WHERE test_more_like_this_table @@@ paradedb.more_like_this( min_doc_frequency => 0, min_term_frequency => 0, document_fields => '{"flavour": "banana"}' ) ORDER BY id; "# .fetch_collect(&mut conn); assert_eq!(rows.len(), 2); } #[rstest] fn more_like_this_time_key(mut conn: PgConnection) { r#" CREATE TABLE test_more_like_this_table ( id TIME PRIMARY KEY, flavour TEXT ); INSERT INTO test_more_like_this_table (id, flavour) VALUES ('08:09:10', 'apple'), ('09:10:11', 'banana'), ('10:11:12', 'cherry'), ('11:12:13', 'banana split'); "# .execute(&mut conn); r#" CREATE INDEX test_more_like_this_index on test_more_like_this_table USING bm25 (id, flavour) WITH (key_field='id'); "# .execute(&mut conn); let rows: Vec<(String, String)> = r#" SELECT CAST(id AS TEXT), flavour FROM test_more_like_this_table WHERE test_more_like_this_table @@@ paradedb.more_like_this( min_doc_frequency => 0, min_term_frequency => 0, document_fields => '{"flavour": "banana"}' ) ORDER BY id; "# .fetch_collect(&mut conn); assert_eq!(rows.len(), 2); } #[rstest] fn more_like_this_timestamp_key(mut conn: PgConnection) { r#" CREATE TABLE test_more_like_this_table ( id TIMESTAMP PRIMARY KEY, flavour TEXT ); INSERT INTO test_more_like_this_table (id, flavour) VALUES ('2023-05-03 08:09:10', 'apple'), ('2023-05-04 09:10:11', 'banana'), ('2023-05-05 10:11:12', 'cherry'), ('2023-05-06 11:12:13', 'banana split'); "# .execute(&mut conn); r#" CREATE INDEX test_more_like_this_index on test_more_like_this_table USING bm25 (id, flavour) WITH (key_field='id'); "# .execute(&mut conn); let rows: Vec<(String, String)> = r#" SELECT CAST(id AS TEXT), flavour FROM test_more_like_this_table WHERE test_more_like_this_table @@@ paradedb.more_like_this( min_doc_frequency => 0, min_term_frequency => 0, document_fields => '{"flavour": "banana"}' ) ORDER BY id; "# .fetch_collect(&mut conn); assert_eq!(rows.len(), 2); } #[rstest] fn more_like_this_timestamptz_key(mut conn: PgConnection) { r#" CREATE TABLE test_more_like_this_table ( id TIMESTAMP WITH TIME ZONE PRIMARY KEY, flavour TEXT ); INSERT INTO test_more_like_this_table (id, flavour) VALUES ('2023-05-03 08:09:10 EST', 'apple'), ('2023-05-04 09:10:11 PST', 'banana'), ('2023-05-05 10:11:12 MST', 'cherry'), ('2023-05-06 11:12:13 CST', 'banana split'); "# .execute(&mut conn); r#" CREATE INDEX test_more_like_this_index on test_more_like_this_table USING bm25 (id, flavour) WITH (key_field='id'); "# .execute(&mut conn); let rows: Vec<(String, String)> = r#" SELECT CAST(id AS TEXT), flavour FROM test_more_like_this_table WHERE test_more_like_this_table @@@ paradedb.more_like_this( min_doc_frequency => 0, min_term_frequency => 0, document_fields => '{"flavour": "banana"}' ) ORDER BY id; "# .fetch_collect(&mut conn); assert_eq!(rows.len(), 2); } #[rstest] fn more_like_this_timetz_key(mut conn: PgConnection) { r#" CREATE TABLE test_more_like_this_table ( id TIME WITH TIME ZONE PRIMARY KEY, flavour TEXT ); INSERT INTO test_more_like_this_table (id, flavour) VALUES ('08:09:10 EST', 'apple'), ('09:10:11 PST', 'banana'), ('10:11:12 MST', 'cherry'), ('11:12:13 CST', 'banana split'); "# .execute(&mut conn); r#" CREATE INDEX test_more_like_this_index on test_more_like_this_table USING bm25 (id, flavour) WITH (key_field='id'); "# .execute(&mut conn); let rows: Vec<(String, String)> = r#" SELECT CAST(id AS TEXT), flavour FROM test_more_like_this_table WHERE test_more_like_this_table @@@ paradedb.more_like_this( min_doc_frequency => 0, min_term_frequency => 0, document_fields => '{"flavour": "banana"}' ) ORDER BY id; "# .fetch_collect(&mut conn); assert_eq!(rows.len(), 2); } #[rstest] fn match_query(mut conn: PgConnection) { SimpleProductsTable::setup().execute(&mut conn); let columns: SimpleProductsTableVec = r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ paradedb.match(field => 'description', value => 'ruling shoeez', distance => 2) ORDER BY id"# .fetch_collect(&mut conn); assert_eq!(columns.id, vec![3, 4, 5]); let columns: SimpleProductsTableVec = r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ paradedb.match( field => 'description', value => 'ruling shoeez', distance => 2, conjunction_mode => true ) ORDER BY id"# .fetch_collect(&mut conn); assert_eq!(columns.id, vec![3]); let columns: SimpleProductsTableVec = r#" SELECT * FROM paradedb.bm25_search WHERE bm25_search @@@ paradedb.match(field => 'description', value => 'ruling shoeez', distance => 1) ORDER BY id"# .fetch_collect(&mut conn); assert_eq!(columns.id.len(), 0); } #[rstest] fn parse_lenient(mut conn: PgConnection) { SimpleProductsTable::setup().execute(&mut conn); // Default lenient should be false let result = r#" SELECT id FROM paradedb.bm25_search WHERE paradedb.bm25_search.id @@@ paradedb.parse('shoes keyboard') ORDER BY id; "# .execute_result(&mut conn); assert!(result.is_err()); // With lenient enabled let rows: Vec<(i32,)> = r#" SELECT id FROM paradedb.bm25_search WHERE paradedb.bm25_search.id @@@ paradedb.parse('shoes keyboard', lenient => true) ORDER BY id; "# .fetch(&mut conn); assert_eq!(rows, vec![(1,), (2,), (3,), (4,), (5,)]); } #[rstest] fn parse_conjunction(mut conn: PgConnection) { SimpleProductsTable::setup().execute(&mut conn); let rows: Vec<(i32,)> = r#" SELECT id FROM paradedb.bm25_search WHERE paradedb.bm25_search.id @@@ paradedb.parse('description:(shoes running)', conjunction_mode => true) ORDER BY id; "#.fetch(&mut conn); assert_eq!(rows, vec![(3,)]); } #[rstest] fn parse_with_field_conjunction(mut conn: PgConnection) { SimpleProductsTable::setup().execute(&mut conn); let rows: Vec<(i32,)> = r#" SELECT id FROM paradedb.bm25_search WHERE paradedb.bm25_search.id @@@ paradedb.parse_with_field('description', 'shoes running', conjunction_mode => true) ORDER BY id; "#.fetch(&mut conn); assert_eq!(rows, vec![(3,)]); } #[rstest] fn range_term(mut conn: PgConnection) { r#" CALL paradedb.create_bm25_test_table( schema_name => 'public', table_name => 'deliveries', table_type => 'Deliveries' ); CREATE INDEX deliveries_idx ON deliveries USING bm25 (delivery_id, weights, quantities, prices, ship_dates, facility_arrival_times, delivery_times) WITH (key_field = 'delivery_id'); "# .execute(&mut conn); // int4range let expected: Vec<(i32,)> = "SELECT delivery_id FROM deliveries WHERE weights @> 1 ORDER BY delivery_id" .fetch(&mut conn); let result: Vec<(i32,)> = "SELECT delivery_id FROM deliveries WHERE delivery_id @@@ paradedb.range_term('weights', 1) ORDER BY delivery_id".fetch(&mut conn); assert_eq!(result, expected); let expected: Vec<(i32,)> = "SELECT delivery_id FROM deliveries WHERE weights @> 13 ORDER BY delivery_id" .fetch(&mut conn); let result: Vec<(i32,)> = "SELECT delivery_id FROM deliveries WHERE delivery_id @@@ paradedb.range_term('weights', 13) ORDER BY delivery_id".fetch(&mut conn); assert_eq!(result, expected); // int8range let expected: Vec<(i32,)> = "SELECT delivery_id FROM deliveries WHERE quantities @> 17000::int8 ORDER BY delivery_id" .fetch(&mut conn); let result: Vec<(i32,)> = "SELECT delivery_id FROM deliveries WHERE delivery_id @@@ paradedb.range_term('quantities', 17000) ORDER BY delivery_id".fetch(&mut conn); assert_eq!(result, expected); // numrange let expected: Vec<(i32,)> = "SELECT delivery_id FROM deliveries WHERE prices @> 3.5 ORDER BY delivery_id" .fetch(&mut conn); let result: Vec<(i32,)> = "SELECT delivery_id FROM deliveries WHERE delivery_id @@@ paradedb.range_term('prices', 3.5) ORDER BY delivery_id".fetch(&mut conn); assert_eq!(result, expected); // daterange let expected: Vec<(i32,)> = "SELECT delivery_id FROM deliveries WHERE ship_dates @> '2023-03-07'::date ORDER BY delivery_id".fetch(&mut conn); let result: Vec<(i32,)> = "SELECT delivery_id FROM deliveries WHERE delivery_id @@@ paradedb.range_term('ship_dates', '2023-03-07'::date) ORDER BY delivery_id".fetch(&mut conn); assert_eq!(result, expected); let expected: Vec<(i32,)> = "SELECT delivery_id FROM deliveries WHERE ship_dates @> '2023-03-06'::date ORDER BY delivery_id".fetch(&mut conn); let result: Vec<(i32,)> = "SELECT delivery_id FROM deliveries WHERE delivery_id @@@ paradedb.range_term('ship_dates', '2023-03-06'::date) ORDER BY delivery_id".fetch(&mut conn); assert_eq!(result, expected); // tsrange let expected: Vec<(i32,)> = "SELECT delivery_id FROM deliveries WHERE facility_arrival_times @> '2024-05-01 14:00:00'::timestamp ORDER BY delivery_id".fetch(&mut conn); let result: Vec<(i32,)> = "SELECT delivery_id FROM deliveries WHERE delivery_id @@@ paradedb.range_term('facility_arrival_times', '2024-05-01 14:00:00'::timestamp) ORDER BY delivery_id".fetch(&mut conn); assert_eq!(result, expected); let expected: Vec<(i32,)> = "SELECT delivery_id FROM deliveries WHERE facility_arrival_times @> '2024-05-01 15:00:00'::timestamp ORDER BY delivery_id".fetch(&mut conn); let result: Vec<(i32,)> = "SELECT delivery_id FROM deliveries WHERE delivery_id @@@ paradedb.range_term('facility_arrival_times', '2024-05-01 15:00:00'::timestamp) ORDER BY delivery_id".fetch(&mut conn); assert_eq!(result, expected); // tstzrange let expected: Vec<(i32,)> = "SELECT delivery_id FROM deliveries WHERE delivery_times @> '2024-05-01 06:31:00-04'::timestamptz ORDER BY delivery_id".fetch(&mut conn); let result: Vec<(i32,)> = "SELECT delivery_id FROM deliveries WHERE delivery_id @@@ paradedb.range_term('delivery_times', '2024-05-01 06:31:00-04'::timestamptz) ORDER BY delivery_id".fetch(&mut conn); assert_eq!(result, expected); let expected: Vec<(i32,)> = "SELECT delivery_id FROM deliveries WHERE delivery_times @> '2024-05-01T11:30:00Z'::timestamptz ORDER BY delivery_id".fetch(&mut conn); let result: Vec<(i32,)> = "SELECT delivery_id FROM deliveries WHERE delivery_id @@@ paradedb.range_term('delivery_times', '2024-05-01T11:30:00Z'::timestamptz) ORDER BY delivery_id".fetch(&mut conn); assert_eq!(result, expected); } #[rstest] async fn prepared_statement_replanning(mut conn: PgConnection) { SimpleProductsTable::setup().execute(&mut conn); // ensure our plan doesn't change into a sequential scan after the 5th execution for _ in 0..10 { let _: Vec = sqlx::query("SELECT id FROM paradedb.bm25_search WHERE id @@@ paradedb.term('rating', $1) ORDER BY id") .bind(2) .fetch_all(&mut conn) .await .unwrap() .into_iter() .map(|row| row.get::("id")) .collect(); } } #[rstest] async fn direct_prepared_statement_replanning(mut conn: PgConnection) { SimpleProductsTable::setup().execute(&mut conn); "PREPARE stmt(text) AS SELECT id FROM paradedb.bm25_search WHERE description @@@ $1" .execute(&mut conn); // ensure our plan doesn't change into a sequential scan after the 5th execution for _ in 0..10 { "EXECUTE stmt('keyboard')".fetch_one::<(i32,)>(&mut conn); } } #[rstest] async fn direct_prepared_statement_replanning_custom_scan(mut conn: PgConnection) { SimpleProductsTable::setup().execute(&mut conn); "PREPARE stmt(text) AS SELECT paradedb.score(id), id FROM paradedb.bm25_search WHERE description @@@ $1 ORDER BY score desc LIMIT 10" .execute(&mut conn); // ensure our plan doesn't change into a sequential scan after the 5th execution for _ in 0..10 { let (score, id) = "EXECUTE stmt('keyboard')".fetch_one::<(f32, i32)>(&mut conn); assert_eq!((score, id), (3.2668595, 2)) } }