Skip to content

Commit

Permalink
WIP pattern matcher support
Browse files Browse the repository at this point in the history
  • Loading branch information
Dalibor Karlović committed Oct 24, 2017
1 parent 2ed80f2 commit a1d74a3
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 70 deletions.
201 changes: 137 additions & 64 deletions src/QueryConditionGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,22 @@ final class QueryConditionGenerator
public const CONDITION_AND = 'must';
public const CONDITION_OR = 'should';

private const COMPARE_OPR_TYPE = ['>=' => 'gte', '<=' => 'lte', '<' => 'lt', '>' => 'gt'];
// Elasticsearch comparison operators
public const COMPARISON_LESS = 'lt';
public const COMPARISON_LESS_OR_EQUAL = 'lte';
public const COMPARISON_GREATER = 'gt';
public const COMPARISON_GREATER_OR_EQUAL = 'gte';

// note: this one is NOT available for Elasticsearch, we use it as a named constant only
private const COMPARISON_UNEQUAL = '<>';

private const COMPARE_OPR_TYPE = [
'<>' => self::COMPARISON_UNEQUAL,
'<' => self::COMPARISON_LESS,
'<=' => self::COMPARISON_LESS_OR_EQUAL,
'>' => self::COMPARISON_GREATER,
'>=' => self::COMPARISON_GREATER_OR_EQUAL,
];

private $searchCondition;
private $fieldSet;
Expand Down Expand Up @@ -119,9 +134,12 @@ public function getMappings(): array
*/
public static function generateRangeParams(Range $range): array
{
$lowerCondition = $range->isLowerInclusive() ? self::COMPARISON_GREATER_OR_EQUAL : self::COMPARISON_GREATER;
$upperCondition = $range->isUpperInclusive() ? self::COMPARISON_LESS_OR_EQUAL : self::COMPARISON_LESS;

return [
$range->isLowerInclusive() ? 'gte' : 'gt' => $range->getLower(),
$range->isUpperInclusive() ? 'lte' : 'lt' => $range->getUpper(),
$lowerCondition => $range->getLower(),
$upperCondition => $range->getUpper(),
];
}

Expand Down Expand Up @@ -178,20 +196,27 @@ private function processGroup(ValuesGroup $group): array
}
}

/** @var Compare $compare */
foreach ($valuesBag->get(Compare::class) as $compare) {
if ('<>' === ($operator = $compare->getOperator())) {
$bool[self::CONDITION_NOT][][self::QUERY_TERM] = [$propertyName => [self::QUERY_VALUE => $compare->getValue()]];
} else {
$bool[$includingType][] = [
$propertyName => [
self::COMPARE_OPR_TYPE[$operator] => $compare->getValue(),
],
];
// comparison
if ($valuesBag->has(Compare::class)) {
/** @var Compare $compare */
foreach ($valuesBag->get(Compare::class) as $compare) {
$compare = $this->convertCompareValue($compare, $valueConverter);
$hints->context = QueryPreparationHints::CONTEXT_COMPARISON;
$localIncludingType = self::COMPARISON_UNEQUAL === $compare->getOperator() ? self::CONDITION_NOT : $includingType;
$bool[$localIncludingType][] = $this->prepareQuery($propertyName, $compare, $hints, $queryConverter, $nested);
}
}

$this->processPatternMatchers($valuesBag->get(PatternMatch::class), $propertyName, $bool, $includingType);
// matchers
if ($valuesBag->has(PatternMatch::class)) {
/** @var PatternMatch $patternMatch */
foreach ($valuesBag->get(PatternMatch::class) as $patternMatch) {
$patternMatch = $this->convertMatcherValue($patternMatch, $valueConverter);
$hints->context = QueryPreparationHints::CONTEXT_PATTERN_MATCH;
$localIncludingType = $patternMatch->isExclusive() ? self::CONDITION_NOT : $includingType;
$bool[$localIncludingType][] = $this->prepareQuery($propertyName, $patternMatch, $hints, $queryConverter, $nested);
}
}
}

foreach ($group->getGroups() as $subGroup) {
Expand All @@ -209,52 +234,6 @@ private function processGroup(ValuesGroup $group): array
return [self::QUERY_BOOL => $bool];
}

private function processPatternMatchers(array $values, string $propertyName, array &$bool, string $includingType)
{
// Note. Elasticsearch supports case-insensitive only at index level.

/** @var PatternMatch $patternMatch */
foreach ($values as $patternMatch) {
$value = [];

switch ($patternMatch->getType()) {
// Faster then Wildcard but less accurate.
// XXX Allow to configure `fuzzy`, `operator`, `zero_terms_query` and `cutoff_frequency` (TextType).
case PatternMatch::PATTERN_CONTAINS:
case PatternMatch::PATTERN_NOT_CONTAINS:
$value[self::QUERY_MATCH] = [$propertyName => [self::QUERY => $patternMatch->getValue()]];
break;

case PatternMatch::PATTERN_STARTS_WITH:
case PatternMatch::PATTERN_NOT_STARTS_WITH:
$value[self::QUERY_PREFIX] = [$propertyName => [self::QUERY_VALUE => $patternMatch->getValue()]];
break;

case PatternMatch::PATTERN_ENDS_WITH:
case PatternMatch::PATTERN_NOT_ENDS_WITH:
$value[self::QUERY_WILDCARD] = [
$propertyName => [self::QUERY_VALUE => '?'.addcslashes($patternMatch->getValue(), '?*')],
];
break;

case PatternMatch::PATTERN_EQUALS:
case PatternMatch::PATTERN_NOT_EQUALS:
$value[self::QUERY_TERM] = [$propertyName => [self::QUERY_VALUE => $patternMatch->getValue()]];
break;

default:
$message = sprintf('Not supported PatternMatch type "%s"', $patternMatch->getType());
throw new BadMethodCallException($message);
}

if ($patternMatch->isExclusive()) {
$bool[self::CONDITION_NOT][] = $value;
} else {
$bool[$includingType][] = $value;
}
}
}

/**
* @param mixed $value
* @param null|ValueConversion $converter
Expand Down Expand Up @@ -286,13 +265,46 @@ private function convertRangeValues(Range $range, ?ValueConversion $converter):
);
}

/**
* @param Compare $compare
* @param ValueConversion $converter
*
* @return Compare
*/
private function convertCompareValue(Compare $compare, ?ValueConversion $converter): Compare
{
return new Compare(
$this->convertValue($compare->getValue(), $converter),
$compare->getOperator()
);
}

/**
* @param PatternMatch $patternMatch
* @param ValueConversion $converter
*
* @throws \InvalidArgumentException
*
* @return PatternMatch
*/
private function convertMatcherValue(PatternMatch $patternMatch, ?ValueConversion $converter): PatternMatch
{
return new PatternMatch(
$this->convertValue($patternMatch->getValue(), $converter),
$patternMatch->getType(),
$patternMatch->isCaseInsensitive()
);
}

/**
* @param string $propertyName
* @param mixed $value
* @param QueryPreparationHints $hints
* @param null|QueryConversion $converter
* @param array|bool $nested
*
* @throws \Rollerworks\Component\Search\Exception\BadMethodCallException
*
* @return array
*/
private function prepareQuery(string $propertyName, $value, QueryPreparationHints $hints, ?QueryConversion $converter, $nested): array
Expand All @@ -313,6 +325,25 @@ private function prepareQuery(string $propertyName, $value, QueryPreparationHint
];
}
break;
case QueryPreparationHints::CONTEXT_COMPARISON:
/** @var Compare $value */
$operator = self::COMPARE_OPR_TYPE[$value->getOperator()];
$query = [
$propertyName => [$operator => $value->getValue()],
];

if (self::COMPARISON_UNEQUAL === $value->getOperator()) {
$query = [
self::QUERY_TERM => [
$propertyName => [self::QUERY_VALUE => $value->getValue()],
],
];
}
break;
case QueryPreparationHints::CONTEXT_PATTERN_MATCH:
/** @var PatternMatch $value */
$query = $this->preparePatternMatch($propertyName, $value);
break;
default:
case QueryPreparationHints::CONTEXT_SIMPLE_VALUES:
case QueryPreparationHints::CONTEXT_EXCLUDED_SIMPLE_VALUES:
Expand All @@ -327,16 +358,58 @@ private function prepareQuery(string $propertyName, $value, QueryPreparationHint

if ($nested) {
while (false !== $nested) {
$path = $nested['path'];
$query = [
self::QUERY_NESTED => [
'path' => $nested['path'],
'query' => $query,
],
self::QUERY_NESTED => compact('path', 'query'),
];
$nested = $nested['nested'];
}
}

return $query;
}

/**
* @param string $propertyName
* @param PatternMatch $patternMatch
*
* @throws \Rollerworks\Component\Search\Exception\BadMethodCallException
*
* @return array
*/
private function preparePatternMatch(string $propertyName, PatternMatch $patternMatch): array
{
$query = [];
switch ($patternMatch->getType()) {
// Faster then Wildcard but less accurate.
// XXX Allow to configure `fuzzy`, `operator`, `zero_terms_query` and `cutoff_frequency` (TextType).
case PatternMatch::PATTERN_CONTAINS:
case PatternMatch::PATTERN_NOT_CONTAINS:
$query[self::QUERY_MATCH] = [$propertyName => [self::QUERY => $patternMatch->getValue()]];
break;

case PatternMatch::PATTERN_STARTS_WITH:
case PatternMatch::PATTERN_NOT_STARTS_WITH:
$query[self::QUERY_PREFIX] = [$propertyName => [self::QUERY_VALUE => $patternMatch->getValue()]];
break;

case PatternMatch::PATTERN_ENDS_WITH:
case PatternMatch::PATTERN_NOT_ENDS_WITH:
$query[self::QUERY_WILDCARD] = [
$propertyName => [self::QUERY_VALUE => '?'.addcslashes($patternMatch->getValue(), '?*')],
];
break;

case PatternMatch::PATTERN_EQUALS:
case PatternMatch::PATTERN_NOT_EQUALS:
$query[self::QUERY_TERM] = [$propertyName => [self::QUERY_VALUE => $patternMatch->getValue()]];
break;

default:
$message = sprintf('Not supported PatternMatch type "%s"', $patternMatch->getType());
throw new BadMethodCallException($message);
}

return $query;
}
}
4 changes: 3 additions & 1 deletion src/QueryPreparationHints.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ class QueryPreparationHints
public const CONTEXT_EXCLUDED_SIMPLE_VALUES = 'EXCLUDED_SIMPLE_VALUES';
public const CONTEXT_RANGE_VALUES = 'RANGE_VALUES';
public const CONTEXT_EXCLUDED_RANGE_VALUES = 'EXCLUDED_RANGE_VALUES';
public const CONTEXT_COMPARISON = 'COMPARISON';
public const CONTEXT_PATTERN_MATCH = 'PATTERN_MATCH';

/** @var bool */
public $identifier = false;

/**
* @var string One of ConversionHints::CONTEXT_* constants
* @var string Preparation context, one of ConversionHints::CONTEXT_* constants
*/
public $context;
}
9 changes: 4 additions & 5 deletions tests/Functional/ConditionGeneratorResultsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,10 @@ public function it_finds_by_status_and_label_or_quantity_limited_by_price()
*/
public function it_finds_by_excluding_equals_pattern()
{
$this->markTestSkipped('nested query support');
$this->makeTest('row-label: ~=Armor, ~=sword;', [2]); // Invoice 3 doesn't match as "sword" is lowercase
$this->makeTest('row-price: "15.00"; row-label: ~!=Sword;', [5]);
// Lowercase
$this->makeTest('row-label: ~=Armor, ~i=sword;', [2, 3]);
// note: everything is case-sensitive by default, must use lowercase here
// TODO: this throws an exception for me from tests, but works if I run the query directly (?!)
// $this->makeTest('row-label: ~=armor, ~=sword;', [2]);
$this->makeTest('row-price: "15.00"; row-label: ~!=sword;', [5]);
}

/**
Expand Down

0 comments on commit a1d74a3

Please sign in to comment.