source: spip-zone/_plugins_/indexer/trunk/lib/Sphinx/SphinxQL/QueryApi.php @ 82870

Last change on this file since 82870 was 82870, checked in by rastapopoulos@…, 5 years ago
  • Modification de la fonction quote() pour utiliser un deuxième argument $type qui permet de forcer l'interprétation (float, int, string).
  • Prise en compte de cette possibilité dans l'API tableau en pouvant donner "type"=>truc dans la description.
  • Ajout d'un argument supplémentaire aux critères {filtermono} et {filtermultijson} pour pouvoir forcer le type des valeurs à comparer.

Toujours un peu chiant de devoir ajouter des arguments, mais c'est le propre des signatures de fonctions avec une liste précise dans un ordre précis…

File size: 12.9 KB
Line 
1<?php
2
3namespace Sphinx\SphinxQL;
4
5
6
7/**
8 * Crée une requête Sphinx à partir d'un tableau de description spécifique
9 *
10        ```
11        // exemple de description
12        array(
13                'index' => 'visites',
14                'select' => array('date', 'properties', '*', 'etc'),
15                'fulltext' => 'ma recherche',
16                'snippet' => array(
17                        'words' => 'un mot',
18                        'field' => 'content',
19                        'limit' => 200,
20                ),
21                'filters' => array(
22                        array(
23                                'type' => 'mono',
24                                'field' => 'properties.lang',
25                                'values' => array('fr'),
26                                'comparison' => '!=', // default : =
27                        ),
28                        array(
29                                'type' => 'multi_json',
30                                'field' => 'properties.tags',
31                                'values' => array('pouet', 'glop'),
32                        ),
33                        array(
34                                'type' => 'distance',
35                                'center' => array(
36                                        'lat' => 44.837862,
37                                        'lon' => -0.580086,
38                                ),
39                                'fields' => array(
40                                        'lat' => 'properties.geo.lat',
41                                        'lon' => 'properties.geo.lon',
42                                ),
43                                'distance' => 10000,
44                                'comparison' => '>', // default : <=
45                        ),
46                        array(
47                                'type' => 'interval',
48                                'expression' => 'uint(properties.truc)',
49                                'intervals' => array(1,2,3,4,5),
50                                'field' => 'truc',
51                                'test' => 'truc = 2',
52                                'select' => 'interval(uint(properties.truc),1,2,3,4)',
53                                'where' => 'test = 2',
54                        ),
55                ),
56                'orders' => array(
57                        array(
58                                'field' => 'score',
59                                'direction' => 'asc', // default : desc
60                        ),
61                        array(
62                                'field' => 'distance',
63                                'center' => array(
64                                        'lat' => 44.837862,
65                                        'lon' => -0.580086,
66                                ),
67                                'fields' => array(
68                                        'lat' => 'properties.geo.lat',
69                                        'lon' => 'properties.geo.lon',
70                                ),
71                        ),
72                ),
73                'facet' => array(
74                        'field' => 'properties.tags',
75                        'group_name' => 'tag',
76                        'order' => 'tag asc', // default : count desc
77                ),
78        );
79        ```
80
81**/
82class QueryApi extends Query {
83
84        /** @var string[] liste des mots pour le snippet */
85        private $snippet_words = array();
86
87        /**
88         * Crée une description de requête Sphinx à partir d'un tableau d'API
89         *
90         * Se reporter aux méthodes spécifiques pour les précisions sur l'API.
91         * Les clés du tableau peuvent être :
92         *
93         * - index
94         * - select
95         * - fulltext
96         * - snippet
97         * - filters
98         *
99         * À faire :
100         *
101         * - orders
102         * - facet
103         *
104         * @param array $api
105         *     API suivant une certaine norme
106         * @return void
107        **/
108        public function __construct($api=array()) {
109                if (!empty($api)){
110                        $this->api2query($api);
111                }
112        }
113
114        /**
115         * Transforme un tableau d'API en requête Sphinx structurée
116         *
117         * @param array $api
118         * @return bool True si tout s'est bien passé, false sinon.
119        **/
120        public function api2query($api) {
121                $ok = true;
122               
123                if (!is_array($api)) {
124                        $ok = false;
125                }
126               
127                // Si une clé reconnue existe dans la description demandée, on applique la méthode adaptée
128                foreach (array('index', 'select', 'fulltext', 'snippet', 'filters', /*'orders', 'facet'*/) as $cle) {
129                        if (isset($api[$cle])) {
130                                $methodApi = 'setApi' . ucfirst($cle);
131                                $ok &= $this->$methodApi($api[$cle]);
132                        }
133                }
134               
135                return $ok;
136        }
137       
138        /**
139         * Ajoute des mots pour la sélection de snippet
140         *
141         * @param string $words Mots à ajouter
142         * @return bool True si au moins un mot présent, false sinon.
143        **/
144        public function addSnippetWords($words) {
145                $words = trim($words);
146                if (!strlen($words)) {
147                        return false;
148                }
149                $this->snippet_words[] = $words;
150                return true;
151        }
152       
153        /**
154         * Extrait et retourne les mots pertinents d'une phrase pour un snippet
155         *
156         * @return string Mots séparés par espace.
157        **/
158        public function getSnippetWords() {
159                $phrase = implode(' ', $this->snippet_words);
160
161                // extraction des mots (évitons les opérateurs, guillements…)
162                preg_match_all('/\w+/u', $phrase, $mots);
163                #var_dump($phrase, $mots);
164                $mots = array_filter($mots[0], function($m) {
165                        // nombres >= 4 chiffres
166                        if (is_numeric($m)) {
167                                return (strlen($m) >= 4);
168                        }
169                        // mots >= 3 lettres
170                        return (strlen($m) >= 3);
171                });
172                return implode(' ', $mots);
173        }
174       
175        /**
176         * Génére le bon select pour produire un snippet suivant un champ et des mots
177         *
178         * @param string $field Nom du champ (ou de la combinaison de champs) pour chercher les mots
179         * @param string $words='' Chaîne contenant les mots à mettre en gras
180         * @param int $limit=200 Limite facultative (200 par défaut)
181         * @return void
182         */
183        public function generateSnippet($field, $words='', $limit=200){
184                if ($words){
185                        $limit = intval($limit);
186                        $this->select('snippet(' . $field . ', ' . $this->quote($words) . ", 'limit=$limit') as snippet");
187                }
188        }
189       
190        /**
191         * Définit l'index de la requête.
192         *
193         * Utilise la clé 'index' du tableau d'API
194         *
195         *     ```
196         *     'index' => 'visites'
197         *     'index' => ['visites', 'autre']
198         *     ```
199         *
200         * @param array $api Tableau de description
201         * @return bool True si index présent.
202        **/
203        public function setApiIndex($index) {
204                if (!$index){ return false; }
205               
206                // Always work with an array of values
207                if (!is_array($index)) {
208                        $index = array($index);
209                }
210                foreach ($index as $i){
211                        $this->from($i);
212                }
213               
214                return true;
215        }
216
217        /**
218         * Définit le select de la requête.
219         *
220         * Utilise la clé 'select' du tableau d'API
221         *
222         *     ```
223         *     'select' => array('date', 'properties', '*', 'etc'),
224         *     ```
225         *
226         * @param array $api Tableau de description
227         * @return bool True si select présent.
228        **/
229        public function setApiSelect($select) {
230                if (!$select){ return false; }
231               
232                // Always work with an array of values
233                if (!is_array($select)){
234                        $select = array($select);
235                }
236                foreach ($select as $s){
237                        $this->select($s);
238                }
239               
240                return true;
241        }
242
243        /**
244         * Définit le fulltext (match) de la requête.
245         *
246         * Utilise la clé 'fulltext' du tableau d'API
247         *
248         *     ```
249         *     'fulltext' => 'ma recherche',
250         *     ```
251         *
252         * @param array $api Tableau de description
253         * @return bool True si fulltext présent.
254        **/
255        public function setApiFulltext($fulltext) {
256                if (!is_string($fulltext)) { return false; }
257               
258                // Add the condition in where
259                $this->where('MATCH(' . $this->quote($fulltext) . ')');
260                // Add the score
261                $this->select('WEIGHT() as score');
262                // Add to snippet
263                $this->addSnippetWords($fulltext);
264               
265                return true;
266        }
267
268        /**
269         * Définit un snippet pour la requête.
270         *
271         * Utilise la clé 'snippet' du tableau d'API
272         *
273         * Un snippet est créé dès qu'un mot est connu,
274         * notamment avec la valeur de la clé 'fulltext'.
275         *
276         * Si la clé snippet n'est pas précisée, les valeurs par défaut
277         * sont appliquées.
278         *
279         *     ```
280         *    'snippet' => array(
281         *        'words' => 'un mot',  // optionnel
282         *        'field' => 'content', // optionnel
283         *        'limit' => 200,       // optionnel
284         *     ),
285         *     ```
286         *
287         * @param array $api Tableau de description
288         * @return bool True si snippet ajouté, false sinon.
289        **/
290        public function setApiSnippet($snippet) {
291                if (isset($snippet['words']) and is_string($snippet['words'])){
292                        $this->addSnippetWords($snippet['words']);
293                }
294
295                // If there is fulltext and/or an other words declaration, generate a snippet
296                if (!$words = $this->getSnippetWords()) {
297                        return false;
298                }
299               
300                // Default values
301                $field = isset($snippet['field']) ? $snippet['field'] : 'content';
302                $limit = isset($snippet['limit']) ? $snippet['limit'] : 200;
303               
304                // Add the snippet in select
305                $this->generateSnippet($field, $words, $limit);
306               
307                return true;
308        }
309
310        /**
311         * Définit les filtres pour la requête.
312         *
313         *     ```
314         *     'filters' => array(
315         *         array(
316         *             'type' => 'mono',
317         *             'field' => 'properties.lang',
318         *             'values' => array('fr'),
319         *             'comparison' => '!=', // default : =
320         *         ),
321         *         array(
322         *             'type' => 'multi_json',
323         *             'field' => 'properties.tags',
324         *             'values' => array('pouet', 'glop'),
325         *         ),
326         *         array(
327         *             'type' => 'distance',
328         *             'point1' => array(
329         *                 'lat' => 44.837862,
330         *                 'lon' => -0.580086,
331         *             ),
332         *             'point2' => array(
333         *                 'lat' => 'properties.geo.lat',
334         *                 'lon' => 'properties.geo.lon',
335         *             ),
336         *             'distance' => 10000,
337         *             'comparison' => '>', // default : <=
338         *         ),
339         *         array(
340         *             'type' => 'interval',
341         *             'expression' => 'uint(properties.truc)',
342         *             'intervals' => array(1,2,3,4,5),
343         *             'field' => 'truc',
344         *             'test' => 'truc = 2',
345         *             'select' => 'interval(uint(properties.truc),1,2,3,4)',
346         *             'where' => 'test = 2',
347         *         ),
348         *     ),
349         *     ```
350         *
351         * @param array $api Tableau de description
352         * @return bool True si filtres ajouté, false sinon.
353        **/
354        public function setApiFilters($filters) {
355                if (!is_array($filters)) { return false; }
356               
357                $ok = true;
358
359                // For each type of filter, call the right method
360                foreach ($filters as $filter) {
361                        if (is_array($filter) and isset($filter['type'])) {
362                                switch ($filter['type']) {
363                                        case 'mono':
364                                                $ok &= $this->setApiFilterMono($filter);
365                                                break;
366                                        case 'multi_json':
367                                                $ok &= $this->setApiFilterMultiJson($filter);
368                                                break;
369                                        case 'distance':
370                                                $ok &= $this->setApiFilterDistance($filter);
371                                                break;
372                                }
373                        }
374                }
375               
376                return $ok;
377        }
378       
379        /**
380         * Add a mono value filter
381         *
382         * @param array $filter Description of the filter
383         * @return bool Return true if the filter has been added
384         */
385        public function setApiFilterMono($filter) {
386                if (
387                        !isset($filter['field'])
388                        or !is_string($filter['field']) // mandatory
389                        or !isset($filter['values']) // mandatory
390                ){
391                        return false;
392                }
393
394                // Default comparison : =
395                if (!isset($filter['comparison'])){
396                        $filter['comparison'] = '=';
397                }
398
399                // Always work with an array of values
400                if (!is_array($filter['values'])){
401                        $filter['values'] = array($filter['values']);
402                }
403               
404                // No type by default
405                if (!isset($filter['type'])) {
406                        $filter['type'] = '';
407                }
408
409                // For each values, we build a comparison
410                $comparisons = array();
411                foreach ($filter['values'] as $value){
412                        $comparison = $filter['field'] . $filter['comparison'] . $this->quote($value, $filter['type']);
413                        if (isset($filter['not']) and $filter['not']){
414                                $comparison = "!($comparison)";
415                        }
416                        $comparisons[] = $comparison;
417                }
418                if ($comparisons){
419                        $comparisons = implode(' OR ', $comparisons);
420                        $this->where($comparisons);
421                }
422
423                return true;
424        }
425       
426        /**
427         * Add a multi values filter in JSON
428         *
429         * @param array $filter
430         *              Description of the filter
431         *              - field : the field to compare to
432         *              - values : an array of values
433         *                      * in this array, all comparisons will be join with AND
434         *                      * if a value is itself an array, it uses an IN (so like an OR)
435         * @return bool
436         *              Return true if the filter has been added
437         */
438        public function setApiFilterMultiJson($filter) {
439                static $as_count = 0;
440
441                // Field and values must be there
442                if (
443                        !isset($filter['field'])
444                        or !is_string($filter['field']) // mandatory
445                        or !isset($filter['values']) // mandatory
446                ){
447                        return false;
448                }
449
450                // Always work with an array of values
451                if (!is_array($filter['values'])){
452                        $filter['values'] = array(array($filter['values']));
453                }
454               
455                // No type by default
456                if (!isset($filter['type'])) {
457                        $filter['type'] = '';
458                }
459
460                // At depth 1, generate AND
461                $ins = array();
462                foreach ($filter['values'] as $values_in){
463                        // Always work with an array of values
464                        if (!is_array($values_in)){
465                                $values_in = array($values_in);
466                        }
467                        // Quote all values if necessary
468                        $values_in = array_filter($values_in);
469                        foreach ($values_in as $k=>$v) {
470                                $values_in[$k] = $this->quote($v, $filter['type']);
471                        }
472                        $ins[] = 'IN(' . $filter['field'] . ', ' . join(', ', $values_in) . ')';
473                }
474
475                if ($ins){
476                        $this->select('(' . join(' AND ', $ins) . ') as multi_'.$as_count);
477                        $this->where('multi_'.$as_count . '=' . ((isset($filter['not']) and $filter['not']) ? '0' : '1'));
478                        $as_count++;
479                }
480               
481                return true;
482        }
483       
484        /**
485         * Add a distance filter
486         *
487         * @param array $filter
488         *              Description of the filter
489         *              - point1 : associative array of lat and lon
490         *              - point2 : associative array of lat and lon
491         *              - distance : distance in meters to compare to
492         *              - comparison : operator, default to "<="
493         * @return bool
494         *              Return true if the filter has been added
495         */
496        public function setApiFilterDistance($filter) {
497                static $as_count = 0;
498
499                // Mandatory parameters
500                if (
501                        !isset($filter['point1']['lat'])
502                        or !isset($filter['point1']['lon'])
503                        or !isset($filter['point2']['lat'])
504                        or !isset($filter['point2']['lon'])
505                        or !isset($filter['distance'])
506                ){
507                        return false;
508                }
509               
510                // Force type
511                $distance = intval($filter['distance']);
512               
513                // Default comparison : =
514                if (!isset($filter['comparison'])){
515                        $filter['comparison'] = '<=';
516                }
517               
518                // Default "as"
519                if (!isset($filter['as'])){
520                        $filter['as'] = 'distance_' . $as_count;
521                        $as_count++;
522                }
523               
524                $this->select('geodist(double(' . $filter['point1']['lat'] . '), double(' . $filter['point1']['lon'] . '), double(' . $filter['point2']['lat'] . '), double(' . $filter['point2']['lon'] . '), {in=deg}) as ' . $filter['as']);
525                $this->where($filter['as'] . ' ' . $filter['comparison'] . ' ' . $this->quote($filter['distance']));
526               
527                return true;
528        }
529}
Note: See TracBrowser for help on using the repository browser.