LMD

TIP: Using "AND" selectors across multiple fields (named selectors to the rescue)

Recommended Posts

I don't know if this is the right place, it's not really a tutorial, just a tip based on notes I wrote myself in a recent project to get it straight in my own head. I thought it might be useful for others in a similar situation.

Scenario:

Create a search function that will search for keywords "foo" and "bar" in multiple fields, but the keywords do not have to be adjacent, in order, or even all in the same field. For eample, the selector must match if "foo" is in "field_a" and "bar" is in "field_b" -- so long as both keywords are present somewhere, the page match is valid.

It is possible to just split the terms and do multiple queries on each field separately and then combine the results into a single PageArray for pagination (I believe there is a module that helps with this). However, I wanted to see if it was possible to do a basic version with a single query.

Not The Solution:

The following selector does not work when keywords appear separately in different fields (operator '~=' - contains all the words):

$selector = "title|field_a|field_b~=foo bar";

What the selector is saying:

FIND BOTH "foo" AND "bar" IN title
OR
FIND BOTH "foo" AND "bar" IN field_a
OR
FIND BOTH "foo" AND "bar" IN field_b

In this case, both "foo" and "bar" have to be in the same field (but not adjacent or in order) to match.

The Actual Solution

What we need to use is "named selectors" to let us match each individual keyword separately while still using one selector.

Using the same example as before:

$selector = "selector1=(title|field_a|field_b~=foo), selector2=(title|field_a|field_b~=bar)";

What the selector is saying at its most basic level:

FIND BOTH selector1 AND selector2

Or, to expand on this, it is saying:

(FIND "foo" IN title OR field_a OR field_b)
AND
(FIND "bar" IN title OR field_a OR field_b)

Crucially, "foo" and "bar" do not have to be in the same field to match.

Practical Method

In this example code, I am actually allowing the search for phrases (using "quoted text") as well as individual terms, so a person could enter...

"foo bar" baz

... and it will keep "foo bar" together aa one term and "baz" as a separate term and match them as an exact phrase.

// Keywords obtained from $input->get and cleaned (multiple spaces removed)/sanitized etc.
$keywords = '"foo bar" baz';

// Split into individual search terms by space (preserve spaces in quoted text)
$terms = str_getcsv($keywords, " "); // array("foo bar", "baz")

// Build up named selectors
$ns = ""; // named selectors string
$i=1; // named selector count

foreach ($terms as $term) {
    // operator '*=' - contains the exact word or phrase
    $ns .= ", ns{$i}=(title|field_a|field_b*=" . trim($term) . ")";
    $i++;
}
//$ns = ", ns1=(title|field_a|field_b*=foo bar), ns2=(title|field_a|field_b*=baz)"

// Construct the whole selector (modify/add other general selectors as needed)
$selector = "template=my-template, limit=20, sort=-date" . $ns;

// Find pages based on selector
$results = $pages->find($selector);

 

DISCLAIMER

I haven't done any tests to see if this method is more efficient than running queries on each field separately and combining the results, I just wanted to see if it was possible!

  • Like 6

Share this post


Link to post
Share on other sites

I think you can also do:

$selector = "title|field_a|field_b~=foo|bar";

 

Share this post


Link to post
Share on other sites
1 hour ago, Tom. said:

I think you can also do:


$selector = "title|field_a|field_b~=foo|bar";

 

Not quite, that would return pages where only ONE of the search terms is present in any of the fields ("foo" OR "bar").

FIND EITHER "foo" OR "bar" IN title
OR
FIND EITHER "foo" OR "bar" IN field_a
OR
FIND EITHER "foo" OR "bar" IN field_b

So it would match a page where only "foo" is contained in any (or all) of the fields -- but "bar" must also be present somewhere.

Likewise, this would not work either:

$selector = "title~=foo|bar, field_a~=foo|bar, field_b~=foo|bar";

FIND EITHER "foo" OR "bar" IN title
AND
FIND EITHER "foo" OR "bar" IN field_a
AND
FIND EITHER "foo" OR "bar" IN field_b

In this case, "foo" or "bar" must be present in all the fields, which is also not what we want

Share this post


Link to post
Share on other sites

Thanks for the tip @LMD

It is worth to note the new "tags for fields" search "shortcut" of ProcessWire 3.0.106:
https://processwire.com/blog/posts/pw-3.0.106/

I guess title|field_a|field_b in your example could be simplified too by using this new feature. Provided it supports "named selectors".

Share this post


Link to post
Share on other sites

@LMD Thanks for this post. I'll definitely keep this bookmarked.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Recently Browsing   0 members

    No registered users viewing this page.

  • Similar Content

    • By Peter Knight
      <?php $Cats=$pages->find("parent=1086, id!=$page, sort=sort"); foreach($Cats as $Cat) { echo " I have a selector that pulls in 4 child pages of a parent and the order is set to reflect the order of the tree.
      My client has asked that instead of
      Cat 1 | Cat 2 | Cat 3 | Cat 4
      we instead display
      Cat 4 | Cat 1 | Cat 2 | Cat 3 
      However, I can't change the sort order in the tree.
      Can anyone guide me on the correct approach here?
      Thanks
       
    • By theoretic
      Hi there! And thanks for Processwire!
      It appears that i've found something interesting about PW selectors. They should only be strings! Here's an example of SQL-like syntax for selector:
      $my_complex_selector = " name='some name', parameter=123, other_parameter=[subparam>=subvalue] "; Trying to use this selector lead to a very buggy PW behaviour. It appears that newlines are treated in a very special manner by PW selector engine, preventing the newlined selectors from working as expected.
      I cannot imagine a situation when an unescaped newline could be a part of selector or selector value, so stripping newline symbols from selector could be a good idea for further PW development. And, currently, another good idea is to write complex selectors as PHP arrays:
      $my_complex_selector = [ "name=$name", "param1=$param1", ]; and to implode them into a single line before using find() and other functions which use selectors.
      Sorry if i wrote something trivial, but having this post already present at support forum could save me a couple of hours. Hope mine will save that tame for someone else 😉
    • By creativejay
      Apologies if this has been covered. I tried a search but didn't hit the usecase I'm after.
      I currently have category pages listing their children products. Someone asked me to put a product in multiple categories, so I created a Page Reference field called prod_othercategories which lets a user pick multiple product category pages.
      When I try to output a list of products for a category page, I came around to the following selector:
      $pages->find("prod_othercategories|parent=$page, template=prod_series, sort=title, prod_status_pages!=1554|1559|1560|4242"); Only the first selector item is giving me trouble, but I'm including the entire string in case something is conflicting and I'm not realizing it.
      The output is currently only outputting matches for "parent" and ignoring prod_othercategories. I tried listing parent first in the selector but it had no effect.
      Appreciate if someone could help me with this! Thanks!
    • By verdeandrea
      Hello girls and guys,
      i just saw that if I use a selector on a textarea, for example 
      $pages->find('body%=notes') it also find that key in html classes and attributes. For example it will find
      <p class="text-notes">Lorem ipsum dolor sit amet cosectetur.</p> Is there a way to avoid this anche check only the texts without the html tags?
      Thanks!
    • By theoretic
      Hi there! And thanks for Processwire!
      This is rather a question than a feature request, maybe there's a well known solution which i missed.
      So what about aggregation in selectors? Let's suppose we have a PW-powered shop where any product has a rating made by customers. Every time the user polls two data fields are updated: points and votes. The rating is calculated as this:
      $rating = $page->points / $page->votes; It was easy, yeah. But what about sorting products by their rating? Basically there are two ways of doing it:
      Creating one more field called rating and updating it after every vote. For sure it will work, but we have to create an extra data field which in fact stores data based on two another fields. Bad practice i think. Getting all products into array and applying a cutom sort function. Will work again, but this demands much more coding than using the native PW selectors. But how could it be done in the best possible way? Let's imagine this:
      $products = $page->children("sort=points/votes"); Or maybe this:
      function rating($a,$b){ return $a->points/$a->votes > $b->points/$b->votes; } $products = $page->children("sort=rating()"); Okay, let's go even deeper. Maybe we need "virtual" fields which are not stored in DB tables like other PW fields but represent the dynamically calculated values?
      $page->rating = function(){ return $page->points/$page->votes; } $products = $page->children("sort=rating"); Will be glad to see advice on the best practices. Thanks in advance!