Commit e72d6b16 authored by thomascorthals's avatar thomascorthals Committed by Markus Kalkbrenner

Added Query Elevation Component (#597)

* Added Query Elevation Component

* Added docs for Query Elevation Component

* Added integration test against techproducts
parent ee77b431
Query Elevation is a Solr component that lets you configure the top results for a given query regardless of the normal Lucene scoring. Elevated query results can be configured in an external XML file or at request time. For more info see <https://lucene.apache.org/solr/guide/the-query-elevation-component.html>.
Options
-------
| Name | Type | Default value | Description |
|-----------------|---------|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
| transformers | string | [elevated] | Comma separated list of transformers to annotate each document. The [elevated] transformer tells whether or not the document was elevated. |
| enableElevation | boolean | null | For debugging it may be useful to see results with and without elevation applied. To get results without elevation, use false. |
| forceElevation | boolean | null | By default, this component respects the requested sort parameter. To return elevated documents first, use true. |
| exclusive | boolean | null | You can force Solr to return only the results specified in the elevation file by using true. |
| markExcludes | boolean | null | You can include documents that the elevation configuration would normally exclude by using true. The [excluded] transformer is added to each document. |
| elevateIds | string | null | Comma separated list of documents to elevate. This overrides the elevations _and_ exclusions that are configured for the query in the elevation file. |
| excludeIds | string | null | Comma separated list of documents to exclude. This overrides the elevations _and_ exclusions that are configured for the query in the elevation file. |
||
Example
-------
```php
<?php
require(__DIR__.'/init.php');
htmlHeader();
// create a client instance
$client = new Solarium\Client($config);
// get a select query instance
$query = $client->createSelect();
$query->setQuery('electronics');
// set a handler that is configured with an elevator component in solrconfig.xml (or add it to your default handler)
$query->setHandler('elevate');
// get query elevation component
$elevate = $query->getQueryElevation();
// return elevated documents first
$elevate->setForceElevation(true);
// specify documents to elevate and/or exclude if you don't use an elevation file or want to override it at request time
$elevate->setElevateIds(array('VS1GB400C3', 'VDBDB1A16'));
$elevate->setExcludeIds(array('SP2514N', '6H500F0'));
// document transformers can be omitted from the results
//$elevate->clearTransformers();
// 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();
// show documents using the resultset iterator
foreach ($resultset as $document) {
echo '<hr/><table>';
// the documents are also iterable, to get all fields
foreach ($document as $field => $value) {
// this converts multivalue fields to a comma-separated string
if (is_array($value)) {
$value = implode(', ', $value);
}
echo '<tr><th>' . $field . '</th><td>' . $value . '</td></tr>';
}
}
htmlFooter();
```
<?php
require(__DIR__.'/init.php');
htmlHeader();
// create a client instance
$client = new Solarium\Client($config);
// get a select query instance
$query = $client->createSelect();
$query->setQuery('electronics');
// set a handler that is configured with an elevator component in solrconfig.xml (or add it to your default handler)
$query->setHandler('elevate');
// get query elevation component
$elevate = $query->getQueryElevation();
// return elevated documents first
$elevate->setForceElevation(true);
// specify documents to elevate and/or exclude if you don't use an elevation file or want to override it at request time
$elevate->setElevateIds(array('VS1GB400C3', 'VDBDB1A16'));
$elevate->setExcludeIds(array('SP2514N', '6H500F0'));
// document transformers can be omitted from the results
//$elevate->clearTransformers();
// 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();
// show documents using the resultset iterator
foreach ($resultset as $document) {
echo '<hr/><table>';
// the documents are also iterable, to get all fields
foreach ($document as $field => $value) {
// this converts multivalue fields to a comma-separated string
if (is_array($value)) {
$value = implode(', ', $value);
}
echo '<tr><th>' . $field . '</th><td>' . $value . '</td></tr>';
}
}
htmlFooter();
...@@ -58,6 +58,7 @@ ...@@ -58,6 +58,7 @@
<li><a href="2.1.5.9-spellcheck.php">2.1.5.9 Spellcheck</a></li> <li><a href="2.1.5.9-spellcheck.php">2.1.5.9 Spellcheck</a></li>
<li><a href="2.1.5.10-stats.php">2.1.5.10 Stats</a></li> <li><a href="2.1.5.10-stats.php">2.1.5.10 Stats</a></li>
<li><a href="2.1.5.11-debug.php">2.1.5.11 Debug (DebugQuery)</a></li> <li><a href="2.1.5.11-debug.php">2.1.5.11 Debug (DebugQuery)</a></li>
<li><a href="2.1.5.12-queryelevation.php">2.1.5.12 Query Elevation</a></li>
</ul> </ul>
<li><a href="2.1.6-helper-functions.php">2.1.6 Helper functions</a></li> <li><a href="2.1.6-helper-functions.php">2.1.6 Helper functions</a></li>
<li><a href="2.1.7-query-reuse.php">2.1.7 Query re-use</a></li> <li><a href="2.1.7-query-reuse.php">2.1.7 Query re-use</a></li>
......
...@@ -72,6 +72,11 @@ interface ComponentAwareQueryInterface ...@@ -72,6 +72,11 @@ interface ComponentAwareQueryInterface
*/ */
const COMPONENT_TERMS = 'terms'; const COMPONENT_TERMS = 'terms';
/**
* Query component queryelevation.
*/
const COMPONENT_QUERYELEVATION = 'queryelevation';
/** /**
* Get all registered component types. * Get all registered component types.
* *
......
<?php
namespace Solarium\Component;
use Solarium\Component\RequestBuilder\QueryElevation as RequestBuilder;
/**
* QueryElevation component.
*
* @see https://lucene.apache.org/solr/guide/the-query-elevation-component.html
*/
class QueryElevation extends AbstractComponent
{
/**
* Default options.
*
* @var array
*/
protected $options = [
'transformers' => '[elevated]',
];
/**
* Document transformers.
*
* @var array
*/
protected $transformers = [];
/**
* Get component type.
*
* @return string
*/
public function getType()
{
return ComponentAwareQueryInterface::COMPONENT_QUERYELEVATION;
}
/**
* Get a requestbuilder for this query.
*
* @return RequestBuilder
*/
public function getRequestBuilder()
{
return new RequestBuilder();
}
/**
* This component has no response parser...
*/
public function getResponseParser()
{
}
/**
* Add a document transformer.
*
* @param string $transformer
*
* @return self fluent interface
*/
public function addTransformer($transformer)
{
$this->transformers[$transformer] = true;
return $this;
}
/**
* Add multiple document transformers.
*
* You can use an array or a comma separated string as input
*
* @param array|string $transformers
*
* @return self Provides fluent interface
*/
public function addTransformers($transformers)
{
if (is_string($transformers)) {
$transformers = explode(',', $transformers);
$transformers = array_map('trim', $transformers);
}
foreach ($transformers as $transformer) {
$this->addTransformer($transformer);
}
return $this;
}
/**
* Remove a document transformer.
*
* @param string $transformer
*
* @return self Provides fluent interface
*/
public function removeTransformer($transformer)
{
if (isset($this->transformers[$transformer])) {
unset($this->transformers[$transformer]);
}
return $this;
}
/**
* Remove all document transformers.
*
* @return self fluent interface
*/
public function clearTransformers()
{
$this->transformers = [];
return $this;
}
/**
* Get all document transformers.
*
* @return array
*/
public function getTransformers()
{
return array_keys($this->transformers);
}
/**
* Set multiple document transformers.
*
* This overwrites any existing transformers
*
* @param array|string $transformers
*
* @return self Provides fluent interface
*/
public function setTransformers($transformers)
{
$this->clearTransformers();
$this->addTransformers($transformers);
return $this;
}
/**
* Set enable elevation.
*
* @param bool $enable
*
* @return self Provides fluent interface
*/
public function setEnableElevation($enable)
{
return $this->setOption('enableElevation', $enable);
}
/**
* Get enable elevation.
*
* @return bool
*/
public function getEnableElevation()
{
return $this->getOption('enableElevation');
}
/**
* Set force elevation.
*
* @param bool $force
*
* @return self Provides fluent interface
*/
public function setForceElevation($force)
{
return $this->setOption('forceElevation', $force);
}
/**
* Get force elevation.
*
* @return bool
*/
public function getForceElevation()
{
return $this->getOption('forceElevation');
}
/**
* Set exclusive.
*
* @param bool $exclusive
*
* @return self Provides fluent interface
*/
public function setExclusive($exclusive)
{
return $this->setOption('exclusive', $exclusive);
}
/**
* Get exclusive.
*
* @return bool
*/
public function getExclusive()
{
return $this->getOption('exclusive');
}
/**
* Set mark excludes.
*
* @param bool $mark
*
* @return self Provides fluent interface
*/
public function setMarkExcludes($mark)
{
if (true === $mark || 'true' === $mark) {
$this->addTransformer('[excluded]');
} else {
$this->removeTransformer('[excluded]');
}
return $this->setOption('markExcludes', $mark);
}
/**
* Get mark excludes.
*
* @return bool
*/
public function getMarkExcludes()
{
return $this->getOption('markExcludes');
}
/**
* Set elevated document ids.
*
* @param string|array $ids can be an array or string with comma separated ids
*
* @return self Provides fluent interface
*/
public function setElevateIds($ids)
{
if (is_string($ids)) {
$ids = explode(',', $ids);
$ids = array_map('trim', $ids);
}
return $this->setOption('elevateIds', $ids);
}
/**
* Get elevated document ids.
*
* @return null|array
*/
public function getElevateIds()
{
return $this->getOption('elevateIds');
}
/**
* Set excluded document ids.
*
* @param string|array $ids can be an array or string with comma separated ids
*
* @return self Provides fluent interface
*/
public function setExcludeIds($ids)
{
if (is_string($ids)) {
$ids = explode(',', $ids);
$ids = array_map('trim', $ids);
}
return $this->setOption('excludeIds', $ids);
}
/**
* Get excluded document ids.
*
* @return null|array
*/
public function getExcludeIds()
{
return $this->getOption('excludeIds');
}
/**
* Initialize options.
*
* Several options need some extra checks or setup work, for these options
* the setters are called.
*/
protected function init()
{
foreach ($this->options as $name => $value) {
switch ($name) {
case 'transformers':
$this->setTransformers($value);
break;
case 'markExcludes':
$this->setMarkExcludes($value);
break;
case 'elevateIds':
$this->setElevateIds($value);
break;
case 'excludeIds':
$this->setExcludeIds($value);
break;
}
}
}
}
<?php
namespace Solarium\Component\QueryTraits;
use Solarium\Component\ComponentAwareQueryInterface;
/**
* Trait query types supporting components.
*/
trait QueryElevationTrait
{
/**
* Get a QueryElevation component instance.
*
* This is a convenience method that maps presets to getComponent
*
* @return \Solarium\Component\QueryElevation
*/
public function getQueryElevation()
{
return $this->getComponent(ComponentAwareQueryInterface::COMPONENT_QUERYELEVATION, true);
}
}
<?php
namespace Solarium\Component\RequestBuilder;
use Solarium\Component\QueryElevation as QueryelevationComponent;
use Solarium\Core\Client\Request;
/**
* Add select component queryelevation to the request.
*/
class QueryElevation implements ComponentRequestBuilderInterface
{
/**
* Add request settings for QueryelevationComponent.
*
* @param QueryelevationComponent $component
* @param Request $request
*
* @return Request
*/
public function buildComponent($component, $request)
{
// add document transformers to request field list
if (null !== ($transformers = $component->getTransformers())) {
$fl = $request->getParam('fl');
$fields = implode(',', null === $fl ? $transformers : array_merge([$fl], $transformers));
$request->addParam('fl', $fields, true);
}
// add basic params to request
$request->addParam('enableElevation', $component->getEnableElevation());
$request->addParam('forceElevation', $component->getForceElevation());
$request->addParam('exclusive', $component->getExclusive());
$request->addParam('markExcludes', $component->getMarkExcludes());
// add overrides for pre-configured elevations
$request->addParam('elevateIds', null === ($ids = $component->getElevateIds()) ? null : implode(',', $ids));
$request->addParam('excludeIds', null === ($ids = $component->getExcludeIds()) ? null : implode(',', $ids));
return $request;
}
}
...@@ -12,6 +12,7 @@ use Solarium\Component\QueryTraits\FacetSetTrait; ...@@ -12,6 +12,7 @@ use Solarium\Component\QueryTraits\FacetSetTrait;
use Solarium\Component\QueryTraits\GroupingTrait; use Solarium\Component\QueryTraits\GroupingTrait;
use Solarium\Component\QueryTraits\HighlightingTrait; use Solarium\Component\QueryTraits\HighlightingTrait;
use Solarium\Component\QueryTraits\MoreLikeThisTrait; use Solarium\Component\QueryTraits\MoreLikeThisTrait;
use Solarium\Component\QueryTraits\QueryElevationTrait;
use Solarium\Component\QueryTraits\SpatialTrait; use Solarium\Component\QueryTraits\SpatialTrait;
use Solarium\Component\QueryTraits\SpellcheckTrait; use Solarium\Component\QueryTraits\SpellcheckTrait;
use Solarium\Component\QueryTraits\StatsTrait; use Solarium\Component\QueryTraits\StatsTrait;
...@@ -44,6 +45,7 @@ class Query extends AbstractQuery implements ComponentAwareQueryInterface ...@@ -44,6 +45,7 @@ class Query extends AbstractQuery implements ComponentAwareQueryInterface
use GroupingTrait; use GroupingTrait;
use DistributedSearchTrait; use DistributedSearchTrait;
use StatsTrait; use StatsTrait;
use QueryElevationTrait;
/** /**
* Solr sort mode descending. * Solr sort mode descending.
...@@ -124,6 +126,7 @@ class Query extends AbstractQuery implements ComponentAwareQueryInterface ...@@ -124,6 +126,7 @@ class Query extends AbstractQuery implements ComponentAwareQueryInterface
ComponentAwareQueryInterface::COMPONENT_GROUPING => 'Solarium\Component\Grouping', ComponentAwareQueryInterface::COMPONENT_GROUPING => 'Solarium\Component\Grouping',
ComponentAwareQueryInterface::COMPONENT_DISTRIBUTEDSEARCH => 'Solarium\Component\DistributedSearch', ComponentAwareQueryInterface::COMPONENT_DISTRIBUTEDSEARCH => 'Solarium\Component\DistributedSearch',
ComponentAwareQueryInterface::COMPONENT_STATS => 'Solarium\Component\Stats\Stats', ComponentAwareQueryInterface::COMPONENT_STATS => 'Solarium\Component\Stats\Stats',
ComponentAwareQueryInterface::COMPONENT_QUERYELEVATION => 'Solarium\Component\QueryElevation',
]; ];
parent::__construct($options); parent::__construct($options);
......
<?php
namespace Solarium\Tests\Component;
use PHPUnit\Framework\TestCase;
use Solarium\Component\ComponentAwareQueryInterface;
use Solarium\Component\QueryElevation;
class QueryElevationTest extends TestCase
{
/**
* @var QueryElevation
*/
protected $queryelevation;
public function setUp()
{
$this->queryelevation = new QueryElevation();
}
public function testConfigMode()
{
$options = [
'transformers' => '[transformer]',
'enableElevation' => false,
'forceElevation' => true,
'exclusive' => true,
'markExcludes' => false,
'elevateIds' => 'doc1,doc2',
'excludeIds' => 'doc3,doc4',
];
$this->queryelevation->setOptions($options);
$this->assertSame(['[transformer]'], $this->queryelevation->getTransformers());
$this->assertFalse($this->queryelevation->getEnableElevation());
$this->assertTrue($this->queryelevation->getForceElevation());
$this->assertTrue($this->queryelevation->getExclusive());
$this->assertFalse($this->queryelevation->getMarkExcludes());
$this->assertSame(['doc1', 'doc2'], $this->queryelevation->getElevateIds());
$this->assertSame(['doc3', 'doc4'], $this->queryelevation->getExcludeIds());
}
public function testGetType()
{
$this->assertEquals(ComponentAwareQueryInterface::COMPONENT_QUERYELEVATION, $this->queryelevation->getType());
}
public function testGetResponseParser()
{
$this->assertNull($this->queryelevation->getResponseParser());
}
public function testGetRequestBuilder()
{
$this->assertInstanceOf(
'Solarium\Component\RequestBuilder\QueryElevation',
$this->queryelevation->getRequestBuilder()
);
}
public function testAddTransformer()
{
$expectedTrans = $this->queryelevation->getTransformers();
$expectedTrans[] = '[newtrans]';
$this->queryelevation->addTransformer('[newtrans]');
$this->assertSame($expectedTrans, $this->queryelevation->getTransformers());
}
public function testClearTransformers()
{
$this->queryelevation->addTransformer('[newtrans]');
$this->queryelevation->clearTransformers();
$this->assertSame([], $this->queryelevation->getTransformers());
}
public function testAddTransformers()
{
$transformers = ['[trans1]', '[trans2]'];
$this->queryelevation->clearTransformers();
$this->queryelevation->addTransformers($transformers);
$this->assertSame($transformers, $this->queryelevation->getTransformers());
}
public function testAddTransformersAsStringWithTrim()
{
$this->queryelevation->clearTransformers();
$this->queryelevation->addTransformers('[trans1], [trans2]');
$this->assertSame(['[trans1]', '[trans2]'], $this->queryelevation->getTransformers());
}
public function testRemoveTransformer()
{
$this->queryelevation->clearTransformers();
$this->queryelevation->addTransformers(['[trans1]', '[trans2]']);
$this->queryelevation->removeTransformer('[trans1]');
$this->assertSame(['[trans2]'], $this->queryelevation->getTransformers());
}
public function testSetTransformers()
{
$this->queryelevation->clearTransformers();
$this->queryelevation->addTransformers(['[trans1]', '[trans2]']);
$this->queryelevation->setTransformers(['[trans3]', '[trans4]']);
$this->assertSame(['[trans3]', '[trans4]'], $this->queryelevation->getTransformers());
}
public function testSetAndGetEnableElevation()
{
$this->queryelevation->setEnableElevation(false);
$this->assertFalse($this->queryelevation->getEnableElevation());
}
public function testSetAndGetForceElevation()
{
$this->queryelevation->setForceElevation(true);
$this->assertTrue($this->queryelevation->getForceElevation());
}
public function testSetAndGetExclusive()
{
$this->queryelevation->setExclusive(true);
$this->assertTrue($this->queryelevation->getExclusive());
}
public function testSetMarkExcludesTrue()
{
$this->queryelevation->removeTransformer('[excluded]');
$this->queryelevation->setMarkExcludes(true);
$this->assertTrue($this->queryelevation->getMarkExcludes());
$this->assertContains('[excluded]', $this->queryelevation->getTransformers());
}
public function testSetMarkExcludesTrueAsString()
{
$this->queryelevation->removeTransformer('[excluded]');
$this->queryelevation->setMarkExcludes('true');
$this->assertSame('true', $this->queryelevation->getMarkExcludes());
$this->assertContains('[excluded]', $this->queryelevation->getTransformers());
}
public function testSetMarkExcludesFalse()
{
$this->queryelevation->addTransformer('[excluded]');
$this->queryelevation->setMarkExcludes(false);
$this->assertFalse($this->queryelevation->getMarkExcludes());
$this->assertNotContains('[excluded]', $this->queryelevation->getTransformers());
}
public function testSetMarkExcludesFalseAsString()
{
$this->queryelevation->addTransformer('[excluded]');
$this->queryelevation->setMarkExcludes('false');
$this->assertSame('false', $this->queryelevation->getMarkExcludes());
$this->assertNotContains('[excluded]', $this->queryelevation->getTransformers());
}
public function testSetMarkExcludesNull()
{
$this->queryelevation->addTransformer('[excluded]');
$this->queryelevation->setMarkExcludes(null);
$this->assertNull($this->queryelevation->getMarkExcludes());
$this->assertNotContains('[excluded]', $this->queryelevation->getTransformers());
}
public function testSetAndGetElevateIds()
{
$ids = ['doc1', 'doc2'];
$this->queryelevation->setElevateIds($ids);
$this->assertSame($ids, $this->queryelevation->getElevateIds());
}
public function testSetElevateIdsAsStringWithTrim()
{
$this->queryelevation->setElevateIds('doc1, doc2');
$this->assertSame(['doc1', 'doc2'], $this->queryelevation->getElevateIds());
}
public function testSetAndGetExcludeIds()
{
$ids = ['doc3', 'doc4'];
$this->queryelevation->setExcludeIds($ids);
$this->assertSame($ids, $this->queryelevation->getExcludeIds());
}
public function testSetExcludeIdsAsStringWithTrim()
{
$this->queryelevation->setExcludeIds('doc3, doc4');
$this->assertSame(['doc3', 'doc4'], $this->queryelevation->getExcludeIds());
}
}
<?php
namespace Solarium\Tests\Component\RequestBuilder;
use PHPUnit\Framework\TestCase;
use Solarium\Component\QueryElevation as Component;
use Solarium\Component\RequestBuilder\QueryElevation as RequestBuilder;
use Solarium\Core\Client\Request;
class QueryElevationTest extends TestCase
{
public function testBuildComponent()
{
$builder = new RequestBuilder();
$request = new Request();
$component = new Component();
$component->setEnableElevation(false);
$component->setForceElevation(true);
$component->setExclusive(true);
$component->setMarkExcludes(true);
$component->setElevateIds(['doc1', 'doc2']);
$component->setExcludeIds(['doc3', 'doc4']);
$request = $builder->buildComponent($component, $request);
$this->assertEquals(
[
'fl' => '[elevated],[excluded]',
'enableElevation' => 'false',
'forceElevation' => 'true',
'exclusive' => 'true',
'markExcludes' => 'true',
'elevateIds' => 'doc1,doc2',
'excludeIds' => 'doc3,doc4',
],
$request->getParams()
);
}
}
...@@ -178,6 +178,38 @@ abstract class AbstractTechproductsTest extends TestCase ...@@ -178,6 +178,38 @@ abstract class AbstractTechproductsTest extends TestCase
} }
} }
public function testQueryElevation()
{
$select = $this->client->createSelect();
// In the techproducts example, the request handler "select" doesn't contain a query elevation component.
// But the "elevate" request handler does.
$select->setHandler('elevate');
$select->setQuery('electronics');
$select->setSorts(['id' => SelectQuery::SORT_ASC]);
$elevate = $select->getQueryElevation();
$elevate->setForceElevation(true);
$elevate->setElevateIds(['VS1GB400C3', 'VDBDB1A16']);
$elevate->setExcludeIds(['SP2514N', '6H500F0']);
$result = $this->client->select($select);
// The techproducts example contains 14 'electronics', 2 of them are excluded.
$this->assertSame(12, $result->getNumFound());
// The first two results are elevated and ignore the sort order.
$iterator = $result->getIterator();
$document = $iterator->current();
$this->assertSame('VS1GB400C3', $document->id);
$this->assertTrue($document->{'[elevated]'});
$iterator->next();
$document = $iterator->current();
$this->assertSame('VDBDB1A16', $document->id);
$this->assertTrue($document->{'[elevated]'});
// Further results aren't elevated.
$iterator->next();
$document = $iterator->current();
$this->assertFalse($document->{'[elevated]'});
}
public function testSpatial() public function testSpatial()
{ {
$select = $this->client->createSelect(); $select = $this->client->createSelect();
......
...@@ -577,6 +577,16 @@ abstract class AbstractQueryTest extends TestCase ...@@ -577,6 +577,16 @@ abstract class AbstractQueryTest extends TestCase
); );
} }
public function testGetQueryElevation()
{
$queryelevation = $this->query->getQueryElevation();
$this->assertSame(
'Solarium\Component\QueryElevation',
get_class($queryelevation)
);
}
public function testRegisterComponentType() public function testRegisterComponentType()
{ {
$components = $this->query->getComponentTypes(); $components = $this->query->getComponentTypes();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment