From 2e3e5feb9b22fc9f0d061a106233e0f569c6c815 Mon Sep 17 00:00:00 2001 From: Simon Stier <52674635+simontaurus@users.noreply.github.com> Date: Thu, 18 May 2023 19:57:43 +0200 Subject: [PATCH] feat(search): add SMW Ask API as search backend option (#625) * feat: add SMW Ask API as search backend option * feat: allow namespace prefix in smw ask query --- README.md | 3 +- includes/Hooks/ResourceLoaderHooks.php | 1 + .../skins.citizen.search/gateway/gateway.js | 2 + .../skins.citizen.search/gateway/smwAskApi.js | 116 ++++++++++++++++++ skin.json | 9 +- 5 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 resources/skins.citizen.search/gateway/smwAskApi.js diff --git a/README.md b/README.md index c822a2fc..3da60f16 100644 --- a/README.md +++ b/README.md @@ -118,8 +118,9 @@ Name | Description | Values | Default Name | Description | Values | Default :--- | :--- | :--- | :--- `$wgCitizenSearchModule` | Which ResourceLoader module to use for search suggestion | `skins.citizen.search`; `mediawiki.searchSuggest`; string | `skins.citizen.search` -`$wgCitizenSearchGateway` | Which gateway to use for fetching search suggestion |`mwActionApi`; `mwRestApi`; string | `mwActionApi` +`$wgCitizenSearchGateway` | Which gateway to use for fetching search suggestion |`mwActionApi`; `mwRestApi`; `smwAskApi`; string | `mwActionApi` `$wgCitizenSearchDescriptionSource` | Source of description text on search suggestions (only takes effect if `$wgCitizenSearchGateway` is `mwActionApi`) | `wikidata` - Use description provided by [WikibaseLib](Extension:WikibaseLib) or [ShortDescription](https://www.mediawiki.org/wiki/Extension:ShortDescription); `textextracts` - Use description provided by [TextExtracts](https://www.mediawiki.org/wiki/Extension:TextExtracts); `pagedescription` - Use description provided by [Description2](https://www.mediawiki.org/wiki/Extension:Description2) or any other extension that sets the `description` page property | `textextracts` +`$wgCitizenSearchSmwAskApiQueryTemplate` | The SMW ask query for fetching search suggestion (only takes effect if `$wgCitizenSearchGateway` is `smwAskApi`). You can replace the SMW properties (but not the mappings). | string | `'[[Display_title_of::~*${input}*]] \|?Display_title_of=displaytitle \|?Page_Image=thumbnail \|?Description=desc'` `$wgCitizenMaxSearchResults` | Max number of search suggestions | Integer > 0 | `6` ### Webapp manifest diff --git a/includes/Hooks/ResourceLoaderHooks.php b/includes/Hooks/ResourceLoaderHooks.php index da98cd9b..a3e9c5bd 100644 --- a/includes/Hooks/ResourceLoaderHooks.php +++ b/includes/Hooks/ResourceLoaderHooks.php @@ -80,6 +80,7 @@ class ResourceLoaderHooks { 'wgCitizenSearchGateway' => $config->get( 'CitizenSearchGateway' ), 'wgCitizenSearchDescriptionSource' => $config->get( 'CitizenSearchDescriptionSource' ), 'wgCitizenMaxSearchResults' => $config->get( 'CitizenMaxSearchResults' ), + 'wgCitizenSearchSmwAskApiQueryTemplate' => $config->get( 'CitizenSearchSmwAskApiQueryTemplate' ), 'wgScriptPath' => $config->get( 'ScriptPath' ), 'wgSearchSuggestCacheExpiry' => $config->get( 'SearchSuggestCacheExpiry' ), 'isMediaSearchExtensionEnabled' => ExtensionRegistry::getInstance()->isLoaded( 'MediaSearch' ), diff --git a/resources/skins.citizen.search/gateway/gateway.js b/resources/skins.citizen.search/gateway/gateway.js index f57aeaaa..c8f0051c 100644 --- a/resources/skins.citizen.search/gateway/gateway.js +++ b/resources/skins.citizen.search/gateway/gateway.js @@ -21,6 +21,8 @@ function getGateway() { return require( './mwActionApi.js' ); case 'mwRestApi': return require( './mwRestApi.js' ); + case 'smwAskApi': + return require( './smwAskApi.js' ); default: throw new Error( 'Unknown search gateway' ); } diff --git a/resources/skins.citizen.search/gateway/smwAskApi.js b/resources/skins.citizen.search/gateway/smwAskApi.js new file mode 100644 index 00000000..94462d6d --- /dev/null +++ b/resources/skins.citizen.search/gateway/smwAskApi.js @@ -0,0 +1,116 @@ +const config = require( '../config.json' ); + +/** + * Build URL used for fetch request + * + * @param {string} input + * @return {string} url + */ +function getUrl( input ) { + const endpoint = config.wgScriptPath + '/api.php?format=json', + maxResults = config.wgCitizenMaxSearchResults, + askQueryTemplate = config.wgCitizenSearchSmwAskApiQueryTemplate + + let askQuery = ''; + + if ( input.includes( ':' ) ) { + let namespace = input.split( ':' )[0]; + if ( namespace === 'Category' ) namespace = ':' + namespace; + input = input.split( ':' )[1]; + askQuery += '[[' + namespace + ':+]]'; + } + + askQuery += askQueryTemplate.replaceAll( '${input}', input ); + askQuery += '|limit=' + maxResults; + + const query = { + action: 'ask', + query: encodeURIComponent(askQuery), + }; + + let queryString = ''; + for ( const property in query ) { + queryString += '&' + property + '=' + query[ property ]; + } + + return endpoint + queryString; +} + +/** + * Map raw response to Results object + * + * @param {Object} data + * @return {Object} Results + */ +function convertDataToResults( data ) { + const userLang = mw.config.get( 'wgUserLanguage' ); + + const getDisplayTitle = ( item ) => { + if ( item.printouts.displaytitle && item.printouts.displaytitle.length + && item.printouts.displaytitle[0]['Language code'] && item.printouts.displaytitle[0]['Text'].item.length ) { + // multi-lang string preference: user lang => English => first result + let textEN = ""; + let textResult = ""; + for ( const text of item.printouts.displaytitle ) { + if ( text['Language code'].item[0] === userLang ) textResult = text['Text'].item[0]; + if ( text['Language code'].item[0] === 'en' ) textEN = text['Text'].item[0]; + } + if ( textResult === "" ) textResult = textEN; + if ( textResult === "" ) textResult = item.printouts.displaytitle[0]['Text'].item[0]; + return textResult; + } else if ( item.printouts.displaytitle && item.printouts.displaytitle.length ) { + return item.printouts.displaytitle[0]; + } else if ( item.displaytitle && item.displaytitle !== "") { + return item.displaytitle; + } + else return item.fulltext; + }; + + const getDescription = ( item ) => { + if ( item.printouts.desc && item.printouts.desc.length + && item.printouts.desc[0]['Language code'] && item.printouts.desc[0]['Text'].item.length ) { + // multi-lang string preference: user lang => English => first result + let textEN = ""; + let textResult = ""; + for ( const text of item.printouts.desc ) { + if ( text['Language code'].item[0] === userLang ) textResult = text['Text'].item[0]; + if ( text['Language code'].item[0] === 'en' ) textEN = text['Text'].item[0]; + } + if ( textResult === "" ) textResult = textEN; + if ( textResult === "" ) textResult = item.printouts.desc[0]['Text'].item[0]; + return textResult; + } else if ( item.printouts.desc && item.printouts.desc.length ) { + return item.printouts.desc[0]; + } + else return ""; + }; + + const getThumbnail = ( item ) => { + if ( item.printouts.thumbnail && item.printouts.thumbnail.length ) { + let img_title = item.printouts.thumbnail[0].fulltext; + return config.wgScriptPath + '/index.php?title=Special:Redirect/file/' + img_title + '&width=200&height=200'; + } + else return undefined; + }; + + const results = []; + + data = Object.values( data.query.results ); + + for ( let i = 0; i < data.length; i++ ) { + results[ i ] = { + id: i, + key: data[ i ].fulltext, + title: getDisplayTitle( data[ i ] ), + desc: getDescription( data[ i ] ), + thumbnail: getThumbnail( data[ i] ), + }; + } + + return results; +} + +module.exports = { + getUrl: getUrl, + convertDataToResults: convertDataToResults +}; diff --git a/skin.json b/skin.json index 612d05b4..f6f0916b 100644 --- a/skin.json +++ b/skin.json @@ -157,7 +157,8 @@ "resources/skins.citizen.search/typeahead.js", "resources/skins.citizen.search/gateway/gateway.js", "resources/skins.citizen.search/gateway/mwActionApi.js", - "resources/skins.citizen.search/gateway/mwRestApi.js" + "resources/skins.citizen.search/gateway/mwRestApi.js", + "resources/skins.citizen.search/gateway/smwAskApi.js" ], "messages": [ "citizen-search-fulltext", @@ -563,6 +564,12 @@ "descriptionmsg": "citizen-config-maxsearchresults", "public": true }, + "SearchSmwAskApiQueryTemplate": { + "value": "[[Display_title_of::~*${input}*]]|?Display_title_of=displaytitle|?Page_Image=thumbnail|?Description=desc", + "description": "The SMW ask query for fetching search suggestion. You can replace the SMW properties (but not the mappings).", + "descriptionmsg": "citizen-config-smwaskapiquerytemplate", + "public": true + }, "ShowPageTools": { "value": true, "description": "Page tools visibility condition",