@azure/search-documents
Version:
Azure client library to use AI Search for node.js and browser.
579 lines • 23.2 kB
JavaScript
// 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