source: spip-zone/_plugins_/indexer/trunk/iterateur/sphinx.php @ 82614

Last change on this file since 82614 was 82614, checked in by marcimat@…, 7 years ago

Laissons la propriété privée (hic) et faisons un getter sur limit.

File size: 16.1 KB
Line 
1<?php
2
3if (!defined('_ECRIRE_INC_VERSION')) return;
4
5/**
6 * Gestion de l'itérateur SPHINX
7 *
8 * @package SPIP\Indexer\Iterateur\Sphinx
9**/
10
11/**
12 * Créer une boucle sur un itérateur SPHINX
13 *
14 * Annonce au compilateur les "champs" disponibles,
15 *
16 * @param Boucle $b
17 *     Description de la boucle
18 * @return Boucle
19 *     Description de la boucle complétée des champs
20 */
21function iterateur_SPHINX_dist($b) {
22        $b->iterateur = 'SPHINX'; # designe la classe d'iterateur
23        $b->show = array(
24                'field' => array(
25                        'docs' => 'ARRAY',
26                        'meta' => 'ARRAY',
27                        'facets' => 'ARRAY',
28                        'query' => 'STRING',
29                        #'*' => 'ALL' // Champ joker *
30                )
31        );
32        return $b;
33}
34
35
36/**
37 * Iterateur SPHINX pour itérer sur des données
38 *
39 * La boucle SPHINX n'a toujours qu'un seul élément.
40 */
41class IterateurSPHINX implements Iterator {
42
43        /**
44         * Type de l'iterateur
45         * @var string
46         */
47        protected $type = 'SPHINX';
48
49        /**
50         * Commandes transmises à l'iterateur
51         * @var array
52         */
53        protected $command = array();
54
55        /**
56         * Infos de debug transmises à l'iterateur
57         * @var array
58         */
59        protected $info = array();
60
61        /**
62         * Instance de SphinxQL
63         * @var \Sphinx\SphinxQL\SphinxQL
64         */
65        protected $sphinxQL = null;
66
67        /**
68         * Instance de SphinxQL\QueryApi
69         * @var \Sphinx\SphinxQL\QueryAPi
70         */
71        protected $queryApi = null;
72
73        /**
74         * Résultat de la requête à Sphinx
75         * @var array
76         */
77        protected $result = array();
78
79        /**
80         * Cle courante
81         * @var null
82         */
83        protected $cle = null;
84
85        /**
86         * Valeur courante
87         * @var null
88         */
89        protected $valeur = null;
90
91        /**
92         * Constructeur
93         *
94         * @param  $command
95         * @param array $info
96         */
97        public function __construct($command, $info=array()) {
98
99                $this->command = $command + array(
100                        'index'     => array(),
101                        'selection' => array(),
102                        'recherche' => array(),
103                        'snippet'   => array(),
104                        'facet'     => array(),
105                        'select_filter' => array(),
106                );
107
108
109                $this->info = $info;
110
111                include_spip('inc/indexer');
112
113                $this->sphinxQL  = new \Sphinx\SphinxQL\SphinxQL(SPHINX_SERVER_HOST, SPHINX_SERVER_PORT);
114                $this->queryApi  = new \Sphinx\SphinxQL\QueryApi();
115
116                $this->setIndex($this->command['index']);
117                $this->setSelection($this->command['selection']);
118                $this->setRecherche($this->command['recherche']);
119                $this->setOrderBy($this->command['orderby']);
120                $this->setFacet($this->command['facet']);
121
122                $this->setSelectFilter($this->command['select_filter']);
123
124                $this->setSnippet($this->command);
125
126                $this->setPagination($this->command['pagination']);
127
128                $this->runQuery();
129        }
130
131
132        public function runQuery() {
133                $query  = $this->queryApi->get();
134                $result = $this->sphinxQL->allfetsel($query);
135                if (!$result) {
136                        return false;
137                }
138
139                // decaler les docs en fonction de la pagination demandee
140                if (is_array($result['query']['docs'])
141                AND $pagination = $this->queryApi->getLimit()) {
142                        list($debut) = array_map('intval',explode(',', $pagination));
143
144                        $result['query']['docs'] = array_pad($result['query']['docs'], - count($result['query']['docs']) - $debut, null);
145                        $result['query']['docs'] = array_pad($result['query']['docs'], $result['query']['meta']['total'], null);
146                }
147
148                $this->result = $result;
149                return true;
150        }
151
152
153        public function quote($m) {
154                return $this->queryApi->quote($m);
155        }
156
157
158        /**
159         * Définir la liste des index interrogés (FROM de la requête)
160         *
161         * Par défaut on utilise l'index déclaré dans la conf
162         *
163         * @param array $index Liste des index
164         * @return bool True si au moins un index est ajouté, false sinon
165        **/
166        public function setIndex($index) {
167                if (!is_array($index)) $index = array($index);
168                $index = array_filter($index);
169                if (!$index) {
170                        $index[] = SPHINX_DEFAULT_INDEX;
171                }
172                foreach ($index as $i) {
173                        $this->queryApi->from($i);
174                }
175                return true;
176        }
177
178
179
180        /**
181         * Définir la liste des champs récupérés (SELECT de la requête)
182         *
183         * Par défaut, en absence de précisions, on prend tous les champs
184         *
185         * @param array $select Liste des index
186         * @return bool True si au moins un index est ajouté, false sinon
187        **/
188        public function setSelection($select) {
189                if (!is_array($select)) $select = array($select);
190                $select = array_filter($select);
191                // si aucune selection demandée, on prend tout !
192                if (!$select) {
193                        $select[] = '*';
194                }
195                foreach ($select as $s) {
196                        $this->queryApi->select($s);
197                }
198                return true;
199        }
200
201
202
203        /**
204         * Définir la recherche fulltext
205         *
206         * @param array $index Liste des index
207         * @return bool True si au moins un index est ajouté, false sinon
208        **/
209        public function setRecherche($recherche) {
210                if (!is_array($recherche)) $recherche = array($recherche);
211                $recherche = array_filter($recherche);
212                if (!$recherche) {
213                        return false;
214                }
215                $match = implode(' ',$recherche);
216                $this->queryApi
217                        ->select('WEIGHT() AS score')
218                        ->where('MATCH(' . $this->quote( $recherche ) . ')');
219                return true;
220        }
221
222
223        public function setOrderby($orderby) {
224                if (!is_array($orderby)) $orderby = array($orderby);
225                $orderby = array_filter($orderby);
226                if (!$orderby) {
227                        return false;
228                }
229                foreach ($orderby as $order) {
230                        // juste ASC ou DESC sans le champ… passer le chemin…
231                        if (in_array(trim($order), array('ASC', 'DESC'))) {
232                                continue;
233                        }
234                        if (!preg_match('/(ASC|DESC)$/i', $order)) {
235                                $order .= ' ASC';
236                        }
237                        $this->queryApi->orderby($order);
238                }
239                return true;
240        }
241
242        /**
243         * Définir la pagination
244         *
245         * @param array $pagination (#DEBUT_DOCUMENTS,20)
246         * @return bool True si une pagination est demandee
247        **/
248        public function setPagination($pagination) {
249                # {pages #DEBUT_DOCUMENTS, 20}
250                if (is_array($pagination)) {
251                        $debut = intval($pagination[0]);
252                        if (isset($pagination[1]))
253                                $nombre = intval($pagination[1]);
254                        else
255                                $nombre = 20;
256                        $this->queryApi
257                                ->limit("$debut,$nombre");
258                        return true;
259                }
260        }
261
262        /**
263         * Définir le snippet
264         */
265        public function setSnippet($command) {
266                $snippet = array_filter($command['snippet']);
267                // si aucune selection demandée, on prend tout !
268                if (!$snippet) {
269                        return $this->setSnippetAuto($command);
270                } else {
271                        $ok = true;
272                        foreach ($snippet as $s) {
273                                if (!is_array($s)) continue;
274                                if (!$s['phrase']) {
275                                        $s['phrase'] = $this->getSnippetAutoPhrase($command);
276                                }
277                                $ok &= $this->setOneSnippet($s);
278                        }
279                }
280                return $ok;
281        }
282
283        /**
284         * Définir 1 snippet depuis sa description
285         *
286         * @param array $desc
287         * @return bool
288        **/
289        public function setOneSnippet($desc) {
290
291                $desc += array(
292                        'champ'  => 'content',
293                        'phrase' => '',
294                        'limit'  => 200,
295                        'as'     => 'snippet'
296                );
297                if (!$desc['phrase']) {
298                        return false;
299                }
300
301                $this->queryApi->addSnippetWords( $desc['phrase'] );
302                $desc['phrase'] = $this->queryApi->getSnippetWords();
303
304                if (!$desc['phrase'] OR !$desc['champ']) {
305                        return false;
306                }
307                $this->queryApi->select("SNIPPET($desc[champ], " . $this->quote($desc['phrase']) . ", 'limit=$desc[limit],html_strip_mode=strip') AS $desc[as]");
308                return true;
309        }
310
311        /**
312         * Définir automatiquement un snippet dans le champ 'snippet'
313         * à partir de la recherche et des filtres
314         */
315        public function setSnippetAuto($command) {
316                $phrase = $this->getSnippetAutoPhrase($command);
317                if (!$phrase) return false;
318                return $this->setOneSnippet(array('phrase' => $phrase));
319        }
320
321        /**
322         * Extrait de la commande de boucle les phrases pertinentes cherchées
323         *
324         * - Cherche la phrase de recherche
325         *
326         * @param array $command Commande de la boucle Sphinx
327         * @return string phrases séparées par espace.
328        **/
329        public function getSnippetAutoPhrase($command) {
330                $phrase = '';
331
332                // mots de la recherche
333                $recherche = $command['recherche'];
334                if (!is_array($recherche)) $recherche = array($recherche);
335                $recherche = array_filter($recherche);
336                $phrase .= implode(' ', $recherche);
337
338                return $phrase;
339        }
340
341
342        /**
343         * Définit les commandes FACET
344         *
345         * @param array $facets Tableau des facettes demandées
346         * @return bool
347        **/
348        public function setFacet($facets) {
349                $facets = array_filter($facets);
350                if (!$facets) {
351                        return false;
352                }
353                $ok = true;
354                foreach ($facets as $facet) {
355                        if (!isset($facet['alias']) OR !isset($facet['query'])) {
356                                $ok = false;
357                                continue;
358                        }
359                        $alias = trim($facet['alias']);
360                        $query = trim($facet['query']);
361                        if (!$alias OR !$query) {
362                                $ok =  false;
363                                continue;
364                        }
365                        $this->facet[] = array('alias' => $alias, 'query' => $query);
366                        $this->queryApi->facet($query);
367                }
368                return $ok;
369        }
370
371
372
373        /**
374         * Définit des filtres
375         *
376         * @param array $facets Tableau des filtres demandées
377         * @return bool
378        **/
379        public function setSelectFilter($filters) {
380                // compter le nombre de filtres ajoutés à la requête.
381                static $nb = 0;
382
383                $facets = array_filter($filters);
384                if (!$filters) {
385                        return false;
386                }
387                foreach ($filters as $filter) {
388                        // ignorer toutes les données vides
389                        if (!is_array($filter) OR !isset($filter['valeur']) OR !$valeur = $filter['valeur']) {
390                                continue;
391                        }
392                        if (is_string($valeur)) {
393                                $valeur = trim($valeur);
394                                $valeurs = array($valeur);
395                        } else {
396                                $valeurs = $valeur;
397                                $valeur = 'Array !';
398                        }
399                        $valeurs = array_unique(array_filter($valeurs));
400                        if (!$valeurs) {
401                                continue;
402                        }
403
404                        $filter += array(
405                                'select_oui'  => '',
406                                'select_null' => '',
407                        );
408
409                        // préparer les données
410                        $valeur = $this->quote($valeur);
411                        $valeurs = array_map(array($this, 'quote'), $valeurs);
412                        $valeurs = implode(', ', $valeurs);
413
414                        if (($valeur == '-') and $filter['select_null']) {
415                                $f = $filter['select_null'];
416                        } elseif ($filter['select_oui']) {
417                                $f = $filter['select_oui'];
418                        }
419
420                        // remplacer d'abord le pluriel !
421                        $f = str_replace(array('@valeurs', '@valeur'), array($valeurs, $valeur), $f);
422                        $this->queryApi->select("($f) AS f$nb");
423                        $this->queryApi->where("f$nb = 1");
424                        $nb++;
425                }
426        }
427
428        /**
429         * Revenir au depart
430         * @return void
431         */
432        public function rewind() {
433                reset($this->result);
434                list($this->cle, $this->valeur) = each($this->result);
435        }
436
437        /**
438         * L'iterateur est-il encore valide ?
439         * @return bool
440         */
441        public function valid(){
442                return !is_null($this->cle);
443        }
444
445        /**
446         * Retourner la valeur
447         * @return null
448         */
449        public function current() {
450                return $this->valeur;
451        }
452
453        /**
454         * Retourner la cle
455         * @return null
456         */
457        public function key() {
458                return $this->cle;
459        }
460
461        /**
462         * Passer a la valeur suivante
463         * @return void
464         */
465        public function next(){
466                if ($this->valid())
467                        list($this->cle, $this->valeur) = each($this->result);
468        }
469
470        /**
471         * Compter le nombre total de resultats
472         * @return int
473         */
474        public function count() {
475                if (is_null($this->total))
476                        $this->total = count($this->result);
477          return $this->total;
478        }
479
480}
481
482
483/**
484 * Transmettre la source (l'index sphinx) désirée
485 * @param string $idb
486 * @param object $boucles
487 * @param object $crit
488 */
489function critere_SPHINX_index_dist($idb, &$boucles, $crit) {
490        $boucle = &$boucles[$idb];
491        // critere unique
492        $boucle->hash .= "\n\t" . '$command[\'index\'] = array();';
493
494        foreach ($crit->param as $param){
495                $boucle->hash .= "\n\t" . '$command[\'index\'][] = '.calculer_liste($param, array(), $boucles, $boucles[$idb]->id_parent).';';
496        }
497}
498
499/**
500 * Transmettre la recherche (le match fulltext) désirée
501 * @param string $idb
502 * @param object $boucles
503 * @param object $crit
504 */
505function critere_SPHINX_recherche_dist($idb, &$boucles, $crit) {
506        $boucle = &$boucles[$idb];
507        // critere unique
508        $boucle->hash .= "\n\t" . '$command[\'recherche\'] = array();';
509
510        foreach ($crit->param as $param){
511                $boucle->hash .= "\n\t" . '$command[\'recherche\'][] = '.calculer_liste($param, array(), $boucles, $boucles[$idb]->id_parent).';';
512        }
513}
514
515
516/**
517 * Indiquer les sélections de la requête
518 *
519 * @param string $idb
520 * @param object $boucles
521 * @param object $crit
522 */
523function critere_SPHINX_select_dist($idb, &$boucles, $crit) {
524        $boucle = &$boucles[$idb];
525        // critere multiple
526        $boucle->hash .= "\n\tif (!isset(\$select_init)) { \$command['selection'] = array(); \$select_init = true; }\n";
527
528        foreach ($crit->param as $param){
529                $boucle->hash .= "\t\$command['selection'][] = "
530                                . calculer_liste($param, array(), $boucles, $boucles[$idb]->id_parent) . ";\n";
531        }
532}
533
534
535/**
536 * Indiquer les snippets de la requête
537 *
538 * @param string $idb
539 * @param object $boucles
540 * @param object $crit
541 */
542function critere_SPHINX_snippet_dist($idb, &$boucles, $crit) {
543        $boucle = &$boucles[$idb];
544        // critere multiple
545        $boucle->hash .= "\n\tif (!isset(\$snippet_init)) { \$command['snippet'] = array(); \$snippet_init = true; }\n";
546
547        $boucle->hash .= "\t\$command['snippet'][] = [\n"
548                . (isset($crit->param[0]) ? "\t\t'champ'  => ". calculer_liste($crit->param[0], array(), $boucles, $boucles[$idb]->id_parent) . ",\n" : '')
549                . (isset($crit->param[1]) ? "\t\t'phrase' => ". calculer_liste($crit->param[1], array(), $boucles, $boucles[$idb]->id_parent) . ",\n" : '')
550                . (isset($crit->param[2]) ? "\t\t'limit'  => ". calculer_liste($crit->param[2], array(), $boucles, $boucles[$idb]->id_parent) . ",\n" : '')
551                . (isset($crit->param[3]) ? "\t\t'as'     => ". calculer_liste($crit->param[3], array(), $boucles, $boucles[$idb]->id_parent) . "\n"  : '')
552                . "\t];\n";
553}
554
555
556
557/**
558 * Indiquer les facets de la requête
559 *
560 * @param string $idb
561 * @param object $boucles
562 * @param object $crit
563 */
564function critere_SPHINX_facet_dist($idb, &$boucles, $crit) {
565        $boucle = &$boucles[$idb];
566        // critere multiple
567        $boucle->hash .= "\n\tif (!isset(\$facet_init)) { \$command['facet'] = array(); \$facet_init = true; }\n";
568
569        $boucle->hash .= "\t\$command['facet'][] = array(\n"
570                . (isset($crit->param[0]) ? "\t\t'alias'  => ". calculer_liste($crit->param[0], array(), $boucles, $boucles[$idb]->id_parent) . ",\n" : '')
571                . (isset($crit->param[1]) ? "\t\t'query' => ". calculer_liste($crit->param[1], array(), $boucles, $boucles[$idb]->id_parent) . ",\n" : '')
572                . "\t);\n";
573}
574
575/**
576 * Indiquer les filtres de la requête
577 *
578 * @param string $idb
579 * @param object $boucles
580 * @param object $crit
581 */
582function critere_SPHINX_select_filter_dist($idb, &$boucles, $crit) {
583        $boucle = &$boucles[$idb];
584        // critere multiple
585        $boucle->hash .= "\n\tif (!isset(\$sfilter_init)) { \$command['select_filter'] = array(); \$sfilter_init = true; }\n";
586
587        $boucle->hash .= "\t\$command['select_filter'][] = [\n"
588                . (isset($crit->param[0]) ? "\t\t'valeur'      => ". calculer_liste($crit->param[0], array(), $boucles, $boucles[$idb]->id_parent) . ",\n" : '')
589                . (isset($crit->param[1]) ? "\t\t'select_oui'  => ". calculer_liste($crit->param[1], array(), $boucles, $boucles[$idb]->id_parent) . ",\n" : '')
590                . (isset($crit->param[2]) ? "\t\t'select_null' => ". calculer_liste($crit->param[2], array(), $boucles, $boucles[$idb]->id_parent) . ",\n" : '')
591                . "\t];\n";
592}
593
594
595
596/**
597 * Tris `{par x}`
598 *
599 * @param string $idb
600 * @param object $boucles
601 * @param object $crit
602 */
603function critere_SPHINX_par_dist($idb, &$boucles, $crit) {
604        return critere_SPHINX_parinverse($idb, $boucles, $crit);
605}
606
607/**
608 * Tris `{inverse}`
609 *
610 * @param string $idb
611 * @param object $boucles
612 * @param object $crit
613 */
614function critere_SPHINX_inverse_dist($idb, &$boucles, $crit) {
615        $boucle = &$boucles[$idb];
616        if ($crit->not) {
617                critere_SPHINX_parinverse($idb, $boucles, $crit);
618        } else {
619                // sinon idem parent.
620                critere_inverse_dist($idb, $boucles, $crit);
621        }
622}
623
624/**
625 * Gestion des critères `{par}` et `{inverse}`
626 *
627 * @note
628 *     Sphinx doit toujours avoir le sens de tri (ASC ou DESC).
629 *
630 *     Version simplifié du critère natif de SPIP, avec une permission
631 *     pour les champs de type json `properties.truc`
632 *
633 * @param string $idb
634 * @param object $boucles
635 * @param object $crit
636**/
637function critere_SPHINX_parinverse($idb, $boucles, $crit, $sens = '') {
638        $boucle = &$boucles[$idb];
639        if ($crit->not) {
640                $sens = $sens ? "" : " . ' DESC'";
641        }
642
643        foreach ($crit->param as $tri){
644                $order = "";
645
646                // tris specifies dynamiquement
647                if ($tri[0]->type!='texte'){
648                        // calculer le order dynamique qui verifie les champs
649                        $order = calculer_critere_arg_dynamique($idb, $boucles, $tri, $sens);
650                } else {
651                        $par = array_shift($tri);
652                        $par = $par->texte;
653                        $order = "'$par'";
654                }
655
656
657                $t = $order.$sens;
658                $boucle->order[] = $t;
659        }
660}
661
662function critere_SPHINX_pages_dist($idb, &$boucles, $crit) {
663        $boucle = &$boucles[$idb];
664
665        // critere multiple
666        $boucle->hash .= "\n\tif (!isset(\$pagination_init)) { \$command['pagination'] = array(); \$pagination_init = true; }\n";
667
668        foreach ($crit->param as $param){
669                $boucle->hash .= "\t\$command['pagination'][] = "
670                                . calculer_liste($param, array(), $boucles, $boucles[$idb]->id_parent) . ";\n";
671        }
672}
673
Note: See TracBrowser for help on using the repository browser.