EGOCMS  18.0
EGOTEC Content-Managament-System
Ego_Search_Elastic.php
gehe zur Dokumentation dieser Datei
1 <?php
7 require_once('base/Ego_Search.php');
8 require_once('composer/vendor/autoload.php');
9 
17 {
23  private $client = null;
24 
30  private $officeImport = false;
31 
37  private const MAX_CLAUSE_COUNT = 10000;
38 
39  public function optimize() {
40  // no function
41  }
42 
51  public function __construct($table = '', $param = []) {
52  if (!Ego_System::checkLicence($GLOBALS['egotec_conf']['lib_dir'] . 'elastic')) {
53  throw new Exception("missing licence");
54  }
55 
56  $this->officeImport = Ego_System::checkLicence($GLOBALS['egotec_conf']['lib_dir'] . 'office');
57 
58  $this->config = [
59  'index' => '', // Pfad für den Index (demo_de / wiki_de / multimedia_de)
60  'type' => 'content', // je INDEX ist nur ein type erlaubt, siehe https://www.elastic.co/guide/en/elasticsearch/reference/6.0/removal-of-types.html
61  'leading_wildcard' => true
62  ];
63 
64  if (empty($table)) {
65  $table = $GLOBALS['site']->pageTable;
66  }
67 
68  if (!$GLOBALS['egotec_conf']['elastic']['max_results']) {
69  $GLOBALS['egotec_conf']['elastic']['max_results'] = 10000;
70  }
71 
72  // examples host
73  // https://username:password@foo.com:9200/
74  // http://foo.com:9200/
75 
76  $hosts = [];
77  for ($i = 0; $i < 4; $i++) {
78  if ($GLOBALS['egotec_conf']['elastic']['host' . $i]) {
79  $hosts[] = trim($GLOBALS['egotec_conf']['elastic']['host' . $i], '/');
80  }
81  }
82 
83  if (empty($hosts)) {
84  throw new Exception("missing hosts for elastic");
85  }
86 
87  $this->client = Elasticsearch\ClientBuilder::create()->setHosts($hosts)->build();
88 
89  $this->config['index'] = strtolower($table);
90  $this->config['table'] = $table;
91  $this->config['param'] = $param;
92 
93  $this->indexCreate();
94  $this->createPipeline();
95  }
96 
104  public function delete($id) {
105  $params = [
106  'index' => $this->config['index'],
107  'type' => $this->config['type'],
108  'id' => $id
109  ];
110 
111  $this->client->delete($params);
112 
113  return true;
114  }
115 
122  public function reset() {
123  $GLOBALS['monitor']['search_reset']++;
124 
125  $this->indexDelete(false);
126  $this->indexCreate();
127 
128  return true;
129  }
130 
131 
137  public function resetAll() {
138  $GLOBALS['monitor']['search_reset_all']++;
139 
140  $this->indexDelete(true);
141 
142  return true;
143  }
144 
150  public function indexCreate() {
151  $params = [
152  'index' => $this->config['index']
153  ];
154 
155  if (!$this->client->indices()->exists($params)) {
156  $params = [
157  'index' => $this->config['index'],
158  'body' => [
159  'settings' => [
160  'number_of_shards' => $GLOBALS['egotec_conf']['elastic']['number_of_shards'],
161  'number_of_replicas' => $GLOBALS['egotec_conf']['elastic']['number_of_replicas'],
162  'index.mapping.ignore_malformed' => true,
163  'index.mapping.total_fields.limit' => self::MAX_CLAUSE_COUNT
164  ],
165  'mappings' => [
166  $this->config['type'] => [
167  'properties' => [
168  'suggest' => [
169  'type' => 'completion'
170  ]
171  ]
172  ]
173  ]
174  ]
175  ];
176 
177  $this->client->indices()->create($params);
178  }
179 
180  return true;
181  }
182 
190  public function indexDelete($all) {
191  $params = [
192  'index' => $all ? "_all" : $this->config['index'],
193  'client' => ['ignore' => [400, 404]]
194  ];
195 
196  if ($this->client->indices()->exists($params)) {
197  $this->client->indices()->delete($params);
198  }
199 
200  return true;
201  }
202 
203  public function updateBulk($pages) {
204  foreach ($pages as $page) {
205  $page = $this->indexFiles($page);
206 
207  $params['body'][] = [
208  'index' => [
209  '_index' => $this->config['index'],
210  '_type' => $this->config['type'],
211  '_id' => $page->field['id']
212  ]
213  ];
214 
215  $params['body'][] = $this->getBody($page);
216  }
217 
218  $this->client->bulk($params);
219  }
220 
230  public function update($id, $page, $count = []) {
231  $page = $this->indexFiles($page);
232 
233  $GLOBALS['monitor']['search_update_count']++;
234 
235  $params = [
236  'index' => $this->config['index'],
237  'type' => $this->config['type'],
238  'id' => $id,
239  'body' => $this->getBody($page),
240  'client' => ['ignore' => 404]
241  ];
242 
243  $this->client->index($params);
244 
245  return true;
246  }
247 
253  private function getBody($page) {
254  // Alle Inhalte aus extra._contents an den Inhalt von field.content hängen
255  $extra_contents = is_array($page->extra['_contents'])
256  ? implode(
257  ' ',
258  array_filter(
259  array_map(
260  function ($value) {
261  return trim(strip_tags(implode(' ', $value)));
262  },
264  $page->extra['_contents'],
265  function ($value) {
266  if (
267  preg_match('/^(index\.php\?|https?:\/\/|[^@ ]+@[^ ]+|\d{4}-\d{2}-\d{2}|\d{2}:\d{2}:?)/si', $value) // Keine URLs, E-Mail und Datum/Uhrzeit
268  || preg_match('/^\{.*?\}$/si', $value) // Kein JSON
269  ) {
270  $value = '';
271  } else {
272  // Zeilenumbrüche umwandeln
273  $value = preg_replace('/(\r\n|\r|\n)/s', ' ', $value);
274  }
275  return $value;
276  }
277  )
278  )
279  )
280  )
281  : '';
282 
283  // Blöcke aus dem Extrafeld nicht doppelt in die Suche übernehmen
284  $extra = $this->_getExtra($page);
285  unset($page->extra['_contents']);
286  $extra_values = $this->_getExtra($page, true);
287 
288  $keywords = $this->_getContent($page, 'keywords');
289  $name = $this->_getContent($page, 'name');
290  $title = $this->_getContent($page, 'title');
291  $short = $this->_getContent($page, 'short');
292  $content = trim(
293  preg_replace( // Mehrfache Leerzeichen entfernen
294  '/\s{2,}?/ms',
295  ' ',
296  $this->_getContent($page, 'content') . ($extra_contents ? ' ' . $extra_contents : '')
297  )
298  );
299 
300  $suggestions = array_values(
301  array_unique(
302  array_filter(
303  array_map(
304  function ($value) {
305  return preg_replace('/\W/', '', $value);
306  },
307  explode(
308  ' ',
309  mb_strtolower(
310  html_entity_decode(
311  implode(
312  ' ',
313  [$keywords, $name, $title, $short, $content]
314  )
315  )
316  )
317  )
318  ),
319  function ($value) {
320  return !is_numeric($value) && strlen($value) > 2;
321  }
322  )
323  )
324  );
325 
326  return [
327  'keywords' => $keywords,
328  'url' => $this->_getContent($page, 'url'),
329  'name' => $name,
330  'title' => $title,
331  'short' => $short,
332  'content' => $content,
333  'extra' => $extra,
334  'extra_values' => trim(
335  implode(
336  ' ',
337  array_values(
338  array_map(
339  function ($value) {
340  return implode(' ', $value);
341  },
342  $extra_values
343  )
344  )
345  )
346  ),
347  'type' => $page->field['type'],
348  // 'nav_hide' => (int)$page->field['nav_hide'],
349  'ignore_search' => (($page->field['nav_hide'] & 4) == 4) ? 1 : 0,
350  'inactive' => (int)$page->field['inactive'],
351  'deleted' => (int)$page->field['deleted'],
352  'suggest' => [
353  'input' => $suggestions
354  ]];
355  }
356 
367  private function getSearchParam($tables, $search, $filter, $fuzzy, $id_list = []) {
368  $fields = ['keywords', 'url', 'name', 'title', 'short', 'content', 'extra_values'];
369 
370  // Verwendete "rewrite" Einstellung
371  $rewrite = 'top_terms_' . self::MAX_CLAUSE_COUNT;
372 
373  // Immer alles klein verwenden: Elastic startet eine Case Sensitive Suche, wenn ein Großbuchstabe im Suchbegriff vorkommt
374  $search = trim(mb_strtolower($search));
375 
376  // Suchbegriff automatisch korrigieren
377  $search = trim(preg_replace([
378  '/[+-?*]( |$)/',
379  '/(^| )\{(.*?)\}/'
380  ], [
381  ' ',
382  '\\1\\2'
383  ], $search));
384 
385  // Suchbegriffe mit Bindestrich automatisch um die einzelnen Begriffe erweitern (diese werden sonst nicht gefunden)
386  if (preg_match_all('/( |^)([^+-][^ "]+-[^ "]+)/is', $search, $matches)) {
387  foreach ($matches[2] as $word) {
388  $search .= ' ' . str_replace('-', ' ', $word);
389  }
390  }
391 
392  // Suchrelevanz für Ein-Mandanten-Suche hinzufügen
393  $count = [];
394  if ($tables === null) {
395  try {
396  list($name, $lang) = preg_split('/_(?=[^_]*$)/', $this->config['table']);
397  $site = new Site($name, $lang);
398  $count = $site->getSearchCount();
399  } catch (Site_Exception $e) {
400  // ignorieren
401  }
402  }
403 
404  $must = [];
405  $must_not = [];
406  $should = [];
407  $must_should = [];
408 
409  if (preg_match_all('/(".*?"|[^ ]+)/is', $search, $matches)) {
410  // Zusätzliche Phrasen-Suche ausführen
411  $match_phrase = sizeof($matches[0]) > 1 && !preg_match('/(^| )["*+-]/', $search);
412 
413  foreach ($fields as $field) {
414  $boost = 1;
415  if ($field == 'extra_values' && isset($count['extra'])) {
416  $boost = (int)$count['extra'];
417  } elseif (isset($count[$field])) {
418  $boost = (int)$count[$field];
419  }
420 
421  foreach ($matches[0] as $query) {
422  // Hotfix: Enthält der Suchbegriff ein "-", muss dieser eine exakte Suche auslösen
423  if (!in_array($query[0], ['"', '-']) && strpos($query, '-') !== false) {
424  $query = "\"$query\"";
425  }
426 
427  if ($query[0] == '"') {
428  // Phrase
429  $query = trim($query, '"');
430  $should[] = [
431  'match_phrase' => [
432  $field => [
433  'query' => $query,
434  'boost' => $boost
435  ]
436  ]
437  ];
438  } else {
439  if ($fuzzy) {
440  // Fuzzy
441  $should[] = [
442  'fuzzy' => [
443  $field => [
444  'value' => $query,
445  'rewrite' => $rewrite,
446  'boost' => $boost
447  ]
448  ]
449  ];
450  } elseif (preg_match('/^(.*?)~([0-9.]+|)$/', $query, $match)) {
451  // Fuzzy für diesen Suchbegriff
452  $query = $match[1];
453  $fuzziness = floatval($match[2] !== '' ? $match[2] : 1);
454 
455  $should[] = [
456  'fuzzy' => [
457  $field => [
458  'value' => $query,
459  'rewrite' => $rewrite,
460  'boost' => $boost,
461  'fuzziness' => round(2 * $fuzziness)
462  ]
463  ]
464  ];
465  } else {
466  // Wildcard
467  $wildcard = function ($query) {
468  if (strpos($query, '*') === false) {
469  $query = "*$query*";
470  }
471  return $query;
472  };
473 
474  switch ($query[0]) {
475  case '+':
476  $query = ltrim($query, '+');
477  $must_should[md5($query)][] = [
478  'wildcard' => [
479  $field => [
480  'value' => $wildcard($query),
481  'rewrite' => $rewrite,
482  'boost' => $boost
483  ]
484  ]
485  ];
486  break;
487  case '-':
488  $query = ltrim($query, '-');
489  $must_not[] = [
490  'wildcard' => [
491  $field => [
492  'value' => $wildcard($query),
493  'rewrite' => $rewrite,
494  'boost' => $boost
495  ]
496  ]
497  ];
498  break;
499  default:
500  $should[] = [
501  'wildcard' => [
502  $field => [
503  'value' => $wildcard($query),
504  'rewrite' => $rewrite,
505  'boost' => $boost
506  ]
507  ]
508  ];
509  }
510  }
511  }
512  }
513 
514  // Ein Suchbegriff mit mehreren Wörtern erzeugt gleichzeitig auch eine bevorzugte Phrasen-Suche
515  if ($match_phrase) {
516  $should[] = [
517  'match_phrase' => [
518  $field => [
519  'query' => trim($search),
520  'boost' => $boost + 1
521  ]
522  ]
523  ];
524  }
525  }
526  }
527 
528  $minimum_should_match = $search == '' && (!empty($filter) || !empty($this->extraQuery)) ? 0 : 1;
529 
530  if (sizeof($must_should)) {
531  $minimum_should_match = 0;
532 
533  foreach ($must_should as $items) {
534  $must[] = [
535  'bool' => [
536  'should' => $items,
537  'minimum_should_match' => 1
538  ]
539  ];
540  }
541  }
542 
543  if ($this->config['param']['only_active']) {
544  $must[] = [
545  'match' => [
546  'inactive' => 0
547  ]
548  ];
549  }
550  if (!$this->config['param']['deleted'] && !$this->conig['param']['deleted_or']) {
551  $must[] = [
552  'match' => [
553  'deleted' => 0
554  ]
555  ];
556  }
557  if ($this->config['param']['search']) {
558  $must[] = [
559  'match' => [
560  'ignore_search' => 0
561  ]
562  ];
563  }
564 
565  // Nur Treffer in einer ID Liste ermitteln
566  if (!empty($id_list)) {
567  $must[] = [
568  'constant_score' => [
569  'filter' => [
570  'terms' => [
571  '_id' => $id_list
572  ]
573  ]
574  ]
575  ];
576  }
577 
578  // Limit ermitteln (standardmäßig werden alle Ergebnisse zurückgegeben)
579  $from = 0;
580  $size = (int) $GLOBALS['egotec_conf']['elastic']['max_results'];
581 
582  // Extra-Suche hinzufügen
583  if (!empty($this->extraQuery)) {
584  $must[] = [
585  'query_string' => [
586  'query' => $this->extraQuery
587  ]
588  ];
589  }
590 
591  // Filter ermitteln
592  if (!empty($filter)) {
593  if ($search == '') {
594  /* Ohne Suchbegriff liefert die Suche keine Ergebnisse.
595  * Der Filter wird dann zum Suchkriterium. */
596  $must[] = [
597  'query_string' => [
598  'query' => $filter
599  ]
600  ];
601  $filter = [];
602  } else {
603  $filter = [
604  'query_string' => [
605  'query' => $filter
606  ]
607  ];
608  }
609  } else {
610  $filter = [];
611  }
612 
613  return [
614  'index' => $tables ? $tables : $this->config['index'],
615  // KEINE Inhalte zurückliefern
616  '_source' => false,
617  'type' => $this->config['type'],
618  'sort' => ['_score'], // The order defaults to desc when sorting on the _score, and defaults to asc when sorting on anything else.
619  'body' => [
620  'from' => $from,
621  'size' => $size,
622  'query' => [
623  'bool' => [
624  'must' => $must,
625  'must_not' => $must_not,
626  'should' => $should,
627  'filter' => $filter,
628  'minimum_should_match' => $minimum_should_match
629  ]
630  ]
631  ]
632  ];
633  }
634 
638  private function createPipeline() {
639  if ($this->officeImport) {
640  $params = [
641  'id' => 'attachment',
642  'body' => [
643  'description' => 'Extract attachment information',
644  'processors' => [
645  [
646  'attachment' => [
647  'field' => 'base64',
648  'indexed_chars' => -1
649  ]
650  ]
651  ]
652  ]
653  ];
654 
655  $this->client->ingest()->putPipeline($params);
656  }
657  }
658 
667  private function indexFile(Page $page, $path) {
668  if (!Ego_System::file_exists($path)) {
669  return null;
670  }
671 
672  @ini_set("memory_limit", "-1");
673 
674  $pageTable = strtolower($page->getSite()->pageTable);
675  $identity = $page->getIdentity();
676 
677  $params = [
678  'index' => $pageTable,
679  'type' => $this->config['type'],
680  'id' => $identity,
681  'pipeline' => 'attachment',
682  'body' => [
683  'base64' => base64_encode(Ego_System::file_get_contents($path, false))
684  ]
685  ];
686  $result = $this->client->index($params);
687 
688  if ($result) {
689  $content = $this->getIndexFile($pageTable, $identity);
690  $this->delete($identity);
691 
692  return $content;
693  }
694 
695  return null;
696  }
697 
706  private function getIndexFile($pageTable, $identity) {
707  $params = [
708  'index' => strtolower($pageTable),
709  'type' => $this->config['type'],
710  'id' => $identity
711  ];
712 
713  $response = $this->client->get($params);
714  return $response['_source']['attachment']['content'];
715  }
716 
724  private function indexFiles(Page $page) {
725  if (!$this->officeImport) {
726  return $page;
727  }
728 
729  if (
730  $page->field['type'] == 'multimedia/file'
731  && !$page->extra['_indexed']
732  ) {
733  if (!preg_match('/(image|video|audio|zip|exe|rar|octet-stream|postscript)/is', $page->extra['mime_type'])) {
734  $page->extra['_indexed'] = true;
735  try {
736  $content = $this->indexFile($page, $GLOBALS['egotec_conf']['var_dir'] . 'media/' . $page->getSite()->name . '/' . $page->getMediaFilename());
737  $page->field['content'] = Ego_System::filterNonUtf8($content, '', true);
738  } catch (Exception $e) {
739  // Datei kann nicht indiziert werden
740  if (
741  strpos($e->getMessage(), 'EncryptedDocumentException') === false
742  && strpos($e->getMessage(), 'TikaException') === false
743  ) {
744  egotec_error_log($page->getIdentity() . " Fehler beim Indizieren: " . $page->extra['mime_type'] . "\n" . $e->getMessage());
745  }
746  }
747 
748  $page->update([], true, true);
749  }
750  }
751  //else {
752  // TODO MediaPool nun auch indezieren?
753  /*foreach ($page->getMediapool() as $file) {
754  if (!$file['isImage']) {
755  $content = $this->indexFile($page, $file['file']);
756  }
757  }*/
758  //}
759 
760  return $page;
761  }
762 
774  public function search($search, $relation, $query, $filter = '', $fuzzy = false) {
775  $this->checkSearch($search);
776 
777  $GLOBALS['monitor']['search_count']++;
778  $GLOBALS['monitor']['search_length'] += mb_strlen($search);
779  $GLOBALS['monitor']['search_words'] += substr_count($search, ' ') + 1;
780 
781  $params = $this->getSearchParam(null, $search, $filter, $fuzzy, $query['id_list']);
782 
783  $start = microtime(true);
784  try {
785  $results = $this->client->search($params);
786  } catch (Exception $e) {
787  $this->error($e);
788  }
789 
790  $hits = $results["hits"]["hits"];
791  $stop = microtime(true);
792 
793  $duration = (int)(($stop - $start) * 1000);
794  $GLOBALS['monitor']['search_duration'] += $duration;
795 
796  if (empty($hits)) {
797  unset($query['order']);
798  unset($query['limit']);
799  $query['where'] = '1=0';
800  return $query;
801  }
802 
803  $ids = [];
804 
805  foreach ($hits as $key => $value) {
806  $ids[$value["_id"]] = $value["_score"];
807  }
808 
809  $max_value = $results["hits"]["max_score"];
810  $min_value = min(array_values($ids));
811 
812  if ($max_value == $min_value) {
813  $multiply = 4 / $max_value;
814  $min_value = 0;
815  } else {
816  $multiply = 4 / ($max_value - $min_value);
817  }
818 
819  return $this->buildQuery($ids, $relation, $query, $min_value, $multiply);
820  }
821 
834  public function globalSearch($search, $sites = [], $query = [], $param = [], $sort = [], $filter = '') {
835  $this->checkSearch($search);
836 
837  $start1 = microtime(true);
838  $GLOBALS['monitor']['search_global_count']++;
839  $GLOBALS['monitor']['search_global_length'] += mb_strlen($search);
840  $GLOBALS['monitor']['search_global_words'] += substr_count($search, ' ') + 1;
841 
842  // Standardmäßig im Frontend nur Seiten anzeigen, die auch von der Suche gefunden werden dürfen
843  if (!isset($param['search']) && empty($GLOBALS['admin_area'])) {
844  $param['search'] = true;
845  }
846 
847  // Betroffene Tabellen ermitteln
848  $lang = $_REQUEST['lang'] ? $_REQUEST['lang'] : ($GLOBALS['site'] ? $GLOBALS['site']->language : null);
849  $relations = [];
850  if (empty($sites)) {
851  // keine bestimmten Mandanten, also alle durchsuchen
852  $sites = Ego_System::getAllSites();
853  }
854  $tables = $this->getTables($sites, $lang, $relations);
855 
856  // Suche durchführen
857  $start2 = microtime(true);
858  $params = $this->getSearchParam($tables, $search, $filter, (bool)$param['fuzzy']);
859  try {
860  $results = $this->client->search($params);
861  } catch (Exception $e) {
862  $this->error($e);
863  }
864  $stop2 = microtime(true);
865  $duration = (int)(($stop2 - $start2) * 1000);
866  $GLOBALS['monitor']['search_global_d2'] += $duration;
867 
868  $hits = $results["hits"]["hits"];
869 
870  if (empty($hits)) {
871  return [];
872  }
873 
874  $min_value = PHP_INT_MAX;
875 
876  $sorted_hits = [];
877  foreach ($hits as $hit) {
878  if (!isset($sorted_hits[$hit['_index']])) {
879  $sorted_hits[$hit['_index']] = [];
880  }
881  $sorted_hits[$hit['_index']][$hit['_id']] = $hit['_score'];
882  if ($hit['_score'] < $min_value) {
883  $min_value = $hit['_score'];
884  }
885  }
886 
887  $max_value = $results["hits"]["max_score"];
888 
889  if ($max_value == $min_value) {
890  $multiply = 4 / $max_value;
891  $min_value = 0;
892  } else {
893  $multiply = 4 / ($max_value - $min_value);
894  }
895 
896  // Treffer sammeln und sortiert zurückliefern
897  $pages = [];
898  foreach ($sorted_hits as $table => $ids) {
899  $site = $relations[$table];
900  foreach ($site->getPages($this->buildQuery($ids, $site->pageTable . '.id', $query, $min_value, $multiply), $param) as $page) {
901  $pages[] = $page;
902  }
903  }
904  $pages = $this->sortPages($pages, $query['order'], $sort);
905 
906  $stop1 = microtime(true);
907  $duration = (int)(($stop1 - $start1) * 1000);
908  $GLOBALS['monitor']['search_global_d1'] += $duration;
909 
910  return $pages;
911  }
912 
922  public function getSuggestions($query, $sites = [], $max = 5) {
923  $tables = $this->config['index'];
924  if (!empty($sites)) {
925  $tables = $this->getTables($sites);
926  }
927  try {
928  $results = $this->client->search([
929  'index' => $tables,
930  // KEINE Inhalte zurückliefern
931  '_source' => false,
932  'type' => $this->config['type'],
933  'size' => $max,
934  'sort' => ['_score'], // The order defaults to desc when sorting on the _score, and defaults to asc when sorting on anything else.
935  'body' => [
936  'suggest' => [
937  'suggest' => [
938  'prefix' => $query,
939  'completion' => [
940  'field' => 'suggest',
941  'skip_duplicates' => true
942  ]
943  ]
944  ]
945  ]
946  ]);
947  } catch (Exception $e) {
948  $this->error($e);
949  }
950 
951  $suggestions = [];
952  if (is_array($results['suggest']['suggest'][0]['options'])) {
953  foreach ($results['suggest']['suggest'][0]['options'] as $result) {
954  if (mb_strtolower($query) != mb_strtolower($result['text'])) {
955  $suggestions[] = $result['text'];
956  }
957  }
958  }
959 
960  return $suggestions;
961  }
962 
974  private function buildQuery($result, $relation, $query, $min_value, $multiply) {
975  if (!empty($result)) {
976  $ids = array_keys($result);
977  if (empty($query['where'])) {
978  $query['where'] = '';
979  } else {
980  $query['where'] .= ' AND ';
981  }
982 
983  // Für Oracle splitten: maximal 1000 pro Liste
984  $id_groups = [];
985  foreach (array_chunk($ids, 999) as $id_group) {
986  $id_groups[] = "$relation IN (" . implode(', ', $id_group) . ")";
987  }
988  $query['where'] .= '(' . implode(' OR ', $id_groups) . ')';
989 
990  $query['fields2'] = 'CASE ';
991  foreach ($result as $id => $value) {
992  $query['fields2'] .= "WHEN $relation = $id THEN " . (str_replace(',', '.', ($value - $min_value) * $multiply)) . " ";
993  }
994  $query['fields2'] .= 'ELSE 0 END AS score';
995  if (empty($query['order'])) {
996  $query['order'] = 'score DESC';
997  }
998  } else { // No result, so construct an query with no results.
999  unset($query['order']);
1000  unset($query['limit']);
1001  $query['where'] = '1=0';
1002  }
1003  return $query;
1004  }
1005 
1014  public function setExtraQuery($query, $bind = []) {
1015  // Binds anwenden
1016  if (is_array($bind)) {
1017  // Zeichen, die von Elastic verwendet werden: + - && || ! ( ) { } [ ] ^ " ~ * ? : \ /
1018  $reserved_characters = preg_quote('+-&|!(){}[]^"~*?:\\');
1019  foreach ($bind as $key => $value) {
1020  $query = str_replace(
1021  ":$key",
1022  preg_replace_callback(
1023  '/[' . $reserved_characters . ']/',
1024  function ($matches) {
1025  return '\\' . $matches[0];
1026  },
1027  $value
1028  ),
1029  $query
1030  );
1031  }
1032  }
1033 
1034  // Elastic Query generieren
1035  $Elastic_query = $query;
1036  $sub_queries = preg_split('/\s+(and|or)\s+/si', $query);
1037  foreach ($sub_queries as $sub_query) {
1038  if (preg_match('/(!?extra\.[^ !=<>]+)(\s*(like|>=|<=|!=|=|>|<)\s*(.*?))?$/si', trim($sub_query, '() '), $matches)) {
1039  $param = $matches[1];
1040  $operator = mb_strtolower($matches[3]);
1041  $value = trim($matches[4], '\'"');
1042 
1043  // Feld darf nicht im Dokument gesetzt sein
1044  $exclude = false;
1045  if ($param[0] == '!') {
1046  $exclude = true;
1047  $param = substr($param, 1);
1048  }
1049 
1050  if ($exclude) {
1051  // Ausnahme bilden
1052  $replace = "-$param:*" . (sizeof($sub_queries) == 1 ? ' AND *' : '');
1053  } else {
1054  // Elastic Syntax schreiben
1055  $replace = "$param:";
1056  if (!is_numeric($value)) {
1057  if ($operator == 'like') {
1058  $value = $this->prepareSearch(str_replace('%', '', $value), '', true);
1059  } else {
1060  $value = "'$value'";
1061  }
1062  }
1063 
1064  // Vergleich bilden
1065  switch ($operator) {
1066  case '=':
1067  case 'like':
1068  $replace .= $value;
1069  break;
1070  case '!=':
1071  $replace = "($param:* NOT $param:$value)";
1072  break;
1073  case '>':
1074  $replace .= '{' . $value . ' TO *}';
1075  break;
1076  case '>=':
1077  $replace .= '[' . $value . ' TO *]';
1078  break;
1079  case '<':
1080  $replace .= '{* TO ' . $value . '}';
1081  break;
1082  case '<=':
1083  $replace .= '[* TO ' . $value . ']';
1084  break;
1085  default:
1086  $replace .= $value;
1087  }
1088  }
1089 
1090  $Elastic_query = str_replace($matches[0], $replace, $Elastic_query);
1091  }
1092  }
1093 
1094  $this->extraQuery = $Elastic_query;
1095  }
1096 
1102  public function getConfig() {
1103  return $this->config;
1104  }
1105 
1111  public function clearCache() {
1112  foreach ($this->hosts as $host) {
1113  @file_get_contents($host . '_cache/clear');
1114  }
1115  }
1116 
1125  private function getTables($sites, $lang = '', &$relations = []) {
1126  $tables = '';
1127  if (!$lang) {
1128  $lang = $_REQUEST['lang'] ? $_REQUEST['lang'] : ($GLOBALS['site'] ? $GLOBALS['site']->language : null);
1129  }
1130  foreach ($sites as $site) {
1131  try {
1132  if (is_string($site)) {
1133  $site = new Site($site);
1134  }
1135  if ($lang) {
1136  $site->setLanguage($lang);
1137  }
1138  } catch (Exception $e) {
1139  // Mandant existiert nicht in dieser Sprache, ignorieren
1140  continue;
1141  }
1142  $table = strtolower($site->pageTable);
1143  $tables = $tables . ',' . $table;
1144  $relations[$table] = $site;
1145  }
1146  return ltrim($tables, ',');
1147  }
1148 
1156  private function error($e) {
1157  $response = @json_decode($e->getMessage(), true);
1158  $messages = [];
1159  if ($response['error']) {
1160  if ($response['error']['root_cause']) {
1161  foreach ($response['error']['root_cause'] as $cause) {
1162  $messages[] = $cause['type'] . ' for index ' . $cause['index'];
1163  }
1164  }
1165  if ($response['error']['failed_shards']) {
1166  foreach ($response['error']['failed_shards'] as $shard) {
1167  if (!empty($shard['reason']['caused_by'])) {
1168  $messages[] = 'caused by ' . $shard['reason']['caused_by']['type'] . ' (reason: ' . $shard['reason']['caused_by']['reason'] . ')';
1169  }
1170  }
1171  }
1172  }
1173  if (empty($messages)) {
1174  $messages[] = $e->getMessage();
1175  }
1176  egotec_error_log('Elastic Exception thrown (' . get_class($e) . ', Code ' . $e->getCode() . '): ' . implode('; ', $messages));
1177  Ego_System::header($response['status'] ?? 400);
1178  exit;
1179  }
1180 }
update($id, $page, $count=[])
__construct($table='', $param=[])
_getContent($page, $k)
Definition: Ego_Search.php:166
checkSearch($search)
Definition: Ego_Search.php:513
getSuggestions($query, $sites=[], $max=5)
static checkLicence($ini_path)
Definition: Ego_System.php:963
globalSearch($search, $sites=[], $query=[], $param=[], $sort=[], $filter='')
setExtraQuery($query, $bind=[])
static arrayFlatRecursive($array, $callback=null)
static getAllSites($username='', $perm='', $table=false, $type='')
update($param=array(), $matrix_flag=true, $asis=false)
Definition: Page.php:3370
static file_exists($file)
_getExtra($page, $clean=false)
Definition: Ego_Search.php:212
Definition: Page.php:27
sortPages($pages, $order='', $sort=array())
Definition: Ego_Search.php:150
getMediaFilename($force_lang=false, $suffix="")
Definition: Page.php:4836
getIdentity()
Definition: Page.php:9149
getSite()
Definition: Page.php:4426
search($search, $relation, $query, $filter='', $fuzzy=false)
static file_get_contents($filename, $utf8=true, $context=null)
static filterNonUtf8($s, $substitute="", $strict=false)
Definition: Ego_System.php:320
Definition: Site.php:29