This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Dapr Search JavaScript SDK

Index, retrieve, and query documents using the Dapr JavaScript SDK

The Dapr JavaScript SDK lets Node.js applications manage indexes and index, retrieve, delete, and query JSON documents in a configured Dapr search component. Use DaprClient.search with either HTTP or gRPC for full-text search, filters, sorting, highlighting, and pagination.

Search requires Dapr runtime 1.19 or later and JavaScript SDK 3.19.0 or later. The underlying Search API is alpha. Available query features depend on the search component.

To get started, follow the Search how-to guide. See the client usage guidance for method signatures, types, write acknowledgements, cancellation, and errors.

1 - How to: Index and search documents with the Dapr JavaScript SDK

Learn how to configure a search component and manage and query documents using the Dapr JavaScript SDK

Use DaprClient.search to manage indexes and search JSON documents through a Dapr search component. This guide uses Meilisearch and covers index creation, document writes, full-text search, filtering, sorting, pagination, retrieval, and deletion. All examples work over HTTP or gRPC.

Prerequisites

In your application directory, initialize an npm project if you do not already have one, then install the SDK:

npm init -y
npm install @dapr/dapr@3.19.0

The examples use JavaScript ES modules and run as a .mjs file without a TypeScript compiler. The same APIs are available in TypeScript.

Configure Meilisearch

Start a local Meilisearch instance:

docker run -d --name meilisearch -p 7700:7700 -e MEILI_MASTER_KEY=masterKey getmeili/meilisearch:v1.43

Create a components directory in your application directory and save the following as components\search.yaml:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: mysearch
spec:
  type: search.meilisearch
  version: v1
  metadata:
    - name: host
      value: "http://localhost:7700"
    - name: apiKey
      value: "masterKey"

The component’s metadata.name, mysearch, is the store name passed to every client operation. It is not the component type or the YAML file name. See the Meilisearch component reference for settings and provider restrictions.

Create an index and search documents

Save the following as app.mjs:

import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: "3500",
  communicationProtocol: CommunicationProtocolEnum.HTTP,
});

const storeName = "mysearch";
const indexName = "articles";
const waitOptions = {
  mode: "waitForCompletion",
  waitTimeout: "10s",
  onWaitTimeout: "failRequest",
};

try {
  await client.search.createIndex(storeName, indexName, {
    metadata: {
      filterableAttributes: "category,price",
      sortableAttributes: "price",
      searchableAttributes: "title",
    },
  });

  const indexed = await client.search.indexDocuments(
    storeName,
    indexName,
    [
      {
        id: "doc-1",
        content: { title: "Getting started with Dapr", category: "guide", price: 10 },
        metadata: { author: "alice" },
      },
      {
        id: "doc-2",
        content: { title: "Dapr pub/sub deep dive", category: "guide", price: 25 },
      },
      {
        id: "doc-3",
        content: { title: "Search API reference", category: "reference", price: 30 },
      },
    ],
    waitOptions,
  );
  if (indexed.failedItems.length > 0) {
    console.error("Documents not indexed:", indexed.failedItems);
    throw new Error("Document indexing had item failures");
  }
  console.log("Write acknowledgement:", indexed.ack);

  const query = {
    text: "dapr",
    filter: { category: { $eq: "guide" } },
    sort: [{ field: "price", order: "asc" }],
    highlightFields: ["title"],
    includeContent: true,
    topK: 1,
  };
  let continuationToken;
  do {
    const page = await client.search.search(storeName, indexName, {
      ...query,
      continuationToken,
    });
    for (const hit of page.hits) {
      console.log(hit.document.id, hit.document.content, hit.score, hit.highlights);
    }
    continuationToken = page.continuationToken;
  } while (continuationToken);
} finally {
  await client.stop();
}

Start the application with a Dapr sidecar, loading the component from components:

dapr run --app-id search-example --dapr-http-port 3500 --dapr-grpc-port 50001 --resources-path .\components -- node app.mjs

To use gRPC instead, change communicationProtocol to CommunicationProtocolEnum.GRPC and daprPort to "50001". No other search code changes are required.

The example waits for indexing to complete before searching, then returns doc-1 and doc-2 in ascending price order across two pages. Meilisearch highlights matches with <em> tags. Scores and total-hit estimates are provider dependent; do not depend on a particular score value.

Creating an existing index fails with ALREADY_EXISTS and does not update its settings. Before rerunning this application against the same Meilisearch instance, delete the example index using deleteIndex below, or choose a new index name. Unlike an in-memory store, Meilisearch can retain documents across sidecar restarts.

The following snippets reuse client, storeName, indexName, and waitOptions. Add them inside the try block after the search loop and before the client is stopped. Run the deletion examples last.

Inspect indexes

const indexes = await client.search.listIndexes(storeName);
console.log("Indexes:", indexes);

const index = await client.search.getIndex(storeName, indexName);
console.log(index.index, index.documentCount, index.properties);

documentCount is an approximate count, or 0 if the provider cannot supply one efficiently. For Meilisearch, properties includes primaryKey and the configured attribute settings. Meilisearch requires filter and sort attributes to be declared at index creation; the component adds id as a sortable tie-breaker for pagination.

Retrieve and update documents

const documents = await client.search.getDocuments(
  storeName,
  indexName,
  ["doc-2", "missing", "doc-1"],
  { includeContent: true },
);
console.log(documents);

const updated = await client.search.indexDocuments(
  storeName,
  indexName,
  [
    {
      id: "doc-1",
      content: { title: "Updated Dapr guide", category: "guide", price: 12 },
      metadata: { author: "alice", revision: "2" },
    },
  ],
  waitOptions,
);
if (updated.failedItems.length > 0) {
  console.error("Documents not updated:", updated.failedItems);
  throw new Error("Document update had item failures");
}

Retrieval returns found documents in request order and omits missing IDs. Without includeContent: true, it returns IDs and stored metadata, with content undefined. Indexing uses keyed upserts: submit an existing ID to update that document. Supply the complete intended content and metadata rather than assuming a partial-patch operation.

Pass ordinary JSON objects as content; the SDK serializes them and handles transport encoding, including base64 for HTTP. Do not base64-encode or stringify content yourself. Stored document metadata is opaque and is not searchable or filterable. Fields to search, filter, or sort must be in content.

For Meilisearch, document IDs must be 1 to 511 bytes of ASCII letters, digits, hyphens, and underscores. Content must not contain the reserved top-level keys id, daprMetadata, daprPayload, or keys beginning with _. Invalid content is reported in failedItems, while valid documents in the batch can still be indexed.

Shape search results

Restrict full-text search to selected fields and return only the content fields you need:

const projected = await client.search.search(storeName, indexName, {
  text: "dapr",
  searchFields: ["title"],
  filter: {
    $and: [
      { category: { $eq: "guide" } },
      { price: { $lt: 20 } },
    ],
  },
  includeContent: true,
  returnFields: ["title", "price"],
  topK: 10,
});
console.log(projected.hits.map((hit) => hit.document.content));

returnFields alone does not request content; set includeContent: true as well. Omit the query to match all documents, or specify only a filter to browse a subset:

const all = await client.search.search(storeName, indexName);
const guides = await client.search.search(storeName, indexName, {
  filter: { category: "guide" },
  includeContent: true,
});
console.log(all.hits, guides.hits);

Each call returns one page, not necessarily every match. Follow continuationToken as in the complete example. Keep all query fields, including topK, unchanged between pages; only change the token. Tokens are opaque: do not decode, edit, or URL-encode them.

Use a provider-native query

Use native instead of text for provider-specific search features:

const native = await client.search.search(storeName, indexName, {
  native: { q: "dapr", matchingStrategy: "all" },
  topK: 10,
  includeContent: true,
});
console.log(native.hits);

text and native are mutually exclusive. For Meilisearch, native queries must not set offset, limit, page, hitsPerPage, or sort; use the portable topK, continuationToken, and sort fields instead. A native filter is ANDed with the portable filter.

Delete documents and the index

const ack = await client.search.deleteDocuments(
  storeName,
  indexName,
  ["doc-2", "missing"],
  waitOptions,
);
console.log("Delete acknowledgement:", ack);

await client.search.deleteIndex(storeName, indexName);

Deleting missing document IDs is not an error. Document deletion uses the same write acknowledgement options as indexing, but returns the acknowledgement string directly. Deleting an index removes the index and all of its documents.

Next steps

Read the client usage guidance for the full query and result shapes, write acknowledgement modes, cancellation, and error handling. See the Search API overview and Search API reference for runtime semantics.

2 - Search client usage

Essential guidance for configuring and using the JavaScript Search client

Search is available through DaprClient.search in JavaScript SDK 3.19.0 or later and requires Dapr runtime 1.19 or later. HTTP and gRPC use the alpha Search API and expose the same methods and JavaScript result shapes.

Client lifetime and configuration

Reuse a long-lived DaprClient. Await outstanding operations before calling await client.stop() during application shutdown.

HTTP is the default protocol. To explicitly configure gRPC:

import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: "50001",
  communicationProtocol: CommunicationProtocolEnum.GRPC,
  daprApiToken: process.env.DAPR_API_TOKEN,
});

For HTTP, use CommunicationProtocolEnum.HTTP and the sidecar’s HTTP port (default "3500"). Port values are strings. When host and port are not explicitly supplied, the SDK can use DAPR_HTTP_ENDPOINT or DAPR_GRPC_ENDPOINT for the selected protocol, or fall back to the host and port defaults and DAPR_HTTP_PORT or DAPR_GRPC_PORT. The API token defaults to DAPR_API_TOKEN. See client configuration.

Choose an operation

storeName is the search component’s metadata.name. All methods except listIndexes also take an index name. The optional options object is the last argument.

MethodResult
createIndex(storeName, index, options?)Promise<void>
getIndex(storeName, index, options?)Promise<SearchIndex>
listIndexes(storeName, options?)Promise<string[]>
deleteIndex(storeName, index, options?)Promise<void>
indexDocuments<T>(storeName, index, documents, options?)Promise<IndexDocumentsResult>
getDocuments<T>(storeName, index, ids, options?)Promise<SearchDocument<T>[]>
deleteDocuments(storeName, index, ids, options?)Promise<IndexAck>
search<T>(storeName, index, query?, options?)Promise<SearchResult<T>>

TypeScript applications can import IClientSearch, SearchOptions, IndexingOptions, SearchDocument, SearchIndex, SearchQuery, SearchResult, IndexAck, and IndexDocumentsResult as types from @dapr/dapr.

Every operation accepts SearchOptions: metadata?: Readonly<Record<string, string>> and signal?: AbortSignal. This metadata is component-specific request metadata, separate from stored document metadata. For example, createIndex uses it for provider-specific index settings. The SDK handles HTTP body or query-string placement and gRPC request fields.

indexDocuments and deleteDocuments accept IndexingOptions, which extends those options with write acknowledgement controls. getDocuments also accepts includeContent?: boolean.

Document content and TypeScript types

SearchDocument<T> contains id: string, optional content: T, and optional string-map metadata. Content must serialize to a JSON object for indexing. The SDK uses JSON serialization independently of the application’s custom DaprClient serializer, converts content to UTF-8 bytes, and decodes returned content automatically. Do not pass pre-encoded base64 or JSON text as content.

Returned content is undefined unless requested with includeContent: true on getDocuments or on the search query. Stored metadata defaults to an empty object when absent. It is returned unchanged and is never indexed, searched, or filtered.

Generics describe the returned content at compile time; they do not validate documents at runtime. Content defaults to unknown when a type is not supplied. In TypeScript, annotate reusable option objects to retain their string-literal types:

import type { IndexingOptions, SearchDocument, SearchQuery } from "@dapr/dapr";

type Article = { title: string; category: string; price: number };

const documents: SearchDocument<Article>[] = [
  { id: "doc-1", content: { title: "Dapr guide", category: "guide", price: 10 } },
];
const waitOptions: IndexingOptions = {
  mode: "waitForCompletion",
  waitTimeout: "10s",
  onWaitTimeout: "failRequest",
};
const query: SearchQuery = {
  text: "dapr",
  includeContent: true,
  returnFields: ["title", "price"],
};

const indexed = await client.search.indexDocuments(storeName, indexName, documents, waitOptions);
if (indexed.failedItems.length > 0) {
  console.error("Documents not indexed:", indexed.failedItems);
  throw new Error("Document indexing had item failures");
}
const result = await client.search.search<Pick<Article, "title" | "price">>(
  storeName,
  indexName,
  query,
);
for (const hit of result.hits) {
  console.log(hit.document.content?.title);
}

This snippet assumes an existing client, storeName, and indexName. Use a content type that reflects the selected returnFields, not the complete stored document type, when projecting fields.

Write acknowledgements and partial failures

Indexing documents and deleting documents can be asynchronous. IndexAck is "queued" or "completed"; these are SDK strings, not the protobuf enum names used by raw HTTP requests.

OptionsBehavior
Omitted, or mode: "returnOnAcceptance"Return when the provider accepts the write. "queued" does not mean the documents are searchable or deleted yet. A provider can return "completed" if work is already finished.
mode: "waitForCompletion"Wait for completion up to waitTimeout. Requires both a positive protobuf duration, such as "10s" or "0.250s", and an explicit onWaitTimeout.
onWaitTimeout: "continueAsync"On wait timeout, return "queued" and let provider-side work continue. Requires a provider that supports queued acknowledgement.
onWaitTimeout: "failRequest"On wait timeout, reject with DEADLINE_EXCEEDED. Provider-side work may still complete.

Do not supply waitTimeout or onWaitTimeout without mode: "waitForCompletion". Keep waitTimeout shorter than the request deadline. If a component resiliency timeout is configured, leave a margin of at least 10% of that timeout and at least 100 ms, as described in the Search API overview.

indexDocuments resolves to { ack, failedItems }. Always inspect failedItems, even when the promise resolves and ack is "completed". Each failure contains id, numeric errorCode (a canonical non-OK google.rpc.Code), errorMessage, and details. For example, non-object content can produce an item failure with code 3 (INVALID_ARGUMENT) while valid documents in the same batch are indexed.

Only failures known at the acknowledgement boundary are returned. Queued work can fail later, and this API does not provide an operation handle or a method to retrieve its eventual outcome. Use completion mode when subsequent operations depend on write visibility. deleteDocuments returns the acknowledgement string directly, not { ack }.

Document IDs must be non-empty and unique within a write request. Duplicate IDs reject the request rather than producing partial item failures. Upserting the same IDs allows idempotent retries, but do not assume a timeout, cancellation, or failed wait means the provider rolled back the write.

Query options

Pass SearchQuery directly as the third argument of client.search.search, not inside a { query: ... } wrapper.

FieldBehavior
textPortable full-text search string. Mutually exclusive with native.
nativeProvider-native JSON query object. Mutually exclusive with text.
filterPortable JSON filter over content fields. Supports $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $exists, $and, $or, and $not. Dotted paths address nested fields.
topKPage size. Omitted or 0 uses the provider default. Must be an integer from 0 to 4294967295; providers can impose smaller limits.
continuationTokenOpaque token from the previous page. Pass it unchanged with the same query shape.
includeContentDefaults to false. Set to true to return document content.
returnFieldsContent fields to project. Does not implicitly enable includeContent.
searchFieldsFields to search with the portable text query.
sortOrdered { field, order?: "asc" | "desc" } clauses. Omitted order means ascending.
highlightFieldsFields for which to return highlighted fragments.

Omitting both text and native, or omitting the query entirely, matches all documents subject to filter. A field assigned directly to a value in a filter is an equality test; conditions in the same object are ANDed. See the portable filter language.

Provider capabilities determine supported query options and field restrictions. Meilisearch requires filterable and sortable attributes to be declared at index creation. Native query syntax is provider specific and is not portable between components.

Results and pagination

SearchResult<T> has the following fields:

FieldMeaning
hitsArray of { document: SearchDocument<T>, score: number, highlights: Record<string, string> }. Empty when there are no matches.
totalHitsOptional best-effort count. Undefined when unavailable, not necessarily 0.
totalHitsRelation"exact", "lowerBound", "estimate", or "unspecified". Describes the accuracy of totalHits.
continuationTokenOptional token for the next page. Stop paginating when no token is returned.

Scores are provider-specific, unnormalized values. Compare them only within the same result set. Highlight fragments are provider-generated strings, not a substitute for content; handle them appropriately before rendering in an application.

For each subsequent page, copy the original query and change only continuationToken. Changing text or native queries, page size, filters, sort, or field options invalidates the token. If a provider cursor expires, the runtime reports FAILED_PRECONDITION with reason SEARCH_CONTINUATION_EXPIRED; start a new query rather than editing the token.

Unlike raw HTTP responses, the SDK returns documentCount and totalHits as JavaScript numbers. Values beyond Number.MAX_SAFE_INTEGER reject with RangeError rather than silently losing precision. SearchIndex contains index, documentCount, and provider-specific string-map properties.

Cancellation

Every search method accepts an AbortSignal in its final options argument. For example, cancel a search after 30 seconds:

const result = await client.search.search(
  storeName,
  indexName,
  { text: "dapr", includeContent: true },
  { signal: AbortSignal.timeout(30_000) },
);
console.log(result.hits);

This snippet assumes an existing client and index. For writes, include signal alongside mode, waitTimeout, and onWaitTimeout in the same options object. An already-aborted signal rejects without sending the search operation. Cancellation is distinct from the provider’s waitTimeout and does not guarantee cancellation of provider-side writes. Error shapes can differ by protocol.

Errors

Await each operation and handle rejected promises at your application’s error boundary, in addition to checking resolved indexing results for failedItems.

  • HTTP: Runtime failures are JavaScript Error instances whose message is a JSON string containing status (HTTP status), error (status text), and error_msg (runtime response body). Transport, cancellation, and local validation failures can have different shapes; do not assume every error message is JSON.
  • gRPC: Runtime failures preserve ConnectError codes from @connectrpc/connect, not HTTP status codes.
  • Invalid arguments: Empty store, index, or document IDs, store or index names equal to . or .., non-string metadata values, conflicting text and native, or invalid option types reject locally. Invalid topK values and non-positive wait durations are rejected; invalid protobuf duration syntax also rejects. Provider-specific validation still happens at the runtime or component.

Creating an existing index fails with ALREADY_EXISTS; missing stores or indexes and unsupported queries also reject. Missing IDs in getDocuments are omitted, and deleting missing document IDs is not an error.

Per-item details are protobuf JSON objects over HTTP. Over gRPC, each detail is represented as { "@type": typeUrl, value: base64ProtobufBytes }, because provider-specific schemas might not be available to decode it. Do not assume identical detail payloads across protocols.

See the Search API reference for runtime error semantics and the how-to guide for a complete setup and runnable example.