5

Say someone searches for "John Smith". Using the search results template on the Craft website, my search results are then a list of titles for entries that include the term "John Smith". For example:

  • Dave Taylor wins award
  • New building built

The user experience is quite poor as there is no indication that these articles are in any way related to "John Smith". What I'm trying to do is output the following:

  • Dave Taylor wins award

...with the help of his brother John Smith,...

  • New building built

...architect John Smith was instrumental...

i.e. something similar to Google search. Any ideas how I could go about this?

3 Answers 3

7

I think creating your own Twig filter is your best bet. So, create a filter that takes a string, searches that for another string and returns an extract. It could do this by using some regular expression hocus pocus, I guess.

{{ entry.bodyText | extract('your search term', 3, 3) }}

would be how you'd use it (assuming, for example, that your filter accepts the three arguments searchTerm, wordsBefore and wordsAfter).

2
  • 1
    To add to what Paul mentioned, there's some snippets in this SE thread that might be useful when crafting an excerpt/highlighting plugin. Feb 27, 2015 at 2:53
  • Thanks, both. I figured this would be the way to go but I just wanted to make sure I wasn't overlooking an easier solution. Feb 27, 2015 at 17:52
4

If I understand you correctly, you want to "highlight" your search query in the results. Your own Twig filter would be the cleanest way to do this, but you don't need to write one. Beware of putting too much logic in your templates, but this would also work nicely, using the native replace filter:

{# Get the search query and break it into individual terms #}
{% set query = craft.request.getParam( 'q' ) %}
{% set searchTerms = query | trim | split( ' ' ) %}

{# Build an associated array w/ terms and the replacement string #}
{% set replace = {} %}
{% for term in searchTerms %}
{% set replace = replace | merge( { (term) : '<strong>' ~ term ~ '</strong>' } ) %}
{% endfor %}

{# Loop through your search results, replacing the terms for all neccessary fields..#}
{% for entry in entries %}
    <li>
        <a href="{{ entry.url }}">{{ entry.title | replace( replace ) | raw }}</a>
        <p>{{ entry.bodyText | replace( replace ) | raw }}
    </li>
{% endfor %}

With that, all your search terms will be wrapped in tags – or whatever else you'd want to highlight them.

EDIT: Reading over, I realize I did misunderstand the question – you need to extract parts of the content matching the search query, not highlight the terms in titles, excerpts or whatever. The commenter above me is correct; you'd need a custom filter to accomplish your goal (without going insane). Apologies for the wrong answer; I'll leave it up, though.

2
  • Thanks anyway. My question probably could have been clearer. Feb 27, 2015 at 17:53
  • Thanks for your answer. I just want to add that if a term is part of an URL for instance, the link will break.
    – Mario
    Apr 20, 2017 at 12:16
3

I know this is an old question but i stumbled upon it while searching for an answer so others might find it as well and I wanted to share my solution which is not perfect but works really good for all sorts of fields.

What I wanted was to display the context why the entry was returned when searching for a certain keyphrase. So What I do is, I iterated over all fields of an entry using $entry->getFieldValues() and then based on what type the field is I chose the next step:

  • Is it a text or redactor field -> search for the searchQuery in the string and if it is found append it to the excerpt
  • Is it an array -> recursively call the function to iterate over all fields of the array
  • Is it of type matrixblock or supertable -> recursively call the function to iterate over all fields

This solution obviously finds more than the search function uses to decided if the entry is part of the result set because I didn't find a way to check if a field is part of the searchindex but it results in a nice excerpt to give users context for their search result.

Full TwigExtension:

<?php

namespace modules\extensions;

use Twig\Extension\AbstractExtension;

class SearchResultExcerpt extends AbstractExtension
{
    public function getFunctions(): array
    {
        return [
            new \Twig\TwigFunction("getSearchResultExcerpt", function ($entry, $searchString) {
                return $this->getBlockExcerpt($entry, $searchString);
            }),
        ];
    }

    private function getBlockExcerpt($block, $searchString): string
    {
        $excerpt = "";
        $fields = is_array($block) ? $block : $block->getFieldValues();
        foreach ($fields as $field) {
            $fieldExcerpt = "";

            if (is_string($field)) {
                $text = strip_tags($field);
                $matches = $this->findMatches($text, $searchString);
                if (strlen($matches) > 0) {
                    $fieldExcerpt .= '<span class="' . array_search($field, $fields) . '">' . $matches . "</span>";
                }
            } elseif (is_array($field)) {
                foreach ($field as $value) {
                    $fieldExcerpt .= $this->getBlockExcerpt($value, $searchString);
                }
            } elseif (is_object($field) && str_contains(get_class($field), "redactor")) {
                $text = strip_tags($field->getParsedContent());
                $matches = $this->findMatches($text, $searchString);
                if (strlen($matches)) {
                    $fieldExcerpt .= '<span class="text-field">' . $matches . "</span>";
                }
            } elseif (
                is_object($field) &&
                (str_contains(get_class($field), "MatrixBlockQuery") ||
                    str_contains(get_class($field), "SuperTableBlockQuery"))
            ) {
                foreach ($field->all() as $block) {
                    $fieldExcerpt .= $this->getBlockExcerpt($block, $searchString);
                }
            }
            if (strlen($fieldExcerpt) > 0) {
                $excerpt .= "<div class='content-field'>" . $fieldExcerpt . "</div>";
            }
        }

        return $excerpt;
    }

    private function findMatches($text, $searchString): string
    {
        $text = "\n" . $text . "\n";
        $words = join("|", explode(" ", preg_quote($searchString)));
        preg_match_all("#((?<=\s).{0,30}(" . $words . ").{0,30}(?=\s))#i", $text, $matches, PREG_SET_ORDER);
        $results = [];
        foreach ($matches as $line) {
            $results[] = htmlspecialchars($line[0], 0, "UTF-8");
        }

        $result = join(" [...] ", $results);
        return preg_replace("#" . $words . "#iu", "<span class=\"highlight\">\$0</span>", $result);
    }
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.