Base.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. <?php
  2. // https://www.drupal.org/docs/8/modules/search-api/developer-documentation/executing-a-search-in-code
  3. namespace Drupal\materio_sapi\Controller;
  4. use Drupal\Core\Controller\ControllerBase;
  5. use Symfony\Component\HttpFoundation\JsonResponse;
  6. use Symfony\Component\HttpFoundation\Request;
  7. use Drupal\Component\Utility\Tags;
  8. use Drupal\Component\Utility\Unicode;
  9. use Drupal\search_api\Entity\Index;
  10. // https://drupal.stackexchange.com/questions/225008/programatically-use-search-api
  11. /**
  12. * Defines a route controller for materio sapi search (regular and ajax).
  13. */
  14. class Base extends ControllerBase {
  15. private $limit = 15;
  16. private $offset = 0;
  17. private function sapiQuery(){
  18. // https://www.drupal.org/docs/8/modules/search-api/developer-documentation/executing-a-search-in-code
  19. // https://www.hashbangcode.com/article/drupal-8-date-search-boosting-search-api-and-solr-search
  20. // https://kgaut.net/blog/2018/drupal-8-search-api-effectuer-une-requete-dans-le-code.html
  21. // Otherwise, if you use Solr, you can also set the parse mode to “Direct query”
  22. // and use something like 'tm_name:alex AND tm_surname:john' as the keywords (field names may vary).
  23. // That will also ensure scoring works for the keywords (the other way just adds filters, which don’t affect scoring).
  24. // https://www.drupal.org/project/search_api/issues/3049097
  25. $lang = \Drupal::languageManager()->getCurrentLanguage()->getId();
  26. $this->index = Index::load('database');
  27. $this->results = [
  28. 'uuids' => [],
  29. 'nids' => []
  30. ];
  31. // PHRASE QUERY
  32. // (to match exact materials names (like "wood-skin"))
  33. $this->phrase_query = $this->index->query(['offset'=>0,'limit'=>10000]);
  34. // set parse mode and conjunction
  35. $parse_mode = \Drupal::service('plugin.manager.search_api.parse_mode')
  36. ->createInstance('phrase');
  37. $parse_mode->setConjunction('AND');
  38. $this->phrase_query->setParseMode($parse_mode);
  39. // Set fulltext search keywords and fields.
  40. if ($this->keys) {
  41. $this->phrase_query->keys(implode(' ', $this->keys));
  42. }
  43. $this->phrase_query->setFulltextFields(['title']);
  44. // Restrict the search to specific languages.
  45. $this->phrase_query->setLanguages([$lang]);
  46. // Do paging.
  47. // $this->and_query->range($this->offset, $this->limit);
  48. // retrieve all results
  49. // $this->and_query->range(0, -1);
  50. // Add sorting.
  51. $this->phrase_query->sort('search_api_relevance', 'DESC');
  52. // Set one or more tags for the query.
  53. // @see hook_search_api_query_TAG_alter()
  54. // @see hook_search_api_results_TAG_alter()
  55. $this->phrase_query->addTag('materio_sapi_search_phrase_query');
  56. $phrase_results = $this->phrase_query->execute();
  57. foreach ($phrase_results as $result) {
  58. $this->results['uuids'][] = $result->getField('uuid')->getValues()[0];
  59. $this->results['nids'][] = $result->getField('nid')->getValues()[0];
  60. }
  61. // AND QUERY
  62. $this->and_query = $this->index->query(['offset'=>0,'limit'=>10000]);
  63. // set parse mode and conjunction
  64. $parse_mode = \Drupal::service('plugin.manager.search_api.parse_mode')
  65. ->createInstance('terms');
  66. $parse_mode->setConjunction('AND');
  67. $this->and_query->setParseMode($parse_mode);
  68. // Set fulltext search keywords and fields.
  69. if ($this->keys) {
  70. $this->and_query->keys(implode(' ', $this->keys));
  71. }
  72. // in case of term id provided restrict the keys to taxo fields
  73. if ($this->terms && count($this->terms)) {
  74. $term_conditions = $this->and_query->createConditionGroup('OR');
  75. // $term = (int) $this->term;
  76. foreach ($this->terms as $term) {
  77. $tid = $term->value;
  78. foreach (['tag_tid', 'thesaurus_tid'] as $field) {
  79. $term_conditions->addCondition($field, (int) $tid);
  80. }
  81. }
  82. $this->and_query->addConditionGroup($term_conditions);
  83. // INSTEAD TRY TO BOOST THE TAG AND THESAURUS FIELDS
  84. // foreach ($taxoSolrFieldsName as $fname) {
  85. // // $solarium_query->addParam('bf', "recip(abs(ms(NOW,{$solrField})),3.16e-11,10,0.1)");
  86. // $bfparam = "if(gt(termfreq({$fname},{$this->term}),0),^21,0)";
  87. // $this->or_query->addParam('bf', $bfparam);
  88. // }
  89. // look @ materio_sapi_search_api_solr_query_alter in materio_sapi.module
  90. // $this->or_query->setOption('termid', $this->term);
  91. }
  92. if ($this->filters) {
  93. // FILTERS
  94. $filters_conditions = $this->and_query->createConditionGroup('AND');
  95. foreach ($this->filters as $filter) {
  96. $filter = (int) $filter;
  97. foreach (['thesaurus_tid'] as $field) { // 'tag_tid',
  98. $filters_conditions->addCondition($field, $filter);
  99. }
  100. }
  101. $this->and_query->addConditionGroup($filters_conditions);
  102. if(!$this->keys) {
  103. // if no keys but filters switch query to direct and add wildcard solr keys *:*
  104. $direct_and_parse_mode = \Drupal::service('plugin.manager.search_api.parse_mode')
  105. ->createInstance('direct');
  106. $direct_and_parse_mode->setConjunction('AND');
  107. $this->and_query->setParseMode($direct_and_parse_mode);
  108. // $this->and_query->keys('*:*');
  109. }
  110. }
  111. // else{
  112. $fulltextFields = [];
  113. // Recherche uniquement sur les champ thésaurus et tag
  114. $fulltextFields += [
  115. 'thesaurus_name_0',
  116. 'thesaurus_synonyms_0',
  117. 'thesaurus_name_1',
  118. 'thesaurus_synonyms_1',
  119. 'thesaurus_name_2',
  120. 'thesaurus_synonyms_2',
  121. 'thesaurus_name_3',
  122. 'thesaurus_synonyms_3',
  123. 'thesaurus_name_4',
  124. 'thesaurus_synonyms_4',
  125. 'thesaurus_name',
  126. 'thesaurus_synonyms',
  127. 'tag_name',
  128. 'tag_synonyms'
  129. ];
  130. if(count($fulltextFields)){
  131. $this->and_query->setFulltextFields($fulltextFields);
  132. }
  133. // }
  134. // Restrict the search to specific languages.
  135. $this->and_query->setLanguages([$lang]);
  136. // Do paging.
  137. // $this->and_query->range($this->offset, $this->limit);
  138. // retrieve all results
  139. // $this->and_query->range(0, -1);
  140. // Add sorting.
  141. $this->and_query->sort('search_api_relevance', 'DESC');
  142. // Set one or more tags for the query.
  143. // @see hook_search_api_query_TAG_alter()
  144. // @see hook_search_api_results_TAG_alter()
  145. $this->and_query->addTag('materio_sapi_search_and_query');
  146. $and_results = $this->and_query->execute();
  147. foreach ($and_results as $result) {
  148. // !! have to remove duplicates from phrase query
  149. $nid = $result->getField('nid')->getValues()[0];
  150. if ( !in_array($nid, $this->results['nids']) ) {
  151. $this->results['uuids'][] = $result->getField('uuid')->getValues()[0];
  152. $this->results['nids'][] = $result->getField('nid')->getValues()[0];
  153. }
  154. }
  155. $this->exactematch_count = count($this->results['nids']);
  156. //
  157. // OR QUERY
  158. //
  159. $this->or_query = $this->index->query(['offset'=>0,'limit'=>10000]);
  160. // Change the parse mode for the search.
  161. // Les différentes possibilités sont
  162. // - « direct » => Requête directe
  163. // - « terms » => Multiple words
  164. // - « phrase » => Single phrase
  165. // - " edismax " => ???
  166. $or_parse_mode = \Drupal::service('plugin.manager.search_api.parse_mode')
  167. ->createInstance('direct');
  168. $or_parse_mode->setConjunction('OR');
  169. $this->or_query->setParseMode($or_parse_mode);
  170. // Set fulltext search keywords and fields.
  171. if ($this->keys) {
  172. $this->or_query->keys(implode(' ', $this->keys));
  173. }
  174. // // exclude results from previous and_query
  175. // !! trigering solr "too many boolean clauses" error
  176. // $exclude_and_results_conditions = $this->or_query->createConditionGroup('AND');
  177. // foreach ($this->results['nids'] as $nid) {
  178. // $exclude_and_results_conditions->addCondition('nid', $nid, '<>');
  179. // }
  180. // $this->or_query->addConditionGroup($exclude_and_results_conditions);
  181. if (preg_match_all('/[WTRPCMFGSO]\d{4}/i', implode(' ', $this->keys), $matches)) {
  182. // in case we search for material references like W0117
  183. $ref_conditions = $this->or_query->createConditionGroup('OR');
  184. foreach ($matches[0] as $key => $value) {
  185. $ref_conditions->addCondition('field_reference', $value);
  186. }
  187. $this->or_query->addConditionGroup($ref_conditions);
  188. }
  189. if ($this->filters) {
  190. // FILTERS
  191. $or_filters_conditions = $this->or_query->createConditionGroup('OR');
  192. foreach ($this->filters as $filter) {
  193. $filter = (int) $filter;
  194. foreach (['thesaurus_tid'] as $field) { // 'tag_tid',
  195. $or_filters_conditions->addCondition($field, $filter);
  196. }
  197. }
  198. $this->or_query->addConditionGroup($or_filters_conditions);
  199. if(!$this->keys) {
  200. // if no keys but filters switch query to direct and add wildcard solr keys *:*
  201. $direct_or_parse_mode = \Drupal::service('plugin.manager.search_api.parse_mode')
  202. ->createInstance('direct');
  203. $direct_or_parse_mode->setConjunction('OR');
  204. $this->or_query->setParseMode($direct_or_parse_mode);
  205. // $this->or_query->keys('*:*');
  206. }
  207. }
  208. // Restrict the search to specific languages.
  209. $this->or_query->setLanguages([$lang]);
  210. // Do paging.
  211. // $this->or_query->range($this->offset, $this->limit);
  212. // retrieve all results
  213. // $this->or_query->range(0, -1);
  214. // Add sorting.
  215. $this->or_query->sort('search_api_relevance', 'DESC');
  216. // Set one or more tags for the query.
  217. // @see hook_search_api_query_TAG_alter()
  218. // @see hook_search_api_results_TAG_alter()
  219. $this->or_query->addTag('materio_sapi_search_or_query');
  220. $or_results = $this->or_query->execute();
  221. foreach ($or_results as $result) {
  222. $nid = $result->getField('nid')->getValues()[0];
  223. // !! have to remove duplicates instead of $exclude_and_results_conditions (solr too many boolean clauses)
  224. if ( !in_array($nid, $this->results['nids']) ) {
  225. $this->results['uuids'][] = $result->getField('uuid')->getValues()[0];
  226. $this->results['nids'][] = $nid;
  227. }
  228. }
  229. // todo you may like / more like this
  230. }
  231. private function defaultQuery(){
  232. $lang = \Drupal::languageManager()->getCurrentLanguage()->getId();
  233. $entity_storage = \Drupal::entityTypeManager()->getStorage('node');
  234. $this->query = $entity_storage->getQuery()
  235. ->condition('type', ['materiau', 'thematique'], 'IN')
  236. ->condition('status', '1')
  237. ->condition('langcode', $lang)
  238. ->range($this->offset, $this->limit)
  239. ->accessCheck(TRUE)
  240. ->sort('created', 'DESC');
  241. // ->condition('field_example', 'test_value')
  242. $this->results = $this->query->execute();
  243. $this->count_query = $entity_storage->getQuery()
  244. ->condition('type', ['materiau', 'thematique'], 'IN')
  245. ->condition('langcode', $lang)
  246. ->accessCheck(TRUE)
  247. ->condition('status', '1')
  248. ->count();
  249. $this->count = $this->count_query->execute();
  250. }
  251. /**
  252. * get params from request
  253. */
  254. private function parseRequest(Request $request){
  255. // Get the typed string from the URL, if it exists.
  256. $this->keys = $request->query->get('keys');
  257. if($this->keys){
  258. $this->keys = mb_strtolower($this->keys);
  259. $this->keys = Tags::explode($this->keys);
  260. // \Drupal::logger('materio_sapi')->notice($this->keys);
  261. }
  262. // get the exacte term id in case of autocomplete
  263. // $this->terms = $request->query->get('terms');
  264. $t = $request->query->get('terms');
  265. // $this->terms = strlen($t) ? explode(',', $t) : null;
  266. $this->terms = strlen($t) ? json_decode($t) : null;
  267. // get the filters of advanced search
  268. $f = $request->query->get('filters');
  269. $this->filters = strlen($f) ? explode(',', $f) : null;
  270. // $this->allparams = $request->query->all();
  271. // $request->attributes->get('_raw_variables')->get('filters')
  272. //
  273. $this->offset = $request->query->get('offset') ?? $this->offset;
  274. $this->limit = $request->query->get('limit') ?? $this->limit;
  275. }
  276. /**
  277. * Handler for ajax search.
  278. */
  279. public function getResults(Request $request) {
  280. $this->parseRequest($request);
  281. $resp = [
  282. 'range' => array(
  283. 'offset' => $this->offset,
  284. 'limit' => $this->limit
  285. ),
  286. ];
  287. if ($this->keys || $this->filters) {
  288. $lang = \Drupal::languageManager()->getCurrentLanguage()->getId();
  289. $this->sapiQuery();
  290. $resp['keys'] = json_encode($this->keys);
  291. $resp['terms'] = json_encode($this->terms);
  292. $resp['filters'] = $this->filters;
  293. // $resp['count'] = $this->results->getResultCount();
  294. $resp['count'] = count($this->results['nids']);
  295. $resp['exactematch_count'] = $this->exactematch_count;
  296. $resp['infos'] = t('The search found @exactmatchcount exact match result(s) for @count total result(s) with', array(
  297. "@exactmatchcount" => $resp['exactematch_count'],
  298. "@count" => $resp['count']
  299. ));
  300. if ($this->keys) {
  301. $resp['infos'] .= t(' keywords @keys', array(
  302. "@keys" => implode(', ', $this->keys)
  303. ));
  304. }
  305. if ($this->keys && $this->filters) {
  306. $resp['infos'] .= " and";
  307. }
  308. if ($this->filters) {
  309. // get the terms names from tid
  310. $storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
  311. $filters_names = [];
  312. foreach ($this->filters as $tid) {
  313. /** @var \Drupal\Core\Entity\EntityInterface $entity */
  314. $entity = $storage->load($tid);
  315. $entity_trans = \Drupal::service('entity.repository')->getTranslationFromContext($entity, $lang);
  316. $filters_names[] = $entity_trans->getName();
  317. }
  318. $resp['infos'] .= t(' filters @filters', array(
  319. "@filters" => implode(', ', $filters_names)
  320. ));
  321. }
  322. // $resp['options'] = $this->query->getOptions();
  323. // $uuids = [];
  324. // $nids = [];
  325. // foreach ($this->results as $result) {
  326. // $uuids[] = $result->getField('uuid')->getValues()[0];
  327. // $nids[] = $result->getField('nid')->getValues()[0];
  328. // }
  329. $resp['nids'] = array_slice($this->results['nids'], $this->offset, $this->limit);
  330. $resp['uuids'] = array_slice($this->results['uuids'], $this->offset, $this->limit);
  331. } else {
  332. // no keys or terms to search for
  333. // display the default base page
  334. $this->defaultQuery();
  335. // $uuids = [];
  336. $nids = [];
  337. // Using entityTypeManager
  338. // Get a node storage object.
  339. // $node_storage = \Drupal::entityTypeManager()->getStorage('node');
  340. foreach ($this->results as $result) {
  341. // $lang = \Drupal::languageManager()->getCurrentLanguage()->getId();
  342. // Load a single node.
  343. // $node = $node_storage->load($result);
  344. // check if has translation
  345. // i used to filter like bellow because of a graphql probleme
  346. // if ($node->hasTranslation($lang)) {
  347. $nids[] = $result;
  348. // }
  349. }
  350. // $resp['uuids'] = $uuids;
  351. $resp['nids'] = $nids;
  352. $resp['count'] = $this->count;
  353. $resp['infos'] = t('Please use the search form to search from our @count materials.', array(
  354. "@count" => $resp['count']
  355. ));
  356. }
  357. return new JsonResponse($resp);
  358. }
  359. /**
  360. * Handler for regular page search.
  361. */
  362. function pageHandler(Request $request){
  363. // \Drupal::logger('materio_sapi')->notice(print_r($request, true));
  364. $this->parseRequest($request);
  365. $resp = [
  366. "#title" => 'Base'
  367. ];
  368. if ($this->keys) {
  369. $resp['#title'] = implode(', ', $this->keys);
  370. // $this->sapiQuery();
  371. // $node_storage = \Drupal::entityTypeManager()->getStorage('node');
  372. // $node_view_builder = \Drupal::entityTypeManager()->getViewBuilder('node');
  373. // // $items = $this->results->getResultItems();
  374. // $nids = $this->results['nids'];
  375. // $this->items = [];
  376. // foreach ($nids as $nid) {
  377. // // \Drupal::logger('materio_sapi')->notice(print_r($nid, true));
  378. // try {
  379. // /** @var \Drupal\Core\Entity\EntityInterface $entity */
  380. // // $entity = $item->getOriginalObject()->getValue();
  381. // $entity = $node_storage->load($nid);
  382. // }
  383. // catch (SearchApiException $e) {
  384. // continue;
  385. // }
  386. // if (!$entity) {
  387. // continue;
  388. // }
  389. // // TODO: define dynamicly viewmode
  390. // $this->items[] = $node_view_builder->view($entity, 'teaser');
  391. // }
  392. // $resp['items'] = $this->items;
  393. $resp['items'] = [];
  394. }else{
  395. $resp['#markup'] = t("no keys to search for");
  396. }
  397. return $resp;
  398. }
  399. }