Smart Search for MODX Revolution

Technical specification

Project title:  Improvement of the snippet for search in MODX Revo

Purpose:  To provide a correct search on the MODX resource, taking into account the following requirements:

  1. Search should support multiple words in the query.
  2. All query words must be in the   field.pagetitle.
  3. For numerical queries, look for only exact matches surrounded by gaps.

Main objectives:

  1. Processing the search phrase:

    • Separate query into words using  explode(" ", $query).
    • For numerical values add verification via  is_numeric  and enable accurate search using regular expressionsREGEXP).
  2. Generation of search conditions:

    • Create an array of conditions for each word using  LIKE  for strings and  REGEXP  for numbers.
    • Conditions must be combined through  AND.
  3. Building a SQL query:

    • Use API MODX (or direct SQL query) to generate a query that meets all conditions.
    • Enable the output of the final SQL request using  toSQL()  or equivalent.
  4. Implementation of logic in snippet:

    • Verify and integrate the terms into existing search code.
    • Provide processing of search results with correct display.
  5. Additional tasks:

    • Implement the possibility of changing the target field (pagetitle) through the snippet parameters.
    • Check compatibility with existing functionality and templatestpl).

Output expected:

  • A snippet that performs a search under the specified conditions returns the correct results and outputs the final SQL query for debugging.

Search page template

{if $searchParam != ''}
{set $resources = $_modx->runSnippet('!mSearch2Strict', [
'parents' => 0,
'limit' => 0,
'showLog' => 1,
'showSearchLog' => 1,
'queryVar' => 'search',
'where' => '{"class_key":"msProduct"}',
'returnIds' => 1,
])}
{/if}



{if $resources && $searchParam}
{$_modx->runSnippet('!mFilter2', [
'parents' => 0,
'element' => 'msProducts',
'includeThumbs' => 'thumb',
'limit' => 9,
'onlyIndex' => false,

'maxLimit' => 9,
'sortby' => '',
'includeTVs' => 'main_options',
'processTVs' => 'main_options',
'filters' => 'parent:categories',
'resources' => $resources,
'values_delimeter' => ';',
'suggestionsMaxResults' => 0,
'tpls' => '@FILE chunks/items/item.product.tpl, @FILE chunks/items/item.product_row.tpl',
'tplOuter' => '@FILE chunks/parts/search/part.search.mFilter2_outer.tpl',
'tplFilter.outer.default' => '@FILE chunks/parts/search/filters/part.search.filter.outer_parent.tpl',
'tplFilter.row.default' => '@FILE chunks/parts/search/filters/part.search.filter.parent.tpl',
'tplPageWrapper' => '@FILE chunks/base/pagination/base.pagination.wrapper.tpl',
'tplPage' => '@FILE chunks/base/pagination/base.pagination.page.tpl',
'tplPageActive' => '@FILE chunks/base/pagination/base.pagination.page_active.tpl',
'tplPageFirst' => '@INLINE',
'tplPageLast' => '@INLINE',
'tplPagePrev' => '@FILE chunks/base/pagination/base.pagination.page_prev.tpl',
'tplPageNext' => '@FILE chunks/base/pagination/base.pagination.page_next.tpl',
'tplPageFirstEmpty' => '@INLINE',
'tplPageLastEmpty' => '@INLINE',
'tplPagePrevEmpty' => '@FILE chunks/base/pagination/base.pagination.page_prev_empty.tpl',
'tplPageNextEmpty' => '@FILE chunks/base/pagination/base.pagination.page_next_empty.tpl',
'tplPageSkip' => '@FILE chunks/base/pagination/base.pagination.page_skip.tpl',
])}
{elseif $searchParam}

snippetmSearch2Strict

<?php
/** @var modX $modx */
/** @var array $scriptProperties */
/** @var mSearch2 $mSearch2 */

if (!$modx->loadClass('msearch2', MODX_CORE_PATH . 'components/msearch2/model/msearch2/', false, true)) {return false;}
$mSearch2 = new mSearch2($modx, $scriptProperties);
$mSearch2->pdoTools->setConfig($scriptProperties);
$mSearch2->pdoTools->addTime('pdoTools loaded.');


if (empty($queryVar)) {$queryVar = 'query';}
if (empty($parentsVar)) {$parentsVar = 'parents';}
if (empty($minQuery)) {$minQuery = $modx->getOption('index_min_words_length', null, 3, true);}
if (empty($htagOpen)) {$htagOpen = '';}
if (empty($htagClose)) {$htagClose = '';}
if (empty($outputSeparator)) {$outputSeparator = "\n";}
if (empty($plPrefix)) {$plPrefix = 'mse2_';}
$returnIds = !empty($returnIds);
$fastMode = !empty($fastMode);

$class = 'modResource';
$found = array();
$output = null;
$query = !empty($_REQUEST[$queryVar])
? $mSearch2->getQuery(rawurldecode($_REQUEST[$queryVar]))
: '';

if (empty($resources)) {
if (empty($query) && isset($_REQUEST[$queryVar])) {
$output = $modx->lexicon('mse2_err_no_query');
}
elseif (empty($query) && !empty($forceSearch)) {
$output = $modx->lexicon('mse2_err_no_query_var');
}
elseif (!empty($query) && !preg_match('/^[0-9]{2,}$/', $query) && mb_strlen($query,'UTF-8') < $minQuery) {
$output = $modx->lexicon('mse2_err_min_query');
}

$modx->setPlaceholder($plPrefix.$queryVar, $query);

if (!empty($output)) {
return !$returnIds
? $output
: '';
}
//echo 1; exit;
elseif (!empty($query)) {

/*
$decodedQuery = str_replace(' ', '%', rawurldecode($_REQUEST[$queryVar]));
//$decodedQuery = rawurldecode($_REQUEST[$queryVar]);
$q = $modx->newQuery('modResource');
$q->leftJoin('msProductData', 'Data', 'modResource.id = Data.id');

$q->where([
[
'deleted' => false,
'published' => true,
],
[
'pagetitle:LIKE' => '%' . $decodedQuery . '%',
'OR:longtitle:LIKE' => '%' . $decodedQuery . '%',
'OR:Data.article:LIKE' => '%' . $decodedQuery . '%'
]
]);
*/
/*
$q->where([
[
'deleted' => false,
'published' => true,
'pagetitle:REGEXP' => '(\b' . preg_quote($decodedQuery, '/') . '\b)',
'OR:longtitle:REGEXP' => '(\b' . preg_quote($decodedQuery, '/') . '\b)',
'OR:Data.article:REGEXP' => '(\b' . preg_quote($decodedQuery, '/') . '\b)',
]
]);
Apply an accurate search if the query consists of several words
if (strpos($decodedQuery, ' ') !== false) {
$q->where([
'deleted' => false,
'published' => true,
'OR:pagetitle:LIKE' => '%' . $decodedQuery . '%',
'OR:longtitle:LIKE' => '%' . $decodedQuery . '%',
'OR:Data.article:LIKE' => '%' . $decodedQuery . '%'
]);
} else {
$q->where([
'deleted' => false,
'published' => true,
'OR:pagetitle:LIKE' => '%' . $decodedQuery . '%',
'OR:longtitle:LIKE' => '%' . $decodedQuery . '%',
'OR:Data.article:LIKE' => '%' . $decodedQuery . '%'
]);
}

######
$q->select('`modResource`.`id`');
if ($q->prepare() && $q->stmt->execute()) {
$exact = $q->stmt->fetchAll(PDO::FETCH_COLUMN);
}
*/

$searchTerms = explode(" ", $query);
//$searchTerms = ['2', 'contact'];

//print_r($searchTerms);

// Basic conditions
$conditions = [
'modResource.deleted' => false,
'modResource.published' => true,
];

Form the LIKE conditions for each word
foreach ($searchTerms as $term) {
if (is_numeric($term)) {
$conditions[] = ['AND:pagetitle:REGEXP' => '(^|\s)'.$term.'(\s|$)'];
}else{
$conditions[] = ['AND:pagetitle:LIKE' => '%' . $term . '%'];
}
}

// Create a request
$searchQuery = $modx->newQuery('modResource');

// Indicate a sample of data
$searchQuery->select(['id', 'pagetitle', 'content']);

// Adding conditions
$searchQuery->where($conditions);

// Output generated SQL


$ids = array();
$resources = $modx->getCollection('modResource', $searchQuery);

//$sql = $searchQuery->toSQL(); echo $sql;

foreach ($resources as $resource) {
//echo $resource->get('pagetitle') . '<br>';
$ids[] = $resource->get('id');
}

//echo '<br><br>';
//echo 'found'. count ($ids) '<br>';
//print_r($ids);



if(count($ids)==0){
$found = $mSearch2->Search($query);
$ids = array_merge($exact, array_keys($found));
}
$resources = implode(',', $ids);
if (empty($ids)) {
if ($returnIds) {
return '';
}
elseif (!empty($query)) {
$output = $modx->lexicon('mse2_err_no_results');
}
if (!empty($tplWrapper) && !empty($wrapIfEmpty)) {
$output = $mSearch2->pdoTools->getChunk(
$tplWrapper,
array(
'output' => $output,
'total' => 0,
'query' => $query,
'parents' => $modx->getPlaceholder($plPrefix.$parentsVar),
),
$fastMode
);
}
if ($modx->user->hasSessionContext('mgr') && !empty($showLog)) {
$output .= '<pre class="mSearchLog">' . print_r($mSearch2->pdoTools->getTime(), 1) . '</pre>';
}
if (!empty($toPlaceholder)) {
$modx->setPlaceholder($toPlaceholder, $output);
return;
}
else {
return $output;
}
}
}
}elseif (strpos($resources, '{') === 0) {
$found = $modx->fromJSON($resources);
$resources = implode(',', array_keys($found));
unset($scriptProperties['resources']);
}

//echo 'output 2<XQ2QXZ>';
/*----------------------------------------------------------------------------------*/
if (empty($returnIds)) {
// Joining tables
$leftJoin = array(
'mseIntro' => array(
'class' => 'mseIntro',
'alias' => 'Intro',
'on' => $class . '.id = Intro.resource'
)
);
// Fields to select
$resourceColumns = !empty($includeContent)
? $modx->getSelectColumns($class, $class)
: $modx->getSelectColumns($class, $class, '', array('content'), true);
$select = array(
$class => $resourceColumns,
'Intro' => 'intro'
);
$groupby = $class.'.id, Intro.intro';
} else {
$leftJoin = array();
$select = array($class . 'id');
$groupby = $class.'.id';
}

// Add custom parameters
foreach (array('leftJoin', 'select') as $v) {
if (!empty($scriptProperties[$v])) {
$tmp = $modx->fromJSON($scriptProperties[$v]);
if (is_array($tmp)) {
$$v = array_merge($$v, $tmp);
}
}
unset($scriptProperties[$v]);
}

// Default parameters
$default = array(
'class' => $class,
'leftJoin' => $leftJoin,
'select' => $select,
'groupby' => $groupby,
'return' => !empty($returnIds)
? 'ids'
: 'data',
'fastMode' => $fastMode,
'nestedChunkPrefix' => 'msearch2_',
);
if (!empty($resources)) {
$default['resources'] = is_array($resources)
? implode(',', $resources)
: $resources;
}

// Merge all properties and run!
$mSearch2->pdoTools->setConfig(array_merge($default, $scriptProperties), false);
$mSearch2->pdoTools->addTime('Query parameters are prepared.');
$rows = $mSearch2->pdoTools->run();

$log = '';
if ($modx->user->hasSessionContext('mgr') && !empty($showLog)) {
$log .= '<pre class="mSearchLog">' . print_r($mSearch2->pdoTools->getTime(), 1) . '</pre>';
}

// Processing results
if (!empty($returnIds)) {
$modx->setPlaceholder('mSearch.log', $log);
if (!empty($toPlaceholder)) {
$modx->setPlaceholder($toPlaceholder, $rows);
return '';
}
else {
return $rows;
}
}
elseif (!empty($rows) && is_array($rows)) {
$output = array();
foreach ($rows as $k => $row) {
// Processing main fields
$row['weight'] = isset($found[$row['id']]) ? $found[$row['id']] : '';
$row['intro'] = $mSearch2->Highlight($row['intro'], $query, $htagOpen, $htagClose);

$row['idx'] = $mSearch2->pdoTools->idx++;
$tplRow = $mSearch2->pdoTools->defineChunk($row);
$output[] .= empty($tplRow)
? $mSearch2->pdoTools->getChunk('', $row)
: $mSearch2->pdoTools->getChunk($tplRow, $row, $fastMode);
}
$mSearch2->pdoTools->addTime('Returning processed chunks');
if (!empty($toSeparatePlaceholders)) {
$output['log'] = $log;
$modx->setPlaceholders($output, $toSeparatePlaceholders);
}
else {
$output = implode($outputSeparator, $output) . $log;
}
}
else {
$output = $modx->lexicon('mse2_err_no_results') . $log;
}

// Return output
if (!empty($tplWrapper) && (!empty($wrapIfEmpty) || !empty($output))) {
$output = $mSearch2->pdoTools->getChunk(
$tplWrapper,
array(
'output' => $output,
'total' => $modx->getPlaceholder($mSearch2->pdoTools->config['totalVar']),
'query' => $modx->getPlaceholder($plPrefix.$queryVar),
'parents' => $modx->getPlaceholder($plPrefix.$parentsVar),
),
$fastMode
);
}

if (!empty($toPlaceholder)) {
$modx->setPlaceholder($toPlaceholder, $output);
}
else {
return $output;
}


Let’s discuss your project

Tell me what you would like to build. I will reply by email.

Or message me on Telegram @ifwcom