diff --git a/docs/InnerHits/Nested.md b/docs/InnerHits/Nested.md new file mode 100644 index 0000000000000000000000000000000000000000..76b6990436d21e561c96ae695bb5aa092ed76892 --- /dev/null +++ b/docs/InnerHits/Nested.md @@ -0,0 +1,111 @@ +# Nested Inner Hits + +> More info about inner hits is in the [official elasticsearch docs][1] + +The nested inner_hits can be used to include nested inner objects as inner hits to a search hit. +The actual matches in the different scopes that caused a document to be returned is hidden. +In many cases, it’s very useful to know which inner nested objects caused certain information to be returned. + +## Simple example + +```JSON +{ + "query" : { + "nested" : { + "path" : "comments", + "query" : { + "match" : {"comments.message" : "[actual query]"} + } + } + }, + "inner_hits" : { + "comment" : { + "path" : { + "comments" : { + "query" : { + "match" : {"comments.message" : "[different query]"} + } + } + } + } + } +} +``` + +And now the query via DSL: + +```php +$matchQuery = new MatchQuery('comments.message', '[different query]'); +$nestedQuery = new NestedQuery('comments', $matchQuery); +$innerHit = new NestedInnerHit('comment', 'comments', $matchQuery); + +$search = new Search(); +$search->addQuery(new MatchQuery('comments.message', '[actual query]')); +$search->addInnerHit($innerHit); +$search->toArray(); +``` + +In the example above `comment` is the name of the inner hit, `comments` is the path +to the nested field and `$matchQuery` is the actual query that will be executed. + +## Nesting inner hits + +It is possible to nest inner hits in order to reach deeper levels of nested objects. +Here is an example of nesting inner hits: + +```JSON +{ + "inner_hits": { + "cars": { + "path": { + "cars": { + "query": { + "nested": { + "path": "cars.manufacturers", + "query": { + "match": { + "cars.manufacturers.country": { + "query": "Japan" + } + } + } + } + }, + "inner_hits": { + "manufacturers": { + "path": { + "cars.manufacturers": { + "query": { + "match": { + "cars.manufacturers.country": { + "query": "Japan" + } + } + } + } + } + } + } + } + } + } + } +} +``` + +And now the query via DSL: + +```php + +$matchQuery = new MatchQuery('cars.manufacturers.country', 'Japan'); +$nestedQuery = new NestedQuery('cars.manufacturers', $matchQuery); +$innerHitNested = new NestedInnerHit('manufacturers', 'cars.manufacturers', $matchQuery); +$innerHit = new NestedInnerHit('cars', 'cars', $nestedQuery); +$innerHit->addInnerHit($innerHitNested); + +$search = new Search(); +$search->addInnerHit($innerHit); +$search->toArray(); + +``` +[1]: https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-inner-hits.html \ No newline at end of file diff --git a/src/InnerHit/NestedInnerHit.php b/src/InnerHit/NestedInnerHit.php new file mode 100644 index 0000000000000000000000000000000000000000..73dc853b116cf937c377ee08ead4c0da4b558aac --- /dev/null +++ b/src/InnerHit/NestedInnerHit.php @@ -0,0 +1,197 @@ +<?php + +/* + * This file is part of the ONGR package. + * + * (c) NFQ Technologies UAB <info@nfq.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ONGR\ElasticsearchDSL\InnerHit; + +use ONGR\ElasticsearchDSL\BuilderBag; +use ONGR\ElasticsearchDSL\BuilderInterface; +use ONGR\ElasticsearchDSL\NameAwareTrait; +use ONGR\ElasticsearchDSL\ParametersTrait; + +/** + * Represents Elasticsearch top level nested inner hits. + * + * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-inner-hits.html + */ +class NestedInnerHit implements BuilderInterface +{ + use ParametersTrait; + use NameAwareTrait; + + /** + * @var string + */ + private $path; + + /** + * @var BuilderInterface + */ + private $query; + + /** + * @var BuilderBag + */ + private $innerHits; + + /** + * Inner hits container init. + * + * @param string $name + * @param string $path + * @param BuilderInterface $query + */ + public function __construct($name, $path, BuilderInterface $query) + { + $this->setName($name); + $this->setPath($path); + $this->setQuery($query); + } + + /** + * @return string + */ + public function getPath() + { + return $this->path; + } + + /** + * @param string $path + */ + public function setPath($path) + { + $this->path = $path; + } + + /** + * @return BuilderInterface + */ + public function getQuery() + { + return $this->query; + } + + /** + * @param BuilderInterface $query + */ + public function setQuery(BuilderInterface $query) + { + $this->query = $query; + } + + /** + * {@inheritdoc} + */ + public function getType() + { + return 'nested'; + } + + /** + * Adds a sub-innerHit. + * + * @param NestedInnerHit $innerHit + */ + public function addInnerHit(NestedInnerHit $innerHit) + { + if (!$this->innerHits) { + $this->innerHits = new BuilderBag(); + } + + $this->innerHits->add($innerHit); + } + + /** + * Returns all sub inner hits. + * + * @return BuilderInterface[] + */ + public function getInnerHits() + { + if ($this->innerHits) { + return $this->innerHits->all(); + } else { + return []; + } + } + + /** + * Returns sub inner hit. + * @param string $name inner hit name to return. + * + * @return NestedInnerHit|null + */ + public function getInnerHit($name) + { + if ($this->innerHits && $this->innerHits->has($name)) { + return $this->innerHits->get($name); + } else { + return null; + } + } + + /** + * {@inheritdoc} + */ + public function toArray() + { + $out = array_filter( + [ + 'query' => $this->getQuery()->toArray(), + 'inner_hits' => $this->collectNestedInnerHits(), + ] + ); + + $out = [ + $this->getPathType() => [ + $this->getPath() => $this->processArray($out), + ], + ]; + + return $out; + } + + /** + * Returns 'path' for neted and 'type' for parent inner hits + * + * @return null|string + */ + private function getPathType() + { + switch ($this->getType()) { + case 'nested': + $type = 'path'; + break; + case 'parent': + $type = 'type'; + break; + default: + $type = null; + } + return $type; + } + + /** + * Process all nested inner hits. + * + * @return array + */ + private function collectNestedInnerHits() + { + $result = []; + /** @var NestedInnerHit $innerHit */ + foreach ($this->getInnerHits() as $innerHit) { + $result[$innerHit->getName()] = $innerHit->toArray(); + } + + return $result; + } +} diff --git a/src/Search.php b/src/Search.php index 9760d652aa626f4ff07cc14838b6cb973de5a43a..e9af022d142f1f64695f0e07a6bc4e36a3dfd602 100644 --- a/src/Search.php +++ b/src/Search.php @@ -13,11 +13,13 @@ namespace ONGR\ElasticsearchDSL; use ONGR\ElasticsearchDSL\Aggregation\AbstractAggregation; use ONGR\ElasticsearchDSL\Highlight\Highlight; +use ONGR\ElasticsearchDSL\InnerHit\NestedInnerHit; use ONGR\ElasticsearchDSL\Query\BoolQuery; use ONGR\ElasticsearchDSL\SearchEndpoint\AbstractSearchEndpoint; use ONGR\ElasticsearchDSL\SearchEndpoint\AggregationsEndpoint; use ONGR\ElasticsearchDSL\SearchEndpoint\FilterEndpoint; use ONGR\ElasticsearchDSL\SearchEndpoint\HighlightEndpoint; +use ONGR\ElasticsearchDSL\SearchEndpoint\InnerHitsEndpoint; use ONGR\ElasticsearchDSL\SearchEndpoint\PostFilterEndpoint; use ONGR\ElasticsearchDSL\SearchEndpoint\QueryEndpoint; use ONGR\ElasticsearchDSL\SearchEndpoint\SearchEndpointFactory; @@ -328,6 +330,30 @@ class Search return $this->getEndpoint(AggregationsEndpoint::NAME)->getAll(); } + /** + * Adds inner hit into search. + * + * @param NestedInnerHit $innerHit + * + * @return $this + */ + public function addInnerHit(NestedInnerHit $innerHit) + { + $this->getEndpoint(InnerHitsEndpoint::NAME)->add($innerHit, $innerHit->getName()); + + return $this; + } + + /** + * Returns all inner hits. + * + * @return BuilderInterface[] + */ + public function getInnerHits() + { + return $this->getEndpoint(InnerHitsEndpoint::NAME)->getAll(); + } + /** * Adds sort to search. * diff --git a/src/SearchEndpoint/InnerHitsEndpoint.php b/src/SearchEndpoint/InnerHitsEndpoint.php new file mode 100644 index 0000000000000000000000000000000000000000..b3d4213090960b1683198a8a0fc1078498b92980 --- /dev/null +++ b/src/SearchEndpoint/InnerHitsEndpoint.php @@ -0,0 +1,42 @@ +<?php + +/* + * This file is part of the ONGR package. + * + * (c) NFQ Technologies UAB <info@nfq.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ONGR\ElasticsearchDSL\SearchEndpoint; + +use ONGR\ElasticsearchDSL\InnerHit\AbstractInnerHit; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +/** + * Search inner hits dsl endpoint. + */ +class InnerHitsEndpoint extends AbstractSearchEndpoint +{ + /** + * Endpoint name + */ + const NAME = 'inner_hits'; + + /** + * {@inheritdoc} + */ + public function normalize(NormalizerInterface $normalizer, $format = null, array $context = []) + { + $output = []; + if (count($this->getAll()) > 0) { + /** @var AbstractInnerHit $innerHit */ + foreach ($this->getAll() as $innerHit) { + $output[$innerHit->getName()] = $innerHit->toArray(); + } + } + + return $output; + } +} diff --git a/src/SearchEndpoint/SearchEndpointFactory.php b/src/SearchEndpoint/SearchEndpointFactory.php index 291a631479b5f94eba66a6a4f6cda191e1348622..abfcf6112fdb86db708974913a992d2e64d63296 100644 --- a/src/SearchEndpoint/SearchEndpointFactory.php +++ b/src/SearchEndpoint/SearchEndpointFactory.php @@ -27,6 +27,7 @@ class SearchEndpointFactory 'highlight' => 'ONGR\ElasticsearchDSL\SearchEndpoint\HighlightEndpoint', 'aggregations' => 'ONGR\ElasticsearchDSL\SearchEndpoint\AggregationsEndpoint', 'suggest' => 'ONGR\ElasticsearchDSL\SearchEndpoint\SuggestEndpoint', + 'inner_hits' => 'ONGR\ElasticsearchDSL\SearchEndpoint\InnerHitsEndpoint', ]; /** diff --git a/tests/InnerHit/NestedInnerHitTest.php b/tests/InnerHit/NestedInnerHitTest.php new file mode 100644 index 0000000000000000000000000000000000000000..9cb88fcddee54176118acec853512654d92b51b8 --- /dev/null +++ b/tests/InnerHit/NestedInnerHitTest.php @@ -0,0 +1,117 @@ +<?php + +namespace ONGR\ElasticsearchDSL\Tests\InnerHit; + +use ONGR\ElasticsearchDSL\InnerHit\NestedInnerHit; +use ONGR\ElasticsearchDSL\Query\MatchQuery; +use ONGR\ElasticsearchDSL\Query\NestedQuery; +use ONGR\ElasticsearchDSL\Query\TermQuery; + +class NestedInnerHitTest extends \PHPUnit_Framework_TestCase +{ + /** + * Data provider for testToArray(). + * + * @return array + */ + public function getTestToArrayData() + { + $out = []; + + $matchQuery = new MatchQuery('foo.bar.aux', 'foo'); + $nestedQuery = new NestedQuery('foo.bar', $matchQuery); + $innerHit = new NestedInnerHit('acme', 'foo', $nestedQuery); + $nestedInnerHit1 = new NestedInnerHit('aux', 'foo.bar.aux', $matchQuery); + $nestedInnerHit2 = new NestedInnerHit('lux', 'foo.bar.aux', $matchQuery); + $innerHit->addInnerHit($nestedInnerHit1); + $innerHit->addInnerHit($nestedInnerHit2); + + $out[] = [ + $nestedInnerHit1, + [ + 'path' => [ + 'foo.bar.aux' => [ + 'query' => $matchQuery->toArray(), + ], + ], + ], + ]; + + $out[] = [ + $innerHit, + [ + 'path' => [ + 'foo' => [ + 'query' => $nestedQuery->toArray(), + 'inner_hits' => [ + 'aux' => [ + 'path' => [ + 'foo.bar.aux' => [ + 'query' => $matchQuery->toArray(), + ], + ], + ], + 'lux' => [ + 'path' => [ + 'foo.bar.aux' => [ + 'query' => $matchQuery->toArray(), + ], + ], + ] + ], + ], + ], + ], + ]; + + return $out; + } + + + /** + * Tests toArray() method. + * + * @param NestedInnerHit $innerHit + * @param array $expected + * + * @dataProvider getTestToArrayData + */ + public function testToArray($innerHit, $expected) + { + $this->assertEquals($expected, $innerHit->toArray()); + } + + + /** + * Tests getters and setters for $name, $path and $query + */ + public function testGettersAndSetters() + { + $query = new MatchQuery('acme', 'test'); + $hit = new NestedInnerHit('test', 'acme', new TermQuery('foo', 'bar')); + $hit->setName('foo'); + $hit->setPath('bar'); + $hit->setQuery($query); + + $this->assertEquals('foo', $hit->getName()); + $this->assertEquals('bar', $hit->getPath()); + $this->assertEquals($query, $hit->getQuery()); + } + + /** + * Tests getInnerHit() method + */ + public function testGetInnerHit() + { + $query = new MatchQuery('acme', 'test'); + $hit = new NestedInnerHit('test', 'acme', $query); + $nestedInnerHit1 = new NestedInnerHit('foo', 'acme.foo', $query); + $nestedInnerHit2 = new NestedInnerHit('bar', 'acme.bar', $query); + $hit->addInnerHit($nestedInnerHit1); + $hit->addInnerHit($nestedInnerHit2); + + $this->assertEquals($nestedInnerHit1, $hit->getInnerHit('foo')); + $this->assertEquals($nestedInnerHit2, $hit->getInnerHit('bar')); + $this->assertNull($hit->getInnerHit('non_existing_hit')); + } +} diff --git a/tests/SearchEndpoint/InnerHitsEndpointTest.php b/tests/SearchEndpoint/InnerHitsEndpointTest.php new file mode 100644 index 0000000000000000000000000000000000000000..268bfccfce931afcb688158a84a315febc8124ef --- /dev/null +++ b/tests/SearchEndpoint/InnerHitsEndpointTest.php @@ -0,0 +1,75 @@ +<?php + +/* + * This file is part of the ONGR package. + * + * (c) NFQ Technologies UAB <info@nfq.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ONGR\ElasticsearchDSL\Tests\Unit\SearchEndpoint; + +use ONGR\ElasticsearchDSL\SearchEndpoint\InnerHitsEndpoint; + +/** + * Class AggregationsEndpointTest. + */ +class InnerHitsEndpointTest extends \PHPUnit_Framework_TestCase +{ + /** + * Tests constructor. + */ + public function testItCanBeInstantiated() + { + $this->assertInstanceOf( + 'ONGR\ElasticsearchDSL\SearchEndpoint\InnerHitsEndpoint', + new InnerHitsEndpoint() + ); + } + + /** + * Tests if endpoint returns builders. + */ + public function testEndpointGetter() + { + $hitName = 'foo'; + $innerHit = $this->getMockBuilder('ONGR\ElasticsearchDSL\BuilderInterface')->getMock(); + $endpoint = new InnerHitsEndpoint(); + $endpoint->add($innerHit, $hitName); + $builders = $endpoint->getAll(); + + $this->assertCount(1, $builders); + $this->assertSame($innerHit, $builders[$hitName]); + } + + /** + * Tests normalize method + */ + public function testNormalization() + { + $normalizer = $this + ->getMockBuilder('Symfony\Component\Serializer\Normalizer\NormalizerInterface') + ->getMock(); + $innerHit = $this + ->getMockBuilder('ONGR\ElasticsearchDSL\BuilderInterface') + ->setMethods(['getName', 'toArray', 'getType']) + ->getMock(); + $innerHit->expects($this->any())->method('getName')->willReturn('foo'); + $innerHit->expects($this->any())->method('toArray')->willReturn(['foo' => 'bar']); + + $endpoint = new InnerHitsEndpoint(); + $endpoint->add($innerHit, 'foo'); + $expected = [ + 'foo' => [ + 'foo' => 'bar' + ] + ]; + + $this->assertEquals( + $expected, + $endpoint->normalize($normalizer) + ); + } +}