Chapter 14 · Full-Text Search, R-Tree, Virtual Tables, and Extensions
FTS5 Fundamentals: Full-Text Tables, MATCH, and Tokenization
Build a small FTS5 search index, learn token-, phrase-, prefix-, Boolean-, and NEAR-style queries, and understand why tokenization makes full-text search fundamentally different from LIKE scans.
Learning outcomes
Full-text search is not “LIKE, but faster.” FTS5 tokenizes documents, builds an inverted index from tokens to matching rows, and evaluates a dedicated query language. The lesson starts with a small searchable FieldNotes document set so every MATCH result can be reasoned about by hand.
Explain why a leading-wildcard LIKE search and a token-based full-text index solve different problems.
Create and populate a basic self-contained FTS5 virtual table with stable rowid identities.
Use MATCH for terms, phrases, prefixes, Boolean expressions, and NEAR groups.
Explain the default unicode61 tokenizer and why tokenization is part of search semantics.
Distinguish default content storage, external-content, and contentless choices conceptually.
Build and verify a small note/article search feature without assuming language stemming that was not configured.
The application problem: searchable maintenance knowledge
FieldNotes has short maintenance notes and operating articles. A user wants “cooling pump vibration,” phrase searches such as “bearing inspection,” and prefixes such as vibrat*. A query like WHERE body LIKE '%vibration%' can find a substring, but a normal B-tree index generally cannot turn an arbitrary leading wildcard into a full-text inverted index. It also lacks FTS concepts such as token phrases, document relevance, NEAR groups, and tokenizer configuration.
| Question | LIKE pattern | FTS5 MATCH |
|---|---|---|
| Find a literal substring | Natural fit, often scan-oriented for %term% | Token search unless a specialized trigram design is chosen |
| Find a phrase of tokens | String pattern can approximate characters | Native phrase semantics after tokenization |
| Find token prefix | Awkward pattern logic | Native prefix syntax such as vibrat* |
| Rank by relevance | Not built in | bm25/rank in Lesson 3 |
| Control word splitting/case/diacritics | Collation/pattern semantics | Tokenizer is explicit search policy |
Confirm FTS5 exists before creating a table
SELECT sqlite_version();SELECT name FROM pragma_module_list WHERE name='fts5';SELECT compile_optionsFROM pragma_compile_optionsWHERE compile_options LIKE '%FTS5%';Modern source builds commonly include FTS5, but an embedded host can omit it. If the module is missing, treat the lab as conceptual until you have an approved SQLite build; do not substitute untrusted binaries.
Create a small self-contained FTS5 table
The default FTS5 mode stores the supplied content as well as the full-text index. For this first lesson that simplicity is useful. We explicitly assign rowids so the search row identity remains stable and can later correspond to an application document id.
DROP TABLE IF EXISTS search_fts;CREATE VIRTUAL TABLE search_fts USING fts5( title, body, tokenize='unicode61');INSERT INTO search_fts(rowid,title,body) VALUES(101,'Cooling pump vibration','Pump 7 shows rising vibration near the drive-end bearing.'),(102,'Bearing inspection','Inspect the cooling-water pump bearing and record vibration trend.'),(103,'Fan inspection due','Exhaust fan 14 requires belt and bearing inspection.'),(104,'Sensor calibration','Vibration sensor 3 calibration passed; verify MQTT telemetry.'),(105,'Pump seal leak','Cooling pump seal shows a small leak but vibration remains normal.');SELECT rowid,title FROM search_fts ORDER BY rowid;The ordinary-looking rows are backed by FTS5's structures. Do not create a separate B-tree index on body; the virtual-table module owns its search index.
MATCH searches tokens, not arbitrary character fragments
-- A token query.SELECT rowid,title FROM search_ftsWHERE search_fts MATCH 'vibration'ORDER BY rowid;-- A phrase: adjacent tokens in this order.SELECT rowid,title FROM search_ftsWHERE search_fts MATCH '"bearing inspection"'ORDER BY rowid;-- A prefix token query.SELECT rowid,title FROM search_ftsWHERE search_fts MATCH 'vibrat*'ORDER BY rowid;With the default tokenizer, punctuation and whitespace help define tokens. A phrase is about token adjacency after tokenization—not a byte-for-byte substring contract. If an application needs arbitrary substring search, investigate an intentionally configured trigram tokenizer or another search design rather than assuming the default FTS index does it.
Boolean expressions and NEAR groups express search intent
FTS5 supports explicit AND, OR, and NOT, plus documented NEAR groups. Keep the syntax readable and parameterize the query string in application code; a search expression is still user-controlled input even though it is not ordinary SQL syntax.
SELECT rowid,title FROM search_ftsWHERE search_fts MATCH 'pump AND vibration'ORDER BY rowid;SELECT rowid,title FROM search_ftsWHERE search_fts MATCH 'pump NOT leak'ORDER BY rowid;SELECT rowid,title FROM search_ftsWHERE search_fts MATCH 'NEAR(pump bearing, 8)'ORDER BY rowid;SELECT rowid,title FROM search_ftsWHERE search_fts MATCH 'sensor OR fan'ORDER BY rowid;NEAR operates on token positions. It is useful when proximity carries meaning, but it is not a substitute for a domain-specific parser or semantic search model.
The tokenizer is part of your schema contract
FTS5's default unicode61 tokenizer follows Unicode 6.1 character categories. It performs case folding according to those rules and, by default, removes diacritics from Latin-script characters in the documented way. That is useful general-purpose behavior, but it is not a promise of language-aware stemming.
| Tokenizer | What it does | Production caution |
|---|---|---|
| unicode61 | Default Unicode 6.1 tokenization/case handling; configurable separators/categories/diacritics | Test real languages and punctuation used by your corpus. |
| ascii | ASCII-oriented case folding/token rules | Non-ASCII handling differs from unicode61. |
| porter | Wraps another tokenizer and applies Porter stemming | Designed for English; do not advertise it as a multilingual stemmer. |
| trigram | Indexes contiguous three-character sequences for substring-style search | Different index size/query behavior; test before choosing it. |
Prefix indexes are optional acceleration, not the meaning of *
FTS5 can be configured with prefix indexes such as prefix='2 3' to accelerate some prefix queries. The * query operator still defines prefix semantics even without that configuration; the extra prefix indexes trade database size/write work for potentially faster prefix searches.
CREATE VIRTUAL TABLE search_prefix USING fts5( title, body, tokenize='unicode61', prefix='2 3');Do not add prefix indexes “just in case.” Measure your actual query mix, corpus size, ingestion rate, and database growth.
Content-storage choices exist because indexing and content are separate concerns
The default FTS5 table stores content and index data together. An external-content FTS5 table stores the full-text index while fetching displayed columns from another table; keeping the two synchronized becomes your responsibility. A contentless design stores no retrievable content columns in the FTS table. Newer SQLite versions also support contentless-delete, which improves update/delete ergonomics for that specialized mode. Lesson 3 develops these tradeoffs only after basic MATCH behavior is solid.
Compare evidence: ordinary substring scan versus FTS MATCH
CREATE TABLE ordinary_docs( doc_id INTEGER PRIMARY KEY, title TEXT NOT NULL, body TEXT NOT NULL);INSERT INTO ordinary_docsSELECT rowid,title,body FROM search_fts;EXPLAIN QUERY PLANSELECT doc_id,title FROM ordinary_docsWHERE body LIKE '%vibration%';EXPLAIN QUERY PLANSELECT rowid,title FROM search_ftsWHERE search_fts MATCH 'vibration';On a tiny five-row dataset timing is meaningless; the useful observation is that the first query is an ordinary-table pattern scan while the second delegates a MATCH constraint to the FTS5 virtual table. Scale tests belong on realistic corpora.
Lab: build a search-query matrix
Predict results before executing. Record which rows match each query and explain the result in terms of tokens, phrase order, prefix semantics, or Boolean logic.
SELECT 'term' AS kind,rowid,title FROM search_fts WHERE search_fts MATCH 'bearing';SELECT 'phrase',rowid,title FROM search_fts WHERE search_fts MATCH '"cooling pump"';SELECT 'prefix',rowid,title FROM search_fts WHERE search_fts MATCH 'calibrat*';SELECT 'boolean',rowid,title FROM search_fts WHERE search_fts MATCH 'pump AND normal';SELECT 'near',rowid,title FROM search_fts WHERE search_fts MATCH 'NEAR(sensor telemetry, 5)';Then add one row containing punctuation, mixed case, and a Latin diacritic relevant to your application and document how your chosen tokenizer treats it. Search behavior should be tested as deliberately as schema constraints.
Verification checkpoint
FTS5 fundamentals checkpoint
Focus on the distinction between text matching and indexed token search.
- Why is LIKE %term% not equivalent to an FTS5 index?
- What does the FTS5 rowid represent in the examples?
- What is the difference between a phrase and a prefix query?
- What does unicode61 provide, and what does it not promise?
- Why might a prefix index be omitted even if prefix queries are supported?
- What new responsibility appears with external-content FTS5 tables?
Review the answers
LIKE can search arbitrary character patterns but does not create FTS token/relevance structures. rowid is the stable FTS document identity used here. Phrases require adjacent token sequences; prefix syntax matches token prefixes. unicode61 defines tokenization/case/diacritic behavior but is not a universal language stemmer. Prefix indexes cost storage/write work and should be measured. External-content tables make the application/schema responsible for keeping the FTS index synchronized with the content table.
Production judgment and bridge
FTS5 makes search semantics explicit: tokens, phrases, prefixes, and tokenizer policy. Lesson 3 adds the application-facing pieces—ranking, excerpts, external-content lifecycle, integrity checks, and index maintenance.