@azure/search-documents
Version:
Azure client library to use Cognitive Search for node.js and browser.
620 lines • 25 kB
JavaScript
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { __asyncDelegator, __asyncGenerator, __asyncValues, __await, __rest } from "tslib";
/// <reference lib="esnext.asynciterable" />
import { isTokenCredential } from "@azure/core-auth";
import { bearerTokenAuthenticationPolicy } from "@azure/core-rest-pipeline";
import { decode, encode } from "./base64";
import { SearchClient as GeneratedClient } from "./generated/data/searchClient";
import { IndexDocumentsBatch } from "./indexDocumentsBatch";
import { logger } from "./logger";
import { createOdataMetadataPolicy } from "./odataMetadataPolicy";
import { createSearchApiKeyCredentialPolicy } from "./searchApiKeyCredentialPolicy";
import { KnownSearchAudience } from "./searchAudience";
import { deserialize, serialize } from "./serialization";
import * as utils from "./serviceUtils";
import { createSpan } from "./tracing";
/**
* Class used to perform operations against a search index,
* including querying documents in the index as well as
* adding, updating, and removing them.
*/
export class SearchClient {
/**
* Creates an instance of SearchClient.
*
* Example usage:
* ```ts
* const { SearchClient, AzureKeyCredential } = require("@azure/search-documents");
*
* const client = new SearchClient(
* "<endpoint>",
* "<indexName>",
* new AzureKeyCredential("<Admin Key>")
* );
* ```
*
* Optionally, the type of the model can be used to enable strong typing and type hints:
* ```ts
* type TModel = {
* keyName: string;
* field1?: string | null;
* field2?: { anotherField?: string | null } | null;
* };
*
* const client = new SearchClient<TModel>(
* ...
* );
* ```
*
* @param endpoint - The endpoint of the search service
* @param indexName - The name of the index
* @param credential - Used to authenticate requests to the service.
* @param options - Used to configure the Search client.
*
* @typeParam TModel - An optional type that represents the documents stored in
* the search index. For the best typing experience, all non-key fields should
* be marked optional and nullable, and the key property should have the
* non-nullable type `string`.
*/
constructor(endpoint, indexName, credential, options = {}) {
var _a, _b;
/// Maintenance note: when updating supported API versions,
/// the ContinuationToken logic will need to be updated below.
/**
* The service version to use when communicating with the service.
*/
this.serviceVersion = utils.defaultServiceVersion;
/**
* The API version to use when communicating with the service.
* @deprecated use {@Link serviceVersion} instead
*/
this.apiVersion = utils.defaultServiceVersion;
this.endpoint = endpoint;
this.indexName = indexName;
const internalClientPipelineOptions = Object.assign(Object.assign({}, options), {
loggingOptions: {
logger: logger.info,
additionalAllowedHeaderNames: [
"elapsed-time",
"Location",
"OData-MaxVersion",
"OData-Version",
"Prefer",
"throttle-reason",
],
},
});
this.serviceVersion =
(_b = (_a = options.serviceVersion) !== null && _a !== void 0 ? _a : options.apiVersion) !== null && _b !== void 0 ? _b : utils.defaultServiceVersion;
this.apiVersion = this.serviceVersion;
this.client = new GeneratedClient(this.endpoint, this.indexName, this.serviceVersion, internalClientPipelineOptions);
if (isTokenCredential(credential)) {
const scope = options.audience
? `${options.audience}/.default`
: `${KnownSearchAudience.AzurePublicCloud}/.default`;
this.client.pipeline.addPolicy(bearerTokenAuthenticationPolicy({ credential, scopes: scope }));
}
else {
this.client.pipeline.addPolicy(createSearchApiKeyCredentialPolicy(credential));
}
this.client.pipeline.addPolicy(createOdataMetadataPolicy("none"));
}
/**
* Retrieves the number of documents in the index.
* @param options - Options to the count operation.
*/
async getDocumentsCount(options = {}) {
const { span, updatedOptions } = createSpan("SearchClient-getDocumentsCount", options);
try {
let documentsCount = 0;
await this.client.documents.count(Object.assign(Object.assign({}, updatedOptions), { onResponse: (rawResponse, flatResponse) => {
documentsCount = Number(rawResponse.bodyAsText);
if (updatedOptions.onResponse) {
updatedOptions.onResponse(rawResponse, flatResponse);
}
} }));
return documentsCount;
}
catch (e) {
span.setStatus({
status: "error",
error: e.message,
});
throw e;
}
finally {
span.end();
}
}
/**
* Based on a partial searchText from the user, return a list
* of potential completion strings based on a specified suggester.
* @param searchText - The search text on which to base autocomplete results.
* @param suggesterName - The name of the suggester as specified in the suggesters collection that's part of the index definition.
* @param options - Options to the autocomplete operation.
* @example
* ```ts
* import {
* AzureKeyCredential,
* SearchClient,
* SearchFieldArray,
* } from "@azure/search-documents";
*
* type TModel = {
* key: string;
* azure?: { sdk: string | null } | null;
* };
*
* const client = new SearchClient<TModel>(
* "endpoint.azure",
* "indexName",
* new AzureKeyCredential("key")
* );
*
* const searchFields: SearchFieldArray<TModel> = ["azure/sdk"];
*
* const autocompleteResult = await client.autocomplete(
* "searchText",
* "suggesterName",
* { searchFields }
* );
* ```
*/
async autocomplete(searchText, suggesterName, options = {}) {
const { searchFields } = options, nonFieldOptions = __rest(options, ["searchFields"]);
const fullOptions = Object.assign({ searchText: searchText, suggesterName: suggesterName, searchFields: this.convertSearchFields(searchFields) }, nonFieldOptions);
if (!fullOptions.searchText) {
throw new RangeError("searchText must be provided.");
}
if (!fullOptions.suggesterName) {
throw new RangeError("suggesterName must be provided.");
}
const { span, updatedOptions } = createSpan("SearchClient-autocomplete", options);
try {
const result = await this.client.documents.autocompletePost(fullOptions, updatedOptions);
return result;
}
catch (e) {
span.setStatus({
status: "error",
error: e.message,
});
throw e;
}
finally {
span.end();
}
}
async searchDocuments(searchText, options = {}, nextPageParameters = {}) {
const _a = options, { includeTotalCount, orderBy, searchFields, select, vectorSearchOptions, semanticSearchOptions } = _a, restOptions = __rest(_a, ["includeTotalCount", "orderBy", "searchFields", "select", "vectorSearchOptions", "semanticSearchOptions"]);
const _b = semanticSearchOptions !== null && semanticSearchOptions !== void 0 ? semanticSearchOptions : {}, { configurationName, errorMode, answers, captions } = _b, restSemanticOptions = __rest(_b, ["configurationName", "errorMode", "answers", "captions"]);
const _c = vectorSearchOptions !== null && vectorSearchOptions !== void 0 ? vectorSearchOptions : {}, { queries, filterMode } = _c, restVectorOptions = __rest(_c, ["queries", "filterMode"]);
const fullOptions = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, restSemanticOptions), restVectorOptions), restOptions), nextPageParameters), { searchFields: this.convertSearchFields(searchFields), select: this.convertSelect(select) || "*", orderBy: this.convertOrderBy(orderBy), includeTotalResultCount: includeTotalCount, vectorQueries: queries === null || queries === void 0 ? void 0 : queries.map(this.convertVectorQuery.bind(this)), answers: this.convertQueryAnswers(answers), captions: this.convertQueryCaptions(captions), semanticErrorHandling: errorMode, semanticConfigurationName: configurationName, vectorFilterMode: filterMode });
const { span, updatedOptions } = createSpan("SearchClient-searchDocuments", options);
try {
const result = await this.client.documents.searchPost(Object.assign(Object.assign({}, fullOptions), { searchText: searchText }), updatedOptions);
const _d = result, { results, nextLink, nextPageParameters: resultNextPageParameters, semanticPartialResponseReason: semanticErrorReason, semanticPartialResponseType: semanticSearchResultsType } = _d, restResult = __rest(_d, ["results", "nextLink", "nextPageParameters", "semanticPartialResponseReason", "semanticPartialResponseType"]);
const modifiedResults = utils.generatedSearchResultToPublicSearchResult(results);
const converted = Object.assign(Object.assign({}, restResult), { results: modifiedResults, semanticErrorReason,
semanticSearchResultsType, continuationToken: this.encodeContinuationToken(nextLink, resultNextPageParameters) });
return deserialize(converted);
}
catch (e) {
span.setStatus({
status: "error",
error: e.message,
});
throw e;
}
finally {
span.end();
}
}
listSearchResultsPage(searchText, options = {}, settings = {}) {
return __asyncGenerator(this, arguments, function* listSearchResultsPage_1() {
let decodedContinuation = this.decodeContinuationToken(settings.continuationToken);
let result = yield __await(this.searchDocuments(searchText, options, decodedContinuation === null || decodedContinuation === void 0 ? void 0 : decodedContinuation.nextPageParameters));
yield yield __await(result);
// Technically, we should also leverage nextLink, but the generated code
// doesn't support this yet.
while (result.continuationToken) {
decodedContinuation = this.decodeContinuationToken(result.continuationToken);
result = yield __await(this.searchDocuments(searchText, options, decodedContinuation === null || decodedContinuation === void 0 ? void 0 : decodedContinuation.nextPageParameters));
yield yield __await(result);
}
});
}
listSearchResultsAll(firstPage, searchText, options = {}) {
return __asyncGenerator(this, arguments, function* listSearchResultsAll_1() {
var _a, e_1, _b, _c;
yield __await(yield* __asyncDelegator(__asyncValues(firstPage.results)));
if (firstPage.continuationToken) {
try {
for (var _d = true, _e = __asyncValues(this.listSearchResultsPage(searchText, options, {
continuationToken: firstPage.continuationToken,
})), _f; _f = yield __await(_e.next()), _a = _f.done, !_a; _d = true) {
_c = _f.value;
_d = false;
const page = _c;
yield __await(yield* __asyncDelegator(__asyncValues(page.results)));
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (!_d && !_a && (_b = _e.return)) yield __await(_b.call(_e));
}
finally { if (e_1) throw e_1.error; }
}
}
});
}
listSearchResults(firstPage, searchText, options = {}) {
const iter = this.listSearchResultsAll(firstPage, searchText, options);
return {
next() {
return iter.next();
},
[Symbol.asyncIterator]() {
return this;
},
byPage: (settings = {}) => {
return this.listSearchResultsPage(searchText, options, settings);
},
};
}
/**
* Performs a search on the current index given
* the specified arguments.
* @param searchText - Text to search
* @param options - Options for the search operation.
* @example
* ```ts
* import {
* AzureKeyCredential,
* SearchClient,
* SearchFieldArray,
* } from "@azure/search-documents";
*
* type TModel = {
* key: string;
* azure?: { sdk: string | null } | null;
* };
*
* const client = new SearchClient<TModel>(
* "endpoint.azure",
* "indexName",
* new AzureKeyCredential("key")
* );
*
* const select = ["azure/sdk"] as const;
* const searchFields: SearchFieldArray<TModel> = ["azure/sdk"];
*
* const searchResult = await client.search("searchText", {
* select,
* searchFields,
* });
* ```
*/
async search(searchText, options) {
const { span, updatedOptions } = createSpan("SearchClient-search", options);
try {
const pageResult = await this.searchDocuments(searchText, updatedOptions);
return Object.assign(Object.assign({}, pageResult), { results: this.listSearchResults(pageResult, searchText, updatedOptions) });
}
catch (e) {
span.setStatus({
status: "error",
error: e.message,
});
throw e;
}
finally {
span.end();
}
}
/**
* Returns a short list of suggestions based on the searchText
* and specified suggester.
* @param searchText - The search text to use to suggest documents. Must be at least 1 character, and no more than 100 characters.
* @param suggesterName - The name of the suggester as specified in the suggesters collection that's part of the index definition.
* @param options - Options for the suggest operation
* @example
* ```ts
* import {
* AzureKeyCredential,
* SearchClient,
* SearchFieldArray,
* } from "@azure/search-documents";
*
* type TModel = {
* key: string;
* azure?: { sdk: string | null } | null;
* };
*
* const client = new SearchClient<TModel>(
* "endpoint.azure",
* "indexName",
* new AzureKeyCredential("key")
* );
*
* const select = ["azure/sdk"] as const;
* const searchFields: SearchFieldArray<TModel> = ["azure/sdk"];
*
* const suggestResult = await client.suggest("searchText", "suggesterName", {
* select,
* searchFields,
* });
* ```
*/
async suggest(searchText, suggesterName, options = {}) {
const { select, searchFields, orderBy } = options, nonFieldOptions = __rest(options, ["select", "searchFields", "orderBy"]);
const fullOptions = Object.assign({ searchText: searchText, suggesterName: suggesterName, searchFields: this.convertSearchFields(searchFields), select: this.convertSelect(select), orderBy: this.convertOrderBy(orderBy) }, nonFieldOptions);
if (!fullOptions.searchText) {
throw new RangeError("searchText must be provided.");
}
if (!fullOptions.suggesterName) {
throw new RangeError("suggesterName must be provided.");
}
const { span, updatedOptions } = createSpan("SearchClient-suggest", options);
try {
const result = await this.client.documents.suggestPost(fullOptions, updatedOptions);
const modifiedResult = utils.generatedSuggestDocumentsResultToPublicSuggestDocumentsResult(result);
return deserialize(modifiedResult);
}
catch (e) {
span.setStatus({
status: "error",
error: e.message,
});
throw e;
}
finally {
span.end();
}
}
/**
* Retrieve a particular document from the index by key.
* @param key - The primary key value of the document
* @param options - Additional options
*/
async getDocument(key, options = {}) {
const { span, updatedOptions } = createSpan("SearchClient-getDocument", options);
try {
const result = await this.client.documents.get(key, Object.assign(Object.assign({}, updatedOptions), { selectedFields: updatedOptions.selectedFields }));
return deserialize(result);
}
catch (e) {
span.setStatus({
status: "error",
error: e.message,
});
throw e;
}
finally {
span.end();
}
}
/**
* Perform a set of index modifications (upload, merge, mergeOrUpload, delete)
* for the given set of documents.
* This operation may partially succeed and not all document operations will
* be reflected in the index. If you would like to treat this as an exception,
* set the `throwOnAnyFailure` option to true.
* For more details about how merging works, see: https://docs.microsoft.com/en-us/rest/api/searchservice/AddUpdate-or-Delete-Documents
* @param batch - An array of actions to perform on the index.
* @param options - Additional options.
*/
async indexDocuments(
// eslint-disable-next-line @azure/azure-sdk/ts-use-interface-parameters
batch, options = {}) {
const { span, updatedOptions } = createSpan("SearchClient-indexDocuments", options);
try {
let status = 0;
const result = await this.client.documents.index({ actions: serialize(batch.actions) }, Object.assign(Object.assign({}, updatedOptions), { onResponse: (rawResponse, flatResponse) => {
status = rawResponse.status;
if (updatedOptions.onResponse) {
updatedOptions.onResponse(rawResponse, flatResponse);
}
} }));
if (options.throwOnAnyFailure && status === 207) {
throw result;
}
return result;
}
catch (e) {
span.setStatus({
status: "error",
error: e.message,
});
throw e;
}
finally {
span.end();
}
}
/**
* Upload an array of documents to the index.
* @param documents - The documents to upload.
* @param options - Additional options.
*/
async uploadDocuments(documents, options = {}) {
const { span, updatedOptions } = createSpan("SearchClient-uploadDocuments", options);
const batch = new IndexDocumentsBatch();
batch.upload(documents);
try {
return await this.indexDocuments(batch, updatedOptions);
}
catch (e) {
span.setStatus({
status: "error",
error: e.message,
});
throw e;
}
finally {
span.end();
}
}
/**
* Update a set of documents in the index.
* For more details about how merging works, see https://docs.microsoft.com/en-us/rest/api/searchservice/AddUpdate-or-Delete-Documents
* @param documents - The updated documents.
* @param options - Additional options.
*/
async mergeDocuments(documents, options = {}) {
const { span, updatedOptions } = createSpan("SearchClient-mergeDocuments", options);
const batch = new IndexDocumentsBatch();
batch.merge(documents);
try {
return await this.indexDocuments(batch, updatedOptions);
}
catch (e) {
span.setStatus({
status: "error",
error: e.message,
});
throw e;
}
finally {
span.end();
}
}
/**
* Update a set of documents in the index or upload them if they don't exist.
* For more details about how merging works, see https://docs.microsoft.com/en-us/rest/api/searchservice/AddUpdate-or-Delete-Documents
* @param documents - The updated documents.
* @param options - Additional options.
*/
async mergeOrUploadDocuments(documents, options = {}) {
const { span, updatedOptions } = createSpan("SearchClient-mergeDocuments", options);
const batch = new IndexDocumentsBatch();
batch.mergeOrUpload(documents);
try {
return await this.indexDocuments(batch, updatedOptions);
}
catch (e) {
span.setStatus({
status: "error",
error: e.message,
});
throw e;
}
finally {
span.end();
}
}
async deleteDocuments(keyNameOrDocuments, keyValuesOrOptions, options = {}) {
const { span, updatedOptions } = createSpan("SearchClient-deleteDocuments", options);
const batch = new IndexDocumentsBatch();
if (typeof keyNameOrDocuments === "string") {
batch.delete(keyNameOrDocuments, keyValuesOrOptions);
}
else {
batch.delete(keyNameOrDocuments);
}
try {
return await this.indexDocuments(batch, updatedOptions);
}
catch (e) {
span.setStatus({
status: "error",
error: e.message,
});
throw e;
}
finally {
span.end();
}
}
encodeContinuationToken(nextLink, nextPageParameters) {
if (!nextLink || !nextPageParameters) {
return undefined;
}
const payload = JSON.stringify({
apiVersion: this.apiVersion,
nextLink,
nextPageParameters,
});
return encode(payload);
}
decodeContinuationToken(token) {
if (!token) {
return undefined;
}
const decodedToken = decode(token);
try {
const result = JSON.parse(decodedToken);
if (result.apiVersion !== this.apiVersion) {
throw new RangeError(`Continuation token uses unsupported apiVersion "${this.apiVersion}"`);
}
return {
nextLink: result.nextLink,
nextPageParameters: result.nextPageParameters,
};
}
catch (e) {
throw new Error(`Corrupted or invalid continuation token: ${decodedToken}`);
}
}
convertSelect(select) {
if (select) {
return select.join(",");
}
return select;
}
convertVectorQueryFields(fields) {
if (fields) {
return fields.join(",");
}
return fields;
}
convertSearchFields(searchFields) {
if (searchFields) {
return searchFields.join(",");
}
return searchFields;
}
convertOrderBy(orderBy) {
if (orderBy) {
return orderBy.join(",");
}
return orderBy;
}
convertQueryAnswers(answers) {
if (!answers) {
return answers;
}
const config = [];
const { answerType: output, count, threshold } = answers;
if (count) {
config.push(`count-${count}`);
}
if (threshold) {
config.push(`threshold-${threshold}`);
}
if (config.length) {
return output + `|${config.join(",")}`;
}
return output;
}
convertQueryCaptions(captions) {
if (!captions) {
return captions;
}
const config = [];
const { captionType: output, highlight } = captions;
if (highlight !== undefined) {
config.push(`highlight-${highlight}`);
}
if (config.length) {
return output + `|${config.join(",")}`;
}
return output;
}
convertVectorQuery(vectorQuery) {
return Object.assign(Object.assign({}, vectorQuery), { fields: this.convertVectorQueryFields(vectorQuery === null || vectorQuery === void 0 ? void 0 : vectorQuery.fields) });
}
}
//# sourceMappingURL=searchClient.js.map