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

Return to the regular view of this page.

Dapr Search .NET SDK

Index and query JSON documents using the Dapr Search .NET SDK

The Dapr.Search package provides a dedicated .NET client for Dapr’s Search building block. It supports index management, typed document upserts and retrieval, full-text and provider-native queries, filters, sorting, projection, highlighting, and continuation-token pagination. A bundled source generator can create a typed client bound to a component, index, and document type.

These guides target Dapr .NET SDK 1.19.0 or later and Dapr runtime 1.19 or later.

Start with How to: Index and search documents. For query options, serialization, generated clients, and error handling, see the Search client guidance.

Search accepts index-ready JSON objects; it does not extract text, chunk documents, or generate embeddings. For similarity search over embeddings, use the Vector .NET SDK.

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

Create indexes, write typed documents, search, and retrieve documents using Dapr.Search

Use DaprSearchClient to index JSON documents and run full-text queries through a Dapr search component. In this guide, you will configure a local store, create an index, write and search typed documents, and delete them.

Prerequisites

Install the package in an ASP.NET Core project:

dotnet add package Dapr.Search

The Search SDK overview describes the alpha API and release requirements.

Configure a search component

Create components\searchstore.yaml in your application directory:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: searchstore
spec:
  type: search.in-memory
  version: v1
  metadata:
  - name: indexingDelay
    value: "0s"

The in-memory component needs no external service. Data is lost when the sidecar stops and is not shared between sidecars. Use it for local development and testing, not durable storage.

For production, configure a supported search component. Component capabilities differ: for example, the in-memory component does not support highlighting or native queries.

Register and resolve the client

Add the following to Program.cs. The client reads the sidecar endpoint and API token from configuration or the environment.

using Dapr.Search;
using Dapr.Search.Extensions;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprSearchClient();

var app = builder.Build();
var client = app.Services.GetRequiredService<DaprSearchClient>();

Place the remaining operation snippets after this code, then call app.Run(); at the end of Program.cs. Create the document type in Article.cs:

public sealed record Article(string Title, string Category, decimal Price);

Run the application with a sidecar that loads the component:

dapr run --app-id search-example --resources-path .\components -- dotnet run

The .NET Search client uses gRPC; it is separate from DaprClient and does not require AddDaprClient().

Create and inspect an index

const string storeName = "searchstore";
const string indexName = "articles";

await client.CreateIndexAsync(storeName, indexName);

var info = await client.GetIndexAsync(storeName, indexName);
Console.WriteLine($"{info.Index}: approximately {info.DocumentCount} documents");

var indexes = await client.ListIndexesAsync(storeName);
Console.WriteLine(string.Join(", ", indexes));

Creating an existing index fails with AlreadyExists; it does not update the index settings. If you keep the sidecar running between executions, delete the example index before creating it again.

CreateIndexAsync accepts component-specific settings in its metadata argument. For example, the Meilisearch component requires filterableAttributes and sortableAttributes to declare fields used for filtering and sorting. These settings are not needed for the in-memory example.

Index typed documents

var writeOptions = new IndexingOptions
{
    Mode = IndexingMode.WaitForCompletion,
    WaitTimeout = TimeSpan.FromSeconds(10),
    OnWaitTimeout = IndexingWaitTimeoutAction.FailRequest
};

var write = await client.IndexDocumentsAsync<Article>(
    storeName,
    indexName,
    [
        new("doc-1", new Article("Getting started with Dapr", "guide", 10),
            new Dictionary<string, string> { ["author"] = "alice" }),
        new("doc-2", new Article("Dapr pub/sub deep dive", "guide", 25)),
        new("doc-3", new Article("Search API reference", "reference", 30))
    ],
    writeOptions);

foreach (var failure in write.FailedItems)
{
    Console.Error.WriteLine($"{failure.Id}: {failure.ErrorCode} {failure.ErrorMessage}");
}

if (write.FailedItems.Count != 0 || write.Ack != IndexAck.Completed)
{
    throw new InvalidOperationException("Not all documents were confirmed indexed.");
}

SearchDocument<T> contains an ID, typed content, and optional string metadata. The SDK serializes content to UTF-8 JSON using the client’s JSON options. Content must serialize to a JSON object; invalid content is reported as an item failure by the runtime. IDs must be non-empty and unique within a write request. Writing an existing ID upserts that document.

Document metadata is stored and returned unchanged, but is not indexed or filterable. Filters address fields of Content, not SearchDocument<T>.Metadata.

Without writeOptions, a write returns on acceptance and can return IndexAck.Queued. An empty FailedItems list at that boundary does not guarantee eventual indexing success. The explicit wait above lets the following reads observe completed writes. See write acknowledgement guidance.

Search and paginate

Add using System.Text.Json; at the top of Program.cs, then search for guides sorted by price:

var query = new SearchQuery
{
    Text = "dapr",
    SearchFields = ["title"],
    Filter = JsonSerializer.Deserialize<JsonElement>("""{"category":{"$eq":"guide"}}"""),
    Sort = [new SortClause("price", SortOrder.Ascending)],
    IncludeContent = true,
    TopK = 1
};

SearchResult<Article> page;
do
{
    page = await client.SearchAsync<Article>(storeName, indexName, query);

    foreach (var hit in page.Hits)
    {
        Console.WriteLine($"{hit.Document.Id}: {hit.Document.Content?.Title} ({hit.Score})");
    }

    Console.WriteLine($"Total: {page.TotalHits}, accuracy: {page.TotalHitsRelation}");
    query = query with { ContinuationToken = page.ContinuationToken };
}
while (!string.IsNullOrEmpty(page.ContinuationToken));

The default JSON naming policy is camelCase, so field names use title, category, and price, not the .NET property names. A continuation token belongs to the query that produced it: keep the text, filter, sort, page size, and field options unchanged between pages.

SearchAsync<T> returns one page, not an automatically enumerated stream. Content is omitted unless IncludeContent is true. Omit the query, or omit both Text and Native, to match all documents, subject to any filter.

Retrieve documents by ID

var documents = await client.GetDocumentsAsync<Article>(
    storeName, indexName, ["doc-1", "missing"], includeContent: true);

foreach (var document in documents)
{
    Console.WriteLine($"{document.Id}: {document.Content?.Title}");
}

Found documents are returned in request order; missing IDs are omitted. Without includeContent: true, Content is default(T).

Delete documents and the index

var ack = await client.DeleteDocumentsAsync(
    storeName, indexName, ["doc-2"], writeOptions);

if (ack != IndexAck.Completed)
{
    throw new InvalidOperationException("Document deletion was not confirmed complete.");
}

await client.DeleteIndexAsync(storeName, indexName);

Deleting a missing document ID is not an error. Deleting an index removes its documents as well.

Next steps

2 - Dapr Search client

Configure DaprSearchClient, shape queries, manage write acknowledgements, and generate typed index clients

DaprSearchClient in Dapr.Search implements the alpha Search API through gRPC. Use .NET SDK 1.19.0 or later with a Dapr runtime and component that support Search. See the overview for release requirements and the how-to for a complete document lifecycle.

Client lifetime and configuration

Reuse a long-lived client rather than creating one per operation. Dependency injection registers a singleton by default and owns its disposal:

using Dapr.Search.Extensions;

builder.Services.AddDaprSearchClient((serviceProvider, clientBuilder) =>
{
    clientBuilder.UseGrpcEndpoint("http://localhost:50001");
});

The callback can resolve configuration or other services through serviceProvider. Use the optional lifetime argument to choose another ServiceLifetime when required.

For applications without dependency injection, create and dispose a client yourself:

using Dapr.Search;

using var client = new DaprSearchClientBuilder()
    .UseGrpcEndpoint("http://localhost:50001")
    .Build();

The builder accepts an optional IConfiguration. Defaults resolve DAPR_GRPC_ENDPOINT, or fall back to the local endpoint using DAPR_GRPC_PORT (50001 by default); DAPR_API_TOKEN supplies sidecar authentication. Explicit builder settings override defaults; use UseDaprApiToken to override the token.

Use UseJsonSerializationOptions for document serialization and UseGrpcChannelOptions for gRPC settings. When supplying channel options, retain ThrowOperationCanceledOnCancellation = true. Although the shared builder also exposes HTTP endpoint, HTTP client factory, and HTTP timeout settings, Search operations use gRPC; changing the HTTP endpoint does not switch their transport, and UseTimeout is not a per-RPC deadline. Pass a time-limited CancellationToken to bound how long the caller waits.

API coverage

All operations accept optional component-specific request metadata (IReadOnlyDictionary<string, string>) and a CancellationToken.

OperationResult
CreateIndexAsync, DeleteIndexAsyncCreate or remove an index. Creation metadata supplies provider-specific settings.
GetIndexAsyncSearchIndex with Index, approximate DocumentCount, and component Properties.
ListIndexesAsyncIndex names in the component.
IndexDocumentsAsync<T>IndexDocumentsResult with Ack and FailedItems.
GetDocumentsAsync<T>Found SearchDocument<T> entries in request order.
DeleteDocumentsAsyncIndexAck for the deletion.
SearchAsync<T>One SearchResult<T> page.

Request metadata configures an operation; it is separate from each document’s stored, opaque Metadata.

Query options and results

SearchQuery is a record, so use with expressions when changing only the continuation token.

PropertyBehavior
TextPortable full-text query. Mutually exclusive with Native; neither means match all.
NativeProvider-native query represented by a JSON object (JsonElement).
FilterPortable JSON-object filter addressing content fields, including dotted nested paths.
TopKNullable uint page size; null or zero selects the component default.
ContinuationTokenOpaque token from the previous response. Keep the rest of the query unchanged.
SearchFieldsContent fields to search.
SortOrdered SortClause entries with Ascending, Descending, or provider-default Unspecified order.
IncludeContentNull or false omits content; returned Content is default(T).
ReturnFieldsFields projected into returned content. Use with IncludeContent = true.
HighlightFieldsFields for which highlighted fragments are requested, if the component supports them.

Filters support $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $exists, $and, $or, and $not. Multiple conditions in an object are ANDed, and a direct field value is an equality test. Use serialized JSON field names, which are camelCase by default. See the portable filter language and your component’s requirements for filterable and sortable fields.

SearchResult<T>.Hits contains SearchHit<T> entries with a Document, unnormalized provider-specific Score, and a Highlights dictionary keyed by field. Higher scores indicate greater relevance within that result set; do not compare them across providers. TotalHits is nullable, and TotalHitsRelation is Unspecified, Exact, LowerBound, or Estimate. Stop paging when ContinuationToken is null or empty.

Projection and highlighting

Using the Article type and client from the how-to, the following query requires a component that supports highlighting, such as Meilisearch:

var page = await client.SearchAsync<Article>("searchstore", "articles", new SearchQuery
{
    Text = "dapr",
    IncludeContent = true,
    ReturnFields = ["title", "category"],
    HighlightFields = ["title"]
});

foreach (var hit in page.Hits)
{
    if (hit.Highlights.TryGetValue("title", out var title))
    {
        Console.WriteLine(title);
    }
}

Projection can omit members expected by your model. Choose a compatible result type or JsonElement for partial documents. Highlight fragments can contain provider-generated markup; treat them as untrusted content when rendering a UI.

Provider-native queries

Pass a JSON object in Native rather than Text for provider-specific features:

using System.Text.Json;

var page = await client.SearchAsync<JsonElement>("searchstore", "articles", new SearchQuery
{
    Native = JsonSerializer.Deserialize<JsonElement>(
        """{"q":"dapr","matchingStrategy":"all"}"""),
    IncludeContent = true,
    TopK = 10
});

This example uses Meilisearch syntax and will not work with the in-memory component. Meilisearch pagination and sorting must still use the portable TopK, ContinuationToken, and Sort properties. Setting both Text and Native, or supplying a non-object Native or Filter, throws ArgumentException before sending an RPC.

Write acknowledgements

IndexDocumentsAsync<T> and DeleteDocumentsAsync accept Dapr.Search.IndexingOptions.

SettingBehavior
Omitted options, ReturnOnAcceptance, or Unspecified modeReturn after acceptance. IndexAck.Queued means background work was accepted, not that indexing succeeded. Synchronous components can return Completed.
WaitForCompletionRequires a positive WaitTimeout and explicit OnWaitTimeout. Completed means the final write result is available, including any item failures.
OnWaitTimeout = ContinueAsyncReturn Queued if the wait expires; requires component support for queued acknowledgement.
OnWaitTimeout = FailRequestFail with DeadlineExceeded if the wait expires. Provider-side work may still complete.

Do not supply WaitTimeout or OnWaitTimeout with acceptance mode. WaitTimeout must be shorter than any remaining RPC deadline and must leave headroom below the store resiliency timeout of at least 10% of that timeout and at least 100 ms. See the runtime acknowledgement rules.

Always inspect both IndexDocumentsResult.Ack and FailedItems. A completed batch can contain failed documents without throwing for the whole request. Each SearchFailedItem includes Id, canonical numeric ErrorCode, ErrorMessage, and Details preserving protobuf TypeUrl and raw Value bytes. At a queued boundary, only failures already known can be reported; there is no client API to poll for a final indexing result.

Serialization

Content uses System.Text.Json with JsonSerializerDefaults.Web by default. Pass .NET objects to SearchDocument<T>; do not manually base64-encode them or pass an already serialized JSON string, which would serialize as a string rather than an object. Use JsonElement for document shapes without a fixed .NET model.

For source-generated JSON serialization, configure the client with a context covering the content type:

using System.Text.Json.Serialization;
using Dapr.Search.Extensions;

builder.Services.AddDaprSearchClient((_, clientBuilder) =>
    clientBuilder.UseJsonSerializationOptions(SearchJsonContext.Default.Options));

[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(Article))]
internal partial class SearchJsonContext : JsonSerializerContext
{
}

The generated index client below does not generate JSON serialization metadata. Configure JSON options separately when reflection-free serialization is required.

Generate a typed index client

The Dapr.Search package bundles a source generator. Declare a top-level, non-generic, non-file-local partial interface, extending exactly one IDaprSearchIndex<T>, without additional abstract members:

using Dapr.Search;

[SearchIndex("searchstore", "articles")]
public partial interface IArticleSearch : IDaprSearchIndex<Article>
{
}

The component and index names must be non-empty. The generator supplies AddArticleSearch, named after the interface with its leading I removed:

builder.Services.AddDaprSearchClient().AddArticleSearch();

Inject the interface instead of repeatedly specifying the component, index, and generic type:

public sealed class ArticleService(IArticleSearch articles)
{
    public Task<SearchResult<Article>> FindAsync(string text, CancellationToken cancellationToken) =>
        articles.SearchAsync(
            new SearchQuery { Text = text, IncludeContent = true },
            cancellationToken: cancellationToken);
}

The generated wrapper exposes the index-scoped operations in the API table, but not store-wide ListIndexesAsync. It forwards options, request metadata, and cancellation unchanged, inherits the registered client’s lifetime, and does not own or dispose that client. Registration does not create the index; call CreateIndexAsync explicitly.

Invalid declarations, bindings, or contracts produce build errors DAPR1801, DAPR1802, or DAPR1803, respectively.

Cancellation and errors

Invalid client arguments throw argument exceptions before an RPC. RPC failures throw DaprException with the original Grpc.Core.RpcException as InnerException, preserving its status and trailers:

using Dapr;
using Grpc.Core;

try
{
    await client.CreateIndexAsync("searchstore", "articles");
}
catch (DaprException ex) when (
    ex.InnerException is RpcException { StatusCode: StatusCode.AlreadyExists })
{
    Console.Error.WriteLine("The index already exists; its settings were not changed.");
}

This is distinct from item failures returned by a successful indexing call. Caller cancellation can throw OperationCanceledException and does not guarantee that the provider stopped processing a write. Retry and timeout policies are configured through Dapr resiliency, not by this client.