|
| 1 | +--- |
| 2 | +title: Quickstart RAG |
| 3 | +titleSuffix: Azure AI Search |
| 4 | +description: In this quickstart, learn how to use grounding data from Azure AI Search with a chat model on Azure OpenAI. |
| 5 | +author: HeidiSteen |
| 6 | +ms.author: heidist |
| 7 | +ms.service: cognitive-search |
| 8 | +ms.topic: quickstart |
| 9 | +ms.date: 07/22/2024 |
| 10 | +--- |
| 11 | + |
| 12 | +# Quickstart: Generative search (RAG) with grounding data from Azure AI Search |
| 13 | + |
| 14 | +This quickstart shows you how to send queries to a Large Language Model (LLM) for a conversational search experience over your indexed content on Azure AI Search. You use the Azure portal to set up the resources, and then run Python code to call the APIs. |
| 15 | + |
| 16 | +## Prerequisites |
| 17 | + |
| 18 | +- An Azure subscription. [Create one for free](https://azure.microsoft.com/free/). |
| 19 | + |
| 20 | +- [Azure AI Search](search-create-service-portal.md), on any tier. Region must be the same one used for Azure OpenAI. |
| 21 | + |
| 22 | +- [Azure OpenAI](https://aka.ms/oai/access) resource with a deployment of `gpt-35-turbo`, `gpt-4`, or equivalent model, in the same region as Azure AI Search. |
| 23 | + |
| 24 | +- [Visual Studio Code](https://code.visualstudio.com/download) with the [Python extension](https://marketplace.visualstudio.com/items?itemName=ms-python.python) and the [Jupyter package](https://pypi.org/project/jupyter/). For more information, see [Python in Visual Studio Code](https://code.visualstudio.com/docs/languages/python). |
| 25 | + |
| 26 | +## Download file |
| 27 | + |
| 28 | +[Download a Jupyter notebook](https://github.com/Azure-Samples/azure-search-python-samples/tree/main/Quickstart-RAG) from GitHub to send the requests in this quickstart. For more information, see [Downloading files from GitHub](https://docs.github.com/get-started/start-your-journey/downloading-files-from-github). |
| 29 | + |
| 30 | +You can also start a new file on your local system and create requests manually by using the instructions in this article. |
| 31 | + |
| 32 | +## Configure access |
| 33 | + |
| 34 | +Requests to the search endpoint must be authenticated and authorized. You can use API keys or roles for this task. Keys are easier to start with, but roles are more secure. This quickstart assumes roles. |
| 35 | + |
| 36 | +1. Configure Azure OpenAI to use a system-assigned managed identity. |
| 37 | +1. In the Azure portal, find your Azure OpenAI resource. |
| 38 | +1. On the left menu, select **Resource management** > **Identity**. |
| 39 | +1. On the System assigned tab, set status to **On**. |
| 40 | + |
| 41 | +1. Grant Azure OpenAI permission to access Azure AI Search: |
| 42 | +1. In the Azure portal, find your Azure AI Search service. |
| 43 | +1. On the left menu, select **Access control (IAM)**. |
| 44 | +1. Add the following role assignments for Azure OpenAI managed identity: Search Index Data Reader |
| 45 | +You only need data reader for this quickstart because this scenario is limited to query operations. |
| 46 | + |
| 47 | +## Create an index |
| 48 | + |
| 49 | +We recommend the hotels-sample-index, which can be created in minutes and runs on any search service tier. This index is created using built-in sample data. |
| 50 | + |
| 51 | +1. In the Azure portal, find your search service. |
| 52 | + |
| 53 | +1. On the **Overview** home page, select **Import data** at the top to start the wizard. |
| 54 | + |
| 55 | +1. On the **Connect to your data** page, select **Samples** from the dropdown list. |
| 56 | + |
| 57 | +1. Choose the **hotels-sample**. |
| 58 | + |
| 59 | +1. Select **Next** through the remaining pages, accepting the default values. |
| 60 | + |
| 61 | +1. Once the index is created, select **Search management** > **Indexes** from the left menu to open the index. |
| 62 | + |
| 63 | +1. Select **Edit JSON**. |
| 64 | + |
| 65 | +1. Search for "semantic" to find the section in the index for a semantic configuration. Replace the "semantic" line with the following semantic configuration. This example specifies a `"defaultConfiguration"`, which is important to the running of this quickstart. |
| 66 | + |
| 67 | + ```json |
| 68 | + "semantic": { |
| 69 | + "defaultConfiguration": "semantic-config", |
| 70 | + "configurations": [ |
| 71 | + { |
| 72 | + "name": "semantic-config", |
| 73 | + "prioritizedFields": { |
| 74 | + "titleField": { |
| 75 | + "fieldName": "HotelName" |
| 76 | + }, |
| 77 | + "prioritizedContentFields": [ |
| 78 | + { |
| 79 | + "fieldName": "Description" |
| 80 | + } |
| 81 | + ], |
| 82 | + "prioritizedKeywordsFields": [ |
| 83 | + { |
| 84 | + "fieldName": "Category" |
| 85 | + }, |
| 86 | + { |
| 87 | + "fieldName": "Tags" |
| 88 | + } |
| 89 | + ] |
| 90 | + } |
| 91 | + } |
| 92 | + ] |
| 93 | + }, |
| 94 | + ``` |
| 95 | + |
| 96 | +1. **Save** your changes. |
| 97 | + |
| 98 | +1. Run the following query to test your index: `hotels near the ocean with beach access and good views`. |
| 99 | + |
| 100 | + Output should look similar to the following partial example, trimmed here for brevity. The presence of `@search.rerankerScore` and `@search.captions` tells you that the semantic ranker is working. |
| 101 | + |
| 102 | + ```json |
| 103 | + "@search.score": 5.600783, |
| 104 | + "@search.rerankerScore": 2.4191176891326904, |
| 105 | + "@search.captions": [ |
| 106 | + { |
| 107 | + "text": "Ocean Air Motel. Budget. pool\r\nair conditioning\r\nbar. Oceanfront hotel overlooking the beach features rooms with a private balcony and 2 indoor and outdoor pools. Various shops and art entertainment are on the boardwalk, just steps away..", |
| 108 | + "highlights": "Ocean Air Motel. Budget.<em> pool\r\nair conditioning\r\nbar. O</em>ceanfront hotel overlooking the beach features rooms with a private balcony and 2 indoor and outdoor pools. Various shops and art entertainment are on the boardwalk, just steps away.." |
| 109 | + } |
| 110 | + ], |
| 111 | + "HotelId": "41", |
| 112 | + "HotelName": "Ocean Air Motel", |
| 113 | + "Description": "Oceanfront hotel overlooking the beach features rooms with a private balcony and 2 indoor and outdoor pools. Various shops and art entertainment are on the boardwalk, just steps away.", |
| 114 | + ``` |
| 115 | + |
| 116 | +## Get service endpoints |
| 117 | + |
| 118 | +1. Sign in to the [Azure portal](https://portal.azure.com). |
| 119 | + |
| 120 | +1. [Find your search service](https://portal.azure.com/#blade/HubsExtension/BrowseResourceBlade/resourceType/Microsoft.Search%2FsearchServices). |
| 121 | + |
| 122 | +1. On the **Overview** home page, copy the URL. An example endpoint might look like `https://mydemo.search.windows.net`. |
| 123 | + |
| 124 | + :::image type="content" source="media/search-get-started-rest/get-endpoint.png" lightbox="media/search-get-started-rest/get-endpoint.png" alt-text="Screenshot of the URL property on the overview page."::: |
| 125 | + |
| 126 | +1. [Find your Azure OpenAI service](https://portal.azure.com/#blade/HubsExtension/BrowseResourceBlade/resourceType/Microsoft.CognitiveServices%2Faccounts). |
| 127 | + |
| 128 | +1. On the **Overview** home page, select the link to view the endpoints. Copy the URL. |
| 129 | + |
| 130 | +## Set up the query and chat thread |
| 131 | + |
| 132 | +This section uses Visual Studio Code and Python to call the chat APIs on Azure OpenAI. |
| 133 | + |
| 134 | +1. Install the following Python packages. |
| 135 | + |
| 136 | + ```python |
| 137 | + ! pip install azure-search-documents==11.6.0b4 --quiet |
| 138 | + ! pip install azure-identity==1.16.0 --quiet |
| 139 | + ! pip install openai --quiet |
| 140 | + ``` |
| 141 | + |
| 142 | +1. Set the following variables, substituting placeholders with valid values. |
| 143 | + |
| 144 | + ```python |
| 145 | + AZURE_SEARCH_SERVICE: str = "PUT YOUR SEARCH SERVICE ENDPOINT HERE" |
| 146 | + AZURE_OPENAI_ACCOUNT: str = "PUT YOUR AZURE OPENAI ENDPOINT HERE" |
| 147 | + AZURE_DEPLOYMENT_MODEL: str = "gpt-35-turbo" |
| 148 | + ``` |
| 149 | + |
| 150 | +1. Specify query parameters. The query is a keyword search using semantic ranking. The search engine returns up to 50 matches, but the model returns just the top 5 in the response. |
| 151 | + |
| 152 | + ```python |
| 153 | + # Set query parameters for grounding the conversation on your search index |
| 154 | + k=50 |
| 155 | + search_type="text" |
| 156 | + use_semantic_reranker=True |
| 157 | + sources_to_include=5 |
| 158 | + ``` |
| 159 | + |
| 160 | +1. Set up clients, functions, prompts, and chat thread. The function retrieves selected fields from the search index. |
| 161 | + |
| 162 | + ```python |
| 163 | + # Set up the query for generating responses |
| 164 | + from azure.core.credentials_async import AsyncTokenCredential |
| 165 | + from azure.identity.aio import get_bearer_token_provider |
| 166 | + from azure.search.documents.aio import SearchClient |
| 167 | + from azure.search.documents.models import VectorizableTextQuery, HybridSearch |
| 168 | + from openai import AsyncAzureOpenAI |
| 169 | + from enum import Enum |
| 170 | + from typing import List, Optional |
| 171 | + |
| 172 | + def create_openai_client(credential: AsyncTokenCredential) -> AsyncAzureOpenAI: |
| 173 | + token_provider = get_bearer_token_provider(credential, "https://cognitiveservices.azure.com/.default") |
| 174 | + return AsyncAzureOpenAI( |
| 175 | + api_version="2024-04-01-preview", |
| 176 | + azure_endpoint=AZURE_OPENAI_ACCOUNT, |
| 177 | + azure_ad_token_provider=token_provider |
| 178 | + ) |
| 179 | + |
| 180 | + def create_search_client(credential: AsyncTokenCredential) -> SearchClient: |
| 181 | + return SearchClient( |
| 182 | + endpoint=AZURE_SEARCH_SERVICE, |
| 183 | + index_name="hotels-sample-index", |
| 184 | + credential=credential |
| 185 | + ) |
| 186 | + |
| 187 | + # This quickstart is only using text at the moment |
| 188 | + class SearchType(Enum): |
| 189 | + TEXT = "text" |
| 190 | + VECTOR = "vector" |
| 191 | + HYBRID = "hybrid" |
| 192 | + |
| 193 | + # This function retrieves the selected fields from the search index |
| 194 | + async def get_sources(search_client: SearchClient, query: str, search_type: SearchType, use_semantic_reranker: bool = True, sources_to_include: int = 5, k: int = 50) -> List[str]: |
| 195 | + search_type == SearchType.TEXT, |
| 196 | + response = await search_client.search( |
| 197 | + search_text=query, |
| 198 | + query_type="semantic" if use_semantic_reranker else "simple", |
| 199 | + top=sources_to_include, |
| 200 | + select="Description,HotelName,Tags" |
| 201 | + ) |
| 202 | + |
| 203 | + return [ document async for document in response ] |
| 204 | + |
| 205 | + GROUNDED_PROMPT=""" |
| 206 | + You are a friendly assistant that recommends hotels based on activities and amenities. |
| 207 | + Answer the query using only the sources provided below in a friendly and concise bulleted manner. |
| 208 | + Answer ONLY with the facts listed in the list of sources below. |
| 209 | + If there isn't enough information below, say you don't know. |
| 210 | + Do not generate answers that don't use the sources below. |
| 211 | + Query: {query} |
| 212 | + Sources:\n{sources} |
| 213 | + """ |
| 214 | + class ChatThread: |
| 215 | + def __init__(self): |
| 216 | + self.messages = [] |
| 217 | + self.search_results = [] |
| 218 | + |
| 219 | + def append_message(self, role: str, message: str): |
| 220 | + self.messages.append({ |
| 221 | + "role": role, |
| 222 | + "content": message |
| 223 | + }) |
| 224 | + |
| 225 | + async def append_grounded_message(self, search_client: SearchClient, query: str, search_type: SearchType, use_semantic_reranker: bool = True, sources_to_include: int = 5, k: int = 50): |
| 226 | + sources = await get_sources(search_client, query, search_type, use_semantic_reranker, sources_to_include, k) |
| 227 | + sources_formatted = "\n".join([f'{document["HotelName"]}:{document["Description"]}:{document["Tags"]}' for document in sources]) |
| 228 | + self.append_message(role="user", message=GROUNDED_PROMPT.format(query=query, sources=sources_formatted)) |
| 229 | + self.search_results.append( |
| 230 | + { |
| 231 | + "message_index": len(self.messages) - 1, |
| 232 | + "query": query, |
| 233 | + "sources": sources |
| 234 | + } |
| 235 | + ) |
| 236 | + |
| 237 | + async def get_openai_response(self, openai_client: AsyncAzureOpenAI, model: str): |
| 238 | + response = await openai_client.chat.completions.create( |
| 239 | + messages=self.messages, |
| 240 | + model=model |
| 241 | + ) |
| 242 | + self.append_message(role="assistant", message=response.choices[0].message.content) |
| 243 | + |
| 244 | + def get_last_message(self) -> Optional[object]: |
| 245 | + return self.messages[-1] if len(self.messages) > 0 else None |
| 246 | + |
| 247 | + def get_last_message_sources(self) -> Optional[List[object]]: |
| 248 | + return self.search_results[-1]["sources"] if len(self.search_results) > 0 else None |
| 249 | + ``` |
| 250 | + |
| 251 | +1. Invoke the chat thread and call the query function, passing in a query string to search for. |
| 252 | + |
| 253 | + ```python |
| 254 | + import azure.identity.aio |
| 255 | + |
| 256 | + chat_thread = ChatThread() |
| 257 | + chat_deployment = AZURE_DEPLOYMENT_MODEL |
| 258 | + |
| 259 | + async with azure.identity.aio.DefaultAzureCredential() as credential, create_search_client(credential) as search_client, create_openai_client(credential) as openai_client: |
| 260 | + await chat_thread.append_grounded_message( |
| 261 | + search_client=search_client, |
| 262 | + query="Can you recommend a few hotels near the ocean with beach access and good views", |
| 263 | + search_type=SearchType(search_type), |
| 264 | + use_semantic_reranker=use_semantic_reranker, |
| 265 | + sources_to_include=sources_to_include, |
| 266 | + k=k) |
| 267 | + await chat_thread.get_openai_response(openai_client=openai_client, model=chat_deployment) |
| 268 | + |
| 269 | + print(chat_thread.get_last_message()["content"]) |
| 270 | + ``` |
| 271 | + |
| 272 | + Output might look similar to the following example: |
| 273 | + |
| 274 | + ```bash |
| 275 | + Based on your request, here are a few hotel recommendations with beach access and good views: |
| 276 | + |
| 277 | + 1. Ocean Air Motel - oceanfront hotel with a private balcony and indoor and outdoor pools |
| 278 | + 2. Marquis Plaza & Suites - offers a view, free Wi-Fi, and a pool |
| 279 | + 3. Pull'r Inn Motel - offers a view, a pool, and free Wi-Fi |
| 280 | + |
| 281 | + I hope this helps! Let me know if you need any further assistance. |
| 282 | + ``` |
| 283 | + |
| 284 | +## Clean up |
| 285 | + |
| 286 | +When you're working in your own subscription, it's a good idea at the end of a project to identify whether you still need the resources you created. Resources left running can cost you money. You can delete resources individually or delete the resource group to delete the entire set of resources. |
| 287 | + |
| 288 | +You can find and manage resources in the portal by using the **All resources** or **Resource groups** link in the leftmost pane. |
| 289 | + |
| 290 | +## Next steps |
| 291 | + |
| 292 | +As a next step, we recommend that you review the demo code for [Python](https://github.com/Azure/azure-search-vector-samples/tree/main/demo-python), [C#](https://github.com/Azure/azure-search-vector-samples/tree/main/demo-dotnet), or [JavaScript](https://github.com/Azure/azure-search-vector-samples/tree/main/demo-javascript). |
0 commit comments