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

Last change on this file since 82611 was 82611, checked in by fil@…, 5 years ago

pour la pagination dans la boucle #DOCS fille, annoncer {pages #DEBUT_DOCUMENTS,20} dans la boucle (SPHINX) principale

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