API Docs for: 2.0.20133.2
Show:

File: src\internal\api\search\SearchApi.ts

/// <reference path="../../common/sdkConfig.ts"/>
/// <reference path="../../common/constants.ts"/>
/// <reference path="../../utils/http.ts"/>
/// <reference path="../../utils/object.ts"/>
/// <reference path="../../utils/typecheck.ts"/>
/// <reference path="../../telemetry/awa/awaEvent.ts"/>
/// <reference path="../../telemetry/awa/eventWriter.ts"/>
/// <reference path="searchResultSet.ts"/>
/// <reference path="knowledgeSearchResultSet.ts"/>
/// <reference path="searchConfig.ts"/>
/// <reference path="knowledgeSearchConfig.ts"/>
/// <reference path="searchConstants.ts"/>

/**
 * @module API
 * @submodule API Search
 * @namespace api
 */
namespace internal.api.search {

    /**
     * Search API to get search results for support
     * @class search
     * @static
     */
    export class SearchApi {
        /**
         * Retrieves search results for the parameters specified
         * @method getSearchResults
         * @static
         * @param {SearchConfig} config A config object to enlist the input criteria.
         * @param {HttpMethod} config.httpMethod. HTTP Method to use. Valid options are GET and POST (defaults to GET).
         * @param {string} config.query. Search query.
         * @param {string} config.locale. Comma delimited locale for search results.
         * @param {string} config.scopes. Comma delimited scopes for the search results.
         * @param {number} config.count. Total number of search results to be returned.
         * @param {number} config.skip. A subset/multiplier of count that needs to be returned in every call.
         * @param {string} config.searchProvider. The search provider to be used to perform the search.
         * @param {string} config.instantAnswerProvider. Instant answer provider to be used.
         * @param {boolean} config.includeInstantAnswers. Include instant answers along with search results.
         * @param {boolean} config.includeWebSearchResults. Include web search results.
         * @param {boolean} config.enablehithighlights. Enable hit highlights.
         * @param {string} config.tenant. Specify tenant to be used.
         * @param {SearchFilter} config.filter. The filter to apply for the search results.
         * @param {SearchFacets} config.facets. The facets to apply for the search results.
         * @param {SearchOrderByExpression[]} config.orderBy. The order by expressions to apply for the search results.
         * @param {string} config.version. Targeting a specific version.
         * @param {string[]} config.flights. The flights the call should use.
         * @param {string} config.suggestId. The suggest id to correlate to this search.
         * @param {string} config.environment. The environment to use (staging or production), default is production.
         * @param {string} config.insiderMode. Insider mode for on-boarding onto Alpha development.
         * @param {string} config.muid. Muid for the current user.
         * @param {string} config.searchId. The search id tracking the virtual session.
         * @return {Promise | SearchResultSet}. Promise of search results.
         */
        static getSearchResults(config: SearchConfig): JQueryPromise<SearchResultSet> {
            let apiUrl = formSearchUrl(config, false);
            let tenant = config.tenant || SdkConfig.current.partnerId + ":" + SdkConfig.current.appId;
            let additionalHeaders = <any>{};
            additionalHeaders["x-caller-name"] = tenant;
            if (config.flights && config.flights.length > 0) {
                additionalHeaders["x-ms-flight"] = config.flights.join(",");
            }

            if (config.muid != undefined && config.muid != "") {
                additionalHeaders["x-caller-muid"] = config.muid;
            }

            if (config.searchId != undefined && config.searchId != "") {
                additionalHeaders["x-msaas-searchid"] = config.searchId;
            }

            let cv = telemetry.getCorrelationVector();
            if (cv) {
                additionalHeaders["ms-cv"] = telemetry.getCorrelationVector();
            }

            let requestOptions: HttpRequestOptions = {
                operationName: "api.search.getResults",
                url: apiUrl,
                additionalHeaders: additionalHeaders,
                dependencyName: "supportsearch.microsoft.com",
                dependencyOperationName: "Get Search Results"
            };

            return getSearchResultsPromise(config, requestOptions);
        }

        /**
         * Retrieves search results for the parameters specified via HTTP POST by default
         * @method postSearchResults
         * @static
         * @param {SearchConfig} config A config object to enlist the input criteria.
         */
        static postSearchResults(config: SearchConfig): JQueryPromise<SearchResultSet> {
            config.httpMethod = HttpMethod.POST; // explicitly set to HTTP
            return SearchApi.getSearchResults(config);
        }

        /**
         * Retrieves knowledge search results for the parameters specified
         * @method getKnowledgeSearchResults
         * @static
         * @param {KnowledgeSearchConfig} config A config object to enlist the input criteria.
         * @param {HttpMethod} config.httpMethod. HTTP Method to use. Valid options are GET and POST (defaults to GET).
         * @param {string} config.query. Search query.
         * @param {string} config.locale. Comma delimited locale for search results.
         * @param {string} config.scopes. Comma delimited scopes for the search results.
         * @param {number} config.count. Total number of search results to be returned.
         * @param {number} config.skip. A subset/multiplier of count that needs to be returned in every call.
         * @param {string} config.searchProvider. The search provider to be used to perform the search.
         * @param {string} config.instantAnswerProvider. Instant answer provider to be used.
         * @param {boolean} config.includeInstantAnswers. Include instant answers along with search results.
         * @param {boolean} config.includeWebSearchResults. Include web search results.
         * @param {boolean} config.enableHitHighlights. Enable hit highlights.
         * @param {string} config.tenant. Specify tenant to be used.
         * @param {SearchFilter} config.filter. The filter to apply for the search results.
         * @param {SearchFacets} config.facets. The facets to apply for the search results.
         * @param {SearchOrderByExpression[]} config.orderBy. The order by expressions to apply for the search results.
         * @param {string} config.version. Targeting a specific version.
         * @param {string[]} config.flights. The flights the call should use.
         * @param {string} config.suggestId. The suggest id to correlate to this search.
         * @param {string} config.environment. The environment to use (staging or production), default is production.
         * @param {string} config.insiderMode. Insider mode for on-boarding onto Alpha development.
         * @param {string} config.muid. Muid for the current user.
         * @param {string} config.token. An authentication token from AAD
         * @param {string} config.searchId. The search id used to track the virtual session
         * @return {Promise | KnowledgeSearchResultSet}. Promise of knowledge search results.
         */
        static getKnowledgeSearchResults(config: KnowledgeSearchConfig): JQueryPromise<KnowledgeSearchResultSet> {
            let apiUrl = formSearchUrl(config, true);
            let tenant = config.tenant || SdkConfig.current.partnerId + ":" + SdkConfig.current.appId;
            let additionalHeaders = <any>{};
            additionalHeaders["x-caller-name"] = tenant;
            additionalHeaders["Authorization"] = `Bearer ${config.token}`;
            if (config.flights && config.flights.length > 0) {
                additionalHeaders["x-ms-flight"] = config.flights.join(",");
            }

            if (config.muid != undefined && config.muid != "") {
                additionalHeaders["x-caller-muid"] = config.muid;
            }

            if (config.searchId != undefined && config.searchId != "") {
                additionalHeaders["x-msaas-searchid"] = config.searchId;
            }

            let cv = telemetry.getCorrelationVector();
            if (cv) {
                additionalHeaders["ms-cv"] = telemetry.getCorrelationVector();
            }

            let requestOptions: HttpRequestOptions = {
                operationName: "api.search.getKnowledgeSearchResults",
                url: apiUrl,
                additionalHeaders: additionalHeaders,
                dependencyName: "https://api.support.microsoft.com/v1/search",
                dependencyOperationName: "Get Knowledge Search Results"
            };

            return getKnowledgeSearchResultsPromise(config, requestOptions);
        }

        /**
         * Retrieves knowledge search results for the parameters specified via HTTP POST by default
         * @method postKnowledgeSearchResults
         * @static
         * @param {KnowledgeSearchConfig} config A config object to enlist the input criteria.
         */
        static postKnowledgeSearchResults(config: KnowledgeSearchConfig): JQueryPromise<KnowledgeSearchResultSet> {
            config.httpMethod = HttpMethod.POST;
            return SearchApi.getKnowledgeSearchResults(config);
        }
    }

    function getSearchResultsPromise(config: SearchConfig, requestOptions: HttpRequestOptions): JQueryPromise<SearchResultSet> {
        let searchDeferred = jQuery.Deferred<SearchResultSet>();
        let params = formSearchParams(config);
        let request: JQueryPromise<any>;

        if (config.httpMethod == HttpMethod.POST) {
            requestOptions.content = params;
            request = utils.httpRequest.post(requestOptions);
        }
        else {
            requestOptions.queryParams = convertDatesToStrings(flatten(params));
            request = utils.httpRequest.get(requestOptions);
        }

        request.then(
            function (jsonResult) {
                let searchResults = new Array<SearchResult>();
                let searchResultFacets = new Array<SearchResultFacet>();
                let flightIds = new Array<string>();

                if (jsonResult.WebSearchResults && jsonResult.WebSearchResults.length != 0) {
                    for (let i = 0; i < jsonResult.WebSearchResults.length; i++) {
                        let searchResult = new SearchResult();
                        searchResult.title = jsonResult.WebSearchResults[i].Title;
                        searchResult.displayUrl = jsonResult.WebSearchResults[i].DisplayUrl;
                        searchResult.targetUrl = jsonResult.WebSearchResults[i].TargetUrl;
                        searchResult.description = jsonResult.WebSearchResults[i].Description;
                        searchResult.appliesTo = jsonResult.WebSearchResults[i].AppliesTo;
                        searchResults.push(searchResult);
                    }
                }

                if (jsonResult.FlightIds && jsonResult.FlightIds.length != 0) {
                    for (let i = 0; i < jsonResult.FlightIds.length; i++) {
                        flightIds.push(jsonResult.FlightIds[i]);
                    }
                }

                if (jsonResult.Facets) {
                    for (let field in jsonResult.Facets) {
                        let facet = new SearchResultFacet();
                        let values = new Array<SearchResultFacetValue>();
                        for (let i = 0; i < jsonResult.Facets[field].length; i++) {
                            let value = new SearchResultFacetValue();
                            value.name = jsonResult.Facets[field][i].Name;
                            value.value = jsonResult.Facets[field][i].Value;
                            value.facetType = jsonResult.Facets[field][i].FacetType;
                            value.count = jsonResult.Facets[field][i].Count;
                            value.from = jsonResult.Facets[field][i].From;
                            value.to = jsonResult.Facets[field][i].To;
                            values.push(value);
                        }

                        facet.field = field;
                        facet.values = values;
                        searchResultFacets.push(facet);
                    }
                }

                let searchResultSet = new SearchResultSet();
                searchResultSet.searchId = jsonResult.SearchId;
                searchResultSet.totalResults = jsonResult.TotalResults;
                searchResultSet.searchResults = searchResults;
                searchResultSet.facets = searchResultFacets;
                searchResultSet.instantAnswerJson = jsonResult.AnswerContentJson;
                searchResultSet.searchProvider = jsonResult.SearchProvider;
                searchResultSet.instantAnswerProvider = jsonResult.InstantAnswerProvider;
                searchResultSet.flightIds = flightIds;
                searchResultSet.suggestId = jsonResult.SuggestId;
                searchResultSet.tenant = jsonResult.CallerId;
                searchDeferred.resolve(searchResultSet);
            }, function (error) {
                console.log(error);
                searchDeferred.reject(error);
            });

        return searchDeferred.promise();
    }

    function getKnowledgeSearchResultsPromise(config: KnowledgeSearchConfig, requestOptions: HttpRequestOptions): JQueryPromise<KnowledgeSearchResultSet> {
        let searchDeferred = jQuery.Deferred<KnowledgeSearchResultSet>();
        let params = formSearchParams(config);
        let request: JQueryPromise<any>;

        if (config.httpMethod == HttpMethod.POST) {
            requestOptions.content = params;
            request = utils.httpRequest.post(requestOptions);
        }
        else {
            requestOptions.queryParams = convertDatesToStrings(flatten(params));
            request = utils.httpRequest.get(requestOptions);
        }

        request.then(
            function (jsonResult) {
                let knowledgeSearchResults = new Array<KnowledgeSearchResult>();
                let knowledgeSearchResultFacets = new Array<SearchResultFacet>();
                let flightIds = new Array<string>();

                if (jsonResult.WebSearchResults && jsonResult.WebSearchResults.length != 0) {
                    for (let i = 0; i < jsonResult.WebSearchResults.length; i++) {
                        let knowledgeSearchResult = new KnowledgeSearchResult();
                        knowledgeSearchResult.title = jsonResult.WebSearchResults[i].Title;
                        knowledgeSearchResult.displayUrl = jsonResult.WebSearchResults[i].DisplayUrl;
                        knowledgeSearchResult.targetUrl = jsonResult.WebSearchResults[i].TargetUrl;
                        knowledgeSearchResult.description = jsonResult.WebSearchResults[i].Description;
                        knowledgeSearchResult.appliesTo = jsonResult.WebSearchResults[i].AppliesTo;
                        knowledgeSearchResult.documentDate = jsonResult.WebSearchResults[i].DocumentDate;
                        knowledgeSearchResult.confidentiality = jsonResult.WebSearchResults[i].Confidentiality;
                        knowledgeSearchResult.contentType = jsonResult.WebSearchResults[i].ContentType;
                        knowledgeSearchResult.documentFamilyId = jsonResult.WebSearchResults[i].DocumentFamilyId;
                        knowledgeSearchResult.audienceLanguage = jsonResult.WebSearchResults[i].AudienceLanguage;
                        knowledgeSearchResult.audienceLocale = jsonResult.WebSearchResults[i].AudienceLocale;
                        knowledgeSearchResult.language = jsonResult.WebSearchResults[i].Language;
                        knowledgeSearchResults.push(knowledgeSearchResult);
                    }
                }

                if (jsonResult.FlightIds && jsonResult.FlightIds.length != 0) {
                    for (let i = 0; i < jsonResult.FlightIds.length; i++) {
                        flightIds.push(jsonResult.FlightIds[i]);
                    }
                }

                if (jsonResult.Facets) {
                    for (let field in jsonResult.Facets) {
                        let facet = new SearchResultFacet();
                        let values = new Array<SearchResultFacetValue>();
                        for (let i = 0; i < jsonResult.Facets[field].length; i++) {
                            let value = new SearchResultFacetValue();
                            value.name = jsonResult.Facets[field][i].Name;
                            value.value = jsonResult.Facets[field][i].Value;
                            value.facetType = jsonResult.Facets[field][i].FacetType;
                            value.count = jsonResult.Facets[field][i].Count;
                            value.from = jsonResult.Facets[field][i].From;
                            value.to = jsonResult.Facets[field][i].To;
                            values.push(value);
                        }

                        facet.field = field;
                        facet.values = values;
                        knowledgeSearchResultFacets.push(facet);
                    }
                }

                let knowledgeSearchResultSet = new KnowledgeSearchResultSet();
                knowledgeSearchResultSet.searchId = jsonResult.SearchId;
                knowledgeSearchResultSet.totalResults = jsonResult.TotalResults;
                knowledgeSearchResultSet.searchResults = knowledgeSearchResults;
                knowledgeSearchResultSet.facets = knowledgeSearchResultFacets;
                knowledgeSearchResultSet.instantAnswerJson = jsonResult.AnswerContentJson;
                knowledgeSearchResultSet.searchProvider = jsonResult.SearchProvider;
                knowledgeSearchResultSet.instantAnswerProvider = jsonResult.InstantAnswerProvider;
                knowledgeSearchResultSet.flightIds = flightIds;
                knowledgeSearchResultSet.suggestId = jsonResult.SuggestId;
                knowledgeSearchResultSet.tenant = jsonResult.CallerId;
                searchDeferred.resolve(knowledgeSearchResultSet);
            }, function (error) {
                console.log(error);
                searchDeferred.reject(error);
            });

        return searchDeferred.promise();
    }

    /**
     * Forms the params to send to the search endpoint
     * @method formSearchUrl
     * @private
     * @static
     * @param {SearchConfig} config A search config object
     */
    function formSearchParams(config: SearchConfig): Object {
        if (config.query == undefined) {
            throw "Please specify a search query";
        }
        let params = <any>{};

        // set certain fields to defaults if they don't exist
        params.query = config.query;
        params.locale = config.locale || DEFAULT_LOCALE;
        params.scopes = config.scopes || DEFAULT_SCOPE;
        params.count = config.count || DEFAULT_SEARCHRESULTSCOUNT;
        params.skip = config.skip == undefined ? 0 : config.skip;
        params.version = config.version || DEFAULT_VERSION;

        // copy over other fields verbatim
        params.searchProvider = config.searchProvider;
        params.instantAnswerProvider = config.instantAnswerProvider;
        params.includeInstantAnswer = config.includeInstantAnswer;
        params.includeWebSearchResults = config.includeWebSearchResults;
        params.enableHitHighlights = config.enableHitHighlights;
        params.insiderMode = config.insiderMode;
        params.suggestId = config.suggestId;

        if (config.version === "6.0") {
            if (config.filter != undefined) {
                params.filter = removeNullValuePairs(config.filter);
            }

            if (config.facets != undefined) {
                params.facets = removeNullValuePairs(config.facets);
            }

            if (config.orderBy != undefined) {
                params.orderBy = config.orderBy;
            }
        }
        return params;
    }

    /**
     * Forms the search endpoint URL
     * @method formSearchUrl
     * @private
     * @static
     * @param {SearchConfig} config A search config object
     * @param {bool} knowledgeSearch Flag indicates whether to use knowledge search
     */
    function formSearchUrl(config: SearchConfig, knowledgeSearch: boolean): string {
        let urlBase: string;
        if (knowledgeSearch) {
            if (config.environment && config.environment.toUpperCase() === "STAGING") {
                urlBase = search.BASE_GATEWAYURI_STG;
            }
            else {
                urlBase = search.BASE_GATEWAYURI;
            }
        }
        else {
            if (config.environment && config.environment.toUpperCase() === "STAGING") {
                urlBase = search.BASE_URI_STG;
            }
            else {
                urlBase = search.BASE_URI;
            }
        }
        if (config.version) {
            return `https://${urlBase}/api/${config.version}/${search.SEARCH_ROUTE}`;
        }
        else {
            return `https://${urlBase}/api/${search.SEARCH_ROUTE}`;
        }
    }

    /**
     * Helper to flatten nested objects to a single level, modified for our api
     * see https://stackoverflow.com/a/49042916/198348 for a reference
     * @method flatten
     * @example
     * 
     * flatten({
     *   "query": "double click",
     *   "locale": "en-us,fr-fr,ja-jp",
     *   "scopes": "default",
     *   "count": "10",
     *   "version": "6.0",
     *   "suggestId": "",
     *   "filter": {
     *     "documentDateFrom": new Date("2016-01-15"),
     *     "documentDateTo": new Date("2018-09-24"),
     *     "contentType": [ "Article", "QnA" ]
     *   },
     *   "facets": {
     *     "confidentiality": { "count": null },
     *     "contentType": { "count": 10 },
     *     "language": { "count": null }
     *   },
     *   "orderBy": [
     *      { "name": OrderByFieldNames.DocumentDate, "direction": OrderByDirection.Descending }, 
     *      { "name": OrderByFieldNames.SearchScore, "direction": OrderByDirection.Ascending }
     *   ]
     * });
     * 
     * // returns this:
     * // {
     * //   "query": "double click",
     * //   "locale": "en-us,fr-fr,ja-jp",
     * //   "scopes": "default",
     * //   "count": "10",
     * //   "skip": "",
     * //   "version": "6.0",
     * //   "suggestId": "",
     * //   "filter.documentDateFrom": new Date("2016-01-15"),
     * //   "filter.documentDateTo": new Date("2018-09-24"),
     * //   "filter.contentType[0]": "Article",
     * //   "filter.contentType[1]": "QnA",
     * //   "facets.confidentiality.count": null,
     * //   "facets.contentType.count": 10,
     * //   "facets.language.count": null,
     * //   "orderBy[0].name": OrderByFieldNames.DocumentDate,
     * //   "orderBy[0].direction": OrderByDirection.Descending,
     * //   "orderBy[1].name": OrderByFieldNames.SearchScore,
     * //   "orderBy[1].direction": OrderByDirection.Ascending
     * // }
     */
    function flatten(obj: any, path = ""): Object {
        if (!(obj instanceof Object) || (obj instanceof Date)) return { [path.replace(/\.$/g, "")]: obj };

        return Object.keys(obj).reduce((output: Object, key: any) => {
            let rest: Object = obj instanceof Array ?
                flatten(obj[key], path.replace(/\.$/g, "") + "[" + key + "].") :
                flatten(obj[key], path + key + ".");
            return Object.assign({}, output, rest);
        }, {});
    }

    /**
     * Converts Date to an ISO string to make it easier for jQuery.param to serialize.
     * Only modifies top level attributes
     * @method convertDatesToStrings
     * @example
     * convertDatesToStrings({"filter.documentDateFrom": new Date("2016-01-15") });
     * // returns {"filter.documentDateFrom":"2016-01-15T00:00:00.000Z"}
     */
    function convertDatesToStrings(obj: any): Object {
        const copy = Object.assign({}, obj);
        for (let key of Object.keys(copy)) {
            if (copy[key] instanceof Date)
                copy[key] = copy[key].toISOString();
        }
        return copy;
    }

    /**
     * Removes keys with null values from an object
     * @example
     * removeNullValuePairs({ a: null, b: null, c: "two", d: 3 })
     * // returns { c: "two", d: 3 }
     * @param obj
     */
    function removeNullValuePairs(obj: any): Object {
        const keys = Object.keys(obj).filter(key => (obj as any)[key] != undefined);
        const pruned = <any>{};
        for (let key of keys)
            pruned[key] = (obj as any)[key];
        return pruned;
    }

}