From 61e55d9d34bd2d6b560dc5936b1883bc73f27e07 Mon Sep 17 00:00:00 2001 From: RiPont Date: Mon, 4 Mar 2024 08:07:52 -0800 Subject: [PATCH 1/2] NCBC-3678: Docs samples for SDK 3.5 features * Scoped search examples * Vector search examples * refactored from inline examples to CSPROJ example that compiles. --- .../SearchV2Examples/Program.cs | 230 ++++++++++++++++++ .../SearchV2Examples/SearchV2Examples.csproj | 24 ++ .../pages/full-text-searching-with-sdk.adoc | 133 ++++++---- 3 files changed, 342 insertions(+), 45 deletions(-) create mode 100644 modules/howtos/examples/Couchbase.Examples.SearchV2/SearchV2Examples/Program.cs create mode 100644 modules/howtos/examples/Couchbase.Examples.SearchV2/SearchV2Examples/SearchV2Examples.csproj diff --git a/modules/howtos/examples/Couchbase.Examples.SearchV2/SearchV2Examples/Program.cs b/modules/howtos/examples/Couchbase.Examples.SearchV2/SearchV2Examples/Program.cs new file mode 100644 index 00000000..0a48ba57 --- /dev/null +++ b/modules/howtos/examples/Couchbase.Examples.SearchV2/SearchV2Examples/Program.cs @@ -0,0 +1,230 @@ +using System.Xml.Schema; +using Couchbase; +using Couchbase.Core.Exceptions; +using Couchbase.KeyValue; +using Couchbase.Protostellar.Query.V1; +using Couchbase.Search; +using Couchbase.Search.Queries.Compound; +using Couchbase.Search.Queries.Range; +using Couchbase.Search.Queries.Simple; +using Couchbase.Search.Queries.Vector; +using DnsClient.Internal; +using Serilog; +using Serilog.Extensions.Logging; +using SearchIndex = Couchbase.Management.Search.SearchIndex; + +Serilog.Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .MinimumLevel.Verbose() + .WriteTo.Console() + .CreateLogger(); + +var clusterOptions = new ClusterOptions() +{ + ConnectionString = "couchbase://localhost", + UserName = "Administrator", + Password = "password", + EnableDnsSrvResolution = false, +}; + +clusterOptions = clusterOptions.WithLogging(new SerilogLoggerFactory()); + +var exampleCluster = await Cluster.ConnectAsync(clusterOptions); +await exampleCluster.WaitUntilReadyAsync(TimeSpan.FromSeconds(10)); +var travelSample = await exampleCluster.BucketAsync("travel-sample"); +var inventoryScope = travelSample.Scope("inventory"); +await Task.Delay(1000); + +await ClusterGetAllIndexes(exampleCluster); +await ClusterSearch(exampleCluster); +await ScopedGetAllIndexes(inventoryScope); +await ScopedSearch(inventoryScope); + +async Task ScopedGetAllIndexes(IScope scope) +{ + // tag::scopedGetAllIndexes[] + Log.Information("Fetching all search indexes on given scope..."); + var searchIndexes = scope.SearchIndexes; + var allScopedIndexes = await searchIndexes.GetAllIndexesAsync(); + foreach (var idx in allScopedIndexes) + { + Serilog.Log.Information("Search Index: {idx} in scope {scope}", idx.Name, scope.Name); + } + // end::scopedGetAllIndexes[] +} + +async Task ScopedSearch(IScope scope) +{ + // Assumes the scoped index "index-hotel-description" has already been created. + // see https://docs.couchbase.com/server/7.6/fts/fts-creating-index-from-UI-classic-editor-dynamic.html + // for information on how to create a scoped index. + // tag::scopedFtsSearch[] + var searchResult = await scope.SearchAsync("index-hotel-description", + SearchRequest.Create( + new MatchQuery("swanky")), + new SearchOptions().Limit(10)); + // end::scopedFtsSearch[] + // tag::scopedEnumerateHits[] + foreach (var hit in searchResult.Hits) + { + string documentId = hit.Id; + double score = hit.Score; + Log.Information("Hit: {id}: {score}", documentId, score); + } + // end::scopedEnumerateHits[] + // tag::scopedEnumerateFacets[] + foreach (var keyValuePair in searchResult.Facets) + { + var facet = keyValuePair.Value; + var name = facet.Name; + var total = facet.Total; + Log.Information("Facet: {key}={name},{total}", keyValuePair.Key, name, total); + } + // end::scopedEnumerateFacets[] +} + +async Task ScopedDateSearch(IScope scope) +{ + // tag::scopedDateSearch[] + var searchResult = await scope.SearchAsync("index-name", + SearchRequest.Create( + new DateRangeQuery() + .Start(DateTime.Parse("2021-01-01"), inclusive: true) + .End(DateTime.Parse("2021-02-01"), inclusive: false) + ), new SearchOptions().Limit(10)); + // end::scopedDateSearch[] + foreach (var row in searchResult) + { + Log.Information("result: {row}", row.Locations?.ToString()); + } +} + +async Task ScopedConjunctionSearch(IScope scope) +{ + // tag::scopedConjunctionSearch[] + var searchResult = await scope.SearchAsync("index-name", + SearchRequest.Create( + new ConjunctionQuery( + new DateRangeQuery() + .Start(DateTime.Parse("2021-01-01"), inclusive: true) + .End(DateTime.Parse("2021-02-01"), inclusive: false), + new MatchQuery("swanky")) + ), new SearchOptions().Limit(10)); + // end::scopedConjunctionSearch[] + foreach (var row in searchResult) + { + Log.Information("result: {row}", row.Locations?.ToString()); + } +} + +async Task ScopedConsistentWithSearch(IScope scope) +{ + // tag::scopedConsistentWithSearch[] + var searchResult = await scope.SearchAsync("index-hotel-description", + SearchRequest.Create( + new MatchQuery("swanky") + ), new SearchOptions() + .Limit(10) + .ScanConsistency(SearchScanConsistency.RequestPlus) + ); + // end::scopedConsistentWithSearch[] + foreach (var row in searchResult) + { + Log.Information("result: {row}", row.Locations?.ToString()); + } +} + +async Task ClusterSearch(ICluster cluster) +{ + try + { + // tag::clusterFtsSearch[] + var searchResult = await cluster.SearchAsync( + "travel-sample.inventory.index-hotel-description", + SearchRequest.Create(new MatchQuery("swanky")), + new SearchOptions().Limit(10) + ); + // end::clusterFtsSearch[] + foreach (var row in searchResult) + { + Log.Information("result: {row}", row.Locations?.ToString()); + } + } + catch (IndexNotFoundException e) + { + Log.Error(e, $"IndexNotFoundException: {nameof(ClusterSearch)}"); + } +} + +async Task ClusterGetAllIndexes(ICluster cluster) +{ + Log.Information("Fetching all search indexes on cluster..."); + // tag::clusterGetAllIndexes[] + var searchIndexes = cluster.SearchIndexes; + var allScopedIndexes = await searchIndexes.GetAllIndexesAsync(); + foreach (var idx in allScopedIndexes) + { + Serilog.Log.Information("Search Index: {idx}", idx.Name); + } + // end::clusterGetAllIndexes[] +} + +async Task ScopedVectorQuery(IScope scope) +{ + // in actual usage, these vectors would be generated by some other AI toolkit. + float[] preGeneratedVectors = new[] { 0.001f, 0.002f, 0.003f }; + //tag::scopedVector1[] + var searchRequest = SearchRequest.Create( + VectorSearch.Create(new VectorQuery("vector_field", preGeneratedVectors)) + ); + + var searchResult = scope.SearchAsync("travel-vector-index", searchRequest, new SearchOptions()); + //end::scopedVector1[] +} + +async Task ScopedVectorQueryMultiple(IScope scope) +{ + // in actual usage, these vectors would be generated by some other AI toolkit. + float[] preGeneratedVectors = new[] { 0.001f, 0.002f, 0.003f }; + var vectorQuery = new VectorQuery("vector-field-1", preGeneratedVectors); + var anotherVectorQuery = new VectorQuery("vector-field-2", preGeneratedVectors); + //tag::scopedVector2[] + var searchRequest = SearchRequest.Create( + VectorSearch.Create(new[] + { + vectorQuery.WithOptions( + new VectorQueryOptions().WithNumCandidates(2).WithBoost(0.3f)), + anotherVectorQuery.WithOptions( + new VectorQueryOptions().WithNumCandidates(5).WithBoost(0.7f)), + }) + ); + + // or with C# record syntax + var searchRequest2 = SearchRequest.Create( + new VectorSearch(new[] + { + vectorQuery + with { Options = new VectorQueryOptions() { NumCandidates = 2, Boost = 0.3f } }, + anotherVectorQuery + with { Options = new VectorQueryOptions() { NumCandidates = 5, Boost = 0.3f } }, + }, + Options: new VectorSearchOptions(VectorQueryCombination.And) + )); + + var searchResult = scope.SearchAsync("travel-vector-index", searchRequest, new SearchOptions()); + //end::scopedVector2[] +} + +async Task ScopedVectorWithFts(IScope scope) +{ + // in actual usage, these vectors would be generated by some other AI toolkit. + float[] preGeneratedVectors = new[] { 0.001f, 0.002f, 0.003f }; + //tag::scopedVectorWithFts[] + var searchRequest = new SearchRequest( + SearchQuery: new MatchQuery("swanky"), + VectorSearch: VectorSearch.Create(new VectorQuery("vector_field", preGeneratedVectors)) + ); + + var searchResult = scope.SearchAsync("travel-index", searchRequest, new SearchOptions()); + //end::scopedVectorWithFts[] +} \ No newline at end of file diff --git a/modules/howtos/examples/Couchbase.Examples.SearchV2/SearchV2Examples/SearchV2Examples.csproj b/modules/howtos/examples/Couchbase.Examples.SearchV2/SearchV2Examples/SearchV2Examples.csproj new file mode 100644 index 00000000..02ed2a19 --- /dev/null +++ b/modules/howtos/examples/Couchbase.Examples.SearchV2/SearchV2Examples/SearchV2Examples.csproj @@ -0,0 +1,24 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/modules/howtos/pages/full-text-searching-with-sdk.adoc b/modules/howtos/pages/full-text-searching-with-sdk.adoc index abc603ab..67d50afe 100644 --- a/modules/howtos/pages/full-text-searching-with-sdk.adoc +++ b/modules/howtos/pages/full-text-searching-with-sdk.adoc @@ -10,57 +10,61 @@ include::project-docs:partial$attributes.adoc[] -Full Text Search or FTS allows you to create, manage, and query full text indexes on JSON documents stored in Couchbase buckets. -It uses natural language processing for querying documents, provides relevance scoring on the results of your queries, and has fast indexes for querying a wide range of possible text searches. -Some of the supported query types include simple queries like Match and Term queries; range queries like Date Range and Numeric Range; and compound queries for conjunctions, disjunctions, and/or boolean queries. +Full Text Search or FTS allows you to create, manage and query full text indexes on JSON documents stored in Couchbase buckets. +It uses natural language processing for indexing and querying documents, provides relevance scoring on the results of your queries and has fast indexes for querying a wide range of possible text searches. + +Some of the supported query-types include simple queries like Match and Term queries, range queries like Date Range and Numeric Range and compound queries for conjunctions, disjunctions and/or boolean queries. + +The Full Text Search service also supports vector search from Couchbase Server 7.6 onwards. + The .NET SDK exposes an API for performing FTS queries which abstracts some of the complexity of using the underlying REST API. +== Getting Started + +After familiarizing yourself with how to create and query a Search index in the UI you can query it from the SDK. + +There are two APIs for querying search: `cluster.searchQuery()`, and `cluster.search()`. +Both are also available at the Scope level. + +The former API supports FTS queries (`SearchQuery`), while the latter additionally supports the `VectorSearch` added in 7.6. +Most of this documentation will focus on the former API, as the latter is in @Stability.Volatile status. + +We will perform an FTS query here - see the <> section for examples of that. // As of Couchbase Server 6.5, FTS... == Examples -Search queries are executed at Cluster level (not bucket or collection). +Search queries are executed at Cluster level. +As of Couchbase Server 7.6, they can also be executed at the Scope level. + Here is a simple MatchQuery that looks for the text “swanky” using a defined index: [source,csharp] ---- -var result = cluster.SearchQuery( - "travel-sample-index-hotel-description", - new MatchQuery("swanky"), - options => { - options.WithLimit(10); - } -); +// as a cluster-level search +include::example$Couchbase.Examples.SearchV2/SearchV2Examples/Program.cs[tag=clusterFtsSearch,indent=0] + +// as a scope-level search +include::example$Couchbase.Examples.SearchV2/SearchV2Examples/Program.cs[tag=scopedFtsSearch,indent=0] ---- + + All simple query types are created in the same manner, some have additional properties, which can be seen in common query type descriptions. Couchbase FTS's xref:7.1@server:fts:fts-query-types.adoc[range of query types] enable powerful searching using multiple options, to ensure results are just within the range wanted. Here is a date range query that looks for dates between 1st January 2021 and 31st January: [source,csharp] ---- -var result = cluster.SearchQuery( - "index-name", - new DateRangeQuery() - .Start(DateTime.Parse("2021-01-01"), true) // final parameter is if the range is inclusive - .End(DateTime.Parse("2021-02-01"), false), - options => options.Limit(10) -); +include::example$Couchbase.Examples.SearchV2/SearchV2Examples/Program.cs[tag=scopedDateSearch,indent=0] ---- A conjunction query contains multiple child queries; its result documents must satisfy all of the child queries: [source,csharp] ---- -var result = cluster.SearchQuery( - "index-name", - new ConjunctionQuery( - new DateRangeQuery() - .Start(DateTime.Parse("2021-01-01"), true) - .Start(DateTime.Parse("2021-02-01"), false), - new MatchQuery("Swanky") -); +include::example$Couchbase.Examples.SearchV2/SearchV2Examples/Program.cs[tag=scopedConjunctionSearch,indent=0] ---- == Working with Results @@ -74,23 +78,13 @@ such as success total hits and how long the query took to execute in the cluster [source,csharp] .Iterating hits ---- -foreach (var hit in result.Hits) -{ - string documentId = hit.Id; - double score = hit.Score; - ... -} +include::example$Couchbase.Examples.SearchV2/SearchV2Examples/Program.cs[tag=scopedEnumerateHits,indent=0] ---- [source,csharp] .Iterating facets ---- -foreach (var facet in result.Facets) -{ - var name = facet.Name; - var total = facet.Total; - ... -} +include::example$Couchbase.Examples.SearchV2/SearchV2Examples/Program.cs[tag=scopedEnumerateFacets,indent=0] ---- @@ -101,11 +95,60 @@ FTS allows `RequestPlus` queries -- _Read-Your-Own_Writes (RYOW)_ consistency, e [source,csharp] ---- -var result = cluster.SearchQuery( - "travel-sample-index-hotel-description", - new MatchQuery("swanky"), - options => { - options.WithConsistency(ScanConsistency.RequestPlus); - } -); + +include::example$Couchbase.Examples.SearchV2/SearchV2Examples/Program.cs[tag=scopedConsistentWithSearch,indent=0] +---- + +== Scoped vs Global Indexes +The FTS APIs exist at both the `Cluster` and `Scope` levels. + +This is because FTS supports, as of Couchbase Server 7.6, a new form of "scoped index" in addition to the traditional "global index". + +It's important to use the `Cluster.SearchAsync()` for global indexes, and `Scope.SearchAsync()` for scoped indexes. +(`Cluster.SearchQueryAsync()` is still available for compatibility with earlier versions of the SDK) + +== Vector Search +As of Couchbase Server 7.6, the FTS service supports vector search in additional to traditional full text search queries. +// todo link to the server docs when available + +==== Single vector query +In this first example we are performing a single vector query: +[source,csharp] +---- +include::example$Couchbase.Examples.SearchV2/SearchV2Examples/Program.cs[tag=scopedVector1,indent=0] +---- + +Let's break this down. +We create a `SearchRequest`, which can contain a traditional FTS query `SearchQuery` and/or the new `VectorSearch`. +Here we are just using the latter. + +The `VectorSearch` allows us to perform one or more `VectorQuery` s. + +The `VectorQuery` itself takes the name of the document field that contains embedded vectors ("vector_field" here), plus actual vector query in the form of a `float[]`. + +(Note that Couchbase itself is not involved in generating the vectors, and these will come from an external source such as an embeddings API.) + +Finally we execute the `SearchRequest` against the FTS index "travel-vector-index", which has previously been setup to vector index the "vector_field" field. + +This happens to be a scoped index so we are using `scope.SearchAsync()`. +If it was a global index we would use `cluster.SearchAsync()` instead - see <>. + +It returns the same `SearchResult` detailed earlier. + +==== Multiple vector queries +You can run multiple vector queries together: + +[source,csharp] +---- +include::example$Couchbase.Examples.SearchV2/SearchV2Examples/Program.cs[tag=scopedVector2,indent=0] +---- + +How the results are combined (ANDed or ORed) can be controlled with `VectorSearchOptions().WithVectorQueryCombination()`. + +==== Combining FTS and vector queries +You can combine a traditional FTS query with vector queries: + +[source,casharp] ---- +include::example$Couchbase.Examples.SearchV2/SearchV2Examples/Program.cs[tag=scopedVectorWithFts,indent=0] +---- \ No newline at end of file From 2361e2e9fc941ca30811763780f608d8ee2c23c5 Mon Sep 17 00:00:00 2001 From: RiPont Date: Mon, 4 Mar 2024 18:04:44 -0800 Subject: [PATCH 2/2] NCBC-3678: Docs samples for SDK 3.5 features (RangeSan and SubDoc replica read) --- .../RangeScan/RangeScan/Program.cs | 93 +++++++++++++++++++ .../RangeScan/RangeScan/RangeScan.csproj | 19 ++++ .../SubDocument/Program.cs | 71 ++++++++++++++ .../SubDocument/SubDocument.csproj | 14 +++ .../SearchV2Examples/Program.cs | 6 +- modules/howtos/pages/kv-operations.adoc | 70 ++++++++++++++ .../howtos/pages/subdocument-operations.adoc | 21 +++++ 7 files changed, 289 insertions(+), 5 deletions(-) create mode 100644 modules/howtos/examples/Couchbase.Examples.KV/RangeScan/RangeScan/Program.cs create mode 100644 modules/howtos/examples/Couchbase.Examples.KV/RangeScan/RangeScan/RangeScan.csproj create mode 100644 modules/howtos/examples/Couchbase.Examples.KV/SubDocument/Program.cs create mode 100644 modules/howtos/examples/Couchbase.Examples.KV/SubDocument/SubDocument.csproj diff --git a/modules/howtos/examples/Couchbase.Examples.KV/RangeScan/RangeScan/Program.cs b/modules/howtos/examples/Couchbase.Examples.KV/RangeScan/RangeScan/Program.cs new file mode 100644 index 00000000..00e80aae --- /dev/null +++ b/modules/howtos/examples/Couchbase.Examples.KV/RangeScan/RangeScan/Program.cs @@ -0,0 +1,93 @@ +using Couchbase; +using Couchbase.KeyValue; +using Couchbase.KeyValue.RangeScan; +using Serilog; +using Serilog.Extensions.Logging; + +Serilog.Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .MinimumLevel.Information() + .WriteTo.Console() + .CreateLogger(); + +var clusterOptions = new ClusterOptions() +{ + ConnectionString = "couchbase://localhost", + UserName = "Administrator", + Password = "password", + EnableDnsSrvResolution = false, +}; + +clusterOptions = clusterOptions.WithLogging(new SerilogLoggerFactory()); + +var exampleCluster = await Cluster.ConnectAsync(clusterOptions); +await exampleCluster.WaitUntilReadyAsync(TimeSpan.FromSeconds(10)); +var travelSample = await exampleCluster.BucketAsync("travel-sample"); +var inventoryScope = travelSample.Scope("inventory"); +await Task.Delay(1000); + +await RangeScanAllDocuments(inventoryScope); +await RangeScanPrefixScan(inventoryScope); + +async Task RangeScanAllDocuments(IScope scope) +{ + var collection = scope.Collection("hotel"); + // tag::rangeScanAllDocuments[] + IAsyncEnumerable results = collection.ScanAsync(new RangeScan()); + + await foreach (var scanResult in results) + { + Log.Information(scanResult.Id); + Log.Information(scanResult.ContentAs().ToString()); + } + + // alternate declaration + var scan2 = new RangeScan(from: ScanTerm.Inclusive("id001"), to: ScanTerm.Inclusive("id999")); + // end::rangeScanAllDocuments[] +} + +async Task RangeScanPrefixScan(IScope scope) +{ + var collection = scope.Collection("hotel"); + // tag::rangeScanPrefix[] + IAsyncEnumerable results = collection.ScanAsync( + new PrefixScan("alice::") + ); + + await foreach (var scanResult in results) + { + Log.Information(scanResult.Id); + } + // end::rangeScanPrefix[] +} + +async Task RangeScanSamplingScan(IScope scope) +{ + var collection = scope.Collection("hotel"); + // tag::rangeScanSample[] + IAsyncEnumerable results = collection.ScanAsync( + new SamplingScan(limit: 100) + ); + + await foreach (var scanResult in results) + { + Log.Information(scanResult.Id); + } + // end::rangeScanSample[] +} +async Task RangeScanAllDocumentIds(IScope scope) +{ + var collection = scope.Collection("hotel"); + // tag::rangeScanAllDocumentIds[] + IAsyncEnumerable results = collection.ScanAsync( + new RangeScan(), + new ScanOptions().IdsOnly(true)); + + await foreach (var scanResult in results) + { + Log.Information(scanResult.Id); + } + // end::rangeScanAllDocumentIds[] +} + +record Hotel(string name, string title, string address); diff --git a/modules/howtos/examples/Couchbase.Examples.KV/RangeScan/RangeScan/RangeScan.csproj b/modules/howtos/examples/Couchbase.Examples.KV/RangeScan/RangeScan/RangeScan.csproj new file mode 100644 index 00000000..78110fba --- /dev/null +++ b/modules/howtos/examples/Couchbase.Examples.KV/RangeScan/RangeScan/RangeScan.csproj @@ -0,0 +1,19 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/modules/howtos/examples/Couchbase.Examples.KV/SubDocument/Program.cs b/modules/howtos/examples/Couchbase.Examples.KV/SubDocument/Program.cs new file mode 100644 index 00000000..ba5021b1 --- /dev/null +++ b/modules/howtos/examples/Couchbase.Examples.KV/SubDocument/Program.cs @@ -0,0 +1,71 @@ +using Couchbase; +using Couchbase.Core.Exceptions.KeyValue; +using Couchbase.KeyValue; + +var clusterOptions = new ClusterOptions() +{ + ConnectionString = "couchbase://localhost", + UserName = "Administrator", + Password = "password", + EnableDnsSrvResolution = false, +}; + +var exampleCluster = await Cluster.ConnectAsync(clusterOptions); +await exampleCluster.WaitUntilReadyAsync(TimeSpan.FromSeconds(10)); +var travelSample = await exampleCluster.BucketAsync("travel-sample"); +var inventoryScope = travelSample.Scope("inventory"); +await Task.Delay(1000); + +await LookupInAnyReplica(travelSample.DefaultCollection()); +await LookupInAllReplicas(travelSample.DefaultCollection()); + +async Task LookupInAnyReplica(ICouchbaseCollection collection) +{ + // tag::lookup-in-any-replica[] + try + { + var result = await collection.LookupInAnyReplicaAsync( + "hotel_10138", + specs => specs.Get("geo.lat") + ); + + var geoLat = result.ContentAs(0); + Console.Out.WriteLine($"getFunc: Latitude={geoLat}"); + } + catch (PathNotFoundException) + { + Console.Error.WriteLine("The version of the document" + + " on the server node that responded quickest" + + " did not have the requested field."); + } + catch (DocumentUnretrievableException) + { + Console.Error.WriteLine("Document was not present" + + " on any server node"); + } + // end::lookup-in-any-replica[] +} + +async Task LookupInAllReplicas(ICouchbaseCollection collection) +{ + // tag::lookup-in-all-replicas[] + IAsyncEnumerable result = collection.LookupInAllReplicasAsync( + "hotel_10138", + specs => specs.Get("geo.lat")); + + await foreach (var replicaResult in result) + { + try + { + var geoLat = replicaResult.ContentAs(0); + Console.Out.WriteLine($"getFunc: Latitude={geoLat}"); + } + catch (PathNotFoundException) + { + Console.Error.WriteLine("The version of the document" + + " on the server node that responded quickest" + + " did not have the requested field."); + } + } + // end::lookup-in-all-replicas[] +} \ No newline at end of file diff --git a/modules/howtos/examples/Couchbase.Examples.KV/SubDocument/SubDocument.csproj b/modules/howtos/examples/Couchbase.Examples.KV/SubDocument/SubDocument.csproj new file mode 100644 index 00000000..81184a28 --- /dev/null +++ b/modules/howtos/examples/Couchbase.Examples.KV/SubDocument/SubDocument.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/modules/howtos/examples/Couchbase.Examples.SearchV2/SearchV2Examples/Program.cs b/modules/howtos/examples/Couchbase.Examples.SearchV2/SearchV2Examples/Program.cs index 0a48ba57..00724de6 100644 --- a/modules/howtos/examples/Couchbase.Examples.SearchV2/SearchV2Examples/Program.cs +++ b/modules/howtos/examples/Couchbase.Examples.SearchV2/SearchV2Examples/Program.cs @@ -1,17 +1,13 @@ -using System.Xml.Schema; -using Couchbase; +using Couchbase; using Couchbase.Core.Exceptions; using Couchbase.KeyValue; -using Couchbase.Protostellar.Query.V1; using Couchbase.Search; using Couchbase.Search.Queries.Compound; using Couchbase.Search.Queries.Range; using Couchbase.Search.Queries.Simple; using Couchbase.Search.Queries.Vector; -using DnsClient.Internal; using Serilog; using Serilog.Extensions.Logging; -using SearchIndex = Couchbase.Management.Search.SearchIndex; Serilog.Log.Logger = new LoggerConfiguration() .Enrich.FromLogContext() diff --git a/modules/howtos/pages/kv-operations.adoc b/modules/howtos/pages/kv-operations.adoc index 644c559d..c468c6af 100644 --- a/modules/howtos/pages/kv-operations.adoc +++ b/modules/howtos/pages/kv-operations.adoc @@ -215,6 +215,76 @@ Here is an example showing an upsert in the `users` collection, which lives in t include::example$KvOperations.csx[tag=named-collection-upsert,indent=0] ---- +[#kv-range-scan] +== KV Range Scan + +A range scan gives you documents from a collection, even if you don't know the document IDs. +This feature requires Couchbase Server 7.6 or newer. + +TIP: KV range scan is suitable for use cases that require relatively low concurrency and tolerate relatively high latency. +If your application does many scans at once, or requires low latency results, we recommend using {sqlpp} (with a primary index on the collection) instead of KV range scan. + + +[#kv-range-scan-range] +=== Range scan + +Here's an example of a KV range scan that gets all documents in a collection: + +.KV Range Scan for all documents in a collection +[source,java] +---- +include::example$Couchbase.Examples.KV/RangeScan/RangeScan/Program.cs[tag=rangeScanAllDocuments,indent=0] +---- +<1> The `RangeScan()` constructor has two optional nullable parameters: `from` and `to`. +If you pass null like in this example, you'll get all documents in the collection. +These parameters are for advanced use cases; you probably won't need to specify them. +Instead, it's more common to use the "prefix" scan type shown in the next example. + +[#kv-range-scan-prefix] +=== Prefix scan + +KV range scan can also give you all documents whose IDs start with the same prefix. + +Imagine you have a collection where documents are named like this: `::`. +In other words, the document ID starts with the name of the user associated with the document, followed by a delimiter, and then a UUID. +If you use this document naming scheme, you can use a prefix range scan to get all documents associated with a user. + +For example, to get all documents associated with user "alice", you would write: + +.KV Range Scan for all documents in a collection whose IDs start with "alice::" +[source,java] +---- +include::example$Couchbase.Examples.KV/RangeScan/RangeScan/Program.cs[tag=rangeScanPrefix,indent=0] +---- +<1> Note the scan type is *prefixScan*. + +[#kv-range-scan-sample] +=== Sample scan + +If you want to get random documents from a collection, use a sample scan. + +.KV Range Scan for 100 random documents +[source,java] +---- +include::example$Couchbase.Examples.KV/RangeScan/RangeScan/Program.cs[tag=rangeScanSample,indent=0] +---- +<1> In this example, no more than `100` documents are returned. + +[#kv-range-scan-only-ids] +=== Get IDs instead of full document + +If you only want the document IDs, set the `idsOnly` option to true, like this: + +.KV Range Scan for all document IDs in a collection +[source,java] +---- +include::example$Couchbase.Examples.KV/RangeScan/RangeScan/Program.cs[tag=rangeScanAllDocumentIds,indent=0] +---- +<1> The returned `ScanResult` objects throw `NoSuchElementException` if you try to access any property other than `Id`. + +Setting `IdsOnly` to true also works with the other scan types described above. + + == Additional Resources diff --git a/modules/howtos/pages/subdocument-operations.adoc b/modules/howtos/pages/subdocument-operations.adoc index b818794f..29ed046f 100644 --- a/modules/howtos/pages/subdocument-operations.adoc +++ b/modules/howtos/pages/subdocument-operations.adoc @@ -260,6 +260,27 @@ In this case, the _createPath_ option may be used. include::example$SubDocument.csx[tag=create-path,indent=0] ---- +== Reading Sub-Documents From Replicas + +Couchbase Server 7.6 and later support Sub-Doc lookup from replicas. + +The `collection.lookupInAnyReplica()` method returns the first response -- from active or replica: + +[source,java] +---- +include::example$Couchbase.Examples.KV/SubDocument/Program.cs[tag=lookup-in-any-replica,indent=0] +---- + +The `collection.LookupInAllReplicas()` fetches all available replicas (and the active copy), and returns all responses. + +[source,java] +---- +include::example$Couchbase.Examples.KV/SubDocument/Program.cs[tag=lookup-in-all-replicas,indent=0] +---- + +You may want to use `LookupInAllReplicas` to build a consensus, +but it's more likely that you'll make use of `LookupInAnyReplica` as a fallback to a `LookupIn`, when the active node times out. + == Concurrent Modifications Concurrent Sub-Document operations on different parts of a document will not conflict.