Full-text search
Find rows that contain words, rank the best matches first, and mark the found words in bold.
- Category: Indexes & search
- Keywords: full-text, fulltext, text search, search engine, find words, FULLTEXT, BM25, relevance, ranking, best matches, phrase, prefix, wildcard, exclude word, stemming, stem, stop words, language, case folding, Porter, Nepali search, Devanagari, bold, search result page, news search, forum search, how the index is used, count from the index, text match is checked on them, word positions
- See also: Indexes, Text functions, Regular expressions, WHERE, EXPLAIN
Full-text search finds rows that contain some words, like a search box on a website. LIKE '%bank%' must read every row and check every letter; it also finds bank inside embankment. A FULLTEXT index keeps a list of words, and for each word the rows that have it. So a search in 200,000 news articles takes a few milliseconds.
Three functions work together:
- match says if a row has the words (use it in
WHERE). - search_rank says how well a row matches (use it in
ORDER BY). - highlight returns the text with the found words inside
<b>and</b>.
The examples use a news table: id, symbol, published_at, title, body, with articles in English and Nepali.
Example: A news search page: the 3 best articles for 'bank' (sample data: content-news)
Setup:
CREATE FULLTEXT INDEX news_text ON news (title, body);
SELECT id,
highlight(title, 'bank') AS title,
round(search_rank(title, body, 'bank'), 3) AS rank
FROM news
WHERE MATCH(title, body, 'bank')
ORDER BY search_rank(title, body, 'bank') DESC
LIMIT 3;
Result:
| id (LONG) | title (STRING) | rank (DOUBLE) |
|---|---|---|
| 5 | Coastal Bank merger talks | 0.845 |
| 1 | City Bank profit rises 18% | 0.816 |
| 4 | Market index closes at 2,840 | 0.562 |
(3 rows)
CREATE FULLTEXT INDEX
Syntax:
CREATE FULLTEXT INDEX [IF NOT EXISTS] name ON table (column [, column ...]) [WITH (language = 'english')]
Makes a full-text index on one or more text columns. With several columns, they are searched together as one text.
| Argument | Type | Meaning |
|---|---|---|
name | name | the index name, unique in the whole database |
table | name | the table |
column | names | 1 to 16 STRING, SYMBOL or JSON columns |
language | text | optional: 'english' cuts words to their stem and leaves out stop words (see Language); 'simple' is the default |
- Returns: a message:
index 'name' created on table 'table' (N rows). - NULL: a
NULLcolumn counts as empty text. - Notes:
- A query uses the index when
match(...)names the same columns as the index (the order of the columns does not matter).match(title, 'bank')does not use an index on(title, body). match,search_rankandhighlightalso work without an index. Then KDB reads every row. That is fine for small tables.- Like every index it lives in memory.
kdb servebuilds it on a background thread right after the database opens, after the other indexes (see built at open); a query that needs it sooner builds it itself. It takes a few seconds for 200,000 articles.INSERT,UPDATEandDELETEkeep it up to date. - Since 2026-10-02 the words of each partition are also saved on disk, so a restart does not split every row into words again (see segments on disk).
- The index keeps where each word is in the text, so a phrase (
"net profit") is also found from the index. With them, the index took a little less memory than the text itself (302 MB for 378 MB of news, measured before 2026-10-02). - Since 2026-10-02 the index is smaller and its searches skip more. Where a row is: two numbers (partition, row), about 40 bytes a row instead of about 200. The rows of a word are packed in blocks of 128; each block notes its last row, so a search for a rare word with a common one jumps over whole blocks of the common word. Each block also notes the highest count of the word in it, for faster top-N ranking later.
GET /api/statsshows its memory undermemory:fulltext_index_bytes(part ofindex_bytes), withfulltext_indexes,fulltext_docsandfulltext_terms(distinct words). JSON (GIN) indexes are counted there too.DROP INDEX,SHOW INDEXESand the other rules of indexes are the same. A FULLTEXT index cannot beUNIQUE.- A column of another type (
LONG,TIMESTAMP, ...) gives an error.
- A query uses the index when
Example: Search news in the title and the body (sample data: content-news)
CREATE FULLTEXT INDEX news_text ON news (title, body);
SELECT id, title FROM news WHERE MATCH(title, body, 'city bank') ORDER BY id;
Result:
CREATE FULLTEXT INDEX news_text ON news (title, body)
index 'news_text' created on table 'news' (5 rows)
SELECT id, title FROM news WHERE match(title, body, 'city bank') ORDER BY id
| id (LONG) | title (STRING) |
|---|---|
| 1 | City Bank profit rises 18% |
| 4 | Market index closes at 2,840 |
(2 rows)
Example: EXPLAIN shows the index (sample data: content-news)
Setup:
CREATE FULLTEXT INDEX news_text ON news (title, body);
EXPLAIN SELECT id FROM news WHERE MATCH(body, title, 'bank');
EXPLAIN SELECT id FROM news WHERE MATCH(title, 'bank');
Result:
EXPLAIN SELECT id FROM news WHERE match(body, title, 'bank')
| plan (STRING) |
|---|
| scan table news (1 of 1 partitions, about 5 rows) |
| use index news_text (full-text search) |
| build 1 of 5 stored column(s) per row: id |
| engine: column |
(4 rows)
EXPLAIN SELECT id FROM news WHERE match(title, 'bank')
| plan (STRING) |
|---|
| scan table news (1 of 1 partitions, about 5 rows) |
| build 2 of 5 stored column(s) per row: id, title |
| filter while scanning: 1 condition(s) |
| engine: row (a condition the column engine does not do) |
(4 rows)
The first query uses news_text (same columns, other order). The second names only title, so it reads the table.
Example: Forum: search the posts (sample data: content-forum)
Setup:
CREATE FULLTEXT INDEX posts_body ON posts (body);
SELECT id, thread_id, author, body FROM posts WHERE MATCH(body, 'bonus') ORDER BY id;
Result:
| id (LONG) | thread_id (LONG) | author (STRING) | body (STRING) |
|---|---|---|---|
| 4 | 1 | gita | What about the bonus shares? |
| 5 | 1 | ram | Bonus is 10% this time. |
(2 rows)
Example: Changes are in the index at once (sample data: content-news)
Setup:
CREATE FULLTEXT INDEX news_text ON news (title, body);
UPDATE news SET body = 'Coastal Bank Bank and Prabhu Bank signed the merger.' WHERE id = 5;
SELECT id FROM news WHERE MATCH(title, body, 'prabhu');
DELETE FROM news WHERE id = 4;
SELECT id FROM news WHERE MATCH(title, body, 'gainers');
Result:
UPDATE news SET body = 'Coastal Bank Bank and Prabhu Bank signed the merger.' WHERE id = 5
1 row(s) updated
SELECT id FROM news WHERE match(title, body, 'prabhu')
| id (LONG) |
|---|
| 5 |
(1 row)
DELETE FROM news WHERE id = 4
1 row(s) deleted
SELECT id FROM news WHERE match(title, body, 'gainers')
| id (LONG) |
|---|
(0 rows)
Example: Only text columns (sample data: content-news; shows an error)
CREATE FULLTEXT INDEX news_time ON news (published_at);
Result:
Error: index 'news_time' on table 'news' is FULLTEXT but column 'published_at' is TIMESTAMP; expected STRING (text) columns
Language: stems and stop words
Syntax:
CREATE FULLTEXT INDEX name ON table (columns) WITH (language = 'english')
By default a search finds whole words only: planning does not find plans. An index made WITH (language = 'english') cuts each word to its stem first (the Porter2 English stemmer), so plans, planned and planning are all plan.
What 'english' changes:
| Rule | Example |
|---|---|
| English words become their stem | running, runs, run are one word; ran stays ran |
| Stop words are left out | the, of, and, is, ... are not in the index; a search for the alone finds nothing |
| Upper and lower case match for Latin, Greek and Cyrillic letters | CAFÉ = café, МОСКВА = москва |
| Phrases skip stop words | "talks about a merger" finds talks about the merger |
| A prefix is stemmed too, and is never a stop word | planning* finds plans; the* finds theory |
- Notes:
- The search text is cut with the same rules as the rows.
match,search_rankandhighlighton the columns of such an index use them too, also when they read the rows instead of the index. - Words with digits, Nepali and other scripts are not stemmed.
- An index without the option (or
language = 'simple') works exactly as before. - To change the language of an index,
DROP INDEXit and create it again. A table has one FULLTEXT index for the same columns. - Other languages give an error. Data with such an index needs KDB 2026-10-02 or newer (see versions).
- The search text is cut with the same rules as the rows.
Example: 'planning' finds 'plans' and 'planned' only with the English analyzer (sample data: content-news)
Setup:
CREATE FULLTEXT INDEX news_en ON news (title, body) WITH (language = 'english');
SELECT id, title FROM news WHERE MATCH(title, body, 'planning') ORDER BY id;
SELECT id, highlight(body, 'shares') AS body FROM news WHERE MATCH(title, body, 'share') ORDER BY id;
SELECT match('Coastal Bank plans', 'planning') AS without_index;
Result:
SELECT id, title FROM news WHERE match(title, body, 'planning') ORDER BY id
| id (LONG) | title (STRING) |
|---|---|
| 3 | Hydropower company plans IPO |
| 5 | Coastal Bank merger talks |
(2 rows)
SELECT id, highlight(body, 'shares') AS body FROM news WHERE match(title, body, 'share') ORDER BY id
| id (LONG) | body (STRING) |
|---|---|
| 3 | Volt River Hydropower will sell new shares. The IPO opens next month. |
| 4 | Banks and hydropower shares moved up. City Bank led the gainers. |
(2 rows)
SELECT match('Coastal Bank plans', 'planning') AS without_index
| without_index (BOOL) |
|---|
| false |
(1 row)
Example: Stop words do not count in a phrase (sample data: content-news)
Setup:
CREATE FULLTEXT INDEX news_en ON news (title, body) WITH (language = 'english');
SELECT id FROM news WHERE MATCH(title, body, '"talks about the merger"');
SELECT count(*) AS found FROM news WHERE MATCH(title, body, 'the');
Result:
SELECT id FROM news WHERE match(title, body, '"talks about the merger"')
| id (LONG) |
|---|
| 5 |
(1 row)
SELECT count(*) AS found FROM news WHERE match(title, body, 'the')
| found (LONG) |
|---|
| 0 |
(1 row)
Example: Only english and simple (sample data: content-news; shows an error)
CREATE FULLTEXT INDEX news_fr ON news (title, body) WITH (language = 'french');
Result:
Error: index 'news_fr' on table 'news' has language 'french'; expected 'english' or 'simple'
match
Syntax:
match(column [, column ...], 'words')
MATCH(column [, column ...], 'words')
true when the text of the columns has the words of the search text. The search text can have phrases, prefixes and words that must not be there (see Search text).
| Argument | Type | Meaning |
|---|---|---|
column | STRING, SYMBOL or JSON (other values are turned into text) | the text to search; several columns are searched as one text |
words | STRING | the search text; always the last argument |
- Returns:
BOOL. - NULL:
NULLsearch text givesNULL(the row is not found). ANULLcolumn counts as empty text. - Notes:
- English letters A-Z: case does not matter. Other letters must be the same.
- Whole words only, no stemming:
bankdoes not findbanks. Use a prefix (bank*), or an index made WITH (language = 'english'). - Every word must be there. There is no
ORinside the search text. For "one word or the other" write two calls:MATCH(body, 'bonus') OR MATCH(body, 'dividend')(this reads every row). - A search with no word to find (empty, only
-word, only*) finds nothing. - At least 2 arguments (1 column and the words), at most 64.
Example: Every word must be there, in any order and any case (sample data: content-news)
SELECT id, title FROM news WHERE MATCH(title, body, 'BANK city') ORDER BY id;
Result:
| id (LONG) | title (STRING) |
|---|---|
| 1 | City Bank profit rises 18% |
| 4 | Market index closes at 2,840 |
(2 rows)
Example: Nepali words, and the same number in Devanagari digits (sample data: content-news)
SELECT id, title FROM news WHERE MATCH(title, body, 'नाफा') ORDER BY id;
SELECT id FROM news WHERE MATCH(title, body, '१२.५') ORDER BY id;
Result:
SELECT id, title FROM news WHERE match(title, body, 'नाफा') ORDER BY id
| id (LONG) | title (STRING) |
|---|---|
| 2 | सिटी बैंकको नाफा बढ्यो |
(1 row)
SELECT id FROM news WHERE match(title, body, '१२.५') ORDER BY id
| id (LONG) |
|---|
| 1 |
| 2 |
(2 rows)
Example: Search together with other conditions (sample data: content-news)
SELECT id, symbol, title
FROM news
WHERE MATCH(title, body, 'bank') AND published_at >= '2026-09-01'
ORDER BY id;
Result:
| id (LONG) | symbol (STRING) | title (STRING) |
|---|---|---|
| 4 | NULL | Market index closes at 2,840 |
| 5 | COAST | Coastal Bank merger talks |
(2 rows)
Example: match on plain text values (sample data: content-news)
SELECT match('City Bank profit rises', 'BANK') AS a,
match('City Bank profit rises', 'banks') AS b,
match('City Bank profit rises', 'bank*') AS c,
match('City Bank', NULL) AS d;
Result:
| a (BOOL) | b (BOOL) | c (BOOL) | d (STRING) |
|---|---|---|---|
| true | false | true | NULL |
(1 row)
Example: The search text is missing (sample data: content-news; shows an error)
SELECT id FROM news WHERE MATCH(title);
Result:
Error: line 1, column 27: function 'match' takes 2 to 64 argument(s), got 1
How the index is used
Syntax:
use index name (full-text search)
use index other (...) (r row(s), the text match is checked on them)
With a FULLTEXT index on the same columns as match, KDB always uses it. It never goes back to reading the text of every row, even for a word that is in most rows.
| Query | What KDB does |
|---|---|
WHERE MATCH(...) | finds the rows in the index, then reads only those rows |
SELECT count(*) ... WHERE MATCH(...) and nothing else in WHERE | counts in the index; no row is read |
a phrase, "hydro power" | answered from the word positions in the index |
ORDER BY search_rank(...) DESC LIMIT n | takes the n best rows from the index (see search_rank) |
ORDER BY id LIMIT n, or no ORDER BY, on a table with id as its PRIMARY KEY | reads only the first n matching rows |
MATCH(...) AND symbol = 'COAST', with an index on symbol too | uses the index that finds fewer rows; the other condition is checked on those rows only |
MATCH(...) OR MATCH(...) | no index: every row is read |
- Notes:
- The
ORDER BY ... LIMITshortcut is for relational tables only, whenORDER BYis the first key columns, ascending, and those columns are numbers or times that cannot beNULL. On a time-series table the matching rows are sorted after they are found. EXPLAINshows the index it picks, but not the count or theLIMITshortcut: those are decided while the query runs.
- The
Example: A symbol finds fewer rows than the word: its index goes first (sample data: content-news)
Setup:
CREATE FULLTEXT INDEX news_text ON news (title, body);
CREATE INDEX news_symbol ON news (symbol);
EXPLAIN SELECT id FROM news WHERE MATCH(title, body, 'bank');
EXPLAIN SELECT id FROM news WHERE MATCH(title, body, 'bank') AND symbol = 'COAST';
SELECT id, title FROM news WHERE MATCH(title, body, 'bank') AND symbol = 'COAST';
Result:
EXPLAIN SELECT id FROM news WHERE match(title, body, 'bank')
| plan (STRING) |
|---|
| scan table news (1 of 1 partitions, about 5 rows) |
| use index news_text (full-text search) |
| build 1 of 5 stored column(s) per row: id |
| engine: column |
(4 rows)
EXPLAIN SELECT id FROM news WHERE match(title, body, 'bank') AND symbol = 'COAST'
| plan (STRING) |
|---|
| scan table news (1 of 1 partitions, about 5 rows) |
| use index news_symbol (symbol = value) (1 row(s), the text match is checked on them) |
| build 4 of 5 stored column(s) per row: id, symbol, title, body |
| filter while scanning: 2 condition(s) |
| engine: row (a condition the column engine does not do) |
(5 rows)
SELECT id, title FROM news WHERE match(title, body, 'bank') AND symbol = 'COAST'
| id (LONG) | title (STRING) |
|---|---|
| 5 | Coastal Bank merger talks |
(1 row)
Example: Count the articles with a phrase: from the index (sample data: content-news)
Setup:
CREATE FULLTEXT INDEX news_text ON news (title, body);
SELECT count(*) AS articles FROM news WHERE MATCH(title, body, '"city bank"');
Result:
| articles (LONG) |
|---|
| 2 |
(1 row)
Segments on disk
Splitting text into words is the slow part of building a full-text index. Since 2026-10-02 KDB saves the words of each partition in a file, a segment, so the next open builds the index from those files and reads no row.
| When | What happens |
|---|---|
| the database closes, or checkpoints while it is idle | a segment is written for each partition whose files changed since its last segment |
| the index is built (at open, or by the first query) | a partition with a segment that fits takes its words from it; the others are read and split into words as before |
| a segment is damaged | it is ignored, the server log says warning: damaged full-text segment ..., and that partition is read; the next close writes it again |
| the partition changed after its segment was written | the segment is ignored (it names the exact files it was made for) |
DROP INDEX, DROP TABLE, a partition dropped | its segments go to _trash at the next close or idle checkpoint |
| a partition only grows (new rows after its last one, tail files) | only the new rows are written, in a small extra segment next to the old one (see below) |
- Notes:
- The segments are in the folder
fts/of the database. They are a copy of what the rows already say: deleting the folder only makes the next build slower. Backups do not copy them. - JSON (
USING GIN) indexes get segments too. - An encrypted database encrypts its segments like every other file.
kdb checkreads every segment and prints aWARNINGfor a damaged one. It does not count as damage: KDB never uses a damaged segment.GET /api/statscounts them undersearch:segments_written,segment_extensions,segments_merged,segments_loaded,segment_rows_loaded,segments_damaged,segments_stale, androws_indexed(rows a build still split into words). After a clean restartrows_indexedstays at 0.- An older KDB ignores the folder and opens the database as before.
kdb downgradeto a KDB from before 2026-10-02 moves the segments to_trash.
- The segments are in the folder
Growing partitions. A table that gets new rows all day (a news feed, a log) would write the segment of today's partition again at every quiet moment, for a few new rows. Instead, KDB writes only the new rows in an extra segment (fts/<name>.1.kfs, .2.kfs, ...) that names the segment before it. Small extra segments are merged into one as they pile up, and into a new whole segment when they reach half of it, so a partition never has more than 5 files. Old ones go to _trash.
| What | Why it is safe |
|---|---|
| a power cut while segments are written or merged | each file is written whole and then renamed; the next open uses the old segments or the new one, and when they do not fit together it reads that partition's rows |
a partition rewritten whole, or changed by UPDATE / DELETE | its next segment is a new whole one; the extra ones go to _trash |
/api/stats search also shows segment_extensions (extra segments written) and segments_merged (older segments a merge replaced). The result is faster restarts: after a clean shutdown no row is read to build a full-text index, even on a table that grows all day.
search_rank
Syntax:
search_rank(column [, column ...], 'words')
A number that says how well a row matches: higher is better, 0 when the row does not match. It uses the BM25 formula (the formula of most search engines):
- a word that is rare in the table counts more than a common word,
- a word that is there many times counts more,
- a long text counts a little less.
| Argument | Type | Meaning |
|---|---|---|
column | STRING, SYMBOL or JSON | the text; use the same columns as in match |
words | STRING | the search text; use the same words as in match |
- Returns:
DOUBLE,0or more. - NULL:
NULLsearch text givesNULL. - Notes:
- The number depends on all rows of the table (how rare each word is). Compare ranks only inside one query. When rows change, the rank of the same row can change.
- Words with
-do not add to the rank. - The fastest form:
WHERE MATCH(cols, 'words') ORDER BY search_rank(cols, 'words') DESC LIMIT n, with the same columns and the same words, and a FULLTEXT index on those columns. KDB takes thenbest rows straight from the index. EXPLAIN then saysbest n row(s) by search_rank, top-k by rank (WAND). - Only the best rows are ranked: in that form KDB keeps the
nbest rows found so far. A row, or a whole block of 128 rows of a word, that cannot rank above then-th best even with the most mentions of each word is skipped without working out its rank. The answer is the same as ranking every match. Conditions on other columns (AND category = 3) are fine: when they turn rows down, KDB looks for more.searchinGET /api/statscounts the ranks worked out (bm25_evaluations), the top-n searches (top_k_searches) and what they skipped (skipped_blocks,skipped_docs). - Without a FULLTEXT index on the same columns,
search_rankreads the whole table once per query. - On values that are not columns of a table (
search_rank('bank bank', 'bank')), the value is ranked alone, as if it were the only row.
Example: The best matches first (sample data: content-news)
Setup:
CREATE FULLTEXT INDEX news_text ON news (title, body);
SELECT id, title, round(search_rank(title, body, 'bank'), 3) AS rank
FROM news
WHERE MATCH(title, body, 'bank')
ORDER BY rank DESC;
Result:
| id (LONG) | title (STRING) | rank (DOUBLE) |
|---|---|---|
| 5 | Coastal Bank merger talks | 0.845 |
| 1 | City Bank profit rises 18% | 0.816 |
| 4 | Market index closes at 2,840 | 0.562 |
(3 rows)
Example: Rows that do not match get 0 (sample data: content-news)
SELECT id, round(search_rank(title, body, 'city bank'), 3) AS rank FROM news ORDER BY id;
Result:
| id (LONG) | rank (DOUBLE) |
|---|---|
| 1 | 1.961 |
| 2 | 0 |
| 3 | 0 |
| 4 | 1.476 |
| 5 | 0 |
(5 rows)
Example: The fast top-20 form: EXPLAIN says 'best 20 row(s) by search_rank' (sample data: content-news)
Setup:
CREATE FULLTEXT INDEX news_text ON news (title, body);
EXPLAIN SELECT id, title FROM news
WHERE MATCH(title, body, 'bank')
ORDER BY search_rank(title, body, 'bank') DESC
LIMIT 20;
Result:
| plan (STRING) |
|---|
| scan table news (1 of 1 partitions, about 5 rows) |
| use index news_text (full-text search, best 20 row(s) by search_rank, top-k by rank (WAND)) |
| build 3 of 5 stored column(s) per row: id, title, body |
| filter while scanning: 1 condition(s) |
| sort by 1 key(s) |
| limit 20 |
| engine: row (a condition the column engine does not do) |
(7 rows)
Example: Forum: the best posts for 'bonus' (sample data: content-forum)
Setup:
CREATE FULLTEXT INDEX posts_body ON posts (body);
SELECT id, author, body, round(search_rank(body, 'bonus'), 3) AS rank
FROM posts
WHERE MATCH(body, 'bonus')
ORDER BY search_rank(body, 'bonus') DESC
LIMIT 5;
Result:
| id (LONG) | author (STRING) | body (STRING) | rank (DOUBLE) |
|---|---|---|---|
| 4 | gita | What about the bonus shares? | 1.319 |
| 5 | ram | Bonus is 10% this time. | 1.319 |
(2 rows)
highlight
Syntax:
highlight(column [, column ...], 'words')
Returns the text with every found word inside <b> and </b>, for a search result page.
| Argument | Type | Meaning |
|---|---|---|
column | STRING, SYMBOL or JSON | the text to show; several columns are joined with one space into one text |
words | STRING | the search text (the same as in match) |
- Returns:
STRING. - NULL:
NULLsearch text givesNULL. ANULLcolumn is left out; when every column isNULLthe result isNULL. - Notes:
highlightdoes not check that the row matches. A row may match because of itsbody, while you highlight only itstitle(the title then has no<b>).- Each word of a phrase is marked alone, wherever it is. A prefix (
bank*) marks the whole word (<b>Banks</b>). Words with-are not marked. - Web pages:
highlightchanges no other character. When users can type<or&(in a forum they can), escape the text first. See the last example.
Example: Mark the search word in the titles (sample data: content-news)
SELECT id, highlight(title, 'bank') AS title
FROM news
WHERE MATCH(title, body, 'bank')
ORDER BY id;
Result:
| id (LONG) | title (STRING) |
|---|---|
| 1 | City Bank profit rises 18% |
| 4 | Market index closes at 2,840 |
| 5 | Coastal Bank merger talks |
(3 rows)
Example: Nepali text, a phrase and a prefix (sample data: content-news)
SELECT id, highlight(body, 'नाफा १२.५') AS body FROM news WHERE id = 2;
SELECT id, highlight(title, body, '"net profit" bank*') AS text FROM news WHERE id = 1;
Result:
SELECT id, highlight(body, 'नाफा १२.५') AS body FROM news WHERE id = 2
| id (LONG) | body (STRING) |
|---|---|
| 2 | सिटी बैंकले चौथो त्रैमासिकमा ३ अर्ब २७ करोड नाफा कमाएको छ। लाभांश १२.५ प्रतिशत। |
(1 row)
SELECT id, highlight(title, body, '"net profit" bank*') AS text FROM news WHERE id = 1
| id (LONG) | text (STRING) |
|---|---|
| 1 | City Bank profit rises 18% City Bank reported a net profit of $3.27 billion. The bank will pay a 12.5% dividend. |
(1 row)
Example: Forum: escape what users typed, then highlight (safe for a web page) (sample data: content-forum)
Setup:
INSERT INTO posts VALUES (9, 2, 7, 'ram', 'Is <b>ACME</b> a hydro IPO & a good buy?', '2026-09-02T09:30:00+05:45');
SELECT id,
highlight(replace(replace(replace(body, '&', '&'), '<', '<'), '>', '>'), 'hydro ipo') AS safe_html
FROM posts
WHERE MATCH(body, 'hydro ipo')
ORDER BY id;
Result:
| id (LONG) | safe_html (STRING) |
|---|---|
| 9 | Is <b>ACME</b> a hydro IPO & a good buy? |
(1 row)
In the last example the <b> that the user typed became <b>, so only the search words are bold. (A search for the words amp, lt or gt would then also mark letters inside &, < and >.)
Search text
Syntax:
word word every word must be there, anywhere, in any order
"word word" these words next to each other, in this order
word* a word that starts with these letters
-word this word must not be there
-"word word" this phrase must not be there
How KDB cuts text (and the search text) into words:
| Rule | Example |
|---|---|
| Letters and digits of any language make words | सिटी बैंकको नाफा is 3 words |
| English letters A-Z: case does not matter; other letters must be the same | Bank = bank; Café is not CAFÉ |
Everything else separates words: spaces, punctuation, ।, ॥, emoji | छ। is the word छ; ram@example.com is ram, example, com |
| A number keeps its dot; a comma inside a number is dropped | 12.5 is one word; 2,840 is 2840 |
| Devanagari digits are the same as 0-9 | १२.५ = 12.5 |
| Numbers are words, not values | 1234.50 is not 1234.5 |
| No stemming | bank does not find banks; बैंक does not find बैंकले (use बैंक*); an English index stems, see Language |
- Notes:
- There is no
OR, noANDword, no brackets, no fuzzy search, no synonyms, and no stop words unless the index is English.bank OR mergerlooks for three words, and one of them isor. - A phrase does not cross from one column to the next: the end of
titleand the start ofbodyare not "next to each other". - Words joined by punctuation in the search text work like a phrase:
city-bankis the same as"city bank". - A prefix works on one word only:
net-prof*and"net prof*"do not findnet profit. - At least one word must be without
-.-mergeralone finds nothing.
- There is no
Example: A phrase, a prefix, and a word that must not be there (sample data: content-news)
SELECT id FROM news WHERE MATCH(title, body, '"net profit"') ORDER BY id;
SELECT id FROM news WHERE MATCH(title, body, 'hydro*') ORDER BY id;
SELECT id FROM news WHERE MATCH(title, body, 'bank -merger') ORDER BY id;
Result:
SELECT id FROM news WHERE match(title, body, '"net profit"') ORDER BY id
| id (LONG) |
|---|
| 1 |
(1 row)
SELECT id FROM news WHERE match(title, body, 'hydro*') ORDER BY id
| id (LONG) |
|---|
| 3 |
| 4 |
(2 rows)
SELECT id FROM news WHERE match(title, body, 'bank -merger') ORDER BY id
| id (LONG) |
|---|
| 1 |
| 4 |
(2 rows)
Example: Nepali: a word ending needs a prefix search (sample data: content-news)
SELECT id FROM news WHERE MATCH(title, body, 'बैंक') ORDER BY id;
SELECT id FROM news WHERE MATCH(title, body, 'बैंक*') ORDER BY id;
Result:
SELECT id FROM news WHERE match(title, body, 'बैंक') ORDER BY id
| id (LONG) |
|---|
(0 rows)
SELECT id FROM news WHERE match(title, body, 'बैंक*') ORDER BY id
| id (LONG) |
|---|
| 2 |
(1 row)
Example: Numbers: 2,840 and 2840 are the same word (sample data: content-news)
SELECT id, title FROM news WHERE MATCH(title, body, '2840');
SELECT id, title FROM news WHERE MATCH(title, body, '2,840');
Result:
SELECT id, title FROM news WHERE match(title, body, '2840')
| id (LONG) | title (STRING) |
|---|---|
| 4 | Market index closes at 2,840 |
(1 row)
SELECT id, title FROM news WHERE match(title, body, '2,840')
| id (LONG) | title (STRING) |
|---|---|
| 4 | Market index closes at 2,840 |
(1 row)
Example: OR is not an operator, and a search needs a word to find (sample data: content-news)
SELECT id FROM news WHERE MATCH(title, body, 'bank OR merger') ORDER BY id;
SELECT id FROM news WHERE MATCH(title, body, '-merger') ORDER BY id;
SELECT id FROM news WHERE MATCH(title, body, 'bank') OR MATCH(title, body, 'hydropower') ORDER BY id;
Result:
SELECT id FROM news WHERE match(title, body, 'bank OR merger') ORDER BY id
| id (LONG) |
|---|
(0 rows)
SELECT id FROM news WHERE match(title, body, '-merger') ORDER BY id
| id (LONG) |
|---|
(0 rows)
SELECT id FROM news WHERE match(title, body, 'bank') OR match(title, body, 'hydropower') ORDER BY id
| id (LONG) |
|---|
| 1 |
| 3 |
| 4 |
| 5 |
(4 rows)