[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"header-categories":3,"post-full-text-search-without-elasticsearch":73,"parsed-post-b4f7b827-eb04-45d0-ac3f-e5e691d391e3":148},{"success":4,"data":5},true,{"items":6,"pagination":70},[7,16,24,32,40,48,56,63],{"id":8,"name":9,"description":10,"parentId":11,"createdAt":12,"updatedAt":12,"parent":11,"children":13,"_count":14},"ai-future","AI & The Future","Artificial intelligence and emerging technology",null,"2026-07-24T06:02:45.990Z",[],{"posts":15},1,{"id":17,"name":18,"description":19,"parentId":11,"createdAt":20,"updatedAt":20,"parent":11,"children":21,"_count":22},"tools-productivity","Tools & Productivity","Developer tools and workflow optimization","2026-07-24T06:02:45.712Z",[],{"posts":23},7,{"id":25,"name":26,"description":27,"parentId":11,"createdAt":28,"updatedAt":28,"parent":11,"children":29,"_count":30},"career","Career & Growth","Professional development for developers","2026-07-24T06:02:45.486Z",[],{"posts":31},3,{"id":33,"name":34,"description":35,"parentId":11,"createdAt":36,"updatedAt":36,"parent":11,"children":37,"_count":38},"opinion","Opinion & Analysis","Industry analysis and technical opinions","2026-07-24T06:02:45.272Z",[],{"posts":39},6,{"id":41,"name":42,"description":43,"parentId":11,"createdAt":44,"updatedAt":44,"parent":11,"children":45,"_count":46},"tutorials","Tutorials","Step-by-step learning guides","2026-07-24T06:02:44.965Z",[],{"posts":47},21,{"id":49,"name":50,"description":51,"parentId":11,"createdAt":52,"updatedAt":52,"parent":11,"children":53,"_count":54},"software-engineering","Software Engineering","Best practices, patterns, and principles","2026-07-24T06:02:44.689Z",[],{"posts":55},24,{"id":57,"name":58,"description":59,"parentId":11,"createdAt":60,"updatedAt":60,"parent":11,"children":61,"_count":62},"devops-cloud","DevOps & Cloud","Infrastructure, deployment, and cloud computing","2026-07-24T06:02:44.453Z",[],{"posts":39},{"id":64,"name":65,"description":66,"parentId":11,"createdAt":67,"updatedAt":67,"parent":11,"children":68,"_count":69},"web-development","Web Development","Frontend and backend web development tutorials and guides","2026-07-24T06:02:44.169Z",[],{"posts":55},{"page":15,"limit":71,"total":71,"totalPages":15,"hasNext":72,"hasPrev":72},8,false,{"success":4,"data":74},{"id":75,"slug":76,"title":77,"excerpt":78,"content":79,"status":80,"visibility":81,"featuredImage":82,"canonicalUrl":11,"readingTime":83,"viewCount":84,"commentEnabled":4,"publishedAt":85,"scheduledAt":11,"createdAt":86,"updatedAt":87,"seoTitle":88,"seoDescription":78,"seoKeywords":11,"authorId":89,"author":90,"coAuthors":95,"tags":96,"categories":112,"comments":115,"_count":116,"relatedPosts":118},"b4f7b827-eb04-45d0-ac3f-e5e691d391e3","full-text-search-without-elasticsearch","Implementing Full-Text Search Without Elasticsearch","You don't always need a dedicated search engine. Implement performant full-text search with PostgreSQL's built-in capabilities.","## When PostgreSQL Search Is Enough\n\nElasticsearch is powerful but adds operational complexity. For most applications under 10M documents, PostgreSQL's built-in full-text search is more than sufficient.\n\n## Setting Up tsvector\n\n```sql\n-- Add a search vector column\nALTER TABLE posts ADD COLUMN search_vector tsvector;\n\n-- Populate it with weighted content\nUPDATE posts SET search_vector =\n  setweight(to_tsvector('english', coalesce(title, '')), 'A') ||\n  setweight(to_tsvector('english', coalesce(excerpt, '')), 'B') ||\n  setweight(to_tsvector('english', coalesce(content, '')), 'C');\n\n-- Create a GIN index for fast lookups\nCREATE INDEX idx_posts_search ON posts USING GIN (search_vector);\n```\n\nWeights (A > B > C > D) control relevance ranking — title matches score higher than body matches.\n\n## Auto-Update with Triggers\n\n```sql\nCREATE OR REPLACE FUNCTION update_post_search_vector()\nRETURNS trigger AS $$\nBEGIN\n  NEW.search_vector :=\n    setweight(to_tsvector('english', coalesce(NEW.title, '')), 'A') ||\n    setweight(to_tsvector('english', coalesce(NEW.excerpt, '')), 'B') ||\n    setweight(to_tsvector('english', coalesce(NEW.content, '')), 'C');\n  RETURN NEW;\nEND;\n$$ LANGUAGE plpgsql;\n\nCREATE TRIGGER posts_search_vector_update\n  BEFORE INSERT OR UPDATE ON posts\n  FOR EACH ROW\n  EXECUTE FUNCTION update_post_search_vector();\n```\n\n## Querying\n\n```sql\n-- Basic search\nSELECT title, ts_rank(search_vector, query) AS rank\nFROM posts, to_tsquery('english', 'typescript & vue') AS query\nWHERE search_vector @@ query\nORDER BY rank DESC\nLIMIT 20;\n\n-- With prefix matching (for autocomplete)\nSELECT title\nFROM posts\nWHERE search_vector @@ to_tsquery('english', 'type:*')\nLIMIT 5;\n```\n\n## Integration with Prisma\n\n```typescript\n\u002F\u002F server\u002Fapi\u002Fsearch.get.ts\nexport default defineEventHandler(async (event) => {\n  const { q, page = 1, limit = 20 } = getQuery(event)\n  if (!q || typeof q !== 'string') return { data: [] }\n\n  \u002F\u002F Convert user query to tsquery format\n  const searchTerms = q.trim().split(\u002F\\s+\u002F).join(' & ')\n\n  const posts = await prisma.$queryRaw`\n    SELECT id, title, slug, excerpt,\n           ts_rank(search_vector, to_tsquery('english', ${searchTerms})) as rank,\n           ts_headline('english', content, to_tsquery('english', ${searchTerms}),\n             'StartSel=\u003Cmark>, StopSel=\u003C\u002Fmark>, MaxWords=50') as highlight\n    FROM posts\n    WHERE search_vector @@ to_tsquery('english', ${searchTerms})\n      AND status = 'published'\n    ORDER BY rank DESC\n    LIMIT ${limit}\n    OFFSET ${(page - 1) * limit}\n  `\n\n  return { data: posts }\n})\n```\n\n::callout{icon=\"i-lucide-search\" color=\"success\"}\n`ts_headline` generates highlighted snippets showing where the match occurred — perfect for search result previews.\n::\n\n## Fuzzy Matching with pg_trgm\n\nFor typo tolerance, add trigram similarity:\n\n```sql\nCREATE EXTENSION IF NOT EXISTS pg_trgm;\nCREATE INDEX idx_posts_title_trgm ON posts USING GIN (title gin_trgm_ops);\n\n-- Find posts with similar titles (typo-tolerant)\nSELECT title, similarity(title, 'javscript tutoral') AS sim\nFROM posts\nWHERE similarity(title, 'javscript tutoral') > 0.3\nORDER BY sim DESC;\n```\n\n## Performance at Scale\n\nOn a table with 1M rows:\n- GIN-indexed tsvector queries: **2-10ms**\n- Without index (seq scan): **500-2000ms**\n- Trigram similarity: **10-50ms** with GIN index\n\n## When to Upgrade to Elasticsearch\u002FTypesense\n\n- 10M+ documents\n- Complex faceted search\n- Real-time indexing with sub-second visibility\n- Multi-language search in the same index\n- Geospatial queries combined with text search\n\nFor everything else, PostgreSQL's built-in search is surprisingly capable and zero extra infrastructure.\n","published","public","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1580894894513-541e068a3e2b?w=1200&q=80",9,454,"2026-04-26T06:04:31.080Z","2026-07-24T06:04:31.083Z","2026-07-28T12:21:22.250Z","Implementing Full-Text Search Without Elasticsearch | BitBlog","fddb5d93-7a2c-4d86-a06a-fa32e73a01c6",{"email":91,"bio":92,"id":89,"name":93,"avatarUrl":94},"mbeahessilfieprince@gmail.com","Fullstack Software Developer ","Mbeah Essilfie","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F93322394?v=4",[],[97,102,107],{"id":98,"name":99,"color":100,"description":101},"prisma","Prisma","#2d3748","Next-generation Node.js ORM",{"id":103,"name":104,"color":105,"description":106},"performance","Performance","#e535ab","Web performance optimization",{"id":108,"name":109,"color":110,"description":111},"database","Database","#336791","Database design and management",[113,114],{"id":64,"name":65,"description":66},{"id":41,"name":42,"description":43},[],{"comments":117},0,[119,128,138],{"id":120,"slug":121,"title":122,"excerpt":123,"featuredImage":124,"viewCount":125,"readingTime":71,"publishedAt":126,"author":127},"2997028f-4d22-4eb7-9d86-894a54cb559a","building-type-safe-apis-nuxt3-prisma","Building Type-Safe APIs with Nuxt 3 and Prisma","Learn how to build end-to-end type-safe APIs using Nuxt 3 server routes and Prisma ORM for a seamless developer experience.","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1498050108023-c5249f4df085?w=1200&q=80",1920,"2026-07-23T06:02:46.910Z",{"id":89,"name":93,"avatarUrl":94},{"id":129,"slug":130,"title":131,"excerpt":132,"featuredImage":133,"viewCount":134,"readingTime":135,"publishedAt":136,"author":137},"da9e4553-8b0d-411e-b4ec-f9684017e163","mastering-tailwind-css-design-systems","Mastering Tailwind CSS: From Utility Classes to Design Systems","Go beyond basic utility classes and learn how to build cohesive, maintainable design systems with Tailwind CSS.","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1555066931-4365d14bab8c?w=1200&q=80",256,10,"2026-07-21T06:02:49.268Z",{"id":89,"name":93,"avatarUrl":94},{"id":139,"slug":140,"title":141,"excerpt":142,"featuredImage":143,"viewCount":144,"readingTime":145,"publishedAt":146,"author":147},"50add5fc-4026-4267-b917-7f71ea9e35b0","complete-guide-vue3-composables","The Complete Guide to Vue 3 Composables","Understand the power of Vue 3's Composition API through practical, real-world composable patterns that you can use today.","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1517694712202-14dd9538aa97?w=1200&q=80",989,12,"2026-07-19T06:02:51.753Z",{"id":89,"name":93,"avatarUrl":94},{"data":149,"body":151,"toc":366},{"title":150,"description":150},"",{"type":152,"children":153},"root",[154,163,169,175,188,193,199,207,213,221,227,237,254,260,265,273,279,284,322,328,356,361],{"type":155,"tag":156,"props":157,"children":159},"element","h2",{"id":158},"when-postgresql-search-is-enough",[160],{"type":161,"value":162},"text","When PostgreSQL Search Is Enough",{"type":155,"tag":164,"props":165,"children":166},"p",{},[167],{"type":161,"value":168},"Elasticsearch is powerful but adds operational complexity. For most applications under 10M documents, PostgreSQL's built-in full-text search is more than sufficient.",{"type":155,"tag":156,"props":170,"children":172},{"id":171},"setting-up-tsvector",[173],{"type":161,"value":174},"Setting Up tsvector",{"type":155,"tag":176,"props":177,"children":182},"pre",{"className":178,"code":179,"language":180,"meta":150,"style":181},"language-sql","-- Add a search vector column\nALTER TABLE posts ADD COLUMN search_vector tsvector;\n\n-- Populate it with weighted content\nUPDATE posts SET search_vector =\n  setweight(to_tsvector('english', coalesce(title, '')), 'A') ||\n  setweight(to_tsvector('english', coalesce(excerpt, '')), 'B') ||\n  setweight(to_tsvector('english', coalesce(content, '')), 'C');\n\n-- Create a GIN index for fast lookups\nCREATE INDEX idx_posts_search ON posts USING GIN (search_vector);\n","sql","undefined",[183],{"type":155,"tag":184,"props":185,"children":186},"code",{"__ignoreMap":150},[187],{"type":161,"value":179},{"type":155,"tag":164,"props":189,"children":190},{},[191],{"type":161,"value":192},"Weights (A > B > C > D) control relevance ranking — title matches score higher than body matches.",{"type":155,"tag":156,"props":194,"children":196},{"id":195},"auto-update-with-triggers",[197],{"type":161,"value":198},"Auto-Update with Triggers",{"type":155,"tag":176,"props":200,"children":202},{"className":178,"code":201,"language":180,"meta":150,"style":181},"CREATE OR REPLACE FUNCTION update_post_search_vector()\nRETURNS trigger AS $$\nBEGIN\n  NEW.search_vector :=\n    setweight(to_tsvector('english', coalesce(NEW.title, '')), 'A') ||\n    setweight(to_tsvector('english', coalesce(NEW.excerpt, '')), 'B') ||\n    setweight(to_tsvector('english', coalesce(NEW.content, '')), 'C');\n  RETURN NEW;\nEND;\n$$ LANGUAGE plpgsql;\n\nCREATE TRIGGER posts_search_vector_update\n  BEFORE INSERT OR UPDATE ON posts\n  FOR EACH ROW\n  EXECUTE FUNCTION update_post_search_vector();\n",[203],{"type":155,"tag":184,"props":204,"children":205},{"__ignoreMap":150},[206],{"type":161,"value":201},{"type":155,"tag":156,"props":208,"children":210},{"id":209},"querying",[211],{"type":161,"value":212},"Querying",{"type":155,"tag":176,"props":214,"children":216},{"className":178,"code":215,"language":180,"meta":150,"style":181},"-- Basic search\nSELECT title, ts_rank(search_vector, query) AS rank\nFROM posts, to_tsquery('english', 'typescript & vue') AS query\nWHERE search_vector @@ query\nORDER BY rank DESC\nLIMIT 20;\n\n-- With prefix matching (for autocomplete)\nSELECT title\nFROM posts\nWHERE search_vector @@ to_tsquery('english', 'type:*')\nLIMIT 5;\n",[217],{"type":155,"tag":184,"props":218,"children":219},{"__ignoreMap":150},[220],{"type":161,"value":215},{"type":155,"tag":156,"props":222,"children":224},{"id":223},"integration-with-prisma",[225],{"type":161,"value":226},"Integration with Prisma",{"type":155,"tag":176,"props":228,"children":232},{"className":229,"code":230,"language":231,"meta":150,"style":181},"language-typescript","\u002F\u002F server\u002Fapi\u002Fsearch.get.ts\nexport default defineEventHandler(async (event) => {\n  const { q, page = 1, limit = 20 } = getQuery(event)\n  if (!q || typeof q !== 'string') return { data: [] }\n\n  \u002F\u002F Convert user query to tsquery format\n  const searchTerms = q.trim().split(\u002F\\s+\u002F).join(' & ')\n\n  const posts = await prisma.$queryRaw`\n    SELECT id, title, slug, excerpt,\n           ts_rank(search_vector, to_tsquery('english', ${searchTerms})) as rank,\n           ts_headline('english', content, to_tsquery('english', ${searchTerms}),\n             'StartSel=\u003Cmark>, StopSel=\u003C\u002Fmark>, MaxWords=50') as highlight\n    FROM posts\n    WHERE search_vector @@ to_tsquery('english', ${searchTerms})\n      AND status = 'published'\n    ORDER BY rank DESC\n    LIMIT ${limit}\n    OFFSET ${(page - 1) * limit}\n  `\n\n  return { data: posts }\n})\n","typescript",[233],{"type":155,"tag":184,"props":234,"children":235},{"__ignoreMap":150},[236],{"type":161,"value":230},{"type":155,"tag":238,"props":239,"children":242},"callout",{"color":240,"icon":241},"success","i-lucide-search",[243],{"type":155,"tag":164,"props":244,"children":245},{},[246,252],{"type":155,"tag":184,"props":247,"children":249},{"className":248},[],[250],{"type":161,"value":251},"ts_headline",{"type":161,"value":253}," generates highlighted snippets showing where the match occurred — perfect for search result previews.",{"type":155,"tag":156,"props":255,"children":257},{"id":256},"fuzzy-matching-with-pg_trgm",[258],{"type":161,"value":259},"Fuzzy Matching with pg_trgm",{"type":155,"tag":164,"props":261,"children":262},{},[263],{"type":161,"value":264},"For typo tolerance, add trigram similarity:",{"type":155,"tag":176,"props":266,"children":268},{"className":178,"code":267,"language":180,"meta":150,"style":181},"CREATE EXTENSION IF NOT EXISTS pg_trgm;\nCREATE INDEX idx_posts_title_trgm ON posts USING GIN (title gin_trgm_ops);\n\n-- Find posts with similar titles (typo-tolerant)\nSELECT title, similarity(title, 'javscript tutoral') AS sim\nFROM posts\nWHERE similarity(title, 'javscript tutoral') > 0.3\nORDER BY sim DESC;\n",[269],{"type":155,"tag":184,"props":270,"children":271},{"__ignoreMap":150},[272],{"type":161,"value":267},{"type":155,"tag":156,"props":274,"children":276},{"id":275},"performance-at-scale",[277],{"type":161,"value":278},"Performance at Scale",{"type":155,"tag":164,"props":280,"children":281},{},[282],{"type":161,"value":283},"On a table with 1M rows:",{"type":155,"tag":285,"props":286,"children":287},"ul",{},[288,300,310],{"type":155,"tag":289,"props":290,"children":291},"li",{},[292,294],{"type":161,"value":293},"GIN-indexed tsvector queries: ",{"type":155,"tag":295,"props":296,"children":297},"strong",{},[298],{"type":161,"value":299},"2-10ms",{"type":155,"tag":289,"props":301,"children":302},{},[303,305],{"type":161,"value":304},"Without index (seq scan): ",{"type":155,"tag":295,"props":306,"children":307},{},[308],{"type":161,"value":309},"500-2000ms",{"type":155,"tag":289,"props":311,"children":312},{},[313,315,320],{"type":161,"value":314},"Trigram similarity: ",{"type":155,"tag":295,"props":316,"children":317},{},[318],{"type":161,"value":319},"10-50ms",{"type":161,"value":321}," with GIN index",{"type":155,"tag":156,"props":323,"children":325},{"id":324},"when-to-upgrade-to-elasticsearchtypesense",[326],{"type":161,"value":327},"When to Upgrade to Elasticsearch\u002FTypesense",{"type":155,"tag":285,"props":329,"children":330},{},[331,336,341,346,351],{"type":155,"tag":289,"props":332,"children":333},{},[334],{"type":161,"value":335},"10M+ documents",{"type":155,"tag":289,"props":337,"children":338},{},[339],{"type":161,"value":340},"Complex faceted search",{"type":155,"tag":289,"props":342,"children":343},{},[344],{"type":161,"value":345},"Real-time indexing with sub-second visibility",{"type":155,"tag":289,"props":347,"children":348},{},[349],{"type":161,"value":350},"Multi-language search in the same index",{"type":155,"tag":289,"props":352,"children":353},{},[354],{"type":161,"value":355},"Geospatial queries combined with text search",{"type":155,"tag":164,"props":357,"children":358},{},[359],{"type":161,"value":360},"For everything else, PostgreSQL's built-in search is surprisingly capable and zero extra infrastructure.",{"type":155,"tag":362,"props":363,"children":364},"style",{},[365],{"type":161,"value":150},{"title":150,"searchDepth":367,"depth":367,"links":368},2,[369,370,371,372,373,374,375,376],{"id":158,"depth":367,"text":162},{"id":171,"depth":367,"text":174},{"id":195,"depth":367,"text":198},{"id":209,"depth":367,"text":212},{"id":223,"depth":367,"text":226},{"id":256,"depth":367,"text":259},{"id":275,"depth":367,"text":278},{"id":324,"depth":367,"text":327}]