MySQL Full-Text Search (FTS) Fine-Tuning Guide¶
Introduction¶
Full-Text Search (FTS) in MySQL allows you to efficiently search text content in your database. While the default settings work well for most applications, MySQL provides several configuration options to optimize search behavior for your specific needs.
Important Note: MySQL's FTS is already well-tuned out of the box. Only modify these settings if you have a specific reason and understand the implications. Most changes require rebuilding your FULLTEXT indexes and restarting the server.
1. Word Length Configuration¶
What It Does¶
Controls which words get indexed based on their length. By default, very short words (like "a", "in") and extremely long words are excluded from the index.
Configuration Variables¶
For InnoDB tables:
- innodb_ft_min_token_size - Minimum word length (default: 3)
- innodb_ft_max_token_size - Maximum word length (default: 84)
For MyISAM tables:
- ft_min_word_len - Minimum word length (default: 4)
- ft_max_word_len - Maximum word length (default: 84)
Example Use Case¶
If you need to search for two-letter words like "AI" or "UK", you'd need to lower the minimum token size.
How to Configure¶
Add to your MySQL configuration file (e.g., /etc/my.cnf or my.ini on Windows):
Important: After changing these settings: 1. Restart the MySQL server 2. Rebuild all FULLTEXT indexes (see Rebuilding Indexes section)
PHP Example - Rebuilding Index After Configuration Change¶
<?php
// Connect to MySQL
$mysqli = new mysqli("localhost", "root", "password", "mydb");
if ($mysqli->connect_error) {
die("Connection failed: " . $mysqli->connect_error);
}
// Rebuild the FULLTEXT index
// Step 1: Drop the existing index
$sql = "ALTER TABLE articles DROP INDEX content_idx";
if ($mysqli->query($sql)) {
echo "Index dropped successfully.\n";
} else {
echo "Error dropping index: " . $mysqli->error . "\n";
}
// Step 2: Recreate the index
$sql = "ALTER TABLE articles ADD FULLTEXT INDEX content_idx (content)";
if ($mysqli->query($sql)) {
echo "Index recreated successfully.\n";
} else {
echo "Error creating index: " . $mysqli->error . "\n";
}
$mysqli->close();
?>
PHP Example - Searching with New Configuration¶
<?php
$mysqli = new mysqli("localhost", "root", "password", "mydb");
// Now you can search for 2-letter words like "AI"
$search_term = "AI technology";
$sql = "SELECT * FROM articles
WHERE MATCH(title, content) AGAINST(? IN NATURAL LANGUAGE MODE)";
$stmt = $mysqli->prepare($sql);
$stmt->bind_param("s", $search_term);
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
echo "Title: " . htmlspecialchars($row['title']) . "<br>";
echo "Content: " . htmlspecialchars($row['content']) . "<br><br>";
}
$stmt->close();
$mysqli->close();
?>
2. Stopword Configuration¶
What It Does¶
Stopwords are common words (like "the", "and", "is") that are excluded from the search index because they appear so frequently they're not useful for searching.
Configuration Variables¶
For InnoDB:
- innodb_ft_enable_stopword - Enable/disable stopword filtering (default: ON)
- innodb_ft_server_stopword_table - Custom server-level stopword table
- innodb_ft_user_stopword_table - Custom session-level stopword table
For MyISAM:
- ft_stopword_file - Path to custom stopword file
Example Use Case¶
If you're building a search engine for legal documents where words like "the" and "and" might be significant, you might want to disable stopwords or create a custom list.
PHP Example - Creating Custom Stopword Table (InnoDB)¶
<?php
$mysqli = new mysqli("localhost", "root", "password", "mydb");
if ($mysqli->connect_error) {
die("Connection failed: " . $mysqli->connect_error);
}
// Step 1: Create a custom stopword table
$sql = "CREATE TABLE IF NOT EXISTS custom_stopwords (
value VARCHAR(30)
) ENGINE = INNODB";
if ($mysqli->query($sql)) {
echo "Stopword table created successfully.\n";
} else {
echo "Error creating table: " . $mysqli->error . "\n";
}
// Step 2: Add your custom stopwords
$stopwords = ['the', 'and', 'or', 'but', 'is', 'are', 'was', 'were'];
$stmt = $mysqli->prepare("INSERT INTO custom_stopwords (value) VALUES (?)");
foreach ($stopwords as $word) {
$stmt->bind_param("s", $word);
$stmt->execute();
}
$stmt->close();
echo "Custom stopwords added.\n";
// Step 3: Configure MySQL to use the custom stopword table
// (This requires appropriate privileges and a server restart)
$sql = "SET GLOBAL innodb_ft_user_stopword_table = 'mydb/custom_stopwords'";
if ($mysqli->query($sql)) {
echo "Stopword table configured.\n";
} else {
echo "Error setting stopword table: " . $mysqli->error . "\n";
}
$mysqli->close();
?>
PHP Example - Disabling Stopwords Temporarily¶
<?php
$mysqli = new mysqli("localhost", "root", "password", "mydb");
// Disable stopwords for the current session
$mysqli->query("SET SESSION innodb_ft_enable_stopword = OFF");
// Now create a FULLTEXT index without stopword filtering
$mysqli->query("CREATE TABLE temp_articles (
id INT AUTO_INCREMENT PRIMARY KEY,
content TEXT,
FULLTEXT(content)
) ENGINE=InnoDB");
// After creating the index, you can re-enable stopwords
$mysqli->query("SET SESSION innodb_ft_enable_stopword = ON");
$mysqli->close();
?>
Note: After changing stopword settings, rebuild your FULLTEXT indexes.
3. Natural Language Search Threshold (MyISAM Only)¶
What It Does¶
By default, MyISAM ignores words that appear in more than 50% of rows. This prevents common words from dominating search results.
Why This Exists¶
If a word appears in more than half your documents, it's not useful for distinguishing between documents.
Example¶
If you have 1000 articles and the word "MySQL" appears in 600 of them, natural language search will ignore "MySQL" in queries.
PHP Example - Understanding the 50% Threshold¶
<?php
$mysqli = new mysqli("localhost", "root", "password", "mydb");
// Check how many rows contain a specific word
$word = "mysql";
$sql = "SELECT COUNT(*) as count FROM articles WHERE content LIKE ?";
$stmt = $mysqli->prepare($sql);
$like_term = "%$word%";
$stmt->bind_param("s", $like_term);
$stmt->execute();
$result = $stmt->get_result();
$row = $result->fetch_assoc();
$word_count = $row['count'];
// Get total row count
$total_result = $mysqli->query("SELECT COUNT(*) as total FROM articles");
$total_row = $total_result->fetch_assoc();
$total_count = $total_row['total'];
$percentage = ($word_count / $total_count) * 100;
echo "Word '$word' appears in $word_count out of $total_count articles ($percentage%).\n";
if ($percentage > 50) {
echo "This word will be ignored in natural language search!\n";
echo "Use IN BOOLEAN MODE instead.\n";
}
$stmt->close();
$mysqli->close();
?>
PHP Example - Using Boolean Mode to Bypass Threshold¶
Option 1: Boolean Mode Search (Recommended) Boolean mode doesn't apply the 50% threshold:
<?php
$mysqli = new mysqli("localhost", "root", "password", "mydb");
$search_term = "mysql";
// Boolean search ignores the 50% threshold
$sql = "SELECT *, MATCH(content) AGAINST(? IN BOOLEAN MODE) AS relevance
FROM articles
WHERE MATCH(content) AGAINST(? IN BOOLEAN MODE)
ORDER BY relevance DESC";
$stmt = $mysqli->prepare($sql);
$stmt->bind_param("ss", $search_term, $search_term);
$stmt->execute();
$result = $stmt->get_result();
echo "<h2>Search Results for: " . htmlspecialchars($search_term) . "</h2>";
while ($row = $result->fetch_assoc()) {
echo "<div>";
echo "<h3>" . htmlspecialchars($row['title']) . "</h3>";
echo "<p>" . htmlspecialchars(substr($row['content'], 0, 200)) . "...</p>";
echo "<small>Relevance: " . $row['relevance'] . "</small>";
echo "</div><hr>";
}
$stmt->close();
$mysqli->close();
?>
4. Boolean Search Operators (MyISAM Only)¶
What It Does¶
Allows you to customize the operators used in Boolean full-text searches.
Default Operators¶
+- Word must be present (AND)-- Word must not be present (NOT)>- Increase word's relevance<- Decrease word's relevance()- Group words into subexpressions~- Negate word's contribution to relevance*- Wildcard (must be at end of word)""- Phrase search (exact match)
Configuration Variable¶
ft_boolean_syntax - Can be changed at runtime (no restart needed)
PHP Example - Viewing Current Boolean Syntax¶
<?php
$mysqli = new mysqli("localhost", "root", "password", "mydb");
// View current boolean syntax
$result = $mysqli->query("SHOW VARIABLES LIKE 'ft_boolean_syntax'");
$row = $result->fetch_assoc();
echo "Current boolean syntax: " . $row['Value'] . "\n";
$mysqli->close();
?>
PHP Example - Advanced Boolean Searches¶
<?php
$mysqli = new mysqli("localhost", "root", "password", "mydb");
// Example 1: Must include "mysql", must exclude "oracle"
$search1 = "+mysql -oracle";
echo "<h3>Search: $search1</h3>";
$sql = "SELECT * FROM articles
WHERE MATCH(content) AGAINST(? IN BOOLEAN MODE)";
$stmt = $mysqli->prepare($sql);
$stmt->bind_param("s", $search1);
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
echo "- " . htmlspecialchars($row['title']) . "<br>";
}
$stmt->close();
echo "<br>";
// Example 2: Phrase search
$search2 = '"full text search"';
echo "<h3>Search: $search2</h3>";
$stmt = $mysqli->prepare($sql);
$stmt->bind_param("s", $search2);
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
echo "- " . htmlspecialchars($row['title']) . "<br>";
}
$stmt->close();
echo "<br>";
// Example 3: Wildcard search
$search3 = "data*"; // matches data, database, databases, etc.
echo "<h3>Search: $search3</h3>";
$stmt = $mysqli->prepare($sql);
$stmt->bind_param("s", $search3);
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
echo "- " . htmlspecialchars($row['title']) . "<br>";
}
$stmt->close();
echo "<br>";
// Example 4: Complex boolean query
$search4 = "+(mysql database) -oracle";
echo "<h3>Search: $search4</h3>";
$stmt = $mysqli->prepare($sql);
$stmt->bind_param("s", $search4);
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
echo "- " . htmlspecialchars($row['title']) . "<br>";
}
$stmt->close();
$mysqli->close();
?>
5. Character Set Modifications¶
What It Does¶
Defines which characters are considered "word characters" versus delimiters. By default, hyphens and most punctuation split words.
Example Problem¶
By default, "full-text" is treated as two words: "full" and "text". If you want to treat hyphens as part of words, you need to modify the character set configuration.
Three Methods to Modify¶
Method 1: Modify MySQL Source Code (Advanced)¶
Edit storage/innobase/handler/ha_innodb.cc (InnoDB) or storage/myisam/ftdefs.h (MyISAM) and recompile MySQL.
Method 2: Modify Character Set XML Files (No Recompilation)¶
Edit the character set definition files to include additional characters as "letters".
Method 3: Add Custom Collation (Recommended)¶
Create a new collation for full-text indexing with custom character definitions.
PHP Example - Working with Hyphenated Words¶
<?php
$mysqli = new mysqli("localhost", "root", "password", "mydb");
// Problem: Searching for "full-text" might not work as expected
$search = "full-text";
$sql = "SELECT * FROM articles
WHERE MATCH(content) AGAINST(? IN BOOLEAN MODE)";
$stmt = $mysqli->prepare($sql);
$stmt->bind_param("s", $search);
$stmt->execute();
$result = $stmt->get_result();
echo "<h3>Results for 'full-text' (may be treated as two words):</h3>";
while ($row = $result->fetch_assoc()) {
echo "- " . htmlspecialchars($row['title']) . "<br>";
}
// Workaround: Search for both variations
$search2 = '+full +text';
echo "<h3>Workaround - searching for both words:</h3>";
$stmt = $mysqli->prepare($sql);
$stmt->bind_param("s", $search2);
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
echo "- " . htmlspecialchars($row['title']) . "<br>";
}
$stmt->close();
$mysqli->close();
?>
6. Optimizing InnoDB Full-Text Indexes¶
What It Does¶
Removes deleted Document IDs and consolidates multiple entries for the same word, improving search performance.
When to Optimize¶
- After bulk DELETE operations
- After many INSERT/UPDATE operations
- Periodically for maintenance
PHP Example - Optimizing Full-Text Index¶
<?php
$mysqli = new mysqli("localhost", "root", "password", "mydb");
// Enable full-text only optimization
$mysqli->query("SET GLOBAL innodb_optimize_fulltext_only = ON");
// Run optimization
$result = $mysqli->query("OPTIMIZE TABLE articles");
$row = $result->fetch_assoc();
echo "Optimization result: " . $row['Msg_text'] . "\n";
// Disable full-text only optimization (return to normal)
$mysqli->query("SET GLOBAL innodb_optimize_fulltext_only = OFF");
$mysqli->close();
?>
PHP Example - Staged Optimization for Large Tables¶
<?php
$mysqli = new mysqli("localhost", "root", "password", "mydb");
// For very large tables, optimize in stages
// This optimizes 2000 words at a time (default)
$mysqli->query("SET GLOBAL innodb_optimize_fulltext_only = ON");
// You can adjust how many words to optimize per run
$mysqli->query("SET GLOBAL innodb_ft_num_word_optimize = 500");
// Run OPTIMIZE multiple times to process the full index
for ($i = 1; $i <= 5; $i++) {
echo "Optimization run $i...\n";
$mysqli->query("OPTIMIZE TABLE articles");
sleep(1); // Brief pause between runs
}
// Reset to defaults
$mysqli->query("SET GLOBAL innodb_ft_num_word_optimize = 2000");
$mysqli->query("SET GLOBAL innodb_optimize_fulltext_only = OFF");
$mysqli->close();
?>
``
## 7. Rebuilding Full-Text Indexes
### When to Rebuild
You must rebuild FULLTEXT indexes after changing:
- `innodb_ft_min_token_size` or `ft_min_word_len`
- `innodb_ft_max_token_size` or `ft_max_word_len`
- `innodb_ft_server_stopword_table` or `innodb_ft_user_stopword_table`
- `innodb_ft_enable_stopword` or `ft_stopword_file`
- `ngram_token_size`
### PHP Example - Complete Rebuild Process
```php
<?php
$mysqli = new mysqli("localhost", "root", "password", "mydb");
if ($mysqli->connect_error) {
die("Connection failed: " . $mysqli->connect_error);
}
$table = "articles";
$column = "content";
$index_name = "content_idx";
echo "Starting index rebuild process...\n";
// Step 1: Drop the existing FULLTEXT index
echo "Dropping existing index...\n";
$sql = "ALTER TABLE $table DROP INDEX $index_name";
if ($mysqli->query($sql)) {
echo "✓ Index dropped successfully.\n";
} else {
echo "✗ Error dropping index: " . $mysqli->error . "\n";
exit;
}
// Step 2: Recreate the FULLTEXT index
echo "Creating new index...\n";
$sql = "ALTER TABLE $table ADD FULLTEXT INDEX $index_name ($column)";
if ($mysqli->query($sql)) {
echo "✓ Index created successfully.\n";
} else {
echo "✗ Error creating index: " . $mysqli->error . "\n";
exit;
}
echo "\nIndex rebuild complete!\n";
$mysqli->close();
?>
PHP Example - Rebuilding Multiple Indexes¶
<?php
$mysqli = new mysqli("localhost", "root", "password", "mydb");
// Define all tables and indexes that need rebuilding
$indexes = [
['table' => 'articles', 'index' => 'content_idx', 'columns' => 'title, content'],
['table' => 'blog_posts', 'index' => 'post_idx', 'columns' => 'body'],
['table' => 'products', 'index' => 'description_idx', 'columns' => 'description']
];
foreach ($indexes as $idx) {
echo "Processing {$idx['table']}...\n";
// Drop index
$sql = "ALTER TABLE {$idx['table']} DROP INDEX {$idx['index']}";
if ($mysqli->query($sql)) {
echo " ✓ Dropped {$idx['index']}\n";
} else {
echo " ✗ Error: " . $mysqli->error . "\n";
continue;
}
// Recreate index
$sql = "ALTER TABLE {$idx['table']} ADD FULLTEXT INDEX {$idx['index']} ({$idx['columns']})";
if ($mysqli->query($sql)) {
echo " ✓ Created {$idx['index']}\n";
} else {
echo " ✗ Error: " . $mysqli->error . "\n";
}
echo "\n";
}
echo "All indexes rebuilt!\n";
$mysqli->close();
?>
8. Practical Full-Text Search Examples¶
PHP Example - Building a Simple Search Function¶
<?php
class FullTextSearch {
private $mysqli;
public function __construct($host, $user, $pass, $db) {
$this->mysqli = new mysqli($host, $user, $pass, $db);
if ($this->mysqli->connect_error) {
die("Connection failed: " . $this->mysqli->connect_error);
}
}
/**
* Natural language search
*/
public function naturalSearch($query, $table, $columns) {
$sql = "SELECT *,
MATCH($columns) AGAINST(? IN NATURAL LANGUAGE MODE) AS relevance
FROM $table
WHERE MATCH($columns) AGAINST(? IN NATURAL LANGUAGE MODE)
ORDER BY relevance DESC";
$stmt = $this->mysqli->prepare($sql);
$stmt->bind_param("ss", $query, $query);
$stmt->execute();
return $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
}
/**
* Boolean search with operators
*/
public function booleanSearch($query, $table, $columns) {
$sql = "SELECT *,
MATCH($columns) AGAINST(? IN BOOLEAN MODE) AS relevance
FROM $table
WHERE MATCH($columns) AGAINST(? IN BOOLEAN MODE)
ORDER BY relevance DESC";
$stmt = $this->mysqli->prepare($sql);
$stmt->bind_param("ss", $query, $query);
$stmt->execute();
return $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
}
/**
* Search with query expansion (finds related terms)
*/
public function queryExpansion($query, $table, $columns) {
$sql = "SELECT *,
MATCH($columns) AGAINST(? WITH QUERY EXPANSION) AS relevance
FROM $table
WHERE MATCH($columns) AGAINST(? WITH QUERY EXPANSION)
ORDER BY relevance DESC";
$stmt = $this->mysqli->prepare($sql);
$stmt->bind_param("ss", $query, $query);
$stmt->execute();
return $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
}
/**
* Get search relevance score without filtering
*/
public function getRelevanceScores($query, $table, $columns, $limit = 10) {
$sql = "SELECT *,
MATCH($columns) AGAINST(? IN NATURAL LANGUAGE MODE) AS relevance
FROM $table
ORDER BY relevance DESC
LIMIT ?";
$stmt = $this->mysqli->prepare($sql);
$stmt->bind_param("si", $query, $limit);
$stmt->execute();
return $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
}
public function close() {
$this->mysqli->close();
}
}
// Usage example
$search = new FullTextSearch("localhost", "root", "password", "mydb");
// Natural language search
$results = $search->naturalSearch("mysql database optimization", "articles", "title, content");
echo "<h2>Natural Language Results:</h2>";
foreach ($results as $row) {
echo "<div>";
echo "<h3>" . htmlspecialchars($row['title']) . "</h3>";
echo "<p>Relevance: " . $row['relevance'] . "</p>";
echo "</div>";
}
// Boolean search
$results = $search->booleanSearch("+mysql +performance -oracle", "articles", "title, content");
echo "<h2>Boolean Search Results:</h2>";
foreach ($results as $row) {
echo "<div>";
echo "<h3>" . htmlspecialchars($row['title']) . "</h3>";
echo "<p>Relevance: " . $row['relevance'] . "</p>";
echo "</div>";
}
$search->close();
?>
PHP Example - Search with Pagination¶
<?php
function paginatedSearch($mysqli, $query, $page = 1, $per_page = 10) {
$offset = ($page - 1) * $per_page;
// Get total count
$count_sql = "SELECT COUNT(*) as total FROM articles
WHERE MATCH(title, content) AGAINST(? IN BOOLEAN MODE)";
$stmt = $mysqli->prepare($count_sql);
$stmt->bind_param("s", $query);
$stmt->execute();
$total = $stmt->get_result()->fetch_assoc()['total'];
$stmt->close();
// Get paginated results
$sql = "SELECT *,
MATCH(title, content) AGAINST(? IN BOOLEAN MODE) AS relevance
FROM articles
WHERE MATCH(title, content) AGAINST(? IN BOOLEAN MODE)
ORDER BY relevance DESC
LIMIT ? OFFSET ?";
$stmt = $mysqli->prepare($sql);
$stmt->bind_param("ssii", $query, $query, $per_page, $offset);
$stmt->execute();
$results = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt->close();
return [
'results' => $results,
'total' => $total,
'page' => $page,
'per_page' => $per_page,
'total_pages' => ceil($total / $per_page)
];
}
// Usage
$mysqli = new mysqli("localhost", "root", "password", "mydb");
$search_query = "+mysql +performance";
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$data = paginatedSearch($mysqli, $search_query, $page, 10);
echo "<h2>Search Results (Page {$data['page']} of {$data['total_pages']})</h2>";
echo "<p>Found {$data['total']} results</p>";
foreach ($data['results'] as $row) {
echo "<div>";
echo "<h3>" . htmlspecialchars($row['title']) . "</h3>";
echo "<p>" . htmlspecialchars(substr($row['content'], 0, 200)) . "...</p>";
echo "<small>Relevance: " . $row['relevance'] . "</small>";
echo "</div><hr>";
}
// Pagination links
echo "<div>";
for ($i = 1; $i <= $data['total_pages']; $i++) {
if ($i == $page) {
echo "<strong>$i</strong> ";
} else {
echo "<a href='?page=$i'>$i</a> ";
}
}
echo "</div>";
$mysqli->close();
?>
``
## 9. Common Issues and Solutions
### Issue 1: Search Returns No Results for Short Words
**Problem:** Searching for "AI" or "UK" returns nothing.
**Solution:** Lower the minimum token size (requires server restart and index rebuild).
```php
// After configuration change, rebuild index
$mysqli->query("ALTER TABLE articles DROP INDEX content_idx");
$mysqli->query("ALTER TABLE articles ADD FULLTEXT INDEX content_idx (content)");
Issue 2: Common Words Are Ignored¶
Problem: Searching for "the best database" ignores "the" and "best".
Solution: Use boolean mode or create custom stopword list.
// Use boolean mode
$query = "+the +best +database";
$sql = "SELECT * FROM articles
WHERE MATCH(content) AGAINST(? IN BOOLEAN MODE)";
Issue 3: 50% Threshold Blocks Results (MyISAM)¶
Problem: Word appears in too many documents and is ignored.
Solution: Always use IN BOOLEAN MODE for common terms.
// Boolean mode bypasses the 50% threshold
$sql = "SELECT * FROM articles
WHERE MATCH(content) AGAINST(? IN BOOLEAN MODE)";
Issue 4: Slow Search Performance¶
Problem: Full-text searches are slow on large tables.
Solution: Optimize the full-text index regularly.
$mysqli->query("SET GLOBAL innodb_optimize_fulltext_only = ON");
$mysqli->query("OPTIMIZE TABLE articles");
$mysqli->query("SET GLOBAL innodb_optimize_fulltext_only = OFF");
10. Best Practices¶
1. Always Use Prepared Statements¶
// ✓ GOOD - Protected against SQL injection
$stmt = $mysqli->prepare("SELECT * FROM articles
WHERE MATCH(content) AGAINST(? IN BOOLEAN MODE)");
$stmt->bind_param("s", $user_input);
$stmt->execute();
// ✗ BAD - Vulnerable to SQL injection
$sql = "SELECT * FROM articles
WHERE MATCH(content) AGAINST('$user_input' IN BOOLEAN MODE)";
2. Sanitize Boolean Operators from User Input¶
function sanitizeBooleanQuery($query) {
// Remove potentially problematic characters
// while keeping valid boolean operators
$query = preg_replace('/[^\w\s+\-"*()]/', '', $query);
return trim($query);
}
$user_query = sanitizeBooleanQuery($_GET['q']);
3. Add Minimum Query Length¶
function search($query) {
// Require at least 2 characters
if (strlen($query) < 2) {
return ['error' => 'Query too short. Minimum 2 characters.'];
}
// Perform search...
}
4. Cache Search Results¶
function cachedSearch($mysqli, $query, $cache_time = 300) {
$cache_key = 'search_' . md5($query);
$cache_file = "/tmp/$cache_key";
// Check if cache exists and is fresh
if (file_exists($cache_file) && (time() - filemtime($cache_file)) < $cache_time) {
return unserialize(file_get_contents($cache_file));
}
// Perform actual search
$stmt = $mysqli->prepare("SELECT * FROM articles
WHERE MATCH(content) AGAINST(? IN BOOLEAN MODE)");
$stmt->bind_param("s", $query);
$stmt->execute();
$results = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt->close();
// Cache results
file_put_contents($cache_file, serialize($results));
return $results;
}
5. Log Slow Queries¶
function logSlowQuery($query, $execution_time) {
if ($execution_time > 1.0) { // Log queries taking more than 1 second
$log_entry = date('Y-m-d H:i:s') . " - Slow query ($execution_time s): $query\n";
file_put_contents('/var/log/mysql_slow_searches.log', $log_entry, FILE_APPEND);
}
}
$start = microtime(true);
// ... perform search ...
$execution_time = microtime(true) - $start;
logSlowQuery($search_query, $execution_time);
11. Summary Checklist¶
Before making changes to FTS configuration:
- [ ] Understand the default behavior and why you need to change it
- [ ] Back up your database
- [ ] Test configuration changes on a development server first
- [ ] Plan for server downtime if restart is required
- [ ] Budget time for index rebuilding (can take hours on large tables)
- [ ] Document all configuration changes
- [ ] Monitor search performance after changes
Configuration changes that require restart and rebuild:
- [ ] Word length settings (innodb_ft_min_token_size, etc.)
- [ ] Stopword tables
- [ ] Token size for ngram parser
Configuration changes that don't require restart: - [ ] Boolean syntax operators (MyISAM only) - [ ] Session-level stopword settings
Resources¶
- MySQL 8.4 Full-Text Search Documentation: https://dev.mysql.com/doc/refman/8.4/en/fulltext-search.html
- MySQL 8.4 Fine-Tuning Guide: https://dev.mysql.com/doc/refman/8.4/en/fulltext-fine-tuning.html
- InnoDB Full-Text Indexes: https://dev.mysql.com/doc/refman/8.4/en/innodb-fulltext-index.html
Remember: Full-text search is powerful, but it's not a replacement for specialized search engines like Elasticsearch or Solr for very large datasets or complex search requirements. Use FTS for moderate-sized applications where you want search functionality without additional infrastructure.