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

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

Déplacement des fonctions utilitaires de snippet au début + renommage en camelCase.

Suppression de la condition pour l'entrée "snippet", car en fait on veut que ça puisse le générer même s'il n'y que "fulltext" et pas de "snippet" explicite (comme avant quoi :D).

Modif de l'ancien squelette de test que j'utilise toujours pour tester l'API tableau, pour mettre directement #DOCS etc afin que ça remarche de nouveau.

File size: 10.8 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        /**
116         * Transforme un tableau d'API en requête Sphinx structurée
117         *
118         * @param array $api
119         * @return bool True si tout s'est bien passé, false sinon.
120        **/
121        public function api2query($api) {
122                if (!is_array($api)) {
123                        return false;
124                }
125
126                $ok = true;
127                foreach (array('index', 'select', 'fulltext', 'snippet', 'filters', /*'orders', 'facet'*/) as $cle) {
128                        if (isset($api[$cle])) {
129                                $methodApi = 'setApi' . ucfirst($cle);
130                                $ok &= $this->$methodApi($api);
131                        }
132                }
133                return $ok;
134        }
135       
136        /**
137         * Ajoute des mots pour la sélection de snippet
138         *
139         * @param string $words Mots à ajouter
140         * @return bool True si au moins un mot présent, false sinon.
141        **/
142        public function addSnippetWords($words) {
143                $words = trim($words);
144                if (!strlen($words)) {
145                        return false;
146                }
147                $this->snippet_words[] = $words;
148                return true;
149        }
150       
151        /**
152         * Extrait et retourne les mots pertinents d'une phrase pour un snippet
153         *
154         * @return string Mots séparés par espace.
155        **/
156        public function getSnippetWords() {
157                $phrase = implode(' ', $this->snippet_words);
158
159                // extraction des mots (évitons les opérateurs, guillements…)
160                preg_match_all('/\w+/u', $phrase, $mots);
161                #var_dump($phrase, $mots);
162                $mots = array_filter($mots[0], function($m) {
163                        // nombres >= 4 chiffres
164                        if (is_numeric($m)) {
165                                return (strlen($m) >= 4);
166                        }
167                        // mots >= 3 lettres
168                        return (strlen($m) >= 3);
169                });
170                return implode(' ', $mots);
171        }
172       
173        /**
174         * Génére le bon select pour produire un snippet suivant un champ et des mots
175         *
176         * @param string $field Nom du champ (ou de la combinaison de champs) pour chercher les mots
177         * @param string $words='' Chaîne contenant les mots à mettre en gras
178         * @param int $limit=200 Limite facultative (200 par défaut)
179         * @return void
180         */
181        public function generateSnippet($field, $words='', $limit=200){
182                if ($words){
183                        $limit = intval($limit);
184                        $this->select('snippet(' . $field . ', ' . $this->quote($words) . ", 'limit=$limit') as snippet");
185                }
186        }
187       
188        /**
189         * Définit l'index de la requête.
190         *
191         * Utilise la clé 'index' du tableau d'API
192         *
193         *     ```
194         *     'index' => 'visites'
195         *     'index' => ['visites', 'autre']
196         *     ```
197         *
198         * @param array $api Tableau de description
199         * @return bool True si index présent.
200        **/
201        public function setApiIndex($api) {
202                if (!isset($api['index'])) {
203                        return false;
204                }
205                // Always work with an array of values
206                if (!is_array($api['index'])) {
207                        $api['index'] = array($api['index']);
208                }
209                foreach ($api['index'] as $index){
210                        $this->from($index);
211                }
212                return true;
213        }
214
215        /**
216         * Définit le select de la requête.
217         *
218         * Utilise la clé 'select' du tableau d'API
219         *
220         *     ```
221         *     'select' => array('date', 'properties', '*', 'etc'),
222         *     ```
223         *
224         * @param array $api Tableau de description
225         * @return bool True si select présent.
226        **/
227        public function setApiSelect($api) {
228                if (!isset($api['select'])) {
229                        return false;
230                }
231                // Always work with an array of values
232                if (!is_array($api['select'])){
233                        $api['select'] = array($api['select']);
234                }
235                foreach ($api['select'] as $select){
236                        $this->select($select);
237                }
238                return true;
239        }
240
241
242        /**
243         * Définit le fulltext (match) de la requête.
244         *
245         * Utilise la clé 'fulltext' du tableau d'API
246         *
247         *     ```
248         *     'fulltext' => 'ma recherche',
249         *     ```
250         *
251         * @param array $api Tableau de description
252         * @return bool True si fulltext présent.
253        **/
254        public function setApiFulltext($api) {
255                // Fulltext search string (optional)
256                if (!isset($api['fulltext']) OR !is_string($api['fulltext'])) {
257                        return false;
258                }
259
260                $this->where('MATCH(' . $this->quote($api['fulltext']) . ')');
261                // add the score
262                $this->select('WEIGHT() as score');
263                // add to snippet
264                $this->addSnippetWords($api['fulltext']);
265        }
266
267        /**
268         * Définit un snippet pour la requête.
269         *
270         * Utilise la clé 'snippet' du tableau d'API
271         *
272         * Un snippet est créé dès qu'un mot est connu,
273         * notamment avec la valeur de la clé 'fulltext'.
274         *
275         * Si la clé snippet n'est pas précisée, les valeurs par défaut
276         * sont appliquées.
277         *
278         *     ```
279         *    'snippet' => array(
280         *        'words' => 'un mot',  // optionnel
281         *        'field' => 'content', // optionnel
282         *        'limit' => 200,       // optionnel
283         *     ),
284         *     ```
285         *
286         * @param array $api Tableau de description
287         * @return bool True si snippet ajouté, false sinon.
288        **/
289        public function setApiSnippet($api) {
290                if (isset($api['snippet']['words']) and is_string($api['snippet']['words'])){
291                        $this->addSnippetWords($api['snippet']['words']);
292                }
293
294                // If there is fulltext and/or an other words declaration, generate a snippet
295                if (!$words = $this->getSnippetWords()) {
296                        return false;
297                }
298
299                $field = isset($api['snippet']['field']) ? $api['snippet']['field'] : 'content';
300                $limit = isset($api['snippet']['limit']) ? $api['snippet']['limit'] : 200;
301
302                $this->generateSnippet($field, $words, $limit);
303                return true;
304        }
305
306        /**
307         * Définit les filtres pour la requête.
308         *
309         *     ```
310         *     'filters' => array(
311         *         array(
312         *             'type' => 'mono',
313         *             'field' => 'properties.lang',
314         *             'values' => array('fr'),
315         *             'comparison' => '!=', // default : =
316         *         ),
317         *         array(
318         *             'type' => 'multi_json',
319         *             'field' => 'properties.tags',
320         *             'values' => array('pouet', 'glop'),
321         *         ),
322         *         array(
323         *             'type' => 'distance',
324         *             'center' => array(
325         *                 'lat' => 44.837862,
326         *                 'lon' => -0.580086,
327         *             ),
328         *             'fields' => array(
329         *                 'lat' => 'properties.geo.lat',
330         *                 'lon' => 'properties.geo.lon',
331         *             ),
332         *             'distance' => 10000,
333         *             'comparison' => '>', // default : <=
334         *         ),
335         *         array(
336         *             'type' => 'interval',
337         *             'expression' => 'uint(properties.truc)',
338         *             'intervals' => array(1,2,3,4,5),
339         *             'field' => 'truc',
340         *             'test' => 'truc = 2',
341         *             'select' => 'interval(uint(properties.truc),1,2,3,4)',
342         *             'where' => 'test = 2',
343         *         ),
344         *     ),
345         *     ```
346         *
347         * @param array $api Tableau de description
348         * @return bool True si filtres ajouté, false sinon.
349        **/
350        public function setApiFilters($api) {
351                if (!isset($api['filters']) or !is_array($api['filters'])) {
352                        return false;
353                }
354
355                foreach ($api['filters'] as $filter) {
356                        if (!is_array($filter) or !isset($filter['type'])) {
357                                continue;
358                        }
359                        switch ($filter['type']) {
360                                case 'mono':
361                                        $this->setFilterMono($api, $filter);
362                                        break;
363                                case 'multi_json':
364                                        $this->setFilterMultiJson($api, $filter);
365                                        break;
366                        }
367                }
368                return true;
369        }
370
371        public function setFilterMono($api, $filter) {
372                if (
373                        ($filter['type'] != 'mono')
374                        or !isset($filter['field'])
375                        or !is_string($filter['field']) // mandatory
376                        or !isset($filter['values']) // mandatory
377                ){
378                        return false;
379                }
380
381                // Default comparison : =
382                if (!isset($filter['comparison'])){
383                        $filter['comparison'] = '=';
384                }
385
386                // Always work with an array of values
387                if (!is_array($filter['values'])){
388                        $filter['values'] = array($filter['values']);
389                }
390
391                // For each values, we build a comparison
392                $comparisons = array();
393                foreach ($filter['values'] as $value){
394                        $comparison = $filter['field'] . $filter['comparison'] . $this->quote($value);
395                        if (isset($filter['not']) and $filter['not']){
396                                $comparison = "!($comparison)";
397                        }
398                        $comparisons[] = $comparison;
399                }
400                if ($comparisons){
401                        $comparisons = implode(' OR ', $comparisons);
402                        $this->where($comparisons);
403                }
404
405                return true;
406        }
407
408        public function setFilterMultiJson($api, $filter) {
409                static $as_count = 0;
410
411                // Multi value JSON
412                if (
413                        ($filter['type'] != 'multi_json')
414                        or !isset($filter['field'])
415                        or !is_string($filter['field']) // mandatory
416                        or !isset($filter['values']) // mandatory
417                ){
418                        return false;
419                }
420
421                // Always work with an array of values
422                if (!is_array($filter['values'])){
423                        $filter['values'] = array(array($filter['values']));
424                }
425
426                // At depth 1, generate AND
427                $ins = array();
428                foreach ($filter['values'] as $values_in){
429                        // Always work with an array of values
430                        if (!is_array($values_in)){
431                                $values_in = array($values_in);
432                        }
433                        $ins[] = 'IN(' . $filter['field'] . ', ' . join(', ', array_map(array($this, 'quote'), array_filter($values_in))) . ')';
434                }
435
436                if ($ins){
437                        $this->select('(' . join(' AND ', $ins) . ') as select_'.$as_count);
438                        $this->where('select_'.$as_count . '=' . ((isset($filter['not']) and $filter['not']) ? '0' : '1'));
439                        $as_count++;
440                }
441        }
442}
Note: See TracBrowser for help on using the repository browser.