BxBaseServicesSearch.php 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714
  1. <?php defined('BX_DOL') or die('hack attempt');
  2. /**
  3. * Copyright (c) UNA, Inc - https://una.io
  4. * MIT License - https://opensource.org/licenses/MIT
  5. *
  6. * @defgroup UnaBaseView UNA Base Representation Classes
  7. * @{
  8. */
  9. /**
  10. * System services related to search.
  11. */
  12. class BxBaseServicesSearch extends BxDol
  13. {
  14. public function __construct()
  15. {
  16. parent::__construct();
  17. }
  18. /**
  19. * @page service Service Calls
  20. * @section bx_system_general System Services
  21. * @subsection bx_system_general-general General
  22. * @subsubsection bx_system_general-keyword_search keyword_search
  23. *
  24. * @code bx_srv('system', 'keyword_search', ["bx_posts", ["keyword" => "test"}], 'TemplServicesSearch'); @endcode
  25. *
  26. * @code {{~system:keyword_search:TemplServicesSearch["bx_posts", {"keyword":"test"}]~}} @endcode
  27. * @code {{~system:keyword_search:TemplServicesSearch["bx_albums", {"meta_type": "location_country", "keyword": "AU"}, "unit.html"]~}} @endcode
  28. * @code {{~system:keyword_search:TemplServicesSearch["bx_albums", {"meta_type": "location_country_state", "state":"NSW", "keyword": "AU"}, "unit.html"]~}} @endcode
  29. * @code {{~system:keyword_search:TemplServicesSearch["bx_albums", {"meta_type": "location_country_city", "state":"NSW", "city":"Manly", "keyword": "AU"}, "unit.html"]~}} @endcode
  30. * @code {{~system:keyword_search:TemplServicesSearch["bx_posts", {"meta_type": "mention", "keyword": 2}, "unit_gallery.html"]~}} @endcode
  31. * @code {{~system:keyword_search:TemplServicesSearch["bx_posts", {"cat": "bx_posts_cats", "keyword": 3}, "unit_gallery.html"]~}} @endcode
  32. *
  33. * Search by keyword
  34. * @param $sSection - search object to search in, usually module name, for example: bx_posts
  35. * @param $aCondition - condition for search, supported conditions:
  36. * - search by keyword: ["keyword" => "test"]
  37. * - search by country: ["meta_type" => "location_country", "keyword" => "AU"]
  38. * - search by country and state: ["meta_type": "location_country_state", "state":"NSW", "keyword": "AU"]
  39. * - search by country, state and city: ["meta_type": "location_country_city", "state":"NSW", "city":"Manly", "keyword": "AU"]
  40. * - search for mentions: ["meta_type" => "mention", "keyword" => 2]
  41. * - search in category: ["cat": "bx_posts_cats", "keyword": 3]
  42. * @param $sTemplate - template for displaying search results, for example: unit.html
  43. * @param $iStart - paginate, display records starting from this number
  44. * @param $iPerPage - paginate, display this number of records per page
  45. * @param $bLiveSearch - search results like in live search
  46. *
  47. * @see BxBaseServicesSearch::serviceKeywordSearch
  48. */
  49. /**
  50. * @ref bx_system_general-keyword_search "keyword_search"
  51. */
  52. public function serviceKeywordSearch ($sSection, $aCondition, $sTemplate = '', $iStart = 0, $iPerPage = 0, $bLiveSearch = 0, $bPaginate = false)
  53. {
  54. if (!$sSection || !isset($aCondition['keyword']))
  55. return '';
  56. $sClass = 'BxTemplSearch';
  57. $sElsName = 'bx_elasticsearch';
  58. $sElsMethod = 'is_configured';
  59. if(BxDolRequest::serviceExists($sElsName, $sElsMethod) && BxDolService::call($sElsName, $sElsMethod)) {
  60. $oModule = BxDolModule::getInstance($sElsName);
  61. bx_import('Search', $oModule->_aModule);
  62. $sClass = 'BxElsSearch';
  63. }
  64. $oSearch = new $sClass(array($sSection));
  65. $oSearch->setLiveSearch($bLiveSearch);
  66. $oSearch->setMetaType(isset($aCondition['meta_type']) ? $aCondition['meta_type'] : '');
  67. $oSearch->setCategoryObject(isset($aCondition['cat']) ? $aCondition['cat'] : '');
  68. $oSearch->setCustomSearchCondition($aCondition);
  69. if (!$bPaginate)
  70. $oSearch->setRawProcessing(true);
  71. $oSearch->setCustomCurrentCondition(array(
  72. 'paginate' => array (
  73. 'start' => $iStart,
  74. 'perPage' => $iPerPage ? $iPerPage : BX_DOL_SEARCH_RESULTS_PER_PAGE_DEFAULT,
  75. )));
  76. if ($sTemplate)
  77. $oSearch->setUnitTemplate($sTemplate);
  78. return $oSearch->response();
  79. }
  80. /**
  81. * @page service Service Calls
  82. * @section bx_system_general System Services
  83. * @subsection bx_system_general-general General
  84. * @subsubsection bx_system_general-keyword_search keyword_search
  85. *
  86. * @code bx_srv('system', 'search_keyword_form', 'TemplServicesSearch'); @endcode
  87. *
  88. * Block with Search by Keywords Form
  89. *
  90. * @see BxBaseServicesSearch::serviceSearchKeywordForm
  91. */
  92. /**
  93. * @ref bx_system_general-keyword_search "keyword_search"
  94. */
  95. public function serviceSearchKeywordForm ()
  96. {
  97. return $this->_getSearchObject()->getForm(BX_DB_PADDING_DEF, false, true);
  98. }
  99. /**
  100. * @page service Service Calls
  101. * @section bx_system_general System Services
  102. * @subsection bx_system_general-general General
  103. * @subsubsection bx_system_general-keyword_search keyword_search
  104. *
  105. * @code bx_srv('system', 'search_keyword_result', 'TemplServicesSearch'); @endcode
  106. *
  107. * Block with Search by Keywords Results
  108. *
  109. * @see BxBaseServicesSearch::serviceSearchKeywordResult
  110. */
  111. /**
  112. * @ref bx_system_general-keyword_search "keyword_search"
  113. */
  114. public function serviceSearchKeywordResult ()
  115. {
  116. $sType = bx_process_input(bx_get('type'));
  117. $sKeyword = bx_process_input(bx_get('keyword'));
  118. $bKeyword = $sKeyword !== false;
  119. if(bx_is_api())
  120. return $this->serviceGetDataSearchApi(['params' => [
  121. 'type' => $sType,
  122. 'keyword' => $sKeyword,
  123. 'section' => bx_process_input(bx_get('section')),
  124. 'cat' => bx_process_input(bx_get('cat')),
  125. 'filter' => $this->_getSearchApiFilterFromRequest(),
  126. 'list' => bx_get('list') ? 1 : 0
  127. ]]);
  128. $sCode = '';
  129. if($bKeyword) {
  130. $oSearch = $this->_getSearchObject();
  131. if(($sCode = $oSearch->response()))
  132. $sCode = BxDolTemplate::getInstance()->parseHtmlByName('search_result_block.html', [
  133. 'html_id' => 'bx-search-results-keyword',
  134. 'class' => 'bx-search-results-container',
  135. 'attrs' => '',
  136. 'content' => $sCode,
  137. 'bx_if:do_center' => [
  138. 'condition' => false,
  139. 'content' => [
  140. 'html_id' => '',
  141. 'selector_content' => ''
  142. ]
  143. ]
  144. ]);
  145. else
  146. $sCode = $oSearch->getEmptyResult();
  147. }
  148. return $sCode;
  149. }
  150. /**
  151. * @ref bx_system_general-get_date_search_api "get_date_search_api"
  152. * @api @ref bx_system_general-get_date_search_api "get_date_search_api"
  153. */
  154. public function serviceGetDataSearchApi ($aParams)
  155. {
  156. if(!bx_is_api())
  157. return false;
  158. if(is_string($aParams))
  159. $aParams = bx_api_get_browse_params($aParams);
  160. $bForceAll = ($aParams['params']['type'] ?? '') == 'keyword';
  161. $aSectionsAvail = explode(',', getParam('sys_api_search_sections'));
  162. $aSectionsAll = BxDolDb::getInstance()->fromCache(
  163. 'sys_global_search_pairs',
  164. 'getPairs',
  165. 'SELECT `ObjectName` AS `name`, `Title` AS `title` FROM `sys_objects_search` WHERE `GlobalSearch`=\'1\' ORDER BY `Order` ASC',
  166. 'name', 'title'
  167. );
  168. $aSections = [];
  169. foreach($aSectionsAll as $sSectionName => $sSectionTitle)
  170. if(in_array($sSectionName, $aSectionsAvail) || $bForceAll)
  171. $aSections[$sSectionName] = [
  172. 'name' => $sSectionName,
  173. 'title' => _t($sSectionTitle)
  174. ];
  175. $aParamsBrowse = array_merge([
  176. 'keyword' => '',
  177. 'section' => '',
  178. 'sections' => array_values($aSections),
  179. 'start' => 0,
  180. 'per_page' => 12
  181. ], !empty($aParams['params']) && is_array($aParams['params']) ? $aParams['params'] : []);
  182. if(empty($aParamsBrowse['section']))
  183. $aParamsBrowse['section'] = array_keys($aSections);
  184. else if(is_string($aParamsBrowse['section']))
  185. $aParamsBrowse['section'] = explode(',', $aParamsBrowse['section']);
  186. $sClass = 'BxTemplSearch';
  187. $sSections = $aParamsBrowse['section'];
  188. if ($aParamsBrowse['live'] !== true && count($sSections) > 1){
  189. $aParamsBrowse['section'] = [];
  190. $aParamsBrowse['sections'] = [];
  191. $aDataRv = [];
  192. foreach ($sSections as $sSection) {
  193. $oSearch = new $sClass($sSection);
  194. $oSearch->setLiveSearch(true);
  195. $oSearch->setDataProcessing(true);
  196. $oSearch->setCustomSearchCondition(['keyword' => $aParamsBrowse['keyword']]);
  197. $oSearch->setCustomCurrentCondition([
  198. 'paginate' => [
  199. 'forceStart' => $aParamsBrowse['start'],
  200. 'perPage' => $aParamsBrowse['per_page'],
  201. ]
  202. ]);
  203. $aData = $oSearch->response();
  204. if(count($aData) > $aParamsBrowse['per_page'])
  205. $aData = array_slice($aData, $aParamsBrowse['start'], $aParamsBrowse['per_page']);
  206. if (count($aData) > 0) {
  207. $oSearchResult = $oSearch->getSearchResultObject($sSection);
  208. $sSectionTitle = $oSearchResult->aCurrent['title'];
  209. $aParamsBrowse['section'][] = $sSection;
  210. $aParamsBrowse['sections'][] = ['name' => $sSection, 'title' => $sSectionTitle];
  211. $aDataRv[] = [
  212. 'section' => $sSection,
  213. 'section_name' => $sSectionTitle,
  214. 'data' => $aData,
  215. 'is_profile' => bx_srv('system', 'is_module_profile', [$oSearchResult->aCurrent['module_name']])
  216. ];
  217. }
  218. }
  219. return [bx_api_get_block('search_sections', [
  220. 'data' => $aDataRv,
  221. 'params' => $aParamsBrowse
  222. ])];
  223. }
  224. else{
  225. $oSearch = new $sClass($aParamsBrowse['section']);
  226. $oSearch->setLiveSearch(true);
  227. $oSearch->setDataProcessing(true);
  228. $oSearch->setCustomSearchCondition(['keyword' => $aParamsBrowse['keyword']]);
  229. $aCurrentCondition = [
  230. 'paginate' => [
  231. 'forceStart' => $aParamsBrowse['start'],
  232. 'perPage' => $aParamsBrowse['per_page'],
  233. ]
  234. ];
  235. // date filter (from search_parse): single section only
  236. $aFilter = !empty($aParamsBrowse['filter']) && is_array($aParamsBrowse['filter']) ? $aParamsBrowse['filter'] : [];
  237. if ($aFilter && count($aParamsBrowse['section']) == 1 && ($oSearchResult = $oSearch->getSearchResultObject($aParamsBrowse['section'][0]))) {
  238. $aRestriction = $this->_getSearchApiFilterRestriction($oSearchResult, $aFilter);
  239. if ($aRestriction) {
  240. $aCurrentCondition['restriction'] = $aRestriction;
  241. // live search returns nothing without a keyword, a date filter alone is enough here
  242. if (empty($aParamsBrowse['keyword']))
  243. $oSearch->setLiveSearch(false);
  244. }
  245. else
  246. unset($aParamsBrowse['filter']);
  247. }
  248. else
  249. unset($aParamsBrowse['filter']);
  250. // "all events": section without keyword and date - list the section instead of an empty live search
  251. if (!empty($aParamsBrowse['list']) && empty($aParamsBrowse['keyword']) && count($aParamsBrowse['section']) == 1)
  252. $oSearch->setLiveSearch(false);
  253. $oSearch->setCustomCurrentCondition($aCurrentCondition);
  254. $aData = $oSearch->response();
  255. if(count($aData) > $aParamsBrowse['per_page'])
  256. $aData = array_slice($aData, $aParamsBrowse['start'], $aParamsBrowse['per_page']);
  257. return [
  258. bx_api_get_block('browse', [
  259. 'unit' => 'search-results',
  260. 'request_url' => '/api.php?r=system/get_data_search_api/TemplServicesSearch&params[]=',
  261. 'params' => $aParamsBrowse,
  262. 'data' => $aData
  263. ])
  264. ];
  265. }
  266. }
  267. /**
  268. * Natural language search query -> structured filter (section, keyword, date) via a judge AI model.
  269. * Guest safe. Without an active judge model the query is returned as a plain keyword.
  270. *
  271. * @code /api.php?r=system/search_parse/TemplServicesSearch&params={"params":{"query":"events on 23/09","timezone":"Europe/Berlin"}} @endcode
  272. */
  273. public function serviceSearchParse ($aParams)
  274. {
  275. if(!bx_is_api())
  276. return false;
  277. if(is_string($aParams))
  278. $aParams = bx_api_get_browse_params($aParams);
  279. $aParams = !empty($aParams['params']) && is_array($aParams['params']) ? $aParams['params'] : [];
  280. $sQuery = isset($aParams['query']) ? bx_process_input(trim((string)$aParams['query'])) : '';
  281. $sTimezone = isset($aParams['timezone']) ? bx_process_input((string)$aParams['timezone']) : '';
  282. $bDebug = !empty($aParams['debug']) && isAdmin();
  283. // extended search object (sys_objects_search_extended): fields of that object -> search_params
  284. $sObject = isset($aParams['object']) ? bx_process_input((string)$aParams['object'], BX_DATA_TEXT) : '';
  285. if($sObject !== '' && $this->_getSearchExtendedObject($sObject)) {
  286. $oParser = BxDolAiSearchExtParser::getInstance();
  287. $aResult = $oParser->parseObject($sObject, $sQuery, [
  288. 'timezone' => $sTimezone,
  289. 'debug' => $bDebug,
  290. ]);
  291. $aResult['available'] = $oParser->isAvailable();
  292. $aResult['mode'] = 'extended';
  293. return [bx_api_get_block('search_parse', $aResult)];
  294. }
  295. $aSections = $this->_getSearchApiSections();
  296. $oParser = BxDolAiSearchParser::getInstance();
  297. $aResult = $oParser->parse($sQuery, [
  298. 'sections' => $aSections,
  299. 'timezone' => $sTimezone,
  300. 'debug' => $bDebug,
  301. ]);
  302. $aResult['available'] = $oParser->isAvailable();
  303. $aResult['mode'] = 'sections';
  304. $aResult['sections'] = [];
  305. foreach($aSections as $sSectionName => $sSectionTitle)
  306. $aResult['sections'][] = ['name' => $sSectionName, 'title' => $sSectionTitle];
  307. // filter for get_data_search_api / search-keyword page
  308. $aResult['filter'] = $aResult['date'] ? [
  309. 'date_field' => $aResult['date']['field'],
  310. 'date_from' => $aResult['date']['from'],
  311. 'date_to' => $aResult['date']['to'],
  312. 'timezone' => $sTimezone,
  313. ] : null;
  314. return [bx_api_get_block('search_parse', $aResult)];
  315. }
  316. /**
  317. * Page block "AI search" (NEO element `search_ai`): one input, the query is parsed by `search_parse`
  318. * into section / keyword / date chips, results come from `get_data_search_api`. API only.
  319. *
  320. * @param $mixedParams optional; a search section (e.g. bx_events) or an extended search object
  321. * (e.g. bx_market) to lock the block to, or an array:
  322. * ['object' => 'bx_market', 'use_page_results' => true] - the block only produces filters
  323. * and the page's own get_results block (TemplSearchExtendedServices) renders the results,
  324. * exactly as the search form does it.
  325. * ['object' => 'bx_market', 'rerank' => true] - the block renders the results itself and
  326. * orders them by meaning (BxDolAiSearchRerank), not by the SQL order.
  327. */
  328. public function serviceGetBlockSearchAi ($mixedParams = '')
  329. {
  330. if(!bx_is_api())
  331. return '';
  332. $bUsePageResults = false;
  333. $bRerank = false;
  334. if(is_array($mixedParams)) {
  335. $bUsePageResults = !empty($mixedParams['use_page_results']);
  336. $bRerank = !empty($mixedParams['rerank']);
  337. $sSection = isset($mixedParams['object']) ? (string)$mixedParams['object'] : (isset($mixedParams['section']) ? (string)$mixedParams['section'] : '');
  338. }
  339. else
  340. $sSection = (string)$mixedParams;
  341. // extended search object (e.g. bx_market): chips are the object's search fields, results via search_ai_results
  342. if($sSection !== '' && $this->_getSearchExtendedObject($sSection)) {
  343. $oParser = BxDolAiSearchExtParser::getInstance();
  344. $aFields = [];
  345. foreach($oParser->getFields($sSection) as $sName => $aField)
  346. $aFields[] = ['name' => $sName, 'caption' => $aField['caption'], 'kind' => $aField['kind']];
  347. return [bx_api_get_block('search_ai', [
  348. 'mode' => 'extended',
  349. 'available' => $oParser->isAvailable(),
  350. 'object' => $sSection,
  351. 'fields' => $aFields,
  352. 'parse_url' => '/api.php?r=system/search_parse/TemplServicesSearch&params=',
  353. // true: the parsed values are pushed into the page's own get_results block as `filters`
  354. // (same contract as the search form); false: the block fetches and renders results itself
  355. 'use_page_results' => $bUsePageResults ? 1 : 0,
  356. // semantic ordering by the judge model: with use_page_results the block swaps the page's
  357. // list source to search_ai_results, ranked results cannot be expressed as form filters
  358. 'rerank' => $bRerank && BxDolAiSearchRerank::getInstance()->isAvailable() ? 1 : 0,
  359. 'results_url' => '/api.php?r=system/search_ai_results/TemplServicesSearch&params=',
  360. ])];
  361. }
  362. $aSections = $this->_getSearchApiSections();
  363. $sSection = $sSection && isset($aSections[$sSection]) ? $sSection : '';
  364. $aSectionsList = [];
  365. foreach($aSections as $sSectionName => $sSectionTitle)
  366. $aSectionsList[] = ['name' => $sSectionName, 'title' => $sSectionTitle];
  367. return [bx_api_get_block('search_ai', [
  368. 'mode' => 'sections',
  369. 'available' => BxDolAiSearchParser::getInstance()->isAvailable(),
  370. 'section' => $sSection,
  371. 'sections' => $aSectionsList,
  372. 'parse_url' => '/api.php?r=system/search_parse/TemplServicesSearch&params=',
  373. 'search_url' => '/api.php?r=system/get_data_search_api/TemplServicesSearch&params=',
  374. 'results_url' => '/search-keyword',
  375. ])];
  376. }
  377. /**
  378. * Results of an extended search object for search_params produced by search_parse (or edited via chips).
  379. * Only field names of the object are accepted; type and operator are taken from the object's field
  380. * definitions, never from the client. Guest safe (the module's own privacy conditions apply).
  381. *
  382. * @code /api.php?r=system/search_ai_results/TemplServicesSearch&params={"params":{"object":"bx_market","search_params":{"cat":{"value":[3]}},"start":0,"per_page":12}} @endcode
  383. */
  384. public function serviceSearchAiResults ($aParams)
  385. {
  386. if(!bx_is_api())
  387. return false;
  388. if(is_string($aParams))
  389. $aParams = bx_api_get_browse_params($aParams);
  390. $aParams = !empty($aParams['params']) && is_array($aParams['params']) ? $aParams['params'] : [];
  391. $sObject = isset($aParams['object']) ? bx_process_input((string)$aParams['object'], BX_DATA_TEXT) : '';
  392. $iStart = isset($aParams['start']) ? (int)$aParams['start'] : 0;
  393. $iPerPage = isset($aParams['per_page']) ? (int)$aParams['per_page'] : 12;
  394. if($iPerPage < 1 || $iPerPage > 50)
  395. $iPerPage = 12;
  396. $aObject = $sObject !== '' ? $this->_getSearchExtendedObject($sObject) : false;
  397. $oSearch = $aObject ? BxDolSearchExtended::getObjectInstance($sObject) : false;
  398. if(!$oSearch || !$oSearch->isEnabled())
  399. return [bx_api_get_msg(_t('Not Found'), ['ext' => ['msg_type' => 'result']])];
  400. $aFields = [];
  401. foreach($aObject['fields'] as $aField)
  402. if(!empty($aField['active']))
  403. $aFields[$aField['name']] = $aField;
  404. $aSearchParams = [];
  405. $aClient = !empty($aParams['search_params']) && is_array($aParams['search_params']) ? $aParams['search_params'] : [];
  406. foreach($aClient as $sName => $aParam) {
  407. if(!isset($aFields[$sName]) || !is_array($aParam) || !isset($aParam['value']))
  408. continue;
  409. $mixedValue = $aParam['value'];
  410. if(is_array($mixedValue)) {
  411. array_walk_recursive($mixedValue, function(&$mixed) {
  412. $mixed = is_string($mixed) ? bx_process_input($mixed) : (is_numeric($mixed) ? $mixed + 0 : '');
  413. });
  414. }
  415. else
  416. $mixedValue = is_numeric($mixedValue) ? $mixedValue + 0 : bx_process_input((string)$mixedValue);
  417. if($mixedValue === '' || $mixedValue === [] || (is_array($mixedValue) && bx_is_empty_array($mixedValue)))
  418. continue;
  419. $aSearchParams[$sName] = [
  420. 'type' => $aFields[$sName]['search_type'],
  421. 'value' => $mixedValue,
  422. 'operator' => $aFields[$sName]['search_operator'],
  423. ];
  424. }
  425. $sQuery = isset($aParams['query']) ? bx_process_input(trim((string)$aParams['query'])) : '';
  426. $bRerank = !empty($aParams['rerank']) && $sQuery !== '';
  427. // semantic ordering: the SQL picks candidates (with and without the keyword), the judge model
  428. // decides which of them answer the query and in which order
  429. if($bRerank) {
  430. $aRank = BxDolAiSearchRerank::getInstance()->rank($sObject, $sQuery, $aSearchParams, [
  431. 'keyword_field' => isset($aParams['keyword_field']) ? bx_process_input((string)$aParams['keyword_field'], BX_DATA_TEXT) : '',
  432. 'debug' => !empty($aParams['debug']) && isAdmin(),
  433. ]);
  434. if(!empty($aRank['ids'])) {
  435. // getContentSearchResultUnit loads the row and, on the API, returns it without
  436. // applying the content filter again, so drop ids this viewer cannot watch first.
  437. $aRank['ids'] = $this->_filterRankedIds($aObject, $aRank['ids']);
  438. if(!empty($aRank['scores']) && is_array($aRank['scores']))
  439. $aRank['scores'] = array_intersect_key($aRank['scores'], array_flip($aRank['ids']));
  440. }
  441. if(!empty($aRank['ids'])) {
  442. $aIds = array_slice($aRank['ids'], $iStart, $iPerPage + 1);
  443. $bHasMore = count($aIds) > $iPerPage;
  444. if($bHasMore)
  445. array_pop($aIds);
  446. $oContentInfo = BxDolContentInfo::getObjectInstance($aObject['object_content_info']);
  447. $aData = [];
  448. foreach($aIds as $iId)
  449. if(($mixedUnit = $oContentInfo->getContentSearchResultUnit($iId)))
  450. $aData[] = $mixedUnit;
  451. $aBlockParams = ['per_page' => $iPerPage, 'start' => $iStart, 'object' => $sObject, 'search_params' => $aSearchParams, 'query' => $sQuery, 'rerank' => 1];
  452. if(!empty($aRank['scores']))
  453. $aBlockParams['scores'] = $aRank['scores'];
  454. return [bx_api_get_block('browse', [
  455. 'nocache' => true,
  456. 'module' => $aObject['module'],
  457. 'unit' => 'general-content-list',
  458. 'request_url' => '/api.php?r=system/search_ai_results/TemplServicesSearch&params=',
  459. 'params' => $aBlockParams,
  460. 'data' => $aData,
  461. ], ['ext' => ['rerank' => ['judged' => $aRank['judged'], 'cached' => $aRank['cached'], 'ms' => $aRank['ms'], 'total' => count($aRank['ids'])]]])];
  462. }
  463. }
  464. if(!$aSearchParams)
  465. return [bx_api_get_msg(_t('Nothing found'), ['ext' => ['msg_type' => 'result']])];
  466. $aResults = $oSearch->getResults([
  467. 'search_params' => $aSearchParams,
  468. 'start' => $iStart,
  469. 'per_page' => $iPerPage,
  470. 'js_mode' => true,
  471. ]);
  472. if(!is_array($aResults))
  473. return [bx_api_get_msg(_t('Nothing found'), ['ext' => ['msg_type' => 'result']])];
  474. // pagination goes through this service again
  475. foreach($aResults as $iIndex => $aBlock)
  476. if(isset($aBlock['data']['request_url'])) {
  477. $aResults[$iIndex]['data']['request_url'] = '/api.php?r=system/search_ai_results/TemplServicesSearch&params=';
  478. $aResults[$iIndex]['data']['params'] = ['object' => $sObject, 'search_params' => $aSearchParams, 'per_page' => $iPerPage, 'start' => $iStart];
  479. }
  480. return $aResults;
  481. }
  482. /**
  483. * Ids still visible to the current profile under the module's content filter, in the same order.
  484. * Modules without a content-filter field are returned unchanged.
  485. */
  486. protected function _filterRankedIds(array $aObject, array $aIds)
  487. {
  488. $aIds = array_values(array_filter(array_map('intval', $aIds)));
  489. if(!$aIds)
  490. return [];
  491. $oModule = !empty($aObject['module']) ? BxDolModule::getInstance($aObject['module']) : null;
  492. $CNF = $oModule && !empty($oModule->_oConfig->CNF) ? $oModule->_oConfig->CNF : [];
  493. if(empty($CNF['TABLE_ENTRIES']) || empty($CNF['FIELD_ID']) || empty($CNF['FIELD_CF']))
  494. return $aIds;
  495. $oCf = BxDolContentFilter::getInstance();
  496. if(!$oCf->isEnabled())
  497. return $aIds;
  498. $oDb = BxDolDb::getInstance();
  499. $aVisible = $oDb->getColumn("SELECT `" . $CNF['FIELD_ID'] . "` FROM `" . $CNF['TABLE_ENTRIES'] . "` WHERE `" . $CNF['FIELD_ID'] . "` IN (" . $oDb->implode_escape($aIds) . ")" . $oCf->getSQLParts($CNF['TABLE_ENTRIES'], $CNF['FIELD_CF']));
  500. if(!is_array($aVisible))
  501. return [];
  502. $aVisible = array_flip(array_map('intval', $aVisible));
  503. $aResult = [];
  504. foreach($aIds as $iId)
  505. if(isset($aVisible[$iId]))
  506. $aResult[] = $iId;
  507. return $aResult;
  508. }
  509. /**
  510. * Extended search object with its fields; BxDolForm must be loaded first, its file defines
  511. * BX_DATA_LISTS_KEY_PREFIX which the fields' pre-values are read with.
  512. */
  513. protected function _getSearchExtendedObject($sObject)
  514. {
  515. bx_import('BxDolForm');
  516. return BxDolSearchExtendedQuery::getSearchObject($sObject);
  517. }
  518. /**
  519. * Global search sections enabled for the API: [name => translated title]
  520. */
  521. protected function _getSearchApiSections()
  522. {
  523. $aSectionsAvail = explode(',', getParam('sys_api_search_sections'));
  524. $aSectionsAll = BxDolDb::getInstance()->fromCache(
  525. 'sys_global_search_pairs',
  526. 'getPairs',
  527. 'SELECT `ObjectName` AS `name`, `Title` AS `title` FROM `sys_objects_search` WHERE `GlobalSearch`=\'1\' ORDER BY `Order` ASC',
  528. 'name', 'title'
  529. );
  530. $aSections = [];
  531. foreach($aSectionsAll as $sSectionName => $sSectionTitle)
  532. if(in_array($sSectionName, $aSectionsAvail))
  533. $aSections[$sSectionName] = _t($sSectionTitle);
  534. return $aSections;
  535. }
  536. /**
  537. * Date filter passed as plain GET params of the search-keyword page
  538. */
  539. protected function _getSearchApiFilterFromRequest()
  540. {
  541. $sFrom = bx_process_input(bx_get('date_from'));
  542. if(!$sFrom)
  543. return null;
  544. return [
  545. 'date_field' => bx_process_input(bx_get('date_field')),
  546. 'date_from' => $sFrom,
  547. 'date_to' => bx_process_input(bx_get('date_to')),
  548. 'timezone' => bx_process_input(bx_get('timezone')),
  549. ];
  550. }
  551. /**
  552. * Convert a date filter [date_field => starts|ends|created, date_from => Y-m-d, date_to => Y-m-d, timezone]
  553. * into search restrictions for the given search result object; [] when it can't be applied.
  554. */
  555. protected function _getSearchApiFilterRestriction($oSearchResult, $aFilter)
  556. {
  557. $sFrom = isset($aFilter['date_from']) ? (string)$aFilter['date_from'] : '';
  558. $sTo = isset($aFilter['date_to']) && $aFilter['date_to'] ? (string)$aFilter['date_to'] : $sFrom;
  559. if(!preg_match('/^\d{4}-\d{2}-\d{2}$/', $sFrom) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $sTo))
  560. return [];
  561. $sTimezone = isset($aFilter['timezone']) && in_array($aFilter['timezone'], timezone_identifiers_list()) ? $aFilter['timezone'] : date_default_timezone_get();
  562. $oTimezone = new DateTimeZone($sTimezone ?: 'UTC');
  563. try {
  564. $iFrom = (new DateTime($sFrom . ' 00:00:00', $oTimezone))->getTimestamp();
  565. $iTo = (new DateTime($sTo . ' 23:59:59', $oTimezone))->getTimestamp();
  566. }
  567. catch (Exception $e) {
  568. return [];
  569. }
  570. if($iTo < $iFrom)
  571. return [];
  572. $sDateField = isset($aFilter['date_field']) ? (string)$aFilter['date_field'] : 'created';
  573. $aCurrent = $oSearchResult->aCurrent;
  574. $sModule = isset($aCurrent['module_name']) ? $aCurrent['module_name'] : '';
  575. if($sModule == 'bx_events') {
  576. $sTable = 'bx_events_data';
  577. $aFields = ['starts' => 'date_start', 'ends' => 'date_end', 'created' => 'added'];
  578. $sField = isset($aFields[$sDateField]) ? $aFields[$sDateField] : 'date_start';
  579. }
  580. else {
  581. $sTable = !empty($aCurrent['tableSearch']) ? $aCurrent['tableSearch'] : (isset($aCurrent['table']) ? $aCurrent['table'] : '');
  582. $sField = !empty($aCurrent['added']) ? $aCurrent['added'] : 'added';
  583. }
  584. if(!$sTable || !BxDolDb::getInstance()->isFieldExists($sTable, $sField))
  585. return [];
  586. return [
  587. 'api_filter_date_from' => ['value' => $iFrom, 'field' => $sField, 'operator' => '>=', 'table' => $sTable],
  588. 'api_filter_date_to' => ['value' => $iTo, 'field' => $sField, 'operator' => '<=', 'table' => $sTable],
  589. ];
  590. }
  591. private function _getSearchObject()
  592. {
  593. $sClass = 'BxTemplSearch';
  594. $sElsName = 'bx_elasticsearch';
  595. $sElsMethod = 'is_configured';
  596. if(BxDolRequest::serviceExists($sElsName, $sElsMethod) && BxDolService::call($sElsName, $sElsMethod) && !bx_get('cat') && !bx_get('type')) {
  597. $oModule = BxDolModule::getInstance($sElsName);
  598. bx_import('Search', $oModule->_aModule);
  599. $sClass = 'BxElsSearch';
  600. }
  601. /**
  602. * @hooks
  603. * @hookdef hook-system-search_keyword 'system', 'search_keyword' - hook to override sClass for search
  604. * - $unit_name - equals `system`
  605. * - $action - equals `search_keyword`
  606. * - $object_id - not used
  607. * - $sender_id - not used
  608. * - $extra_params - array of additional params with the following array keys:
  609. * - `override_result` - [string] by ref, class name for search, can be overridden in hook processing
  610. * @hook @ref hook-system-search_keyword
  611. */
  612. bx_alert('system', 'search_keyword', 0, 0, array('class' => &$sClass, 'class_name_ref' => &$sClass));
  613. $oSearch = new $sClass(bx_get('section'));
  614. $oSearch->setLiveSearch(bx_get('live_search') ? 1 : 0);
  615. $oSearch->setMetaType(bx_process_input(bx_get('type')));
  616. $oSearch->setCategoryObject(bx_process_input(bx_get('cat')));
  617. return $oSearch;
  618. }
  619. }
  620. /** @} */