Jump to content
rash

[Solved] Search function AND/OR and sorting of the results

Recommended Posts

Hi guys,

I would like to realize a search function on my site that follows some clear rules:

1 – Search is performed in the two fields 'tite' and 'body'.
2 – If a user searches for 'foo' and 'bar' he can select whether both terms have to be in at least one of the fields (AND) or at least one of the terms has to be in at least one of the fields (OR).
3 – 'foo' should match words like 'food' or 'foolish' as well as 'foo' (LIKE).
4 – Search results where 'foo' or 'bar' are found in the title should be displayed first, followed by the results found in 'body' only.

This is what I have:

<?php

if($input->get->q) {
    $q = $sanitizer->text($input->get->q);
    if($q) {
        $input->whitelist('q', $q);
        $qs = explode(" ", $q);
        
        foreach($qs as $key => $q) :
              $qs[$key] = $sanitizer->selectorValue($q);
        endforeach;
        
        $selector1 = "title%=".implode("|", $qs).", template=entry, limit=50";
        $selector2 = "body%=".implode("|", $qs).", template=entry, limit=50";
        
        // Trying to separate 'important' title matches from 'less important' body matches
        $matches = $pages->find($selector1);
        if ($matches->count) < $limit)) $matches->import($pages->find($selector2));

        if ($matches->count) {        
            foreach($matches as $m) :
                
                // Output matches title & body excerpt

            endforeach;
        }
        else {
            // Output no matches
        }
    }
}
else {
    // Output no search term(s)
}

?>

First problem is regarding rule 2: I don’t know how to do an AND search. As far as I can see, it’s always an OR.

Second problem is the order of the search results (rule 4): I split the queries to separate them nicely, but they appear mixed up. Output starts with a few title matches as I would expect, followed by some body matches, then some further title matches appears. I don’t understand how this happens:

$matches->import

adds array 2 to array 1 without mixing them, isn’t that true?

And just in case I will get the job done someday: how could I avoid doubled matches? Matches in body aren’t that interesting anymore, when the terms was already found in the title field.

I will appreciate any helping hand – thanks.
Ralf

Share this post


Link to post
Share on other sites

In a hurry, so a quick (and not thoroughly tested) part answer

Actually, the 'normal' selector is an AND not an OR (i.e. the comma separated selector).

2a. Both terms in at least one field (@see docs)

// both terms in at least one field
$sel1 = '(title%=foo, title%=bar), (body%=foo, body%=bar)';

2b. At least one term in at least one field (@see docs)

// at least one term in at least one field
$sel2 = 'title|body%=foo|bar';

3 LIKE (@see docs). Here we use %. See the notes in the docs about the alternative + matching order, etc

Edited by kongondo
  • Like 2

Share this post


Link to post
Share on other sites
11 hours ago, rash said:

I don’t understand how this happens:


$matches->import

adds array 2 to array 1 without mixing them, isn’t that true?

Not if PageArray 2 contains some of the same pages as PageArray 1. If you want to maintain the sort order make sure the PageArray you are importing has only unique pages not in the other PageArray. For example:

$pa2->removeItems($pa1);
$pa1->import($pa2);

 

Regarding the %= operator, note that if there are multiple words in the search string then the entire string must appear in that exact order in the field(s) being searched. See this solution for allowing the words in any order/location:

 

  • Like 2
  • Thanks 1

Share this post


Link to post
Share on other sites

kongondo and Robin S – thank you both a lot. Your explanations sound clear and doable, so I will try how far I can get with the new knowledge this evening. And of course I will post the final solution just in case someone has a need for it.

Share this post


Link to post
Share on other sites

Okay, the job is done now. The final solution might not be too elegant, but it works exactly the way I want as I described it in the opening post. My search form has two additional selects: scope for setting the fields to search and mode for setting the search mode.

kongondo and Robin S: I want to thank you once more – I wouldn't be where I am without your kind help.

<?php

if ($input->get->search_terms) {
    
    // ---- sanitizing text input -------------------------------------------------------
    $search_terms = $sanitizer->text($input->get->search_terms);

    // ---- getting desired search scope, default = title + body ------------------------
    if ($input->get->scope) {
        $scope = $input->get->scope;
    }
    else $scope = "t_body";

    // ---- getting desired search mode, default = AND ----------------------------------
    if ($input->get->mode) {
        $mode = $input->get->mode;
    }
    else $mode = "and";

    // ---- template and limit settings --------------------------------------------------
    $template = "pg_entry";
    $limit = 50;
    $selector_complete = ", template=".$template.", limit=".$limit;
        
    // ---- replace multiple spaces with single space
    $search_terms = preg_replace("/\s+/", " ", $search_terms);
    $terms = explode(" ", $search_terms);

    // ---- both scope options: search title ----------------------------------------
    // ---- mode: AND search
    if ($mode == "and") {
        $selector_1 = "(";
        foreach($terms as $term) {
            $selector_1 .= "title%=".$term.", ";
        }
        $selector_1 = substr($selector_1, 0, -2); // delete trailing comma and space                    
        $selector_1 .= ")";
    }
    // ---- mode: OR search
    else {
        $selector_1 = "title%=";
        foreach($terms as $term) {
            $selector_1 .= $term."|";
        }
        $selector_1 = substr($selector_1, 0, -1); // delete trailing pipe
        
    }
    // ---- adding template and limit values
    $selector_1 .= $selector_complete;
    
    // ---- scope: t_body, adding 2nd selector for body -----------------------------
    if ($scope == "t_body") {
    
        // ---- mode: AND search
        if ($mode == "and") {
            $selector_2 = "(";
            foreach($terms as $term) {
                $selector_2 .= "body%=".$term.", ";
                
            }
            // ---- delete trailing comma and space
            $selector_2 = substr($selector_2, 0, -2);                
            $selector_2 .= ")";
        }
        // ---- mode: OR search
        else {
            $selector_2 = "body%=";
            foreach($terms as $term) {
                $selector_2 .= $term."|";
            }
            // ---- delete trailing pipe
            $selector_2 = substr($selector_2, 0, -1);
        }
        // ---- adding template and limit values
        $selector_2 .= $selector_complete;    
    }
    
    // ++++ matches for selector 1 (title) ++++++++++++++++++++++++++++++++++++++++++
    $matches = $pages->find($selector_1);
    
    // ++++ scope t_body && limit not reached yet: matches for selector 2 (body) ++++
    if ($matches->count < $limit && $scope == "t_body") {
        $body_matches = $pages->find($selector_2);
    } else {
        $body_matches = "";
    }

    // --- matches found in body
    if ($body_matches) {
        // ---- remove duplicates already in title matches
        $body_matches->removeItems($matches);
        // ---- add body_matches to matches
        $matches->import($body_matches);
    }

    // ++++ Output search result ++++++++++++++++++++++++++++++++++++++++++++++++++++
    if ($matches->count) {

        foreach($matches as $match) {
            // Output matches
        }
    }
    else {
         // Output no matches
    }
}
else {
     // Output no search term(s)
}

?>

 

Edited by rash
removed an unneccessary if clause
  • Like 1

Share this post


Link to post
Share on other sites

Hello rash, I know this may be a long shot, but I am trying to solve the same problem 😄

I was wondering if you had the form code that posts to this search script? Even a screen shot of the form would be most helpful! Thanks in advance!

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.


  • Recently Browsing   0 members

    No registered users viewing this page.

  • Similar Content

    • By dandeckr
      Hello!
      I'm making my way through my first encounter with ProcessWire, and I'm very near the end of my tasks! I've searched Google, these forums, and for add-ons, but I haven't found any documentation or reference work for implementing boolean searching in the native CMS? Am I missing a thread, add-on, or docs that can point the way? I'm sure I'm not the first to have a need like this? Thanks in advance! 
    • By MateThemes
      Hello everyone!
      I am trying to add my repeater matrix fields to the search selector, but unfortunately nothing seems to work for me.
      I have following search code in my search.php:
      <?php namespace ProcessWire; // look for a GET variable named 'q' and sanitize it $q = input()->get('q'); // sanitize to text, which removes markup, newlines, too long, etc. $q = sanitizer()->text($q); // did $q have anything in it after sanitizing to text? if($q) { // Make the search query appear in the top-right search box. // Always entity encode any user input that also gets output echo '<input id="search-query" value="' . sanitizer()->entities($q) . '">'; // Sanitize for placement within a selector string. This is important for any // values that you plan to bundle in a selector string like we are doing here. // It quotes them when necessary, and removes characters that might cause issues. $q = sanitizer()->selectorValue($q); // Search the title and body fields for our query text. // Limit the results to 50 pages. The has_parent!=2 excludes irrelevant admin // pages from the search, for when an admin user performs a search. $selector = "title|body~=$q, limit=50, has_parent!=2"; // Find pages that match the selector $matches = pages()->find($selector); } else { $matches = array(); } // unset the variable that we no longer need, since it can contain user input unset($q); ?> <main pw-replace='main'> <?php include('./includes/_pageheadersearch.php'); ?> <div id='content-body' class='uk-section uk-section-large uk-section-large'> <div class='uk-container uk-container-small'> <?php // did we find any matches? if(count($matches)) { // yes we did, render them echo ukAlert(sprintf(_n('Found %d page', 'Found %d pages', $matches->count), $matches->count), "default", "check"); echo ukDescriptionListPages($matches); } else { // we didn't find any echo ukAlert(__('Sorry, no results were found'), "danger", "warning"); } ?> </div> </div> </main> I have tried to add my fields to the selector code (repeater_matrix.aboutsblock_repeaters.mytextfield) . But I didn't get any results.
      What I am doing wrong?
      Thanks for your help!
    • By Violet
      I've been trying to figure this out... It seems like I'm probably missing something really simple, but I'm still puzzled as to how to move forward with this. I'd appreciate any help or suggestions anyone can give.
      Aim: I'm trying to modify the default search template so that my search results come out sorted firstly with those which contain the search term in the title and secondly with those that contain it in the body.
      The basic code where I made sure everything was working first was:
       
      $selector = "title|body~=$q, template=BN-article|BN-infopage, sort=-published, limit=15"; // Find pages that match the selector $matches = $pages->find($selector); // did we find any matches? if($matches->count) { // yes we did $entries = $matches; include("./INC-main-blogroll-panels.html"); } It gave me the search results sorted by publication date, as I expected.
      Next I modified the first portion of the code by using the following to generate the matches as follows:
      $matchest = $pages->find("title~=$q, template=BN-article|BN-infopage"); $matchesb = $pages->find("body~=$q, template=BN-article|BN-infopage"); $entries = $matchest->and($matchesb); However, the problem is that $entries in my resultant displayed list did NOT start with those matches that were in the title first from $matchest. It seemed like $matchest->and($matchesb) sorted the resultant list its own way. This is even without the added complication of trying to use unique() afterward to remove duplicates - which appears to have its own default sort.
      Would anyone please point me in the right direction for what what I'm seeking to do? Thank you so much!
    • By BFD Calendar
      I'm getting an error from sending a search in the search box:
      "Notice: Trying to get property of non-object in /home/mekanoinsa/www/site/assets/cache/FileCompiler/site/templates/_func.php on line 45

      Fatal error: Uncaught Error: Call to a member function each() on null in /home/mekanoinsa/www/site/assets/cache/FileCompiler/site/templates/_func.php:49 Stack trace: #0 /home/mekanoinsa/www/site/assets/cache/FileCompiler/site/templates/search.php(42): renderNav(Object(ProcessWire\PageArray)) #1 /home/mekanoinsa/www/wire/core/TemplateFile.php(287): require('/home/mekanoins...') #2 /home/mekanoinsa/www/wire/core/Wire.php(380): ProcessWire\TemplateFile->___render() #3 /home/mekanoinsa/www/wire/core/WireHooks.php(723): ProcessWire\Wire->_callMethod('___render', Array) #4 /home/mekanoinsa/www/wire/core/Wire.php(442): ProcessWire\WireHooks->runHooks(Object(ProcessWire\TemplateFile), 'render', Array) #5 /home/mekanoinsa/www/wire/modules/PageRender.module(514): ProcessWire\Wire->__call('render', Array) #6 /home/mekanoinsa/www/wire/core/Wire.php(383): ProcessWire\PageRender->___renderPage(Object(ProcessWire\HookEvent)) #7 /home/mekanoinsa/www/wire/core/WireHooks.php(723): ProcessWire\Wire->_callMethod('___renderPage', Array) #8 /h in /home/mekanoinsa/www/site/assets/cache/FileCompiler/site/templates/_func.php on line 49
      This is the code part in _func.php
      // markup for the text $programme = "{$item->stu_programme->title}"; $out .= "<div class='list'><div class='listtext'><span class='verdana_18_bold'><a href='$item->url'>$item->title</a></span><br><br><span class='verdana'><b>$programme</b><br>"; $out .= $item->workshops_list->each( "<font color='green'>| {title}</font>" ); // if the item has summary text, include that too if($item->summary) $out .= "<br><br>$item->summary"; // end markup for the text $out .= "</div>"; Weird, it doesn't happen all the time and I can't figure out what makes the difference....
    • By joe_ma
      Hello
      I tryed to modify the search template so as to search also descriptions of an upload field.
      $selector = "title|body|upload->description~=$q, include=hidden, limit=50"; That throws the following exception:
      Exception: Unknown Selector operator: '' -- was your selector value properly escaped?
      So how is this correctly done?
      Thanks.
×
×
  • Create New...