Skip to content

Commit

Permalink
Merge pull request #5 from lmc-eu/feature/improve-edismax
Browse files Browse the repository at this point in the history
Feature/improve edismax
  • Loading branch information
MortalFlesh authored Mar 30, 2022
2 parents 70c7a54 + e9a39c8 commit 12d3264
Show file tree
Hide file tree
Showing 16 changed files with 410 additions and 78 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
<!-- There should always be "Unreleased" section at the beginning. -->

## Unreleased
- Allow setting an edismax as local parameters in query
- [**BC**] Add method `useEDisMaxGlobally` to `FulltextInterface`
- Set `phraseBigramFields`, `boostQuery`, `phraseSlop` only when `eDisMax` is enabled

## 1.3.0 - 2022-03-29
- Use `*` as a placeholder for all fields in `EntityApplicator`
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ class PersonSearch implements FulltextInterface
return true;
}

public function useEDisMaxGlobally(): bool
{
return true;
}

// Note: there are more methods, you need to implement, but we want to keep this example simple as possible. If you don't need other functionality, simply return null or empty variant from a method.
}

Expand Down
48 changes: 43 additions & 5 deletions src/QueryBuilder/Applicator/FulltextApplicator.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,21 @@ public function applyOnQuery(Query $query): void
->setDefaults();

if ($this->entity->isEDisMaxEnabled()) {
$this->setEdisMax();
if ($this->entity->useEDisMaxGlobally()) {
$this->setGlobalEdisMax();
} else {
$this->setLocalEdisMax();
}
}
}

private function addKeywords(): self
{
$keywords = $this->entity->getKeywords();
if ($this->entity->isEDisMaxEnabled() && !$this->entity->useEDisMaxGlobally()) {
return $this;
}

if (!empty($keywords)) {
if (!empty($keywords = $this->entity->getKeywords())) {
$searchQuery = implode(' ', $keywords);

$this->query->setQuery($searchQuery);
Expand All @@ -52,12 +58,14 @@ private function addKeywords(): self

private function setDefaults(): self
{
$this->query->setQueryDefaultOperator($this->entity->getDefaultQueryOperator());
if (!empty($defaultOperator = $this->entity->getDefaultQueryOperator())) {
$this->query->setQueryDefaultOperator($defaultOperator);
}

return $this;
}

private function setEdisMax(): self
private function setGlobalEdisMax(): self
{
$edismax = $this->query->getEDisMax();
$edismax
Expand All @@ -72,4 +80,34 @@ private function setEdisMax(): self

return $this;
}

private function setLocalEdisMax(): self
{
$this->query->setQuery('');

$localParameters = $this->query->getLocalParameters();
$localParameters->setType('edismax');

if (!empty($keyWords = $this->entity->getKeywords())) {
$localParameters->setLocalValue('$userQuery');
$this->query->addParam('userQuery', implode(' ', $keyWords));
}

if (!empty($queryFields = $this->entity->getQueryFields())) {
$localParameters->setQueryField('$queryFields');
$this->query->addParam('queryFields', implode(' ', $queryFields));
}

if (!empty($phraseFields = $this->entity->getPhraseFields())) {
$localParameters['pf'] = 'pf=$phraseFields';
$this->query->addParam('phraseFields', implode(' ', $phraseFields));
}

$localParameters['tie'] = 'tie=' . $this->entity->getTie();
$localParameters['mm'] = 'mm=' . $this->entity->getMinimumMatch();

$this->query->addParam('q.alt', $this->entity->getQueryAlternative());

return $this;
}
}
13 changes: 10 additions & 3 deletions src/QueryBuilder/Applicator/FulltextBigramApplicator.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,17 @@ public function setEntity(EntityInterface $entity): void

public function applyOnQuery(Query $query): void
{
$phraseBigramFields = $this->entity->getPhraseBigramFields();
if (!$this->entity->isEDisMaxEnabled()) {
return;
}

if (!empty($phraseBigramFields)) {
$query->getEDisMax()->setPhraseBigramFields(implode(' ', $phraseBigramFields));
if (!empty($phraseBigramFields = $this->entity->getPhraseBigramFields())) {
if ($this->entity->useEDisMaxGlobally()) {
$query->getEDisMax()->setPhraseBigramFields(implode(' ', $phraseBigramFields));
} else {
$query->getLocalParameters()->offsetSet('pf2', 'pf2=$phraseBigramFields');
$query->addParam('phraseBigramFields', implode(' ', $phraseBigramFields));
}
}
}
}
28 changes: 24 additions & 4 deletions src/QueryBuilder/Applicator/FulltextBoostApplicator.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,35 @@ public function setEntity(EntityInterface $entity): void

public function applyOnQuery(Query $query): void
{
if (!$this->entity->isEDisMaxEnabled()) {
return;
}

$boostQuery = $this->entity->getBoostQuery();
$phraseSlop = $this->entity->getPhraseSlop();

if (!empty($boostQuery)) {
$query->getEDisMax()->setBoostQuery($boostQuery);
if (empty($boostQuery) && $phraseSlop === null) {
return;
}

if ($phraseSlop !== null) {
$query->getEDisMax()->setPhraseSlop($phraseSlop);
if ($this->entity->useEDisMaxGlobally()) {
if (!empty($boostQuery)) {
$query->getEDisMax()->setBoostQuery($boostQuery);
}

if ($phraseSlop !== null) {
$query->getEDisMax()->setPhraseSlop($phraseSlop);
}
} else {
if (!empty($boostQuery)) {
$query->getLocalParameters()->offsetSet('bq', 'bq=$boostQuery');
$query->addParam('boostQuery', $this->entity->getBoostQuery());
}

if ($phraseSlop !== null) {
$query->getLocalParameters()->offsetSet('ps', 'ps=$phraseSlop');
$query->addParam('phraseSlop', $this->entity->getPhraseSlop());
}
}
}
}
12 changes: 12 additions & 0 deletions src/QueryBuilder/EntityInterface/FulltextInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ interface FulltextInterface extends EntityInterface
{
public function getKeywords(): array;

/**
* TIP: If you are using EDisMax, use getMinimumMatch instead
* @see isEDisMaxEnabled()
* @see getMinimumMatch()
*/
public function getDefaultQueryOperator(): string;

public function getQueryFields(): array;
Expand All @@ -14,9 +19,16 @@ public function getPhraseFields(): array;

public function getQueryAlternative(): string;

/**
* TIP: You can use minimum match as a default Query Operator alternative:
* - OR -> mm=1
* - AND -> mm=100%
*/
public function getMinimumMatch(): ?string;

public function getTie(): float;

public function isEDisMaxEnabled(): bool;

public function useEDisMaxGlobally(): bool;
}
76 changes: 76 additions & 0 deletions tests/Fixture/FulltextApplicatorTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php declare(strict_types=1);

namespace Lmc\Cqrs\Solr\Fixture;

use Lmc\Cqrs\Solr\QueryBuilder\EntityInterface\FulltextInterface;

trait FulltextApplicatorTrait
{
public function provideGlobalEdismax(): array
{
return [
// isGlobal
'global' => [true],
'local' => [false],
];
}

private function assertApplyFulltextOnQuery(FulltextInterface $entity, string $queryUri): void
{
$parameters = $this->parseQueryUriParameters($queryUri);

$rows = $this->assertParameterExists('rows', $parameters);
$this->assertSame((string) $entity->getNumberOfRows(), $rows);

$queryOperator = $this->assertParameterExists('q.op', $parameters);
$this->assertSame($queryOperator, $entity->getDefaultQueryOperator());

$queryAlternative = $this->assertParameterExists('q.alt', $parameters);
$this->assertSame($entity->getQueryAlternative(), $queryAlternative);

if ($entity->useEDisMaxGlobally()) {
$query = $this->assertParameterExists('q', $parameters);
$this->assertSame(implode(' ', $entity->getKeywords()), $query);

$defType = $this->assertParameterExists('defType', $parameters);
$this->assertSame('edismax', $defType);

$queryFields = $this->assertParameterExists('qf', $parameters);
$this->assertSame(implode(' ', $entity->getQueryFields()), $queryFields);

$phraseFields = $this->assertParameterExists('pf', $parameters);
$this->assertStringContainsString(implode(' ', $entity->getPhraseFields()), $phraseFields);
} else {
$query = $this->assertParameterExists('q', $parameters);
$this->assertStringStartsWith('{!type=edismax', $query);
$this->assertStringEndsWith('}', $query);
$this->assertStringContainsString('v=$userQuery', $query);
$this->assertStringContainsString('mm=' . $entity->getMinimumMatch(), $query);
$this->assertStringContainsString('tie=' . $entity->getTie(), $query);
$this->assertStringContainsString('qf=$queryFields', $query);
$this->assertStringContainsString('pf=$phraseFields', $query);

$this->assertArrayNotHasKey('defType', $parameters);

$queryFields = $this->assertParameterExists('queryFields', $parameters);
$this->assertSame(implode(' ', $entity->getQueryFields()), $queryFields);

$phraseFields = $this->assertParameterExists('phraseFields', $parameters);
$this->assertSame(implode(' ', $entity->getPhraseFields()), $phraseFields);
}
}

private function assertApplyFulltextOnQueryWithoutEdisMax(FulltextInterface $entity, string $queryUri): void
{
$this->assertStringContainsString('rows=' . $entity->getNumberOfRows(), $queryUri);
$this->assertStringContainsString('q=' . implode(' ', $entity->getKeywords()), $queryUri);
$this->assertStringContainsString('q.op=' . $entity->getDefaultQueryOperator(), $queryUri);

$this->assertStringNotContainsString('defType=edismax', $queryUri);
$this->assertStringNotContainsString('qf=' . implode(' ', $entity->getQueryFields()), $queryUri);
$this->assertStringNotContainsString('pf=' . implode(' ', $entity->getPhraseFields()), $queryUri);
$this->assertStringNotContainsString('q.alt=' . $entity->getQueryAlternative(), $queryUri);
$this->assertStringNotContainsString('tie=' . $entity->getTie(), $queryUri);
$this->assertStringNotContainsString('mm=' . $entity->getMinimumMatch(), $queryUri);
}
}
29 changes: 29 additions & 0 deletions tests/QueryBuilder/Applicator/ApplicatorTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,33 @@ protected function getCustomQueryUri(array $applicators = []): string

return urldecode($query->__toString());
}

protected function parseQueryUriParameters(string $queryUri): array
{
$parameters = [];
$pairs = explode('&', (string) parse_url($queryUri, PHP_URL_QUERY));

foreach ($pairs as $pair) {
[$key, $value] = explode('=', $pair, 2);

$parameters[$key] = $value;
}

return $parameters;
}

protected function assertParameterExists(string $expectedKey, array $parameters): string
{
$this->assertArrayHasKey(
$expectedKey,
$parameters,
sprintf(
'Parameter "%s" does not exists in [%s].',
$expectedKey,
implode(', ', array_keys($parameters))
)
);

return $parameters[$expectedKey];
}
}
35 changes: 11 additions & 24 deletions tests/QueryBuilder/Applicator/FulltextApplicatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

namespace Lmc\Cqrs\Solr\QueryBuilder\Applicator;

use Lmc\Cqrs\Solr\Fixture\FulltextApplicatorTrait;
use Lmc\Cqrs\Solr\QueryBuilder\Fixture\DisabledEDisMaxDummyEntity;
use Lmc\Cqrs\Solr\QueryBuilder\Fixture\FulltextDummyEntity;

class FulltextApplicatorTest extends ApplicatorTestCase
{
use FulltextApplicatorTrait;

private FulltextApplicator $fulltextApplicator;

protected function setUp(): void
Expand All @@ -16,10 +19,11 @@ protected function setUp(): void

/**
* @test
* @dataProvider provideGlobalEdismax
*/
public function shouldApplyFulltextOnQuery(): void
public function shouldApplyFulltextOnQuery(bool $isGlobalEdismax): void
{
$entity = new FulltextDummyEntity();
$entity = new FulltextDummyEntity($isGlobalEdismax);
$this->assertTrue($this->fulltextApplicator->supportEntity($entity));
$this->fulltextApplicator->setEntity($entity);

Expand All @@ -28,24 +32,16 @@ public function shouldApplyFulltextOnQuery(): void
$this->fulltextApplicator,
]);

$this->assertStringContainsString('rows=' . $entity->getNumberOfRows(), $queryUri);

$this->assertStringContainsString('q=' . implode(' ', $entity->getKeywords()), $queryUri);
$this->assertStringContainsString('q.op=' . $entity->getDefaultQueryOperator(), $queryUri);
$this->assertStringContainsString('defType=edismax', $queryUri);
$this->assertStringContainsString('qf=' . implode(' ', $entity->getQueryFields()), $queryUri);
$this->assertStringContainsString('pf=' . implode(' ', $entity->getPhraseFields()), $queryUri);
$this->assertStringContainsString('q.alt=' . $entity->getQueryAlternative(), $queryUri);
$this->assertStringContainsString('tie=' . $entity->getTie(), $queryUri);
$this->assertStringContainsString('mm=' . $entity->getMinimumMatch(), $queryUri);
$this->assertApplyFulltextOnQuery($entity, $queryUri);
}

/**
* @test
* @dataProvider provideGlobalEdismax
*/
public function shouldApplyFulltextOnQueryWithDisabledEDisMax(): void
public function shouldApplyFulltextOnQueryWithDisabledEDisMax(bool $isGlobalEdismax): void
{
$entity = new DisabledEDisMaxDummyEntity();
$entity = new DisabledEDisMaxDummyEntity($isGlobalEdismax);
$this->assertTrue($this->fulltextApplicator->supportEntity($entity));
$this->fulltextApplicator->setEntity($entity);

Expand All @@ -54,15 +50,6 @@ public function shouldApplyFulltextOnQueryWithDisabledEDisMax(): void
$this->fulltextApplicator,
]);

$this->assertStringContainsString('rows=' . $entity->getNumberOfRows(), $queryUri);
$this->assertStringContainsString('q=' . implode(' ', $entity->getKeywords()), $queryUri);
$this->assertStringContainsString('q.op=' . $entity->getDefaultQueryOperator(), $queryUri);

$this->assertStringNotContainsString('defType=edismax', $queryUri);
$this->assertStringNotContainsString('qf=' . implode(' ', $entity->getQueryFields()), $queryUri);
$this->assertStringNotContainsString('pf=' . implode(' ', $entity->getPhraseFields()), $queryUri);
$this->assertStringNotContainsString('q.alt=' . $entity->getQueryAlternative(), $queryUri);
$this->assertStringNotContainsString('tie=' . $entity->getTie(), $queryUri);
$this->assertStringNotContainsString('mm=' . $entity->getMinimumMatch(), $queryUri);
$this->assertApplyFulltextOnQueryWithoutEdisMax($entity, $queryUri);
}
}
Loading

0 comments on commit 12d3264

Please sign in to comment.