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

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

Il me semble qu'on peut stocker directement la limite de pagination dans l'iterateur. À vérifier.

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