[{"data":1,"prerenderedAt":3725},["ShallowReactive",2],{"page-\u002Fsql\u002F20-full-text-search":3},{"id":4,"title":5,"body":6,"description":3718,"extension":3719,"meta":3720,"navigation":213,"path":3721,"seo":3722,"stem":3723,"__hash__":3724},"content\u002Fsql\u002F20-full-text-search.md","20 — Full-Text Search",{"type":7,"value":8,"toc":3694},"minimark",[9,13,30,33,38,44,86,97,99,103,140,351,367,369,376,519,524,526,530,540,736,745,750,900,902,906,909,1961,1967,2016,2018,2022,2071,2145,2147,2151,2357,2362,2364,2368,2372,2446,2450,2533,2537,2571,2573,2577,2718,2727,2729,2733,2736,2939,2946,2948,2952,3045,3047,3051,3166,3168,3172,3352,3354,3358,3361,3434,3661,3663,3666,3690],[10,11,5],"h1",{"id":12},"_20-full-text-search",[14,15,16,17,21,22,25,26,29],"p",{},"Full-text search (FTS) finds documents matching a query, ranked by relevance — far more powerful than ",[18,19,20],"code",{},"LIKE"," for natural-language search. PostgreSQL has a built-in FTS engine via ",[18,23,24],{},"tsvector",", ",[18,27,28],{},"tsquery",", and GIN indexes.",[31,32],"hr",{},[34,35,37],"h2",{"id":36},"why-not-like","Why Not LIKE?",[14,39,40,43],{},[18,41,42],{},"LIKE '%word%'"," has three fundamental problems:",[45,46,47,55,80],"ol",{},[48,49,50,54],"li",{},[51,52,53],"strong",{},"Slow"," — leading wildcards defeat B-tree indexes → full table scan on every query.",[48,56,57,60,61,64,65,68,69,72,73,64,76,79],{},[51,58,59],{},"No linguistic awareness"," — ",[18,62,63],{},"'running'"," doesn't match ",[18,66,67],{},"'run'"," or ",[18,70,71],{},"'ran'","; ",[18,74,75],{},"'database'",[18,77,78],{},"'databases'",".",[48,81,82,85],{},[51,83,84],{},"No relevance ranking"," — you get matches, but no \"best match first.\"",[14,87,88,89,92,93,96],{},"FTS solves all three: it's indexed (GIN inverted index), it stems words (",[18,90,91],{},"running"," → ",[18,94,95],{},"run","), and it ranks results by relevance.",[31,98],{},[34,100,102],{"id":101},"tsvector-and-tsquery","tsvector and tsquery",[104,105,106,113],"ul",{},[48,107,108,112],{},[51,109,110],{},[18,111,24],{}," — a document preprocessed into a sorted list of distinct words (lexemes), each with positions. This is the searchable form.",[48,114,115,119,120,123,124,127,128,131,132,135,136,139],{},[51,116,117],{},[18,118,28],{}," — a query of lexemes combined with ",[18,121,122],{},"&"," (AND), ",[18,125,126],{},"|"," (OR), ",[18,129,130],{},"!"," (NOT), ",[18,133,134],{},"\u003C->"," (followed by), and ",[18,137,138],{},"\u003CN>"," (within N positions).",[141,142,144],"code-wrapper",{"language":143},"sql",[145,146,150],"pre",{"className":147,"code":148,"language":143,"meta":149,"style":149},"language-sql shiki shiki-themes github-light github-dark","-- Convert text to a tsvector: lowercase, remove stop words, stem\nSELECT to_tsvector('english', 'The quick brown fox jumps over the lazy dog');\n--  'brown':3 'dog':9 'fox':4 'jump':5 'lazi':8 'quick':2\n--  Stop words removed: 'the', 'over'\n--  Stemmed: 'jumps' → 'jump', 'lazy' → 'lazi'\n--  Numbers are positions (word order in the original text)\n\n-- Convert a query string to a tsquery (boolean operators: & | !)\nSELECT to_tsquery('english', 'quick & fox');\n--  'quick' & 'fox'\n\n-- Phrase search: words in order, adjacent (phraseto_tsquery)\nSELECT phraseto_tsquery('english', 'quick brown fox');\n--  'quick' \u003C-> 'brown' \u003C-> 'fox'\n\n-- Proximity: within N positions\nSELECT to_tsquery('english', 'quick \u003C3> fox');\n--  'quick' \u003C3> 'fox'   (quick and fox within 3 positions of each other)\n\n-- User-friendly query syntax (websearch_to_tsquery)\nSELECT websearch_to_tsquery('english', '\"full text\" -mysql postgres');\n--  'full' \u003C-> 'text' & !'mysql' & 'postgres'\n--  Supports: quotes for phrases, - for NOT, OR for disjunction\n","",[18,151,152,161,184,190,196,202,208,215,221,238,244,249,255,272,278,283,289,305,311,316,322,339,345],{"__ignoreMap":149},[153,154,157],"span",{"class":155,"line":156},"line",1,[153,158,160],{"class":159},"sdCPZ","-- Convert text to a tsvector: lowercase, remove stop words, stem\n",[153,162,164,168,172,176,178,181],{"class":155,"line":163},2,[153,165,167],{"class":166},"svdQ7","SELECT",[153,169,171],{"class":170},"ssxIu"," to_tsvector(",[153,173,175],{"class":174},"sJ6F3","'english'",[153,177,25],{"class":170},[153,179,180],{"class":174},"'The quick brown fox jumps over the lazy dog'",[153,182,183],{"class":170},");\n",[153,185,187],{"class":155,"line":186},3,[153,188,189],{"class":159},"--  'brown':3 'dog':9 'fox':4 'jump':5 'lazi':8 'quick':2\n",[153,191,193],{"class":155,"line":192},4,[153,194,195],{"class":159},"--  Stop words removed: 'the', 'over'\n",[153,197,199],{"class":155,"line":198},5,[153,200,201],{"class":159},"--  Stemmed: 'jumps' → 'jump', 'lazy' → 'lazi'\n",[153,203,205],{"class":155,"line":204},6,[153,206,207],{"class":159},"--  Numbers are positions (word order in the original text)\n",[153,209,211],{"class":155,"line":210},7,[153,212,214],{"emptyLinePlaceholder":213},true,"\n",[153,216,218],{"class":155,"line":217},8,[153,219,220],{"class":159},"-- Convert a query string to a tsquery (boolean operators: & | !)\n",[153,222,224,226,229,231,233,236],{"class":155,"line":223},9,[153,225,167],{"class":166},[153,227,228],{"class":170}," to_tsquery(",[153,230,175],{"class":174},[153,232,25],{"class":170},[153,234,235],{"class":174},"'quick & fox'",[153,237,183],{"class":170},[153,239,241],{"class":155,"line":240},10,[153,242,243],{"class":159},"--  'quick' & 'fox'\n",[153,245,247],{"class":155,"line":246},11,[153,248,214],{"emptyLinePlaceholder":213},[153,250,252],{"class":155,"line":251},12,[153,253,254],{"class":159},"-- Phrase search: words in order, adjacent (phraseto_tsquery)\n",[153,256,258,260,263,265,267,270],{"class":155,"line":257},13,[153,259,167],{"class":166},[153,261,262],{"class":170}," phraseto_tsquery(",[153,264,175],{"class":174},[153,266,25],{"class":170},[153,268,269],{"class":174},"'quick brown fox'",[153,271,183],{"class":170},[153,273,275],{"class":155,"line":274},14,[153,276,277],{"class":159},"--  'quick' \u003C-> 'brown' \u003C-> 'fox'\n",[153,279,281],{"class":155,"line":280},15,[153,282,214],{"emptyLinePlaceholder":213},[153,284,286],{"class":155,"line":285},16,[153,287,288],{"class":159},"-- Proximity: within N positions\n",[153,290,292,294,296,298,300,303],{"class":155,"line":291},17,[153,293,167],{"class":166},[153,295,228],{"class":170},[153,297,175],{"class":174},[153,299,25],{"class":170},[153,301,302],{"class":174},"'quick \u003C3> fox'",[153,304,183],{"class":170},[153,306,308],{"class":155,"line":307},18,[153,309,310],{"class":159},"--  'quick' \u003C3> 'fox'   (quick and fox within 3 positions of each other)\n",[153,312,314],{"class":155,"line":313},19,[153,315,214],{"emptyLinePlaceholder":213},[153,317,319],{"class":155,"line":318},20,[153,320,321],{"class":159},"-- User-friendly query syntax (websearch_to_tsquery)\n",[153,323,325,327,330,332,334,337],{"class":155,"line":324},21,[153,326,167],{"class":166},[153,328,329],{"class":170}," websearch_to_tsquery(",[153,331,175],{"class":174},[153,333,25],{"class":170},[153,335,336],{"class":174},"'\"full text\" -mysql postgres'",[153,338,183],{"class":170},[153,340,342],{"class":155,"line":341},22,[153,343,344],{"class":159},"--  'full' \u003C-> 'text' & !'mysql' & 'postgres'\n",[153,346,348],{"class":155,"line":347},23,[153,349,350],{"class":159},"--  Supports: quotes for phrases, - for NOT, OR for disjunction\n",[14,352,353,354,25,356,25,359,362,363,366],{},"The text search configuration (",[18,355,175],{},[18,357,358],{},"'spanish'",[18,360,361],{},"'german'",", etc.) controls stemming and stop words. Use ",[18,364,365],{},"'simple'"," for no stemming\u002Fstop words (exact word matching — useful for codes, identifiers).",[31,368],{},[34,370,372,373],{"id":371},"the-match-operator","The Match Operator: ",[18,374,375],{},"@@",[141,377,378],{"language":143},[145,379,381],{"className":147,"code":380,"language":143,"meta":149,"style":149},"-- Does the document match the query?\nSELECT to_tsvector('english', 'The quick brown fox') @@ to_tsquery('english', 'quick & fox');\n--  true   (both 'quick' and 'fox' are in the document)\n\nSELECT to_tsvector('english', 'The quick brown fox') @@ to_tsquery('english', 'quick & cat');\n--  false  ('cat' is not in the document)\n\n-- Phrase match: words must be adjacent and in order\nSELECT to_tsvector('english', 'The quick brown fox') @@ phraseto_tsquery('english', 'quick brown');\n--  true\n\nSELECT to_tsvector('english', 'The brown quick fox') @@ phraseto_tsquery('english', 'quick brown');\n--  false  (words are present but not in the right order\u002Fadjacency)\n",[18,382,383,388,412,417,421,444,449,453,458,482,487,491,514],{"__ignoreMap":149},[153,384,385],{"class":155,"line":156},[153,386,387],{"class":159},"-- Does the document match the query?\n",[153,389,390,392,394,396,398,401,404,406,408,410],{"class":155,"line":163},[153,391,167],{"class":166},[153,393,171],{"class":170},[153,395,175],{"class":174},[153,397,25],{"class":170},[153,399,400],{"class":174},"'The quick brown fox'",[153,402,403],{"class":170},") @@ to_tsquery(",[153,405,175],{"class":174},[153,407,25],{"class":170},[153,409,235],{"class":174},[153,411,183],{"class":170},[153,413,414],{"class":155,"line":186},[153,415,416],{"class":159},"--  true   (both 'quick' and 'fox' are in the document)\n",[153,418,419],{"class":155,"line":192},[153,420,214],{"emptyLinePlaceholder":213},[153,422,423,425,427,429,431,433,435,437,439,442],{"class":155,"line":198},[153,424,167],{"class":166},[153,426,171],{"class":170},[153,428,175],{"class":174},[153,430,25],{"class":170},[153,432,400],{"class":174},[153,434,403],{"class":170},[153,436,175],{"class":174},[153,438,25],{"class":170},[153,440,441],{"class":174},"'quick & cat'",[153,443,183],{"class":170},[153,445,446],{"class":155,"line":204},[153,447,448],{"class":159},"--  false  ('cat' is not in the document)\n",[153,450,451],{"class":155,"line":210},[153,452,214],{"emptyLinePlaceholder":213},[153,454,455],{"class":155,"line":217},[153,456,457],{"class":159},"-- Phrase match: words must be adjacent and in order\n",[153,459,460,462,464,466,468,470,473,475,477,480],{"class":155,"line":223},[153,461,167],{"class":166},[153,463,171],{"class":170},[153,465,175],{"class":174},[153,467,25],{"class":170},[153,469,400],{"class":174},[153,471,472],{"class":170},") @@ phraseto_tsquery(",[153,474,175],{"class":174},[153,476,25],{"class":170},[153,478,479],{"class":174},"'quick brown'",[153,481,183],{"class":170},[153,483,484],{"class":155,"line":240},[153,485,486],{"class":159},"--  true\n",[153,488,489],{"class":155,"line":246},[153,490,214],{"emptyLinePlaceholder":213},[153,492,493,495,497,499,501,504,506,508,510,512],{"class":155,"line":251},[153,494,167],{"class":166},[153,496,171],{"class":170},[153,498,175],{"class":174},[153,500,25],{"class":170},[153,502,503],{"class":174},"'The brown quick fox'",[153,505,472],{"class":170},[153,507,175],{"class":174},[153,509,25],{"class":170},[153,511,479],{"class":174},[153,513,183],{"class":170},[153,515,516],{"class":155,"line":257},[153,517,518],{"class":159},"--  false  (words are present but not in the right order\u002Fadjacency)\n",[14,520,521,523],{},[18,522,375],{}," returns true if the tsvector contains all the tsquery's lexemes with the specified boolean\u002Fphrase structure.",[31,525],{},[34,527,529],{"id":528},"storing-and-indexing-generated-column-gin","Storing and Indexing: Generated Column + GIN",[14,531,532,533,535,536,539],{},"Store the ",[18,534,24],{}," in a ",[51,537,538],{},"generated column"," so it's always in sync with the source text, and index it with GIN:",[141,541,542],{"language":143},[145,543,545],{"className":147,"code":544,"language":143,"meta":149,"style":149},"CREATE TABLE articles (\n  id BIGSERIAL PRIMARY KEY,\n  title TEXT NOT NULL,\n  body TEXT NOT NULL,\n\n  -- Generated tsvector: combines title (weight A) and body (weight B)\n  -- STORED: the column is physically stored (so it can be indexed)\n  -- Always in sync with title\u002Fbody — no trigger needed\n  search_vec TSVECTOR GENERATED ALWAYS AS (\n    setweight(to_tsvector('english', coalesce(title, '')), 'A') ||  -- weight A = highest priority\n    setweight(to_tsvector('english', coalesce(body, '')),   'B')    -- weight B = lower priority\n  ) STORED\n);\n\n-- GIN index on the generated tsvector: makes @@ queries O(matches) not O(all docs)\nCREATE INDEX articles_search_vec_gin ON articles USING gin(search_vec);\n",[18,546,547,562,576,589,600,604,609,614,619,635,669,696,701,705,709,714],{"__ignoreMap":149},[153,548,549,552,555,559],{"class":155,"line":156},[153,550,551],{"class":166},"CREATE",[153,553,554],{"class":166}," TABLE",[153,556,558],{"class":557},"sIsaT"," articles",[153,560,561],{"class":170}," (\n",[153,563,564,567,570,573],{"class":155,"line":163},[153,565,566],{"class":170},"  id ",[153,568,569],{"class":166},"BIGSERIAL",[153,571,572],{"class":166}," PRIMARY KEY",[153,574,575],{"class":170},",\n",[153,577,578,581,584,587],{"class":155,"line":186},[153,579,580],{"class":170},"  title ",[153,582,583],{"class":166},"TEXT",[153,585,586],{"class":166}," NOT NULL",[153,588,575],{"class":170},[153,590,591,594,596,598],{"class":155,"line":192},[153,592,593],{"class":170},"  body ",[153,595,583],{"class":166},[153,597,586],{"class":166},[153,599,575],{"class":170},[153,601,602],{"class":155,"line":198},[153,603,214],{"emptyLinePlaceholder":213},[153,605,606],{"class":155,"line":204},[153,607,608],{"class":159},"  -- Generated tsvector: combines title (weight A) and body (weight B)\n",[153,610,611],{"class":155,"line":210},[153,612,613],{"class":159},"  -- STORED: the column is physically stored (so it can be indexed)\n",[153,615,616],{"class":155,"line":217},[153,617,618],{"class":159},"  -- Always in sync with title\u002Fbody — no trigger needed\n",[153,620,621,624,627,630,633],{"class":155,"line":223},[153,622,623],{"class":170},"  search_vec TSVECTOR ",[153,625,626],{"class":166},"GENERATED",[153,628,629],{"class":166}," ALWAYS",[153,631,632],{"class":166}," AS",[153,634,561],{"class":170},[153,636,637,640,642,644,648,651,654,657,660,663,666],{"class":155,"line":240},[153,638,639],{"class":170},"    setweight(to_tsvector(",[153,641,175],{"class":174},[153,643,25],{"class":170},[153,645,647],{"class":646},"snvgF","coalesce",[153,649,650],{"class":170},"(title, ",[153,652,653],{"class":174},"''",[153,655,656],{"class":170},")), ",[153,658,659],{"class":174},"'A'",[153,661,662],{"class":170},") ",[153,664,665],{"class":166},"||",[153,667,668],{"class":159},"  -- weight A = highest priority\n",[153,670,671,673,675,677,679,682,684,687,690,693],{"class":155,"line":246},[153,672,639],{"class":170},[153,674,175],{"class":174},[153,676,25],{"class":170},[153,678,647],{"class":646},[153,680,681],{"class":170},"(body, ",[153,683,653],{"class":174},[153,685,686],{"class":170},")),   ",[153,688,689],{"class":174},"'B'",[153,691,692],{"class":170},")    ",[153,694,695],{"class":159},"-- weight B = lower priority\n",[153,697,698],{"class":155,"line":251},[153,699,700],{"class":170},"  ) STORED\n",[153,702,703],{"class":155,"line":257},[153,704,183],{"class":170},[153,706,707],{"class":155,"line":274},[153,708,214],{"emptyLinePlaceholder":213},[153,710,711],{"class":155,"line":280},[153,712,713],{"class":159},"-- GIN index on the generated tsvector: makes @@ queries O(matches) not O(all docs)\n",[153,715,716,718,721,724,727,730,733],{"class":155,"line":285},[153,717,551],{"class":166},[153,719,720],{"class":166}," INDEX",[153,722,723],{"class":557}," articles_search_vec_gin",[153,725,726],{"class":166}," ON",[153,728,729],{"class":170}," articles ",[153,731,732],{"class":166},"USING",[153,734,735],{"class":170}," gin(search_vec);\n",[14,737,738,741,742,744],{},[18,739,740],{},"setweight"," assigns a weight (A > B > C > D) to each lexeme, so title matches rank higher than body matches. The ",[18,743,665],{}," concatenates the two weighted vectors. The generated column ensures the tsvector is always in sync — no triggers, no stale vectors.",[746,747,749],"h3",{"id":748},"why-generated-columns-not-triggers","Why Generated Columns (Not Triggers)?",[141,751,752],{"language":143},[145,753,755],{"className":147,"code":754,"language":143,"meta":149,"style":149},"-- ❌ Old way: regular column + trigger to keep it in sync\nCREATE TABLE articles_old (\n  id BIGSERIAL PRIMARY KEY,\n  title TEXT NOT NULL,\n  body TEXT NOT NULL,\n  search_vec TSVECTOR                          -- regular column, can go stale\n);\n\nCREATE TRIGGER articles_search_vec_update\n  BEFORE INSERT OR UPDATE ON articles_old\n  FOR EACH ROW EXECUTE FUNCTION\n  tsvector_update_trigger(search_vec, 'pg_catalog.english', title, body);\n-- Risk: direct updates bypassing the trigger, or trigger bugs → stale tsvector\n\n-- ✅ New way: generated column (PostgreSQL 12+)\n-- search_vec is ALWAYS correct — computed from title\u002Fbody on every read\u002Fwrite\n-- No trigger to forget, no stale vectors, no maintenance\n",[18,756,757,762,773,783,793,803,811,815,819,829,848,865,876,881,885,890,895],{"__ignoreMap":149},[153,758,759],{"class":155,"line":156},[153,760,761],{"class":159},"-- ❌ Old way: regular column + trigger to keep it in sync\n",[153,763,764,766,768,771],{"class":155,"line":163},[153,765,551],{"class":166},[153,767,554],{"class":166},[153,769,770],{"class":557}," articles_old",[153,772,561],{"class":170},[153,774,775,777,779,781],{"class":155,"line":186},[153,776,566],{"class":170},[153,778,569],{"class":166},[153,780,572],{"class":166},[153,782,575],{"class":170},[153,784,785,787,789,791],{"class":155,"line":192},[153,786,580],{"class":170},[153,788,583],{"class":166},[153,790,586],{"class":166},[153,792,575],{"class":170},[153,794,795,797,799,801],{"class":155,"line":198},[153,796,593],{"class":170},[153,798,583],{"class":166},[153,800,586],{"class":166},[153,802,575],{"class":170},[153,804,805,808],{"class":155,"line":204},[153,806,807],{"class":170},"  search_vec TSVECTOR                          ",[153,809,810],{"class":159},"-- regular column, can go stale\n",[153,812,813],{"class":155,"line":210},[153,814,183],{"class":170},[153,816,817],{"class":155,"line":217},[153,818,214],{"emptyLinePlaceholder":213},[153,820,821,823,826],{"class":155,"line":223},[153,822,551],{"class":166},[153,824,825],{"class":166}," TRIGGER",[153,827,828],{"class":557}," articles_search_vec_update\n",[153,830,831,834,837,840,843,845],{"class":155,"line":240},[153,832,833],{"class":166},"  BEFORE",[153,835,836],{"class":166}," INSERT",[153,838,839],{"class":166}," OR",[153,841,842],{"class":166}," UPDATE",[153,844,726],{"class":166},[153,846,847],{"class":170}," articles_old\n",[153,849,850,853,856,859,862],{"class":155,"line":246},[153,851,852],{"class":166},"  FOR",[153,854,855],{"class":170}," EACH ",[153,857,858],{"class":166},"ROW",[153,860,861],{"class":166}," EXECUTE",[153,863,864],{"class":166}," FUNCTION\n",[153,866,867,870,873],{"class":155,"line":251},[153,868,869],{"class":170},"  tsvector_update_trigger(search_vec, ",[153,871,872],{"class":174},"'pg_catalog.english'",[153,874,875],{"class":170},", title, body);\n",[153,877,878],{"class":155,"line":257},[153,879,880],{"class":159},"-- Risk: direct updates bypassing the trigger, or trigger bugs → stale tsvector\n",[153,882,883],{"class":155,"line":274},[153,884,214],{"emptyLinePlaceholder":213},[153,886,887],{"class":155,"line":280},[153,888,889],{"class":159},"-- ✅ New way: generated column (PostgreSQL 12+)\n",[153,891,892],{"class":155,"line":285},[153,893,894],{"class":159},"-- search_vec is ALWAYS correct — computed from title\u002Fbody on every read\u002Fwrite\n",[153,896,897],{"class":155,"line":291},[153,898,899],{"class":159},"-- No trigger to forget, no stale vectors, no maintenance\n",[31,901],{},[34,903,905],{"id":904},"complex-implementation-production-article-search","Complex Implementation: Production Article Search",[14,907,908],{},"A production search system with weighted multi-column search, GIN indexing, ranking, and headline display:",[141,910,911],{"language":143},[145,912,914],{"className":147,"code":913,"language":143,"meta":149,"style":149},"-- ============================================================================\n-- Schema: article search with weighted title\u002Fbody matching\n-- ============================================================================\nCREATE TABLE articles (\n  id BIGSERIAL PRIMARY KEY,\n  title TEXT NOT NULL,\n  body TEXT NOT NULL,\n  author TEXT NOT NULL,\n  published_at TIMESTAMPTZ NOT NULL DEFAULT now(),\n\n  -- Generated tsvector: title (weight A) + body (weight B) + author (weight C)\n  -- Title hits rank highest, then body hits, then author hits\n  search_vec TSVECTOR GENERATED ALWAYS AS (\n    setweight(to_tsvector('english', coalesce(title, '')),  'A') ||\n    setweight(to_tsvector('english', coalesce(body, '')),   'B') ||\n    setweight(to_tsvector('english', coalesce(author, '')), 'C')\n  ) STORED\n);\n\n-- GIN index: accelerates the @@ filter (the expensive part)\nCREATE INDEX articles_search_vec_gin ON articles USING gin(search_vec);\n\n-- B-tree index for secondary sort\u002Ffilter\nCREATE INDEX articles_published_at_idx ON articles(published_at);\n\n-- ============================================================================\n-- Sample data\n-- ============================================================================\nINSERT INTO articles (title, body, author) VALUES\n  ('PostgreSQL Full-Text Search Guide',\n   'PostgreSQL has a powerful full-text search engine using tsvector and tsquery. It supports stemming, stop words, and relevance ranking with GIN indexes.',\n   'Alice Chen'),\n  ('Introduction to Databases',\n   'A database is an organized collection of data. Relational databases use SQL for querying and managing data. PostgreSQL is a popular relational database.',\n   'Bob Smith'),\n  ('Advanced PostgreSQL Indexing',\n   'GIN indexes accelerate full-text search and JSONB containment queries. GIST indexes support geometric and range queries.',\n   'Alice Chen');\n\n-- ============================================================================\n-- Query 1: basic search with ranking\n-- ============================================================================\nSELECT\n  id,\n  title,\n  ts_rank(search_vec, query) AS rank      -- relevance score (higher = better)\nFROM articles, to_tsquery('english', 'postgres & search') query\nWHERE search_vec @@ query                   -- GIN index accelerates this filter\nORDER BY rank DESC, published_at DESC       -- rank first, then recency\nLIMIT 10;\n\n-- ============================================================================\n-- Query 2: weighted search — title matches rank higher than body matches\n-- ============================================================================\n-- The search_vec already has weights (A=title, B=body, C=author)\n-- ts_rank incorporates the weights automatically\nSELECT\n  id,\n  title,\n  ts_rank(search_vec, query) AS rank,\n  ts_rank_cd(search_vec, query) AS rank_cd  -- cover density ranking (different algorithm)\nFROM articles, to_tsquery('english', 'postgres') query\nWHERE search_vec @@ query\nORDER BY rank DESC\nLIMIT 10;\n\n-- ============================================================================\n-- Query 3: phrase search + headline display\n-- ============================================================================\nSELECT\n  id,\n  title,\n  ts_headline(\n    'english',\n    body,\n    phraseto_tsquery('english', 'full text search'),\n    'MaxWords=35, MinWords=15, ShortWord=3, MaxFragments=3'\n  ) AS snippet,                            -- highlighted snippet of the body\n  ts_rank(search_vec, phraseto_tsquery('english', 'full text search')) AS rank\nFROM articles\nWHERE search_vec @@ phraseto_tsquery('english', 'full text search')\nORDER BY rank DESC;\n\n-- ============================================================================\n-- Query 4: websearch_to_tsquery for user-friendly query syntax\n-- ============================================================================\n-- Accepts: \"phrase\" -exclude OR alternative\nSELECT\n  id,\n  title,\n  ts_rank(search_vec, query) AS rank\nFROM articles, websearch_to_tsquery('english', 'postgres -mysql OR database') query\nWHERE search_vec @@ query\nORDER BY rank DESC\nLIMIT 10;\n\n-- ============================================================================\n-- Query 5: prefix matching (typeahead search)\n-- ============================================================================\nSELECT\n  id,\n  title,\n  ts_rank(search_vec, query) AS rank\nFROM articles, to_tsquery('english', 'post:*') query   -- :* = prefix match\nWHERE search_vec @@ query                                -- matches 'postgres', 'posting', etc.\nORDER BY rank DESC\nLIMIT 10;\n\n-- ============================================================================\n-- Query 6: combined FTS + structured filters\n-- ============================================================================\nSELECT\n  id,\n  title,\n  ts_rank(search_vec, query) AS rank\nFROM articles, to_tsquery('english', 'postgres & index') query\nWHERE search_vec @@ query\n  AND published_at >= now() - INTERVAL '30 days'   -- time filter (uses published_at index)\n  AND author = 'Alice Chen'                         -- equality filter\nORDER BY rank DESC\nLIMIT 10;\n",[18,915,916,921,926,930,940,950,960,970,981,1000,1004,1009,1014,1026,1050,1072,1095,1099,1103,1107,1112,1128,1132,1137,1152,1157,1162,1168,1173,1185,1196,1204,1213,1223,1231,1239,1249,1257,1264,1269,1274,1280,1285,1291,1297,1303,1318,1337,1349,1369,1381,1386,1391,1397,1402,1408,1414,1419,1424,1429,1439,1453,1469,1477,1487,1496,1501,1506,1512,1517,1522,1527,1532,1538,1546,1552,1567,1573,1587,1607,1615,1631,1642,1647,1652,1658,1663,1669,1674,1679,1684,1693,1710,1717,1726,1735,1740,1745,1751,1756,1761,1766,1771,1780,1800,1811,1820,1829,1834,1839,1845,1850,1855,1860,1865,1874,1890,1897,1926,1943,1952],{"__ignoreMap":149},[153,917,918],{"class":155,"line":156},[153,919,920],{"class":159},"-- ============================================================================\n",[153,922,923],{"class":155,"line":163},[153,924,925],{"class":159},"-- Schema: article search with weighted title\u002Fbody matching\n",[153,927,928],{"class":155,"line":186},[153,929,920],{"class":159},[153,931,932,934,936,938],{"class":155,"line":192},[153,933,551],{"class":166},[153,935,554],{"class":166},[153,937,558],{"class":557},[153,939,561],{"class":170},[153,941,942,944,946,948],{"class":155,"line":198},[153,943,566],{"class":170},[153,945,569],{"class":166},[153,947,572],{"class":166},[153,949,575],{"class":170},[153,951,952,954,956,958],{"class":155,"line":204},[153,953,580],{"class":170},[153,955,583],{"class":166},[153,957,586],{"class":166},[153,959,575],{"class":170},[153,961,962,964,966,968],{"class":155,"line":210},[153,963,593],{"class":170},[153,965,583],{"class":166},[153,967,586],{"class":166},[153,969,575],{"class":170},[153,971,972,975,977,979],{"class":155,"line":217},[153,973,974],{"class":170},"  author ",[153,976,583],{"class":166},[153,978,586],{"class":166},[153,980,575],{"class":170},[153,982,983,986,989,991,994,997],{"class":155,"line":223},[153,984,985],{"class":170},"  published_at ",[153,987,988],{"class":166},"TIMESTAMPTZ",[153,990,586],{"class":166},[153,992,993],{"class":166}," DEFAULT",[153,995,996],{"class":166}," now",[153,998,999],{"class":170},"(),\n",[153,1001,1002],{"class":155,"line":240},[153,1003,214],{"emptyLinePlaceholder":213},[153,1005,1006],{"class":155,"line":246},[153,1007,1008],{"class":159},"  -- Generated tsvector: title (weight A) + body (weight B) + author (weight C)\n",[153,1010,1011],{"class":155,"line":251},[153,1012,1013],{"class":159},"  -- Title hits rank highest, then body hits, then author hits\n",[153,1015,1016,1018,1020,1022,1024],{"class":155,"line":257},[153,1017,623],{"class":170},[153,1019,626],{"class":166},[153,1021,629],{"class":166},[153,1023,632],{"class":166},[153,1025,561],{"class":170},[153,1027,1028,1030,1032,1034,1036,1038,1040,1043,1045,1047],{"class":155,"line":274},[153,1029,639],{"class":170},[153,1031,175],{"class":174},[153,1033,25],{"class":170},[153,1035,647],{"class":646},[153,1037,650],{"class":170},[153,1039,653],{"class":174},[153,1041,1042],{"class":170},")),  ",[153,1044,659],{"class":174},[153,1046,662],{"class":170},[153,1048,1049],{"class":166},"||\n",[153,1051,1052,1054,1056,1058,1060,1062,1064,1066,1068,1070],{"class":155,"line":280},[153,1053,639],{"class":170},[153,1055,175],{"class":174},[153,1057,25],{"class":170},[153,1059,647],{"class":646},[153,1061,681],{"class":170},[153,1063,653],{"class":174},[153,1065,686],{"class":170},[153,1067,689],{"class":174},[153,1069,662],{"class":170},[153,1071,1049],{"class":166},[153,1073,1074,1076,1078,1080,1082,1085,1087,1089,1092],{"class":155,"line":285},[153,1075,639],{"class":170},[153,1077,175],{"class":174},[153,1079,25],{"class":170},[153,1081,647],{"class":646},[153,1083,1084],{"class":170},"(author, ",[153,1086,653],{"class":174},[153,1088,656],{"class":170},[153,1090,1091],{"class":174},"'C'",[153,1093,1094],{"class":170},")\n",[153,1096,1097],{"class":155,"line":291},[153,1098,700],{"class":170},[153,1100,1101],{"class":155,"line":307},[153,1102,183],{"class":170},[153,1104,1105],{"class":155,"line":313},[153,1106,214],{"emptyLinePlaceholder":213},[153,1108,1109],{"class":155,"line":318},[153,1110,1111],{"class":159},"-- GIN index: accelerates the @@ filter (the expensive part)\n",[153,1113,1114,1116,1118,1120,1122,1124,1126],{"class":155,"line":324},[153,1115,551],{"class":166},[153,1117,720],{"class":166},[153,1119,723],{"class":557},[153,1121,726],{"class":166},[153,1123,729],{"class":170},[153,1125,732],{"class":166},[153,1127,735],{"class":170},[153,1129,1130],{"class":155,"line":341},[153,1131,214],{"emptyLinePlaceholder":213},[153,1133,1134],{"class":155,"line":347},[153,1135,1136],{"class":159},"-- B-tree index for secondary sort\u002Ffilter\n",[153,1138,1140,1142,1144,1147,1149],{"class":155,"line":1139},24,[153,1141,551],{"class":166},[153,1143,720],{"class":166},[153,1145,1146],{"class":557}," articles_published_at_idx",[153,1148,726],{"class":166},[153,1150,1151],{"class":170}," articles(published_at);\n",[153,1153,1155],{"class":155,"line":1154},25,[153,1156,214],{"emptyLinePlaceholder":213},[153,1158,1160],{"class":155,"line":1159},26,[153,1161,920],{"class":159},[153,1163,1165],{"class":155,"line":1164},27,[153,1166,1167],{"class":159},"-- Sample data\n",[153,1169,1171],{"class":155,"line":1170},28,[153,1172,920],{"class":159},[153,1174,1176,1179,1182],{"class":155,"line":1175},29,[153,1177,1178],{"class":166},"INSERT INTO",[153,1180,1181],{"class":170}," articles (title, body, author) ",[153,1183,1184],{"class":166},"VALUES\n",[153,1186,1188,1191,1194],{"class":155,"line":1187},30,[153,1189,1190],{"class":170},"  (",[153,1192,1193],{"class":174},"'PostgreSQL Full-Text Search Guide'",[153,1195,575],{"class":170},[153,1197,1199,1202],{"class":155,"line":1198},31,[153,1200,1201],{"class":174},"   'PostgreSQL has a powerful full-text search engine using tsvector and tsquery. It supports stemming, stop words, and relevance ranking with GIN indexes.'",[153,1203,575],{"class":170},[153,1205,1207,1210],{"class":155,"line":1206},32,[153,1208,1209],{"class":174},"   'Alice Chen'",[153,1211,1212],{"class":170},"),\n",[153,1214,1216,1218,1221],{"class":155,"line":1215},33,[153,1217,1190],{"class":170},[153,1219,1220],{"class":174},"'Introduction to Databases'",[153,1222,575],{"class":170},[153,1224,1226,1229],{"class":155,"line":1225},34,[153,1227,1228],{"class":174},"   'A database is an organized collection of data. Relational databases use SQL for querying and managing data. PostgreSQL is a popular relational database.'",[153,1230,575],{"class":170},[153,1232,1234,1237],{"class":155,"line":1233},35,[153,1235,1236],{"class":174},"   'Bob Smith'",[153,1238,1212],{"class":170},[153,1240,1242,1244,1247],{"class":155,"line":1241},36,[153,1243,1190],{"class":170},[153,1245,1246],{"class":174},"'Advanced PostgreSQL Indexing'",[153,1248,575],{"class":170},[153,1250,1252,1255],{"class":155,"line":1251},37,[153,1253,1254],{"class":174},"   'GIN indexes accelerate full-text search and JSONB containment queries. GIST indexes support geometric and range queries.'",[153,1256,575],{"class":170},[153,1258,1260,1262],{"class":155,"line":1259},38,[153,1261,1209],{"class":174},[153,1263,183],{"class":170},[153,1265,1267],{"class":155,"line":1266},39,[153,1268,214],{"emptyLinePlaceholder":213},[153,1270,1272],{"class":155,"line":1271},40,[153,1273,920],{"class":159},[153,1275,1277],{"class":155,"line":1276},41,[153,1278,1279],{"class":159},"-- Query 1: basic search with ranking\n",[153,1281,1283],{"class":155,"line":1282},42,[153,1284,920],{"class":159},[153,1286,1288],{"class":155,"line":1287},43,[153,1289,1290],{"class":166},"SELECT\n",[153,1292,1294],{"class":155,"line":1293},44,[153,1295,1296],{"class":170},"  id,\n",[153,1298,1300],{"class":155,"line":1299},45,[153,1301,1302],{"class":170},"  title,\n",[153,1304,1306,1309,1312,1315],{"class":155,"line":1305},46,[153,1307,1308],{"class":170},"  ts_rank(search_vec, query) ",[153,1310,1311],{"class":166},"AS",[153,1313,1314],{"class":170}," rank      ",[153,1316,1317],{"class":159},"-- relevance score (higher = better)\n",[153,1319,1321,1324,1327,1329,1331,1334],{"class":155,"line":1320},47,[153,1322,1323],{"class":166},"FROM",[153,1325,1326],{"class":170}," articles, to_tsquery(",[153,1328,175],{"class":174},[153,1330,25],{"class":170},[153,1332,1333],{"class":174},"'postgres & search'",[153,1335,1336],{"class":170},") query\n",[153,1338,1340,1343,1346],{"class":155,"line":1339},48,[153,1341,1342],{"class":166},"WHERE",[153,1344,1345],{"class":170}," search_vec @@ query                   ",[153,1347,1348],{"class":159},"-- GIN index accelerates this filter\n",[153,1350,1352,1355,1358,1361,1364,1366],{"class":155,"line":1351},49,[153,1353,1354],{"class":166},"ORDER BY",[153,1356,1357],{"class":170}," rank ",[153,1359,1360],{"class":166},"DESC",[153,1362,1363],{"class":170},", published_at ",[153,1365,1360],{"class":166},[153,1367,1368],{"class":159},"       -- rank first, then recency\n",[153,1370,1372,1375,1378],{"class":155,"line":1371},50,[153,1373,1374],{"class":166},"LIMIT",[153,1376,1377],{"class":646}," 10",[153,1379,1380],{"class":170},";\n",[153,1382,1384],{"class":155,"line":1383},51,[153,1385,214],{"emptyLinePlaceholder":213},[153,1387,1389],{"class":155,"line":1388},52,[153,1390,920],{"class":159},[153,1392,1394],{"class":155,"line":1393},53,[153,1395,1396],{"class":159},"-- Query 2: weighted search — title matches rank higher than body matches\n",[153,1398,1400],{"class":155,"line":1399},54,[153,1401,920],{"class":159},[153,1403,1405],{"class":155,"line":1404},55,[153,1406,1407],{"class":159},"-- The search_vec already has weights (A=title, B=body, C=author)\n",[153,1409,1411],{"class":155,"line":1410},56,[153,1412,1413],{"class":159},"-- ts_rank incorporates the weights automatically\n",[153,1415,1417],{"class":155,"line":1416},57,[153,1418,1290],{"class":166},[153,1420,1422],{"class":155,"line":1421},58,[153,1423,1296],{"class":170},[153,1425,1427],{"class":155,"line":1426},59,[153,1428,1302],{"class":170},[153,1430,1432,1434,1436],{"class":155,"line":1431},60,[153,1433,1308],{"class":170},[153,1435,1311],{"class":166},[153,1437,1438],{"class":170}," rank,\n",[153,1440,1442,1445,1447,1450],{"class":155,"line":1441},61,[153,1443,1444],{"class":170},"  ts_rank_cd(search_vec, query) ",[153,1446,1311],{"class":166},[153,1448,1449],{"class":170}," rank_cd  ",[153,1451,1452],{"class":159},"-- cover density ranking (different algorithm)\n",[153,1454,1456,1458,1460,1462,1464,1467],{"class":155,"line":1455},62,[153,1457,1323],{"class":166},[153,1459,1326],{"class":170},[153,1461,175],{"class":174},[153,1463,25],{"class":170},[153,1465,1466],{"class":174},"'postgres'",[153,1468,1336],{"class":170},[153,1470,1472,1474],{"class":155,"line":1471},63,[153,1473,1342],{"class":166},[153,1475,1476],{"class":170}," search_vec @@ query\n",[153,1478,1480,1482,1484],{"class":155,"line":1479},64,[153,1481,1354],{"class":166},[153,1483,1357],{"class":170},[153,1485,1486],{"class":166},"DESC\n",[153,1488,1490,1492,1494],{"class":155,"line":1489},65,[153,1491,1374],{"class":166},[153,1493,1377],{"class":646},[153,1495,1380],{"class":170},[153,1497,1499],{"class":155,"line":1498},66,[153,1500,214],{"emptyLinePlaceholder":213},[153,1502,1504],{"class":155,"line":1503},67,[153,1505,920],{"class":159},[153,1507,1509],{"class":155,"line":1508},68,[153,1510,1511],{"class":159},"-- Query 3: phrase search + headline display\n",[153,1513,1515],{"class":155,"line":1514},69,[153,1516,920],{"class":159},[153,1518,1520],{"class":155,"line":1519},70,[153,1521,1290],{"class":166},[153,1523,1525],{"class":155,"line":1524},71,[153,1526,1296],{"class":170},[153,1528,1530],{"class":155,"line":1529},72,[153,1531,1302],{"class":170},[153,1533,1535],{"class":155,"line":1534},73,[153,1536,1537],{"class":170},"  ts_headline(\n",[153,1539,1541,1544],{"class":155,"line":1540},74,[153,1542,1543],{"class":174},"    'english'",[153,1545,575],{"class":170},[153,1547,1549],{"class":155,"line":1548},75,[153,1550,1551],{"class":170},"    body,\n",[153,1553,1555,1558,1560,1562,1565],{"class":155,"line":1554},76,[153,1556,1557],{"class":170},"    phraseto_tsquery(",[153,1559,175],{"class":174},[153,1561,25],{"class":170},[153,1563,1564],{"class":174},"'full text search'",[153,1566,1212],{"class":170},[153,1568,1570],{"class":155,"line":1569},77,[153,1571,1572],{"class":174},"    'MaxWords=35, MinWords=15, ShortWord=3, MaxFragments=3'\n",[153,1574,1576,1579,1581,1584],{"class":155,"line":1575},78,[153,1577,1578],{"class":170},"  ) ",[153,1580,1311],{"class":166},[153,1582,1583],{"class":170}," snippet,                            ",[153,1585,1586],{"class":159},"-- highlighted snippet of the body\n",[153,1588,1590,1593,1595,1597,1599,1602,1604],{"class":155,"line":1589},79,[153,1591,1592],{"class":170},"  ts_rank(search_vec, phraseto_tsquery(",[153,1594,175],{"class":174},[153,1596,25],{"class":170},[153,1598,1564],{"class":174},[153,1600,1601],{"class":170},")) ",[153,1603,1311],{"class":166},[153,1605,1606],{"class":170}," rank\n",[153,1608,1610,1612],{"class":155,"line":1609},80,[153,1611,1323],{"class":166},[153,1613,1614],{"class":170}," articles\n",[153,1616,1618,1620,1623,1625,1627,1629],{"class":155,"line":1617},81,[153,1619,1342],{"class":166},[153,1621,1622],{"class":170}," search_vec @@ phraseto_tsquery(",[153,1624,175],{"class":174},[153,1626,25],{"class":170},[153,1628,1564],{"class":174},[153,1630,1094],{"class":170},[153,1632,1634,1636,1638,1640],{"class":155,"line":1633},82,[153,1635,1354],{"class":166},[153,1637,1357],{"class":170},[153,1639,1360],{"class":166},[153,1641,1380],{"class":170},[153,1643,1645],{"class":155,"line":1644},83,[153,1646,214],{"emptyLinePlaceholder":213},[153,1648,1650],{"class":155,"line":1649},84,[153,1651,920],{"class":159},[153,1653,1655],{"class":155,"line":1654},85,[153,1656,1657],{"class":159},"-- Query 4: websearch_to_tsquery for user-friendly query syntax\n",[153,1659,1661],{"class":155,"line":1660},86,[153,1662,920],{"class":159},[153,1664,1666],{"class":155,"line":1665},87,[153,1667,1668],{"class":159},"-- Accepts: \"phrase\" -exclude OR alternative\n",[153,1670,1672],{"class":155,"line":1671},88,[153,1673,1290],{"class":166},[153,1675,1677],{"class":155,"line":1676},89,[153,1678,1296],{"class":170},[153,1680,1682],{"class":155,"line":1681},90,[153,1683,1302],{"class":170},[153,1685,1687,1689,1691],{"class":155,"line":1686},91,[153,1688,1308],{"class":170},[153,1690,1311],{"class":166},[153,1692,1606],{"class":170},[153,1694,1696,1698,1701,1703,1705,1708],{"class":155,"line":1695},92,[153,1697,1323],{"class":166},[153,1699,1700],{"class":170}," articles, websearch_to_tsquery(",[153,1702,175],{"class":174},[153,1704,25],{"class":170},[153,1706,1707],{"class":174},"'postgres -mysql OR database'",[153,1709,1336],{"class":170},[153,1711,1713,1715],{"class":155,"line":1712},93,[153,1714,1342],{"class":166},[153,1716,1476],{"class":170},[153,1718,1720,1722,1724],{"class":155,"line":1719},94,[153,1721,1354],{"class":166},[153,1723,1357],{"class":170},[153,1725,1486],{"class":166},[153,1727,1729,1731,1733],{"class":155,"line":1728},95,[153,1730,1374],{"class":166},[153,1732,1377],{"class":646},[153,1734,1380],{"class":170},[153,1736,1738],{"class":155,"line":1737},96,[153,1739,214],{"emptyLinePlaceholder":213},[153,1741,1743],{"class":155,"line":1742},97,[153,1744,920],{"class":159},[153,1746,1748],{"class":155,"line":1747},98,[153,1749,1750],{"class":159},"-- Query 5: prefix matching (typeahead search)\n",[153,1752,1754],{"class":155,"line":1753},99,[153,1755,920],{"class":159},[153,1757,1759],{"class":155,"line":1758},100,[153,1760,1290],{"class":166},[153,1762,1764],{"class":155,"line":1763},101,[153,1765,1296],{"class":170},[153,1767,1769],{"class":155,"line":1768},102,[153,1770,1302],{"class":170},[153,1772,1774,1776,1778],{"class":155,"line":1773},103,[153,1775,1308],{"class":170},[153,1777,1311],{"class":166},[153,1779,1606],{"class":170},[153,1781,1783,1785,1787,1789,1791,1794,1797],{"class":155,"line":1782},104,[153,1784,1323],{"class":166},[153,1786,1326],{"class":170},[153,1788,175],{"class":174},[153,1790,25],{"class":170},[153,1792,1793],{"class":174},"'post:*'",[153,1795,1796],{"class":170},") query   ",[153,1798,1799],{"class":159},"-- :* = prefix match\n",[153,1801,1803,1805,1808],{"class":155,"line":1802},105,[153,1804,1342],{"class":166},[153,1806,1807],{"class":170}," search_vec @@ query                                ",[153,1809,1810],{"class":159},"-- matches 'postgres', 'posting', etc.\n",[153,1812,1814,1816,1818],{"class":155,"line":1813},106,[153,1815,1354],{"class":166},[153,1817,1357],{"class":170},[153,1819,1486],{"class":166},[153,1821,1823,1825,1827],{"class":155,"line":1822},107,[153,1824,1374],{"class":166},[153,1826,1377],{"class":646},[153,1828,1380],{"class":170},[153,1830,1832],{"class":155,"line":1831},108,[153,1833,214],{"emptyLinePlaceholder":213},[153,1835,1837],{"class":155,"line":1836},109,[153,1838,920],{"class":159},[153,1840,1842],{"class":155,"line":1841},110,[153,1843,1844],{"class":159},"-- Query 6: combined FTS + structured filters\n",[153,1846,1848],{"class":155,"line":1847},111,[153,1849,920],{"class":159},[153,1851,1853],{"class":155,"line":1852},112,[153,1854,1290],{"class":166},[153,1856,1858],{"class":155,"line":1857},113,[153,1859,1296],{"class":170},[153,1861,1863],{"class":155,"line":1862},114,[153,1864,1302],{"class":170},[153,1866,1868,1870,1872],{"class":155,"line":1867},115,[153,1869,1308],{"class":170},[153,1871,1311],{"class":166},[153,1873,1606],{"class":170},[153,1875,1877,1879,1881,1883,1885,1888],{"class":155,"line":1876},116,[153,1878,1323],{"class":166},[153,1880,1326],{"class":170},[153,1882,175],{"class":174},[153,1884,25],{"class":170},[153,1886,1887],{"class":174},"'postgres & index'",[153,1889,1336],{"class":170},[153,1891,1893,1895],{"class":155,"line":1892},117,[153,1894,1342],{"class":166},[153,1896,1476],{"class":170},[153,1898,1900,1903,1906,1909,1911,1914,1917,1920,1923],{"class":155,"line":1899},118,[153,1901,1902],{"class":166},"  AND",[153,1904,1905],{"class":170}," published_at ",[153,1907,1908],{"class":166},">=",[153,1910,996],{"class":166},[153,1912,1913],{"class":170},"() ",[153,1915,1916],{"class":166},"-",[153,1918,1919],{"class":170}," INTERVAL ",[153,1921,1922],{"class":174},"'30 days'",[153,1924,1925],{"class":159},"   -- time filter (uses published_at index)\n",[153,1927,1929,1931,1934,1937,1940],{"class":155,"line":1928},119,[153,1930,1902],{"class":166},[153,1932,1933],{"class":170}," author ",[153,1935,1936],{"class":166},"=",[153,1938,1939],{"class":174}," 'Alice Chen'",[153,1941,1942],{"class":159},"                         -- equality filter\n",[153,1944,1946,1948,1950],{"class":155,"line":1945},120,[153,1947,1354],{"class":166},[153,1949,1357],{"class":170},[153,1951,1486],{"class":166},[153,1953,1955,1957,1959],{"class":155,"line":1954},121,[153,1956,1374],{"class":166},[153,1958,1377],{"class":646},[153,1960,1380],{"class":170},[14,1962,1963,1966],{},[51,1964,1965],{},"Key design decisions",":",[45,1968,1969,1978,1986,1995,2007],{},[48,1970,1971,60,1974,1977],{},[51,1972,1973],{},"Generated column",[18,1975,1976],{},"search_vec"," is always in sync, no trigger maintenance.",[48,1979,1980,60,1983,1985],{},[51,1981,1982],{},"Weighted columns",[18,1984,740],{}," makes title hits rank higher than body hits.",[48,1987,1988,1991,1992,1994],{},[51,1989,1990],{},"GIN index"," — accelerates the ",[18,1993,375],{}," filter from O(all docs) to O(matches).",[48,1996,1997,2003,2004,2006],{},[51,1998,1999,2002],{},[18,2000,2001],{},"ts_rank"," for sorting"," — the GIN index finds matches; ",[18,2005,2001],{}," ranks the survivors.",[48,2008,2009,2015],{},[51,2010,2011,2014],{},[18,2012,2013],{},"ts_headline"," for snippets"," — generates highlighted search-result previews.",[31,2017],{},[34,2019,2021],{"id":2020},"ranking-ts_rank-vs-ts_rank_cd","Ranking: ts_rank vs ts_rank_cd",[2023,2024,2025,2041],"table",{},[2026,2027,2028],"thead",{},[2029,2030,2031,2035,2038],"tr",{},[2032,2033,2034],"th",{},"Function",[2032,2036,2037],{},"Algorithm",[2032,2039,2040],{},"When to use",[2042,2043,2044,2058],"tbody",{},[2029,2045,2046,2052,2055],{},[2047,2048,2049],"td",{},[18,2050,2051],{},"ts_rank(vec, query)",[2047,2053,2054],{},"Sum of weighted lexeme frequencies",[2047,2056,2057],{},"General relevance",[2029,2059,2060,2065,2068],{},[2047,2061,2062],{},[18,2063,2064],{},"ts_rank_cd(vec, query)",[2047,2066,2067],{},"Cover density (how close lexemes are)",[2047,2069,2070],{},"Phrase\u002Fproximity relevance",[141,2072,2073],{"language":143},[145,2074,2076],{"className":147,"code":2075,"language":143,"meta":149,"style":149},"-- ts_rank: higher when a lexeme appears more often in the document\nSELECT ts_rank(search_vec, to_tsquery('english', 'postgres')) FROM articles;\n\n-- ts_rank_cd: higher when matched lexemes are closer together\nSELECT ts_rank_cd(search_vec, to_tsquery('english', 'postgres & index')) FROM articles;\n\n-- ⚠️ ts_rank is NOT normalized: it's not a 0–1 score\n-- Don't compare ts_rank across different queries — only within a result set\n",[18,2077,2078,2083,2103,2107,2112,2131,2135,2140],{"__ignoreMap":149},[153,2079,2080],{"class":155,"line":156},[153,2081,2082],{"class":159},"-- ts_rank: higher when a lexeme appears more often in the document\n",[153,2084,2085,2087,2090,2092,2094,2096,2098,2100],{"class":155,"line":163},[153,2086,167],{"class":166},[153,2088,2089],{"class":170}," ts_rank(search_vec, to_tsquery(",[153,2091,175],{"class":174},[153,2093,25],{"class":170},[153,2095,1466],{"class":174},[153,2097,1601],{"class":170},[153,2099,1323],{"class":166},[153,2101,2102],{"class":170}," articles;\n",[153,2104,2105],{"class":155,"line":186},[153,2106,214],{"emptyLinePlaceholder":213},[153,2108,2109],{"class":155,"line":192},[153,2110,2111],{"class":159},"-- ts_rank_cd: higher when matched lexemes are closer together\n",[153,2113,2114,2116,2119,2121,2123,2125,2127,2129],{"class":155,"line":198},[153,2115,167],{"class":166},[153,2117,2118],{"class":170}," ts_rank_cd(search_vec, to_tsquery(",[153,2120,175],{"class":174},[153,2122,25],{"class":170},[153,2124,1887],{"class":174},[153,2126,1601],{"class":170},[153,2128,1323],{"class":166},[153,2130,2102],{"class":170},[153,2132,2133],{"class":155,"line":204},[153,2134,214],{"emptyLinePlaceholder":213},[153,2136,2137],{"class":155,"line":210},[153,2138,2139],{"class":159},"-- ⚠️ ts_rank is NOT normalized: it's not a 0–1 score\n",[153,2141,2142],{"class":155,"line":217},[153,2143,2144],{"class":159},"-- Don't compare ts_rank across different queries — only within a result set\n",[31,2146],{},[34,2148,2150],{"id":2149},"highlighting-with-ts_headline","Highlighting with ts_headline",[141,2152,2153],{"language":143},[145,2154,2156],{"className":147,"code":2155,"language":143,"meta":149,"style":149},"-- Basic headline: snippet of the body with matched terms highlighted\nSELECT\n  id,\n  ts_headline('english', body, to_tsquery('english', 'postgres')) AS snippet\nFROM articles\nWHERE search_vec @@ to_tsquery('english', 'postgres');\n\n-- Customized headline: control snippet length, highlighting, fragments\nSELECT\n  id,\n  ts_headline(\n    'english',\n    body,\n    to_tsquery('english', 'postgres & index'),\n    'MaxWords=35, MinWords=15, ShortWord=3, MaxFragments=3, FragmentDelimiter=\" ... \"'\n  ) AS snippet\nFROM articles\nWHERE search_vec @@ to_tsquery('english', 'postgres & index');\n\n-- Custom highlight tags (default is \u003Cb>...\u003C\u002Fb>)\nSELECT\n  ts_headline('english', body, to_tsquery('english', 'postgres'),\n    'StartSel=\u003Cmark>, StopSel=\u003C\u002Fmark>') AS snippet\nFROM articles\nWHERE search_vec @@ to_tsquery('english', 'postgres');\n-- Output: ... \u003Cmark>PostgreSQL\u003C\u002Fmark> has a powerful ...\n",[18,2157,2158,2163,2167,2171,2194,2200,2215,2219,2224,2228,2232,2236,2242,2246,2259,2264,2272,2278,2292,2296,2301,2305,2321,2332,2338,2352],{"__ignoreMap":149},[153,2159,2160],{"class":155,"line":156},[153,2161,2162],{"class":159},"-- Basic headline: snippet of the body with matched terms highlighted\n",[153,2164,2165],{"class":155,"line":163},[153,2166,1290],{"class":166},[153,2168,2169],{"class":155,"line":186},[153,2170,1296],{"class":170},[153,2172,2173,2176,2178,2181,2183,2185,2187,2189,2191],{"class":155,"line":192},[153,2174,2175],{"class":170},"  ts_headline(",[153,2177,175],{"class":174},[153,2179,2180],{"class":170},", body, to_tsquery(",[153,2182,175],{"class":174},[153,2184,25],{"class":170},[153,2186,1466],{"class":174},[153,2188,1601],{"class":170},[153,2190,1311],{"class":166},[153,2192,2193],{"class":170}," snippet\n",[153,2195,2196,2198],{"class":155,"line":198},[153,2197,1323],{"class":166},[153,2199,1614],{"class":170},[153,2201,2202,2204,2207,2209,2211,2213],{"class":155,"line":204},[153,2203,1342],{"class":166},[153,2205,2206],{"class":170}," search_vec @@ to_tsquery(",[153,2208,175],{"class":174},[153,2210,25],{"class":170},[153,2212,1466],{"class":174},[153,2214,183],{"class":170},[153,2216,2217],{"class":155,"line":210},[153,2218,214],{"emptyLinePlaceholder":213},[153,2220,2221],{"class":155,"line":217},[153,2222,2223],{"class":159},"-- Customized headline: control snippet length, highlighting, fragments\n",[153,2225,2226],{"class":155,"line":223},[153,2227,1290],{"class":166},[153,2229,2230],{"class":155,"line":240},[153,2231,1296],{"class":170},[153,2233,2234],{"class":155,"line":246},[153,2235,1537],{"class":170},[153,2237,2238,2240],{"class":155,"line":251},[153,2239,1543],{"class":174},[153,2241,575],{"class":170},[153,2243,2244],{"class":155,"line":257},[153,2245,1551],{"class":170},[153,2247,2248,2251,2253,2255,2257],{"class":155,"line":274},[153,2249,2250],{"class":170},"    to_tsquery(",[153,2252,175],{"class":174},[153,2254,25],{"class":170},[153,2256,1887],{"class":174},[153,2258,1212],{"class":170},[153,2260,2261],{"class":155,"line":280},[153,2262,2263],{"class":174},"    'MaxWords=35, MinWords=15, ShortWord=3, MaxFragments=3, FragmentDelimiter=\" ... \"'\n",[153,2265,2266,2268,2270],{"class":155,"line":285},[153,2267,1578],{"class":170},[153,2269,1311],{"class":166},[153,2271,2193],{"class":170},[153,2273,2274,2276],{"class":155,"line":291},[153,2275,1323],{"class":166},[153,2277,1614],{"class":170},[153,2279,2280,2282,2284,2286,2288,2290],{"class":155,"line":307},[153,2281,1342],{"class":166},[153,2283,2206],{"class":170},[153,2285,175],{"class":174},[153,2287,25],{"class":170},[153,2289,1887],{"class":174},[153,2291,183],{"class":170},[153,2293,2294],{"class":155,"line":313},[153,2295,214],{"emptyLinePlaceholder":213},[153,2297,2298],{"class":155,"line":318},[153,2299,2300],{"class":159},"-- Custom highlight tags (default is \u003Cb>...\u003C\u002Fb>)\n",[153,2302,2303],{"class":155,"line":324},[153,2304,1290],{"class":166},[153,2306,2307,2309,2311,2313,2315,2317,2319],{"class":155,"line":341},[153,2308,2175],{"class":170},[153,2310,175],{"class":174},[153,2312,2180],{"class":170},[153,2314,175],{"class":174},[153,2316,25],{"class":170},[153,2318,1466],{"class":174},[153,2320,1212],{"class":170},[153,2322,2323,2326,2328,2330],{"class":155,"line":347},[153,2324,2325],{"class":174},"    'StartSel=\u003Cmark>, StopSel=\u003C\u002Fmark>'",[153,2327,662],{"class":170},[153,2329,1311],{"class":166},[153,2331,2193],{"class":170},[153,2333,2334,2336],{"class":155,"line":1139},[153,2335,1323],{"class":166},[153,2337,1614],{"class":170},[153,2339,2340,2342,2344,2346,2348,2350],{"class":155,"line":1154},[153,2341,1342],{"class":166},[153,2343,2206],{"class":170},[153,2345,175],{"class":174},[153,2347,25],{"class":170},[153,2349,1466],{"class":174},[153,2351,183],{"class":170},[153,2353,2354],{"class":155,"line":1159},[153,2355,2356],{"class":159},"-- Output: ... \u003Cmark>PostgreSQL\u003C\u002Fmark> has a powerful ...\n",[14,2358,2359,2361],{},[18,2360,2013],{}," returns a snippet of the body with matched terms highlighted — a built-in \"search result preview\" that beats manually substring-slicing in application code.",[31,2363],{},[34,2365,2367],{"id":2366},"search-features","Search Features",[746,2369,2371],{"id":2370},"boolean-operators","Boolean Operators",[141,2373,2374],{"language":143},[145,2375,2377],{"className":147,"code":2376,"language":143,"meta":149,"style":149},"to_tsquery('english', 'postgres & index')      -- both terms (AND)\nto_tsquery('english', 'postgres | mysql')      -- either term (OR)\nto_tsquery('english', 'postgres & !mysql')     -- postgres, NOT mysql\nto_tsquery('english', 'postgres & (index | search)')  -- grouping with parentheses\n",[18,2378,2379,2396,2412,2429],{"__ignoreMap":149},[153,2380,2381,2384,2386,2388,2390,2393],{"class":155,"line":156},[153,2382,2383],{"class":170},"to_tsquery(",[153,2385,175],{"class":174},[153,2387,25],{"class":170},[153,2389,1887],{"class":174},[153,2391,2392],{"class":170},")      ",[153,2394,2395],{"class":159},"-- both terms (AND)\n",[153,2397,2398,2400,2402,2404,2407,2409],{"class":155,"line":163},[153,2399,2383],{"class":170},[153,2401,175],{"class":174},[153,2403,25],{"class":170},[153,2405,2406],{"class":174},"'postgres | mysql'",[153,2408,2392],{"class":170},[153,2410,2411],{"class":159},"-- either term (OR)\n",[153,2413,2414,2416,2418,2420,2423,2426],{"class":155,"line":186},[153,2415,2383],{"class":170},[153,2417,175],{"class":174},[153,2419,25],{"class":170},[153,2421,2422],{"class":174},"'postgres & !mysql'",[153,2424,2425],{"class":170},")     ",[153,2427,2428],{"class":159},"-- postgres, NOT mysql\n",[153,2430,2431,2433,2435,2437,2440,2443],{"class":155,"line":192},[153,2432,2383],{"class":170},[153,2434,175],{"class":174},[153,2436,25],{"class":170},[153,2438,2439],{"class":174},"'postgres & (index | search)'",[153,2441,2442],{"class":170},")  ",[153,2444,2445],{"class":159},"-- grouping with parentheses\n",[746,2447,2449],{"id":2448},"phrase-and-proximity-search","Phrase and Proximity Search",[141,2451,2452],{"language":143},[145,2453,2455],{"className":147,"code":2454,"language":143,"meta":149,"style":149},"-- Phrase: words must be adjacent and in order\nphraseto_tsquery('english', 'full text search')\n--  'full' \u003C-> 'text' \u003C-> 'search'\n\n-- Proximity: within N positions (any order)\nto_tsquery('english', 'quick \u003C3> fox')\n--  'quick' \u003C3> 'fox'   (within 3 positions)\n\n-- Adjacent (distance 1, any order)\nto_tsquery('english', 'quick \u003C-> fox')\n--  'quick' \u003C-> 'fox'\n",[18,2456,2457,2462,2475,2480,2484,2489,2501,2506,2510,2515,2528],{"__ignoreMap":149},[153,2458,2459],{"class":155,"line":156},[153,2460,2461],{"class":159},"-- Phrase: words must be adjacent and in order\n",[153,2463,2464,2467,2469,2471,2473],{"class":155,"line":163},[153,2465,2466],{"class":170},"phraseto_tsquery(",[153,2468,175],{"class":174},[153,2470,25],{"class":170},[153,2472,1564],{"class":174},[153,2474,1094],{"class":170},[153,2476,2477],{"class":155,"line":186},[153,2478,2479],{"class":159},"--  'full' \u003C-> 'text' \u003C-> 'search'\n",[153,2481,2482],{"class":155,"line":192},[153,2483,214],{"emptyLinePlaceholder":213},[153,2485,2486],{"class":155,"line":198},[153,2487,2488],{"class":159},"-- Proximity: within N positions (any order)\n",[153,2490,2491,2493,2495,2497,2499],{"class":155,"line":204},[153,2492,2383],{"class":170},[153,2494,175],{"class":174},[153,2496,25],{"class":170},[153,2498,302],{"class":174},[153,2500,1094],{"class":170},[153,2502,2503],{"class":155,"line":210},[153,2504,2505],{"class":159},"--  'quick' \u003C3> 'fox'   (within 3 positions)\n",[153,2507,2508],{"class":155,"line":217},[153,2509,214],{"emptyLinePlaceholder":213},[153,2511,2512],{"class":155,"line":223},[153,2513,2514],{"class":159},"-- Adjacent (distance 1, any order)\n",[153,2516,2517,2519,2521,2523,2526],{"class":155,"line":240},[153,2518,2383],{"class":170},[153,2520,175],{"class":174},[153,2522,25],{"class":170},[153,2524,2525],{"class":174},"'quick \u003C-> fox'",[153,2527,1094],{"class":170},[153,2529,2530],{"class":155,"line":246},[153,2531,2532],{"class":159},"--  'quick' \u003C-> 'fox'\n",[746,2534,2536],{"id":2535},"prefix-matching","Prefix Matching",[141,2538,2539],{"language":143},[145,2540,2542],{"className":147,"code":2541,"language":143,"meta":149,"style":149},"-- :* matches any lexeme starting with the prefix\nto_tsquery('english', 'post:*')\n--  'post':*   matches 'postgres', 'posting', 'post', etc.\n-- Useful for typeahead\u002Fas-you-type search\n",[18,2543,2544,2549,2561,2566],{"__ignoreMap":149},[153,2545,2546],{"class":155,"line":156},[153,2547,2548],{"class":159},"-- :* matches any lexeme starting with the prefix\n",[153,2550,2551,2553,2555,2557,2559],{"class":155,"line":163},[153,2552,2383],{"class":170},[153,2554,175],{"class":174},[153,2556,25],{"class":170},[153,2558,1793],{"class":174},[153,2560,1094],{"class":170},[153,2562,2563],{"class":155,"line":186},[153,2564,2565],{"class":159},"--  'post':*   matches 'postgres', 'posting', 'post', etc.\n",[153,2567,2568],{"class":155,"line":192},[153,2569,2570],{"class":159},"-- Useful for typeahead\u002Fas-you-type search\n",[31,2572],{},[34,2574,2576],{"id":2575},"anti-pattern-like-for-search-at-scale","Anti-Pattern: LIKE for Search at Scale",[141,2578,2579],{"language":143},[145,2580,2582],{"className":147,"code":2581,"language":143,"meta":149,"style":149},"-- ❌ WRONG: LIKE '%word%' for search at scale\nSELECT * FROM articles WHERE body LIKE '%postgres%';\n-- Problems:\n-- 1. Full table scan (leading wildcard defeats B-tree index)\n-- 2. No stemming: 'postgres' won't match 'postgresql' or 'postgreSQL'\n-- 3. No ranking: all matches are equal, no \"best match first\"\n-- 4. No linguistic awareness: no stop word removal, no stemming\n\n-- ✅ RIGHT: FTS with GIN index\nSELECT id, title, ts_rank(search_vec, query) AS rank\nFROM articles, to_tsquery('english', 'postgres') query\nWHERE search_vec @@ query    -- GIN index: O(matches), not O(all docs)\nORDER BY rank DESC\nLIMIT 10;\n-- 1. Index-accelerated (GIN inverted index)\n-- 2. Stemming: 'postgres' matches 'postgresql', 'postgreSQL'\n-- 3. Ranked by relevance\n-- 4. Stop words removed, linguistically aware\n",[18,2583,2584,2589,2613,2618,2623,2628,2633,2638,2642,2647,2658,2672,2682,2690,2698,2703,2708,2713],{"__ignoreMap":149},[153,2585,2586],{"class":155,"line":156},[153,2587,2588],{"class":159},"-- ❌ WRONG: LIKE '%word%' for search at scale\n",[153,2590,2591,2593,2596,2599,2601,2603,2606,2608,2611],{"class":155,"line":163},[153,2592,167],{"class":166},[153,2594,2595],{"class":166}," *",[153,2597,2598],{"class":166}," FROM",[153,2600,729],{"class":170},[153,2602,1342],{"class":166},[153,2604,2605],{"class":170}," body ",[153,2607,20],{"class":166},[153,2609,2610],{"class":174}," '%postgres%'",[153,2612,1380],{"class":170},[153,2614,2615],{"class":155,"line":186},[153,2616,2617],{"class":159},"-- Problems:\n",[153,2619,2620],{"class":155,"line":192},[153,2621,2622],{"class":159},"-- 1. Full table scan (leading wildcard defeats B-tree index)\n",[153,2624,2625],{"class":155,"line":198},[153,2626,2627],{"class":159},"-- 2. No stemming: 'postgres' won't match 'postgresql' or 'postgreSQL'\n",[153,2629,2630],{"class":155,"line":204},[153,2631,2632],{"class":159},"-- 3. No ranking: all matches are equal, no \"best match first\"\n",[153,2634,2635],{"class":155,"line":210},[153,2636,2637],{"class":159},"-- 4. No linguistic awareness: no stop word removal, no stemming\n",[153,2639,2640],{"class":155,"line":217},[153,2641,214],{"emptyLinePlaceholder":213},[153,2643,2644],{"class":155,"line":223},[153,2645,2646],{"class":159},"-- ✅ RIGHT: FTS with GIN index\n",[153,2648,2649,2651,2654,2656],{"class":155,"line":240},[153,2650,167],{"class":166},[153,2652,2653],{"class":170}," id, title, ts_rank(search_vec, query) ",[153,2655,1311],{"class":166},[153,2657,1606],{"class":170},[153,2659,2660,2662,2664,2666,2668,2670],{"class":155,"line":246},[153,2661,1323],{"class":166},[153,2663,1326],{"class":170},[153,2665,175],{"class":174},[153,2667,25],{"class":170},[153,2669,1466],{"class":174},[153,2671,1336],{"class":170},[153,2673,2674,2676,2679],{"class":155,"line":251},[153,2675,1342],{"class":166},[153,2677,2678],{"class":170}," search_vec @@ query    ",[153,2680,2681],{"class":159},"-- GIN index: O(matches), not O(all docs)\n",[153,2683,2684,2686,2688],{"class":155,"line":257},[153,2685,1354],{"class":166},[153,2687,1357],{"class":170},[153,2689,1486],{"class":166},[153,2691,2692,2694,2696],{"class":155,"line":274},[153,2693,1374],{"class":166},[153,2695,1377],{"class":646},[153,2697,1380],{"class":170},[153,2699,2700],{"class":155,"line":280},[153,2701,2702],{"class":159},"-- 1. Index-accelerated (GIN inverted index)\n",[153,2704,2705],{"class":155,"line":285},[153,2706,2707],{"class":159},"-- 2. Stemming: 'postgres' matches 'postgresql', 'postgreSQL'\n",[153,2709,2710],{"class":155,"line":291},[153,2711,2712],{"class":159},"-- 3. Ranked by relevance\n",[153,2714,2715],{"class":155,"line":307},[153,2716,2717],{"class":159},"-- 4. Stop words removed, linguistically aware\n",[14,2719,2720,2723,2724,2726],{},[51,2721,2722],{},"Why the wrong way fails",": ",[18,2725,42],{}," wraps the column in a pattern match that can't use any index — it scans every row, compares the pattern, and returns unranked matches. For 1M rows, that's 1M string comparisons per query.",[31,2728],{},[34,2730,2732],{"id":2731},"trigrams-substring-search-without-fts","Trigrams: Substring Search Without FTS",[14,2734,2735],{},"For substring search where FTS's stemming\u002Fstop-words aren't appropriate (product names, codes, identifiers):",[141,2737,2738],{"language":143},[145,2739,2741],{"className":147,"code":2740,"language":143,"meta":149,"style":149},"CREATE EXTENSION pg_trgm;\n\n-- GIN index with trigram ops: accelerates LIKE\u002FILIKE and similarity matching\nCREATE INDEX products_name_trgm ON products USING gin(name gin_trgm_ops);\n\n-- These now use the GIN index (no full table scan):\nSELECT * FROM products WHERE name LIKE '%phone%';     -- substring match\nSELECT * FROM products WHERE name ILIKE '%PHONE%';    -- case-insensitive substring\nSELECT * FROM products WHERE name % 'iphone';         -- similarity match (fuzzy)\n\n-- Similarity score (0–1, higher = more similar)\nSELECT name, similarity(name, 'iphone') AS sim\nFROM products\nWHERE name % 'iphone'\nORDER BY sim DESC\nLIMIT 5;\n",[18,2742,2743,2750,2754,2759,2784,2788,2793,2820,2846,2872,2876,2881,2903,2910,2921,2930],{"__ignoreMap":149},[153,2744,2745,2747],{"class":155,"line":156},[153,2746,551],{"class":166},[153,2748,2749],{"class":170}," EXTENSION pg_trgm;\n",[153,2751,2752],{"class":155,"line":163},[153,2753,214],{"emptyLinePlaceholder":213},[153,2755,2756],{"class":155,"line":186},[153,2757,2758],{"class":159},"-- GIN index with trigram ops: accelerates LIKE\u002FILIKE and similarity matching\n",[153,2760,2761,2763,2765,2768,2770,2773,2775,2778,2781],{"class":155,"line":192},[153,2762,551],{"class":166},[153,2764,720],{"class":166},[153,2766,2767],{"class":557}," products_name_trgm",[153,2769,726],{"class":166},[153,2771,2772],{"class":170}," products ",[153,2774,732],{"class":166},[153,2776,2777],{"class":170}," gin(",[153,2779,2780],{"class":166},"name",[153,2782,2783],{"class":170}," gin_trgm_ops);\n",[153,2785,2786],{"class":155,"line":198},[153,2787,214],{"emptyLinePlaceholder":213},[153,2789,2790],{"class":155,"line":204},[153,2791,2792],{"class":159},"-- These now use the GIN index (no full table scan):\n",[153,2794,2795,2797,2799,2801,2803,2805,2808,2811,2814,2817],{"class":155,"line":210},[153,2796,167],{"class":166},[153,2798,2595],{"class":166},[153,2800,2598],{"class":166},[153,2802,2772],{"class":170},[153,2804,1342],{"class":166},[153,2806,2807],{"class":166}," name",[153,2809,2810],{"class":166}," LIKE",[153,2812,2813],{"class":174}," '%phone%'",[153,2815,2816],{"class":170},";     ",[153,2818,2819],{"class":159},"-- substring match\n",[153,2821,2822,2824,2826,2828,2830,2832,2834,2837,2840,2843],{"class":155,"line":217},[153,2823,167],{"class":166},[153,2825,2595],{"class":166},[153,2827,2598],{"class":166},[153,2829,2772],{"class":170},[153,2831,1342],{"class":166},[153,2833,2807],{"class":166},[153,2835,2836],{"class":170}," ILIKE ",[153,2838,2839],{"class":174},"'%PHONE%'",[153,2841,2842],{"class":170},";    ",[153,2844,2845],{"class":159},"-- case-insensitive substring\n",[153,2847,2848,2850,2852,2854,2856,2858,2860,2863,2866,2869],{"class":155,"line":223},[153,2849,167],{"class":166},[153,2851,2595],{"class":166},[153,2853,2598],{"class":166},[153,2855,2772],{"class":170},[153,2857,1342],{"class":166},[153,2859,2807],{"class":166},[153,2861,2862],{"class":170}," % ",[153,2864,2865],{"class":174},"'iphone'",[153,2867,2868],{"class":170},";         ",[153,2870,2871],{"class":159},"-- similarity match (fuzzy)\n",[153,2873,2874],{"class":155,"line":240},[153,2875,214],{"emptyLinePlaceholder":213},[153,2877,2878],{"class":155,"line":246},[153,2879,2880],{"class":159},"-- Similarity score (0–1, higher = more similar)\n",[153,2882,2883,2885,2887,2890,2892,2894,2896,2898,2900],{"class":155,"line":251},[153,2884,167],{"class":166},[153,2886,2807],{"class":166},[153,2888,2889],{"class":170},", similarity(",[153,2891,2780],{"class":166},[153,2893,25],{"class":170},[153,2895,2865],{"class":174},[153,2897,662],{"class":170},[153,2899,1311],{"class":166},[153,2901,2902],{"class":170}," sim\n",[153,2904,2905,2907],{"class":155,"line":257},[153,2906,1323],{"class":166},[153,2908,2909],{"class":170}," products\n",[153,2911,2912,2914,2916,2918],{"class":155,"line":274},[153,2913,1342],{"class":166},[153,2915,2807],{"class":166},[153,2917,2862],{"class":170},[153,2919,2920],{"class":174},"'iphone'\n",[153,2922,2923,2925,2928],{"class":155,"line":280},[153,2924,1354],{"class":166},[153,2926,2927],{"class":170}," sim ",[153,2929,1486],{"class":166},[153,2931,2932,2934,2937],{"class":155,"line":285},[153,2933,1374],{"class":166},[153,2935,2936],{"class":646}," 5",[153,2938,1380],{"class":170},[14,2940,2941,2942,2945],{},"Trigrams (3-character substrings) enable fast substring and fuzzy matching. Use ",[18,2943,2944],{},"pg_trgm"," for short-text\u002Fsubstring search; use FTS for natural-language document search.",[31,2947],{},[34,2949,2951],{"id":2950},"choosing-fts-vs-trigrams-vs-like","Choosing: FTS vs Trigrams vs LIKE",[2023,2953,2954,2967],{},[2026,2955,2956],{},[2029,2957,2958,2961,2964],{},[2032,2959,2960],{},"Need",[2032,2962,2963],{},"Use",[2032,2965,2966],{},"Index",[2042,2968,2969,2986,3001,3015,3024],{},[2029,2970,2971,2974,2983],{},[2047,2972,2973],{},"Natural-language documents (articles, descriptions), ranked",[2047,2975,2976,2977,2979,2980,2982],{},"FTS (",[18,2978,24],{},"\u002F",[18,2981,28],{},")",[2047,2984,2985],{},"GIN on tsvector",[2029,2987,2988,2991,2995],{},[2047,2989,2990],{},"Substring search on short text (product names, codes)",[2047,2992,2993],{},[18,2994,2944],{},[2047,2996,2997,2998],{},"GIN with ",[18,2999,3000],{},"gin_trgm_ops",[2029,3002,3003,3009,3012],{},[2047,3004,3005,3006,2982],{},"Exact prefix (",[18,3007,3008],{},"LIKE 'foo%'",[2047,3010,3011],{},"B-tree index",[2047,3013,3014],{},"B-tree",[2029,3016,3017,3020,3022],{},[2047,3018,3019],{},"Simple equality",[2047,3021,3011],{},[2047,3023,3014],{},[2029,3025,3026,3029,3041],{},[2047,3027,3028],{},"Fuzzy\u002Fsimilarity matching",[2047,3030,3031,3033,3034,3037,3038,2982],{},[18,3032,2944],{}," (",[18,3035,3036],{},"%"," operator, ",[18,3039,3040],{},"similarity()",[2047,3042,2997,3043],{},[18,3044,3000],{},[31,3046],{},[34,3048,3050],{"id":3049},"tips-tricks","💡 Tips & Tricks",[104,3052,3053,3067,3079,3094,3112,3119,3139,3152],{},[48,3054,3055,3058,3059,535,3061,3033,3063,3066],{},[51,3056,3057],{},"Idiom"," — store the ",[18,3060,24],{},[51,3062,538],{},[18,3064,3065],{},"GENERATED ALWAYS AS (...) STORED","): it's always in sync with the source text — no trigger needed, no stale vectors. Index the generated column with GIN.",[48,3068,3069,3071,3072,3075,3076,3078],{},[51,3070,3057],{}," — use ",[18,3073,3074],{},"setweight(to_tsvector('english', title), 'A') || setweight(to_tsvector('english', body), 'B')"," to rank title matches above body matches — ",[18,3077,2001],{}," incorporates the weights.",[48,3080,3081,3084,3085,3087,3088,3090,3091,3093],{},[51,3082,3083],{},"Performance"," — always GIN-index the ",[18,3086,24],{},": without it, ",[18,3089,375],{}," scans every document. A GIN index makes ",[18,3092,375],{}," queries O(matches) instead of O(all documents).",[48,3095,3096,3071,3098,3101,3102,3105,3106,2979,3108,2979,3110,79],{},[51,3097,3057],{},[18,3099,3100],{},"websearch_to_tsquery"," for user-facing search boxes: it accepts a user-friendly syntax (",[18,3103,3104],{},"\"phrase\" -exclude OR alternative",") without requiring users to know ",[18,3107,122],{},[18,3109,126],{},[18,3111,130],{},[48,3113,3114,3071,3116,3118],{},[51,3115,3057],{},[18,3117,2013],{}," to generate search-result snippets with highlighted terms: it's a built-in \"search result preview\" that beats manual substring-slicing in application code.",[48,3120,3121,3071,3123,3126,3127,3130,3131,3134,3135,3138],{},[51,3122,3057],{},[18,3124,3125],{},"phraseto_tsquery"," for exact phrase matching (",[18,3128,3129],{},"'quick' \u003C-> 'brown' \u003C-> 'fox'"," — adjacent, in order) vs ",[18,3132,3133],{},"to_tsquery"," for boolean presence (",[18,3136,3137],{},"'quick' & 'brown' & 'fox'"," — any order).",[48,3140,3141,3143,3144,3147,3148,3151],{},[51,3142,3057],{}," — use prefix matching (",[18,3145,3146],{},":*",") for typeahead search: ",[18,3149,3150],{},"to_tsquery('english', 'post:*')"," matches any lexeme starting with \"post\".",[48,3153,3154,3157,3158,3161,3162,3165],{},[51,3155,3156],{},"Portability"," — PostgreSQL FTS is PostgreSQL-specific. MySQL has ",[18,3159,3160],{},"FULLTEXT"," indexes (",[18,3163,3164],{},"MATCH ... AGAINST","). SQLite has FTS5 (a separate virtual table module). The concepts (stemming, ranking, inverted index) are universal; the syntax isn't.",[31,3167],{},[34,3169,3171],{"id":3170},"️-edge-cases-gotchas","⚠️ Edge Cases & Gotchas",[104,3173,3174,3194,3206,3217,3226,3243,3251,3266,3290,3303,3318,3329,3342],{},[48,3175,3176,2723,3179,3182,3183,3186,3187,3190,3191,3193],{},[51,3177,3178],{},"Stop words are removed",[18,3180,3181],{},"to_tsquery('english', 'the & fox')"," drops ",[18,3184,3185],{},"the"," (a stop word), so it's just ",[18,3188,3189],{},"'fox'",". ",[18,3192,3125],{}," keeps positions, so phrase search still works correctly.",[48,3195,3196,2723,3199,3202,3203,3205],{},[51,3197,3198],{},"Stemming can over-match",[18,3200,3201],{},"to_tsquery('english', 'run')"," matches \"run\", \"running\", \"runs\", \"ran\" (all stem to \"run\"). Good for search, bad for exact-term queries — use ",[18,3204,20],{}," or trigrams for exact matching.",[48,3207,3208,3213,3214,3216],{},[51,3209,3210,3212],{},[18,3211,2001],{}," is not normalized",": it's a sum of weighted frequencies, not a 0–1 score. Don't compare ",[18,3215,2001],{}," across different queries — compare within a result set.",[48,3218,3219,3222,3223,3225],{},[51,3220,3221],{},"GIN index size",": a GIN index on a ",[18,3224,24],{}," can be large (it's an inverted index of every lexeme). For huge tables, consider a partial index or periodic reindex.",[48,3227,3228,2723,3231,3234,3235,25,3237,3239,3240,3242],{},[51,3229,3230],{},"Language matters",[18,3232,3233],{},"to_tsvector('english', ...)"," uses English stemming\u002Fstop words. Use the right configuration for your content (",[18,3236,358],{},[18,3238,361],{},", etc.) — or ",[18,3241,365],{}," for no stemming\u002Fstop words.",[48,3244,3245,3250],{},[51,3246,3247,3249],{},[18,3248,365],{}," configuration",": no stemming, no stop words — treats every word as a literal lexeme. Useful for codes, identifiers, or non-natural-language text.",[48,3252,3253,3258,3259,2979,3262,3265],{},[51,3254,3255,3257],{},[18,3256,24],{}," column must be kept in sync",": if you store it as a regular column (not generated), you need a trigger to update it on ",[18,3260,3261],{},"INSERT",[18,3263,3264],{},"UPDATE",". Generated columns are the modern, maintenance-free way.",[48,3267,3268,2723,3278,3281,3282,3285,3286,3289],{},[51,3269,3270,3272,3273,3275,3276],{},[18,3271,3125],{}," vs ",[18,3274,3133],{}," with ",[18,3277,134],{},[18,3279,3280],{},"phraseto_tsquery('a b c')"," builds ",[18,3283,3284],{},"'a' \u003C-> 'b' \u003C-> 'c'"," (adjacent in order). ",[18,3287,3288],{},"to_tsquery('a & b & c')"," is \"all three present, any order.\" Pick based on whether order\u002Fadjacency matters.",[48,3291,3292,2723,3297,3299,3300,3302],{},[51,3293,3294,3295],{},"Prefix matching with ",[18,3296,3146],{},[18,3298,3150],{}," matches any lexeme starting with \"post\" — useful for typeahead. The ",[18,3301,3146],{}," must be on a single lexeme, not a phrase.",[48,3304,3305,2723,3308,3311,3312,3314,3315,3317],{},[51,3306,3307],{},"FTS doesn't do fuzzy spelling",[18,3309,3310],{},"to_tsquery('english', 'popstgres')"," won't match \"postgres\" (different lexeme after stemming). For typo tolerance, use ",[18,3313,2944],{},"'s similarity operator (",[18,3316,3036],{},") alongside FTS.",[48,3319,3320,3323,3324,3326,3327,79],{},[51,3321,3322],{},"Diacritics handling",": the ",[18,3325,175],{}," configuration may or may not strip diacritics depending on the dictionary. For multilingual content with diacritics, configure the dictionary appropriately or use ",[18,3328,365],{},[48,3330,3331,2723,3334,3337,3338,3341],{},[51,3332,3333],{},"NULL handling",[18,3335,3336],{},"to_tsvector('english', NULL)"," returns an empty tsvector, not NULL. ",[18,3339,3340],{},"coalesce(col, '')"," is still recommended for clarity.",[48,3343,3344,3347,3348,3351],{},[51,3345,3346],{},"tsvector position tracking",": positions are tracked per lexeme and used by ",[18,3349,3350],{},"ts_rank_cd"," (cover density) and phrase\u002Fproximity queries. If you rebuild the tsvector differently, positions change and phrase matching may break.",[31,3353],{},[34,3355,3357],{"id":3356},"spot-the-bug","🧠 Spot the Bug",[14,3359,3360],{},"A developer indexes an article body for FTS and queries it, but gets no results for a search on \"databases\" even though the body contains the word \"databases\":",[141,3362,3363],{"language":143},[145,3364,3366],{"className":147,"code":3365,"language":143,"meta":149,"style":149},"-- Index built with 'english' config\nCREATE INDEX articles_body_gin ON articles USING gin(to_tsvector('english', body));\n\n-- Query built with 'simple' config (different!)\nSELECT * FROM articles\nWHERE to_tsvector('simple', body) @@ to_tsquery('simple', 'database');\n",[18,3367,3368,3373,3396,3400,3405,3415],{"__ignoreMap":149},[153,3369,3370],{"class":155,"line":156},[153,3371,3372],{"class":159},"-- Index built with 'english' config\n",[153,3374,3375,3377,3379,3382,3384,3386,3388,3391,3393],{"class":155,"line":163},[153,3376,551],{"class":166},[153,3378,720],{"class":166},[153,3380,3381],{"class":557}," articles_body_gin",[153,3383,726],{"class":166},[153,3385,729],{"class":170},[153,3387,732],{"class":166},[153,3389,3390],{"class":170}," gin(to_tsvector(",[153,3392,175],{"class":174},[153,3394,3395],{"class":170},", body));\n",[153,3397,3398],{"class":155,"line":186},[153,3399,214],{"emptyLinePlaceholder":213},[153,3401,3402],{"class":155,"line":192},[153,3403,3404],{"class":159},"-- Query built with 'simple' config (different!)\n",[153,3406,3407,3409,3411,3413],{"class":155,"line":198},[153,3408,167],{"class":166},[153,3410,2595],{"class":166},[153,3412,2598],{"class":166},[153,3414,1614],{"class":170},[153,3416,3417,3419,3421,3423,3426,3428,3430,3432],{"class":155,"line":204},[153,3418,1342],{"class":166},[153,3420,171],{"class":170},[153,3422,365],{"class":174},[153,3424,3425],{"class":170},", body) @@ to_tsquery(",[153,3427,365],{"class":174},[153,3429,25],{"class":170},[153,3431,75],{"class":174},[153,3433,183],{"class":170},[3435,3436,3437,3441,3479,3488,3557,3651],"details",{},[3438,3439,3440],"summary",{},"Answer",[14,3442,3443,3446,3447,3450,3451,3454,3455,3457,3458,3461,3462,3464,3465,3467,3468,3470,3471,3473,3474,3470,3476,3478],{},[51,3444,3445],{},"Configuration mismatch",": the index is built with ",[18,3448,3449],{},"to_tsvector('english', body)"," but the query uses ",[18,3452,3453],{},"to_tsvector('simple', body)",". The ",[18,3456,175],{}," config stems \"databases\" → ",[18,3459,3460],{},"'databas'",", while the ",[18,3463,365],{}," config leaves it as ",[18,3466,75],{}," (no stemming). The tsquery ",[18,3469,75],{}," (from ",[18,3472,365],{},") doesn't match the tsvector's ",[18,3475,3460],{},[18,3477,175],{},") — different lexemes.",[14,3480,3481,3482,3484,3485,3487],{},"The index can't be used either — the query's expression (",[18,3483,3453],{},") doesn't match the index's expression (",[18,3486,3449],{},").",[141,3489,3490],{"language":143},[145,3491,3493],{"className":147,"code":3492,"language":143,"meta":149,"style":149},"-- ✅ Fix 1: use the same configuration in both index and query\nCREATE INDEX articles_body_gin ON articles USING gin(to_tsvector('english', body));\n\nSELECT * FROM articles\nWHERE to_tsvector('english', body) @@ to_tsquery('english', 'database');\n-- 'database' stems to 'databas' (english), 'databases' stems to 'databas' (english) → MATCH\n",[18,3494,3495,3500,3520,3524,3534,3552],{"__ignoreMap":149},[153,3496,3497],{"class":155,"line":156},[153,3498,3499],{"class":159},"-- ✅ Fix 1: use the same configuration in both index and query\n",[153,3501,3502,3504,3506,3508,3510,3512,3514,3516,3518],{"class":155,"line":163},[153,3503,551],{"class":166},[153,3505,720],{"class":166},[153,3507,3381],{"class":557},[153,3509,726],{"class":166},[153,3511,729],{"class":170},[153,3513,732],{"class":166},[153,3515,3390],{"class":170},[153,3517,175],{"class":174},[153,3519,3395],{"class":170},[153,3521,3522],{"class":155,"line":186},[153,3523,214],{"emptyLinePlaceholder":213},[153,3525,3526,3528,3530,3532],{"class":155,"line":192},[153,3527,167],{"class":166},[153,3529,2595],{"class":166},[153,3531,2598],{"class":166},[153,3533,1614],{"class":170},[153,3535,3536,3538,3540,3542,3544,3546,3548,3550],{"class":155,"line":198},[153,3537,1342],{"class":166},[153,3539,171],{"class":170},[153,3541,175],{"class":174},[153,3543,3425],{"class":170},[153,3545,175],{"class":174},[153,3547,25],{"class":170},[153,3549,75],{"class":174},[153,3551,183],{"class":170},[153,3553,3554],{"class":155,"line":204},[153,3555,3556],{"class":159},"-- 'database' stems to 'databas' (english), 'databases' stems to 'databas' (english) → MATCH\n",[141,3558,3559],{"language":143},[145,3560,3562],{"className":147,"code":3561,"language":143,"meta":149,"style":149},"-- ✅ Fix 2 (better): generated column — name the expression once, reuse the name\nALTER TABLE articles ADD COLUMN body_vec TSVECTOR\n  GENERATED ALWAYS AS (to_tsvector('english', body)) STORED;\nCREATE INDEX articles_body_vec_gin ON articles USING gin(body_vec);\n\nSELECT * FROM articles WHERE body_vec @@ to_tsquery('english', 'database');\n-- No expression mismatch possible — the column is the indexed expression\n",[18,3563,3564,3569,3584,3601,3619,3623,3646],{"__ignoreMap":149},[153,3565,3566],{"class":155,"line":156},[153,3567,3568],{"class":159},"-- ✅ Fix 2 (better): generated column — name the expression once, reuse the name\n",[153,3570,3571,3574,3576,3578,3581],{"class":155,"line":163},[153,3572,3573],{"class":166},"ALTER",[153,3575,554],{"class":166},[153,3577,729],{"class":170},[153,3579,3580],{"class":166},"ADD",[153,3582,3583],{"class":170}," COLUMN body_vec TSVECTOR\n",[153,3585,3586,3589,3591,3593,3596,3598],{"class":155,"line":186},[153,3587,3588],{"class":166},"  GENERATED",[153,3590,629],{"class":166},[153,3592,632],{"class":166},[153,3594,3595],{"class":170}," (to_tsvector(",[153,3597,175],{"class":174},[153,3599,3600],{"class":170},", body)) STORED;\n",[153,3602,3603,3605,3607,3610,3612,3614,3616],{"class":155,"line":192},[153,3604,551],{"class":166},[153,3606,720],{"class":166},[153,3608,3609],{"class":557}," articles_body_vec_gin",[153,3611,726],{"class":166},[153,3613,729],{"class":170},[153,3615,732],{"class":166},[153,3617,3618],{"class":170}," gin(body_vec);\n",[153,3620,3621],{"class":155,"line":198},[153,3622,214],{"emptyLinePlaceholder":213},[153,3624,3625,3627,3629,3631,3633,3635,3638,3640,3642,3644],{"class":155,"line":204},[153,3626,167],{"class":166},[153,3628,2595],{"class":166},[153,3630,2598],{"class":166},[153,3632,729],{"class":170},[153,3634,1342],{"class":166},[153,3636,3637],{"class":170}," body_vec @@ to_tsquery(",[153,3639,175],{"class":174},[153,3641,25],{"class":170},[153,3643,75],{"class":174},[153,3645,183],{"class":170},[153,3647,3648],{"class":155,"line":210},[153,3649,3650],{"class":159},"-- No expression mismatch possible — the column is the indexed expression\n",[14,3652,3653,3656,3657,3660],{},[51,3654,3655],{},"The lesson",": when indexing an expression, the query must use the ",[51,3658,3659],{},"exact same expression"," (including the text search configuration). Generated columns eliminate this class of bug by naming the expression once and reusing the name.",[31,3662],{},[34,3664,3665],{"id":3438},"Summary",[14,3667,3668,3669,2979,3671,2979,3673,3675,3676,2979,3678,3680,3681,3683,3684,3686,3687,3689],{},"You can now build full-text search with ",[18,3670,24],{},[18,3672,28],{},[18,3674,375],{},", weight and rank results with ",[18,3677,740],{},[18,3679,2001],{},", highlight with ",[18,3682,2013],{},", store vectors in generated columns with GIN indexes, and choose between FTS (natural language), ",[18,3685,2944],{}," (substring\u002Ffuzzy), and ",[18,3688,20],{}," (prefix). Next: views and materialized views.",[3691,3692,3693],"style",{},"html pre.shiki code .sdCPZ, html code.shiki .sdCPZ{--shiki-default:#6A737D;--shiki-github-dark:#6A737D}html pre.shiki code .svdQ7, html code.shiki .svdQ7{--shiki-default:#D73A49;--shiki-github-dark:#F97583}html pre.shiki code .ssxIu, html code.shiki .ssxIu{--shiki-default:#24292E;--shiki-github-dark:#E1E4E8}html pre.shiki code .sJ6F3, html code.shiki .sJ6F3{--shiki-default:#032F62;--shiki-github-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html.github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html pre.shiki code .sIsaT, html code.shiki .sIsaT{--shiki-default:#6F42C1;--shiki-github-dark:#B392F0}html pre.shiki code .snvgF, html code.shiki .snvgF{--shiki-default:#005CC5;--shiki-github-dark:#79B8FF}",{"title":149,"searchDepth":163,"depth":163,"links":3695},[3696,3697,3698,3700,3703,3704,3705,3706,3711,3712,3713,3714,3715,3716,3717],{"id":36,"depth":163,"text":37},{"id":101,"depth":163,"text":102},{"id":371,"depth":163,"text":3699},"The Match Operator: @@",{"id":528,"depth":163,"text":529,"children":3701},[3702],{"id":748,"depth":186,"text":749},{"id":904,"depth":163,"text":905},{"id":2020,"depth":163,"text":2021},{"id":2149,"depth":163,"text":2150},{"id":2366,"depth":163,"text":2367,"children":3707},[3708,3709,3710],{"id":2370,"depth":186,"text":2371},{"id":2448,"depth":186,"text":2449},{"id":2535,"depth":186,"text":2536},{"id":2575,"depth":163,"text":2576},{"id":2731,"depth":163,"text":2732},{"id":2950,"depth":163,"text":2951},{"id":3049,"depth":163,"text":3050},{"id":3170,"depth":163,"text":3171},{"id":3356,"depth":163,"text":3357},{"id":3438,"depth":163,"text":3665},"Full-text search (FTS) finds documents matching a query, ranked by relevance — far more powerful than LIKE for natural-language search. PostgreSQL has a built-in FTS engine via tsvector, tsquery, and GIN indexes.","md",{},"\u002Fsql\u002F20-full-text-search",{"title":5,"description":3718},"sql\u002F20-full-text-search","VPvIk07kp-eGrJmQJDldGCi9Fnkm-2C_CGuFF9KpgMw",1789924654854]