Skip to content

Commit

Permalink
Build Elasticsearch queries
Browse files Browse the repository at this point in the history
  • Loading branch information
malberts committed Jan 28, 2025
1 parent f63fecf commit bdeefa2
Show file tree
Hide file tree
Showing 14 changed files with 645 additions and 18 deletions.
18 changes: 15 additions & 3 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,19 @@ parameters:
path: src/Persistence/ConfigDeserializer.php

-
message: '#^Cannot cast mixed to string\.$#'
identifier: cast.string
message: '#^Parameter \#2 \$terms of class Elastica\\Query\\Terms constructor expects list\<bool\|float\|int\|string\>, array\<string\> given\.$#'
identifier: argument.type
count: 1
path: src/Persistence/Search/Query/ItemTypeQueryBuilder.php

-
message: '#^Parameter \#2 \$terms of class Elastica\\Query\\Terms constructor expects list\<bool\|float\|int\|string\>, array given\.$#'
identifier: argument.type
count: 1
path: src/Persistence/Search/Query/ListFacetQueryBuilder.php

-
message: '#^Parameter \#2 \$terms of class Elastica\\Query\\Terms constructor expects list\<bool\|float\|int\|string\>, array\<float\> given\.$#'
identifier: argument.type
count: 1
path: src/WikibaseFacetedSearchExtension.php
path: src/Persistence/Search/Query/ListFacetQueryBuilder.php
1 change: 1 addition & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ parameters:
- src/Persistence/Search/SearchIndexFieldsBuilder.php
- src/Persistence/Search/Query/HasWbFacetFeature.php
- src/Persistence/Elastic*.php
- src/WikibaseFacetedSearchExtension.php
reportUnmatchedIgnoredErrors: false
ignoreErrors:
- '#no value type specified in iterable type array\.$#'
4 changes: 3 additions & 1 deletion src/EntryPoints/WikibaseFacetedSearchHooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,9 @@ private static function getConfigPageHtml( string $html ): string {
* @param KeywordFeature[] &$extraFeatures
*/
public static function onCirrusSearchAddQueryFeatures( SearchConfig $config, array &$extraFeatures ): void {
$extraFeatures[] = new HasWbFacetFeature();
if ( WikibaseFacetedSearchExtension::getInstance()->getConfig()->isComplete() ) {
$extraFeatures[] = WikibaseFacetedSearchExtension::getInstance()->newHasWbFacetFeature();
}
}

/**
Expand Down
31 changes: 31 additions & 0 deletions src/Persistence/Search/Query/DelegatingFacetQueryBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare( strict_types = 1 );

namespace ProfessionalWiki\WikibaseFacetedSearch\Persistence\Search\Query;

use Elastica\Query\AbstractQuery;
use ProfessionalWiki\WikibaseFacetedSearch\Application\FacetConfig;
use ProfessionalWiki\WikibaseFacetedSearch\Application\FacetType;
use ProfessionalWiki\WikibaseFacetedSearch\Application\PropertyConstraints;

class DelegatingFacetQueryBuilder implements FacetQueryBuilder {

/**
* @var array<string, FacetQueryBuilder>
*/
private array $buildersPerType;

public function addBuilder( FacetType $type, FacetQueryBuilder $builder ): void {
$this->buildersPerType[$type->value] = $builder;
}

public function buildQuery( FacetConfig $config, PropertyConstraints $constraints ): ?AbstractQuery {
if ( !array_key_exists( $config->type->value, $this->buildersPerType ) ) {
throw new \RuntimeException( 'No query builder for facet type ' . $config->type->value );
}

return $this->buildersPerType[$config->type->value]->buildQuery( $config, $constraints );
}

}
15 changes: 15 additions & 0 deletions src/Persistence/Search/Query/FacetQueryBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare( strict_types = 1 );

namespace ProfessionalWiki\WikibaseFacetedSearch\Persistence\Search\Query;

use Elastica\Query\AbstractQuery;
use ProfessionalWiki\WikibaseFacetedSearch\Application\FacetConfig;
use ProfessionalWiki\WikibaseFacetedSearch\Application\PropertyConstraints;

interface FacetQueryBuilder {

public function buildQuery( FacetConfig $config, PropertyConstraints $constraints ): ?AbstractQuery;

}
61 changes: 49 additions & 12 deletions src/Persistence/Search/Query/HasWbFacetFeature.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,22 @@

namespace ProfessionalWiki\WikibaseFacetedSearch\Persistence\Search\Query;

use CirrusSearch\Parser\AST\KeywordFeatureNode;
use CirrusSearch\Query\Builder\QueryBuildingContext;
use CirrusSearch\Query\FilterQueryFeature;
use CirrusSearch\Query\SimpleKeywordFeature;
use CirrusSearch\Search\SearchContext;
use Elastica\Query\AbstractQuery;
use ProfessionalWiki\WikibaseFacetedSearch\Application\Config;
use ProfessionalWiki\WikibaseFacetedSearch\Application\PropertyConstraints;
use ProfessionalWiki\WikibaseFacetedSearch\Application\QueryStringParser;
use Wikibase\DataModel\Entity\ItemId;

class HasWbFacetFeature extends SimpleKeywordFeature implements FilterQueryFeature {
class HasWbFacetFeature extends SimpleKeywordFeature {

public function __construct(
private readonly Config $config,
private readonly QueryStringParser $queryStringParser,
private readonly ItemTypeQueryBuilder $itemTypeQueryBuilder,
private readonly DelegatingFacetQueryBuilder $facetQueryBuilder
) {
}

/**
* @return string[]
Expand All @@ -20,17 +28,46 @@ protected function getKeywords(): array {
return [ 'haswbfacet' ];
}

/**
* @return array
*/
protected function doApply( SearchContext $context, $key, $value, $quotedValue, $negated ): array {
// TODO
$itemTypes = $this->getItemTypes( $context->getOriginalSearchTerm() );

if ( $itemTypes === [] ) {
return [ null, false ];
}

$constraints = $this->getConstraintsForValue( $value );

if ( $constraints === null ) {
return [ $this->itemTypeQueryBuilder->buildQuery( $itemTypes ), false ];
}

foreach ( $itemTypes as $itemTypeId ) {
$facet = $this->config->getConfigForProperty( $itemTypeId, $constraints->propertyId );

if ( $facet === null ) {
continue;
}

return [ $this->facetQueryBuilder->buildQuery( $facet, $constraints ), false ];
}

return [ null, false ];
}

public function getFilterQuery( KeywordFeatureNode $node, QueryBuildingContext $context ): ?AbstractQuery {
// TODO
return null;
/**
* @return ItemId[]
*/
private function getItemTypes( string $originalQueryString ): array {
return $this->queryStringParser->parse( $originalQueryString )->getItemTypes();
}

/**
* @param string $value Keyword value for a single facet
*/
private function getConstraintsForValue( string $value ): ?PropertyConstraints {
$constraints = $this->queryStringParser->parse( "haswbfacet:$value" )->getConstraintsPerProperty();

return $constraints === [] ? null : reset( $constraints );
}

}
29 changes: 29 additions & 0 deletions src/Persistence/Search/Query/ItemTypeQueryBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare( strict_types = 1 );

namespace ProfessionalWiki\WikibaseFacetedSearch\Persistence\Search\Query;

use Elastica\Query;
use Elastica\Query\AbstractQuery;
use Wikibase\DataModel\Entity\ItemId;
use Wikibase\DataModel\Entity\PropertyId;

class ItemTypeQueryBuilder {

public function __construct(
private readonly PropertyId $itemTypeProperty
) {
}

public function buildQuery( array $itemTypes ): AbstractQuery {
return new Query\Terms(
'wbfs_' . $this->itemTypeProperty->getSerialization(),
array_map(
fn( ItemId $itemType ) => $itemType->getSerialization(),
$itemTypes
)
);
}

}
56 changes: 56 additions & 0 deletions src/Persistence/Search/Query/ListFacetQueryBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare( strict_types = 1 );

namespace ProfessionalWiki\WikibaseFacetedSearch\Persistence\Search\Query;

use Elastica\Query;
use Elastica\Query\AbstractQuery;
use ProfessionalWiki\WikibaseFacetedSearch\Application\FacetConfig;
use ProfessionalWiki\WikibaseFacetedSearch\Application\PropertyConstraints;
use Wikibase\DataModel\Services\Lookup\PropertyDataTypeLookup;

class ListFacetQueryBuilder implements FacetQueryBuilder {

public function __construct(
private readonly PropertyDataTypeLookup $dataTypeLookup
) {
}

public function buildQuery( FacetConfig $config, PropertyConstraints $constraints ): ?AbstractQuery {
$name = 'wbfs_' . $config->propertyId->getSerialization();

return match ( $this->dataTypeLookup->getDataTypeIdForProperty( $config->propertyId ) ) {
'string' => $this->buildStringQuery( $name, $constraints ),
'wikibase-item' => $this->buildStringQuery( $name, $constraints ),
'quantity' => $this->buildQuantityQuery( $name, $constraints ),
default => null
};
}

private function buildStringQuery( string $name, PropertyConstraints $constraints ): AbstractQuery {
return new Query\Terms(
$name,
$this->getFacetValues( $constraints )
);
}

private function getFacetValues( PropertyConstraints $constraints ): array {
if ( $constraints->getOrSelectedValues() !== [] ) {
return $constraints->getOrSelectedValues();
}

return $constraints->getAndSelectedValues();
}

private function buildQuantityQuery( string $name, PropertyConstraints $constraints ): AbstractQuery {
return new Query\Terms(
$name,
array_map(
fn( $value ) => (float)$value,
$this->getFacetValues( $constraints )
)
);
}

}
50 changes: 50 additions & 0 deletions src/Persistence/Search/Query/RangeFacetQueryBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare( strict_types = 1 );

namespace ProfessionalWiki\WikibaseFacetedSearch\Persistence\Search\Query;

use Elastica\Query;
use Elastica\Query\AbstractQuery;
use ProfessionalWiki\WikibaseFacetedSearch\Application\FacetConfig;
use ProfessionalWiki\WikibaseFacetedSearch\Application\PropertyConstraints;
use Wikibase\DataModel\Services\Lookup\PropertyDataTypeLookup;

class RangeFacetQueryBuilder implements FacetQueryBuilder {

public function __construct(
private readonly PropertyDataTypeLookup $dataTypeLookup
) {
}

public function buildQuery( FacetConfig $config, PropertyConstraints $constraints ): ?AbstractQuery {
$name = 'wbfs_' . $config->propertyId->getSerialization();

return match ( $this->dataTypeLookup->getDataTypeIdForProperty( $config->propertyId ) ) {
'quantity' => $this->buildQuantityQuery( $name, $constraints ),
'time' => $this->buildTimeQuery( $name, $constraints ),
default => null
};
}

private function buildQuantityQuery( string $name, PropertyConstraints $constraints ): AbstractQuery {
return new Query\Range(
$name,
[
'gte' => $constraints->getInclusiveMinimum(),
'lte' => $constraints->getInclusiveMaximum()
]
);
}

private function buildTimeQuery( string $name, PropertyConstraints $constraints ): AbstractQuery {
return new Query\Range(
$name,
[
'gte' => $constraints->getInclusiveMinimum() === null ? null : (int)$constraints->getInclusiveMinimum() . '-01-01',
'lte' => $constraints->getInclusiveMaximum() === null ? null : (int)$constraints->getInclusiveMaximum() . '-12-31',
]
);
}

}
46 changes: 45 additions & 1 deletion src/WikibaseFacetedSearchExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@
use ProfessionalWiki\WikibaseFacetedSearch\Persistence\PageContentConfigLookup;
use ProfessionalWiki\WikibaseFacetedSearch\Persistence\PageContentFetcher;
use ProfessionalWiki\WikibaseFacetedSearch\Persistence\PageItemLookupFactory;
use ProfessionalWiki\WikibaseFacetedSearch\Persistence\Search\Query\DelegatingFacetQueryBuilder;
use ProfessionalWiki\WikibaseFacetedSearch\Persistence\Search\Query\HasWbFacetFeature;
use ProfessionalWiki\WikibaseFacetedSearch\Persistence\Search\Query\ItemTypeQueryBuilder;
use ProfessionalWiki\WikibaseFacetedSearch\Persistence\Search\Query\ListFacetQueryBuilder;
use ProfessionalWiki\WikibaseFacetedSearch\Persistence\Search\Query\RangeFacetQueryBuilder;
use ProfessionalWiki\WikibaseFacetedSearch\Persistence\Search\SearchIndexFieldsBuilder;
use ProfessionalWiki\WikibaseFacetedSearch\Persistence\SitelinkBasedStatementsLookup;
use ProfessionalWiki\WikibaseFacetedSearch\Presentation\DelegatingFacetHtmlBuilder;
Expand All @@ -41,6 +46,7 @@
use TemplateParser;
use Title;
use Wikibase\DataModel\Services\Lookup\LabelLookup;
use Wikibase\DataModel\Services\Lookup\PropertyDataTypeLookup;
use Wikibase\Lib\Store\SiteLinkStore;
use Wikibase\Repo\WikibaseRepo;

Expand Down Expand Up @@ -126,10 +132,14 @@ public function newSearchIndexFieldsBuilder( CirrusSearch $engine ): SearchIndex
return new SearchIndexFieldsBuilder(
engine: $engine,
config: $this->getConfig(),
dataTypeLookup: WikibaseRepo::getPropertyDataTypeLookup()
dataTypeLookup: $this->getPropertyDataTypeLookup()
);
}

private function getPropertyDataTypeLookup(): PropertyDataTypeLookup {
return WikibaseRepo::getPropertyDataTypeLookup();
}

public function newStatementsLookup(): StatementsLookup {
if ( $this->getConfig()->linkTargetSitelinkSiteId === null ) {
return new FromPageStatementsLookup();
Expand Down Expand Up @@ -253,4 +263,38 @@ public function getLabelLookup( Language $language ): LabelLookup {
return WikibaseRepo::getFallbackLabelDescriptionLookupFactory()->newLabelDescriptionLookup( $language );
}

public function newHasWbFacetFeature(): HasWbFacetFeature {
return new HasWbFacetFeature(
config: $this->getConfig(),
queryStringParser: $this->getQueryStringParser(),
itemTypeQueryBuilder: $this->getItemTypeQueryBuilder(),
facetQueryBuilder: $this->getFacetQueryBuilder()
);
}

private function getItemTypeQueryBuilder(): ItemTypeQueryBuilder {
return new ItemTypeQueryBuilder(
itemTypeProperty: $this->getConfig()->getItemTypeProperty()
);
}

private function getFacetQueryBuilder(): DelegatingFacetQueryBuilder {
$delegator = new DelegatingFacetQueryBuilder();
$delegator->addBuilder( FacetType::LIST, $this->newListFacetQueryBuilder() );
$delegator->addBuilder( FacetType::RANGE, $this->newRangeFacetQueryBuilder() );
return $delegator;
}

private function newListFacetQueryBuilder(): ListFacetQueryBuilder {
return new ListFacetQueryBuilder(
dataTypeLookup: $this->getPropertyDataTypeLookup()
);
}

private function newRangeFacetQueryBuilder(): RangeFacetQueryBuilder {
return new RangeFacetQueryBuilder(
dataTypeLookup: $this->getPropertyDataTypeLookup()
);
}

}
Loading

0 comments on commit bdeefa2

Please sign in to comment.