diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index fc08a29afe..06b8e4c078 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -16,8 +16,9 @@ jobs: strategy: fail-fast: false matrix: - # 1.0.1 is skipped because it doesn't have a tag in OpenSearch repo, we can't check out to compile plugins - version: + version: + - 2.9.0 + - 2.8.0 - 2.7.0 - 2.6.0 - 2.5.0 @@ -26,10 +27,9 @@ jobs: - 2.2.1 - 2.1.0 - 2.0.1 - - 1.3.10 + - 1.3.11 - 1.2.4 - 1.1.0 - - 1.0.0 steps: - name: Checkout Client @@ -50,35 +50,10 @@ jobs: restore-keys: | ${{ runner.os }}-nuget- - # Due to https://github.com/opensearch-project/project-website/issues/737 - # No plugins released for versions 1.0.x , so we need to compile plugins on our own to run tests properly - # Compiled plugins are copied to ${OPENSEARCH_PLUGINS_DIRECTORY} where picked up by the test framework - # Versions of OpenSearch starting 1.1.0 are able to download plugins from the Internet - - name: Restore or Build OpenSearch Plugins - uses: ./client/.github/actions/cached-git-build - if: matrix.version == '1.0.0' - with: - repository: opensearch-project/OpenSearch - ref: ${{ matrix.version }} - path: opensearch - cached_paths: | - ./opensearch/plugins/*/build/distributions/*.zip - build_script: | - PluginList=("analysis-icu" "analysis-kuromoji" "analysis-nori" "analysis-phonetic" "ingest-attachment" "mapper-murmur3") - for plugin in ${PluginList[*]}; do - ./gradlew :plugins:$plugin:assemble -Dbuild.snapshot=false - done - - - name: Copy OpenSearch Plugins - if: matrix.version == '1.0.0' - run: | - mkdir -p ${{ env.OPENSEARCH_PLUGINS_DIRECTORY }} - cp opensearch/plugins/*/build/distributions/*-${{ matrix.version }}.zip ${{ env.OPENSEARCH_PLUGINS_DIRECTORY }} - - run: "./build.sh integrate ${{ matrix.version }} readonly,writable random:test_only_one --report" name: Integration Tests working-directory: client - + - name: Upload test report if: failure() uses: actions/upload-artifact@v3 @@ -87,12 +62,13 @@ jobs: path: client/build/output/* integration-opensearch-unreleased: + if: false # TODO: Temporarily disabled due to failures building & running OpenSearch from source, pending investigation & fixes (https://github.com/opensearch-project/opensearch-net/issues/268) name: Integration OpenSearch Unreleased runs-on: ubuntu-latest strategy: fail-fast: false matrix: - opensearch_ref: + opensearch_ref: - '1.x' - '2.x' - 'main' @@ -102,7 +78,7 @@ jobs: uses: actions/checkout@v3 with: path: client - + - uses: actions/setup-dotnet@v3 with: dotnet-version: | @@ -115,82 +91,22 @@ jobs: key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.?sproj') }} restore-keys: | ${{ runner.os }}-nuget- - - name: Restore or Build OpenSearch - uses: ./client/.github/actions/cached-git-build - with: - repository: opensearch-project/OpenSearch - ref: ${{ matrix.opensearch_ref }} - path: opensearch - cached_paths: | - ./opensearch/distribution/archives/linux-tar/build/distributions/opensearch-*.tar.gz - ./opensearch/plugins/*/build/distributions/*.zip - build_script: | - ./gradlew :distribution:archives:linux-tar:assemble - - PluginList=("analysis-icu" "analysis-kuromoji" "analysis-nori" "analysis-phonetic" "ingest-attachment" "mapper-murmur3") - for plugin in ${PluginList[*]}; do - ./gradlew :plugins:$plugin:assemble - done - - - name: Determine OpenSearch distribution path and version - shell: bash -eo pipefail {0} - run: | - distribution=`ls -1 $PWD/opensearch/distribution/archives/linux-tar/build/distributions/opensearch-*.tar.gz | head -1` - version=`basename $distribution | cut -d'-' -f3,4` - echo "OPENSEARCH_DISTRIBUTION=$distribution" >> $GITHUB_ENV - echo "OPENSEARCH_VERSION=$version" >> $GITHUB_ENV - - - name: Restore or Build OpenSearch Security - uses: ./client/.github/actions/cached-git-build - if: matrix.opensearch_ref == '1.x' + id: opensearch + uses: ./client/.github/actions/build-opensearch with: - repository: opensearch-project/security ref: ${{ matrix.opensearch_ref }} - path: opensearch-security - cached_paths: | - ./opensearch-security/build/distributions/opensearch-security-*-SNAPSHOT.zip - build_script: ./gradlew assemble -Dopensearch.version=$OPENSEARCH_VERSION - - - name: Restore or Build OpenSearch k-NN - uses: ./client/.github/actions/cached-git-build - with: - repository: opensearch-project/k-NN - ref: ${{ matrix.opensearch_ref }} - path: opensearch-knn - cached_paths: | - ./opensearch-knn/build/distributions/opensearch-knn-*-SNAPSHOT.zip - build_script: | - sudo apt-get install -y libopenblas-dev libomp-dev - ./gradlew buildJniLib assemble -Dopensearch.version=$OPENSEARCH_VERSION - distributions=./build/distributions - lib_dir=$distributions/lib - mkdir $lib_dir - cp -v $(ldconfig -p | grep libgomp | cut -d ' ' -f 4) $lib_dir - cp -v ./jni/release/libopensearchknn_* $lib_dir - ls -l $lib_dir - cd $distributions - zip -ur opensearch-knn-*.zip lib - - - name: Copy OpenSearch plugins - shell: bash -eo pipefail {0} - run: | - mkdir -p $OPENSEARCH_PLUGINS_DIRECTORY - cp -v ./opensearch/plugins/*/build/distributions/*-$OPENSEARCH_VERSION.zip $OPENSEARCH_PLUGINS_DIRECTORY/ - - plugins=("opensearch-knn" "opensearch-security") - for plugin in ${plugins[*]}; do - if [[ -d "./$plugin" ]]; then - cp -v ./$plugin/build/distributions/$plugin-*-SNAPSHOT.zip $OPENSEARCH_PLUGINS_DIRECTORY/ - fi - done - - ls -l $OPENSEARCH_PLUGINS_DIRECTORY - - - run: "./build.sh integrate ${{ env.OPENSEARCH_VERSION }} readonly,writable random:test_only_one --report" + security_plugin: ${{ matrix.opensearch_ref == '1.x' }} + knn_plugin: true + plugins_output_directory: ${{ env.OPENSEARCH_PLUGINS_DIRECTORY }} + + - run: "./build.sh integrate $OPENSEARCH_VERSION readonly,writable random:test_only_one --report" name: Integration Tests working-directory: client - + env: + OPENSEARCH_VERSION: ${{ steps.opensearch.outputs.version }} + OPENSEARCH_DISTRIBUTION: ${{ steps.opensearch.outputs.distribution }} + - name: Upload test report if: failure() uses: actions/upload-artifact@v3 diff --git a/CHANGELOG.md b/CHANGELOG.md index c1c53be378..cdb6dd40dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ### Added - Added support for point-in-time search and associated APIs ([#405](https://github.com/opensearch-project/opensearch-net/pull/405)) - Added support for the component template APIs ([#411](https://github.com/opensearch-project/opensearch-net/pull/411)) +- Added support for the composable index template APIs ([#437](https://github.com/opensearch-project/opensearch-net/pull/437)) ### Dependencies - Bumps `FSharp.Data` from 6.2.0 to 6.3.0 diff --git a/OpenSearch.sln.DotSettings b/OpenSearch.sln.DotSettings index 0326966f7b..d831b30a7f 100644 --- a/OpenSearch.sln.DotSettings +++ b/OpenSearch.sln.DotSettings @@ -14,33 +14,6 @@ &lt;inspection_tool class="UnnecessaryReturnJS" enabled="false" level="WARNING" enabled_by_default="false" /&gt; &lt;inspection_tool class="WrongPropertyKeyValueDelimiter" enabled="false" level="WEAK WARNING" enabled_by_default="false" /&gt; &lt;/profile&gt;</IDEA_SETTINGS><VBReformatCode>True</VBReformatCode><HtmlReformatCode>True</HtmlReformatCode><CSUpdateFileHeader>True</CSUpdateFileHeader><CSReorderTypeMembers>True</CSReorderTypeMembers><CSReformatCode>True</CSReformatCode></Profile> - /* SPDX-License-Identifier: Apache-2.0 -* -* The OpenSearch Contributors require contributions made to -* this file be licensed under the Apache-2.0 license or a -* compatible open source license. -* -* Modifications Copyright OpenSearch Contributors. See -* GitHub history for details. -* -* Licensed to Elasticsearch B.V. under one or more contributor -* license agreements. See the NOTICE file distributed with -* this work for additional information regarding copyright -* ownership. Elasticsearch B.V. licenses this file to you under -* the Apache License, Version 2.0 (the "License"); you may -* not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, -* software distributed under the License is distributed on an -* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -* KIND, either express or implied. See the License for the -* specific language governing permissions and limitations -* under the License. -*/ - True 56A87048-9065-459B-826D-3DF68B409845/d:Views diff --git a/guides/index-template.md b/guides/index-template.md index ffda5e59d6..495d873b39 100644 --- a/guides/index-template.md +++ b/guides/index-template.md @@ -1,10 +1,8 @@ # Index Template Index templates allow you to define default settings, mappings, and aliases for one or more indices during their creation. This guide will teach you how to create index templates and apply them to indices using the OpenSearch .NET client. +See [samples/Samples/IndexTemplate/IndexTemplateSample.cs](../samples/Samples/IndexTemplate/IndexTemplateSample.cs) for a complete working sample. ## Setup -**At the time of writing the API methods related to composable templates do not yet exist in the high-level client, as such this guide makes use of their low-level counterparts.** - - Assuming you have OpenSearch running locally on port 9200, you can create a client instance with the following code: ```csharp @@ -19,95 +17,88 @@ var config = new ConnectionSettings(node) var client = new OpenSearchClient(config);; ``` +The below examples are based off of the following class definition to represent the contents of the index: -## Index Template API Actions +```csharp +public class Book +{ + public string? Title { get; set; } + public string? Author { get; set; } + public DateTime? PublishedOn { get; set; } + public int? Pages { get; set; } +} +``` +## Index Template API Actions ### Create an Index Template You can create an index template to define default settings and mappings for indices of certain patterns. The following example creates an index template named `books` with default settings and mappings for indices of the `books-*` pattern: ```csharp -client.LowLevel.Indices.PutTemplateV2ForAll("books", PostData.Serializable(new -{ - index_patterns = new[] { "books-*" }, - priority = 0, - template = new - { - settings = new - { - index = new - { - number_of_shards = 3, - number_of_replicas = 0 - } - }, - mappings = new - { - properties = new - { - title = new { type = "text" }, - author = new { type = "text" }, - published_on = new { type = "date" }, - pages = new { type = "integer" } - } - } - } -})); +var putTemplate = await client.Indices.PutComposableTemplateAsync("books", d => d + .IndexPatterns("books-*") + .Priority(0) + .Template(t => t + .Settings(s => s + .NumberOfShards(3) + .NumberOfReplicas(0)) + .Map(m => m + .Properties(p => p + .Text(f => f.Name(b => b.Title)) + .Text(f => f.Name(b => b.Author)) + .Date(f => f.Name(b => b.PublishedOn)) + .Number(f => f.Name(b => b.Pages).Type(NumberType.Integer)) + )))); +Console.WriteLine($"Put Template: {putTemplate.IsValid}"); +// -> Put Template: True ``` Now, when you create an index that matches the `books-*` pattern, OpenSearch will automatically apply the template's settings and mappings to the index. Let's create an index named `books-nonfiction` and verify that its settings and mappings match those of the template: ```csharp -client.Indices.Create("books-nonfiction"); -var getResponse = client.Indices.Get("books-nonfiction"); -Console.WriteLine(getResponse.Indices["books-nonfiction"].Mappings.Properties["pages"].Type); // integer -``` +var createIndex = await client.Indices.CreateAsync("books-nonfiction"); +Console.WriteLine($"Create Index: {createIndex.IsValid}"); +// -> Create Index: True +var getIndex = await client.Indices.GetAsync("books-nonfiction"); +Console.WriteLine($"`pages` property type: {getIndex.Indices["books-nonfiction"].Mappings.Properties["pages"].Type}"); +// -> `pages` property type: integer +``` ### Multiple Index Templates ```csharp -var createResponseOne = client.LowLevel.Indices.PutTemplateV2ForAll("books", PostData.Serializable(new -{ - index_patterns = new[] { "books-*" }, - priority = 0, - template = new - { - settings = new - { - index = new - { - number_of_shards = 3, - number_of_replicas = 0 - } - } - } -})); - -client.LowLevel.Indices.PutTemplateV2ForAll("books-fiction", PostData.Serializable(new -{ - index_patterns = new[] { "books-fiction-*" }, - priority = 1, // higher priority than the `books` template - template = new - { - settings = new - { - index = new - { - number_of_shards = 1, - number_of_replicas = 1 - } - } - } -})); +putTemplate = await client.Indices.PutComposableTemplateAsync("books", d => d + .IndexPatterns("books-*") + .Priority(0) + .Template(t => t + .Settings(s => s + .NumberOfShards(3) + .NumberOfReplicas(0)))); +Console.WriteLine($"Put Template: {putTemplate.IsValid}"); +// -> Put Template: True + +putTemplate = await client.Indices.PutComposableTemplateAsync("books-fiction", d => d + .IndexPatterns("books-fiction-*") + .Priority(1) // higher priority than the `books` template + .Template(t => t + .Settings(s => s + .NumberOfShards(1) + .NumberOfReplicas(1)))); +Console.WriteLine($"Put Template: {putTemplate.IsValid}"); +// -> Put Template: True ``` When we create an index named `books-fiction-romance`, OpenSearch will apply the `books-fiction-*` template's settings to the index: ```csharp -client.Indices.Create("books-fiction-romance"); -var getResponse = client.Indices.Get("books-fiction-romance"); -Console.WriteLine(getResponse.Indices["books-fiction-romance"].Settings.NumberOfShards); // 1 +createIndex = await client.Indices.CreateAsync("books-fiction-romance"); +Console.WriteLine($"Create Index: {createIndex.IsValid}"); +// -> Create Index: True + +getIndex = await client.Indices.GetAsync("books-fiction-romance"); +Console.WriteLine($"Number of shards: {getIndex.Indices["books-fiction-romance"].Settings.NumberOfShards}"); +// -> Number of shards: 1 ``` @@ -115,83 +106,71 @@ Console.WriteLine(getResponse.Indices["books-fiction-romance"].Settings.NumberOf Composable index templates are a new type of index template that allow you to define multiple component templates and compose them into a final template. The following example creates a component template named `books_mappings` with default mappings for indices of the `books-*` and `books-fiction-*` patterns: ```csharp -// Create a component template -client.Cluster.PutComponentTemplate("books_mappings", ct => ct - .Template(t => t - .Map(m => m - .Properties(p => p - .Text(tp => tp - .Name("title")) - .Text(tp => tp - .Name("author")) - .Date(d => d - .Name("published_on")) - .Number(n => n - .Name("pages") - .Type(NumberType.Integer)))))); - -// Create an index template for "books" -var createBooksTemplateResponse = client.LowLevel.Indices.PutTemplateV2ForAll("books", PostData.Serializable(new -{ - index_patterns = new[] { "books-*" }, - composed_of = new[] { "books_mappings" }, - priority = 0, - template = new - { - settings = new - { - index = new - { - number_of_shards = 3, - number_of_replicas = 0 - } - } - } -})); - -// Create an index template for "books-fiction" -var createBooksFictionTemplateResponse = client.LowLevel.Indices.PutTemplateV2ForAll("books-fiction", PostData.Serializable(new -{ - index_patterns = new[] { "books-fiction-*" }, - composed_of = new[] { "books_mappings" }, - priority = 1, - template = new - { - settings = new - { - index = new - { - number_of_shards = 1, - number_of_replicas = 1 - } - } - } -})); +var putComponentTemplate = await client.Cluster.PutComponentTemplateAsync("books_mappings", d => d + .Template(t => t + .Map(m => m + .Properties(p => p + .Text(f => f.Name(b => b.Title)) + .Text(f => f.Name(b => b.Author)) + .Date(f => f.Name(b => b.PublishedOn)) + .Number(f => f.Name(b => b.Pages).Type(NumberType.Integer)) + )))); +Console.WriteLine($"Put Component Template: {putComponentTemplate.IsValid}"); +// -> Put Component Template: True + +putTemplate = await client.Indices.PutComposableTemplateAsync("books", d => d + .IndexPatterns("books-*") + .Priority(0) + .ComposedOf("books_mappings") + .Template(t => t + .Settings(s => s + .NumberOfShards(3) + .NumberOfReplicas(0)))); +Console.WriteLine($"Put Template: {putTemplate.IsValid}"); +// -> Put Template: True + +putTemplate = await client.Indices.PutComposableTemplateAsync("books-fiction", d => d + .IndexPatterns("books-fiction-*") + .Priority(1) // higher priority than the `books` template + .ComposedOf("books_mappings") + .Template(t => t + .Settings(s => s + .NumberOfShards(1) + .NumberOfReplicas(1)))); +Console.WriteLine($"Put Template: {putTemplate.IsValid}"); +// -> Put Template: True ``` When we create an index named `books-fiction-horror`, OpenSearch will apply the `books-fiction-*` template's settings, and `books_mappings` template mappings to the index: ```csharp -client.Indices.Create("books-fiction-horror"); -var getResponse = client.Indices.Get("books-fiction-horror"); -Console.WriteLine(getResponse.Indices["books-fiction-horror"].Settings.NumberOfShards); // 1 -Console.WriteLine(getResponse.Indices["books-fiction-horror"].Mappings.Properties["pages"].Type); // integer +createIndex = await client.Indices.CreateAsync("books-fiction-horror"); +Console.WriteLine($"Create Index: {createIndex.IsValid}"); +// -> Create Index: True + +getIndex = await client.Indices.GetAsync("books-fiction-horror"); +Console.WriteLine($"Number of shards: {getIndex.Indices["books-fiction-horror"].Settings.NumberOfShards}"); +Console.WriteLine($"`pages` property type: {getIndex.Indices["books-fiction-horror"].Mappings.Properties["pages"].Type}"); +// -> Number of shards: 1 +// -> `pages` property type: integer ``` ### Get an Index Template -You can get an index template with the `GetTemplateV2ForAll` API action. The following example gets the `books` index template: +You can get an index template with the `GetComposableTemplate` API action. The following example gets the `books` index template: ```csharp -var getResponse = client.LowLevel.Indices.GetTemplateV2ForAll("books").Body; -Console.WriteLine($"Get response: {getResponse}"); // Get response: {"books":{"order":0,"index_patterns":["books-*"],"settings":{"index":{"number_of_shards":"3","number_of_replicas":"0"}},"mappings":{},"aliases":{}}} +var getTemplate = await client.Indices.GetComposableTemplateAsync("books"); +Console.WriteLine($"First index pattern: {getTemplate.IndexTemplates.First().IndexTemplate.IndexPatterns.First()}"); +// -> First index pattern: books-* ``` ### Delete an Index Template -You can delete an index template with the `DeleteTemplateV2ForAll` API action. The following example deletes the `books` index template: +You can delete an index template with the `DeleteComposableTemplate` API action. The following example deletes the `books` index template: ```csharp -var deleteResponse = client.LowLevel.Indices.DeleteTemplateV2ForAll("books"); -Console.WriteLine($"Delete response: {deleteResponse}"); // Delete response: {"acknowledged":true} +var deleteTemplate = await client.Indices.DeleteComposableTemplateAsync("books"); +Console.WriteLine($"Delete Template: {deleteTemplate.IsValid}"); +// -> Delete Template: True ``` @@ -199,7 +178,12 @@ Console.WriteLine($"Delete response: {deleteResponse}"); // Delete response: {"a Let's delete all resources created in this guide: ```csharp -client.Indices.Delete("books-"); -client.LowLevel.Indices.DeleteTemplateV2ForAll("books-fiction"); -client.Cluster.DeleteComponentTemplate("books_mappings"); +var deleteIndex = await client.Indices.DeleteAsync("books-*"); +Console.WriteLine($"Delete Index: {deleteIndex.IsValid}"); + +deleteTemplate = await client.Indices.DeleteComposableTemplateAsync("books-fiction"); +Console.WriteLine($"Delete Template: {deleteTemplate.IsValid}"); + +var deleteComponentTemplate = await client.Cluster.DeleteComponentTemplateAsync("books_mappings"); +Console.WriteLine($"Delete Component Template: {deleteComponentTemplate.IsValid}"); ``` diff --git a/guides/json.md b/guides/json.md new file mode 100644 index 0000000000..b946b59000 --- /dev/null +++ b/guides/json.md @@ -0,0 +1,129 @@ +- [Making Raw JSON REST Requests](#making-raw-json-rest-requests) + - [HTTP Methods](#http-methods) + - [GET](#get) + - [PUT](#put) + - [POST](#post) + - [DELETE](#delete) + - [Using Different Types Of PostData](#using-different-types-of-postdata) + - [PostData.String](#postdatastring) + - [PostData.Bytes](#postdatabytes) + - [PostData.Serializable](#postdataserializable) + - [PostData.MultiJson](#postdatamultijson) + +# Making Raw JSON REST Requests +The OpenSearch client implements many high-level REST DSLs that invoke OpenSearch APIs. However you may find yourself in a situation that requires you to invoke an API that is not supported by the client. You can use `client.LowLevel.DoRequest` to do so. See [samples/Samples/RawJson/RawJsonSample.cs](../samples/Samples/RawJson/RawJsonSample.cs) for a complete working sample. + +## HTTP Methods + +### GET +The following example returns the server version information via `GET /`. + +```csharp +var info = await client.LowLevel.DoRequestAsync(HttpMethod.GET, "/", CancellationToken.None); +Console.WriteLine($"Welcome to {info.Body.version.distribution} {info.Body.version.number}!"); +``` + +### PUT +The following example creates an index. + +```csharp +var indexBody = new { settings = new { index = new { number_of_shards = 4 } } }; + +var createIndex = await client.LowLevel.DoRequestAsync(HttpMethod.PUT, "/movies", CancellationToken.None, PostData.Serializable(indexBody)); +Debug.Assert(createIndex.Success && (bool)createIndex.Body.acknowledged, createIndex.DebugInformation); +``` + +### POST +The following example searches for a document. + +```csharp +const string q = "miller"; + +var query = new +{ + size = 5, + query = new { multi_match = new { query = q, fields = new[] { "title^2", "director" } } } +}; + +var search = await client.LowLevel.DoRequestAsync(HttpMethod.POST, $"/{indexName}/_search", CancellationToken.None, PostData.Serializable(query)); +Debug.Assert(search.Success, search.DebugInformation); + +foreach (var hit in search.Body.hits.hits) Console.WriteLine(hit["_source"]["title"]); +``` + +### DELETE +The following example deletes an index. + +```csharp +var deleteDocument = await client.LowLevel.DoRequestAsync(HttpMethod.DELETE, $"/{indexName}/_doc/{id}", CancellationToken.None); +Debug.Assert(deleteDocument.Success, deleteDocument.DebugInformation); +``` + +## Using Different Types Of PostData +The OpenSearch .NET client provides a `PostData` class that is used to provide the request body for a request. The `PostData` class has several static methods that can be used to create a `PostData` object from different types of data. + +### PostData.String +The following example shows how to use the `PostData.String` method to create a `PostData` object from a string. + +```csharp +string indexBody = @" +{{ + ""settings"": { + ""index"": { + ""number_of_shards"": 4 + } + } +}}"; + +await client.LowLevel.DoRequestAsync(HttpMethod.PUT, "/movies", CancellationToken.None, PostData.String(indexBody)); +``` + +### PostData.Bytes +The following example shows how to use the `PostData.Bytes` method to create a `PostData` object from a byte array. + +```csharp +byte[] indexBody = Encoding.UTF8.GetBytes(@" +{{ + ""settings"": { + ""index"": { + ""number_of_shards"": 4 + } + } +}}"); + +await client.LowLevel.DoRequestAsync(HttpMethod.PUT, "/movies", CancellationToken.None, PostData.Bytes(indexBody)); +``` + +### PostData.Serializable +The following example shows how to use the `PostData.Serializable` method to create a `PostData` object from a serializable object. + +```csharp +var indexBody = new +{ + settings = new + { + index = new + { + number_of_shards = 4 + } + } +}; + +await client.LowLevel.DoRequestAsync(HttpMethod.PUT, "/movies", CancellationToken.None, PostData.Serializable(indexBody)); +``` + +### PostData.MultiJson +The following example shows how to use the `PostData.MultiJson` method to create a `PostData` object from a collection of serializable objects. +The `PostData.MultiJson` method is useful when you want to send multiple documents in a bulk request. + +```csharp +var bulkBody = new object[] +{ + new { index = new { _index = "movies", _id = "1" } }, + new { title = "The Godfather", director = "Francis Ford Coppola", year = 1972 }, + new { index = new { _index = "movies", _id = "2" } }, + new { title = "The Godfather: Part II", director = "Francis Ford Coppola", year = 1974 } +}; + +await client.LowLevel.DoRequestAsync(HttpMethod.POST, "/_bulk", CancellationToken.None, PostData.MultiJson(bulkBody)); +``` diff --git a/samples/Samples/IndexTemplate/IndexTemplateSample.cs b/samples/Samples/IndexTemplate/IndexTemplateSample.cs new file mode 100644 index 0000000000..eb89ed1344 --- /dev/null +++ b/samples/Samples/IndexTemplate/IndexTemplateSample.cs @@ -0,0 +1,172 @@ +/* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +*/ + +using System.Diagnostics; +using OpenSearch.Client; + +namespace Samples.IndexTemplate; + +public class IndexTemplateSample : Sample +{ + public IndexTemplateSample() + : base("index-template", "A sample demonstrating how to use the client to create and manage index templates") { } + + protected override async Task Run(IOpenSearchClient client) + { + // Create index template + + var putTemplate = await client.Indices.PutComposableTemplateAsync("books", d => d + .IndexPatterns("books-*") + .Priority(0) + .Template(t => t + .Settings(s => s + .NumberOfShards(3) + .NumberOfReplicas(0)) + .Map(m => m + .Properties(p => p + .Text(f => f.Name(b => b.Title)) + .Text(f => f.Name(b => b.Author)) + .Date(f => f.Name(b => b.PublishedOn)) + .Number(f => f.Name(b => b.Pages).Type(NumberType.Integer)) + )))); + Debug.Assert(putTemplate.IsValid, putTemplate.DebugInformation); + Console.WriteLine($"Put Template: {putTemplate.IsValid}"); + + // Confirm mapping + + var createIndex = await client.Indices.CreateAsync("books-nonfiction"); + Debug.Assert(createIndex.IsValid, createIndex.DebugInformation); + Console.WriteLine($"Create Index: {createIndex.IsValid}"); + + var getIndex = await client.Indices.GetAsync("books-nonfiction"); + Debug.Assert( + getIndex.Indices["books-nonfiction"].Mappings.Properties["pages"].Type == "integer", + "`pages` property should have `integer` type"); + Console.WriteLine($"`pages` property type: {getIndex.Indices["books-nonfiction"].Mappings.Properties["pages"].Type}"); + + // Multiple index templates + + putTemplate = await client.Indices.PutComposableTemplateAsync("books", d => d + .IndexPatterns("books-*") + .Priority(0) + .Template(t => t + .Settings(s => s + .NumberOfShards(3) + .NumberOfReplicas(0)))); + Debug.Assert(putTemplate.IsValid, putTemplate.DebugInformation); + Console.WriteLine($"Put Template: {putTemplate.IsValid}"); + + putTemplate = await client.Indices.PutComposableTemplateAsync("books-fiction", d => d + .IndexPatterns("books-fiction-*") + .Priority(1) // higher priority than the `books` template + .Template(t => t + .Settings(s => s + .NumberOfShards(1) + .NumberOfReplicas(1)))); + Debug.Assert(putTemplate.IsValid, putTemplate.DebugInformation); + Console.WriteLine($"Put Template: {putTemplate.IsValid}"); + + // Validate settings + + createIndex = await client.Indices.CreateAsync("books-fiction-romance"); + Debug.Assert(createIndex.IsValid, createIndex.DebugInformation); + Console.WriteLine($"Create Index: {createIndex.IsValid}"); + + getIndex = await client.Indices.GetAsync("books-fiction-romance"); + Debug.Assert( + getIndex.Indices["books-fiction-romance"].Settings.NumberOfShards == 1, + "`books-fiction-romance` index should have 1 shard"); + Console.WriteLine($"Number of shards: {getIndex.Indices["books-fiction-romance"].Settings.NumberOfShards}"); + + // Component templates + + var putComponentTemplate = await client.Cluster.PutComponentTemplateAsync("books_mappings", d => d + .Template(t => t + .Map(m => m + .Properties(p => p + .Text(f => f.Name(b => b.Title)) + .Text(f => f.Name(b => b.Author)) + .Date(f => f.Name(b => b.PublishedOn)) + .Number(f => f.Name(b => b.Pages).Type(NumberType.Integer)) + )))); + Debug.Assert(putComponentTemplate.IsValid, putComponentTemplate.DebugInformation); + Console.WriteLine($"Put Component Template: {putComponentTemplate.IsValid}"); + + putTemplate = await client.Indices.PutComposableTemplateAsync("books", d => d + .IndexPatterns("books-*") + .Priority(0) + .ComposedOf("books_mappings") + .Template(t => t + .Settings(s => s + .NumberOfShards(3) + .NumberOfReplicas(0)))); + Debug.Assert(putTemplate.IsValid, putTemplate.DebugInformation); + Console.WriteLine($"Put Template: {putTemplate.IsValid}"); + + putTemplate = await client.Indices.PutComposableTemplateAsync("books-fiction", d => d + .IndexPatterns("books-fiction-*") + .Priority(1) // higher priority than the `books` template + .ComposedOf("books_mappings") + .Template(t => t + .Settings(s => s + .NumberOfShards(1) + .NumberOfReplicas(1)))); + Debug.Assert(putTemplate.IsValid, putTemplate.DebugInformation); + Console.WriteLine($"Put Template: {putTemplate.IsValid}"); + + // Validate settings & mappings + createIndex = await client.Indices.CreateAsync("books-fiction-horror"); + Debug.Assert(createIndex.IsValid, createIndex.DebugInformation); + Console.WriteLine($"Create Index: {createIndex.IsValid}"); + + getIndex = await client.Indices.GetAsync("books-fiction-horror"); + Debug.Assert( + getIndex.Indices["books-fiction-horror"].Settings.NumberOfShards == 1, + "`books-fiction-horror` index should have 1 shard"); + Debug.Assert( + getIndex.Indices["books-fiction-horror"].Mappings.Properties["pages"].Type == "integer", + "`pages` property should have `integer` type"); + Console.WriteLine($"Number of shards: {getIndex.Indices["books-fiction-horror"].Settings.NumberOfShards}"); + Console.WriteLine($"`pages` property type: {getIndex.Indices["books-fiction-horror"].Mappings.Properties["pages"].Type}"); + + // Get index template + + var getTemplate = await client.Indices.GetComposableTemplateAsync("books"); + Debug.Assert( + getTemplate.IndexTemplates.First().IndexTemplate.IndexPatterns.First() == "books-*", + "First index pattern should be `books-*`"); + Console.WriteLine($"First index pattern: {getTemplate.IndexTemplates.First().IndexTemplate.IndexPatterns.First()}"); + + // Delete index template + + var deleteTemplate = await client.Indices.DeleteComposableTemplateAsync("books"); + Debug.Assert(deleteTemplate.IsValid, deleteTemplate.DebugInformation); + Console.WriteLine($"Delete Template: {deleteTemplate.IsValid}"); + + // Cleanup + + var deleteIndex = await client.Indices.DeleteAsync("books-*"); + Debug.Assert(deleteIndex.IsValid, deleteIndex.DebugInformation); + Console.WriteLine($"Delete Index: {deleteIndex.IsValid}"); + + deleteTemplate = await client.Indices.DeleteComposableTemplateAsync("books-fiction"); + Debug.Assert(deleteTemplate.IsValid, deleteTemplate.DebugInformation); + Console.WriteLine($"Delete Template: {deleteTemplate.IsValid}"); + + var deleteComponentTemplate = await client.Cluster.DeleteComponentTemplateAsync("books_mappings"); + Debug.Assert(deleteComponentTemplate.IsValid, deleteComponentTemplate.DebugInformation); + Console.WriteLine($"Delete Component Template: {deleteComponentTemplate.IsValid}"); + } + + private class Book + { + public string? Title { get; set; } + public string? Author { get; set; } + public DateTime? PublishedOn { get; set; } + public int? Pages { get; set; } + } +} diff --git a/samples/Samples/Program.cs b/samples/Samples/Program.cs new file mode 100644 index 0000000000..e4174840de --- /dev/null +++ b/samples/Samples/Program.cs @@ -0,0 +1,24 @@ +/* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +*/ + +using System.CommandLine; +using Samples.Utils; + +namespace Samples; + +public static class Program +{ + public static async Task Main(string[] args) + { + var rootCommand = new RootCommand("A collection of samples demonstrating how to use the OpenSearch .NET client"); + var clientDescriptor = rootCommand.AddOpenSearchClientOptions(); + + foreach (var sample in Sample.GetAllSamples()) rootCommand.AddCommand(sample.AsCommand(clientDescriptor)); + + await rootCommand.InvokeAsync(args); + } +} diff --git a/samples/Samples/RawJson/RawJsonSample.cs b/samples/Samples/RawJson/RawJsonSample.cs new file mode 100644 index 0000000000..62a75719ef --- /dev/null +++ b/samples/Samples/RawJson/RawJsonSample.cs @@ -0,0 +1,68 @@ +/* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +*/ + +using System.Diagnostics; +using OpenSearch.Client; +using OpenSearch.Net; +using HttpMethod = OpenSearch.Net.HttpMethod; + +namespace Samples.RawJson; + +public class RawJsonSample : Sample +{ + public RawJsonSample() : base("raw-json", "A sample demonstrating how to use the low-level client to perform raw JSON requests") { } + + protected override async Task Run(IOpenSearchClient client) + { + var info = await client.LowLevel.DoRequestAsync(HttpMethod.GET, "/", CancellationToken.None); + Debug.Assert(info.Success, info.DebugInformation); + Console.WriteLine($"Welcome to {info.Body.version.distribution} {info.Body.version.number}!"); + + // Create an index + + const string indexName = "movies"; + + var indexBody = new { settings = new { index = new { number_of_shards = 4 } } }; + + var createIndex = await client.LowLevel.DoRequestAsync(HttpMethod.PUT, $"/{indexName}", CancellationToken.None, PostData.Serializable(indexBody)); + Debug.Assert(createIndex.Success && (bool)createIndex.Body.acknowledged, createIndex.DebugInformation); + + // Add a document to the index + var document = new { title = "Moneyball", director = "Bennett Miller", year = 2011}; + + const string id = "1"; + + var addDocument = await client.LowLevel.DoRequestAsync(HttpMethod.PUT, $"/{indexName}/_doc/{id}", CancellationToken.None, PostData.Serializable(document)); + Debug.Assert(addDocument.Success, addDocument.DebugInformation); + + // Refresh the index + var refresh = await client.LowLevel.DoRequestAsync(HttpMethod.POST, $"/{indexName}/_refresh", CancellationToken.None); + Debug.Assert(refresh.Success, refresh.DebugInformation); + + // Search for a document + const string q = "miller"; + + var query = new + { + size = 5, + query = new { multi_match = new { query = q, fields = new[] { "title^2", "director" } } } + }; + + var search = await client.LowLevel.DoRequestAsync(HttpMethod.POST, $"/{indexName}/_search", CancellationToken.None, PostData.Serializable(query)); + Debug.Assert(search.Success, search.DebugInformation); + + foreach (var hit in search.Body.hits.hits) Console.WriteLine(hit["_source"]["title"]); + + // Delete the document + var deleteDocument = await client.LowLevel.DoRequestAsync(HttpMethod.DELETE, $"/{indexName}/_doc/{id}", CancellationToken.None); + Debug.Assert(deleteDocument.Success, deleteDocument.DebugInformation); + + // Delete the index + var deleteIndex = await client.LowLevel.DoRequestAsync(HttpMethod.DELETE, $"/{indexName}", CancellationToken.None); + Debug.Assert(deleteIndex.Success && (bool)deleteIndex.Body.acknowledged, deleteIndex.DebugInformation); + } +} diff --git a/samples/Samples/Sample.cs b/samples/Samples/Sample.cs new file mode 100644 index 0000000000..6126255c99 --- /dev/null +++ b/samples/Samples/Sample.cs @@ -0,0 +1,42 @@ +/* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +*/ + +using System.CommandLine; +using System.CommandLine.Binding; +using OpenSearch.Client; + +namespace Samples; + +public abstract class Sample +{ + public static IEnumerable GetAllSamples() => + typeof(Sample) + .Assembly + .GetTypes() + .Where(t => !t.IsAbstract && t.IsSubclassOf(typeof(Sample))) + .Select(t => (Sample) Activator.CreateInstance(t)!); + + private readonly string _name; + private readonly string _description; + + protected Sample(string name, string description) + { + _name = name; + _description = description; + } + + public Command AsCommand(IValueDescriptor clientDescriptor) + { + var command = new Command(_name, _description); + + command.SetHandler(Run, clientDescriptor); + + return command; + } + + protected abstract Task Run(IOpenSearchClient client); +} diff --git a/samples/Samples/Samples.csproj b/samples/Samples/Samples.csproj new file mode 100644 index 0000000000..6ac00aa311 --- /dev/null +++ b/samples/Samples/Samples.csproj @@ -0,0 +1,19 @@ + + + + Exe + net6.0 + enable + enable + False + + + + + + + + + + + \ No newline at end of file diff --git a/samples/Samples/Utils/OpenSearchClientOptions.cs b/samples/Samples/Utils/OpenSearchClientOptions.cs new file mode 100644 index 0000000000..ae42bdb387 --- /dev/null +++ b/samples/Samples/Utils/OpenSearchClientOptions.cs @@ -0,0 +1,59 @@ +/* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +*/ + +using System.CommandLine; +using System.CommandLine.Binding; +using OpenSearch.Client; +using OpenSearch.Net; + +namespace Samples.Utils; + +public static class OpenSearchClientOptions +{ + public static IValueDescriptor AddOpenSearchClientOptions(this Command command, bool global = true) + { + Option host = new("--host", () => new Uri("https://localhost:9200"), "The OpenSearch host to connect to"); + Option username = new("--username", () => "admin", "The username to use for authentication"); + Option password = new("--password", () => "admin", "The password to use for authentication"); + + Action