From 5ff06b03171fbc8cefeadd273ec4d3fec3063b42 Mon Sep 17 00:00:00 2001 From: Ahmed Darwich Date: Thu, 17 Sep 2026 13:58:26 -0500 Subject: [PATCH 1/2] fix: max_score when used with full_score --- postgres/src/lib.rs | 32 ++++++++++++++++++++ postgres/src/score.rs | 69 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 98 insertions(+), 3 deletions(-) diff --git a/postgres/src/lib.rs b/postgres/src/lib.rs index 5aec8c2..6f12121 100644 --- a/postgres/src/lib.rs +++ b/postgres/src/lib.rs @@ -250,6 +250,38 @@ mod tests { assert_eq!(inspected, Some(vec!["rare".to_owned()])); } + #[pg_test] + fn full_score_normalization_matches_tin() { + Spi::run( + "CREATE TABLE lite_normalization (id int, body text); + INSERT INTO lite_normalization VALUES + (1, 'I love fuji apples and juicy mangoes'), + (2, 'Grape tasting notes from the orchard'), + (3, 'The best juicy fuji apple in town'); + CREATE INDEX lite_normalization_idx ON lite_normalization USING tin (body)", + ) + .unwrap(); + for expression in [ + "tin.full_score(ctid) / tin.max_score(ctid)", + "1::real / tin.max_score(ctid) * tin.full_score(ctid)", + ] { + let sql = format!( + "SELECT {expression} FROM lite_normalization + WHERE body ==> 'apple OR grape' AND tin.max_score(ctid) > 0 ORDER BY id" + ); + let scores = Spi::connect(|client| { + client + .select(&sql, None, &[]) + .unwrap() + .map(|row| row.get::(1).unwrap().unwrap()) + .collect::>() + }); + assert_eq!(scores.len(), 2); + assert!((scores[0] - 1.0).abs() < 0.000001); + assert!((scores[1] - 0.9398665).abs() < 0.000001); + } + } + #[pg_test] fn scoring_binds_to_expression_indexes() { Spi::run( diff --git a/postgres/src/score.rs b/postgres/src/score.rs index d6cb957..2594575 100644 --- a/postgres/src/score.rs +++ b/postgres/src/score.rs @@ -104,7 +104,7 @@ fn score_bound( heap_oid: heap_oid as u32, index_oid: index_oid as u32, query: query.to_owned(), - full: mode == 1, + full: mode == 1 || mode == 3, dense: dense_ratio.unwrap_or(DenseRatio::DEFAULT).to_bits(), k1: bits(k1), b: bits(b), @@ -116,7 +116,7 @@ fn score_bound( *slot = Some(build_corpus(key.clone(), k1, b, term_add, term_replace)); } let corpus = slot.as_ref().expect("score corpus was just populated"); - if mode == 2 { + if mode == 2 || mode == 3 { corpus.max } else { corpus.by_document.get(document).copied().unwrap_or(0.0) @@ -475,6 +475,57 @@ pub(crate) unsafe fn find_matching_tin_index( matched } +struct FullScoreBinding { + ctid: *const pg_sys::Var, + document: *mut pg_sys::Node, + support: pg_sys::Oid, + bound: pg_sys::Oid, +} + +#[pg_guard] +unsafe extern "C-unwind" fn has_full_score(node: *mut pg_sys::Node, context: *mut c_void) -> bool { + unsafe { + if node.is_null() || (*node).type_ == pg_sys::NodeTag::T_Query { + return false; + } + let binding = &*context.cast::(); + if (*node).type_ == pg_sys::NodeTag::T_FuncExpr { + let function = &*node.cast::(); + // Earlier query clauses may already contain the rewritten scorer. + if function.funcid == binding.bound { + let mode = pg_sys::list_nth(function.args, 4).cast::(); + if (*mode).xpr.type_ == pg_sys::NodeTag::T_Const + && (*mode).constvalue.value() == 1 + && pg_sys::equal(pg_sys::list_nth(function.args, 0), binding.document.cast()) + { + return true; + } + } else if pg_sys::get_func_support(function.funcid) == binding.support + && CStr::from_ptr(pg_sys::get_func_name(function.funcid)).to_bytes() + == b"full_score" + { + for position in 0..pg_sys::list_length(function.args) { + let mut argument = + pg_sys::list_nth(function.args, position).cast::(); + if (*argument).type_ == pg_sys::NodeTag::T_NamedArgExpr { + let named = &*argument.cast::(); + if named.argnumber != 0 { + continue; + } + argument = named.arg.cast(); + } else if position != 0 { + continue; + } + if pg_sys::equal(argument.cast(), binding.ctid.cast()) { + return true; + } + } + } + } + pg_sys::expression_tree_walker(node, Some(has_full_score), context) + } +} + #[pg_extern(immutable, parallel_unsafe)] fn score_support(request: Internal) -> Internal { let unhandled = || Internal::from(Some(pg_sys::Datum::from(0_usize))); @@ -523,7 +574,19 @@ fn score_support(request: Internal) -> Internal { let mode = if fname.as_ref() == "full_score" { 1 } else if fname.as_ref() == "max_score" { - 2 + let mut binding = FullScoreBinding { + ctid, + document, + support: pg_sys::get_func_support((*request.fcall).funcid), + bound: lookup_score_bound(), + }; + let full = pg_sys::query_tree_walker( + parse, + Some(has_full_score), + (&mut binding as *mut FullScoreBinding).cast(), + pg_sys::QTW_IGNORE_RC_SUBQUERIES as i32, + ); + if full { 3 } else { 2 } } else { 0 }; From 1d74a22a3b44fe946f4b464c4b727c641fc75c74 Mon Sep 17 00:00:00 2001 From: Patrick Reynolds Date: Thu, 17 Sep 2026 15:58:28 -0400 Subject: [PATCH 2/2] Use GitHub Actions own runners for CI --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 501b905..7ff6093 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,9 +18,9 @@ jobs: matrix: arch: - name: x86-64 - runner: depot-ubuntu-24.04-8 + runner: ubuntu-24.04 - name: arm64 - runner: depot-ubuntu-24.04-arm-8 + runner: ubuntu-24.04-arm postgres: [17, 18] env: