From a28d911e8173eede5ecff6134279f91c49eb8f26 Mon Sep 17 00:00:00 2001 From: wickedOne Date: Mon, 13 Jan 2020 11:38:40 +0100 Subject: [PATCH] range pivot (#726) * range pivot added the ability to add a pivot to your range facet --- CHANGELOG.md | 3 + .../facetset-component/facet-range.md | 70 ++++++++++++++++--- src/Component/Facet/AbstractRange.php | 18 +++++ src/Component/Facet/Range.php | 2 + src/Component/RequestBuilder/FacetSet.php | 7 ++ src/Component/ResponseParser/FacetSet.php | 36 ++++++++-- .../Result/Facet/Pivot/PivotItem.php | 28 ++++++++ src/Component/Result/Facet/Range.php | 20 +++++- .../Component/RequestBuilder/FacetSetTest.php | 25 +++++++ .../Component/ResponseParser/FacetSetTest.php | 53 +++++++++++++- .../Query/Component/Facet/RangeTest.php | 8 +++ 11 files changed, 249 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d10006e8..72a3a3e84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## [unreleased] +### Added +- Range facet pivot support + ### Fixed - setting limit for pivot facets diff --git a/docs/queries/select-query/building-a-select-query/components/facetset-component/facet-range.md b/docs/queries/select-query/building-a-select-query/components/facetset-component/facet-range.md index 0132ef573..a9cc03286 100644 --- a/docs/queries/select-query/building-a-select-query/components/facetset-component/facet-range.md +++ b/docs/queries/select-query/building-a-select-query/components/facetset-component/facet-range.md @@ -7,19 +7,21 @@ The options below can be set as query option values, but also by using the set/g Only the facet-type specific options are listed. See [Facetset component](V3:Facetset_component "wikilink") for the option shared by all facet types. -| Name | Type | Default value | Description | -|---------|--------|---------------|---------------------------------------------------------------------------------------------------------------------------------------------------------| -| field | string | null | This param indicates what field to create range facets for | -| start | string | null | The lower bound of the ranges. | -| end | string | null | The upper bound of the ranges. | -| gap | string | null | The size of each range expressed as a value to be added to the lower bound. | -| hardend | string | null | A Boolean parameter instructing Solr what to do in the event that facet.range.gap does not divide evenly between facet.range.start and facet.range.end. | -| other | string | null | This param indicates what to count in addition to the counts for each range constraint between facet.range.start and facet.range.en | -| include | string | null | Specify count bounds | +| Name | Type | Default value | Description | +|---------|----------------|---------------|---------------------------------------------------------------------------------------------------------------------------------------------------------| +| field | string | null | This param indicates what field to create range facets for | +| start | string | null | The lower bound of the ranges. | +| end | string | null | The upper bound of the ranges. | +| gap | string | null | The size of each range expressed as a value to be added to the lower bound. | +| hardend | string | null | A Boolean parameter instructing Solr what to do in the event that facet.range.gap does not divide evenly between facet.range.start and facet.range.end. | +| other | string | null | This param indicates what to count in addition to the counts for each range constraint between facet.range.start and facet.range.en | +| include | string | null | Specify count bounds | +| tag | string | null | When defined, it's used as the identifier in the select query. Required when specifying pivot fields | +| pivot | string / array | null | One or more fields which should be used to create pivot values | || -Example -------- +Examples +-------- ```php createSelect(); + +// get the facetset component +$facetSet = $query->getFacetSet(); + +// create a facet field instance and set options +$facet = $facetSet->createFacetRange(['key' => 'manufacturedate_dt', 'tag' => 'r1']); + +$facet->setField('manufacturedate_dt'); +$facet->setStart('2006-01-01T00:00:00Z'); +$facet->setEnd('NOW/YEAR')); +$facet->setGap('+1YEAR'); +$facet->setPivot(['inStock']); + +// this executes the query and returns the result +$resultset = $client->select($query); + +// display the total number of documents found by solr +echo 'NumFound: '.$resultset->getNumFound(); + +// display pivot facet counts +echo '
Facet ranges:
'; +$facets = $resultset->getFacetSet()->getFacet('manufacturedate_dt'); +foreach ($facets as $facet) { + foreach ($facet->getRanges() as $range) { + foreach ($range->getValues() as $date => $count) { + echo $date . ' [' . $count . ']
'; + } + } +} + +htmlFooter(); + +``` \ No newline at end of file diff --git a/src/Component/Facet/AbstractRange.php b/src/Component/Facet/AbstractRange.php index 7a9b789ae..d04b1a69c 100644 --- a/src/Component/Facet/AbstractRange.php +++ b/src/Component/Facet/AbstractRange.php @@ -260,6 +260,24 @@ public function getInclude(): array return $include; } + /** + * @param \Solarium\Component\Facet\Pivot|array $pivot + * + * @return \Solarium\Core\Configurable + */ + public function setPivot($pivot) + { + return $this->setOption('pivot', $pivot); + } + + /** + * @return \Solarium\Component\Facet\Pivot|array|null + */ + public function getPivot() + { + return $this->getOption('pivot'); + } + /** * Initialize options. * diff --git a/src/Component/Facet/Range.php b/src/Component/Facet/Range.php index f8658e0e0..ce8f6a912 100644 --- a/src/Component/Facet/Range.php +++ b/src/Component/Facet/Range.php @@ -60,6 +60,8 @@ protected function init() case 'exclude': $this->getLocalParameters()->addExcludes($value); break; + case 'pivot': + $this->setPivot(new Pivot($value)); } } } diff --git a/src/Component/RequestBuilder/FacetSet.php b/src/Component/RequestBuilder/FacetSet.php index 8a2c33ac0..f67658735 100644 --- a/src/Component/RequestBuilder/FacetSet.php +++ b/src/Component/RequestBuilder/FacetSet.php @@ -218,6 +218,13 @@ public function addFacetRange($request, $facet) foreach ($facet->getInclude() as $includeValue) { $request->addParam("f.$field.facet.range.include", $includeValue); } + + if (null !== $pivot = $facet->getPivot()) { + $request->addParam( + 'facet.pivot', + sprintf('%s%s', $pivot->getLocalParameters()->render(), implode(',', $pivot->getFields())) + ); + } } /** diff --git a/src/Component/ResponseParser/FacetSet.php b/src/Component/ResponseParser/FacetSet.php index f5528fbac..45f1fbd7d 100644 --- a/src/Component/ResponseParser/FacetSet.php +++ b/src/Component/ResponseParser/FacetSet.php @@ -40,7 +40,6 @@ class FacetSet extends ResponseParserAbstract implements ComponentParserInterfac /** * Parse result data into result objects. * - * * @param ComponentAwareQueryInterface|AbstractQuery $query * @param AbstractComponent|QueryFacetSet $facetSet * @param array $data @@ -148,15 +147,15 @@ protected function parseJsonFacetSet(array $facet_data, array $facets): array { $buckets_and_aggregations = []; foreach ($facet_data as $key => $values) { - if (is_array($values)) { + if (\is_array($values)) { if (isset($values['buckets'])) { $buckets = []; // Parse buckets. foreach ($values['buckets'] as $bucket) { $val = $bucket['val']; $count = $bucket['count']; - unset($bucket['val']); - unset($bucket['count']); + unset($bucket['val'], $bucket['count']); + $buckets[] = new Bucket($val, $count, new ResultFacetSet($this->parseJsonFacetSet($bucket, (isset($facets[$key]) && $facets[$key] instanceof JsonFacetInterface) ? $facets[$key]->getFacets() : [] ))); @@ -179,6 +178,7 @@ protected function parseJsonFacetSet(array $facet_data, array $facets): array $buckets_and_aggregations[$key] = new Aggregation($values); } } + return $buckets_and_aggregations; } @@ -256,7 +256,7 @@ protected function facetMultiQuery(FacetInterface $facet, array $data): ?ResultF } } - if (count($values) <= 0) { + if (\count($values) <= 0) { return null; } @@ -274,6 +274,28 @@ protected function facetMultiQuery(FacetInterface $facet, array $data): ?ResultF */ protected function facetRange(AbstractQuery $query, FacetInterface $facet, array $data): ?ResultFacetRange { + if (null !== $pivot = $facet->getPivot()) { + foreach ($pivot->getLocalParameters()->getKeys() as $key) { + if (isset($data['facet_counts']['facet_pivot'][$key])) { + $pivot = $data['facet_counts']['facet_pivot'][$key]; + + foreach ($pivot as $pivotKey => $piv) { + if (isset($piv['ranges'])) { + foreach ($piv['ranges'] as $rangeKey => $range) { + if (isset($range['counts'])) { + $pivot[$pivotKey]['ranges'][$rangeKey]['counts'] = $this->convertToKeyValueArray($range['counts']); + } + } + } + } + + $pivot = new ResultFacetPivot($pivot); + } else { + $pivot = null; + } + } + } + $key = $facet->getKey(); if (!isset($data['facet_counts']['facet_ranges'][$key])) { return null; @@ -291,7 +313,7 @@ protected function facetRange(AbstractQuery $query, FacetInterface $facet, array $data['counts'] = $this->convertToKeyValueArray($data['counts']); } - return new ResultFacetRange($data['counts'], $before, $after, $between, $start, $end, $gap); + return new ResultFacetRange($data['counts'], $before, $after, $between, $start, $end, $gap, $pivot); } /** @@ -350,7 +372,7 @@ protected function pivotStats(PivotItem $pivotItem): void if (null !== $stats = $pivotItem->getStats()) { foreach ($stats->getResults() as $key => $result) { - if ($result instanceof Result || false === is_array($result)) { + if ($result instanceof Result || false === \is_array($result)) { continue; } diff --git a/src/Component/Result/Facet/Pivot/PivotItem.php b/src/Component/Result/Facet/Pivot/PivotItem.php index cf00f3a29..0f6cffcb6 100644 --- a/src/Component/Result/Facet/Pivot/PivotItem.php +++ b/src/Component/Result/Facet/Pivot/PivotItem.php @@ -3,6 +3,7 @@ namespace Solarium\Component\Result\Facet\Pivot; use Solarium\Component\Result\Stats\Stats; +use Solarium\Component\Result\Facet\Range; /** * Select field pivot result. @@ -37,6 +38,11 @@ class PivotItem extends Pivot */ protected $stats; + /** + * @var \Solarium\Component\Result\Facet\Range[] + */ + protected $ranges; + /** * Constructor. * @@ -59,6 +65,18 @@ public function __construct(array $data) if (isset($data['stats'])) { $this->stats = new Stats($data['stats']); } + + if (isset($data['ranges'])) { + foreach ($data['ranges'] as $range) { + $before = $range['before'] ?? null; + $after = $range['after'] ?? null; + $between = $range['between'] ?? null; + $start = $range['start'] ?? null; + $end = $range['end'] ?? null; + $gap = $range['gap'] ?? null; + $this->ranges[] = new Range($range['counts'], $before, $after, $between, $start, $end, $gap); + } + } } /** @@ -100,4 +118,14 @@ public function getStats(): ?Stats { return $this->stats; } + + /** + * Get ranges. + * + * @return \Solarium\Component\Result\Facet\Range[] + */ + public function getRanges(): array + { + return $this->ranges; + } } diff --git a/src/Component/Result/Facet/Range.php b/src/Component/Result/Facet/Range.php index fbb2011b5..31e859941 100644 --- a/src/Component/Result/Facet/Range.php +++ b/src/Component/Result/Facet/Range.php @@ -2,6 +2,8 @@ namespace Solarium\Component\Result\Facet; +use Solarium\Component\Result\Facet\Pivot\Pivot; + /** * Select range facet result. * @@ -56,6 +58,11 @@ class Range extends Field */ protected $gap; + /** + * @var \Solarium\Component\Result\Facet\Pivot\Pivot|null + */ + protected $pivot; + /** * Constructor. * @@ -66,16 +73,19 @@ class Range extends Field * @param string|int|null $start * @param string|int|null $end * @param string|int|null $gap + * @param Pivot|null $pivot */ - public function __construct(array $values, ?int $before, ?int $after, ?int $between, $start, $end, $gap) + public function __construct(array $values, ?int $before, ?int $after, ?int $between, $start, $end, $gap, ?Pivot $pivot = null) { parent::__construct($values); + $this->before = $before; $this->after = $after; $this->between = $between; $this->start = $start; $this->end = $end; $this->gap = $gap; + $this->pivot = $pivot; } /** @@ -152,4 +162,12 @@ public function getGap(): string { return (string) $this->gap; } + + /** + * @return \Solarium\Component\Result\Facet\Pivot\Pivot|null + */ + public function getPivot(): ?Pivot + { + return $this->pivot; + } } diff --git a/tests/Component/RequestBuilder/FacetSetTest.php b/tests/Component/RequestBuilder/FacetSetTest.php index 777817d82..50d179d21 100644 --- a/tests/Component/RequestBuilder/FacetSetTest.php +++ b/tests/Component/RequestBuilder/FacetSetTest.php @@ -279,6 +279,31 @@ public function testBuildWithRangeFacetExcludingOptionalParams() ); } + public function testBuildWithRangeFacetAndPivot() + { + $this->component->addFacet( + new FacetRange( + [ + 'key' => 'key', + 'local_tag' => 'r1', + 'field' => 'manufacturedate_dt', + 'start' => '2006-01-01T00:00:00Z', + 'end' => 'NOW/YEAR', + 'gap' => '+1YEAR', + 'pivot' => ['fields' => ['cat', 'inStock'], 'local_range' => 'r1'], + ] + ) + ); + + $request = $this->builder->buildComponent($this->component, $this->request); + + $this->assertNull($request->getRawData()); + $this->assertEquals( + '?facet.range={!key=key tag=r1}manufacturedate_dt&f.manufacturedate_dt.facet.range.start=2006-01-01T00:00:00Z&f.manufacturedate_dt.facet.range.end=NOW/YEAR&f.manufacturedate_dt.facet.range.gap=+1YEAR&facet.pivot={!range=r1}cat,inStock&facet=true', + urldecode($request->getUri()) + ); + } + public function testBuildWithFacetsAndGlobalFacetSettings() { $this->component->setMissing(true); diff --git a/tests/Component/ResponseParser/FacetSetTest.php b/tests/Component/ResponseParser/FacetSetTest.php index 6bc205999..6052ddc6f 100644 --- a/tests/Component/ResponseParser/FacetSetTest.php +++ b/tests/Component/ResponseParser/FacetSetTest.php @@ -46,7 +46,9 @@ public function setUp(): void ] ); $this->facetSet->createFacet('range', ['key' => 'keyD']); + $this->facetSet->createFacet('range', ['key' => 'keyD_A', 'pivot' => ['key' => 'keyF']]); $this->facetSet->createFacet('pivot', ['key' => 'keyE', 'fields' => 'cat,price']); + $this->facetSet->createFacet('pivot', ['key' => 'keyF', 'fields' => 'cat']); $this->query = new Query(); } @@ -82,6 +84,19 @@ public function testParse() 1, ], ], + 'keyD_A' => [ + 'before' => 3, + 'after' => 5, + 'between' => 4, + 'counts' => [ + '1.0', + 1, + '101.0', + 2, + '201.0', + 1, + ], + ], ], 'facet_pivot' => [ 'keyE' => [ @@ -95,6 +110,26 @@ public function testParse() ], ], ], + 'keyF' => [ + [ + 'field' => 'cat', + 'value' => 'abc', + 'count' => 2, + 'ranges' => [ + [ + 'gap' => '+1YEAR', + 'start' => '2016-01-01T00:00:00Z', + 'end' => '2020-01-01T00:00:00Z', + 'counts' => [ + '2018-01-01T00:00:00Z', + 0, + '2019-01-01T00:00:00Z', + 1, + ], + ], + ], + ], + ], ], ], ]; @@ -102,7 +137,7 @@ public function testParse() $result = $this->parser->parse($this->query, $this->facetSet, $data); $facets = $result->getFacets(); - $this->assertEquals(['keyA', 'keyB', 'keyC', 'keyD', 'keyE'], array_keys($facets)); + $this->assertEquals(['keyA', 'keyB', 'keyC', 'keyD', 'keyD_A', 'keyE', 'keyF'], array_keys($facets)); $this->assertEquals( ['value1' => 12, 'value2' => 3], @@ -124,9 +159,23 @@ public function testParse() $this->assertEquals(3, $facets['keyD']->getBefore()); $this->assertEquals(4, $facets['keyD']->getBetween()); $this->assertEquals(5, $facets['keyD']->getAfter()); - $this->assertEquals(1, count($facets['keyE'])); + $this->assertEquals(1, \count($facets['keyE'])); $this->assertEquals(23, $result->getFacet('keyB')->getValue()); + + $facet = $result->getFacet('keyD_A')->getPivot()->getPivot()[0]; + + $this->assertEquals('cat', $facet->getField()); + $this->assertEquals('abc', $facet->getValue()); + $this->assertEquals(2, $facet->getCount()); + + $range = $facet->getRanges()[0]; + + $this->assertEquals('2016-01-01T00:00:00Z', $range->getStart()); + $this->assertEquals('2020-01-01T00:00:00Z', $range->getEnd()); + $this->assertEquals('+1YEAR', $range->getGap()); + + $this->assertEquals(['2018-01-01T00:00:00Z' => 0, '2019-01-01T00:00:00Z' => 1], $range->getValues()); } public function testParseExtractFromResponse() diff --git a/tests/QueryType/Select/Query/Component/Facet/RangeTest.php b/tests/QueryType/Select/Query/Component/Facet/RangeTest.php index f58daae9b..e6ed81e6b 100644 --- a/tests/QueryType/Select/Query/Component/Facet/RangeTest.php +++ b/tests/QueryType/Select/Query/Component/Facet/RangeTest.php @@ -30,6 +30,8 @@ public function testConfigMode() 'hardend' => true, 'other' => 'all', 'include' => 'lower', + 'tag' => 'myTag', + 'pivot' => ['pivot', 'fields'], ]; $this->facet->setOptions($options); @@ -113,4 +115,10 @@ public function testSetAndGetIncludeArray() $this->facet->setInclude(['lower', 'upper']); $this->assertSame(['lower', 'upper'], $this->facet->getInclude()); } + + public function testSetAndGetPivot() + { + $this->facet->setPivot(['pivot', 'fields']); + $this->assertSame(['pivot', 'fields'], $this->facet->getPivot()); + } }