Related:

Supercharging Your Ghost Blog with Meilisearch: A Powerful Search Integration
Transform your Ghost blog with lightning-fast search using the Ghost-Meilisearch integration. Deliver sub-50ms results, enjoy typo tolerance, and customize your search experience. Boost reader engagement with real-time indexing and seamless theme integration.
New in ghost-meilisearch: Deploy Your Webhook with Cloudflare Workers
Enhance your Ghost blog with lightning-fast search! Ghost-meilisearch now supports Cloudflare Workers alongside Netlify Functions, offering global edge distribution, generous free tier limits, and zero cold starts. Learn how to deploy for better performance and reader experience.

GitHub - MFYDev/ghost-meilisearch: Add powerful, lightning-fast search to your Ghost blog with Meilisearch. This integration provides everything you need to create a seamless search experience for your readers.
Add powerful, lightning-fast search to your Ghost blog with Meilisearch. This integration provides everything you need to create a seamless search experience for your readers. - MFYDev/ghost-meilis…

As a developer who's worked extensively with content management systems, I've always appreciated how the right search functionality can transform a blog from good to exceptional.

Today, I'm excited to share the latest updates to the Ghost-Meilisearch integration, which has just reached version v0.5.0. This update introduces several features that significantly enhance the search experience for both readers and blog owners.

If you're not familiar with the project, Ghost-Meilisearch combines the elegant simplicity of Ghost CMS with the lightning-fast search capabilities of Meilisearch. The result is a seamless, intuitive search experience that feels native to your Ghost blog while delivering results in milliseconds.

The latest 0.5.0 release addresses two critical aspects of search that can make or break the user experience: highlighting search results and improving the quality of plain text extraction.

Release v0.5.0 · MFYDev/ghost-meilisearch
Ghost-Meilisearch 0.5.0 Release Notes I’m excited to announce the release of Ghost-Meilisearch v0.5.0, which brings significant improvements to both search quality and user experience! Major Change…

New Highlighting Features for Precise Search Results

Have you ever searched for something specific only to be presented with results that technically match your query but don't show you where or how? The new highlighting feature in Ghost-Meilisearch 0.5.0 solves this problem elegantly.

When users search for specific phrases, the integration now precisely highlights those terms in both titles and content. This makes it immediately obvious why a particular result was returned and helps users quickly determine if it contains what they're looking for.

Advanced Exact Phrase Matching

One of the most impressive features in this update is the sophisticated support for exact phrase matching. Let's look at how this works under the hood:

// Extract text between double quotes for exact phrase matching
extractTextBetweenQuotes(text) {
  if (!text) return null;
  const match = text.match(/"([^"]+)"/);
  return match ? match[1] : null;
}

async performSearch() {
  const query = this.state.query.trim();
  
  // Early return logic for empty queries...
  
  // Set loading state
  this.state.loading = true;
  this.loadingState.classList.add('active');
  this.emptyState.classList.remove('active');
  
  try {
    // Check if the query is wrapped in quotes for exact phrase matching
    const hasQuotes = query.startsWith('"') && query.endsWith('"');
    
    // Extract exact phrases if there are any quotes within the query
    const exactPhrase = this.extractTextBetweenQuotes(query);
    
    // Determine if we need exact phrase matching
    const isExactMatch = hasQuotes || (exactPhrase !== null);
    
    // Prepare search parameters
    const searchParams = {
      limit: 100,
      attributesToHighlight: Object.entries(this.config.searchFields)
        .filter(([_, config]) => config.highlight)
        .map(([field]) => field),
      attributesToRetrieve: ['title', 'url', 'excerpt', 'plaintext', 'tags'],
      attributesToSearchOn: ['title', 'plaintext', 'excerpt']
    };
    
    let searchResults = [];
    
    // Handle exact phrase search
    if (isExactMatch) {
      // Get the phrase to search for
      const searchPhrase = hasQuotes ? query.slice(1, -1) : exactPhrase;
      
      // First, get all documents that contain all the words
      searchParams.matchingStrategy = 'all';
      const initialResults = await this.index.search(searchPhrase, searchParams);
      
      // Then manually filter for the exact phrase
      if (initialResults.hits.length > 0) {
        // Convert search phrase to lowercase for case-insensitive comparison
        const lowerPhrase = searchPhrase.toLowerCase();
        
        // Filter results to only include documents with the exact phrase
        searchResults = initialResults.hits.filter(hit => {
          // Check each searchable field for the exact phrase
          return (
            (hit.title && hit.title.toLowerCase().includes(lowerPhrase)) ||
            (hit.plaintext && hit.plaintext.toLowerCase().includes(lowerPhrase)) ||
            (hit.excerpt && hit.excerpt.toLowerCase().includes(lowerPhrase))
          );
        });
      }
    } else {
      // Regular search - use 'last' matching strategy for normal queries
      searchParams.matchingStrategy = 'last';
      const results = await this.index.search(query, searchParams);
      searchResults = results.hits;
    }
    
    // Update state and UI...
  } catch (error) {
    // Error handling...
  }
}

This implementation cleverly handles two different ways users might search for an exact phrase:

  1. Full quoted search: When a user encloses their entire search in quotes (e.g., "ghost blog setup")
  2. Partial quoted search: When a user includes a quoted phrase within a larger query (e.g., search for "ghost blog" setup)

The code first detects if exact phrase matching is needed, then uses a two-step process to ensure accurate results:

  1. First, it performs a search with Meilisearch's matchingStrategy: 'all' to find all documents containing all the words in the phrase
  2. Then it performs a second-level filter using JavaScript's includes() method to find only results where the words appear together as a phrase

This is significantly more powerful than standard search, as it ensures users can find exactly what they're looking for, especially in content-rich blogs where the same keywords might appear in many different articles but in different arrangements.

If you want to see the difference, normal search now is like:

And below is how precise search looks like:

Smart Plain Text Extraction Powered by Cheerio

Behind the scenes, one of the most substantial improvements is the enhanced plain text extraction from HTML content. The integration now uses Cheerio, a fast and flexible implementation of jQuery for server-side HTML parsing.

Here's a look at how the HTML content is processed to extract meaningful plain text:

// Generate plaintext from HTML
if (post.html) {
  try {
    // Load HTML into cheerio
    const $ = cheerio.load(post.html);
    
    // Remove script and style tags with their content
    $('script, style').remove();
    
    // Extract alt text from images and add it to the text
    $('img').each((_, el) => {
      const alt = $(el).attr('alt');
      if (alt) {
        $(el).replaceWith(` ${alt} `);
      } else {
        $(el).remove();
      }
    });
    
    // Handle special block elements for better formatting
    $('p, div, h1, h2, h3, h4, h5, h6, br, hr, blockquote').each((_, el) => {
      $(el).append('\n');
    });
    
    // Special handling for list items
    $('li').each((_, el) => {
      $(el).prepend('• ');
      $(el).append('\n');
    });
    
    // Get the text content of the body
    plaintext = $('body').text();
    
    // Normalize whitespace
    plaintext = plaintext.replace(/\s+/g, ' ').trim();
  } catch (error) {
    // Fallback to simple regex if cheerio parsing fails
    console.error('HTML parsing error:', error);
    // Fallback regex processing code...
  }
}

Why does this matter? In previous versions, search was performed directly on HTML content, which could lead to:

  • Irrelevant matches within HTML tags
  • Poor context in search results
  • Difficulty extracting meaningful snippets

With Cheerio, the integration now intelligently:

  • Removes script and style tags completely
  • Preserves alt text from images, making image-heavy content searchable
  • Maintains document structure by adding appropriate line breaks for block elements
  • Formats list items with bullets for better readability
  • Preserves meaningful link text while ignoring URLs
  • Normalizes whitespace for cleaner results

The resulting plain text is not only better for searching but also provides much more meaningful context in search results.

Smarter Result Excerpts

Another subtle but powerful improvement is how search result excerpts are generated. Instead of simply truncating text or showing the beginning of an article, the new version extracts relevant portions surrounding the matched terms.

When someone searches for a specific phrase or keyword, the system extracts a window of text (approximately 120 characters) centered around the match, adds appropriate ellipses if needed, and highlights the matching terms. This provides immediate context for why the result is relevant.

The highlighting logic is particularly clever:

// Don't re-highlight words that are already part of a highlighted phrase
excerptContent = excerptContent.replace(
  regex, 
  function(match) {
    // Only highlight if not already inside an em tag
    if (/<em[^>]*>[^<]*$/i.test(excerptContent.substring(0, excerptContent.indexOf(match))) &&
        /^[^<]*<\/em>/i.test(excerptContent.substring(excerptContent.indexOf(match) + match.length))) {
      return match; // Already highlighted
    }
    return '<em>' + match + '</em>';
  }
);

This ensures that the same word isn't highlighted multiple times within a phrase, creating a cleaner, more intuitive display of search results.

Looking Forward

This update represents more than just feature additions – it reflects a deeper understanding of what makes search truly effective. By focusing on highlighting, context, and precision, Ghost-Meilisearch 0.5.0 creates a search experience that feels intuitive and predictable for users while being simple to implement for developers.

For blog owners, these improvements mean readers can find exactly what they're looking for more quickly and with greater confidence. This ultimately leads to better engagement, longer time on site, and a more satisfying user experience.

If you're running a Ghost blog and haven't integrated with Meilisearch yet, now is certainly the time to consider it. The combination of speed, relevance, and now these enhanced display capabilities makes it a compelling addition to any content-rich site.

Have you implemented search on your Ghost blog? What features do you find most important for your readers? I'd love to hear about your experiences in the comments below.