Skip to content

Instantly share code, notes, and snippets.

@MadFaill
Created December 6, 2015 10:43
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 MadFaill/0c576a5f9c555a9a1aae to your computer and use it in GitHub Desktop.
Save MadFaill/0c576a5f9c555a9a1aae to your computer and use it in GitHub Desktop.
Quto.ru new search (with MONGO)
{
"_id" : ObjectId("7d04bbbe5494ae9d2f5a76aa"),
"car_modification_id" : 486,
"title" : "Audi A4 2.0 TFSI quattro AMT",
"price_min" : 1503100,
"price_max" : 2443936,
"car_brand_id" : 33,
"car_model_id" : 62,
"car_model_sub_generation_id" : 470,
"car_body_id" : 1,
"car_class_id" : 4,
"car_type_id" : 1,
"main_info" : {
"modification_url" : "/Audi/A4/b8/sedan4d/20TFSIAMT_Quattro/",
"generation_url" : "/Audi/A4/b8/sedan4d/",
"image_hash" : "4bb9e5fac9f59.jpeg",
"brand" : "Audi",
"model" : "A4",
"brand_model_title" : "Audi A4 седан",
"modification_title" : "2.0 TFSI quattro AMT Базовая",
"model_sub_title" : "A4 седан",
"horsepower" : 211,
"fuel" : "бензин",
"gear" : "полный",
"is_swf" : false,
"is_new" : "1",
"url" : {
"brand" : "Audi",
"model" : "A4",
"model_sub" : "sedan4d",
"model_gen" : "b8",
"modification" : "20TFSIAMT_Quattro"
}
},
"property_60" : "made_in_germany",
"property_58" : "fr",
"property_56" : "robot",
"property_62" : "benzin",
"property_67" : "european",
"property_8" : 1984,
"property_10" : 211,
"property_44" : 6.5,
"property_45" : 7.5,
"property_34" : 0,
"property_61" : 0,
"property_16" : 1,
"property_29" : 0,
"property_28" : 0,
"property_30" : 0,
"property_27" : 0,
"property_39" : 0,
"property_61_min" : 5,
"property_61_max" : 5,
"advanced_options" : [
20,
22,
31,
40,
42,
59,
64,
69,
80,
81,
94,
101,
103,
122,
131,
149,
193,
198,
135,
117,
88,
93,
149,
144,
198,
84
],
"advanced_options_price" : {
"20" : 60918,
"22" : 4446,
"31" : 54288,
"40" : 14664,
"42" : 19422,
"59" : 19968,
"64" : 11856,
"69" : 48750,
"80" : 16614,
"81" : 14976,
"94" : 13572,
"101" : 155610,
"103" : 68094,
"122" : 34086,
"131" : 14664,
"149" : 29094,
"193" : 30498,
"198" : 43212,
"135" : 14976,
"117" : 155610,
"88" : 0,
"93" : 0,
"144" : 0,
"84" : 43212
},
"base_options" : [
1,
2,
6,
8,
14,
15,
16,
39,
48,
58,
61,
62,
63,
66,
74,
85,
104,
3,
9,
135,
117,
75,
76,
88,
93,
149,
144,
116,
198,
84,
20,
22,
31,
40,
42,
59,
64,
69,
80,
81,
94,
101,
103,
122,
131,
193
],
"ignore_options" : [],
"linked_options" : {
"1" : "138289",
"2" : "138290",
"6" : "138293",
"8" : "138294",
"14" : "138297",
"15" : "138298",
"16" : "138299",
"20" : "233642",
"22" : "138304",
"31" : "138314",
"39" : "138319",
"40" : "138320",
"42" : "138322",
"48" : "138329",
"52" : "205568",
"58" : "138335",
"59" : "138336",
"61" : "138337",
"62" : "138338",
"63" : "138339",
"64" : "138340",
"66" : "138341",
"69" : "138342",
"74" : "138347",
"80" : "138350",
"81" : "138351",
"85" : "138356",
"87" : "224080",
"90" : "138367",
"92" : "138365",
"94" : "138368",
"101" : "138364",
"103" : "138358",
"104" : "138371",
"122" : "138349",
"131" : "138308",
"144" : "233648",
"149" : "138306",
"193" : "138307",
"198" : "138357",
"3" : "138290",
"9" : "138294",
"135" : "138351",
"117" : "138364",
"75" : "138347",
"76" : "138347",
"88" : "224080",
"93" : "138365",
"116" : "138356",
"84" : "138357"
},
"brand_models_available" : [
62,
190,
240,
266,
267,
448,
460,
461,
462,
463,
464,
465,
466,
467,
468,
469,
470,
471,
472,
551,
552,
553,
554,
555,
563
]
}
[
{
"$match": {
"$and": [
{
"car_type_id": 1
},
{
"price_min": {
"$gte": 0
}
},
{
"price_min": {
"$lte": 4000000
}
},
{
"car_body_id": {
"$in": [1]
}
},
{
"base_options": {
"$in": [74]
}
},
{
"base_options": {
"$all": [83, 122, 337]
}
},
{
"ignore_options.83": {
"$nin": [122, 74, 337]
}
},
{
"ignore_options.122": {
"$nin": [83, 74, 337]
}
},
{
"ignore_options.74": {
"$nin": [83, 122, 337]
}
},
{
"ignore_options.337": {
"$nin": [83, 122, 74]
}
}
]
}
},
{
"$group": {
"_id": "$car_model_sub_generation_id",
"brand": {
"$first": "$car_brand_id"
},
"model": {
"$first": "$car_model_id"
},
"sub_generation_id": {
"$first": "$car_model_sub_generation_id"
},
"is_new": {
"$first": "$main_info.is_new"
},
"is_swf": {
"$first": "$main_info.is_swf"
},
"modifications": {
"$addToSet": {
"title": "$title",
"id": "$car_modification_id",
"price_min": "$price_min",
"price_max": "$price_max",
"price": {
"$add": ["$price_min", {
"$cond": [
{
"$ne": ["$advanced_options_price.83", []]
},
{
"$cond": [
{
"$gt": ["$advanced_options_price.83", 0]
},
"$advanced_options_price.83",
0
]
},
0
]
}, {
"$cond": [
{
"$ne": ["$advanced_options_price.122", []]
},
{
"$cond": [
{
"$gt": ["$advanced_options_price.122", 0]
},
"$advanced_options_price.122",
0
]
},
0
]
}, {
"$cond": [
{
"$ne": ["$advanced_options_price.74", []]
},
{
"$cond": [
{
"$gt": ["$advanced_options_price.74", 0]
},
"$advanced_options_price.74",
0
]
},
0
]
}, {
"$cond": [
{
"$ne": ["$advanced_options_price.337", []]
},
{
"$cond": [
{
"$gt": ["$advanced_options_price.337", 0]
},
"$advanced_options_price.337",
0
]
},
0
]
}]
},
"options_with_prices": {
"PRICE": "$price_min",
"OP_83": {
"$cond": [
{
"$ne": ["$advanced_options_price.83", []]
},
{
"$cond": [
{
"$gt": ["$advanced_options_price.83", 0]
},
"$advanced_options_price.83",
0
]
},
0
]
},
"OP_122": {
"$cond": [
{
"$ne": ["$advanced_options_price.122", []]
},
{
"$cond": [
{
"$gt": ["$advanced_options_price.122", 0]
},
"$advanced_options_price.122",
0
]
},
0
]
},
"OP_74": {
"$cond": [
{
"$ne": ["$advanced_options_price.74", []]
},
{
"$cond": [
{
"$gt": ["$advanced_options_price.74", 0]
},
"$advanced_options_price.74",
0
]
},
0
]
},
"OP_337": {
"$cond": [
{
"$ne": ["$advanced_options_price.337", []]
},
{
"$cond": [
{
"$gt": ["$advanced_options_price.337", 0]
},
"$advanced_options_price.337",
0
]
},
0
]
}
},
"main_info": "$main_info",
"power": "$property_10",
"linked_options": "$linked_options"
}
},
"modifications_count": {
"$sum": 1
}
}
},
{
"$unwind": "$modifications"
},
{
"$match": {
"modifications.price": {
"$gte": 0,
"$lte": 4000000
}
}
},
{
"$group": {
"_id": "$_id",
"modifications": {
"$addToSet": "$modifications"
},
"brand": {
"$first": "$brand"
},
"model": {
"$first": "$model"
},
"sub_generation_id": {
"$first": "$sub_generation_id"
},
"is_new": {
"$first": "$is_new"
},
"is_swf": {
"$first": "$is_swf"
},
"modifications_count": {
"$sum": 1
}
}
},
{
"$sort": {
"modifications.price": 1
}
},
{
"$skip": 0
},
{
"$limit": 10
}
]
<?php
namespace Quto\Common\Search;
class Result implements \ArrayAccess, \Iterator
{
/** @var array */
private $search_result, $pager, $last_search_params = array();
/** @var int */
private $key=0;
/** @var array линковка ТТХ */
private $ttx_links = array();
private $ttx_options_result_data = array(
// для опций а-ля ТТХ
183 => 'пневматическая',
131 => 'спортивная',
20 => 'с регулировкой жесткости',
// сами ТТХ
'engine_type' => array(
'benzin' => 'бензиновый',
'disel' => 'дизельный',
'hybride' => 'гибридный'
),
'control' => array(
'fr' => 'полный',
'f' => 'передний',
'r' => 'задний'
),
'transmission' => array(
'mex' => 'механическая',
'avt' => 'автоматическая',
'robot' => 'робот',
'variator' => 'вариатор'
)
);
/**
* @param array $search_result
* @param array $pager
* @param SearchCriteriaDefault $criteria
*/
public function __construct(array $search_result, array $pager, SearchCriteriaDefault $criteria)
{
$this->search_result = $search_result;
$this->pager = $pager;
$this->last_search_params = $criteria->getLastSearchParams();
$this->ttx_links = $criteria->list_ttx_options_links();
}
/**
* Кол-во результатов поиска (без среза)
*
* @return int
*/
public function totalCount()
{
return $this->pager['total_count'];
}
/**
* Размер среза (лимит записей при выводе)
*
* @return int
*/
public function limit()
{
return $this->pager['limit'];
}
/**
* Текущая страница (с учетом среза)
*
* @return int
*/
public function currentPage()
{
return $this->pager['page'];
}
/**
* Общее кол-во страниц с учетом среза
*
* @return int
*/
public function totalPagesCount()
{
return $this->pager['pages'];
}
/**
* Параметры поиска
* для JS фильтра
*
* @return array
*/
public function filterJsParams()
{
$filter = array();
if (isset($this->last_search_params['option']))
{
$options = (array)$this->last_search_params['option'];
foreach ($options as $op) {
$filter[] = 'option-'.$op;
}
}
return $filter;
}
/**
* Параметры поиска
* для smarty-фильтра
*
* @return array
*/
public function filterSmartyParams()
{
$params = array(
'ttx' => false,
);
foreach ($this->last_search_params as $param => $value)
{
if ('car_body_id' == $param)
{
$param = 'body';
$values = $value;
if (is_string($value)) {
$values = explode('+', $value);
}
$values = array_filter($values, function($v) { return (int)$v?true:false; });
$values = array_values($values);
$value = array_fill_keys($values, true);
}
if ('car_class_id' == $param)
{
$param = 'class';
$values = $value;
if (is_string($value)) {
$values = explode('+', $value);
}
$values = array_map(function($v){ return str_replace('class_', '', $v); }, $values);
$values = array_filter($values, function($v) { return (int)$v?true:false; });
$values = array_values($values);
$value = array_fill_keys($values, true);
}
if ('car_brand_id' == $param) {
$value = array_fill_keys((array)$value, true);
}
if ('car_model_id' == $param) {
$value = json_encode((array)$value);
}
// if ('transmission' == $param) {
// $value = array_fill_keys((array)$value, true);
// }
if ('countries' == $param) {
$value = array_fill_keys((array)$value, true);
}
if ('country_collected' == $param) {
$value = array_fill_keys((array)$value, true);
}
// TTX
if (array_key_exists($param, $this->ttx_links))
{
$p_key = $param;
if (strpos($param, '_min') || strpos($param, '_max')) {
$is_min = strpos($param, '_min')!==false;
$p_key = $is_min ? 'min' : 'max';
}
$params['ttx'] = true;
$ttx_key = $this->ttx_links[$param];
if (!array_key_exists($ttx_key, $params)) {
$params[$ttx_key] = array();
}
$params[$ttx_key][$p_key] = $value;
$params[$ttx_key][$param] = $value;
}
if (array_key_exists($param, $this->ttx_options_result_data))
{
$value = array_fill_keys((array) $value, true);
foreach ($value as $v=>$_) {
if (array_key_exists($v, $this->ttx_options_result_data[$param])) {
$value[$v] = $this->ttx_options_result_data[$param][$v];
}
}
}
if ('boost' == $param) {
// fix
if (is_array($value)) { $value = 100500; }
else { $value = intval($value); }
}
if ('option' == $param)
{
$value = array_fill_keys((array) $value, false);
foreach ($this->ttx_options_result_data as $op_key=>$ttx_option) {
if (isset($value[$op_key])) {
$params['ttx'] = true;
$value[$op_key] = $ttx_option;
}
}
$value = array_filter($value, function($v) { return (bool) $v; });
}
$params[$param] = $value;
}
// var_dump($params);exit;
return $params;
}
/**
* Жопа, конечно....
* Но делать нечего - формирование строки запроса из параметров
*
* @param array $exclude
* @param array $include
* @return string
*/
public function generateQueryString(array $exclude=array(), array $include=array())
{
$params = $this->last_search_params;
$result_string = '?';
unset(
$params['page'],
$params['car_type_id'],
$params['search_price_to_usd'],
$params['search_price_to_euro']
);
// exclude params
foreach ($exclude as $param) {
if(isset($params[$param])) {
unset($params[$param]);
}
}
// exclude if not include
if ($include) {
foreach ($params as $p => $pv) {
if (!in_array($p, $include)) {
unset($params[$p]);
}
}
}
if (isset($params['car_body_id'])) {
$params['car_body_id'] = str_replace(' ', '+', $params['car_body_id']);
}
if (isset($params['car_class_id'])) {
$params['car_class_id'] = str_replace(' ', '+', $params['car_class_id']);
}
$array_properties = array('option', 'transmission', 'countries', 'country_collected');
foreach ($params as $o => $v)
{
if (in_array($o, $array_properties)) {$v = (array)$v;}
if (is_array($v)) {
foreach ($v as $vv) {
$result_string .= "$o=$vv&";
}
} else {
$result_string .= "$o=$v&";
}
}
if ($result_string == '?') {
$result_string = '';
}
return $result_string;
}
/**
* Генерация результативного массива по запросу
* Так как существует необходимость дергать данные
* в одном и том же формате в разных местах,
* включая json api - собирается вся эта история
* в отдельном методе
*
* @return array
*/
public function generateSearchResultArrayFromResultData()
{
$elements = array();
foreach ($this->search_result as $item)
{
$element = array(
'id' => $item['sub_generation_id'],
'is_new' => $item['is_new'],
'is_swf' => $item['is_swf'],
'title' => '',
'brand_title' => '',
'model_sub_title' => '',
's_url' => '',
'image' => '',
'modifications' => array()
);
foreach ($item['modifications'] as $modification)
{
$element['title'] = $modification['main_info']['brand_model_title'];
$element['brand_title'] = $modification['main_info']['brand'];
$element['model_sub_title'] = $modification['main_info']['model_sub_title'];
$element['s_url'] = $modification['main_info']['generation_url'];
if ($modification['main_info']['image_hash']) {
$element['image'] = $modification['main_info']['image_hash'];
}
$price_options = 0;
$modification_options = array();
foreach ($modification['options_with_prices'] as $op=>$opv) {
if ($op != 'PRICE') {
$price_options += $opv;
if (intval($opv))
{
$option_current = (int)preg_replace('|[^\d]+|ius', '', $op);
$modification_options[] = array(
'id' => $modification['linked_options'][$option_current]
);
}
}
}
$element['modifications'][] = array(
'id' => $modification['id'],
#fixed-mf[9900]: дублирование в выдаче
'title' => $modification['main_info']['modification_title'], //$modification['title'],
// 'completion_title' => $modification['main_info']['modification_title'],
'price' => $modification['price'],
'price_special' => null,
'fuel' => $modification['main_info']['fuel'],
'control' => $modification['main_info']['gear'],
'power' => $modification['main_info']['horsepower'],
's_url' => $modification['main_info']['modification_url'],
'url' => $modification['main_info']['url'],
'price_options' => $price_options,
'options' => $modification_options
);
}
$elements[] = $element;
}
// var_dump($elements);exit;
return $elements;
}
public function filterSocial()
{
$data = array(
1 => array(1000, 1001),
2 => range(1002, 1005),
3 => range(1006, 1009),
4 => range(1010, 1015),
);
$result = array(
1 => 0,
2 => 0,
3 => 0,
4 => 0
);
$options = isset($this->last_search_params['option']) ? (array) $this->last_search_params['option'] : array();
// вот реально... что в голову пришло..
// по хорошему - переписать бы....
foreach ($options as $op) {
foreach ($data as $k => $a) {
if (in_array($op, $a)) {
$result[$k] = (int) $op;
}
}
}
return $result;
}
/**
* Вхерачиваем в смарти результат
*
* @param \Smarty $smarty
*/
public function assignToSmarty(\Smarty $smarty)
{
$elements = $this->generateSearchResultArrayFromResultData();
$smarty->assign ('search_result', $elements);
$smarty->assign ('page_iteration', ($this->currentPage()-1)*$this->limit()+1);
$smarty->assign ('page_total_result_count', $this->totalCount());
}
# ---- // IMPLEMENTATION
/**
* @param mixed $offset
* @return bool
*/
public function offsetExists($offset)
{
return isset($this->search_result[$offset]);
}
/**
* @param mixed $offset
* @return mixed|null
*/
public function offsetGet($offset)
{
return isset($this->search_result[$offset]) ? $this->search_result[$offset] : null;
}
/**
* @param mixed $offset
* @param mixed $value
* @throws \Exception
*/
public function offsetSet($offset, $value)
{
throw new \Exception('Unable to set search result item');
}
/**
* @param mixed $offset
* @throws \Exception
*/
public function offsetUnset($offset)
{
throw new \Exception('Unable to unset search result item');
}
/**
* @return mixed
*/
public function current()
{
return $this->search_result[$this->key];
}
public function next()
{
$this->key += 1;
}
/**
* @return int|mixed
*/
public function key()
{
return $this->key;
}
/**
* @return bool
*/
public function valid()
{
return isset($this->search_result[$this->key]);
}
public function rewind()
{
$this->key = 0;
}
}
<?php
namespace Quto\Common\Search;
class Search
{
/** @var SearchCriteriaDefault */
private $criteria;
public function __construct(SearchCriteriaDefault $criteria)
{
$this->criteria = $criteria;
}
/**
* Получение кол-ва эл-тов выборки
*
* @param $collection
* @return int
* @throws \Exception
*/
public function count($collection)
{
if ( !($collection instanceof \MongoCollection)
&& !($collection instanceof \MongoPinba\MongoCollection)
) {
throw new \Exception('Collection must be instance of MongoCollection');
}
$result = $collection->aggregate($this->criteria->count());
if (isset($result['result']) && $result['result']) {
return (int) $result['result'][0]['count'];
}
return 0;
}
/**
* Получение результатов
*
* @param \MongoCollection $collection
* @param bool $use_custom_sort
* @return array
* @throws \Exception
*/
public function result($collection, $use_custom_sort=false)
{
if ( !($collection instanceof \MongoCollection)
&& !($collection instanceof \MongoPinba\MongoCollection)
) {
throw new \Exception('Collection must be instance of MongoCollection');
}
$result = $collection->aggregate($this->criteria->result());
if (isset($result['result']))
{
if ($use_custom_sort) {
return $this->_sort($result['result'], $this->criteria->getSortOrder());
}
return $result['result'];
}
throw new \Exception(json_encode($result));
}
/**
* Так как монга не совсем нормально некоорые вещи сортирует
* вынужденная мера - своя сортировка
*
* @param array $result
* @param $order
*
* @return array
*/
private function _sort(array $result, $order)
{
$sort_function = function($a, $b) use($order)
{
$direction_up = 0;
$direction_down = 0;
$key = null;
switch ($order)
{
case SearchCriteriaDefault::ORDER_PRICE_ASC:
$key = 'price';
$direction_down = -1;
$direction_up = 1;
break;
case SearchCriteriaDefault::ORDER_PRICE_DESC:
$key = 'price';
$direction_down = 1;
$direction_up = -1;
break;
case SearchCriteriaDefault::ORDER_POWER_ASC:
$key = 'power';
$direction_down = -1;
$direction_up = 1;
break;
case SearchCriteriaDefault::ORDER_POWER_DESC:
$key = 'power';
$direction_down = 1;
$direction_up = -1;
break;
case SearchCriteriaDefault::ORDER_BM_ASC:
$key = 'brand_model';
$direction_down = -1;
$direction_up = 1;
break;
case SearchCriteriaDefault::ORDER_BM_DESC:
$key = 'brand_model';
$direction_down = 1;
$direction_up = -1;
break;
}
if (!$key) {
return 0;
}
if ('brand_model' == $key) {
return strcmp($a['title'], $b['title']) < 0 ? $direction_down : $direction_up;
}
if ($a[$key]===$b[$key]) {return 0;}
return $a[$key] < $b[$key] ? $direction_down : $direction_up;
};
foreach ($result as $k=>$item) {
uasort($result[$k]['modifications'],$sort_function);
}
return $result;
}
}
<?php
namespace Quto\Common\Search;
class SearchCriteriaBuilder
{
const SORT_DESC = -1;
const SORT_ASC = 1;
private $limit = 0;
private $skip = 0;
private $match = array();
private $group = array();
private $project = array();
private $sort = array();
private $sort_before = array();
private $pipelines = array();
private $unwind = '';
private $having = array();
private $finalize_group = array();
public function prependPipeline(array $pipeline)
{
array_unshift($this->pipelines, $pipeline);
}
public function setFinalizeGroup(array $group)
{
$this->finalize_group = $group;
}
public function appendPipeline(array $pipeline)
{
$this->pipelines[] = $pipeline;
}
public function setUnwind($unwind)
{
$this->unwind = $unwind;
}
public function getLimit()
{
return $this->limit;
}
public function getOffset()
{
return $this->skip;
}
public function setProject(array $project)
{
$this->project = $project;
}
public function setMatch(array $match)
{
$this->match = $match;
}
public function setGroup(array $group)
{
$this->group = $group;
}
public function setLimit($limit)
{
$this->limit = (int) $limit;
}
public function setOffset($offset)
{
$this->skip = (int) $offset;
}
public function setSort(array $sort)
{
$this->sort = $sort;
}
public function addMatch($key, array $value)
{
$this->match[$key] = $value;
}
public function addSort($key, $direction)
{
$this->sort[$key] = (int) $direction;
}
public function addSortBefore($key, $direction)
{
$this->sort_before[$key] = (int) $direction;
}
/**
* @return array
*/
public function getPipelines()
{
$pipelines = $this->pipelines;
$this->pipelines = array();
return $pipelines;
}
/**
* Aggregation count pipeline
* @return array
*/
public function count()
{
$this->appendPipeline(array('$match'=>$this->match));
$this->appendPipeline(array('$group'=>$this->group));
if ($this->unwind) {
$this->appendPipeline(array('$unwind'=>$this->unwind));
}
if ($this->having) {
$this->appendPipeline(array('$match'=>$this->having));
}
if ($this->finalize_group) {
$this->appendPipeline(array('$group'=>$this->finalize_group));
}
$this->appendPipeline(array(
'$group' => array(
'_id' => null,
'count' => array('$sum'=>1),
)
));
$this->appendPipeline(array('$skip' => 0));
$this->appendPipeline(array('$limit' => 1));
return $this->getPipelines();
}
public function setHaving(array $having)
{
$this->having = $having;
}
/**
* Aggregation search pipeline
* @return array
*/
public function result()
{
$this->appendPipeline(array('$match'=>$this->match));
if ($this->sort_before) {
$this->appendPipeline(array('$sort' => $this->sort_before));
}
$this->appendPipeline(array('$group'=>$this->group));
if ($this->project) {
$this->appendPipeline(array('$project' => $this->project));
}
if ($this->unwind) {
$this->appendPipeline(array('$unwind'=>$this->unwind));
}
if ($this->having) {
$this->appendPipeline(array('$match'=>$this->having));
}
if ($this->finalize_group) {
$this->appendPipeline(array('$group'=>$this->finalize_group));
}
if ($this->sort) {
$this->appendPipeline(array('$sort' => $this->sort));
}
if ($this->limit) {
$this->appendPipeline(array('$skip' => $this->skip));
$this->appendPipeline(array('$limit' => $this->limit));
}
return $this->getPipelines();
}
}
<?php
namespace Quto\Common\Search;
use \Quto\Fetcher_Car_Property;
class SearchCriteriaDefault
{
const ORDER_PRICE_ASC = 'price_asc';
const ORDER_PRICE_DESC = 'price_desc';
const ORDER_POWER_ASC = 'power_asc';
const ORDER_POWER_DESC = 'power_desc';
const ORDER_BM_ASC = 'brand_model_asc';
const ORDER_BM_DESC = 'brand_model_desc';
const LIMIT_PER_PAGE = 10;
const CAR_TYPE_LCV = 2;
const CAR_TYPE_CAR = 1;
/** @var SearchCriteriaBuilder */
private $builder;
/** @var int Кол-во элементов поиска */
private $limit_per_page = 0;
/** @var array список того что передается через OR */
private $or_list = array(
array(324, 326, 328, 329, 331), // салон ткань там или кожа
array(72, 73, 74), // климаты
array(183, 171), // подвеска
array(144, 149), // мультируль
array(85, 116, 198), // парктроники
array(84, 116, 198), // парктроники
);
/** @var string sort order */
private $sort_order=SearchCriteriaDefault::ORDER_PRICE_ASC;
/** @var array */
private $last_search_params = array();
/**
* @param SearchCriteriaBuilder $builder
*/
public function __construct(SearchCriteriaBuilder $builder)
{
$this->builder = $builder;
// default
$this->limit_per_page = static::LIMIT_PER_PAGE;
}
/**
* @param $lpp
*/
public function setLimitPerPage($lpp)
{
$this->limit_per_page = (int) $lpp;
}
/**
* @return array
*/
public function getLastSearchParams()
{
return $this->last_search_params;
}
/**
* @param array $params
* @return array
*/
private function filter_params(array $params)
{
return array_filter($params, function($param) {
if (is_numeric($param)) {
return true;
}
if (is_string($param) && empty($param)) {
return null;
}
return true;
});
}
private function filter_array_param($param)
{
if (is_array($param)) {
return $param[0];
}
return $param;
}
/**
* Магический метод.
* Формирование запроса в БД исходя из параметров в query_string
*
* @param array $params
*/
public function fillWithParams(array $params)
{
$params = $this->filter_params($params);
// sort must have
if (!isset($params['order'])) { $params['order'] = $this->sort_order; }
else {$this->sort_order = $params['order'];}
// just save it
$this->last_search_params = $params;
$b = $this->getBuilder();
$match = array(
'$and' => array(),
);
# ----- // тип авто: LCV или CAR
if (array_key_exists('car_type_id', $params)) {
$match['$and'][] = array('car_type_id'=>intval($params['car_type_id']));
}
# ----- // если по modification_id
if (array_key_exists('mids', $params))
{
$m_ids = array_map(function($v){ return intval($v); }, (array)$params['mids']);
if ($m_ids) {
$match['$and'][] = array('car_modification_id'=>array('$in'=>array_values($m_ids)));
}
}
# ----- // если по sub_generation_id
if (array_key_exists('ids', $params))
{
$ids = array_map(function($v){ return intval($v); }, (array)$params['ids']);
if ($ids) {
$match['$and'][] = array('car_model_sub_generation_id'=>array('$in'=>array_values($ids)));
}
}
# ----- // обработка ценового диапазона
$having = array();
if (array_key_exists('price_min', $params))
{
$match['$and'][] = array('price_min' => array('$gte'=>(int)$params['price_min']));
$having['$gte'] = (int)$params['price_min'];
}
if (array_key_exists('price_max', $params))
{
$match['$and'][] = array('price_min' => array('$lte'=>(int)$params['price_max']));
$having['$lte'] = (int)$params['price_max'];
$having['$lte'] = (int)$params['price_max'];
}
// дополнительно фильтруем результат
// первоначально идет выборка по дельте цены
// второй фильтр уже отвечает за отсечку после установки опций
if (count($having) > 0) {
$b->setHaving(array('modifications.price'=>$having));
}
# ----- // кузов
if (array_key_exists('car_body_id', $params))
{
/** кароче. суть в том что оооочень криво сделана штука в query_string... */
if (is_string($params['car_body_id'])) {
$op_slice = strpos($params['car_body_id'], '+')===false ? '%20' : '+';
$body_ids = explode($op_slice, $params['car_body_id']);
} else {
$body_ids = $params['car_body_id'];
}
$body_ids = array_filter($body_ids, function ($v) {
$v = intval($v);
return $v ?: null;
});
$body_ids = array_map(function($v){ return intval($v); }, $body_ids);
if ($body_ids) {
$match['$and'][] = array('car_body_id'=>array('$in'=>array_values($body_ids)));
}
}
# ----- // класс
if (array_key_exists('car_class_id', $params))
{
/** кароче. суть в том что оооочень криво сделана штука в query_string... */
if (is_string($params['car_class_id'])) {
$op_slice = strpos($params['car_class_id'], '+')===false ? '%20' : '+';
$class_ids = explode($op_slice, $params['car_class_id']);
} else {
$class_ids = $params['car_class_id'];
}
$class_ids = array_filter($class_ids, function ($v) {
return strpos($v, 'class_')===0 ? $v : null;
});
$class_ids = array_map(function($v){ return intval(str_replace('class_', '', $v)); }, $class_ids);
if ($class_ids) {
$match['$and'][] = array('car_class_id'=>array('$in'=>array_values($class_ids)));
}
}
# ----- // модель и бренд
if (array_key_exists('car_brand_id', $params))
{
$params['car_brand_id'] = (array) $params['car_brand_id'];
$params['car_brand_id'] = array_map(function($v){ return intval($v); }, $params['car_brand_id']);
}
if (array_key_exists('car_model_id', $params))
{
$params['car_model_id'] = (array) $params['car_model_id'];
$params['car_model_id'] = array_map(function($v){ return intval($v); }, $params['car_model_id']);
}
# если выбраны и бренд и модельки
if (isset($params['car_brand_id']) && isset($params['car_model_id']))
{
# собственно тут уже находится та самая магия
# дело в том что у нас не передаются в запрос
# связки моделей с брендами, но у нас есть возможность
# на этапе индексации влить в коллекцию все возможные модели
# для конкретного бренда. это такого рода "чит"
# идем от обратного.
#
# если выбран бренд и модель, сначала проверяем на вхождение
# брендов и НЕ ВХОЖДЕНИЕ выбранных моделей в возможные для бренда
# (это на случай если у одного бренда выбраны модели а у второго нет)
# после чего уже просто ищим вхождение моделей
# у нас не может один ид модельки принадлежать двум брендам
# PROFIT !!!
$match['$and'][] = array(
'$or' => array(
array(
# условия для выборки отдельного бренда без моделей
# прокатит только в том случае если модели переданы
# но к конкретному бренду они не имеют отношения
'$and'=> array(
array('car_brand_id'=>array('$in'=>$params['car_brand_id'])),
array('brand_models_available'=>array('$nin'=>$params['car_model_id'])),
),
),
# так как модели без бренда не бывает и ид модельки уникален
# тут уже выборка по переданным моделям
array('car_model_id'=>array('$in'=>$params['car_model_id']))
),
);
}
elseif (isset($params['car_brand_id'])) {
$match['$and'][] = array('car_brand_id'=>array('$in'=>$params['car_brand_id']));
}
// не уверен, что такое в принципе бывает
// но мало ли =)
elseif (isset($params['car_model_id'])) {
$match['$and'][] = array('car_model_id'=>array('$in'=>$params['car_model_id']));
}
# ----- // ограничения по ТТХ
$properties = $this->list_ttx_options_from_params($params);
if ($properties) {
$match['$and'][] = $properties;
}
# ----- // переборо опций и формирование их выборки
if (array_key_exists('option', $params))
{
$params['option'] = (array) $params['option'];
$params['option'] = array_map(function($v){ return intval($v); }, $params['option']);
$params['option'] = array_filter($params['option'], function($v) { return intval($v) > 0; });
$params['option'] = array_fill_keys($params['option'], true);
$params['option'] = array_keys($params['option']);
// var_dump($params['option']);exit;
$or = array();
$all = array();
foreach ($params['option'] as $option)
{
foreach ($this->or_list as $__i=>$__v) {
if (in_array($option, $this->or_list[$__i])) {
$or[] = $option;
continue 2;
}
}
$all[] = $option;
}
if ($or) {
$match['$and'][] = array('base_options' => array('$in'=>$or));
}
if ($all) {
$match['$and'][] = array('base_options'=>array('$all'=>$all));
}
foreach ($params['option'] as $option)
{
$nin = array_diff($params['option'], array($option));
if ($nin) {
$match['$and'][] = array('ignore_options.'.$option => array('$nin'=>array_values($nin)));
}
}
}
# ----- // сортировка
switch ($this->sort_order)
{
case self::ORDER_PRICE_ASC:
$b->addSort('modifications.price', SearchCriteriaBuilder::SORT_ASC);
break;
case self::ORDER_PRICE_DESC:
$b->addSort('modifications.price', SearchCriteriaBuilder::SORT_DESC);
break;
case self::ORDER_POWER_ASC:
$b->addSort('modifications.power', SearchCriteriaBuilder::SORT_ASC);
break;
case self::ORDER_POWER_DESC:
$b->addSort('modifications.power', SearchCriteriaBuilder::SORT_DESC);
break;
case self::ORDER_BM_ASC:
$b->addSort('modifications.title', SearchCriteriaBuilder::SORT_ASC);
break;
case self::ORDER_BM_DESC:
$b->addSort('modifications.title', SearchCriteriaBuilder::SORT_DESC);
break;
}
$b->setMatch($match);
# ----- // лимит
$page = isset($params['page']) ? $params['page'] : 1;
$offset = ($page-1)*$this->limit_per_page;
$b->setLimit($this->limit_per_page);
$b->setOffset($offset);
# ----- // после того как все отфильтровано - процессинг группы
$this->_process_group($params, count($having)>0);
}
private function _process_group(array $params, $use_having=false)
{
$sum_array = array(
'PRICE' => '$price_min',
);
if (isset($params['option']))
{
# (!) WARNING: тут собирается цена.
# как для выборки блока с ценами опций
# так и для формирования конечной цены модификации
# в зависимости от поставленых опций
$params['option'] = (array) $params['option'];
foreach ($params['option'] as $op) {
$sum_array['OP_'.$op] = array(
'$cond'=>array(
array('$ne' => array('$advanced_options_price.'.$op, array())),
array('$cond'=>array(
array('$gt'=>array('$advanced_options_price.'.$op, 0)),
'$advanced_options_price.'.$op,
0
)),
0
)
);
}
}
// group
$group = array(
'_id' => '$car_model_sub_generation_id',
"brand" => array('$first'=>'$car_brand_id'),
"model" => array('$first'=>'$car_model_id'),
"sub_generation_id" => array('$first'=>'$car_model_sub_generation_id'),
"is_new" => array('$first'=>'$main_info.is_new'),
"is_swf" => array('$first'=>'$main_info.is_swf'),
"modifications" => array('$addToSet' => array(
'title' => '$title',
'id' => '$car_modification_id',
'price_min' => '$price_min',
'price_max' => '$price_max',
'price' => array('$add'=>array_values($sum_array)),
'options_with_prices' => $sum_array,
'main_info' => '$main_info',
'power' => '$property_'.Fetcher_Car_Property::HORSE_POWER_ID,
'linked_options' => '$linked_options',
)),
"modifications_count" => array('$sum'=>1),
);
$this->builder->setGroup($group);
if ($use_having)
{
$this->builder->setUnwind('$modifications');
$final = array(
"_id" => '$_id',
"modifications" => array('$addToSet' => '$modifications'),
'brand' => array('$first' => '$brand'),
'model' => array('$first' => '$model'),
'sub_generation_id' => array('$first' => '$sub_generation_id'),
'is_new' => array('$first' => '$is_new'),
'is_swf' => array('$first' => '$is_swf'),
'modifications_count' => array('$sum' => 1),
);
$this->builder->setFinalizeGroup($final);
}
}
/**
* @return SearchCriteriaBuilder
*/
public function getBuilder()
{
return $this->builder;
}
public function getSortOrder()
{
return $this->sort_order;
}
public function count()
{
return $this->builder->count();
}
public function result()
{
return $this->builder->result();
}
/**
* Выборка всех ТТХ параметров
* Формирование запроса в монгу
*
* @param array $params
* @return array
*/
private function list_ttx_options_from_params(array $params)
{
$result = array();
foreach ($params as $key=>$value)
{
if ($option = $this->read_ttx_option($key))
{
if (in_array($key, array('country_collected', 'countries', 'transmission', 'engine_type', 'control')))
{
$result[$option] = array(
'$in' => array_values((array) $value)
);
}
elseif (strpos($key,'_min') !== false)
{
$value = $this->filter_array_param($value);
if (in_array($option,
array('property_8', 'property_39', 'property_27', 'property_28', 'property_29')) ) {
$value = $value*1000;
}
if ('property_8'==$option) {
# remove 3% of value
$value = $value * .97;
}
if (in_array($option, array('property_61', 'property_39'))) { $option .= '_min'; }
if (isset($result[$option])) {
$result[$option]['$gte'] = (int) $value;
} else {
$result[$option] = array('$gte'=>(int) $value);
}
}
elseif (strpos($key,'_max') !== false)
{
$value = $this->filter_array_param($value);
if (in_array($option,
array('property_8', 'property_39', 'property_27', 'property_28', 'property_29')) ) {
$value = $value*1000;
}
if ('property_8'==$option) {
# + 3% of power
$value = $value + ($value - $value * .97);
}
if (in_array($option, array('property_61', 'property_39'))) { $option .= '_max'; }
if (isset($result[$option])) {
$result[$option]['$lte'] = (int) $value;
} else {
$result[$option] = array('$lte'=>(int) $value);
}
}
else
{
// clearence
if (in_array($option, array('property_34')) ) {
$value = array('$gte'=>$value*10);
}
if (in_array($key, array('number_of_seats'))) {
$value = array('$gte'=>7);
$option = 'property_61_min';
}
$result[$option] = is_numeric($value) ? (int)$value : $value;
}
#fixed-mf[9900]: суть в том что если мы не знаем сколько и есть ли вообще описание
# конкретного параметра - необходимо его исключить
# так как в противном случае показываются авто которые показывать нельзя
# исходя из параметров в фильтре
if (is_array($result[$option])) {
#todo-mf: надо как-то по-умнее переписать.. костыльненько это...
$result[$option]['$ne'] = 0;
}
}
}
// var_dump($result);exit;
return $result;
}
/**
* Получение опции из поискового параметра и приведение ее к свойству в БД
* @param $option
* @return null|string
*/
private function read_ttx_option($option)
{
$options = $this->list_ttx_options_links();
if (isset($options[$option])) {
return 'property_'.$options[$option];
}
return null;
}
/**
* Линковка ТТХ
*
* [get_key=>[mongo_key]]
*
* @return array
*/
public function list_ttx_options_links()
{
return array(
'country_collected' => Fetcher_Car_Property::MAID_IN,
'control' => Fetcher_Car_Property::GEAR_TYPE_ID,
'transmission' => Fetcher_Car_Property::TRANSMISSION_ID,
'engine_type' => Fetcher_Car_Property::ENGINE_TYPE_ID,
'countries' => Fetcher_Car_Property::COUNTRY_ID,
'volume_min' => Fetcher_Car_Property::DISPLACEMENT_ID,
'volume_max' => Fetcher_Car_Property::DISPLACEMENT_ID,
'power_min' => Fetcher_Car_Property::HORSE_POWER_ID,
'power_max' => Fetcher_Car_Property::HORSE_POWER_ID,
'razgon_min' => Fetcher_Car_Property::RAZGON_ID,
'razgon_max' => Fetcher_Car_Property::RAZGON_ID,
'rashod_min' => Fetcher_Car_Property::RASCHOD_ID,
'rashod_max' => Fetcher_Car_Property::RASCHOD_ID,
'number_of_seats' => Fetcher_Car_Property::NUMBER_OF_SEATS_ID,
'boost' => Fetcher_Car_Property::BOOST_ID,
'clearance' => Fetcher_Car_Property::CLEARANCE_ID,
'number_places_min' => Fetcher_Car_Property::NUMBER_OF_SEATS_ID,
'number_places_max' => Fetcher_Car_Property::NUMBER_OF_SEATS_ID,
'dimensions_cargo_length_min' => Fetcher_Car_Property::DIMENSIONS_LENGTH_ID,
'dimensions_cargo_length_max' => Fetcher_Car_Property::DIMENSIONS_LENGTH_ID,
'dimensions_cargo_width_min' => Fetcher_Car_Property::DIMENSIONS_WIDTH_ID,
'dimensions_cargo_width_max' => Fetcher_Car_Property::DIMENSIONS_WIDTH_ID,
'dimensions_cargo_height_min' => Fetcher_Car_Property::DIMENSIONS_HEIGHT_ID,
'dimensions_cargo_height_max' => Fetcher_Car_Property::DIMENSIONS_HEIGHT_ID,
'dimensions_cargo_volume_min' => Fetcher_Car_Property::DIMENSIONS_VOLUME_ID,
'dimensions_cargo_volume_max' => Fetcher_Car_Property::DIMENSIONS_VOLUME_ID,
'capacity_min' => Fetcher_Car_Property::CAPACITY_ID,
'capacity_max' => Fetcher_Car_Property::CAPACITY_ID,
);
}
}
<?php
namespace Quto\Common\Search;
use \Quto\Common_Bootstrap;
final class SearchHelper
{
const SEARCH_COLLECTION_NAME = 'new_search_collection';
/** @var SearchCriteriaBuilder */
private $builder;
/** @var SearchCriteriaDefault */
private $criteria;
/** @var Search */
private $search;
private $mongo_collection;
/**
* @return Search
*/
public function getSearchObject()
{
return $this->search;
}
/**
* @return SearchCriteriaDefault
*/
public function getCriteriaObject()
{
return $this->criteria;
}
/**
* @return SearchCriteriaBuilder
*/
public function getBuilderObject()
{
return $this->builder;
}
/**
* @param $limit
*/
public function setLimit($limit)
{
$this->getCriteriaObject()->setLimitPerPage($limit);
}
/**
* Именованый конструктор
*
* @param \MongoCollection $collection
* @throws \Exception
* @return SearchHelper
*/
public static function createWithCollection($collection)
{
if ( !($collection instanceof \MongoCollection)
&& !($collection instanceof \MongoPinba\MongoCollection)
) {
throw new \Exception('Collection must be instance of MongoCollection');
}
$helper = new self();
$helper->builder = new SearchCriteriaBuilder();
$helper->criteria = new SearchCriteriaDefault($helper->builder);
$helper->search = new Search($helper->criteria);
$helper->mongo_collection = $collection;
return $helper;
}
/**
* Чит-инит =)
*
* @return SearchHelper
*/
public static function cheatInit()
{
/** @var \MongoDB $mongo */
$mongo = Common_Bootstrap::getMongoDb();
$collection = $mongo->selectCollection(self::SEARCH_COLLECTION_NAME);
return self::createWithCollection($collection);
}
/**
* Поиск
* @param array $filter
* @param bool $sort
* @return mixed
*/
public function find(array $filter, $sort=false)
{
$this->getCriteriaObject()->fillWithParams($filter);
return $this->getSearchObject()->result($this->mongo_collection, $sort);
}
/**
* Кол-во элементов
*
* @param array $filter
* @return int
*/
public function count(array $filter)
{
$this->getCriteriaObject()->fillWithParams($filter);
return $this->getSearchObject()->count($this->mongo_collection);
}
/**
* Возвращает массив для пагинации
*
* @param array $filter
* @return array
*/
public function pager(array $filter)
{
$count = $this->count($filter);
$limit = $this->getBuilderObject()->getLimit();
$offset = $this->getBuilderObject()->getOffset();
return array(
'pages' => ceil($count/$limit),
'page' => ceil($offset/$limit)+1,
'total_count' => $count,
'limit' => $limit
);
}
/**
* Фасад.
* Ищет сразу пагинашку + результат
*
* @param array $filter
* @param bool $use_custom_sort
* @return Result
*/
public function findResult(array $filter, $use_custom_sort=false)
{
// print $this->getCriteriaFindJson($filter);exit;
$pager = $this->pager($filter);
$result = $this->find($filter, $use_custom_sort);
return new Result($result, $pager, $this->criteria);
}
/**
* JSON критерия запроса
*
* @param array $filter
* @return string
*/
public function getCriteriaFindJson(array $filter)
{
$this->getCriteriaObject()->fillWithParams($filter);
return json_encode($this->getCriteriaObject()->result());
}
/**
* @return QueryLog
*/
public function getLogger()
{
static $logger;
if (!$logger) {
$logger = QueryLog::createWithCollection($this->mongo_collection);
}
return $logger;
}
/**
* Фасад
*
* @param array $filter
* @param array $advanced
*/
public function logQuery(array $filter, array $advanced=array())
{
$this->getLogger()->log($filter, $advanced);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment