// 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 .
use crate::index::merge_policy::LayeredMergePolicy;
use crate::index::mvcc::MvccSatisfies;
use crate::index::reader::index::SearchIndexReader;
use crate::postgres::index::IndexKind;
use crate::postgres::insert::merge_index_with_policy;
use crate::postgres::options::SearchIndexCreateOptions;
use crate::postgres::storage::block::{
LinkedList, MVCCEntry, SegmentMetaEntry, SEGMENT_METAS_START,
};
use crate::postgres::storage::merge::MergeLock;
use crate::postgres::storage::LinkedItemList;
use crate::postgres::utils::item_pointer_to_u64;
use crate::query::SearchQueryInput;
use crate::schema::SearchFieldConfig;
use crate::schema::SearchFieldName;
use anyhow::bail;
use anyhow::Result;
use pgrx::prelude::*;
use pgrx::JsonB;
use pgrx::PgRelation;
use rustc_hash::FxHashMap;
use serde_json::Value;
use std::collections::HashSet;
#[allow(clippy::too_many_arguments)]
#[pg_extern]
fn format_create_bm25(
index_name: &str,
table_name: &str,
key_field: &str,
schema_name: default!(&str, "''"),
text_fields: default!(JsonB, "'{}'::jsonb"),
numeric_fields: default!(JsonB, "'{}'::jsonb"),
boolean_fields: default!(JsonB, "'{}'::jsonb"),
json_fields: default!(JsonB, "'{}'::jsonb"),
range_fields: default!(JsonB, "'{}'::jsonb"),
datetime_fields: default!(JsonB, "'{}'::jsonb"),
predicates: default!(&str, "''"),
) -> Result {
let mut column_names = vec![key_field.to_string()];
for fields in [
&text_fields,
&numeric_fields,
&boolean_fields,
&json_fields,
&range_fields,
&datetime_fields,
] {
if let Value::Object(ref map) = fields.0 {
for key in map.keys() {
if key != key_field {
column_names.push(spi::quote_identifier(key.clone()));
}
}
} else {
bail!("Expected a JSON object, received: {}", fields.0);
}
}
let column_names_csv = column_names
.clone()
.into_iter()
.filter(|name| name != key_field)
.collect::>()
.join(", ");
let predicate_where = if !predicates.is_empty() {
format!("WHERE {}", predicates)
} else {
"".to_string()
};
let schema_prefix = if schema_name.is_empty() {
"".to_string()
} else {
format!("{}.", spi::quote_identifier(schema_name))
};
Ok(format!(
"CREATE INDEX {} ON {}{} USING bm25 ({}, {}) WITH (key_field={}, text_fields={}, numeric_fields={}, boolean_fields={}, json_fields={}, range_fields={}, datetime_fields={}) {};",
spi::quote_identifier(index_name),
schema_prefix,
spi::quote_identifier(table_name),
spi::quote_identifier(key_field),
column_names_csv,
spi::quote_literal(key_field),
spi::quote_literal(&serde_json::to_string(&text_fields)?),
spi::quote_literal(&serde_json::to_string(&numeric_fields)?),
spi::quote_literal(&serde_json::to_string(&boolean_fields)?),
spi::quote_literal(&serde_json::to_string(&json_fields)?),
spi::quote_literal(&serde_json::to_string(&range_fields)?),
spi::quote_literal(&serde_json::to_string(&datetime_fields)?),
predicate_where))
}
#[pg_extern]
pub unsafe fn index_fields(index: PgRelation) -> anyhow::Result {
let options = SearchIndexCreateOptions::from_relation(&index);
let fields = options.get_all_fields(&index).collect::>();
let name_and_config: FxHashMap = fields
.into_iter()
.map(|(field_name, field_config, _)| (field_name, field_config))
.collect();
Ok(JsonB(serde_json::to_value(name_and_config)?))
}
#[pg_extern]
pub unsafe fn layer_sizes(index: PgRelation) -> Vec {
let options = SearchIndexCreateOptions::from_relation(&index);
options
.layer_sizes(crate::postgres::insert::DEFAULT_LAYER_SIZES)
.into_iter()
.map(|layer_size| layer_size.into())
.collect()
}
#[pg_extern]
unsafe fn merge_info(
index: PgRelation,
) -> TableIterator<
'static,
(
name!(index_name, String),
name!(pid, i32),
name!(xmin, AnyNumeric),
name!(xmax, AnyNumeric),
name!(segno, String),
),
> {
let index_kind = IndexKind::for_index(index).unwrap();
let mut result = Vec::new();
for index in index_kind.partitions() {
let merge_lock = MergeLock::acquire(index.oid());
let merge_entries = merge_lock.in_progress_merge_entries();
result.extend(merge_entries.into_iter().flat_map(move |merge_entry| {
let index_name = index.name().to_owned();
merge_entry
.segment_ids(index.oid())
.into_iter()
.map(move |segment_id| {
(
index_name.clone(),
merge_entry.pid,
merge_entry.xmin.into(),
merge_entry.xmax.into(),
segment_id.short_uuid_string(),
)
})
}));
}
TableIterator::new(result)
}
/// Deprecated: Use `paradedb.merge_info` instead.
#[pg_extern]
fn is_merging(index: PgRelation) -> bool {
unsafe { merge_info(index).next().is_some() }
}
#[pg_extern]
unsafe fn vacuum_info(
index: PgRelation,
) -> TableIterator<'static, (name!(index_name, String), name!(segno, String))> {
let index_kind = IndexKind::for_index(index).unwrap();
let mut result = Vec::new();
for index in index_kind.partitions() {
let mut merge_lock = MergeLock::acquire(index.oid());
let vacuum_list = merge_lock.list_vacuuming_segments();
result.extend(
vacuum_list
.iter()
.map(|segment_id| (index.name().to_owned(), segment_id.short_uuid_string())),
);
}
TableIterator::new(result)
}
#[allow(clippy::type_complexity)]
#[pg_extern]
fn index_info(
index: PgRelation,
show_invisible: default!(bool, false),
) -> anyhow::Result<
TableIterator<
'static,
(
name!(index_name, String),
name!(visible, bool),
name!(recyclable, bool),
name!(xmin, AnyNumeric),
name!(xmax, AnyNumeric),
name!(segno, String),
name!(byte_size, Option),
name!(num_docs, Option),
name!(num_deleted, Option),
name!(termdict_bytes, Option),
name!(postings_bytes, Option),
name!(positions_bytes, Option),
name!(fast_fields_bytes, Option),
name!(fieldnorms_bytes, Option),
name!(store_bytes, Option),
name!(deletes_bytes, Option),
),
>,
> {
// # Safety
//
// Lock the index relation until the end of this function so it is not dropped or
// altered while we are reading it.
//
// Because we accept a PgRelation above, we have confidence that Postgres has already
// validated the existence of the relation. We are safe calling the function below as
// long we do not pass pg_sys::NoLock without any other locking mechanism of our own.
let index = unsafe { PgRelation::with_lock(index.oid(), pg_sys::AccessShareLock as _) };
let index_kind = IndexKind::for_index(index)?;
let mut results = Vec::new();
for index in index_kind.partitions() {
// open the specified index
let mut segment_components =
LinkedItemList::::open(index.oid(), SEGMENT_METAS_START);
let all_entries = unsafe { segment_components.list() };
let snapshot = unsafe { pg_sys::GetActiveSnapshot() };
for entry in all_entries {
if !show_invisible && unsafe { !entry.visible(snapshot) } {
continue;
}
results.push((
index.name().to_owned(),
unsafe { entry.visible(snapshot) },
unsafe { entry.recyclable(segment_components.bman_mut()) },
entry.xmin.into(),
entry.xmax.into(),
entry.segment_id.short_uuid_string(),
Some(entry.byte_size().into()),
Some(entry.num_docs().into()),
Some(entry.num_deleted_docs().into()),
entry.terms.map(|file| file.total_bytes.into()),
entry.postings.map(|file| file.total_bytes.into()),
entry.positions.map(|file| file.total_bytes.into()),
entry.fast_fields.map(|file| file.total_bytes.into()),
entry.field_norms.map(|file| file.total_bytes.into()),
entry.store.map(|file| file.total_bytes.into()),
entry.delete.map(|file| file.file_entry.total_bytes.into()),
));
}
}
Ok(TableIterator::new(results))
}
/// Returns the list of segments that contain the specified [`pg_sys::ItemPointerData]` heap tuple
/// identifier.
///
/// If the specified `ctid` is the result of a HOT chain update, then it's likely this function will
/// return NULL -- HOT chains cannot be "reverse searched".
///
/// Otherwise, if this function returns NULL that is likely indicative of a pg_search bug. We wish
/// you luck in determining which is which.
#[pg_extern]
fn find_ctid(index: PgRelation, ctid: pg_sys::ItemPointerData) -> Result