MySQLFull-Text Indexes

MySQL FULLTEXT Index

A FULLTEXT index enables efficient natural-language text search across one or more TEXT or VARCHAR columns. Unlike a regular B-tree index (which only helps with prefix matches), a FULLTEXT index builds an inverted index of every word in the indexed text, making it possible to search for any word or phrase anywhere in a large body of text.

Creating a FULLTEXT Index

SQL
-- At table creation time
CREATE TABLE articles (
  id      INT          NOT NULL AUTO_INCREMENT,
  title   VARCHAR(255) NOT NULL,
  body    TEXT         NOT NULL,
  created DATETIME     DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id),
  FULLTEXT INDEX ft_article_content (title, body)
);

-- On an existing table
ALTER TABLE articles ADD FULLTEXT INDEX ft_article_content (title, body);

-- Using CREATE INDEX syntax
CREATE FULLTEXT INDEX ft_article_content ON articles (title, body);

-- Remove a FULLTEXT index
ALTER TABLE articles DROP INDEX ft_article_content;
Note
FULLTEXT indexes can only be created on columns with type CHAR, VARCHAR, or TEXT. You cannot create a FULLTEXT index on numeric or date columns.
MATCH() AGAINST() Syntax

FULLTEXT searches are performed with MATCH(columns) AGAINST(search_string [mode]). The column list in MATCH must exactly match the columns in the FULLTEXT index.

SQL
-- Basic full-text search (natural language mode, default)
SELECT id, title
FROM articles
WHERE MATCH(title, body) AGAINST('mysql database');

-- MATCH() returns a relevance score — use it for ranking
SELECT id, title,
  ROUND(MATCH(title, body) AGAINST('mysql database'), 4) AS relevance
FROM articles
WHERE MATCH(title, body) AGAINST('mysql database')
ORDER BY relevance DESC
LIMIT 10;
Boolean Mode — Full Operator Reference

Boolean mode gives you explicit control over which words must or must not appear. Use operators to build precise search queries.

Operator

Meaning

Example

Effect

+

Word MUST be present

+mysql +index

Both words required

-

Word MUST NOT be present

+mysql -oracle

Must have mysql, no oracle

(none)

Word is optional — boosts score

mysql tutorial

Optional, increases relevance

Wildcard (trailing only)

data*

Matches: data, database, datatype

"..."

Exact phrase match

"primary key"

Words adjacent in that order

Increase contribution to rank

mysql tutorial

mysql weighted higher

<

Decrease contribution to rank

mysql <tutorial

tutorial weighted lower

~

Negate relevance (still matched)

~beginner tutorial

beginner reduces score

()

Group sub-expressions

+(mysql postgres) -oracle

mysql OR postgres, no oracle

SQL
-- Must contain 'mysql', must not contain 'oracle'
SELECT title FROM articles
WHERE MATCH(title, body) AGAINST('+mysql -oracle' IN BOOLEAN MODE);

-- Must contain 'mysql' AND 'index'
SELECT title FROM articles
WHERE MATCH(title, body) AGAINST('+mysql +index' IN BOOLEAN MODE);

-- Exact phrase match — words must be adjacent
SELECT title FROM articles
WHERE MATCH(title, body) AGAINST('"primary key"' IN BOOLEAN MODE);

-- Wildcard: find 'data', 'database', 'datatype', etc.
SELECT title FROM articles
WHERE MATCH(title, body) AGAINST('data*' IN BOOLEAN MODE);

-- Complex: must have 'mysql', optionally 'tutorial' or 'guide', never 'paid'
-- 'guide' ranks higher than 'tutorial'
SELECT title, MATCH(title, body) AGAINST('+mysql >guide tutorial -paid' IN BOOLEAN MODE) AS score
FROM articles
WHERE MATCH(title, body) AGAINST('+mysql >guide tutorial -paid' IN BOOLEAN MODE)
ORDER BY score DESC;
Note
Boolean mode does not apply the 50% threshold rule, so common words are not ignored. Results are not sorted by relevance by default — add an explicit ORDER BY score.
Relevance Score Explained (TF-IDF)

The relevance score returned by MATCH() AGAINST() is based on a TF-IDF (Term Frequency - Inverse Document Frequency) algorithm:

  • Term Frequency (TF): how many times the search term appears in this particular row. More occurrences = higher score.
  • Inverse Document Frequency (IDF): terms that appear in many rows get lower scores (they are less distinctive). A term in only 5% of rows scores much higher than a term in 80% of rows.
  • Document length normalization: the same number of occurrences in a shorter document ranks higher than in a longer one.

A score of 0 means no match. Scores are not normalized to a fixed range — they are relative to the dataset.

SQL
-- Show relevance scores for debugging and tuning
SELECT
  id,
  title,
  ROUND(MATCH(title, body) AGAINST('mysql index performance'), 4) AS score
FROM articles
WHERE MATCH(title, body) AGAINST('mysql index performance')
ORDER BY score DESC;

-- Scores differ between natural language mode and boolean mode
SELECT
  title,
  MATCH(title, body) AGAINST('mysql' IN NATURAL LANGUAGE MODE) AS nl_score,
  MATCH(title, body) AGAINST('+mysql' IN BOOLEAN MODE)         AS bool_score
FROM articles
WHERE MATCH(title, body) AGAINST('mysql' IN NATURAL LANGUAGE MODE);
Natural Language Mode

SQL
-- Explicit natural language mode
SELECT title,
  MATCH(title, body) AGAINST('mysql tutorial' IN NATURAL LANGUAGE MODE) AS score
FROM articles
WHERE MATCH(title, body) AGAINST('mysql tutorial' IN NATURAL LANGUAGE MODE)
ORDER BY score DESC
LIMIT 10;
Tip
In natural language mode, words appearing in more than 50% of rows are treated as stop words and ignored. This is why a single very common word may return no results. Boolean mode does not apply this threshold.
Query Expansion Mode

Query expansion performs two searches automatically:

  1. A natural language search for the original terms.
  2. A second search that adds the most significant words found in the top results of step 1.

This broadens results to related content even if the exact search term does not appear.

SQL
-- Search for 'database' but also find articles about SQL, MySQL, schema
-- even if they do not contain the word 'database'
SELECT title
FROM articles
WHERE MATCH(title, body) AGAINST('database' WITH QUERY EXPANSION);

-- Useful for finding conceptually related articles a user might want
Warning
Query expansion can return surprising or noisy results on small datasets. It works best with hundreds or thousands of documents. Always test on your real data before exposing it to users.
Combining Relevance with Recency for Ranking

Pure TF-IDF relevance scores favor articles that mention the search term most. In practice, you often want to blend relevance with how recent the content is:

SQL
-- Blend relevance score with recency using a time-decay factor
SELECT
  id,
  title,
  created_at,
  MATCH(title, body) AGAINST('mysql performance' IN BOOLEAN MODE) AS relevance,
  -- Recency factor: 1.0 for today, ~0.5 for 30 days ago, decays exponentially
  EXP(-0.023 * DATEDIFF(NOW(), created_at))                       AS recency_factor,
  -- Combined score: adjust weights to tune the balance
  MATCH(title, body) AGAINST('mysql performance' IN BOOLEAN MODE)
    * EXP(-0.023 * DATEDIFF(NOW(), created_at))                   AS combined_score
FROM articles
WHERE MATCH(title, body) AGAINST('mysql performance' IN BOOLEAN MODE)
  AND published = 1
ORDER BY combined_score DESC
LIMIT 20;
ft_min_word_len and Stop Word Tuning

Bash
# Check current minimum token size
SHOW VARIABLES LIKE 'innodb_ft_min_token_size';
# Default: 3

# To index 2-character words (e.g. "go", "UK", "AI"):
# Add to my.cnf under [mysqld]:
# innodb_ft_min_token_size = 2

# View the default stop word list
SELECT * FROM information_schema.INNODB_FT_DEFAULT_STOPWORD;

# Use a custom stop word file
# innodb_ft_server_stopword_table = mydb/my_stopwords
# (table with a single VARCHAR column named 'value')

SQL
-- After changing innodb_ft_min_token_size, rebuild ALL FULLTEXT indexes
ALTER TABLE articles DROP INDEX ft_article_content;
ALTER TABLE articles ADD FULLTEXT INDEX ft_article_content (title, body);

-- Also run OPTIMIZE TABLE to flush the FTS cache
OPTIMIZE TABLE articles;
InnoDB FULLTEXT Internals

InnoDB implements FULLTEXT indexes using several hidden auxiliary tables in the same database. Understanding this helps with troubleshooting and performance:

SQL
-- Each FULLTEXT index creates 6 auxiliary tables (FTS_*)
-- They are visible in information_schema
SELECT TABLE_NAME FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE()
  AND TABLE_NAME LIKE 'FTS_%';
-- Example: FTS_000000000169_00000000016a_INDEX_1, etc.

-- InnoDB adds a hidden FTS_DOC_ID column (BIGINT UNSIGNED) to every table
-- with a FULLTEXT index for fast lookup
SHOW CREATE TABLE articlesG

-- InnoDB batches FULLTEXT index updates optimistically
-- Recent inserts/updates may not be immediately searchable
-- Force sync with OPTIMIZE TABLE
OPTIMIZE TABLE articles;

-- Check the size of FULLTEXT auxiliary tables
SELECT
  table_name,
  ROUND(data_length / 1024 / 1024, 2) AS data_mb,
  ROUND(index_length / 1024 / 1024, 2) AS index_mb
FROM information_schema.TABLES
WHERE table_schema = DATABASE()
  AND table_name LIKE 'FTS_%';
Ngram Parser for CJK and Short Words

The default FULLTEXT parser breaks text on whitespace and punctuation — it does not work for Chinese, Japanese, or Korean (CJK) text, which has no spaces between words. The ngram parser tokenizes text into overlapping n-character sequences, making it suitable for CJK and for searching short terms that the default parser would skip.

SQL
-- Check ngram token size (default: 2)
SHOW VARIABLES LIKE 'ngram_token_size';

-- Create a FULLTEXT index using the ngram parser
CREATE TABLE products_cjk (
  id      INT AUTO_INCREMENT PRIMARY KEY,
  name_ja TEXT,
  FULLTEXT INDEX ft_name (name_ja) WITH PARSER ngram
);

-- Search CJK text with ngram
SELECT * FROM products_cjk
WHERE MATCH(name_ja) AGAINST('データベース' IN BOOLEAN MODE);

-- ngram also helps search short English words like "MySQL", "AI", "JS"
-- when innodb_ft_min_token_size > 2 would otherwise exclude them
FULLTEXT vs LIKE vs Elasticsearch

Factor

FULLTEXT Index

LIKE %keyword%

Elasticsearch

Performance on large text

Very fast (inverted index)

Slow (full scan)

Very fast (distributed inverted index)

Relevance ranking

Built-in TF-IDF scoring

No ranking

Advanced BM25 scoring, custom ranking

Phrase search

Yes (Boolean mode)

Partial (careful patterns)

Yes + proximity, slop

Fuzzy matching

No (exact tokens only)

No

Yes (edit distance)

Synonyms

No

No

Yes (synonym token filter)

Faceted search

Requires GROUP BY

Requires GROUP BY

Native aggregation API

Data freshness

Near real-time (InnoDB buffer)

Always current

~1 second default refresh

Setup complexity

Single SQL command

None

Separate infrastructure

Best for

App search on moderate data

Simple substring matching

Large-scale, advanced search products

Practical E-Commerce Product Search

SQL
-- Product table with FULLTEXT on name + description + tags
CREATE TABLE products (
  id          INT          NOT NULL AUTO_INCREMENT,
  name        VARCHAR(255) NOT NULL,
  description TEXT         NOT NULL,
  tags        VARCHAR(500),
  price       DECIMAL(10,2),
  published   TINYINT(1)   NOT NULL DEFAULT 0,
  created_at  DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id),
  FULLTEXT INDEX ft_products (name, description, tags)
);

-- Search API: rank by relevance, blend with recency
-- ? placeholder will be bound by the application
SELECT
  id,
  name,
  price,
  LEFT(description, 150)                                       AS excerpt,
  MATCH(name, description, tags) AGAINST(? IN BOOLEAN MODE)    AS relevance
FROM products
WHERE published = 1
  AND MATCH(name, description, tags) AGAINST(? IN BOOLEAN MODE)
ORDER BY relevance DESC
LIMIT 20 OFFSET 0;

-- Boolean search: must contain 'laptop', optionally 'gaming', exclude 'refurbished'
SELECT name, price
FROM products
WHERE published = 1
  AND MATCH(name, description, tags)
      AGAINST('+laptop gaming -refurbished' IN BOOLEAN MODE)
ORDER BY
  MATCH(name, description, tags) AGAINST('+laptop gaming -refurbished' IN BOOLEAN MODE) DESC,
  price ASC;
FULLTEXT on InnoDB vs MyISAM

Feature

InnoDB

MyISAM

Supported since

MySQL 5.6

MySQL 3.23

Transactions

Yes

No

Default min word length

3 (innodb_ft_min_token_size)

4 (ft_min_word_len)

Phrase search

Yes (Boolean mode)

Yes (Boolean mode)

Update behavior

Near real-time (batched)

Full rebuild required

Recommended for

All new tables

Legacy only

Tip
Always use InnoDB for FULLTEXT indexes in modern MySQL. MyISAM lacks transactions and foreign key support. There is no reason to use MyISAM for new tables.
Limitations
  • FULLTEXT searches only work on TEXT, CHAR, and VARCHAR columns.

  • The column list in MATCH() must exactly match the FULLTEXT index definition.

  • You cannot use FULLTEXT in a subquery that references a different table.

  • Very short words (below min token size) are silently ignored — test with your actual data.

  • FULLTEXT indexes are larger than B-tree indexes and slow down INSERT/UPDATE/DELETE.

  • Natural language mode silently ignores words in more than 50% of rows.

  • No native fuzzy matching or synonym support — use Elasticsearch for those requirements.

Checking FULLTEXT Index Internals

SQL
-- View tokenized words in the FULLTEXT index
-- (Requires setting innodb_ft_aux_table first)
SET GLOBAL innodb_ft_aux_table = 'mydb/articles';

SELECT * FROM information_schema.INNODB_FT_INDEX_CACHE LIMIT 20;
-- Shows: word, doc_id, position, count

SELECT * FROM information_schema.INNODB_FT_INDEX_TABLE LIMIT 20;
-- Shows committed (persisted) index entries

-- Check FULLTEXT configuration
SHOW VARIABLES LIKE 'innodb_ft%';
-- innodb_ft_min_token_size    = 3
-- innodb_ft_max_token_size    = 84
-- innodb_ft_enable_stopword   = ON
-- innodb_ft_server_stopword_table = (empty = use default)
FULLTEXT Quick Reference

Mode

Syntax

Key Behavior

Natural Language (default)

AGAINST('mysql database')

TF-IDF relevance, stop words, 50% threshold

Natural Language explicit

AGAINST('mysql' IN NATURAL LANGUAGE MODE)

Same as default — explicit declaration

Boolean Mode

AGAINST('+mysql -oracle' IN BOOLEAN MODE)

Operators control inclusion/exclusion, no 50% threshold

Query Expansion

AGAINST('database' WITH QUERY EXPANSION)

Two-pass search, finds related terms automatically

Variable

Default

Effect

innodb_ft_min_token_size

3

Words shorter than this are not indexed

innodb_ft_max_token_size

84

Words longer than this are not indexed

innodb_ft_enable_stopword

ON

Whether to filter common stop words

innodb_ft_server_stopword_table

(empty)

Custom stop word table: schema/table format

ngram_token_size

2

Token size for ngram parser (CJK/short words)

Common FULLTEXT Pitfalls

Pitfall

Symptom

Fix

MATCH() columns do not match index

ERROR: MATCH column list does not match any FULLTEXT index

MATCH() column list must exactly match the indexed columns

Short words not found

Search for "go" or "AI" returns 0 results

Lower innodb_ft_min_token_size to 2 and rebuild the index

Common word returns nothing (NL mode)

Search for 'the' returns nothing

Use Boolean mode: AGAINST("+the" IN BOOLEAN MODE)

50% threshold in NL mode

Word appears in many rows — returns no results

Use Boolean mode: AGAINST("+word" IN BOOLEAN MODE)

No results for exact phrase

Phrase search returns unexpected results

Use double quotes in Boolean mode: AGAINST('"primary key"' IN BOOLEAN MODE)

Wildcard at start does not work

'*word' matches nothing

Wildcards only work at the END of a term: "word*"

Score always 0

MATCH() returns 0 in EXPLAIN but matches in WHERE

Use both in WHERE and SELECT: WHERE MATCH()... ORDER BY MATCH()...

Adding FULLTEXT to an Existing Busy Table

Adding a FULLTEXT index to a large production table is a heavyweight operation that builds the entire inverted index from scratch. Plan accordingly:

SQL
-- Option 1: ALTER TABLE (rebuilds table online in MySQL 5.6+ / InnoDB)
-- This acquires a metadata lock for the duration — may block writes briefly
ALTER TABLE articles ADD FULLTEXT INDEX ft_content (title, body),
  ALGORITHM=INPLACE, LOCK=NONE;
-- ALGORITHM=INPLACE allows concurrent DML on MySQL 5.7+ (for FULLTEXT, LOCK=SHARED may be needed)

-- Option 2: CREATE INDEX (same as ALTER TABLE ADD FULLTEXT)
CREATE FULLTEXT INDEX ft_content ON articles (title, body);

-- Option 3: For very large tables, use pt-online-schema-change
-- pt-online-schema-change --alter "ADD FULLTEXT INDEX ft_content (title, body)" --   D=mydb,t=articles --execute

-- After adding the index, optimize to flush FTS cache to disk
OPTIMIZE TABLE articles;

-- Verify the index was created
SHOW INDEX FROM articles WHERE Index_type = 'FULLTEXT';