Skip to content

Instantly share code, notes, and snippets.

@touol
Created August 17, 2021 09:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save touol/a1c46d7fd199c0889c361c4075c28d39 to your computer and use it in GitHub Desktop.
Save touol/a1c46d7fd199c0889c361c4075c28d39 to your computer and use it in GitHub Desktop.
Исправление кеширования mfilter2
<?php
/**
* The base class for mSearch2.
*
* @package msearch2
*/
class mSearch2 {
/** @var modX $modx */
public $modx;
/** @var array $config */
public $config = array();
/** @var mSearch2ControllerRequest $request */
protected $request;
/** @var mse2FiltersHandler $filtersHandler */
public $filtersHandler = null;
/** @var array $phpMorphy */
public $phpMorphy = array();
/** @var array $methods */
public $methods = array();
/** @var array $filters */
public $filters = array();
/** @var array $aliases */
public $aliases = array();
/** @var integer Total number of filter operations */
public $filter_operations = 0;
/** @var string Current search query */
public $query = '';
/** @var pdoFetch $pdoTools */
public $pdoTools = null;
/** @var array $fields */
public $fields = array();
const version = '1.10.6';
/**
* mSearch2 constructor.
*
* @param modX $modx
* @param array $config
*/
public function __construct(modX &$modx, array $config = array()) {
$this->modx =& $modx;
$corePath = $this->modx->getOption('msearch2.core_path', $config, $this->modx->getOption('core_path') . 'components/msearch2/');
$assetsPath = $this->modx->getOption('msearch2.assets_path', $config, $this->modx->getOption('assets_path') . 'components/msearch2/');
$assetsUrl = $this->modx->getOption('msearch2.assets_url', $config, $this->modx->getOption('assets_url') . 'components/msearch2/');
$actionUrl = $this->modx->getOption('msearch2.action_url', $config, $assetsUrl . 'action.php');
$connectorUrl = $assetsUrl . 'connector.php';
$this->config = array_merge(array(
'assetsUrl' => $assetsUrl,
'cssUrl' => $assetsUrl . 'css/',
'jsUrl' => $assetsUrl . 'js/',
'jsPath' => $assetsPath . 'js/',
'imagesUrl' => $assetsUrl . 'images/',
'customPath' => $corePath . 'custom/',
'dictsPath' => $corePath . 'phpmorphy/dicts/',
'connectorUrl' => $connectorUrl,
'actionUrl' => $actionUrl,
'corePath' => $corePath,
'modelPath' => $corePath . 'model/',
'templatesPath' => $corePath . 'elements/templates/',
'processorsPath' => $corePath . 'processors/',
'cacheTime' => 1800,
'min_word_length' => $this->modx->getOption('mse2_index_min_words_length', null, 3, true),
'exact_match_bonus' => $this->modx->getOption('mse2_search_exact_match_bonus', null, 10, true),
'like_match_bonus' => $this->modx->getOption('mse2_search_like_match_bonus', null, 3, true),
'all_words_bonus' => $this->modx->getOption('mse2_search_all_words_bonus', null, 5, true),
'old_search_algorithm' => $this->modx->getOption('mse2_old_search_algorithm', null, false),
'introCutBefore' => 50,
'introCutAfter' => 250,
'filter_delimeter' => '|',
'method_delimeter' => ':',
'values_delimeter' => ',',
'split_words' => $this->modx->getOption('mse2_search_split_words', null, '#\s#', true),
'split_all' => $this->modx->getOption('mse2_index_split_words', null, '#\s|[,.:;!?"\'(){}\\/\#]#', true),
'index_all' => $this->modx->getOption('mse2_index_all', null, false),
'suggestionsRadio' => array(),
'autocomplete' => 0,
'queryVar' => 'query',
'minQuery' => 3,
'onlyIndex' => false,
'config_file' => $assetsPath . 'js/web/config.js',
), $config);
if (!is_array($this->config['suggestionsRadio'])) {
$this->config['suggestionsRadio'] = array_map('trim', explode(',', $this->config['suggestionsRadio']));
}
mb_internal_encoding($this->modx->getOption('modx_charset', null, 'UTF-8'));
$this->modx->addPackage('msearch2', $this->config['modelPath']);
$this->modx->lexicon->load('msearch2:default');
$this->pdoTools = $this->modx->getService('pdoFetch');
$fqn = $this->modx->getOption('pdoFetch.class', null, 'pdotools.pdofetch', true);
$path = $this->modx->getOption('pdofetch_class_path', null, MODX_CORE_PATH . 'components/pdotools/model/', true);
if ($pdoClass = $modx->loadClass($fqn, $path, false, true)) {
$this->pdoTools = new $pdoClass($modx, $config);
} else {
$this->pdoTools = $this->modx->getService('pdoFetch');
}
$this->getWorkFields();
//$this->checkStat();
}
/**
* @param string $query
*
* @return string
*/
public function getQuery($query = '') {
$query = htmlspecialchars_decode($query, ENT_QUOTES);
$query = strip_tags($query);
$query = preg_replace('#["\'\(\)\[\]\{\}]#', '', $query);
$query = htmlspecialchars($query);
return trim($query);
}
/**
* Prepares working fields of resources for search
*
* @param array $config
*
* @return array
*/
public function getWorkFields($config = array()) {
$config = array_merge($this->config, $config);
$setting = $this->modx->getOption('mse2_index_fields', null, 'content:3,description:2,introtext:2,pagetitle:3,longtitle:3', true);
$fields = $default = array();
// Preparing default fields for work
$tmp = array_map('trim', explode(',', strtolower($setting)));
foreach ($tmp as $v) {
$tmp2 = explode(':', $v);
$default[$tmp2[0]] = !empty($tmp2[1])
? $tmp2[1]
: 1;
}
if ($this->modx->getOption('mse2_index_comments', null, true)) {
$default['comment'] = $this->modx->getOption('mse2_index_comments', null, 1, true);
}
if (!empty($config['fields'])) {
$tmp = array_map('trim', explode(',', strtolower($config['fields'])));
foreach ($tmp as $v) {
$tmp2 = explode(':', $v);
$fields[$tmp2[0]] = !empty($tmp2[1])
? $tmp2[1]
: (isset($default[$tmp[0]])
? $default[$tmp[0]]
: 1
);
}
}
else {
$fields = $default;
}
$this->fields = $fields;
return $fields;
}
/**
* Initializes mSearch2 into different contexts.
*
* @param string $ctx The context to load. Defaults to web.
* @param array $scriptProperties
*
* @return boolean
*/
public function initialize($ctx = 'web', $scriptProperties = array()) {
switch ($ctx) {
case 'mgr':
if (!$this->modx->loadClass('msearch2.request.mSearch2ControllerRequest', $this->config['modelPath'], true, true)) {
return 'Could not load controller request handler.';
}
$this->request = new mSearch2ControllerRequest($this);
return $this->request->handleRequest();
break;
default:
$this->config = array_merge($this->config, $scriptProperties);
$this->config['ctx'] = $ctx;
if (!defined('MODX_API_MODE') || !MODX_API_MODE) {
$config = $this->makePlaceholders($this->config);
if ($css = trim($this->modx->getOption('mse2_frontend_css'))) {
$this->modx->regClientCSS(str_replace($config['pl'], $config['vl'], $css));
}
if ($js = trim($this->modx->getOption('mse2_frontend_js'))) {
$this->modx->regClientScript(str_replace($config['pl'], $config['vl'], $js));
}
}
}
return true;
}
/**
* Method loads custom classes from specified directory
*
* @var string $dir Directory for load classes
* @return void
*/
public function loadCustomClasses($dir)
{
$customPath = $this->config['customPath'];
$placeholders = array(
'base_path' => MODX_BASE_PATH,
'core_path' => MODX_CORE_PATH,
'assets_path' => MODX_ASSETS_PATH,
);
$pl1 = $this->pdoTools->makePlaceholders($placeholders, '', '[[+', ']]', false);
$pl2 = $this->pdoTools->makePlaceholders($placeholders, '', '[[++', ']]', false);
$pl3 = $this->pdoTools->makePlaceholders($placeholders, '', '{', '}', false);
$customPath = str_replace($pl1['pl'], $pl1['vl'], $customPath);
$customPath = str_replace($pl2['pl'], $pl2['vl'], $customPath);
$customPath = str_replace($pl3['pl'], $pl3['vl'], $customPath);
if (strpos($customPath, MODX_BASE_PATH) === false && strpos($customPath, MODX_CORE_PATH) === false) {
$customPath = MODX_BASE_PATH . ltrim($customPath, '/');
}
$customPath = rtrim($customPath, '/') . '/' . ltrim($dir, '/');
if (file_exists($customPath) && $files = scandir($customPath)) {
foreach ($files as $file) {
if (preg_match('#\.class\.php$#i', $file)) {
/** @noinspection PhpIncludeInspection */
include $customPath . '/' . $file;
}
}
} else {
$this->modx->log(modX::LOG_LEVEL_ERROR, "[mSearch2] Custom path is not exists: \"{$customPath}\"");
}
}
/**
* Initializes phpMorphy for needed language
*
* @return boolean
*/
public function loadPhpMorphy() {
if (!class_exists('phpMorphy_Exception')) {
/** @noinspection PhpIncludeInspection */
require_once $this->config['corePath'] . 'phpmorphy/src/common.php';
}
$dicts = $this->getDictionaries();
foreach (array_keys($dicts) as $lang) {
if (!empty($this->phpMorphy[$lang]) && $this->phpMorphy[$lang] instanceof phpMorphy) {
continue;
}
else {
try {
$this->phpMorphy[$lang] = new phpMorphy(
$this->config['corePath'] . 'phpmorphy/dicts/',
$lang,
array(
'storage' => 'file',
)
);
}
catch (phpMorphy_Exception $e) {
$this->modx->log(modX::LOG_LEVEL_ERROR, '[mSearch2] Could not initialize phpMorphy for language .' . $lang . ': .' . $e->getMessage());
}
}
}
if (!count($this->phpMorphy)) {
$this->modx->log(modX::LOG_LEVEL_ERROR, '[mSearch2] Could not find any phpMorphy dictionary.');
return false;
}
else {
return true;
}
}
/**
* Get dictionaries from phpMorphy directory
*
* @return array
*/
public function getDictionaries() {
$dicts = array();
$path = $this->config['dictsPath'];
if (!file_exists($path)) {
return $dicts;
}
$files = scandir($path);
foreach ($files as $file) {
if (preg_match('/\.([a-z]{2,2}\_[a-z]{2,2})\./i', $file, $matches)) {
$dicts[$matches[1]][] = $file;
}
}
return $dicts;
}
/**
* Returns array with words for search
*
* @param string $text
* @param string $pcre
* @param bool $with_count
*
* @return array
*/
public function getBulkWords($text = '', $pcre = '', $with_count = false) {
if (empty($pcre)) {
$pcre = $this->config['split_words'];
}
$words = preg_split($pcre, $text, -1, PREG_SPLIT_NO_EMPTY);
$bulk_words = array();
foreach ($words as $v) {
if (preg_match('/^[0-9]{2,}$/', $v) || mb_strlen($v) >= $this->config['min_word_length']) {
$word = mb_strtoupper($v);
if (!$with_count) {
$bulk_words[$word] = $word;
}
elseif (isset($bulk_words[$word])) {
$bulk_words[$word] += 1;
}
else {
$bulk_words[$word] = 1;
}
}
}
return $bulk_words;
}
/**
* Gets base form of the words
*
* @param array|string $text
* @param boolean $only_words
*
* @return array|string
*/
public function getBaseForms($text, $only_words = true) {
$result = array();
if (is_array($text)) {
foreach ($text as $v) {
$result = array_merge($result, $this->getBaseForms($v, $only_words));
}
}
else {
$text = str_ireplace('ё', 'е', $this->modx->stripTags($text));
$text = preg_replace('#\[.*\]#isU', '', $text);
$bulk_words = $this->getBulkWords($text, $this->config['split_all']);
$this->loadPhpMorphy();
/* @var phpMorphy $phpMorphy */
$base_forms = array();
foreach ($this->phpMorphy as $phpMorphy) {
$locale = $phpMorphy->getLocale();
$base_forms[$locale] = $phpMorphy->getBaseForm($bulk_words);
}
$result = array();
foreach ($base_forms as $lang => $array) {
if (!empty($array)) {
foreach ($array as $word => $forms) {
if (!$forms) {
if (!$this->config['index_all']) {
continue;
}
else {
$forms = array($word);
}
}
foreach ($forms as $form) {
if (preg_match('/^[0-9]{2,}$/', $form) || mb_strlen($form) >= $this->config['min_word_length']) {
$result[$form] = $lang == 'en_EN'
? @iconv('WINDOWS-1250', 'UTF-8//IGNORE', $word)
: $word;
}
}
}
}
}
if ($only_words) {
$result = array_keys($result);
}
}
return $result;
}
/**
* Gets all morphological forms of the words
*
* @param array|string $text
*
* @return array|string
*/
public function getAllForms($text) {
$result = array();
if (is_array($text)) {
foreach ($text as $v) {
$result = array_merge($result, $this->getAllForms($v));
}
}
else {
$text = str_ireplace('ё', 'е', $this->modx->stripTags($text));
$bulk_words = $this->getBulkWords($text);
$this->loadPhpMorphy();
/* @var phpMorphy $phpMorphy */
$all_forms = array();
foreach ($this->phpMorphy as $phpMorphy) {
$locale = $phpMorphy->getLocale();
$all_forms[$locale] = $phpMorphy->getAllForms($bulk_words);
}
$result = array();
if (!empty($all_forms)) {
foreach ($all_forms as $lang => $array) {
if (!empty($array)) {
foreach ($array as $word => $forms) {
if (!empty($forms)) {
if ($lang == 'en_EN') {
foreach ($forms as &$v) {
$v = iconv('WINDOWS-1250', 'UTF-8//IGNORE', $v);
}
}
$result[$word] = isset($result[$word])
? array_merge($result[$word], $forms)
: $forms;
}
}
}
}
}
}
return $result;
}
/**
* Search and return array with resources ids as a key and sum of weight as value
*
* @param $query
* @param bool $process_aliases
*
* @return array
*/
function Search($query, $process_aliases = true) {
if (!empty($this->config['old_search_algorithm'])) {
/** @noinspection PhpDeprecationInspection */
return $this->OldSearch($query);
}
if ($process_aliases) {
$query = preg_replace('/[^_-а-яёa-z0-9\s\.\/]+/iu', ' ', $this->modx->stripTags($query));
$this->log('Filtered search query: "' . mb_strtolower($query) . '"');
if ($aliases = $this->getAliases($query)) {
$this->log('Generated aliases for search query: "' . implode('; ', $aliases) . '"');
$results = array();
foreach ($aliases as $query) {
$this->log('Search by alias: "' . $query . '"');
$results[] = $this->Search($query, false);
}
$found = array();
foreach ($results as $result) {
foreach ($result as $k => $v) {
if (isset($found[$k])) {
$found[$k] += $v;
} else {
$found[$k] = $v;
}
}
}
arsort($found);
return $found;
}
}
$search_forms = $this->getBaseForms($query, false);
$bulk_words = $this->getBulkWords($query);
// Search by words index
$index = $debug = array();
if (!empty($search_forms)) {
$q = $this->modx->newQuery('mseWord');
$q->select($this->modx->getSelectColumns('mseWord', 'mseWord'));
$q->where(array('word:IN' => array_keys($search_forms), 'field:IN' => array_keys($this->fields)));
$tstart = microtime(true);
if ($q->prepare() && $q->stmt->execute()) {
$this->modx->queryTime += microtime(true) - $tstart;
$this->modx->executedQueries++;
while ($row = $q->stmt->fetch(PDO::FETCH_ASSOC)) {
$weight = $this->fields[$row['field']] * $row['count'];
// Add to results
if (!isset($index[$row['resource']])) {
$index[$row['resource']] = array(
'words' => array(
$row['word'] => $weight,
),
'weight' => $weight,
);
} else {
if (!isset($index[$row['resource']][$row['word']])) {
$index[$row['resource']]['words'][$row['word']] = $weight;
} else {
$index[$row['resource']]['words'][$row['word']] += $weight;
}
$index[$row['resource']]['weight'] += $weight;
}
// Search by INDEX debug
$debug[$row['resource']][] = array(
'field' => $row['field'],
'word' => $row['word'],
'count' => $row['count'],
'weight' => $this->fields[$row['field']],
'total' => $weight,
);
}
}
}
$message = '';
$leaved = $removed = 0;
$result = array();
if (!empty($index)) {
$this->log('Found results by words INDEX (' . implode(',', $bulk_words) . '): <b>' . count($index) . '</b>');
$all_forms = $this->getAllForms($bulk_words);
foreach ($index as $k => $v) {
$not_found = array();
foreach ($bulk_words as $word) {
if (!isset($all_forms[$word])) {
$not_found[] = $word;
} elseif (!array_intersect($all_forms[$word], array_keys($v['words']))) {
$not_found[] = $word;
}
}
if (!empty($not_found)) {
if ($this->simpleSearch(implode(' ', $not_found), false, $k)) {
foreach ($not_found as $word) {
$index[$k]['words'][$word] = $this->config['like_match_bonus'];
$index[$k]['weight'] += $this->config['like_match_bonus'];
$message .= "\n\t+ {$this->config['like_match_bonus']} points to resource {$k} for word \"{$word}\" in LIKE search";
}
} else {
unset($index[$k]);
$message .= "\n\t- resource {$k} because it does not contain all necessary words";
$removed++;
continue;
}
}
foreach ($debug[$k] as $v2) {
$message .= "\n\t+ {$v2['total']} points to resource {$k} for word \"{$v2['word']}\" in field \"{$v2['field']}\" ({$v2['count']} * {$v2['weight']})";
}
$result[$k] = $index[$k]['weight'];
$leaved++;
}
$this->log("Filtering results of INDEX search: <b>{$leaved}</b> leaved, <b>{$removed}</b> removed." . $message);
} else {
$this->log('Nothing found by words INDEX');
}
if (empty($this->config['onlyIndex'])) {
$added = 0;
$like = $this->simpleSearch($query, false);
$message = '';
foreach ($like as $k) {
if (!isset($result[$k])) {
$result[$k] = $this->config['like_match_bonus'];
$message .= "\n\t+ {$this->config['like_match_bonus']} points to resource {$k} for words \"" . implode(',', $bulk_words) . "\"";
$added++;
}
}
if ($added) {
$this->log("Added results by LIKE search: <b>{$added}</b>" . $message);
}
if (count($bulk_words) > 1) {
if ($exact = $this->simpleSearch($query, true, array_keys($result))) {
$message = 'Trying to apply "exact_match_bonus":';
foreach ($exact as $k) {
$result[$k] += $this->config['exact_match_bonus'];
$message .= "\n\t+ {$this->config['exact_match_bonus']} to resource {$k}";
}
$this->log($message);
}
}
}
// Sort results by weight
arsort($result);
// Log the search query
$query = preg_replace('#[^\w\s-]#iu', '', $query);
/** @var mseQuery $object */
if ($object = $this->modx->getObject('mseQuery', array('query' => $query))) {
$object->set('quantity', $object->get('quantity') + 1);
} else {
$object = $this->modx->newObject('mseQuery');
$object->set('query', $query);
$object->set('quantity', 1);
}
$object->set('found', count($result));
$object->save();
return $result;
}
/**
* Search and return array with resources ids as a key and sum of weight as value
*
* @param $query
*
* @deprecated
*
* @return array
*/
public function OldSearch($query) {
$string = preg_replace('/[^_-а-яёa-z0-9\s\.\/]+/iu', ' ', $this->modx->stripTags($query));
$this->log('Filtered search query: "' . mb_strtolower($query . '"'));
$query = $this->query = $this->addAliases($string);
$this->log('Search query with processed aliases: "' . mb_strtolower($query . '"'));
$words = $this->getBaseForms($query, false);
$result = $all_words = $found_words = $debug = array();
// Search by words index
if (!empty($words)) {
$q = $this->modx->newQuery('mseWord');
$q->select($this->modx->getSelectColumns('mseWord', 'mseWord'));
$q->where(array('word:IN' => array_keys($words), 'field:IN' => array_keys($this->fields)));
$tstart = microtime(true);
if ($q->prepare() && $q->stmt->execute()) {
$this->modx->queryTime += microtime(true) - $tstart;
$this->modx->executedQueries++;
while ($row = $q->stmt->fetch(PDO::FETCH_ASSOC)) {
$weight = $this->fields[$row['field']] * $row['count'];
// Add to results
if (isset($result[$row['resource']])) {
$result[$row['resource']] += $weight;
}
else {
$result[$row['resource']] = $weight;
}
// Search by INDEX debug
$debug[$row['resource']][] = array(
'field' => $row['field'],
'word' => $row['word'],
'count' => $row['count'],
'weight' => $this->fields[$row['field']],
'total' => $weight,
);
if (isset($words[$row['word']])) {
$all_words[$row['resource']][$words[$row['word']]] = 1;
$found_words[$words[$row['word']]] = 1;
}
}
}
}
$added = 0;
if (!empty($found_words)) {
$message = 'Found results by words INDEX (' . implode(',', array_keys($found_words)) . '): ' . count($result);
ksort($debug);
foreach ($debug as $k => $v) {
foreach ($v as $v2) {
$message .= "\n\t+ {$v2['total']} points to resource {$k} for word \"{$v2['word']}\" in field \"{$v2['field']}\" ({$v2['count']} * {$v2['weight']})";
}
}
$this->log($message);
}
else {
$this->log('Nothing found by words INDEX');
}
// Add bonuses
$bulk_words = $this->getBulkWords($query);
if (!empty($this->config['all_words_bonus'])) {
if (count($bulk_words) > 1) {
// All words bonus
foreach ($all_words as $k => $v) {
if (count($bulk_words) == count($v)) {
$result[$k] += $this->config['all_words_bonus'];
$this->log("+ {$this->config['all_words_bonus']} points to resource {$k} for system setting \"all_words_bonus\" with words \"" . implode(', ', $bulk_words) . "\"");
}
elseif (!empty($this->config['onlyAllWords'])) {
unset($result[$k]);
$this->log("Resource {$k} was removed because of enabled &onlyAllWords parameter with words \"" . implode(', ', array_keys($v)) . "\"");
}
}
}
}
if (empty($this->config['onlyIndex'])) {
$tmp_words = preg_split($this->config['split_words'], $query, -1, PREG_SPLIT_NO_EMPTY);
if (count($bulk_words) > 1 || count($tmp_words) > 1 /*|| empty($result)*/) {
if (!empty($this->config['exact_match_bonus']) || !empty($this->config['all_words_bonus'])) {
$exact = $this->simpleSearch($query);
// Exact match bonus
if (!empty($this->config['exact_match_bonus'])) {
foreach ($exact as $v) {
if (isset($result[$v])) {
$result[$v] += $this->config['exact_match_bonus'];
$this->log("+ {$this->config['exact_match_bonus']} points to resource {$v} for system setting \"exact_match_bonus\" with words \"{$query}\"");
}
else {
$result[$v] = $this->config['exact_match_bonus'];
$this->log("+ {$this->config['exact_match_bonus']} points to new resource {$v} that was found by LIKE search with exact matched words \"{$query}\"");
$added++;
}
}
}
}
}
elseif (!empty($this->config['like_match_bonus'])) {
// Add the whole possible results for query
$all_results = $this->simpleSearch($query);
foreach ($all_results as $v) {
if (!isset($result[$v])) {
$weight = round($this->config['like_match_bonus']);
$result[$v] = $weight;
$this->log("+ {$weight} points to resource {$v} by LIKE search with words \"{$query}\"");
$added++;
}
}
}
// Add matches by %LIKE% search
if (!empty($this->config['like_match_bonus']) && $not_found = array_diff($bulk_words, $words)) {
foreach ($not_found as $word) {
$found = $this->simpleSearch($word);
foreach ($found as $v) {
$weight = round($this->config['like_match_bonus']);
if (!isset($result[$v])) {
$result[$v] = $weight;
$this->log("+ {$weight} points to new resource {$v} that was found by LIKE search with single word \"{$word}\"");
$added++;
}
else {
$result[$v] += $weight;
$this->log("+ {$weight} points to resource {$v} by LIKE search for word \"{$word}\"");
}
}
}
}
$this->log('Added resources by LIKE search: ' . $added);
}
// Log the search query
$query = preg_replace('#[^\w\s-]#iu', '', $query);
/** @var mseQuery $object */
if ($object = $this->modx->getObject('mseQuery', array('query' => $query))) {
$object->set('quantity', $object->get('quantity') + 1);
}
else {
$object = $this->modx->newObject('mseQuery');
$object->set('query', $query);
$object->set('quantity', 1);
}
$object->set('found', count($result));
$object->save();
arsort($result);
return $result;
}
/**
* Search and return array with resources that matched for LIKE search
*
* @param $query
* @param bool $exact
* @param array $resources
*
* @return array
*/
public function simpleSearch($query, $exact = true, $resources = array()) {
$string = $this->modx->stripTags($query);
$result = array();
$q = $this->modx->newQuery('mseIntro');
$q->select('resource');
if ($exact && $string) {
$q->where(array('intro:LIKE' => '%' . $string . '%'));
} elseif ($bulk_words = $this->getBulkWords($string, $this->config['split_all'])) {
foreach ($bulk_words as $word) {
$q->andCondition("intro LIKE '%{$word}%'");
}
} else {
return $result;
}
if (!empty($resources)) {
if (!is_array($resources)) {
$q->where(array('resource' => $resources));
}
else {
$q->where(array('resource:IN' => $resources));
}
}
$tstart = microtime(true);
if ($q->prepare() && $q->stmt->execute()) {
$this->modx->queryTime += microtime(true) - $tstart;
$this->modx->executedQueries++;
$result = $q->stmt->fetchAll(PDO::FETCH_COLUMN);
}
return $result;
}
/**
* @param $string
*
* @return string
*/
public function addAliases($string) {
$string = mb_strtoupper(str_ireplace('ё', 'е', $this->modx->stripTags($string)));
$string = preg_replace('#\[.*\]#isU', '', $string);
$pcre = $this->config['split_words'];
$words = preg_split($pcre, $string, -1, PREG_SPLIT_NO_EMPTY);
foreach ($words as $k => $v) {
if (!preg_match('/^[0-9]{2,}$/', $v) && mb_strlen($v) < $this->config['min_word_length']) {
unset($words[$k]);
}
}
$words = array_unique($words);
$forms = $this->getBaseForms($words, false);
if (!$words && !$forms) {
return '';
}
$q = $this->modx->newQuery('mseAlias', array('word:IN' => array_merge($words, array_keys($forms))));
$q->select('word,alias,replace');
$tstart = microtime(true);
if ($q->prepare() && $q->stmt->execute()) {
$this->modx->queryTime += microtime(true) - $tstart;
$this->modx->executedQueries++;
while ($row = $q->stmt->fetch(PDO::FETCH_ASSOC)) {
if ($row['replace']) {
$all = current($this->getAllForms($row['word']));
$all[] = $row['word'];
foreach ($all as $word) {
$key = array_search(mb_strtoupper($word), array_map('mb_strtoupper', $words), false);
if ($key !== false) {
$words[$key] = $row['alias'];
break;
}
}
}
else {
$words[] = $row['alias'];
}
}
}
$words = array_unique(array_map('mb_strtoupper', $words));
return implode(' ', $words);
}
/**
* @param $string
*
* @return array
*/
public function getAliases($string) {
$string = mb_strtoupper(str_ireplace('ё', 'е', $this->modx->stripTags($string)));
$string = preg_replace('#\[.*\]#isU', '', $string);
$words = array_map('mb_strtoupper', array_unique($this->getBulkWords($string)));
$forms = $this->getBaseForms($words, false);
if (!$words && !$forms) {
return array();
}
$aliases = array();
$c = $this->modx->newQuery('mseAlias', array('word:IN' => array_merge($words, array_keys($forms))));
$c->select('word, alias, replace');
$c->sortby('`replace`', 'DESC');
$tstart = microtime(true);
if ($c->prepare() && $c->stmt->execute()) {
$this->modx->queryTime += microtime(true) - $tstart;
$this->modx->executedQueries++;
while ($row = $c->stmt->fetch(PDO::FETCH_ASSOC)) {
if (!$all = current($this->getAllForms($row['word']))) {
$all = array($row['word']);
}
if ($row['replace']) {
$string = preg_replace('#\b('.implode('|', $all).')\b#iu', $row['alias'], $string);
} else {
$aliases[] = $string . ' ' . $row['alias'];
}
}
}
return array_merge(array($string), $aliases);
}
/**
* Highlight search string in given text string
*
* @param $text
* @param string $query
* @param string $htag_open
* @param string $htag_close
* @param boolean $strict
*
* @return mixed
*/
public function Highlight($text, $query, $htag_open = '<b>', $htag_close = '</b>', $strict = true) {
if (empty($query)) {
return $text;
}
$from = $to = array();
$tmp_words = preg_split($this->config['split_words'], $query, -1, PREG_SPLIT_NO_EMPTY);
// Exact match
$pcre = $strict
? '#\b' . preg_quote($query) . '\b#imus'
: '#' . preg_quote($query) . '#imus';
if (count($tmp_words) > 1 && preg_match($pcre, $text, $matches)) {
$pos = mb_stripos($text, $matches[0], 0);
if ($pos >= $this->config['introCutBefore']) {
$text_cut = '... ';
$pos -= $this->config['introCutBefore'];
}
else {
$text_cut = '';
$pos = 0;
}
$text_cut .= mb_substr($text, $pos, $this->config['introCutAfter']);
if (mb_strlen($text) > $this->config['introCutAfter']) {
$text_cut .= ' ...';
}
foreach ($matches as $v) {
$from[$v] = $v;
$to[$v] = $htag_open . $v . $htag_close;
}
}
// Matching by separate words
else {
if (empty($this->query)) {
$this->query = $this->addAliases($query);
}
$tmp = array_merge(
$tmp_words,
explode(' ', $this->query)
);
$tmp = array_unique($tmp);
$tmp = $this->getAllForms($tmp);
$words = array_keys($tmp);
foreach ($tmp as $v) {
$words = array_merge($words, $v);
}
if (empty($words)) {
$words = array($query => $query);
}
$text_cut = '';
foreach ($words as $key => $word) {
/*
if (!preg_match('/^[0-9]{2,}$/', $word) && mb_strlen($word) < $this->config['min_word_length']) {
unset($words[$key]);
continue;
}
*/
$word = preg_quote($word, '/');
$words[$key] = $word;
// Cutting text on first occurrence
$pcre = $strict ? '/\b' . $word . '\b/imu' : '/' . $word . '/imu';
if (empty($text_cut) && !empty($word) && preg_match($pcre, $text, $matches)) {
$pos = mb_stripos($text, $matches[0], 0);
if ($pos >= $this->config['introCutBefore']) {
$text_cut = '... ';
$pos -= $this->config['introCutBefore'];
}
else {
$text_cut = '';
$pos = 0;
}
$text_cut .= mb_substr($text, $pos, $this->config['introCutAfter']);
if (mb_strlen($text) > $this->config['introCutAfter']) {
$text_cut .= ' ...';
}
}
}
if (empty($text_cut) && $strict) {
return $this->Highlight($text, $query, $htag_open, $htag_close, false);
}
$pcre = $strict ? '/\b(' . implode('|', $words) . ')\b/imu' : '/(' . implode('|', $words) . ')/imu';
if (preg_match_all($pcre, $text_cut, $matches)) {
foreach ($matches[0] as $v) {
$from[$v] = $v;
$to[$v] = $htag_open . $v . $htag_close;
}
}
if (!empty($matches[1])) {
foreach ($matches[1] as $v) {
$from[$v] = $v;
$to[$v] = $htag_open . $v . $htag_close;
}
}
elseif ($strict) {
return $this->Highlight($text, $query, $htag_open, $htag_close, false);
}
}
return str_replace($from, $to, $text_cut);
}
/**
* Return array with filters
*
* @param array|string $ids
* @param boolean $build
*
* @return array|boolean
*/
public function getFilters($ids, $build = true) {
// prepare ids
if (!is_array($ids)) {
$ids = array_map('trim', explode(',', $ids));
}
if (empty($ids)) {
return false;
}
// Return results from cache
if ($build && $cache = $this->modx->cacheManager->get('msearch2/prep_' . md5(implode(',', $ids) . $this->config['filters']))) {
$this->methods = $cache['methods'];
$this->aliases = $cache['aliases'];
return $cache['filters'];
}
// elseif (!$build && !empty($this->filters)) {
// return $this->filters;
// }
elseif (!$build && $cache = $this->modx->cacheManager->get('msearch2/prep_' . md5(implode(',', $ids) . $this->config['filters']))) {
$this->methods = $cache['methods'];
$this->aliases = $cache['aliases'];
$filters = $cache['filters'];
$filters2 = [];
foreach($filters as $k=>$v){
@list($table, $filter) = explode($this->config['filter_delimeter'], $k);
foreach($v as $k2=>$v2){
$filters2[$table][$filter][$k2] = $v2['resources'];
}
}
$this->filters = $filters2;
unset($filters);unset($filters2);
return $this->filters;
}
// elseif (!$build && $cache = $this->modx->cacheManager->get('msearch2/fltr_' . md5(implode(',', $ids) . $this->config['filters']))) {
// $this->methods = $cache['methods'];
// $this->aliases = $cache['aliases'];
// return $cache['filters'];
// }
if (!is_object($this->filtersHandler)) {
$this->loadHandler();
}
if (empty($this->filters) || empty($this->methods)) {
// Preparing filters
$filters = $built = $duplicates = array();
$tmp_filters = array_map('trim', explode(',', $this->config['filters']));
foreach ($tmp_filters as $v) {
$v = strtolower($v);
if (empty($v)) {
continue;
}
elseif (strpos($v, $this->config['filter_delimeter']) !== false) {
@list($table, $filter) = explode($this->config['filter_delimeter'], $v);
}
else {
$table = 'resource';
$filter = $v;
}
$tmp = explode($this->config['method_delimeter'], $filter);
$name = $tmp[0];
$filter = !empty($tmp[1])
? $tmp[1]
: 'default';
// Duplicates
if (isset($filters[$table][$name])) {
$old_filter = $built[$table . $this->config['filter_delimeter'] . $name];
$new_name = $name . '-' . $old_filter;
$built[$table . $this->config['filter_delimeter'] . $new_name] = $old_filter;
$filters[$table][$new_name] = $filters[$table][$name];
$duplicates[$table][$new_name] = $name;
$new_name = $name . '-' . $filter;
$duplicates[$table][$new_name] = $name;
$name = $new_name;
}
$filters[$table][$name] = array();
$built[$table . $this->config['filter_delimeter'] . $name] = $filter;
}
// Retrieving filters
foreach ($filters as $table => &$fields) {
$method = 'get' . ucfirst($table) . 'Values';
$keys = !empty($duplicates[$table])
? array_diff(array_keys($fields), array_keys($duplicates[$table]))
: array_keys($fields);
if (method_exists($this->filtersHandler, $method)) {
$fields = call_user_func_array(array($this->filtersHandler, $method), array($keys, $ids));
if (!empty($duplicates[$table])) {
foreach ($duplicates[$table] as $key => $field) {
$fields[$key] = $fields[$field];
}
}
}
else {
$this->modx->log(modX::LOG_LEVEL_ERROR, '[mSearch2] Method "' . $method . '" not exists in class "' . get_class($this->filtersHandler) . '". Could not retrieve filters from "' . $table . '"');
}
}
unset($fields);
// Remove duplicates
foreach ($duplicates as $table => $fields) {
$tmp = array_unique($fields);
foreach ($tmp as $tmp2) {
unset($filters[$table][$tmp2]);
unset($built[$table . $this->config['filter_delimeter'] . $tmp2]);
}
}
// Prepare aliases
$aliases = array();
if (!empty($this->config['aliases'])) {
$tmp = array_map('trim', explode(',', $this->config['aliases']));
foreach ($tmp as $v) {
if (strpos($v, '==') !== false) {
$tmp2 = array_map('trim', explode('==', $v));
$aliases[str_replace('.', '_', $tmp2[0])] = $tmp2[1];
}
}
$this->aliases = $aliases;
}
$this->filters = $filters;
$this->methods = $built;
}
// The replacement dots to underscores in filters names
foreach ($this->filters as $table => $fields) {
foreach ($fields as $key => $values) {
if (strpos($key, '.') !== false) {
$this->filters[$table][str_replace('.', '_', $key)] = $values;
unset($this->filters[$table][$key]);
}
}
}
if (!$build) {
return $this->filters;
}
$built = $this->methods;
$prepared = array();
foreach ($this->filters as $table => $filters) {
foreach ($filters as $key => $values) {
$new_key = $table . $this->config['filter_delimeter'] . $key;
$filter = !empty($built[$new_key])
? $built[$new_key]
: 'default';
$method = 'build' . ucfirst($filter) . 'Filter';
if ($filter == 'default') {
switch ($table) {
case 'tv':
$method = 'buildTVsFilter';
break;
case 'msoption':
$method = 'buildOptionsFilter';
break;
}
}
if (method_exists($this->filtersHandler, $method)) {
$prepared[$new_key] = call_user_func_array(array($this->filtersHandler, $method), array($values, $key));
}
elseif (method_exists($this->filtersHandler, 'buildDefaultFilter')) {
$prepared[$new_key] = call_user_func_array(array($this->filtersHandler, 'buildDefaultFilter'), array($values, $key));
}
else {
$this->modx->log(modX::LOG_LEVEL_ERROR, '[mSearch2] Method "' . $method . '" not exists in class "' . get_class($this->filtersHandler) . '". Could not build filter for "' . $new_key . '"');
$prepared[$new_key] = $values;
}
}
}
// Sort filters
foreach ($built as $key => &$values) {
if (isset($prepared[$key])) {
$values = $prepared[$key];
unset($prepared[$key]);
}
}
// Add new generated filters to the end of list
$built = array_merge($built, $prepared);
$cache = array(
'filters' => $built,
'methods' => $this->methods,
'aliases' => $this->aliases,
);
// Set cache
if (!empty($this->config['cacheTime'])) {
$this->modx->cacheManager->set('msearch2/prep_' . md5(implode(',', $ids) . $this->config['filters']), $cache, $this->config['cacheTime']);
}
return $built;
}
/**
* Filters resources by given params
*
* @param array|string $ids
* @param array $request
*
* @return array
*/
public function Filter($ids, array $request) {
if (!is_array($ids)) {
$ids = explode(',', $ids);
}
if (!is_object($this->filtersHandler)) {
$this->loadHandler();
}
//$this->pdoTools->addTime('Getting filters2 for '.count($ids).' ids');
$this->getFilters($ids, false);
//$this->pdoTools->addTime('End Getting filters2 for '.count($ids).' ids');
$filters = $this->filters;
$methods = $this->methods;
$aliases = array_flip($this->aliases);
//$this->pdoTools->addTime('End Getting filters2 for '.print_r($filters,1).' ids');
//$this->pdoTools->addTime('ids1');
foreach ($request as $filter => $requested) {
if (!empty($aliases[$filter])) {
$filter = $aliases[$filter];
}
if (!preg_match('/(.*?)' . preg_quote($this->config['filter_delimeter'], '/') . '(.*?)/', $filter)) {
continue;
}
$method = !empty($methods[$filter])
? 'filter' . ucfirst($methods[$filter])
: 'filterDefault';
list($table, $filter) = explode($this->config['filter_delimeter'], $filter);
//$this->pdoTools->addTime("table $table filter $filter ".print_r($filters,1));
if (isset($filters[$table][$filter])) {
$values = $filters[$table][$filter];
$requested = array_map('rawurldecode', explode($this->config['values_delimeter'], $requested));
if (method_exists($this->filtersHandler, $method)) {
$ids = call_user_func_array(array($this->filtersHandler, $method), array($requested, $values, $ids));
//$this->pdoTools->addTime('ids2');
}
else {
//$this->modx->log(modX::LOG_LEVEL_ERROR, '[mSearch2] Method "'.$method.'" not exists in class "'.get_class($this->filtersHandler).'". Could not build filter "'.$table.$this->config['filter_delimeter'].$filter.'"');
$ids = @call_user_func_array(array($this->filtersHandler, 'filterDefault'), array($requested, $values, $ids));
//$this->pdoTools->addTime('ids21');
}
}
$this->filter_operations++;
}
//$this->pdoTools->addTime('ids3 '.print_r($ids,1).' ids');
return $ids;
}
/**
* This method returns preliminary results for each filter
*
* @param $ids
* @param array $request
* @param array $current
*
* @return array
*/
public function getSuggestions($ids, array $request, array $current = array()) {
if (!is_array($ids)) {
$ids = explode(',', $ids);
}
if (!is_object($this->filtersHandler)) {
$this->loadHandler();
}
foreach ($request as $k => $v) {
$request[$k] = str_replace('"', '&quot;', $v);
}
if (method_exists($this->filtersHandler, 'getSuggestions')) {
return $this->filtersHandler->getSuggestions($ids, $request, $current);
}
else {
$current = array_flip($current);
$filters = $this->getFilters($ids, false);
$built = $this->getFilters($ids, true);
$radio = $this->config['suggestionsRadio'];
$aliases = $this->aliases;
$suggestions = array();
foreach ($filters as $table => $fields) {
foreach ($fields as $field => $values) {
$key = $alias = $table . $this->config['filter_delimeter'] . $field;
if (!empty($aliases[$key])) {
$alias = $aliases[$key];
}
$tmp = current($built[$key]);
if (empty($tmp['type']) || $tmp['type'] != 'number') {
$tmp = $built[$key];
$values = array();
foreach ($tmp as $v) {
$values[] = $v['value'];
}
} elseif (!empty($this->config['suggestionsSliders'])) {
$values = array_keys($values);
} else {
continue;
}
foreach ($values as $value) {
$suggest = $request;
$added = 0;
if (isset($request[$alias])) {
// Types of suggestion can depend from method
if (!empty($radio) && in_array($key, $radio)) {
$suggest[$alias] = $value;
} elseif ($tmp['type'] == 'number') {
$tmp2 = explode($this->config['values_delimeter'], $request[$alias]);
if ($value <= $tmp2[0]) {
$tmp2[0] = $value;
} else {
$tmp2[1] = $value;
}
$suggest[$alias] = implode($this->config['values_delimeter'], $tmp2);
} else {
$tmp2 = explode($this->config['values_delimeter'], $request[$alias]);
if (!in_array($value, $tmp2)) {
$suggest[$alias] .= $this->config['values_delimeter'] . $value;
$added = 1;
}
}
$res = $this->Filter($ids, $suggest);
if ($added && !empty($res)) {
foreach ($res as $k => $v) {
if (isset($current[$v])) {
unset($res[$k]);
}
}
$count = count($res);
if ($count != 0) {
$count = '+' . $count;
}
}
else {
$count = count($res);
}
}
else {
$suggest[$alias] = $value;
$res = $this->Filter($ids, $suggest);
$count = count($res);
}
$suggestions[$alias][$value] = $count;
}
}
}
return $suggestions;
}
}
/**
* Method for transform array to placeholders
*
* @var array $array With keys and values
* @var string $prefix
*
* @return array $array Two nested arrays With placeholders and values
*/
public function makePlaceholders(array $array = array(), $prefix = '') {
$result = array(
'pl' => array(),
'vl' => array(),
);
foreach ($array as $k => $v) {
if (is_array($v)) {
$result = array_merge_recursive($result, $this->makePlaceholders($v, $prefix . $k . '.'));
}
else {
$result['pl'][$prefix . $k] = '[[+' . $prefix . $k . ']]';
$result['vl'][$prefix . $k] = $v;
}
}
return $result;
}
/**
* Returns string for insert into sorting properties of pdoTools snippet
*
* @param string
*
* @return string
*/
public function getSortFields($sort) {
$this->loadHandler();
return $this->filtersHandler->getSortFields($sort);
}
/**
* Loads custom filters handler class
*
* @return bool
*/
public function loadHandler() {
if (!is_object($this->filtersHandler)) {
require_once 'filters.class.php';
$filters_class = $this->modx->getOption('mse2_filters_handler_class', null, 'mse2FiltersHandler', true);
if ($filters_class != 'mse2FiltersHandler') {
$this->loadCustomClasses('filters');
}
if (!class_exists($filters_class)) {
$filters_class = 'mse2FiltersHandler';
}
$this->filtersHandler = new $filters_class($this, $this->config);
if (!($this->filtersHandler instanceof mse2FiltersHandler)) {
$this->modx->log(modX::LOG_LEVEL_ERROR, '[mSearch2] Could not initialize filters handler class: "' . $filters_class . '"');
return false;
}
}
return true;
}
/**
* Checks for exists of miniShop2
*
* @return bool
*/
public function checkMS2() {
return file_exists(MODX_CORE_PATH . 'components/minishop2/model/minishop2/msproduct.class.php');
}
/**
* @param $entry
*/
public function log($entry) {
if ($this->pdoTools && !empty($this->config['showSearchLog'])) {
$this->pdoTools->addTime('[mSearch2] ' . $entry);
}
}
/**
*
*/
protected function checkStat() {
}
}
@touol
Copy link
Author

touol commented Oct 20, 2021

Мои правки только в функции public function getFilters($ids, $build = true) {
А сам файл может быть от другой версии mSearch2 и при полном копировании может вызывать ошибки. Попробуйте только функцию скопировать.
И при вызове mFilter2 ставить &cacheTime число больше 0.

@ychelovek
Copy link

Заменил только эту функцию. Ничего не поменялось. Печаль. Он так ускорял работу(

@touol
Copy link
Author

touol commented Oct 20, 2021

Пишите мне на скайп touols или на почту touols@yandex.ru. Посмотрим что у вас не работает. Только это платно 600р час.

@touol
Copy link
Author

touol commented Oct 26, 2021

Вчера применял решение на сайт с 50кило товаров. Выяснилось несколько недоработок. Наверно "фильтры типа boolean и типа vendors" в итоге правок будут работать. Но мне не охота отдавать решение бесплатно. Был бы mFilter2 беслатным, то вопросов нет давно бы сделал пул реквест. А так жаба давит. Я тут напрягался дебажил придумывал, а разрабы, если опубликую решение, к себе применят и будут компонент еще лучше продовать деньги зашибать :-).
Всем кому нужно, предлогаю, 1000р решение включая 20минут применить и 600р в час оптимизация страниц каталога.
Например, вчера в чанке выявился сниппет на 6-10с грузящий страницу. Его тоже закешировал и страница каталога на 3500 товаров стала выдаваться за 1,5 секунды вместо 25с.
Если вдруг разрабы mSearch2 захотят приобрести решение, то продам за 15 000р :-)

@ychelovek
Copy link

Напишу в скайпе

@dimasites
Copy link

dimasites commented Nov 15, 2021

Если вдруг разрабы mSearch2 захотят приобрести решение, то продам за 15 000р :-)

@touol Привет! Так запости может краудфандинг или типа того на modx.pro, а то блин есть проблема с платными компонентами что если автор не добавляет фиксов, то их вообще нет)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment