George Ongoro.

Insights, engineering, and storytelling. Exploring the intersection of technology and creativity to build the future of the web.

Navigation

Home FeedFor YouAboutContactRSS FeedUse my articles on your site

Legal

Privacy PolicyTerms of ServiceAdmin Portal

Stay Updated

Get the latest engineering insights delivered to your inbox.

© 2026 George Ongoro. All rights reserved.

System Online
    Homebackend-apis

    Designing Search for Blogs (Beyond LIKE Queries)

    December 23, 202510 min read
    backend-apis
    Designing Search for Blogs (Beyond LIKE Queries)
    Cover image for Designing Search for Blogs (Beyond LIKE Queries)

    So your blog is growing, and users are asking for a search feature. Your first instinct might be to throw together a quick SQL query with a LIKE operator and call it a day. I mean, how hard can search be, right?

    Well, turns out pretty hard if you want it to actually work well. Let's talk about why that simple LIKE '%keyword%' approach is going to cause you headaches, and what you should do instead.

    Why LIKE Queries Will Let You Down

    Let's start with the obvious problem. When you write something like:

    SELECT * FROM posts 
    WHERE title LIKE '%nodejs%' OR content LIKE '%nodejs%';
    

    You're basically telling your database to scan every single row in your table. No indexes can help you here because the % wildcard at the beginning makes it impossible for the database to use its index efficiently. With a few hundred posts? Sure, it might be fast enough. But once you hit thousands of posts, your search will start crawling.

    But speed isn't even the worst part. Here are the real problems:

    Case sensitivity issues. Depending on your database, LIKE might be case-sensitive, so "NodeJS" won't match "nodejs". You could use ILIKE in PostgreSQL or LOWER() functions, but that's just adding more overhead.

    No relevance ranking. When someone searches for "react hooks tutorial", which result should come first? The post titled "React Hooks Tutorial" or the one that mentions "react" and "hooks" buried somewhere in the content? LIKE can't tell you. It's either a match or it isn't.

    No fuzzy matching. If someone types "javscript" (notice the typo), they get zero results instead of JavaScript posts. Not great for user experience.

    Phrase matching problems. Searching for "node package manager" won't find posts that say "package manager for Node" because the words aren't in that exact order.

    What Actually Makes Search Work

    Before we jump into solutions, let's understand what makes search feel good to users.

    Ranking by Relevance

    When you search Google, the best results appear first. That's not magic, that's ranking algorithms that calculate relevance scores using factors like term frequency (how often a word appears), inverse document frequency (how unique that word is across all documents), and field length normalization.

    For a blog, you might want matches in the title to rank higher than matches in the content. A post titled "Getting Started with Docker" is probably more relevant to "docker" than a post that just mentions Docker once in passing.

    Understanding Word Stems

    When someone searches "running", they probably want results about "run", "runs", and "runner" too. This is called stemming, where search matches are based on a root form or stem of a word. Good search engines handle this automatically.

    Handling Common Words

    Common words like "the" or "an" are often ignored because they're called stop words. Nobody searching "the best way to learn Python" really cares about matching "the" in their results. Smart search knows to focus on "best", "way", "learn", and "Python" instead.

    Full-Text Search Options

    Alright, so LIKE queries suck. What should you use instead? You've got options.

    Built-in Database Solutions

    PostgreSQL has excellent full-text search capabilities built right in. You don't need to look farther than your database management system for a full-text search solution.

    Here's a basic example:

    -- Add a tsvector column for search
    ALTER TABLE posts 
    ADD COLUMN search_vector tsvector;
    
    -- Update it with searchable content
    UPDATE posts 
    SET search_vector = 
      to_tsvector('english', title || ' ' || content);
    
    -- Create a GIN index for fast searches
    CREATE INDEX posts_search_idx 
    ON posts USING GIN(search_vector);
    
    -- Now search like this
    SELECT title, 
           ts_rank(search_vector, query) AS rank
    FROM posts, 
         to_tsquery('english', 'nodejs & tutorial') query
    WHERE search_vector @@ query
    ORDER BY rank DESC;
    

    The document is parsed into tokens, which are small fragments like words and phrases from the document's text. PostgreSQL handles stemming, stop words, and ranking for you.

    MySQL has similar capabilities with FULLTEXT indexes:

    -- Create a fulltext index
    ALTER TABLE posts 
    ADD FULLTEXT(title, content);
    
    -- Search with natural language mode
    SELECT title, 
           MATCH(title, content) AGAINST('nodejs tutorial') AS relevance
    FROM posts
    WHERE MATCH(title, content) AGAINST('nodejs tutorial')
    ORDER BY relevance DESC;
    

    Dedicated Search Engines

    For larger sites or more advanced needs, dedicated search engines might be worth considering.

    Elasticsearch is probably the most popular choice. It's a distributed, RESTful search and analytics engine built on top of Apache Lucene, offering powerful full-text search capabilities, real-time indexing, and scalability. The downside? It's another service to maintain, and for a simple blog, it might be overkill.

    MeiliSearch and Typesense are newer alternatives that are easier to set up and more resource-friendly. They're great middle-ground options if PostgreSQL's full-text search isn't cutting it but you don't need Elasticsearch's complexity.

    Client-Side Search for Static Sites

    If you're running a static blog (like with Next.js or Hugo), you might want to handle search entirely on the client side. You would generate a JSON document at build time containing each blog post's metadata, which contains the data needed to perform the search.

    Libraries like Lunr.js can run searches right in the browser. Your search index gets downloaded once, then all searches happen locally without hitting your server. Perfect for small to medium-sized blogs.

    Index Strategies That Matter

    Whether you're using PostgreSQL, MySQL, or something else, how you index your data makes a massive difference.

    Understanding Index Types

    B-tree indexes are the default in most databases. They're best suited for handling equality and range queries on data that can be sorted into some ordering. Great for IDs, dates, and sorting, but not ideal for full-text search.

    GIN indexes (Generalized Inverted Index) are designed to deal with data types that are subdividable and you want to search for individual component values like array elements or lexemes in a text document. This is what you want for full-text search in PostgreSQL.

    Here's the thing about GIN indexes though: they're amazing for reads but slower for writes. GIN indexes are slower to write but faster to read, making them great for applications that query more than they update. For a blog where you write new posts occasionally but search all the time, that's perfect.

    Composite Indexes for Better Performance

    If you frequently filter searches by category or date, consider combining fields:

    -- Instead of separate indexes
    CREATE INDEX ON posts(category);
    CREATE INDEX ON posts(published_at);
    CREATE INDEX ON posts USING GIN(search_vector);
    
    -- Consider a composite approach
    CREATE INDEX ON posts(category, published_at);
    CREATE INDEX ON posts USING GIN(search_vector);
    

    This way, queries like "show me all JavaScript posts from last month" can use a single index lookup.

    Maintaining Your Indexes

    Indexes don't stay perfect forever. As your blog grows and posts get updated, indexes can become fragmented. In PostgreSQL, you can use REINDEX to rebuild them, though be careful on live sites since it locks the table.

    A better approach for production is to create a new index concurrently, then swap:

    -- Create new index without locking
    CREATE INDEX CONCURRENTLY posts_search_idx_new 
    ON posts USING GIN(search_vector);
    
    -- Drop old, rename new
    DROP INDEX posts_search_idx;
    ALTER INDEX posts_search_idx_new 
    RENAME TO posts_search_idx;
    

    Making Search Feel Good (UX Stuff)

    Technical implementation is only half the battle. How users interact with search matters just as much.

    Autocomplete/Suggestions

    Users expect search to guide them. Make sure results are returned from the first keystroke for maximum responsiveness and user control.

    You'll want to show suggestions as they type. Include 6 to 8 query suggestions, and with modern autocomplete libraries, query suggestions will include recent searches.

    Here's a simple approach with PostgreSQL:

    -- Get search suggestions
    SELECT DISTINCT 
      ts_headline('english', title, query) AS suggestion
    FROM posts, 
         to_tsquery('english', 'javasc:*') query
    WHERE search_vector @@ query
    LIMIT 8;
    

    The :* is a prefix match, so "javasc" matches "javascript", "javascript", etc.

    Highlight Matches in Results

    When showing search results, highlight what actually matched. Bolding search terms in autocomplete results allows users to quickly scan through relevant matches.

    PostgreSQL can do this for you:

    SELECT 
      ts_headline('english', content, query) AS excerpt
    FROM posts, 
         to_tsquery('english', 'react & hooks') query
    WHERE search_vector @@ query;
    

    This returns snippets with your search terms wrapped in <b> tags.

    Keep It Fast

    Search needs to feel instant. Aim for responses under 200ms. If your searches are taking longer, check if:

    • Your indexes are actually being used (use EXPLAIN ANALYZE)
    • You're searching too much content at once (limit results)
    • Your search_vector column isn't being updated properly

    Mobile Matters

    More people probably read your blog on their phones than their computers. On a smaller device such as a mobile phone, which can accommodate fewer results, cap the list of suggestions at 4 to 8.

    Also, make the search input big enough to tap easily. Nobody likes trying to hit a tiny search box on a phone screen.

    Handle Typos Gracefully

    When possible, suggest corrections. "Did you mean JavaScript?" can save a search. PostgreSQL has some fuzzy matching capabilities, but for better results, you might need to look at edit distance algorithms or libraries like fuzzywuzzy.

    Putting It All Together

    Here's a realistic implementation for a Node.js blog using PostgreSQL:

    // Set up your search index
    async function createSearchIndex() {
      await db.query(`
        ALTER TABLE posts 
        ADD COLUMN IF NOT EXISTS search_vector tsvector;
        
        UPDATE posts 
        SET search_vector = 
          setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
          setweight(to_tsvector('english', coalesce(content, '')), 'B') ||
          setweight(to_tsvector('english', coalesce(tags, '')), 'C');
        
        CREATE INDEX IF NOT EXISTS posts_search_idx 
        ON posts USING GIN(search_vector);
      `);
    }
    
    // Keep it updated with triggers
    async function createSearchTrigger() {
      await db.query(`
        CREATE OR REPLACE FUNCTION posts_search_update() 
        RETURNS trigger AS $$
        BEGIN
          NEW.search_vector := 
            setweight(to_tsvector('english', coalesce(NEW.title, '')), 'A') ||
            setweight(to_tsvector('english', coalesce(NEW.content, '')), 'B') ||
            setweight(to_tsvector('english', coalesce(NEW.tags, '')), 'C');
          RETURN NEW;
        END
        $$ LANGUAGE plpgsql;
        
        CREATE TRIGGER posts_search_update 
        BEFORE INSERT OR UPDATE ON posts
        FOR EACH ROW EXECUTE FUNCTION posts_search_update();
      `);
    }
    
    // Search function
    async function searchPosts(query, limit = 10) {
      const result = await db.query(`
        SELECT 
          id,
          title,
          slug,
          ts_headline('english', content, query, 
            'MaxWords=30, MinWords=15') AS excerpt,
          ts_rank(search_vector, query) AS rank
        FROM posts, 
             to_tsquery('english', $1) query
        WHERE search_vector @@ query
        ORDER BY rank DESC
        LIMIT $2;
      `, [query.replace(/\s+/g, ' & '), limit]);
      
      return result.rows;
    }
    
    // Autocomplete suggestions
    async function getSuggestions(partial, limit = 8) {
      const result = await db.query(`
        SELECT DISTINCT title
        FROM posts
        WHERE search_vector @@ to_tsquery('english', $1)
        ORDER BY title
        LIMIT $2;
      `, [partial + ':*', limit]);
      
      return result.rows.map(r => r.title);
    }
    

    The setweight() calls make title matches rank higher than content matches, which rank higher than tag matches.

    With All Said

    Search is one of those things that seems simple until you try to build it properly. LIKE queries are tempting because they're easy, but they'll cause performance and usability problems as your blog grows.

    If you're using PostgreSQL or MySQL, start with their built-in full-text search. It's good enough for most blogs and you don't need to add another service to your stack. Focus on proper indexing, relevance ranking, and a decent user interface with autocomplete.

    Only reach for Elasticsearch or similar tools when you've actually outgrown your database's capabilities. For most blogs, that day might never come.

    And remember: fast search that returns decent results beats perfect search that takes forever. Start simple, measure what matters, and improve from there.

    References

    • PostgreSQL Full-Text Search Documentation
    • Understanding GIN Indexes in PostgreSQL
    • BM25 Relevance Scoring
    • Building a Full-Text Search Engine
    • Search UX Best Practices
    George Ongoro
    George Ongoro

    Blog Author & Software Engineer

    I'm George Ongoro, a passionate software engineer focusing on full-stack development. This blog is where I share insights, engineering deep dives, and personal growth stories. Let's build something great!

    View Full Bio

    Related Posts

    Comments (0)

    Join the Discussion

    Please login to join the discussion