1<?php
2
3namespace Elastica;
4
5use Elastica\Exception\InvalidException;
6use Elastica\Exception\NotFoundException;
7use Elastica\Exception\RuntimeException;
8use Elastica\ResultSet\BuilderInterface;
9use Elastica\Script\AbstractScript;
10use Elastica\Type\Mapping;
11use Elasticsearch\Endpoints\AbstractEndpoint;
12use Elasticsearch\Endpoints\Delete;
13use Elasticsearch\Endpoints\DeleteByQuery;
14use Elasticsearch\Endpoints\Indices\Mapping\Get;
15use Elasticsearch\Endpoints\Indices\Type\Exists;
16
17/**
18 * Elastica type object.
19 *
20 * elasticsearch has for every types as a substructure. This object
21 * represents a type inside a context
22 * The hierarchy is as following: client -> index -> type -> document
23 *
24 * @author   Nicolas Ruflin <spam@ruflin.com>
25 */
26class Type implements SearchableInterface
27{
28    /**
29     * Index.
30     *
31     * @var \Elastica\Index Index object
32     */
33    protected $_index;
34
35    /**
36     * Type name.
37     *
38     * @var string Type name
39     */
40    protected $_name;
41
42    /**
43     * @var array|string A callable that serializes an object passed to it
44     */
45    protected $_serializer;
46
47    /**
48     * Creates a new type object inside the given index.
49     *
50     * @param \Elastica\Index $index Index Object
51     * @param string          $name  Type name
52     */
53    public function __construct(Index $index, $name)
54    {
55        $this->_index = $index;
56        $this->_name = $name;
57    }
58
59    /**
60     * Adds the given document to the search index.
61     *
62     * @param \Elastica\Document $doc Document with data
63     *
64     * @return \Elastica\Response
65     */
66    public function addDocument(Document $doc)
67    {
68        $endpoint = new \Elasticsearch\Endpoints\Index();
69
70        if (null !== $doc->getId() && '' !== $doc->getId()) {
71            $endpoint->setID($doc->getId());
72        }
73
74        $options = $doc->getOptions(
75            [
76                'version',
77                'version_type',
78                'routing',
79                'percolate',
80                'parent',
81                'op_type',
82                'consistency',
83                'replication',
84                'refresh',
85                'timeout',
86                'pipeline',
87            ]
88        );
89
90        $endpoint->setBody($doc->getData());
91        $endpoint->setParams($options);
92
93        $response = $this->requestEndpoint($endpoint);
94
95        $data = $response->getData();
96        // set autogenerated id to document
97        if (($doc->isAutoPopulate()
98                || $this->getIndex()->getClient()->getConfigValue(['document', 'autoPopulate'], false))
99            && $response->isOk()
100        ) {
101            if (!$doc->hasId()) {
102                if (isset($data['_id'])) {
103                    $doc->setId($data['_id']);
104                }
105            }
106            if (isset($data['_version'])) {
107                $doc->setVersion($data['_version']);
108            }
109        }
110
111        return $response;
112    }
113
114    /**
115     * @param $object
116     * @param Document $doc
117     *
118     * @throws Exception\RuntimeException
119     *
120     * @return Response
121     */
122    public function addObject($object, Document $doc = null)
123    {
124        if (!isset($this->_serializer)) {
125            throw new RuntimeException('No serializer defined');
126        }
127
128        $data = \call_user_func($this->_serializer, $object);
129        if (!$doc) {
130            $doc = new Document();
131        }
132        $doc->setData($data);
133
134        return $this->addDocument($doc);
135    }
136
137    /**
138     * Update document, using update script. Requires elasticsearch >= 0.19.0.
139     *
140     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update.html
141     *
142     * @param \Elastica\Document|\Elastica\Script\AbstractScript $data    Document with update data
143     * @param array                                              $options array of query params to use for query. For possible options check es api
144     *
145     * @throws \Elastica\Exception\InvalidException
146     *
147     * @return \Elastica\Response
148     */
149    public function updateDocument($data, array $options = [])
150    {
151        if (!($data instanceof Document) && !($data instanceof AbstractScript)) {
152            throw new \InvalidArgumentException('Data should be a Document or Script');
153        }
154
155        if (!$data->hasId()) {
156            throw new InvalidException('Document or Script id is not set');
157        }
158
159        return $this->getIndex()->getClient()->updateDocument(
160            $data->getId(),
161            $data,
162            $this->getIndex()->getName(),
163            $this->getName(),
164            $options
165        );
166    }
167
168    /**
169     * Uses _bulk to send documents to the server.
170     *
171     * @param array|\Elastica\Document[] $docs    Array of Elastica\Document
172     * @param array                      $options Array of query params to use for query. For possible options check es api
173     *
174     * @return \Elastica\Bulk\ResponseSet
175     *
176     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html
177     */
178    public function updateDocuments(array $docs, array $options = [])
179    {
180        foreach ($docs as $doc) {
181            $doc->setType($this->getName());
182        }
183
184        return $this->getIndex()->updateDocuments($docs, $options);
185    }
186
187    /**
188     * Uses _bulk to send documents to the server.
189     *
190     * @param array|\Elastica\Document[] $docs    Array of Elastica\Document
191     * @param array                      $options Array of query params to use for query. For possible options check es api
192     *
193     * @return \Elastica\Bulk\ResponseSet
194     *
195     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html
196     */
197    public function addDocuments(array $docs, array $options = [])
198    {
199        foreach ($docs as $doc) {
200            $doc->setType($this->getName());
201        }
202
203        return $this->getIndex()->addDocuments($docs, $options);
204    }
205
206    /**
207     * Uses _bulk to send documents to the server.
208     *
209     * @param object[] $objects
210     * @param array    $options Array of query params to use for query. For possible options check es api
211     *
212     * @return Bulk\ResponseSet
213     *
214     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html
215     */
216    public function addObjects(array $objects, array $options = [])
217    {
218        if (!isset($this->_serializer)) {
219            throw new RuntimeException('No serializer defined');
220        }
221
222        $docs = [];
223        foreach ($objects as $object) {
224            $data = \call_user_func($this->_serializer, $object);
225            $doc = new Document();
226            $doc->setData($data);
227            $doc->setType($this->getName());
228            $docs[] = $doc;
229        }
230
231        return $this->getIndex()->addDocuments($docs, $options);
232    }
233
234    /**
235     * Get the document from search index.
236     *
237     * @param int|string $id      Document id
238     * @param array      $options options for the get request
239     *
240     * @throws \Elastica\Exception\NotFoundException
241     * @throws \Elastica\Exception\ResponseException
242     *
243     * @return \Elastica\Document
244     */
245    public function getDocument($id, array $options = [])
246    {
247        $endpoint = new \Elasticsearch\Endpoints\Get();
248        $endpoint->setID($id);
249        $endpoint->setParams($options);
250
251        $response = $this->requestEndpoint($endpoint);
252        $result = $response->getData();
253
254        if (!isset($result['found']) || false === $result['found']) {
255            throw new NotFoundException('doc id '.$id.' not found');
256        }
257
258        if (isset($result['fields'])) {
259            $data = $result['fields'];
260        } elseif (isset($result['_source'])) {
261            $data = $result['_source'];
262        } else {
263            $data = [];
264        }
265
266        $document = new Document($id, $data, $this->getName(), $this->getIndex());
267        $document->setVersion($result['_version']);
268
269        return $document;
270    }
271
272    /**
273     * @param string       $id
274     * @param array|string $data
275     *
276     * @return Document
277     */
278    public function createDocument($id = '', $data = [])
279    {
280        $document = new Document($id, $data);
281        $document->setType($this);
282
283        return $document;
284    }
285
286    /**
287     * Returns the type name.
288     *
289     * @return string Type name
290     */
291    public function getName()
292    {
293        return $this->_name;
294    }
295
296    /**
297     * Sets value type mapping for this type.
298     *
299     * @param \Elastica\Type\Mapping|array $mapping Elastica\Type\MappingType object or property array with all mappings
300     * @param array                        $query   querystring when put mapping (for example update_all_types)
301     *
302     * @return \Elastica\Response
303     */
304    public function setMapping($mapping, array $query = [])
305    {
306        $mapping = Mapping::create($mapping);
307        $mapping->setType($this);
308
309        return $mapping->send($query);
310    }
311
312    /**
313     * Returns current mapping for the given type.
314     *
315     * @return array Current mapping
316     */
317    public function getMapping($includeTypeName = true)
318    {
319        $response = $this->requestEndpoint(new Get(), $includeTypeName);
320        $data = $response->getData();
321
322        $mapping = \array_shift($data);
323        if (isset($mapping['mappings'])) {
324            return $mapping['mappings'];
325        }
326
327        return [];
328    }
329
330    /**
331     * Create search object.
332     *
333     * @param string|array|\Elastica\Query $query   Array with all query data inside or a Elastica\Query object
334     * @param int|array                    $options OPTIONAL Limit or associative array of options (option=>value)
335     * @param BuilderInterface             $builder
336     *
337     * @return Search
338     */
339    public function createSearch($query = '', $options = null, BuilderInterface $builder = null)
340    {
341        $search = $this->getIndex()->createSearch($query, $options, $builder);
342        $search->addType($this);
343
344        return $search;
345    }
346
347    /**
348     * Do a search on this type.
349     *
350     * @param string|array|\Elastica\Query $query   Array with all query data inside or a Elastica\Query object
351     * @param int|array                    $options OPTIONAL Limit or associative array of options (option=>value)
352     *
353     * @return \Elastica\ResultSet with all results inside
354     *
355     * @see \Elastica\SearchableInterface::search
356     */
357    public function search($query = '', $options = null)
358    {
359        $search = $this->createSearch($query, $options);
360
361        return $search->search();
362    }
363
364    /**
365     * Count docs by query.
366     *
367     * @param string|array|\Elastica\Query $query Array with all query data inside or a Elastica\Query object
368     *
369     * @return int number of documents matching the query
370     *
371     * @see \Elastica\SearchableInterface::count
372     */
373    public function count($query = '')
374    {
375        $search = $this->createSearch($query);
376
377        return $search->count();
378    }
379
380    /**
381     * Returns index client.
382     *
383     * @return \Elastica\Index Index object
384     */
385    public function getIndex()
386    {
387        return $this->_index;
388    }
389
390    /**
391     * @param \Elastica\Document $document
392     *
393     * @return \Elastica\Response
394     */
395    public function deleteDocument(Document $document)
396    {
397        $options = $document->getOptions(
398            [
399                '_version',
400                'version_type',
401                'routing',
402                'parent',
403                'replication',
404                'consistency',
405                'refresh',
406                'timeout',
407            ]
408        );
409
410        return $this->deleteById($document->getId(), $options);
411    }
412
413    /**
414     * Uses _bulk to delete documents from the server.
415     *
416     * @param array|\Elastica\Document[] $docs Array of Elastica\Document
417     *
418     * @return \Elastica\Bulk\ResponseSet
419     *
420     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html
421     */
422    public function deleteDocuments(array $docs)
423    {
424        foreach ($docs as $doc) {
425            $doc->setType($this->getName());
426        }
427
428        return $this->getIndex()->deleteDocuments($docs);
429    }
430
431    /**
432     * Deletes an entry by its unique identifier.
433     *
434     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete.html
435     *
436     * @param int|string $id      Document id
437     * @param array      $options
438     *
439     * @throws \InvalidArgumentException
440     * @throws \Elastica\Exception\NotFoundException
441     *
442     * @return \Elastica\Response Response object
443     */
444    public function deleteById($id, array $options = [])
445    {
446        if (empty($id) || !\trim($id)) {
447            throw new \InvalidArgumentException();
448        }
449
450        $endpoint = new Delete();
451        $endpoint->setID($id);
452        $endpoint->setParams($options);
453
454        $response = $this->requestEndpoint($endpoint);
455
456        $responseData = $response->getData();
457
458        if (isset($responseData['result']) && 'not_found' == $responseData['result']) {
459            throw new NotFoundException('Doc id '.$id.' not found and can not be deleted');
460        }
461
462        return $response;
463    }
464
465    /**
466     * Deletes the given list of ids from this type.
467     *
468     * @param array       $ids
469     * @param string|bool $routing Optional routing key for all ids
470     *
471     * @return \Elastica\Response Response  object
472     */
473    public function deleteIds(array $ids, $routing = false)
474    {
475        return $this->getIndex()->getClient()->deleteIds($ids, $this->getIndex(), $this, $routing);
476    }
477
478    /**
479     * Deletes entries in the db based on a query.
480     *
481     * @param \Elastica\Query|\Elastica\Query\AbstractQuery|string|array $query   Query object
482     * @param array                                                      $options Optional params
483     *
484     * @return \Elastica\Response
485     *
486     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete-by-query.html
487     */
488    public function deleteByQuery($query, array $options = [])
489    {
490        $query = Query::create($query);
491
492        $endpoint = new DeleteByQuery();
493        $endpoint->setBody($query->toArray());
494        $endpoint->setParams($options);
495
496        return $this->requestEndpoint($endpoint);
497    }
498
499    /**
500     * Makes calls to the elasticsearch server based on this type.
501     *
502     * @param string $path   Path to call
503     * @param string $method Rest method to use (GET, POST, DELETE, PUT)
504     * @param array  $data   OPTIONAL Arguments as array
505     * @param array  $query  OPTIONAL Query params
506     *
507     * @return \Elastica\Response Response object
508     */
509    public function request($path, $method, $data = [], array $query = [])
510    {
511        $path = $this->getName().'/'.$path;
512
513        return $this->getIndex()->request($path, $method, $data, $query);
514    }
515
516    /**
517     * Makes calls to the elasticsearch server with usage official client Endpoint based on this type.
518     *
519     * @param AbstractEndpoint $endpoint
520     *
521     * @return Response
522     */
523    public function requestEndpoint(AbstractEndpoint $endpoint, $includeTypeName = false)
524    {
525        $cloned = clone $endpoint;
526        $cloned->setType($this->getName());
527        if ($includeTypeName) {
528            $cloned->setParams(['include_type_name' => true]);
529        }
530
531        return $this->getIndex()->requestEndpoint($cloned);
532    }
533
534    /**
535     * Sets the serializer callable used in addObject.
536     *
537     * @see \Elastica\Type::addObject
538     *
539     * @param array|string $serializer @see \Elastica\Type::_serializer
540     *
541     * @return $this
542     */
543    public function setSerializer($serializer)
544    {
545        $this->_serializer = $serializer;
546
547        return $this;
548    }
549
550    /**
551     * Checks if the given type exists in Index.
552     *
553     * @return bool True if type exists
554     */
555    public function exists()
556    {
557        $response = $this->requestEndpoint(new Exists());
558
559        return 200 === $response->getStatus();
560    }
561}
562