Looking for the solution to searching entries in one section that are related to multiple categories and match number field criteria. This is for a used auto website.


Make/Model/Trim: Nested category levels for better searching a leveling of vehicles. Category example: Chevrolet -> Malibu -> LS

Transmission: Automatic and Manual

Class: Coupe, Sedan, SUV, etc...

Fields: Price - number field with up to 2 decimal places. Year - 4 digit number no punctuation or decimal places.

The search bar fields: Make Model Transmission Class Minimum Year Maximum Year Minimum Price Maximum Price

Current search form submits a url such as: http://examplesite.com/search?make=chevrolet&model=malibu&transmission=automatic&min-year=2007&max-year=2017&min-price=3000&max-price=20000&class=sedan

Whether or not a search field was used it still applies at empty field to the url.

I've read every Craft CMS Multi search page and SO question on this as well as the new Craft CMS 3 documentation. I can't seem to find solution to the categories part first off and then a decent way to handle the number criteria. Also, I am planning on paginating the entries (causes a separate issue of not being able to use loop.last, which I can solve with some loop counters).

Current search page code:

{% set make = craft.app.request.getParam('make') %}
{% set model = craft.app.request.getParam('model') %}
{% set transmission = craft.app.request.getParam('transmission') %}
{% set class = craft.app.request.getParam('class') %}

{% set minYear = craft.app.request.getParam('min-year') %}
{% set maxYear = craft.app.request.getParam('max-year') %}
{% set minPrice = craft.app.request.getParam('min-price') %}
{% set maxPrice = craft.app.request.getParam('max-price') %}

{% set makeCat = make ? craft.categories.group('makeModelTrim').slug(make).find() %}
{% set modelCat = model ? craft.categories.group('makeModelTrim').slug(model).find() %}
{% set transmissionCat = transmission ? craft.categories.group('transmission').slug(transmission).find() %}
{% set classCat = class ? craft.categories.group('class').slug(class).find() %}

{% set makeParam = makeCat ? {targetElement: ['and']|merge(makeCat)} %}
{% set modelParam = modelCat ? {targetElement: ['and']|merge(modelCat)} %}
{% set transmissionParam = transmissionCat ? {targetElement: ['and']|merge(transmissionCat)} %}
{% set classParam = classCat ? {targetElement: ['and']|merge(classCat)} %}

{% set relationParams = [makeParam, modelParam, transmissionParam, classParam]|filter %}

{% set entries = craft.entries({section: 'vehicles',relatedTo: relationParams ? ['and']|merge(relationParams) : null}) %}

Works but is kinda wonky. I would like the related entries to match all of the given categories but it currently pulls vehicles that match any of the categories. If Chevrolet and Automatic transmission are given criteria, both Chevrolets and Automatics are pulled which is not what I want.

Is there a way to add the min/max year and price to the related to parameters? If so then that would solve the next issue of sorting through the entries though it's difficult to paginate the entries if I have to throw out some retrieved ones after the fact. When I try to convert the set entries to paginate with this:

{% paginate craft.entries({section: 'vehicles',relatedTo: relationParams ? ['and']|merge(relationParams) : null}).limit(12) as pageInfo, pageEntries %}

It doesn't seem to work though I can get the count of pageEntries and it will return some number. But what I'm currently doing is the former setting of the entries to the related parameters then running this:

{% for vehicle in pageEntries if minYear|length and minYear <= vehicle.year and maxYear|length and maxYear >= vehicle.year and minPrice|length and minPrice <= vehicle.price and maxPrice|length and maxPrice >= vehicle.price %}

Which doesn't work. I'm guessing that my if statements aren't in order or can't be used that way. I could have a complicated if/else or case scenario to handle whether min/max price and year have been set rather than checking it in the for loop, though I'm not certain what's best.

I'm sure someone reading this has a simple solution to multi category and multi field entry searching. And if it's possible to eliminate the the empty parameters in the url, that would be helpful as well. I currently have empty options with no values set holding the place of the select drop-downs until a value is selected.

  • 1
    Why would you want to include the min/max price in a relatedTo parameter? You can set this parameter via magic setter craft.entries.minPrice('>=50').maxPrice('<=100).minYear(>=2004).maxYear('<=2016).all() however. I'm usually no fan of complex search functions in twig because of certain experiences in the past years. It's much easier to extend the query more freely and flexible via php in order to join relation tables on your own to build more complex nested relationships Commented Apr 18, 2018 at 20:06
  • Wow! Thank you! I have never heard of magic setter and it never came up in any of my searches for something like this. I will definitely us this. Yes I could do something in php but I think coming up with something here will be simpler and I'm not as familiar with running php commands directly to pull craft entries. Commented Apr 19, 2018 at 2:05
  • You are using magic methods the entire time. entry.fieldHandle is a magic getter too. Usually you won't see any search result here on Stack Exchange for similar questions without them. Commented Apr 19, 2018 at 4:51
  • Thanks! I guess I just didn't know the term. Commented Apr 19, 2018 at 13:48

1 Answer 1


Solved this by merging the search parameters together for both the multiple categories and the number fields if they were set in the url parameters. This solves the loop.last pagination problem as well. Here is the code I used:

{# set base params #}
{% set params = {
    section: 'vehicles',
    limit: null
} %}
{% set relatedParams = ['and'] %}

{# add category parameters if they exist #}
{% if craft.app.request.getParam('make') %}
    {% set category = craft.categories.group('makeModel').slug(craft.app.request.getParam('make')).one() %}
    {% set relatedParams = relatedParams|merge([{ targetElement:category }]) %}
{% endif %}
{% if craft.app.request.getParam('model') %}
    {% set category = craft.categories.group('makeModel').slug(craft.app.request.getParam('model')).one() %}
    {% set relatedParams = relatedParams|merge([{ targetElement:category }]) %}
{% endif %}
{% if craft.app.request.getParam('transmission') %}
    {% set category = craft.categories.group('transmission').slug(craft.app.request.getParam('transmission')).one() %}
    {% set relatedParams = relatedParams|merge([{ targetElement:category }]) %}
{% endif %}
{% if craft.app.request.getParam('body') %}
    {% set category = craft.categories.group('class').slug(craft.app.request.getParam('body')).one() %}
    {% set relatedParams = relatedParams|merge([{ targetElement:category }]) %}
{% endif %}

{# add number fields with greater/lesser than comparisons #}
{% if craft.app.request.getParam('min-year') %}
    {% set minYear = craft.app.request.getParam('min-year') %}
    {% set params = params|merge({'year':'>=' ~ minYear}) %}
{% endif %}
{% if craft.app.request.getParam('max-year') %}
    {% set maxYear = craft.app.request.getParam('max-year') %}
    {% set params = params|merge({'year':'<=' ~ maxYear}) %}
{% endif %}
{% if craft.app.request.getParam('min-price') %}
    {% set minPrice = craft.app.request.getParam('min-price') %}
    {% set params = params|merge({'price':'>=' ~ minPrice}) %}
{% endif %}
{% if craft.app.request.getParam('max-price') %}
    {% set maxPrice = craft.app.request.getParam('max-price') %}
    {% set params = params|merge({'price':'<=' ~ maxPrice}) %}
{% endif %}

{# add relatedParams to params #}
{% if relatedParams|length > 1 %}
    {% set params = params|merge({'relatedTo':relatedParams}) %}
{% endif %}

{# perform search #}
{% paginate craft.entries(params).limit(12) as pageInfo, pageEntries %}

I believe all the code is Craft 3 compliant as well. Searching looks for all parameters to be met so this works well for product searches.

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.