MODX REVO умный поиск

Техническое задание

Название проекта: Доработка сниппета для поиска в MODX Revo

Цель: Обеспечить корректный поиск по ресурсу MODX с учетом следующих требований:

  1. Поиск должен поддерживать множественные слова в запросе.
  2. Все слова запроса должны присутствовать в поле pagetitle.
  3. Для числовых запросов искать только точные совпадения, окруженные пробелами.

Основные задачи:

  1. Обработка поисковой фразы:

    • Разделить запрос на слова с помощью explode(" ", $query).
    • Для числовых значений добавить проверку через is_numeric и включение точного поиска с использованием регулярных выражений (REGEXP).
  2. Генерация условий поиска:

    • Формировать массив условий для каждого слова с использованием LIKE для строк и REGEXP для чисел.
    • Условия должны объединяться через AND.
  3. Построение SQL-запроса:

    • Использовать API MODX (или прямой SQL-запрос), чтобы сформировать запрос с учетом всех условий.
    • Включить вывод итогового SQL-запроса с помощью toSQL() или эквивалента.
  4. Реализация логики в сниппете:

    • Проверить и интегрировать условия в существующий код поиска.
    • Обеспечить обработку результатов поиска с корректным отображением.
  5. Дополнительные задачи:

    • Реализовать возможность изменения целевого поля (pagetitle) через параметры сниппета.
    • Проверить совместимость с существующим функционалом и шаблонами (tpl).

Ожидаемый результат:

  • Сниппет, который выполняет поиск по указанным условиям, возвращает корректные результаты и выводит итоговый SQL-запрос для отладки.

Шаблон страницы поиска

{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}

сниппет mSearch2Strict

<?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)',
]
]);
// Примените точный поиск, если запрос состоит из нескольких слов
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', 'контакта'];

//print_r($searchTerms);

// Базовые условия
$conditions = [
'modResource.deleted' => false,
'modResource.published' => true,
];

// Формируем условия LIKE для каждого слова
foreach ($searchTerms as $term) {
if (is_numeric($term)) {
$conditions[] = ['AND:pagetitle:REGEXP' => '(^|\\s)'.$term.'(\\s|$)'];
}else{
$conditions[] = ['AND:pagetitle:LIKE' => '%' . $term . '%'];
}
}

// Создаём запрос
$searchQuery = $modx->newQuery('modResource');

// Указываем выборку данных
$searchQuery->select(['id', 'pagetitle', 'content']);

// Добавляем условия
$searchQuery->where($conditions);

// Выводим сгенерированный 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 'найдено'.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 'выводим 2<br>';
/*----------------------------------------------------------------------------------*/
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;
}


Сделать заказ

| необходим для связи с вами
В кротчайшие сроки я свяжусь с вами.

Также вы можетете связать со мной:
telegram: @ifwcom