UNPKG

@azure/search-documents

Version:
579 lines 23.2 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. import { isTokenCredential } from "@azure/core-auth"; import { bearerTokenAuthenticationPolicy, bearerTokenAuthenticationPolicyName, } from "@azure/core-rest-pipeline"; import { decode, encode } from "./base64.js"; import { SearchClient as GeneratedClient } from "./search/searchClient.js"; import { IndexDocumentsBatch } from "./indexDocumentsBatch.js"; import { logger } from "./logger.js"; import { createOdataMetadataPolicy } from "./odataMetadataPolicy.js"; import { createSearchApiKeyCredentialPolicy } from "./searchApiKeyCredentialPolicy.js"; import { KnownSearchAudience } from "./searchAudience.js"; import { deserialize, serialize } from "./serialization.js"; import * as utils from "./serviceUtils.js"; import { tracingClient } from "./tracing.js"; /** * 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 { /// 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. */ serviceVersion = utils.defaultServiceVersion; /** * The API version to use when communicating with the service. * @deprecated use {@Link serviceVersion} instead */ apiVersion = utils.defaultServiceVersion; /** * The endpoint of the search service */ endpoint; /** * The name of the index */ indexName; /** * @hidden * A reference to the auto-generated SearchClient */ client; /** * A reference to the internal HTTP pipeline for use with raw requests */ pipeline; /** * Creates an instance of SearchClient. * * Example usage: * ```ts snippet:ReadmeSampleSearchClient * import { SearchClient, AzureKeyCredential } from "@azure/search-documents"; * * const searchClient = new SearchClient( * "<endpoint>", * "<indexName>", * new AzureKeyCredential("<apiKey>"), * ); * ``` * * Optionally, the type of the model can be used to enable strong typing and type hints: * ```ts snippet:ReadmeSampleSearchClientWithModel * import { SearchClient, AzureKeyCredential } from "@azure/search-documents"; * * type TModel = { * keyName: string; * field1?: string | null; * field2?: { * anotherField?: string | null; * } | null; * }; * * const searchClient = new SearchClient<TModel>( * "<endpoint>", * "<indexName>", * new AzureKeyCredential("<apiKey>"), * ); * ``` * * @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 = {}) { this.endpoint = endpoint; this.indexName = indexName; const internalClientPipelineOptions = { ...options, ...{ loggingOptions: { logger: logger.info, additionalAllowedHeaderNames: [ "elapsed-time", "Location", "OData-MaxVersion", "OData-Version", "Prefer", "throttle-reason", ], }, }, }; this.serviceVersion = options.serviceVersion ?? options.apiVersion ?? utils.defaultServiceVersion; this.apiVersion = this.serviceVersion; this.client = new GeneratedClient(this.endpoint, credential, this.indexName, internalClientPipelineOptions); this.pipeline = this.client.pipeline; // Replaced with a custom policy below this.pipeline.removePolicy({ name: bearerTokenAuthenticationPolicyName }); 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. */ // eslint-disable-next-line @azure/azure-sdk/ts-naming-options async getDocumentsCount(options = {}) { return tracingClient.withSpan("SearchClient-getDocumentsCount", options, async (updatedOptions) => { const count = await this.client.getDocumentCount(updatedOptions); return Number(count); }); } /** * 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 snippet:ReadmeSampleAutocomplete * import { SearchClient, AzureKeyCredential, 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 = {}) { if (!searchText) { throw new RangeError("searchText must be provided."); } if (!suggesterName) { throw new RangeError("suggesterName must be provided."); } const { searchFields, ...restOptions } = options; return tracingClient.withSpan("SearchClient-autocomplete", options, async (updatedOptions) => { return this.client.autocompletePost(searchText, suggesterName, { ...updatedOptions, ...restOptions, // Cast readonly array to mutable - the generated code doesn't mutate it searchFields: searchFields, }); }); } async searchDocuments(searchText, options = {}, nextPageParameters = {}) { const { includeTotalCount, orderBy, searchFields, select, highlightFields, vectorSearchOptions, semanticSearchOptions, debug, ...restOptions } = options; const { semanticFields, configurationName, errorMode, answers, captions, debugMode, ...restSemanticOptions } = semanticSearchOptions ?? {}; const { queries, filterMode, ...restVectorOptions } = vectorSearchOptions ?? {}; const fullOptions = { ...restSemanticOptions, ...restVectorOptions, ...restOptions, ...nextPageParameters, searchFields: this.convertSearchFields(searchFields), select: this.convertSelect(select) || "*", highlightFields: highlightFields?.split(","), orderBy: this.convertOrderBy(orderBy), includeTotalCount, vectorQueries: queries?.map(this.convertVectorQuery.bind(this)), answers: this.convertQueryAnswers(answers), captions: this.convertQueryCaptions(captions), semanticErrorHandling: errorMode, semanticConfigurationName: configurationName, debug: debugMode ?? debug, // Use semanticSearchOptions.debugMode if set, otherwise use top-level debug vectorFilterMode: filterMode, }; return tracingClient.withSpan("SearchClient-searchDocuments", fullOptions, async (updatedOptions) => { const result = await this.client.searchPost({ ...updatedOptions, searchText: searchText, }); const { results, nextLink, nextPageParameters: resultNextPageParameters, semanticPartialResponseReason: semanticErrorReason, semanticPartialResponseType: semanticSearchResultsType, facets, answers: resultAnswers, ...restResult } = result; const modifiedResults = utils.generatedSearchResultToPublicSearchResult(results); const converted = { ...restResult, facets: utils.convertGeneratedFacetsToPublic(facets), answers: utils.convertGeneratedAnswersToPublic(resultAnswers), results: modifiedResults, semanticErrorReason, semanticSearchResultsType, continuationToken: this.encodeContinuationToken(nextLink, resultNextPageParameters), }; return deserialize(converted); }); } async *listSearchResultsPage(searchText, options = {}, settings = {}) { let decodedContinuation = this.decodeContinuationToken(settings.continuationToken); let result = await this.searchDocuments(searchText, options, decodedContinuation?.nextPageParameters); yield 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 = await this.searchDocuments(searchText, options, decodedContinuation?.nextPageParameters); yield result; } } async *listSearchResultsAll(firstPage, searchText, options = {}) { yield* firstPage.results; if (firstPage.continuationToken) { for await (const page of this.listSearchResultsPage(searchText, options, { continuationToken: firstPage.continuationToken, })) { yield* page.results; } } } 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 snippet:ReadmeSampleSearchTModel * import { SearchClient, AzureKeyCredential, 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 = {}) { return tracingClient.withSpan("SearchClient-search", options, async (updatedOptions) => { const pageResult = await this.searchDocuments(searchText, updatedOptions); return { ...pageResult, results: this.listSearchResults(pageResult, searchText, updatedOptions), }; }); } /** * 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 snippet:ReadmeSampleSuggest * import { SearchClient, AzureKeyCredential, 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, ...nonFieldOptions } = options; const fullOptions = { // Cast readonly arrays to mutable - the generated code doesn't mutate them searchFields: this.convertSearchFields(searchFields), select: this.convertSelect(select), orderBy: this.convertOrderBy(orderBy), ...nonFieldOptions, }; if (!searchText) { throw new RangeError("searchText must be provided."); } if (!suggesterName) { throw new RangeError("suggesterName must be provided."); } return tracingClient.withSpan("SearchClient-suggest", fullOptions, async (updatedOptions) => { const result = await this.client.suggestPost(searchText, suggesterName, updatedOptions); const modifiedResult = utils.generatedSuggestDocumentsResultToPublicSuggestDocumentsResult(result); return deserialize(modifiedResult); }); } /** * 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 = {}) { return tracingClient.withSpan("SearchClient-getDocument", options, async (updatedOptions) => { const result = await this.client.getDocument(key, { ...updatedOptions, selectedFields: updatedOptions.selectedFields, }); // The generated code puts document fields in additionalProperties return deserialize(result.additionalProperties ?? {}); }); } /** * 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://learn.microsoft.com/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 = {}) { return tracingClient.withSpan("SearchClient-indexDocuments", options, async (updatedOptions) => { let status = 0; const serializedActions = serialize(batch.actions); const result = await this.client.index({ actions: utils.convertPublicActionsToGeneratedActions(serializedActions) }, { ...updatedOptions, onResponse: (rawResponse, flatResponse) => { status = rawResponse.status; if (updatedOptions.onResponse) { updatedOptions.onResponse(rawResponse, flatResponse); } }, }); if (options.throwOnAnyFailure && status === 207) { throw result; } return result; }); } /** * Upload an array of documents to the index. * @param documents - The documents to upload. * @param options - Additional options. */ async uploadDocuments(documents, options = {}) { return tracingClient.withSpan("SearchClient-uploadDocuments", options, async (updatedOptions) => { const batch = new IndexDocumentsBatch(); batch.upload(documents); return this.indexDocuments(batch, updatedOptions); }); } /** * Update a set of documents in the index. * * For more details about how merging works, see * https://learn.microsoft.com/rest/api/searchservice/AddUpdate-or-Delete-Documents * @param documents - The updated documents. * @param options - Additional options. */ async mergeDocuments(documents, options = {}) { return tracingClient.withSpan("SearchClient-mergeDocuments", options, async (updatedOptions) => { const batch = new IndexDocumentsBatch(); batch.merge(documents); return this.indexDocuments(batch, updatedOptions); }); } /** * 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://learn.microsoft.com/rest/api/searchservice/AddUpdate-or-Delete-Documents * @param documents - The updated documents. * @param options - Additional options. */ async mergeOrUploadDocuments(documents, options = {}) { return tracingClient.withSpan("SearchClient-mergeOrUploadDocuments", options, async (updatedOptions) => { const batch = new IndexDocumentsBatch(); batch.mergeOrUpload(documents); return this.indexDocuments(batch, updatedOptions); }); } async deleteDocuments(keyNameOrDocuments, keyValuesOrOptions, options = {}) { return tracingClient.withSpan("SearchClient-deleteDocuments", options, async (updatedOptions) => { const batch = new IndexDocumentsBatch(); if (typeof keyNameOrDocuments === "string") { batch.delete(keyNameOrDocuments, keyValuesOrOptions); } else { batch.delete(keyNameOrDocuments); } return this.indexDocuments(batch, updatedOptions); }); } 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 undefined; } convertVectorQueryFields(fields) { if (fields) { return fields.join(","); } return undefined; } convertSearchFields(searchFields) { if (searchFields) { return searchFields.join(","); } return undefined; } convertOrderBy(orderBy) { if (orderBy) { return orderBy.join(","); } return undefined; } convertQueryAnswers(answers) { if (!answers) { return undefined; } const config = []; const { answerType: output, count, threshold, maxAnswerLength } = answers; if (count) { config.push(`count-${count}`); } if (threshold) { config.push(`threshold-${threshold}`); } if (maxAnswerLength) { config.push(`maxcharlength-${maxAnswerLength}`); } if (config.length) { return output + `|${config.join(",")}`; } return output; } convertQueryCaptions(captions) { if (!captions) { return undefined; } const config = []; const { captionType: output, highlight, maxCaptionLength } = captions; if (highlight !== undefined) { config.push(`highlight-${highlight}`); } if (maxCaptionLength) { config.push(`maxcharlength-${maxCaptionLength}`); } if (config.length) { return output + `|${config.join(",")}`; } return output; } convertVectorQuery(vectorQuery) { switch (vectorQuery.kind) { case "text": { const { fields, ...restFields } = vectorQuery; return { ...restFields, fields: this.convertVectorQueryFields(fields), }; } case "vector": case "imageUrl": { return { ...vectorQuery, fields: this.convertVectorQueryFields(vectorQuery?.fields) }; } case "imageBinary": { // Map convenience layer's binaryImage to generated layer's base64Image const { binaryImage, fields, ...rest } = vectorQuery; return { ...rest, base64Image: binaryImage, fields: this.convertVectorQueryFields(fields), }; } default: { logger.warning("Unknown vector query kind; sending without serialization"); return vectorQuery; } } } } //# sourceMappingURL=searchClient.js.map