/// <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;
}
}