From ed0d7e6f97389df4d6e3004562ac61785196ef34 Mon Sep 17 00:00:00 2001 From: Colin <144122160+colinrobinsonuib@users.noreply.github.com> Date: Mon, 8 Dec 2025 12:25:17 +0100 Subject: [PATCH 1/3] Create ApiWikibaseFacetedSearch.php --- src/EntryPoints/ApiWikibaseFacetedSearch.php | 131 +++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 src/EntryPoints/ApiWikibaseFacetedSearch.php diff --git a/src/EntryPoints/ApiWikibaseFacetedSearch.php b/src/EntryPoints/ApiWikibaseFacetedSearch.php new file mode 100644 index 0000000..73081d7 --- /dev/null +++ b/src/EntryPoints/ApiWikibaseFacetedSearch.php @@ -0,0 +1,131 @@ +extractRequestParams(); + $term = $params['search']; + $namespaces = $params['namespaces']; + + $facets = $this->getFacets($term, $namespaces); + + $this->getResult()->addValue(null, $this->getModuleName(), $facets); + } catch (\Throwable $e) { + $this->getResult()->addValue(null, 'error', [ + 'message' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]); + } + } + + private function getFacets(string $term, ?array $namespaces): array + { + $searchEngine = MediaWikiServices::getInstance()->newSearchEngine(); + // We limit to 0 because we only need the query structure, but CirrusSearch might need 1 or execution. + // However, ElasticValueCounter needs the original query. + // SearchEngine::searchText executes the query. + $searchEngine->setLimitOffset(1); + if ($namespaces) { + $searchEngine->setNamespaces($namespaces); + } + $results = $searchEngine->searchText($term); + + if ($results instanceof Status) { + if (!$results->isOK()) { + return []; + } + $results = $results->getValue(); + } + + if (!$results instanceof CirrusSearchResultSet) { + return []; + } + + $query = $results->getElasticaResultSet()->getQuery()->getQuery(); + + if (is_array($query)) { + $queryArr = $query; + $query = new class ($queryArr) extends AbstractQuery { + public function __construct(private array $arr) + { + } + public function toArray(): array + { + return $this->arr; + } + }; + } + + $extension = WikibaseFacetedSearchExtension::getInstance(); + $queryStringParser = $extension->getQueryStringParser(); + $parsedQuery = $queryStringParser->parse($term); + + $itemType = $parsedQuery->getItemTypes()[0] ?? null; + if (!$itemType) { + return []; + } + + $config = $extension->getConfig(); + $facetConfigs = $config->getFacetConfigForItemType($itemType); + + $data = []; + $valueCounter = $extension->getValueCounter($query); + $labelLookup = $extension->getLabelLookup($this->getLanguage()); + $formatter = $extension->getFacetValueFormatter($this->getLanguage()); + + foreach ($facetConfigs as $facetConfig) { + $constraints = $parsedQuery->getConstraintsForProperty($facetConfig->propertyId) + ?? new PropertyConstraints($facetConfig->propertyId); + + $counts = $valueCounter->countValues($constraints); + + $facetData = [ + 'property' => $facetConfig->propertyId->getSerialization(), + 'label' => $labelLookup->getLabel($facetConfig->propertyId)?->getText() ?? $facetConfig->propertyId->getSerialization(), + 'values' => [] + ]; + + foreach ($counts->asArray() as $valCount) { + $facetData['values'][] = [ + 'value' => $valCount->value, + 'count' => $valCount->count, + 'label' => $formatter->getLabel((string) $valCount->value, $facetConfig->propertyId) + ]; + } + $data[] = $facetData; + } + + return $data; + } + + public function getAllowedParams() + { + return [ + 'search' => [ + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true, + ], + 'namespaces' => [ + ApiBase::PARAM_TYPE => 'integer', + ApiBase::PARAM_ISMULTI => true, + ], + ]; + } +} From 38f6401fe537b2d2fa6a7d277f9e671490b12fcc Mon Sep 17 00:00:00 2001 From: Colin <144122160+colinrobinsonuib@users.noreply.github.com> Date: Mon, 8 Dec 2025 12:35:39 +0100 Subject: [PATCH 2/3] Add API module configuration for wbfacetsearch --- extension.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/extension.json b/extension.json index ff2f551..74a33ba 100644 --- a/extension.json +++ b/extension.json @@ -65,6 +65,10 @@ "WikibaseFacetedSearchConfig": "ProfessionalWiki\\WikibaseFacetedSearch\\EntryPoints\\SpecialWikibaseFacetedSearchConfig" }, + "APIModules": { + "wbfacetsearch": "ProfessionalWiki\\WikibaseFacetedSearch\\EntryPoints\\ApiWikibaseFacetedSearch" + }, + "ResourceFileModulePaths": { "localBasePath": "resources", "remoteExtPath": "WikibaseFacetedSearch/resources" From ae510739557ec090e58a017b0f75fba543239f46 Mon Sep 17 00:00:00 2001 From: colin Date: Thu, 8 Jan 2026 11:50:59 +0100 Subject: [PATCH 3/3] Match code style guidelines --- src/EntryPoints/ApiWikibaseFacetedSearch.php | 244 ++++++++++--------- 1 file changed, 127 insertions(+), 117 deletions(-) diff --git a/src/EntryPoints/ApiWikibaseFacetedSearch.php b/src/EntryPoints/ApiWikibaseFacetedSearch.php index 73081d7..9407a69 100644 --- a/src/EntryPoints/ApiWikibaseFacetedSearch.php +++ b/src/EntryPoints/ApiWikibaseFacetedSearch.php @@ -1,6 +1,6 @@ extractRequestParams(); - $term = $params['search']; - $namespaces = $params['namespaces']; - - $facets = $this->getFacets($term, $namespaces); - - $this->getResult()->addValue(null, $this->getModuleName(), $facets); - } catch (\Throwable $e) { - $this->getResult()->addValue(null, 'error', [ - 'message' => $e->getMessage(), - 'trace' => $e->getTraceAsString(), - 'file' => $e->getFile(), - 'line' => $e->getLine() - ]); - } - } - - private function getFacets(string $term, ?array $namespaces): array - { - $searchEngine = MediaWikiServices::getInstance()->newSearchEngine(); - // We limit to 0 because we only need the query structure, but CirrusSearch might need 1 or execution. - // However, ElasticValueCounter needs the original query. - // SearchEngine::searchText executes the query. - $searchEngine->setLimitOffset(1); - if ($namespaces) { - $searchEngine->setNamespaces($namespaces); - } - $results = $searchEngine->searchText($term); - - if ($results instanceof Status) { - if (!$results->isOK()) { - return []; - } - $results = $results->getValue(); - } - - if (!$results instanceof CirrusSearchResultSet) { - return []; - } - - $query = $results->getElasticaResultSet()->getQuery()->getQuery(); - - if (is_array($query)) { - $queryArr = $query; - $query = new class ($queryArr) extends AbstractQuery { - public function __construct(private array $arr) - { - } - public function toArray(): array - { - return $this->arr; - } - }; - } - - $extension = WikibaseFacetedSearchExtension::getInstance(); - $queryStringParser = $extension->getQueryStringParser(); - $parsedQuery = $queryStringParser->parse($term); - - $itemType = $parsedQuery->getItemTypes()[0] ?? null; - if (!$itemType) { - return []; - } - - $config = $extension->getConfig(); - $facetConfigs = $config->getFacetConfigForItemType($itemType); - - $data = []; - $valueCounter = $extension->getValueCounter($query); - $labelLookup = $extension->getLabelLookup($this->getLanguage()); - $formatter = $extension->getFacetValueFormatter($this->getLanguage()); - - foreach ($facetConfigs as $facetConfig) { - $constraints = $parsedQuery->getConstraintsForProperty($facetConfig->propertyId) - ?? new PropertyConstraints($facetConfig->propertyId); - - $counts = $valueCounter->countValues($constraints); - - $facetData = [ - 'property' => $facetConfig->propertyId->getSerialization(), - 'label' => $labelLookup->getLabel($facetConfig->propertyId)?->getText() ?? $facetConfig->propertyId->getSerialization(), - 'values' => [] - ]; - - foreach ($counts->asArray() as $valCount) { - $facetData['values'][] = [ - 'value' => $valCount->value, - 'count' => $valCount->count, - 'label' => $formatter->getLabel((string) $valCount->value, $facetConfig->propertyId) - ]; - } - $data[] = $facetData; - } - - return $data; - } - - public function getAllowedParams() - { - return [ - 'search' => [ - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true, - ], - 'namespaces' => [ - ApiBase::PARAM_TYPE => 'integer', - ApiBase::PARAM_ISMULTI => true, - ], - ]; - } +class ApiWikibaseFacetedSearch extends ApiBase { + + public function execute(): void { + try { + $params = $this->extractRequestParams(); + $term = $params['search']; + $namespaces = $params['namespaces']; + + $facets = $this->getFacets( $term, $namespaces ); + + $this->getResult()->addValue( null, $this->getModuleName(), $facets ); + } catch ( \Throwable $e ) { + $this->getResult()->addValue( null, 'error', [ + 'message' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + 'file' => $e->getFile(), + 'line' => $e->getLine() + ] ); + } + } + + private function getFacets( string $term, ?array $namespaces ): array { + $results = $this->runSearch( $term, $namespaces ); + + if ( $results instanceof Status ) { + if ( !$results->isOK() ) { + return []; + } + $results = $results->getValue(); + } + + if ( !$results instanceof CirrusSearchResultSet ) { + return []; + } + + $elasticaResultSet = $results->getElasticaResultSet(); + if ( $elasticaResultSet === null ) { + return []; + } + $query = $elasticaResultSet->getQuery()->getQuery(); + + if ( is_array( $query ) ) { + $queryArr = $query; + $query = new class ( $queryArr ) extends AbstractQuery { + public function __construct( private array $arr ) { + } + + public function toArray(): array { + return $this->arr; + } + }; + } + + $extension = WikibaseFacetedSearchExtension::getInstance(); + $queryStringParser = $extension->getQueryStringParser(); + $parsedQuery = $queryStringParser->parse( $term ); + + $itemType = $parsedQuery->getItemTypes()[0] ?? null; + if ( !$itemType ) { + return []; + } + + $config = $extension->getConfig(); + $facetConfigs = $config->getFacetConfigForItemType( $itemType ); + + $data = []; + $valueCounter = $extension->getValueCounter( $query ); + $labelLookup = $extension->getLabelLookup( $this->getLanguage() ); + $formatter = $extension->getFacetValueFormatter( $this->getLanguage() ); + + $data = []; + foreach ( $facetConfigs as $facetConfig ) { + $data[] = $this->buildFacetData( + $facetConfig, + $parsedQuery, + $valueCounter, + $labelLookup, + $formatter + ); + } + + return $data; + } + + private function runSearch( string $term, ?array $namespaces ) { + $searchEngine = MediaWikiServices::getInstance()->newSearchEngine(); + $searchEngine->setLimitOffset( 1 ); + + if ( $namespaces ) { + $searchEngine->setNamespaces( $namespaces ); + } + + return $searchEngine->searchText( $term ); + } + + private function buildFacetData( + $facetConfig, + $parsedQuery, + $valueCounter, + $labelLookup, + $formatter + ): array { + $constraints = $parsedQuery->getConstraintsForProperty( $facetConfig->propertyId ) + ?? new PropertyConstraints( $facetConfig->propertyId ); + + $counts = $valueCounter->countValues( $constraints ); + + $propertyId = $facetConfig->propertyId; + $propertySerialization = $propertyId->getSerialization(); + + $facetData = [ + 'property' => $propertySerialization, + 'label' => $labelLookup->getLabel( $propertyId )?->getText() ?? $propertySerialization, + 'values' => [], + ]; + + foreach ( $counts->asArray() as $valCount ) { + $facetData['values'][] = [ + 'value' => $valCount->value, + 'count' => $valCount->count, + 'label' => $formatter->getLabel( (string)$valCount->value, $propertyId ), + ]; + } + + return $facetData; + } }