Issues (137)

src/Percolate.php (1 issue)

Severity
1
<?php
2
3
namespace Foolz\SphinxQL;
4
5
use Foolz\SphinxQL\Drivers\ConnectionInterface;
6
use Foolz\SphinxQL\Exception\SphinxQLException;
7
8
/**
9
 * Query Builder class for Percolate Queries.
10
 *
11
 * ### INSERT ###
12
 *
13
 * $query = (new Percolate($conn))
14
 *    ->insert('full text query terms', noEscape = false)       // Allowed only one insert per query (Symbol @ indicates field in sphinx.conf)
15
 *                                                                 No escape tag cancels characters shielding (default on)
16
 *    ->into('pq')                                              // Index for insert
17
 *    ->tags(['tag1','tag2'])                                   // Adding tags. Can be array ['tag1','tag2'] or string delimited by coma
18
 *    ->filter('price>3')                                       // Adding filter (Allowed only one)
19
 *    ->execute();
20
 *
21
 *
22
 * ### CALL PQ ###
23
 *
24
 *
25
 * $query = (new Percolate($conn))
26
 *    ->callPQ()
27
 *    ->from('pq')                                              // Index for call pq
28
 *    ->documents(['multiple documents', 'go this way'])        // see getDocuments function
29
 *    ->options([                                               // See https://docs.manticoresearch.com/latest/html/searching/percolate_query.html#call-pq
30
 *          Percolate::OPTION_VERBOSE => 1,
31
 *          Percolate::OPTION_DOCS_JSON => 1
32
 *    ])
33
 *    ->execute();
34
 *
35
 *
36
 */
37
class Percolate
38
{
39
40
    /**
41
     * @var ConnectionInterface
42
     */
43
    protected $connection;
44
45
    /**
46
     * Documents for CALL PQ
47
     *
48
     * @var array|string
49
     */
50
    protected $documents;
51
52
    /**
53
     * Index name
54
     *
55
     * @var string
56
     */
57
    protected $index;
58
59
    /**
60
     * Insert query
61
     *
62
     * @var string
63
     */
64
    protected $query;
65
66
    /**
67
     * Options for CALL PQ
68
     * @var array
69
     */
70
    protected $options = [self::OPTION_DOCS_JSON => 1];
71
72
    /**
73
     * @var string
74
     */
75
    protected $filters = '';
76
77
    /**
78
     * Query type (call | insert)
79
     *
80
     * @var string
81
     */
82
    protected $type = 'call';
83
84
    /** INSERT STATEMENT  **/
85
86
    protected $tags = [];
87
88
    /**
89
     * Throw exceptions flag.
90
     * Activates if option OPTION_DOCS_JSON setted
91
     *
92
     * @var int
93
     */
94
    protected $throwExceptions = 0;
95
    /**
96
     * @var array
97
     */
98
    protected $escapeChars = [
99
        '\\' => '\\\\',
100
        '-' => '\\-',
101
        '~' => '\\~',
102
        '<' => '\\<',
103
        '"' => '\\"',
104
        "'" => "\\'",
105
        '/' => '\\/',
106
        '!' => '\\!'
107
    ];
108
109
    /** @var SphinxQL */
110
    protected $sphinxQL;
111
112
    /**
113
     * CALL PQ option constants
114
     */
115
    const OPTION_DOCS_JSON = 'as docs_json';
116
    const OPTION_DOCS = 'as docs';
117
    const OPTION_VERBOSE = 'as verbose';
118
    const OPTION_QUERY = 'as query';
119
120
    /**
121
     * @param ConnectionInterface $connection
122
     */
123
    public function __construct(ConnectionInterface $connection)
124
    {
125
        $this->connection = $connection;
126
        $this->sphinxQL = new SphinxQL($this->connection);
127
128
    }
129
130
131
    /**
132
     * Clear all fields after execute
133
     */
134
    private function clear()
135
    {
136
        $this->documents = null;
137
        $this->index = null;
138
        $this->query = null;
139
        $this->options = [self::OPTION_DOCS_JSON => 1];
140
        $this->type = 'call';
141
        $this->filters = '';
142
        $this->tags = [];
143
    }
144
145
    /**
146
     * Analog into function
147
     * Sets index name for query
148
     *
149
     * @param string $index
150
     *
151
     * @return $this
152
     * @throws SphinxQLException
153
     */
154
    public function from($index)
155
    {
156
        if (empty($index)) {
157
            throw new SphinxQLException('Index can\'t be empty');
158
        }
159
160
        $this->index = trim($index);
161
        return $this;
162
    }
163
164
    /**
165
     * Analog from function
166
     * Sets index name for query
167
     *
168
     * @param string $index
169
     *
170
     * @return $this
171
     * @throws SphinxQLException
172
     */
173
    public function into($index)
174
    {
175
        if (empty($index)) {
176
            throw new SphinxQLException('Index can\'t be empty');
177
        }
178
        $this->index = trim($index);
179
        return $this;
180
    }
181
182
    /**
183
     * Replacing bad chars
184
     *
185
     * @param string $query
186
     *
187
     * @return string mixed
188
     */
189
    protected function escapeString($query)
190
    {
191
        return str_replace(
192
            array_keys($this->escapeChars),
193
            array_values($this->escapeChars),
194
            $query);
195
    }
196
197
198
    /**
199
     * @param $query
200
     * @return mixed
201
     */
202
    protected function clearString($query)
203
    {
204
        return str_replace(
205
            array_keys(array_merge($this->escapeChars, ['@' => ''])),
206
            ['', '', '', '', '', '', '', '', '', ''],
207
            $query);
208
    }
209
210
    /**
211
     * Adding tags for insert query
212
     *
213
     * @param array|string $tags
214
     *
215
     * @return $this
216
     */
217
    public function tags($tags)
218
    {
219
        if (is_array($tags)) {
220
            $tags = array_map([$this, 'escapeString'], $tags);
221
            $tags = implode(',', $tags);
222
        } else {
223
            $tags = $this->escapeString($tags);
224
        }
225
        $this->tags = $tags;
226
        return $this;
227
    }
228
229
    /**
230
     * Add filter for insert query
231
     *
232
     * @param string $filter
233
     * @return $this
234
     *
235
     * @throws SphinxQLException
236
     */
237
    public function filter($filter)
238
    {
239
        $this->filters = $this->clearString($filter);
240
        return $this;
241
    }
242
243
    /**
244
     * Add insert query
245
     *
246
     * @param string $query
247
     * @param bool $noEscape
248
     *
249
     * @return $this
250
     * @throws SphinxQLException
251
     */
252
    public function insert($query, $noEscape = false)
253
    {
254
        $this->clear();
255
256
        if (empty($query)) {
257
            throw new SphinxQLException('Query can\'t be empty');
258
        }
259
        if (!$noEscape) {
260
            $query = $this->escapeString($query);
261
        }
262
        $this->query = $query;
263
        $this->type = 'insert';
264
265
        return $this;
266
    }
267
268
    /**
269
     * Generate array for insert, from setted class parameters
270
     *
271
     * @return array
272
     */
273
    private function generateInsert()
274
    {
275
        $insertArray = ['query' => $this->query];
276
277
        if (!empty($this->tags)) {
278
            $insertArray['tags'] = $this->tags;
279
        }
280
281
        if (!empty($this->filters)) {
282
            $insertArray['filters'] = $this->filters;
283
        }
284
285
        return $insertArray;
286
    }
287
288
    /**
289
     * Executs query and clear class parameters
290
     *
291
     * @return Drivers\ResultSetInterface
292
     * @throws Exception\ConnectionException
293
     * @throws Exception\DatabaseException
294
     * @throws SphinxQLException
295
     */
296
    public function execute()
297
    {
298
299
        if ($this->type == 'insert') {
300
            return $this->sphinxQL
301
                ->insert()
302
                ->into($this->index)
303
                ->set($this->generateInsert())
304
                ->execute();
305
        }
306
307
        return $this->sphinxQL
308
            ->query("CALL PQ ('" .
309
                $this->index . "', " . $this->getDocuments() . " " . $this->getOptions() . ")")
310
            ->execute();
311
    }
312
313
    /**
314
     * Set one option for CALL PQ
315
     *
316
     * @param string $key
317
     * @param int $value
318
     *
319
     * @return $this
320
     * @throws SphinxQLException
321
     */
322
    private function setOption($key, $value)
323
    {
324
        $value = intval($value);
325
        if (!in_array($key, [
326
            self::OPTION_DOCS_JSON,
327
            self::OPTION_DOCS,
328
            self::OPTION_VERBOSE,
329
            self::OPTION_QUERY
330
        ])) {
331
            throw new SphinxQLException('Unknown option');
332
        }
333
334
        if ($value != 0 && $value != 1) {
335
            throw new SphinxQLException('Option value can be only 1 or 0');
336
        }
337
338
        if ($key == self::OPTION_DOCS_JSON) {
339
            $this->throwExceptions = 1;
340
        }
341
342
        $this->options[$key] = $value;
343
        return $this;
344
    }
345
346
    /**
347
     * Set document parameter for CALL PQ
348
     *
349
     * @param array|string $documents
350
     * @return $this
351
     * @throws SphinxQLException
352
     */
353
    public function documents($documents)
354
    {
355
        if (empty($documents)) {
356
            throw new SphinxQLException('Document can\'t be empty');
357
        }
358
        $this->documents = $documents;
359
360
        return $this;
361
    }
362
363
    /**
364
     * Set options for CALL PQ
365
     *
366
     * @param array $options
367
     * @return $this
368
     * @throws SphinxQLException
369
     */
370
    public function options(array $options)
371
    {
372
        foreach ($options as $option => $value) {
373
            $this->setOption($option, $value);
374
        }
375
        return $this;
376
    }
377
378
379
    /**
380
     * Get and prepare options for CALL PQ
381
     *
382
     * @return string string
383
     */
384
    protected function getOptions()
385
    {
386
        $options = '';
387
        if (!empty($this->options)) {
388
            foreach ($this->options as $option => $value) {
389
                $options .= ', ' . $value . ' ' . $option;
390
            }
391
        }
392
393
        return $options;
394
    }
395
396
    /**
397
     * Check is array associative
398
     * @param array $arr
399
     * @return bool
400
     */
401
    private function isAssocArray(array $arr)
402
    {
403
        if (array() === $arr) {
404
            return false;
405
        }
406
        return array_keys($arr) !== range(0, count($arr) - 1);
407
    }
408
409
    /**
410
     * Get documents for CALL PQ. If option setted JSON - returns json_encoded
411
     *
412
     * Now selection of supported types work automatically. You don't need set
413
     * OPTION_DOCS_JSON to 1 or 0. But if you will set this option,
414
     * automatically enables exceptions on unsupported types
415
     *
416
     *
417
     * 1) If expect json = 0:
418
     *      a) doc can be 'catch me'
419
     *      b) doc can be multiple ['catch me if can','catch me']
420
     *
421
     * 2) If expect json = 1:
422
     *      a) doc can be jsonOBJECT {"subject": "document about orange"}
423
     *      b) docs can be jsonARRAY of jsonOBJECTS [{"subject": "document about orange"}, {"doc": "document about orange"}]
424
     *      c) docs can be phpArray of jsonObjects ['{"subject": "document about orange"}', '{"doc": "document about orange"}']
425
     *      d) doc can be associate array ['subject'=>'document about orange']
426
     *      e) docs can be array of associate arrays [['subject'=>'document about orange'], ['doc'=>'document about orange']]
427
     *
428
     *
429
     *
430
     *
431
     * @return string
432
     * @throws SphinxQLException
433
     */
434
    protected function getDocuments()
435
    {
436
        if (!empty($this->documents)) {
437
438
            if ($this->throwExceptions) {
439
440
                if ($this->options[self::OPTION_DOCS_JSON]) {
441
442
                    if (!is_array($this->documents)) {
443
                        $json = $this->prepareFromJson($this->documents);
444
                        if (!$json) {
445
                            throw new SphinxQLException('Documents must be in json format');
446
                        }
447
                    } else {
448
                        if (!$this->isAssocArray($this->documents) && !is_array($this->documents[0])) {
449
                            throw new SphinxQLException('Documents array must be associate');
450
                        }
451
                    }
452
                }
453
            }
454
455
            if (is_array($this->documents)) {
456
457
                // If input is phpArray with json like
458
                // ->documents(['{"body": "body of doc 1", "title": "title of doc 1"}',
459
                //             '{"subject": "subject of doc 3"}'])
460
                //
461
                // Checking first symbol of first array value. If [ or { - call checkJson
462
463
                if (!empty($this->documents[0]) && !is_array($this->documents[0]) &&
464
                    ($this->documents[0][0] == '[' || $this->documents[0][0] == '{')) {
465
466
                    $json = $this->prepareFromJson($this->documents);
467
                    if ($json) {
468
                        $this->options[self::OPTION_DOCS_JSON] = 1;
469
                        return $json;
470
                    }
471
472
                } else {
473
                    if (!$this->isAssocArray($this->documents)) {
474
475
                        // if incoming single array like ['catch me if can', 'catch me']
476
477
                        if (is_string($this->documents[0])) {
478
                            $this->options[self::OPTION_DOCS_JSON] = 0;
479
                            return '(' . $this->quoteString(implode('\', \'', $this->documents)) . ')';
480
                        }
481
482
                        // if doc is array of associate arrays [['foo'=>'bar'], ['foo1'=>'bar1']]
483
                        if (!empty($this->documents[0]) && $this->isAssocArray($this->documents[0])) {
484
                            $this->options[self::OPTION_DOCS_JSON] = 1;
485
                            return $this->convertArrayForQuery($this->documents);
486
                        }
487
488
                    } else {
489
                        if ($this->isAssocArray($this->documents)) {
490
                            // Id doc is associate array ['foo'=>'bar']
491
                            $this->options[self::OPTION_DOCS_JSON] = 1;
492
                            return $this->quoteString(json_encode($this->documents));
493
                        }
494
                    }
495
                }
496
497
            } else {
498
                if (is_string($this->documents)) {
0 ignored issues
show
The condition is_string($this->documents) is always true.
Loading history...
499
500
                    $json = $this->prepareFromJson($this->documents);
501
                    if ($json) {
502
                        $this->options[self::OPTION_DOCS_JSON] = 1;
503
                        return $json;
504
                    }
505
506
                    $this->options[self::OPTION_DOCS_JSON] = 0;
507
                    return $this->quoteString($this->documents);
508
                }
509
            }
510
        }
511
        throw new SphinxQLException('Documents can\'t be empty');
512
    }
513
514
515
    /**
516
     * Set type
517
     *
518
     * @return $this
519
     */
520
    public function callPQ()
521
    {
522
        $this->clear();
523
        $this->type = 'call';
524
        return $this;
525
    }
526
527
528
    /**
529
     * Prepares documents for insert in valid format.
530
     * $data can be jsonArray of jsonObjects,
531
     * phpArray of jsonObjects, valid json or string
532
     *
533
     * @param string|array $data
534
     *
535
     * @return bool|string
536
     */
537
    private function prepareFromJson($data)
538
    {
539
        if (is_array($data)) {
540
            if (is_array($data[0])) {
541
                return false;
542
            }
543
            $return = [];
544
            foreach ($data as $item) {
545
                $return[] = $this->prepareFromJson($item);
546
            }
547
548
            return '(' . implode(', ', $return) . ')';
549
        }
550
        $array = json_decode($data, true);
551
552
        if (json_last_error() == JSON_ERROR_NONE) { // if json
553
            if ( ! empty($array[0])) { // If docs is jsonARRAY of jsonOBJECTS
554
                return $this->convertArrayForQuery($array);
555
            }
556
557
            // If docs is jsonOBJECT
558
            return $this->quoteString($data);
559
        }
560
561
        return false;
562
    }
563
564
565
    /**
566
     * Converts array of associate arrays to valid for query statement
567
     * ('jsonOBJECT1', 'jsonOBJECT2' ...)
568
     *
569
     *
570
     * @param array $array
571
     * @return string
572
     */
573
    private function convertArrayForQuery(array $array)
574
    {
575
        $documents = [];
576
        foreach ($array as $document) {
577
            $documents[] = json_encode($document);
578
        }
579
580
        return '(' . $this->quoteString(implode('\', \'', $documents)) . ')';
581
    }
582
583
584
    /**
585
     * Adding single quotes to string
586
     *
587
     * @param string $string
588
     * @return string
589
     */
590
    private function quoteString($string)
591
    {
592
        return '\'' . $string . '\'';
593
    }
594
595
596
    /**
597
     * Get last compiled query
598
     *
599
     * @return string
600
     */
601
    public function getLastQuery()
602
    {
603
        return $this->sphinxQL->getCompiled();
604
    }
605
}
606