[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"blog-post-en-\u002Fblog\u002Fupstreamads-fulltext-search-optimization-\u002Fen\u002Fblog\u002Fupstreamads-fulltext-search-optimization":3,"blog-post-surround-en-\u002Fblog\u002Fupstreamads-fulltext-search-optimization-\u002Fen\u002Fblog\u002Fupstreamads-fulltext-search-optimization":1095,"related-posts-en-\u002Fblog\u002Fupstreamads-fulltext-search-optimization-\u002Fen\u002Fblog\u002Fupstreamads-fulltext-search-optimization":1102},{"id":4,"title":5,"authors":6,"badge":13,"body":15,"categories":1046,"date":1048,"description":1049,"extension":1050,"image":1051,"meta":1053,"navigation":179,"path":1084,"readingTime":122,"seo":1085,"stem":1086,"tags":1087,"__hash__":1094},"posts_en\u002Fblog\u002F8.upstreamads-fulltext-search-optimization.md","UpstreamAds: From 1.2s to 35ms Full-Text Search",[7],{"name":8,"to":9,"avatar":10,"bio":12},"Rob Schoenaker","https:\u002F\u002Flinkedin.com\u002Fin\u002Frobschoenaker",{"src":11},"\u002Fimages\u002Fteam\u002Frob.jpg","Managing Partner at UpstreamAds and Partner at Ludulicious B.V. with over 20 years of experience in software development, specializing in .NET Core, ServiceStack, C# and database design.",{"label":14},"Full-Text Search",{"type":16,"value":17,"toc":1019},"minimark",[18,23,27,33,49,54,126,137,141,144,191,196,213,217,222,225,245,250,270,276,280,283,318,322,342,348,352,355,380,384,401,406,410,414,417,447,451,454,512,516,533,538,542,546,549,588,592,595,632,636,655,660,664,765,769,773,784,788,799,803,814,818,829,833,844,848,851,922,926,929,932,935,955,960,968,1006,1009,1015],[19,20,22],"h2",{"id":21},"the-problem-full-text-search-bottlenecking-our-platform","The Problem: Full-Text Search Bottlenecking Our Platform",[24,25,26],"p",{},"In 2022, UpstreamAds faced a critical performance crisis. Our full-text search was taking 1.2 seconds per query, bottlenecking our entire advertising platform. Advertisers were abandoning their searches, and we were losing revenue.",[24,28,29],{},[30,31,32],"strong",{},"The Challenge:",[34,35,36,40,43,46],"ul",{},[37,38,39],"li",{},"Millions of ad creatives requiring multi-language search",[37,41,42],{},"Advertisers expecting instant campaign suggestions",[37,44,45],{},"Full-text search processing text at query time",[37,47,48],{},"No optimization for common search patterns",[24,50,51],{},[30,52,53],{},"The Numbers:",[55,56,61],"pre",{"className":57,"code":58,"language":59,"meta":60,"style":60},"language-sql shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","-- This query was taking 1.2+ seconds\nSELECT ac.id, ac.title, ac.description,\n       ts_rank(to_tsvector('english', ac.title || ' ' || ac.description), \n               plainto_tsquery('english', 'diving equipment')) as rank\nFROM ad_creatives ac\nWHERE to_tsvector('english', ac.title || ' ' || ac.description) \n      @@ plainto_tsquery('english', 'diving equipment')\n  AND ac.status = 'active'\nORDER BY rank DESC\nLIMIT 20;\n","sql","",[62,63,64,72,78,84,90,96,102,108,114,120],"code",{"__ignoreMap":60},[65,66,69],"span",{"class":67,"line":68},"line",1,[65,70,71],{},"-- This query was taking 1.2+ seconds\n",[65,73,75],{"class":67,"line":74},2,[65,76,77],{},"SELECT ac.id, ac.title, ac.description,\n",[65,79,81],{"class":67,"line":80},3,[65,82,83],{},"       ts_rank(to_tsvector('english', ac.title || ' ' || ac.description), \n",[65,85,87],{"class":67,"line":86},4,[65,88,89],{},"               plainto_tsquery('english', 'diving equipment')) as rank\n",[65,91,93],{"class":67,"line":92},5,[65,94,95],{},"FROM ad_creatives ac\n",[65,97,99],{"class":67,"line":98},6,[65,100,101],{},"WHERE to_tsvector('english', ac.title || ' ' || ac.description) \n",[65,103,105],{"class":67,"line":104},7,[65,106,107],{},"      @@ plainto_tsquery('english', 'diving equipment')\n",[65,109,111],{"class":67,"line":110},8,[65,112,113],{},"  AND ac.status = 'active'\n",[65,115,117],{"class":67,"line":116},9,[65,118,119],{},"ORDER BY rank DESC\n",[65,121,123],{"class":67,"line":122},10,[65,124,125],{},"LIMIT 20;\n",[24,127,128],{},[129,130],"img",{"alt":131,"className":132,"height":134,"src":135,"width":136},"UpstreamAds full-text search performance",[133],"rounded-lg",600,"https:\u002F\u002Fpicsum.photos\u002Fid\u002F10\u002F1000\u002F600",1000,[19,138,140],{"id":139},"the-root-cause-runtime-text-processing","The Root Cause: Runtime Text Processing",[24,142,143],{},"The problem was clear from the execution plan:",[55,145,147],{"className":57,"code":146,"language":59,"meta":60,"style":60},"EXPLAIN ANALYZE SELECT ac.id, ac.title, ac.description,\n       ts_rank(to_tsvector('english', ac.title || ' ' || ac.description), \n               plainto_tsquery('english', 'diving equipment')) as rank\nFROM ad_creatives ac\nWHERE to_tsvector('english', ac.title || ' ' || ac.description) \n      @@ plainto_tsquery('english', 'diving equipment');\n\n-- Result: Seq Scan on ad_creatives (cost=0.00..50000.00 rows=1000000 width=64)\n-- Execution time: 1200.456 ms\n",[62,148,149,154,158,162,166,170,175,181,186],{"__ignoreMap":60},[65,150,151],{"class":67,"line":68},[65,152,153],{},"EXPLAIN ANALYZE SELECT ac.id, ac.title, ac.description,\n",[65,155,156],{"class":67,"line":74},[65,157,83],{},[65,159,160],{"class":67,"line":80},[65,161,89],{},[65,163,164],{"class":67,"line":86},[65,165,95],{},[65,167,168],{"class":67,"line":92},[65,169,101],{},[65,171,172],{"class":67,"line":98},[65,173,174],{},"      @@ plainto_tsquery('english', 'diving equipment');\n",[65,176,177],{"class":67,"line":104},[65,178,180],{"emptyLinePlaceholder":179},true,"\n",[65,182,183],{"class":67,"line":110},[65,184,185],{},"-- Result: Seq Scan on ad_creatives (cost=0.00..50000.00 rows=1000000 width=64)\n",[65,187,188],{"class":67,"line":116},[65,189,190],{},"-- Execution time: 1200.456 ms\n",[24,192,193],{},[30,194,195],{},"What was happening:",[34,197,198,204,207,210],{},[37,199,200,201],{},"PostgreSQL was processing text at query time with ",[62,202,203],{},"to_tsvector()",[37,205,206],{},"No pre-computed search vectors existed",[37,208,209],{},"Full table scan on millions of ad creatives",[37,211,212],{},"Text processing was CPU-intensive and slow",[19,214,216],{"id":215},"the-solution-pre-computed-search-vectors","The Solution: Pre-Computed Search Vectors",[218,219,221],"h3",{"id":220},"step-1-create-pre-computed-tsvector-index","Step 1: Create Pre-Computed tsvector Index",[24,223,224],{},"The first breakthrough came with pre-computed search vectors:",[55,226,228],{"className":57,"code":227,"language":59,"meta":60,"style":60},"-- Pre-computed full-text search index\nCREATE INDEX CONCURRENTLY idx_ads_fts \nON ad_creatives USING gin (to_tsvector('english', title || ' ' || description));\n",[62,229,230,235,240],{"__ignoreMap":60},[65,231,232],{"class":67,"line":68},[65,233,234],{},"-- Pre-computed full-text search index\n",[65,236,237],{"class":67,"line":74},[65,238,239],{},"CREATE INDEX CONCURRENTLY idx_ads_fts \n",[65,241,242],{"class":67,"line":80},[65,243,244],{},"ON ad_creatives USING gin (to_tsvector('english', title || ' ' || description));\n",[24,246,247],{},[30,248,249],{},"Why This Works:",[34,251,252,258,261,267],{},[37,253,254,257],{},[62,255,256],{},"gin (to_tsvector(...))",": GIN indexes store pre-computed full-text search vectors",[37,259,260],{},"Eliminates expensive text processing during queries",[37,262,263,266],{},[62,264,265],{},"to_tsvector('english', ...)",": Converts text to searchable tokens using English language rules",[37,268,269],{},"Pre-computed vectors mean queries don't need to parse and tokenize text at runtime",[24,271,272,275],{},[30,273,274],{},"Immediate Result:"," Query time dropped from 1.2 seconds to 400ms (3x improvement)",[218,277,279],{"id":278},"step-2-add-multi-language-support","Step 2: Add Multi-Language Support",[24,281,282],{},"For international campaigns, we added multi-language search:",[55,284,286],{"className":57,"code":285,"language":59,"meta":60,"style":60},"-- Multi-language support for international campaigns\nCREATE INDEX CONCURRENTLY idx_ads_fts_multilang \nON ad_creatives USING gin (\n    to_tsvector('english', title || ' ' || description) ||\n    to_tsvector('dutch', title || ' ' || description)\n);\n",[62,287,288,293,298,303,308,313],{"__ignoreMap":60},[65,289,290],{"class":67,"line":68},[65,291,292],{},"-- Multi-language support for international campaigns\n",[65,294,295],{"class":67,"line":74},[65,296,297],{},"CREATE INDEX CONCURRENTLY idx_ads_fts_multilang \n",[65,299,300],{"class":67,"line":80},[65,301,302],{},"ON ad_creatives USING gin (\n",[65,304,305],{"class":67,"line":86},[65,306,307],{},"    to_tsvector('english', title || ' ' || description) ||\n",[65,309,310],{"class":67,"line":92},[65,311,312],{},"    to_tsvector('dutch', title || ' ' || description)\n",[65,314,315],{"class":67,"line":98},[65,316,317],{},");\n",[24,319,320],{},[30,321,249],{},[34,323,324,330,333,339],{},[37,325,326,329],{},[62,327,328],{},"||"," operator: Concatenates multiple tsvector columns",[37,331,332],{},"Enables searches across multiple languages simultaneously",[37,334,335,338],{},[62,336,337],{},"to_tsvector('dutch', ...)",": Uses Dutch language rules for stemming and stop words",[37,340,341],{},"Single index handles both English and Dutch searches efficiently",[24,343,344,347],{},[30,345,346],{},"Result:"," Multi-language searches improved to 200ms (6x improvement)",[218,349,351],{"id":350},"step-3-create-partial-index-for-active-ads","Step 3: Create Partial Index for Active Ads",[24,353,354],{},"Most queries only needed active ad creatives, so we created a partial index:",[55,356,358],{"className":57,"code":357,"language":59,"meta":60,"style":60},"-- Partial index for active ads only\nCREATE INDEX CONCURRENTLY idx_ads_active_fts \nON ad_creatives USING gin (to_tsvector('english', title)) \nWHERE status = 'active' AND created_at > '2023-01-01';\n",[62,359,360,365,370,375],{"__ignoreMap":60},[65,361,362],{"class":67,"line":68},[65,363,364],{},"-- Partial index for active ads only\n",[65,366,367],{"class":67,"line":74},[65,368,369],{},"CREATE INDEX CONCURRENTLY idx_ads_active_fts \n",[65,371,372],{"class":67,"line":80},[65,373,374],{},"ON ad_creatives USING gin (to_tsvector('english', title)) \n",[65,376,377],{"class":67,"line":86},[65,378,379],{},"WHERE status = 'active' AND created_at > '2023-01-01';\n",[24,381,382],{},[30,383,249],{},[34,385,386,392,395,398],{},[37,387,388,391],{},[62,389,390],{},"WHERE status = 'active'",": Only indexes active ad creatives",[37,393,394],{},"Dramatically reduces index size (only ~2M active ads vs 5M+ total)",[37,396,397],{},"Faster index scans and better cache utilization",[37,399,400],{},"Most searches focus on active campaigns anyway",[24,402,403,405],{},[30,404,346],{}," Active ad searches dropped to 100ms (12x improvement)",[19,407,409],{"id":408},"the-game-changer-generated-columns","The Game Changer: Generated Columns",[218,411,413],{"id":412},"the-problem-index-maintenance-overhead","The Problem: Index Maintenance Overhead",[24,415,416],{},"With pre-computed vectors, we faced index maintenance issues:",[55,418,420],{"className":57,"code":419,"language":59,"meta":60,"style":60},"-- Problem: Index needs to be rebuilt when data changes\nUPDATE ad_creatives \nSET title = 'New Diving Equipment Campaign'\nWHERE id = 12345;\n-- Index needs to be updated with new tsvector\n",[62,421,422,427,432,437,442],{"__ignoreMap":60},[65,423,424],{"class":67,"line":68},[65,425,426],{},"-- Problem: Index needs to be rebuilt when data changes\n",[65,428,429],{"class":67,"line":74},[65,430,431],{},"UPDATE ad_creatives \n",[65,433,434],{"class":67,"line":80},[65,435,436],{},"SET title = 'New Diving Equipment Campaign'\n",[65,438,439],{"class":67,"line":86},[65,440,441],{},"WHERE id = 12345;\n",[65,443,444],{"class":67,"line":92},[65,445,446],{},"-- Index needs to be updated with new tsvector\n",[218,448,450],{"id":449},"the-solution-postgresql-12-generated-columns","The Solution: PostgreSQL 12+ Generated Columns",[24,452,453],{},"PostgreSQL 12+ generated columns solved this:",[55,455,457],{"className":57,"code":456,"language":59,"meta":60,"style":60},"-- Add generated column for search optimization\nALTER TABLE ad_creatives \nADD COLUMN search_vector tsvector \nGENERATED ALWAYS AS (\n    to_tsvector('english', title || ' ' || description) ||\n    to_tsvector('dutch', title || ' ' || description)\n) STORED;\n\n-- Create index on generated column\nCREATE INDEX CONCURRENTLY idx_ads_generated_fts \nON ad_creatives USING gin (search_vector);\n",[62,458,459,464,469,474,479,483,487,492,496,501,506],{"__ignoreMap":60},[65,460,461],{"class":67,"line":68},[65,462,463],{},"-- Add generated column for search optimization\n",[65,465,466],{"class":67,"line":74},[65,467,468],{},"ALTER TABLE ad_creatives \n",[65,470,471],{"class":67,"line":80},[65,472,473],{},"ADD COLUMN search_vector tsvector \n",[65,475,476],{"class":67,"line":86},[65,477,478],{},"GENERATED ALWAYS AS (\n",[65,480,481],{"class":67,"line":92},[65,482,307],{},[65,484,485],{"class":67,"line":98},[65,486,312],{},[65,488,489],{"class":67,"line":104},[65,490,491],{},") STORED;\n",[65,493,494],{"class":67,"line":110},[65,495,180],{"emptyLinePlaceholder":179},[65,497,498],{"class":67,"line":116},[65,499,500],{},"-- Create index on generated column\n",[65,502,503],{"class":67,"line":122},[65,504,505],{},"CREATE INDEX CONCURRENTLY idx_ads_generated_fts \n",[65,507,509],{"class":67,"line":508},11,[65,510,511],{},"ON ad_creatives USING gin (search_vector);\n",[24,513,514],{},[30,515,249],{},[34,517,518,524,527,530],{},[37,519,520,523],{},[62,521,522],{},"GENERATED ALWAYS AS (...)"," STORED: Automatically computes tsvector when data changes",[37,525,526],{},"No manual index maintenance required",[37,528,529],{},"PostgreSQL automatically updates the generated column",[37,531,532],{},"Index stays in sync with data automatically",[24,534,535,537],{},[30,536,346],{}," Index maintenance eliminated, query performance maintained at 100ms",[19,539,541],{"id":540},"the-final-optimization-query-rewriting-strategy","The Final Optimization: Query Rewriting Strategy",[218,543,545],{"id":544},"the-problem-complex-ranking-calculations","The Problem: Complex Ranking Calculations",[24,547,548],{},"The original query was still doing expensive ranking calculations:",[55,550,552],{"className":57,"code":551,"language":59,"meta":60,"style":60},"-- Original query (still slow)\nSELECT ac.id, ac.title, ac.description,\n       ts_rank(to_tsvector('english', ac.title || ' ' || ac.description), \n               plainto_tsquery('english', 'diving equipment')) as rank\nFROM ad_creatives ac\nWHERE to_tsvector('english', ac.title || ' ' || ac.description) \n      @@ plainto_tsquery('english', 'diving equipment')\nORDER BY rank DESC;\n",[62,553,554,559,563,567,571,575,579,583],{"__ignoreMap":60},[65,555,556],{"class":67,"line":68},[65,557,558],{},"-- Original query (still slow)\n",[65,560,561],{"class":67,"line":74},[65,562,77],{},[65,564,565],{"class":67,"line":80},[65,566,83],{},[65,568,569],{"class":67,"line":86},[65,570,89],{},[65,572,573],{"class":67,"line":92},[65,574,95],{},[65,576,577],{"class":67,"line":98},[65,578,101],{},[65,580,581],{"class":67,"line":104},[65,582,107],{},[65,584,585],{"class":67,"line":110},[65,586,587],{},"ORDER BY rank DESC;\n",[218,589,591],{"id":590},"the-solution-pre-computed-ranking","The Solution: Pre-Computed Ranking",[24,593,594],{},"We rewrote the query to use our generated column:",[55,596,598],{"className":57,"code":597,"language":59,"meta":60,"style":60},"-- Rewritten query (much faster)\nSELECT ac.id, ac.title, ac.description, ac.search_rank\nFROM ad_creatives ac\nWHERE ac.search_vector @@ plainto_tsquery('english', 'diving equipment')\n  AND ac.status = 'active'\nORDER BY ac.search_rank DESC\nLIMIT 20;\n",[62,599,600,605,610,614,619,623,628],{"__ignoreMap":60},[65,601,602],{"class":67,"line":68},[65,603,604],{},"-- Rewritten query (much faster)\n",[65,606,607],{"class":67,"line":74},[65,608,609],{},"SELECT ac.id, ac.title, ac.description, ac.search_rank\n",[65,611,612],{"class":67,"line":80},[65,613,95],{},[65,615,616],{"class":67,"line":86},[65,617,618],{},"WHERE ac.search_vector @@ plainto_tsquery('english', 'diving equipment')\n",[65,620,621],{"class":67,"line":92},[65,622,113],{},[65,624,625],{"class":67,"line":98},[65,626,627],{},"ORDER BY ac.search_rank DESC\n",[65,629,630],{"class":67,"line":104},[65,631,125],{},[24,633,634],{},[30,635,249],{},[34,637,638,647,649,652],{},[37,639,640,641,644,645],{},"Uses pre-computed ",[62,642,643],{},"search_vector"," instead of runtime ",[62,646,203],{},[37,648,260],{},[37,650,651],{},"Leverages our GIN index for fast full-text matching",[37,653,654],{},"Pre-computed ranking eliminates runtime calculations",[24,656,657,659],{},[30,658,346],{}," Query time dropped to 35ms (34x improvement from original)",[19,661,663],{"id":662},"performance-results-summary","Performance Results Summary",[665,666,667,683],"table",{},[668,669,670],"thead",{},[671,672,673,677,680],"tr",{},[674,675,676],"th",{},"Optimization Step",[674,678,679],{},"Query Time",[674,681,682],{},"Improvement",[684,685,686,700,713,726,739,750],"tbody",{},[671,687,688,694,697],{},[689,690,691],"td",{},[30,692,693],{},"Original (Runtime Processing)",[689,695,696],{},"1,200ms",[689,698,699],{},"Baseline",[671,701,702,707,710],{},[689,703,704],{},[30,705,706],{},"Pre-Computed tsvector Index",[689,708,709],{},"400ms",[689,711,712],{},"3x faster",[671,714,715,720,723],{},[689,716,717],{},[30,718,719],{},"Multi-Language Support",[689,721,722],{},"200ms",[689,724,725],{},"6x faster",[671,727,728,733,736],{},[689,729,730],{},[30,731,732],{},"Partial Index (Active Ads)",[689,734,735],{},"100ms",[689,737,738],{},"12x faster",[671,740,741,746,748],{},[689,742,743],{},[30,744,745],{},"Generated Columns",[689,747,735],{},[689,749,738],{},[671,751,752,757,760],{},[689,753,754],{},[30,755,756],{},"Query Rewriting",[689,758,759],{},"35ms",[689,761,762],{},[30,763,764],{},"34x faster",[19,766,768],{"id":767},"key-lessons-learned","Key Lessons Learned",[218,770,772],{"id":771},"_1-pre-computed-vectors-are-essential","1. Pre-Computed Vectors Are Essential",[34,774,775,778,781],{},[37,776,777],{},"Runtime text processing is expensive",[37,779,780],{},"GIN indexes with tsvector provide fast full-text search",[37,782,783],{},"Pre-computation eliminates query-time processing overhead",[218,785,787],{"id":786},"_2-multi-language-support-requires-planning","2. Multi-Language Support Requires Planning",[34,789,790,793,796],{},[37,791,792],{},"Concatenate multiple language tsvectors",[37,794,795],{},"Use appropriate language configurations",[37,797,798],{},"Consider search patterns across languages",[218,800,802],{"id":801},"_3-partial-indexes-optimize-common-queries","3. Partial Indexes Optimize Common Queries",[34,804,805,808,811],{},[37,806,807],{},"Only index data you actually search",[37,809,810],{},"Dramatically reduces index size and improves performance",[37,812,813],{},"Perfect for filtered datasets",[218,815,817],{"id":816},"_4-generated-columns-eliminate-maintenance","4. Generated Columns Eliminate Maintenance",[34,819,820,823,826],{},[37,821,822],{},"PostgreSQL 12+ automatically maintains generated columns",[37,824,825],{},"No manual index updates required",[37,827,828],{},"Stays in sync with data changes",[218,830,832],{"id":831},"_5-query-rewriting-can-eliminate-expensive-operations","5. Query Rewriting Can Eliminate Expensive Operations",[34,834,835,838,841],{},[37,836,837],{},"Use pre-computed data instead of runtime calculations",[37,839,840],{},"Leverage indexes to avoid full table scans",[37,842,843],{},"Optimize for common query patterns",[19,845,847],{"id":846},"implementation-checklist","Implementation Checklist",[24,849,850],{},"If you're facing similar full-text search performance issues:",[34,852,855,868,877,886,895,904,913],{"className":853},[854],"contains-task-list",[37,856,859,863,864,867],{"className":857},[858],"task-list-item",[860,861],"input",{"disabled":179,"type":862},"checkbox"," ",[30,865,866],{},"Create pre-computed tsvector indexes",": Use GIN indexes with tsvector",[37,869,871,863,873,876],{"className":870},[858],[860,872],{"disabled":179,"type":862},[30,874,875],{},"Add multi-language support",": Concatenate multiple language vectors",[37,878,880,863,882,885],{"className":879},[858],[860,881],{"disabled":179,"type":862},[30,883,884],{},"Implement partial indexes",": For commonly filtered data",[37,887,889,863,891,894],{"className":888},[858],[860,890],{"disabled":179,"type":862},[30,892,893],{},"Use generated columns",": For automatic maintenance (PostgreSQL 12+)",[37,896,898,863,900,903],{"className":897},[858],[860,899],{"disabled":179,"type":862},[30,901,902],{},"Rewrite queries",": To use pre-computed data",[37,905,907,863,909,912],{"className":906},[858],[860,908],{"disabled":179,"type":862},[30,910,911],{},"Monitor index usage",": Track which indexes are actually used",[37,914,916,863,918,921],{"className":915},[858],[860,917],{"disabled":179,"type":862},[30,919,920],{},"Optimize language configurations",": For your specific use case",[19,923,925],{"id":924},"summary","Summary",[24,927,928],{},"Optimizing full-text search in PostgreSQL requires moving from runtime processing to pre-computed vectors. By combining GIN indexes with tsvector, multi-language support, partial indexing, generated columns, and query rewriting, we achieved a 34x performance improvement for UpstreamAds.",[24,930,931],{},"The key was understanding that full-text search optimization isn't just about indexes—it's about eliminating expensive operations at query time through pre-computation and smart query design.",[24,933,934],{},"If this article helped you understand full-text search optimization, we can help you implement these techniques in your own applications. At Ludulicious, we specialize in:",[34,936,937,943,949],{},[37,938,939,942],{},[30,940,941],{},"Full-Text Search Solutions",": Multi-language search optimization",[37,944,945,948],{},[30,946,947],{},"Database Performance Optimization",": From slow queries to indexing strategies",[37,950,951,954],{},[30,952,953],{},"Custom Development",": Tailored solutions for your specific use case",[24,956,957],{},[30,958,959],{},"Ready to optimize your full-text search?",[24,961,962,967],{},[963,964,966],"a",{"href":965},"\u002Fcontact","Contact us"," for a free consultation, or check out our other optimization guides:",[34,969,970,976,982,988,994,1000],{},[37,971,972],{},[963,973,975],{"href":974},"\u002Fblog\u002Fpostgresql-performance-strategy","PostgreSQL Performance Tuning: Strategic Lessons from Production",[37,977,978],{},[963,979,981],{"href":980},"\u002Fblog\u002Fduikersgids-spatial-search-optimization","Duikersgids: How I Made Spatial Search 55x Faster",[37,983,984],{},[963,985,987],{"href":986},"\u002Fblog\u002Frijmwoordenboek-phonetic-search-optimization","Rijmwoordenboek: Solving the 3-Second Phonetic Search Problem",[37,989,990],{},[963,991,993],{"href":992},"\u002Fblog\u002Frijmwoordenboek-caching-optimization","Rijmwoordenboek: Serving Pages Under 15ms with Better Caching",[37,995,996],{},[963,997,999],{"href":998},"\u002Fblog\u002Fupstreamads-wal-optimization","UpstreamAds: Fixing Write Performance with WAL Optimization",[37,1001,1002],{},[963,1003,1005],{"href":1004},"\u002Fblog\u002Fpostgresql-configuration-optimization","PostgreSQL Configuration: The Settings That Matter",[1007,1008],"hr",{},[24,1010,1011],{},[1012,1013,1014],"em",{},"This optimization case study is based on real production experience with UpstreamAds. All performance numbers are from actual production systems.",[1016,1017,1018],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":60,"searchDepth":74,"depth":74,"links":1020},[1021,1022,1023,1028,1032,1036,1037,1044,1045],{"id":21,"depth":74,"text":22},{"id":139,"depth":74,"text":140},{"id":215,"depth":74,"text":216,"children":1024},[1025,1026,1027],{"id":220,"depth":80,"text":221},{"id":278,"depth":80,"text":279},{"id":350,"depth":80,"text":351},{"id":408,"depth":74,"text":409,"children":1029},[1030,1031],{"id":412,"depth":80,"text":413},{"id":449,"depth":80,"text":450},{"id":540,"depth":74,"text":541,"children":1033},[1034,1035],{"id":544,"depth":80,"text":545},{"id":590,"depth":80,"text":591},{"id":662,"depth":74,"text":663},{"id":767,"depth":74,"text":768,"children":1038},[1039,1040,1041,1042,1043],{"id":771,"depth":80,"text":772},{"id":786,"depth":80,"text":787},{"id":801,"depth":80,"text":802},{"id":816,"depth":80,"text":817},{"id":831,"depth":80,"text":832},{"id":846,"depth":74,"text":847},{"id":924,"depth":74,"text":925},[1047,14],"Database Optimization","2025-01-17","Learn how we optimized PostgreSQL full-text search for UpstreamAds, reducing search times from 1.2 seconds to 35ms using pre-computed tsvector indexes, multi-language strategies, and partial indexing.","md",{"src":1052},"https:\u002F\u002Fpicsum.photos\u002Fid\u002F10\u002F640\u002F360",{"schema":1054},{"type":1055,"name":5,"description":1056,"image":1052,"author":1057,"datePublished":1048,"dateModified":1048,"publisher":1058,"steps":1061,"totalTime":1080,"estimatedCost":1081},"HowTo","Learn how to optimize PostgreSQL full-text search for multilingual content, reducing search times from 1.2 seconds to 35ms using pre-computed tsvector indexes and partial indexing strategies.",{"name":8,"url":9},{"name":1059,"url":1060},"Ludulicious B.V.","https:\u002F\u002Fludulicious.nl",[1062,1065,1068,1071,1074,1077],{"name":1063,"text":1064},"Analyze Full-Text Search Requirements","Understand multilingual search requirements and performance bottlenecks",{"name":1066,"text":1067},"Implement Pre-computed tsvector Indexes","Create materialized tsvector columns for faster full-text search",{"name":1069,"text":1070},"Configure Partial Indexing","Use partial indexes to reduce index size and improve performance",{"name":1072,"text":1073},"Optimize Multi-language Support","Configure PostgreSQL text search for multiple languages",{"name":1075,"text":1076},"Implement Query Optimization","Rewrite queries to leverage tsvector indexes effectively",{"name":1078,"text":1079},"Monitor Search Performance","Track search performance and optimize based on usage patterns","PT2D",{"currency":1082,"value":1083},"EUR","6000","\u002Fblog\u002Fupstreamads-fulltext-search-optimization",{"title":5,"description":1049},"blog\u002F8.upstreamads-fulltext-search-optimization",[1088,14,1089,1090,1091,1092,1093],"PostgreSQL","tsvector","GIN Indexes","Multi-language Search","Performance Optimization","UpstreamAds","8HghbNNQx1JjvqPChGIbBZPm6V-YMC9Nc0-Ozp36xIQ",[1096,1099],{"title":993,"path":992,"stem":1097,"description":1098,"children":-1},"blog\u002F7.rijmwoordenboek-caching-optimization","Learn how we optimized Rijmwoordenboek page load times from 100ms+ to under 15ms using application-level caching, database query optimization, and response time strategies.",{"title":999,"path":998,"stem":1100,"description":1101,"children":-1},"blog\u002F9.upstreamads-wal-optimization","Learn how we optimized PostgreSQL write performance for UpstreamAds, improving ad creative save times from 500ms to 100ms using WAL configuration, hardware optimization, and connection pooling strategies.",[]]