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

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

Dans le découpage des méthodes de l'API tableau, ne plus envoyer tout le tableau de description, mais seulement le morceau de description réellement utilisé.

Ce qui permet ensuite d'appeler plus logiquement les fonctions toutes seules avec uniquement la description dont elles ont besoin.

+ quelques commentaires

File size: 11.2 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($api['fulltext']) . ')');
260                // Add the score
261                $this->select('WEIGHT() as score');
262                // Add to snippet
263                $this->addSnippetWords($api['fulltext']);
264        }
265
266        /**
267         * Définit un snippet pour la requête.
268         *
269         * Utilise la clé 'snippet' du tableau d'API
270         *
271         * Un snippet est créé dès qu'un mot est connu,
272         * notamment avec la valeur de la clé 'fulltext'.
273         *
274         * Si la clé snippet n'est pas précisée, les valeurs par défaut
275         * sont appliquées.
276         *
277         *     ```
278         *    'snippet' => array(
279         *        'words' => 'un mot',  // optionnel
280         *        'field' => 'content', // optionnel
281         *        'limit' => 200,       // optionnel
282         *     ),
283         *     ```
284         *
285         * @param array $api Tableau de description
286         * @return bool True si snippet ajouté, false sinon.
287        **/
288        public function setApiSnippet($snippet) {
289                if (isset($snippet['words']) and is_string($snippet['words'])){
290                        $this->addSnippetWords($snippet['words']);
291                }
292
293                // If there is fulltext and/or an other words declaration, generate a snippet
294                if (!$words = $this->getSnippetWords()) {
295                        return false;
296                }
297               
298                // Default values
299                $field = isset($snippet['field']) ? $snippet['field'] : 'content';
300                $limit = isset($snippet['limit']) ? $snippet['limit'] : 200;
301               
302                // Add the snippet in select
303                $this->generateSnippet($field, $words, $limit);
304               
305                return true;
306        }
307
308        /**
309         * Définit les filtres pour la requête.
310         *
311         *     ```
312         *     'filters' => array(
313         *         array(
314         *             'type' => 'mono',
315         *             'field' => 'properties.lang',
316         *             'values' => array('fr'),
317         *             'comparison' => '!=', // default : =
318         *         ),
319         *         array(
320         *             'type' => 'multi_json',
321         *             'field' => 'properties.tags',
322         *             'values' => array('pouet', 'glop'),
323         *         ),
324         *         array(
325         *             'type' => 'distance',
326         *             'center' => array(
327         *                 'lat' => 44.837862,
328         *                 'lon' => -0.580086,
329         *             ),
330         *             'fields' => array(
331         *                 'lat' => 'properties.geo.lat',
332         *                 'lon' => 'properties.geo.lon',
333         *             ),
334         *             'distance' => 10000,
335         *             'comparison' => '>', // default : <=
336         *         ),
337         *         array(
338         *             'type' => 'interval',
339         *             'expression' => 'uint(properties.truc)',
340         *             'intervals' => array(1,2,3,4,5),
341         *             'field' => 'truc',
342         *             'test' => 'truc = 2',
343         *             'select' => 'interval(uint(properties.truc),1,2,3,4)',
344         *             'where' => 'test = 2',
345         *         ),
346         *     ),
347         *     ```
348         *
349         * @param array $api Tableau de description
350         * @return bool True si filtres ajouté, false sinon.
351        **/
352        public function setApiFilters($filters) {
353                if (!is_array($filters)) { return false; }
354               
355                $ok = true;
356
357                // For each type of filter, call the right method
358                foreach ($filters as $filter) {
359                        if (is_array($filter) and isset($filter['type'])) {
360                                switch ($filter['type']) {
361                                        case 'mono':
362                                                $ok &= $this->setFilterMono($filter);
363                                                break;
364                                        case 'multi_json':
365                                                $ok &= $this->setFilterMultiJson($filter);
366                                                break;
367                                }
368                        }
369                }
370               
371                return $ok;
372        }
373       
374        /**
375         * Add a mono value filter
376         *
377         * @param array $filter Description of the filter
378         * @return bool Return true if the filter has been added
379         */
380        public function setFilterMono($filter) {
381                if (
382                        !isset($filter['field'])
383                        or !is_string($filter['field']) // mandatory
384                        or !isset($filter['values']) // mandatory
385                ){
386                        return false;
387                }
388
389                // Default comparison : =
390                if (!isset($filter['comparison'])){
391                        $filter['comparison'] = '=';
392                }
393
394                // Always work with an array of values
395                if (!is_array($filter['values'])){
396                        $filter['values'] = array($filter['values']);
397                }
398
399                // For each values, we build a comparison
400                $comparisons = array();
401                foreach ($filter['values'] as $value){
402                        $comparison = $filter['field'] . $filter['comparison'] . $this->quote($value);
403                        if (isset($filter['not']) and $filter['not']){
404                                $comparison = "!($comparison)";
405                        }
406                        $comparisons[] = $comparison;
407                }
408                if ($comparisons){
409                        $comparisons = implode(' OR ', $comparisons);
410                        $this->where($comparisons);
411                }
412
413                return true;
414        }
415       
416        /**
417         * Add a multi values filter in JSON
418         *
419         * @param array $filter
420         *              Description of the filter
421         *              - field : the field to compare to
422         *              - values : an array of values
423         *                      * in this array, all comparisons will be join with AND
424         *                      * if a value is itself an array, it uses an IN (so like an OR)
425         * @return bool
426         *              Return true if the filter has been added
427         */
428        public function setFilterMultiJson($filter) {
429                static $as_count = 0;
430
431                // Multi value JSON
432                if (
433                        !isset($filter['field'])
434                        or !is_string($filter['field']) // mandatory
435                        or !isset($filter['values']) // mandatory
436                ){
437                        return false;
438                }
439
440                // Always work with an array of values
441                if (!is_array($filter['values'])){
442                        $filter['values'] = array(array($filter['values']));
443                }
444
445                // At depth 1, generate AND
446                $ins = array();
447                foreach ($filter['values'] as $values_in){
448                        // Always work with an array of values
449                        if (!is_array($values_in)){
450                                $values_in = array($values_in);
451                        }
452                        $ins[] = 'IN(' . $filter['field'] . ', ' . join(', ', array_map(array($this, 'quote'), array_filter($values_in))) . ')';
453                }
454
455                if ($ins){
456                        $this->select('(' . join(' AND ', $ins) . ') as multi_'.$as_count);
457                        $this->where('multi_'.$as_count . '=' . ((isset($filter['not']) and $filter['not']) ? '0' : '1'));
458                        $as_count++;
459                }
460               
461                return true;
462        }
463}
Note: See TracBrowser for help on using the repository browser.