From 347ef9b0222abc235f8bb1cf7a6adfbb23794b0d Mon Sep 17 00:00:00 2001 From: Alex Keh Date: Wed, 3 Sep 2025 08:59:35 +0800 Subject: [PATCH] Oracle .NET AI vector sample and repo README updates (#459) * Oracle .NET AI vector sample upload * Update repo README.md * Update samples README.md --- README.md | 9 +- samples/README.md | 9 +- ...Ai-vector-search-stores-and-collections.cs | 192 ++++++++++++++++++ samples/ai-vector/AppSettings.json | 5 + samples/ai-vector/Hotel.cs | 49 +++++ samples/ai-vector/Hotels.json | 142 +++++++++++++ samples/ai-vector/Load-model.sql | 56 +++++ .../ai-vector/Oracle AI Vector Data.csproj | 25 +++ 8 files changed, 480 insertions(+), 7 deletions(-) create mode 100644 samples/ai-vector/Ai-vector-search-stores-and-collections.cs create mode 100644 samples/ai-vector/AppSettings.json create mode 100644 samples/ai-vector/Hotel.cs create mode 100644 samples/ai-vector/Hotels.json create mode 100644 samples/ai-vector/Load-model.sql create mode 100644 samples/ai-vector/Oracle AI Vector Data.csproj diff --git a/README.md b/README.md index b51e3dc5..d1bdc013 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,16 @@ This repository provides .NET code samples for Oracle developers, specifically f and other Oracle .NET components. .NET products for Oracle Database are free of charge. They consist of: ## Getting Started -Oracle .NET components are available individually on NuGet Gallery or bundled together as part of Oracle Data Access Components (ODAC). Provider downloads are available from NuGet Gallery and Oracle .NET download page. Oracle Developer Tools for VS Code or Visual Studio can be downloaded from the VS Code Marketplace or Visual Studio Marketplace, respectively. +Oracle .NET components are available individually on NuGet Gallery or bundled together as part of Oracle Data Access Components (ODAC). Provider downloads are available from NuGet Gallery and Oracle .NET download page. Oracle Developer Tools for Visual Studio can be downloaded from the Visual Studio Marketplace. ## Tutorials -For beginning Oracle .NET developers, these on-premises .NET database tutorials and Autonomous Database .NET tutorials will assist you in getting started with .NET application development with Oracle Database. +For novice Oracle .NET developers, these on-premises .NET database tutorials and Autonomous Database .NET tutorials will assist you in getting started with .NET application development with Oracle Database. Oracle .NET video tutorials are available from the Oracle .NET YouTube site. @@ -27,7 +26,7 @@ For those that want to build an end-to-end basic .NET web app for Oracle Databas diff --git a/samples/README.md b/samples/README.md index 94581b03..c8627c08 100644 --- a/samples/README.md +++ b/samples/README.md @@ -24,6 +24,10 @@ Running ODP.NET Core Samples from Command Line Below is the feature list the samples cover. Each feature's sample has its own subdirectory. +AI Vector +---------------------- +* Search Vector Stores and Collections Sample: Load and vectorize data from .NET. Then, perform exact match searches and similarity searches against the data set. + Application Continuity ---------------------- * Sample 1: Unmanaged ODP.NET Application Continuity code sample with setup and runtime demo instructions. @@ -53,9 +57,9 @@ Autonomous Database * Managed ODP.NET Samples: Demonstrates how to connect managed ODP.NET to Oracle Autonomous Database via a console and an ASP.NET web app.
* Unmanaged ODP.NET Sample: Demonstrates how to connect unmanaged ODP.NET to Oracle Autonomous Database via a console app. -Azure Active Directory +Azure Active Directory/Microsoft Entra ID ---------------------- -* Demonstrates connecting to Oracle Autonomous Database using an Azure Active Directory token with ODP.NET Core, managed, and unmanaged. +* Demonstrates connecting to Oracle Autonomous Database using a Microsoft Entra ID token with ODP.NET Core, managed, and unmanaged. Bulk Copy --------- @@ -177,3 +181,4 @@ User-Defined Types (UDT) * Ref Sample: Demonstrates how to fetch UDTs referenced by REFs.
* Ref Inheritance Sample: Demonstrates how to obtain and update Custom Type objects from OracleRef objects.
* VARRAY Sample: Demonstrates how to map, fetch, and manipulate the Oracle VARRAY as a custom object. + diff --git a/samples/ai-vector/Ai-vector-search-stores-and-collections.cs b/samples/ai-vector/Ai-vector-search-stores-and-collections.cs new file mode 100644 index 00000000..5a79af21 --- /dev/null +++ b/samples/ai-vector/Ai-vector-search-stores-and-collections.cs @@ -0,0 +1,192 @@ +using Oracle.VectorData; +using Oracle.ManagedDataAccess.Client; +using Microsoft.Extensions.VectorData; +using Microsoft.Extensions.Configuration; +using System.Text.Json; + +namespace OracleAIVectorData; + +// This ODP.NET sample app shows various ways to search vector data and collections. +// First, the data (hotels.json) is loaded into a .NET object list. +// The data initially includes hotel names, descriptions, ids, ratings, and parking availability. +// The hotel names and descriptions are then vectorized using the database ONNX embeddings, consisting of 384 dimensions in Float format. +// The entire data set is upserted into the database. +// Finally, four search operations are demonstrated. +// 1. Search by primary key. +// 2. Search scalar value properties. +// 3. Similarity search using cosine similarity. +// 4. Similarity search using Euclidean distance. +// This sample requires Oracle Database 23ai or higher. +// Add Oracle.VectorData and Microsoft.Extensions.Configuration.Json NuGet packages to your project. + +public class AIHotelSearchApp +{ + public static async Task Main(string[] args) + { + await SearchHotels(); + } + static async Task SearchHotels() + { + // Setup ODP.NET connection and vector configuration. + // Set connection string values in AppSettings.json file. + // Add AppSettings.json directory path below or place file in app's output directory. + var configuration = new ConfigurationBuilder() + .AddJsonFile(path: "AppSettings.json", optional: false) + .Build(); + + string? connStr = configuration.GetSection("Oracle")["ConnectionString"]; + OracleDataSource? ds = null; + OracleVectorStore? vs = null; + OracleCollection? collection = null; + string collectionName = "Hotels"; + + try + { + ds = new OracleDataSourceBuilder(connStr).Build(); + + // Create a vector store + vs = new OracleVectorStore(ds); + + // Create a vector collection + collection = (OracleCollection)vs.GetCollection(collectionName); + + // HotelsData.json contains plain text information about various hotels. + // Add Hotels.json directory path below or place file in app's output directory. + string jsonContent = File.ReadAllText("Hotels.json"); + List? hotels = JsonSerializer.Deserialize>(jsonContent); + + // Use the database ONNX generator to create VECTOR(384, FLOAT32) embeddings for each hotel/record. + foreach (Hotel hotel in hotels) + { + hotel.NameEmbedding = await GenerateEmbeddingAsync(ds, hotel.HotelName); + hotel.DescriptionEmbedding = await GenerateEmbeddingAsync(ds, hotel.Description); + } + + // Verify the collection exists in the database. + await collection.EnsureCollectionExistsAsync(); + + // Upsert the records into the database. + await collection.UpsertAsync(hotels); + + // Search hotels in the vector collection by primary key. + Console.WriteLine("Search for hotels with ID 5 and 10."); + Console.WriteLine("==============================================================================="); + IAsyncEnumerable hotelsById = collection.GetAsync([5, 10]); + await foreach (Hotel hotel in hotelsById) + { + Output(hotel); + } + Console.WriteLine(); + + // Search hotels by their characteristics, such as rating and parking availability. + Console.WriteLine("Search for hotels with a 9 or higher rating and parking."); + Console.WriteLine("==============================================================================="); + IAsyncEnumerable hotelsByFilter2 = collection.GetAsync(r => r.Rating >= 9 && r.HasParking == true, 3); + await foreach (Hotel hotel in hotelsByFilter2) + { + Output(hotel); + } + Console.WriteLine(); + + // Search hotels by their names. Return top three most similar matches. + // Provide a search term, such as "beach". Generate a vector embedding using the search term. + // ODP.NET performs a similarity search using the hotel name and search term embeddings. + // The cosine similarity metric is used to calculate vector distances to find the best matches. + // Scores closer to zero are more similar. Higher scores mean more dissimilarity. + // Results are ranked from most similar to least. + string hotelNameSearchStr = "beach"; + + var nameEmbedding = await GenerateEmbeddingAsync(ds, hotelNameSearchStr); + // Specify the search option for hotel name. + VectorSearchOptions nameOptions = new() { VectorProperty = r => r.NameEmbedding }; + IAsyncEnumerable> namesVectorSearch = collection.SearchAsync(nameEmbedding, top: 3, nameOptions); + + int rank = 1; + Console.WriteLine($"Hotel name similarity search with \"{hotelNameSearchStr}\"."); + Console.WriteLine("==============================================================================="); + await foreach (VectorSearchResult searchResult in namesVectorSearch) + { + Console.WriteLine(rank + $". {searchResult.Record.HotelName}"); + Console.WriteLine($"Score : {searchResult.Score}"); + Console.WriteLine(); + rank++; + } + Console.WriteLine(); + + // Search hotels using their descriptions. Return top three most similar matches. + // Provide a search phrase or sentence. Generate its vector embedding. + // ODP.NET performs a similarity search using the hotel description and search text embeddings. + // The Euclidean distance metric is used to calculate vector distances to find the best matches. + string descriptionSearchStr = "I want a hotel with nature activities."; + var descriptionEmbedding = await GenerateEmbeddingAsync(ds, descriptionSearchStr); + + // Specify the search option for hotel description. + VectorSearchOptions descriptionOptions = new() { VectorProperty = r => r.DescriptionEmbedding }; + IAsyncEnumerable> descriptionVectorSearch = collection.SearchAsync(descriptionEmbedding, top: 3, descriptionOptions); + + rank = 1; + Console.WriteLine($"Hotel description similarity search with \"{descriptionSearchStr}\"."); + Console.WriteLine("==============================================================================="); + await foreach (VectorSearchResult searchResult in descriptionVectorSearch) + { + Console.WriteLine(rank + $". {searchResult.Record.HotelName}"); + Console.WriteLine($"Score : {searchResult.Score}"); + Console.WriteLine($"Description: {searchResult.Record.Description}"); + Console.WriteLine(); + rank++; + } + } + + finally + { + // Clean up and delete the collection + if (vs != null) { await vs.EnsureCollectionDeletedAsync(collectionName); } + ds?.Dispose(); + vs?.Dispose(); + collection?.Dispose(); + } + } + + // Generate embeddings in ONNX format. + // This app uses Hugging Face's all-MiniLM-L12-v2 model for all its embeddings. + static async Task GenerateEmbeddingAsync(OracleDataSource ds, string searchText, CancellationToken cancellationtoken = default) + { + using (OracleConnection conn = await ds.OpenConnectionAsync(cancellationtoken)) + { + using (OracleCommand cmd = new OracleCommand($"SELECT TO_VECTOR(VECTOR_EMBEDDING(ALL_MINILM_L12_V2 USING :1 as DATA), 384, FLOAT32)", conn)) + { + cmd.Parameters.Add("searchStr", OracleDbType.Varchar2, null, System.Data.ParameterDirection.Input); + cmd.Parameters[0].Value = searchText; + return (float[])cmd.ExecuteScalar(); + } + } + } + + // Output the hotel's information to the console. + static void Output(Hotel hotel) + { + Console.WriteLine($"Hotel Name = {hotel.HotelName}"); + Console.WriteLine($"Hotel Id = {hotel.HotelId}"); + Console.WriteLine($"Rating = {hotel.Rating}"); + Console.WriteLine($"HasParking = {hotel.HasParking}"); + Console.WriteLine(); + } +} +/* Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved. */ + +/****************************************************************************** + * + * You may not use the identified files except in compliance with The MIT + * License (the "License.") + * + * You may obtain a copy of the License at + * https://github.com/oracle/Oracle.NET/blob/master/LICENSE.txt + * + * 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. + * + *****************************************************************************/ \ No newline at end of file diff --git a/samples/ai-vector/AppSettings.json b/samples/ai-vector/AppSettings.json new file mode 100644 index 00000000..aef4274b --- /dev/null +++ b/samples/ai-vector/AppSettings.json @@ -0,0 +1,5 @@ +{ + "Oracle": { + "ConnectionString": "User Id=ADMIN; Password=; Data Source=;" + } +} \ No newline at end of file diff --git a/samples/ai-vector/Hotel.cs b/samples/ai-vector/Hotel.cs new file mode 100644 index 00000000..1ff94d3a --- /dev/null +++ b/samples/ai-vector/Hotel.cs @@ -0,0 +1,49 @@ +using Microsoft.Extensions.VectorData; + +namespace OracleAIVectorData +{ + public class Hotel + { + [VectorStoreKey] + public int HotelId { get; set; } + + [VectorStoreData] + public string HotelName { get; set; } + + [VectorStoreData] + public float Rating { get; set; } + + [VectorStoreData] + public bool HasParking { get; set; } + + [VectorStoreData] + public string Description { get; set; } + + //Oracle has numerous vector distance functions to identify the most relevant results. + //Let's use cosine similarity for the hotel name vectors. + [VectorStoreVector(Dimensions: 384, DistanceFunction = DistanceFunction.CosineDistance)] + public float[] NameEmbedding { get; set; } + + //Let's use Euclidean distance for the hotel description vectors. + [VectorStoreVector(Dimensions: 384, DistanceFunction = DistanceFunction.EuclideanDistance)] + public float[] DescriptionEmbedding { get; set; } + } +} +/* Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved. */ + +/****************************************************************************** + * + * You may not use the identified files except in compliance with The MIT + * License (the "License.") + * + * You may obtain a copy of the License at + * https://github.com/oracle/Oracle.NET/blob/master/LICENSE.txt + * + * 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. + * + *****************************************************************************/ \ No newline at end of file diff --git a/samples/ai-vector/Hotels.json b/samples/ai-vector/Hotels.json new file mode 100644 index 00000000..452610df --- /dev/null +++ b/samples/ai-vector/Hotels.json @@ -0,0 +1,142 @@ +[ + { + "HotelName": "The Lodge by the Sea", + "Description": "A legendary beach hotel with beautiful sunsets, outdoor activites, and near top restaurants.", + "HotelId": 1, + "Rating": 9.5, + "HasParking": true + }, + { + "HotelName": "Hotel New York", + "Description": "An urban retreat in the heart of New York City known for its skyline views and well-appointed rooms.", + "HotelId": 2, + "Rating": 8, + "HasParking": false + }, + { + "HotelName": "Maui Island Retreat", + "Description": "A luxury Maui beach resort with environmentally sustainable design and privacy.", + "HotelId": 3, + "Rating": 3, + "HasParking": true + }, + { + "HotelName": "Hotel San Francisco", + "Description": "A historic hotel in the middle of downtown with modern elegance and legendary service.", + "HotelId": 4, + "Rating": 7.9, + "HasParking": false + }, + { + "HotelName": "The Mountain Lake Resort", + "Description": "A wellness retreat set in the Swiss Alps, offering spa, yoga, and cross-country skiing in tranquil surroundings.", + "HotelId": 5, + "Rating": 7, + "HasParking": true + }, + { + "HotelName": "The Standard", + "Description": "Palace-level luxury in London, famed for gourmet dining and the epitome of service.floral lobbies.", + "HotelId": 6, + "Rating": 6.2, + "HasParking": false + }, + { + "HotelName": "Desert Dream", + "Description": "An oasis of luxury for relaxation and fine dining.", + "HotelId": 7, + "Rating": 8, + "HasParking": true + }, + { + "HotelName": "The Cottages by the Lake", + "Description": "Countryside luxury, blending warm design, personal attention, and water activites.", + "HotelId": 8, + "Rating": 5, + "HasParking": false + }, + { + "HotelName": "The Paris", + "Description": "Historic hotel emblematic of French refinement, dining, and service.", + "HotelId": 9, + "Rating": 3.8, + "HasParking": true + }, + { + "HotelName": "Marina Hotel", + "Description": "A sanctuary for sailing enthusiasts on the beach.", + "HotelId": 10, + "Rating": 6, + "HasParking": false + }, + { + "HotelName": "The Grand Hotel", + "Description": "Iconic Tokyo landmark offering classic luxury and a celebrated history next to the city business center.", + "HotelId": 11, + "Rating": 10, + "HasParking": true + }, + { + "HotelName": "The Cabins", + "Description": "Located at the edge of a large forest. Perfect for hiking and exploring.", + "HotelId": 12, + "Rating": 5.5, + "HasParking": false + }, + { + "HotelName": "Botique Hotel", + "Description": "A chateau-hotel in wine country surrounded by the serene beauty of the nature.", + "HotelId": 13, + "Rating": 2, + "HasParking": true + }, + { + "HotelName": "Business Hotel", + "Description": "For the executive wanting to maximize productivity with minimal distractions.", + "HotelId": 14, + "Rating": 4.9, + "HasParking": false + }, + { + "HotelName": "The Country Motel", + "Description": "An affordable hotel in the outskirts of town with basic services and nearby trails.", + "HotelId": 15, + "Rating": 1, + "HasParking": true + }, + { + "HotelName": "The HK", + "Description": "Iconic urban hotel in Hong Kong with harbor views and banquet dining.", + "HotelId": 16, + "Rating": 8.1, + "HasParking": false + }, + { + "HotelName": "The Nature Inn", + "Description": "Located by a river in the hills, this hotel offers rooms with private terraces and stunning views.", + "HotelId": 17, + "Rating": 2.5, + "HasParking": true + }, + { + "HotelName": "Casino Hotel", + "Description": "Elegance, renowed entertainment, and the largest choice of games on the Las Vegas strip.", + "HotelId": 18, + "Rating": 7.3, + "HasParking": false + }, + { + "HotelName": "The Island Hotel", + "Description": "Located in the South Pacific, this luxurious resort has beach and water activities.", + "HotelId": 19, + "Rating": 4, + "HasParking": true + }, + { + "HotelName": "The Healthy Inn", + "Description": "A renowned wellness resort in California, offering holistic health programs and yoga.", + "HotelId": 20, + "Rating": 3.2, + "HasParking": false + } +] \ No newline at end of file diff --git a/samples/ai-vector/Load-model.sql b/samples/ai-vector/Load-model.sql new file mode 100644 index 00000000..d24463eb --- /dev/null +++ b/samples/ai-vector/Load-model.sql @@ -0,0 +1,56 @@ +DECLARE + ONNX_MOD_FILE VARCHAR2(100) := 'all_MiniLM_L12_v2.onnx'; + MODNAME VARCHAR2(500); + LOCATION_URI VARCHAR2(200) := 'https://adwc4pm.objectstorage.us-ashburn-1.oci.customer-oci.com/p/eLddQappgBJ7jNi6Guz9m9LOtYe2u8LWY19GfgU8flFK4N9YgP4kTlrE9Px3pE12/n/adwc4pm/b/OML-Resources/o/'; + +BEGIN + DBMS_OUTPUT.PUT_LINE('ONNX model file name in object storage is: '||ONNX_MOD_FILE); +-------------------------------------------- +-- Define a model name for the loaded model +-------------------------------------------- + SELECT UPPER(REGEXP_SUBSTR(ONNX_MOD_FILE, '[^.]+')) INTO MODNAME from dual; + DBMS_OUTPUT.PUT_LINE('Model will be loaded and saved with name: '||MODNAME); + +----------------------------------------------------- +-- Read the ONNX model file from object storage into +-- the Autonomous Database data pump directory +----------------------------------------------------- + +BEGIN DBMS_DATA_MINING.DROP_MODEL(model_name => MODNAME); +EXCEPTION WHEN OTHERS THEN NULL; END; + + DBMS_CLOUD.GET_OBJECT( + credential_name => NULL, + directory_name => 'DATA_PUMP_DIR', + object_uri => LOCATION_URI||ONNX_MOD_FILE); + +----------------------------------------- +-- Load the ONNX model to the database +----------------------------------------- + + DBMS_VECTOR.LOAD_ONNX_MODEL( + directory => 'DATA_PUMP_DIR', + file_name => ONNX_MOD_FILE, + model_name => MODNAME); + + DBMS_OUTPUT.PUT_LINE('New model successfully loaded with name: '||MODNAME); +END; + +/* Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved. */ + +/****************************************************************************** + * + * You may not use the identified files except in compliance with The MIT + * License (the "License.") + * + * You may obtain a copy of the License at + * https://github.com/oracle/Oracle.NET/blob/master/LICENSE.txt + * + * 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. + * + *****************************************************************************/ \ No newline at end of file diff --git a/samples/ai-vector/Oracle AI Vector Data.csproj b/samples/ai-vector/Oracle AI Vector Data.csproj new file mode 100644 index 00000000..b5da0f8c --- /dev/null +++ b/samples/ai-vector/Oracle AI Vector Data.csproj @@ -0,0 +1,25 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + +