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

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

plein de [] a transformer en array()

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