import { Text } from '@microsoft/sp-core-library' ; import { ServiceKey , ServiceScope , Log } from "@microsoft/sp-core-library" ; import { SPHttpClient } from "@microsoft/sp-http" ; import { ISharePointSearchService } from './ISharePointSearchService' ; import { PnPClientStorage } from "@pnp/common/storage" ; import { dateAdd } from "@pnp/common/util" ; import { PageContext } from '@microsoft/sp-page-context' ; import { ISharePointSearchResult , ISharePointSearchResults , ISharePointSearchResultBlock , ISharePointSearchPromotedResult } from '../../models/search/ISharePointSearchResults' ; import ISharePointManagedProperty from '../../models/search/ISharePointManagedProperty' ; import { isEmpty } from '@microsoft/sp-lodash-subset' ; import { IDataFilterResultValue , IDataFilterResult , FilterComparisonOperator } from '@pnp/modern-search-extensibility' ; import LocalizationHelper from '../../helpers/LocalizationHelper' ; import { ISharePointSearchResponse } from '../../models/search/ISharePointSearchResponse' ; import { ISuggestResult , ISuggestQuery } from '../../models/search/ISuggestQuery' ; import { ISharePointSearchQuery , SortDirection } from '../../models/search/ISharePointSearchQuery' ; import { cloneDeep } from "@microsoft/sp-lodash-subset" ; import { Constants } from '../../common/Constants' ; const SearchService_ServiceKey = 'pnpSearchResults:SharePointSearchService' ; const AvailableQueryLanguages_StorageKey = 'pnpSearchResults_AvailableQueryLanguages' ; export class SharePointSearchService implements ISharePointSearchService { public static ServiceKey : ServiceKey < ISharePointSearchService > = ServiceKey . create ( SearchService_ServiceKey , SharePointSearchService ) ; /**
     * The current page context instance
     */ private pageContext : PageContext ; /**
     * The SharePoint search service endpoint REST URL
     */ private searchEndpointUrl : string ; /**
     * The current service scope
     */ private serviceScope : ServiceScope ; /**
     * The SPHttpClient instance
     */ private spHttpClient : SPHttpClient ; /**
     * The client storage instance
     */ private clientStorage : PnPClientStorage ; constructor ( serviceScope ) { this . serviceScope = serviceScope ; this . clientStorage = new PnPClientStorage ( ) ; serviceScope . whenFinished ( async ( ) => { this . pageContext = serviceScope . consume < PageContext > ( PageContext . serviceKey ) ; this . spHttpClient = serviceScope . consume < SPHttpClient > ( SPHttpClient . serviceKey ) ; this . searchEndpointUrl = `${ this . pageContext . web . absoluteUrl }/_api/search/postquery` ; } ) ; } param searchQuery return public async search ( searchQuery ) { let results = { queryKeywords : searchQuery . Querytext , refinementResults : [ ] , relevantResults : [ ] , secondaryResults : [ ] , totalRows : 0 } ; try { const response = await this . spHttpClient . post ( this . searchEndpointUrl , SPHttpClient . configurations . v1 , { body : this . getRequestPayload ( searchQuery ) , headers : { 'odata-version' : '3.0' , 'accept' : 'application/json;odata=nometadata' , 'X-ClientService-ClientTag' : Constants . X_CLIENTSERVICE_CLIENTTAG , 'UserAgent' : Constants . X_CLIENTSERVICE_CLIENTTAG } } ) ; if ( response . ok ) { const searchResponse = await response . json ( ) ; if ( searchResponse . PrimaryQueryResult ) { let refinementResults = [ ] ; const properties = searchResponse . PrimaryQueryResult . RelevantResults . Properties . filter ( ( property ) => { return property . Key === 'QueryModification' ; } ) ; if ( properties . length === 1 ) { results . queryModification = properties [ 0 ] . Value ; } const resultRows = searchResponse . PrimaryQueryResult . RelevantResults . Table . Rows ; let refinementResultsRows = searchResponse . PrimaryQueryResult . RefinementResults ; const refinementRows = refinementResultsRows ? refinementResultsRows . Refiners : [ ] ; let searchResults = this . getSearchResults ( resultRows ) ; refinementRows . forEach ( ( refiner ) => { let values = [ ] ; refiner . Entries . forEach ( ( item ) => { values . push ( { count : parseInt ( item . RefinementCount , 10 ) , name : item . RefinementValue . replace ( "string;#" , "" ) , value : item . RefinementToken , operator : FilterComparisonOperator . Contains } as IDataFilterResultValue ) ; } ) ; refinementResults . push ( { filterName : refiner . Name , values : values } ) ; } ) ; results . relevantResults = searchResults ; results . refinementResults = refinementResults ; results . totalRows = searchResponse . PrimaryQueryResult . RelevantResults . TotalRows ; if ( ! isEmpty ( searchResponse . SpellingSuggestion ) ) { results . spellingSuggestion = searchResponse . SpellingSuggestion ; } } if ( searchResponse . SecondaryQueryResults ) { const secondaryQueryResults = searchResponse . SecondaryQueryResults ; if ( Array . isArray ( secondaryQueryResults ) && secondaryQueryResults . length > 0 ) { let promotedResults = [ ] ; let secondaryResults = [ ] ; secondaryQueryResults . forEach ( ( e ) => { if ( e . SpecialTermResults ) { ( e . SpecialTermResults ) . Results . forEach ( ( result ) => { promotedResults . push ( { title : result . Title , url : result . Url , description : result . Description } as ISharePointSearchPromotedResult ) ; } ) ; } if ( e . RelevantResults ) { const secondaryResultItems = this . getSearchResults ( e . RelevantResults . Table . Rows ) ; const secondaryResultBlock = { title : e . RelevantResults . ResultTitle , results : secondaryResultItems } ; if ( secondaryResultBlock . results . length > 0 ) { secondaryResults . push ( secondaryResultBlock ) ; } } } ) ; results . promotedResults = promotedResults ; results . secondaryResults = secondaryResults ; } } return results ; } else { throw new Error ( `${ response [ 'statusMessage' ] }` ) ; } } catch ( error ) { Log . error ( "[SharePointSearchService.search()]" , error , this . serviceScope ) ; throw error ; } } /**
     * Get available SharePoint search managed properties from the search schema
     */ public async getAvailableManagedProperties ( ) { let managedProperties = [ ] ; let searchQuery = { } ; searchQuery . Querytext = '*' ; searchQuery . Refiners = 'ManagedProperties(filter=50000/0/*,sort=name/ascending)' ; searchQuery . RowLimit = 1 ; try { const response = await this . spHttpClient . post ( this . searchEndpointUrl , SPHttpClient . configurations . v1 , { body : this . getRequestPayload ( searchQuery ) , headers : { 'odata-version' : '3.0' , 'accept' : 'application/json;odata=nometadata' } } ) ; if ( response . ok ) { const searchResponse = await response . json ( ) ; const refinementResultsRows = searchResponse . PrimaryQueryResult . RefinementResults ; const refinementRows = refinementResultsRows ? refinementResultsRows . Refiners : [ ] ; refinementRows . forEach ( ( refiner ) => { refiner . Entries . forEach ( ( item ) => { managedProperties . push ( { name : item . RefinementName } ) ; } ) ; } ) ; } else { throw new Error ( `${ response [ 'statusMessage' ] }` ) ; } } catch ( error ) { Log . error ( "[SharePointSearchService.getAvailableManagedProperties()]" , error , this . serviceScope ) ; throw error ; } return managedProperties ; } /**
     * Get all available languages for the search query
     */ public async getAvailableQueryLanguages ( ) { try { this . clientStorage . local . deleteExpired ( ) ; let availableLanguages = this . clientStorage . local . get ( AvailableQueryLanguages_StorageKey ) ; if ( availableLanguages ) { return Promise . resolve ( availableLanguages ) ; } else { const response = await this . spHttpClient . get ( `${ this . pageContext . web . absoluteUrl }/_api/web/RegionalSettings/InstalledLanguages` , SPHttpClient . configurations . v1 , { headers : { 'X-ClientService-ClientTag' : Constants . X_CLIENTSERVICE_CLIENTTAG , 'UserAgent' : Constants . X_CLIENTSERVICE_CLIENTTAG } } ) ; if ( response . ok ) { availableLanguages = await response . json ( ) ; this . clientStorage . local . put ( AvailableQueryLanguages_StorageKey , availableLanguages . Items , dateAdd ( new Date ( ) , 'week' , 1 ) ) ; return availableLanguages . Items ; } else { throw new Error ( `${ response [ 'statusMessage' ] }` ) ; } } } catch ( error ) { Log . error ( "[SharePointSearchService._getQueryLanguages()]" , error , this . serviceScope ) ; throw new Error ( error ) ; } } param property public async validateSortableProperty ( property ) { let isSortable = false ; let searchQuery = { } ; searchQuery . Querytext = "*" ; searchQuery . SortList = [ { Property : property , Direction : SortDirection . Ascending } ] ; searchQuery . RowLimit = 1 ; searchQuery . SelectProperties = [ 'Path' ] ; try { const response = await this . spHttpClient . post ( this . searchEndpointUrl , SPHttpClient . configurations . v1 , { body : this . getRequestPayload ( searchQuery ) , headers : { 'odata-version' : '3.0' , 'accept' : 'application/json;odata=nometadata' , 'X-ClientService-ClientTag' : Constants . X_CLIENTSERVICE_CLIENTTAG , 'UserAgent' : Constants . X_CLIENTSERVICE_CLIENTTAG } } ) ; if ( response . ok ) { isSortable = true ; } else { isSortable = false ; } } catch { isSortable = false ; } return isSortable ; } param query public async suggest ( query ) { let suggestions = [ ] ; const searchSuggestQuery = { preQuery : true , queryText : encodeURIComponent ( query . replace ( /'/g , '\'\'' ) ) , count : 10 , hitHighlighting : true , prefixMatch : true , culture : LocalizationHelper . getLocaleId ( this . pageContext . cultureInfo . currentUICultureName ) . toString ( ) , numberOfQuerySuggestions : 10 , capitalize : false } ; try { const endpointUrl = Text . format ( `${ this . pageContext . web . absoluteUrl }/_api/search/suggest?querytext='{0}'&inumberofquerysuggestions={1}&fHitHighlighting={2}&fCapitalizeFirstLetters={3}&Culture={4}&fPrefixMatchAllTerms={5}` , searchSuggestQuery . queryText , searchSuggestQuery . numberOfQuerySuggestions , searchSuggestQuery . hitHighlighting , searchSuggestQuery . capitalize , searchSuggestQuery . culture , searchSuggestQuery . prefixMatch ) ; const response = await this . spHttpClient . get ( endpointUrl , SPHttpClient . configurations . v1 , { headers : { 'X-ClientService-ClientTag' : Constants . X_CLIENTSERVICE_CLIENTTAG , 'UserAgent' : Constants . X_CLIENTSERVICE_CLIENTTAG } } ) ; if ( response . ok ) { const suggestionResponse = await response . json ( ) ; if ( suggestionResponse . Queries . length > 0 ) { suggestions = suggestionResponse . Queries . map ( elt => { return elt . Query ; } ) ; } return suggestions ; } else { throw new Error ( `${ response [ 'statusMessage' ] }` ) ; } } catch ( error ) { Log . error ( "[SharePointSearchService.suggest()]" , error , this . serviceScope ) ; throw error ; } } param resultRows private getSearchResults ( resultRows ) { let searchResults = resultRows . map ( ( elt ) => { let result = { } ; elt . Cells . map ( ( item ) => { if ( item . Key === "HtmlFileType" && item . Value ) { result [ "FileType" ] = item . Value ; } else if ( ! result [ item . Key ] ) { result [ item . Key ] = item . Value ; } } ) ; return result ; } ) ; return searchResults ; } private getRequestPayload ( searchQuery ) { let queryPayload = cloneDeep ( searchQuery ) ; queryPayload . HitHighlightedProperties = this . fixArrProp ( queryPayload . HitHighlightedProperties ) ; queryPayload . Properties = this . fixArrProp ( queryPayload . Properties ) ; queryPayload . RefinementFilters = this . fixArrProp ( queryPayload . RefinementFilters ) ; queryPayload . ReorderingRules = this . fixArrProp ( queryPayload . ReorderingRules ) ; queryPayload . SelectProperties = this . fixArrProp ( queryPayload . SelectProperties ) ; queryPayload . SortList = this . fixArrProp ( queryPayload . SortList ) ; const postBody = { request : { '__metadata' : { 'type' : 'Microsoft.Office.Server.Search.REST.SearchRequest' } , ... queryPayload } } ; return JSON . stringify ( postBody ) ; } param prop private fixArrProp ( prop ) { if ( typeof prop === "undefined" ) { return ( { results : [ ] } ) ; } return { results : Array . isArray ( prop ) ? prop : [ prop ] } ; } }