Fulltext Search Planner
Creates a safe fulltext search plan with fallback options and relevance sorting.
Function signature
ogBuildFulltextSearchPlan(table, query, search_fields = array(), options = array())
Categories
- Search and Discovery
Parameters
tableDatabase table name.querySearch query.search_fieldsFulltext fields.optionsAllowlist, fallback, limit, and filter options. Recognized keys: `allowed_fields`, `limit`, `published_only`.Return value
Public-safe status string returned by the function.
- success
- message
- data
Compatibility
Existing function name, slug, path, and call order preserved; advertised metadata corrected to the actual source behavior.
Minimum PHP version: 7.4
Security notes
Use caller-owned allowlists and procedural mysqli prepared execution where SQL plans are returned; validate file paths, MIME policies, and permissions before file or download workflows.
Code
<?php
/*
* Copyright (c) 2026 Jeffery L. Paris <jparis@phpog.com>.
* Free for personal and internal use. Paid project use requires visible credit
* to Jeffery L. Paris. Corporate use requires a paid license fee unless a
* separate written license states otherwise.
*/
/**
* Creates a safe fulltext search plan with fallback options and relevance sorting.
*
* The function builds SQL, bind types, and parameters. The caller remains responsible
* for preparing/executing the statement.
*
* @param string $table Database table name.
* @param string $query Search query.
* @param array $search_fields Fulltext fields.
* @param array $options Allowlist, fallback, limit, and filter options.
* @return array Search plan.
*/
function ogBuildFulltextSearchPlan($table, $query, $search_fields = array(), $options = array()) {
$result = array(
'success' => false,
'message' => '',
'data' => array()
);
$table = trim((string)$table);
$query = trim((string)$query);
if (empty($table) || !preg_match('/^[a-zA-Z0-9_]+$/', $table)) {
$result['message'] = 'Invalid table name.';
return $result;
}
if (strlen($query) < 2) {
$result['message'] = 'Search query is too short.';
return $result;
}
$allowed_fields = array();
if (!empty($options['allowed_fields']) && is_array($options['allowed_fields'])) {
$allowed_fields = $options['allowed_fields'];
}
$fields = array();
foreach ((array)$search_fields as $field) {
$field = trim((string)$field);
if (!preg_match('/^[a-zA-Z0-9_]+$/', $field)) {
continue;
}
if (!empty($allowed_fields) && !in_array($field, $allowed_fields, true)) {
continue;
}
$fields[] = '`' . $field . '`';
}
if (empty($fields)) {
$result['message'] = 'At least one approved search field is required.';
return $result;
}
$boolean_query = preg_replace('/[^a-zA-Z0-9\s+\-*"_]/', ' ', $query);
$boolean_query = trim(preg_replace('/\s+/', ' ', $boolean_query));
if (empty($boolean_query)) {
$result['message'] = 'Search query did not contain searchable text.';
return $result;
}
$field_sql = implode(', ', $fields);
$sql = 'SELECT *, MATCH (' . $field_sql . ') AGAINST (? IN BOOLEAN MODE) AS `relevance` FROM `' . $table . '` WHERE MATCH (' . $field_sql . ') AGAINST (? IN BOOLEAN MODE)';
$types = 'ss';
$params = array($boolean_query, $boolean_query);
if (!empty($options['published_only'])) {
$sql .= ' AND `is_published` = ?';
$types .= 'i';
$params[] = 1;
}
$sql .= ' ORDER BY `relevance` DESC';
$limit = 25;
if (!empty($options['limit'])) {
$limit = (int)$options['limit'];
}
if ($limit < 1) {
$limit = 25;
}
if ($limit > 100) {
$limit = 100;
}
$sql .= ' LIMIT ' . $limit;
$result['success'] = true;
$result['message'] = 'Fulltext search plan built.';
$result['data'] = array(
'sql' => $sql,
'types' => $types,
'params' => $params,
'query' => $boolean_query,
'limit' => $limit
);
return $result;
}