From 6b1f29fc51f835ec99d5a406d2cd67177cfbff1e Mon Sep 17 00:00:00 2001 From: Jelle Druyts Date: Tue, 28 Nov 2023 14:28:26 +0100 Subject: [PATCH] Add support for integrated vectorization --- README.md | 26 +- azuredeploy.json | 10 + src/Azure.AISearch.WebApp/AppSettings.cs | 4 +- src/Azure.AISearch.WebApp/Constants.cs | 5 +- .../Models/AppSettingsOverride.cs | 4 +- .../Models/DocumentChunk.cs | 3 - .../Models/SearchRequest.cs | 1 + .../Models/SearchResult.cs | 1 - src/Azure.AISearch.WebApp/Pages/Index.cshtml | 42 ++- src/Azure.AISearch.WebApp/Pages/Manage.cshtml | 23 +- .../Pages/Shared/_SearchResponse.cshtml | 4 - ...zureCognitiveSearchConfigurationService.cs | 312 +++++++++++------- .../Services/AzureCognitiveSearchService.cs | 29 +- .../Services/SearchScenarioProvider.cs | 13 +- 14 files changed, 312 insertions(+), 165 deletions(-) diff --git a/README.md b/README.md index ae29419..8d7a41a 100644 --- a/README.md +++ b/README.md @@ -38,22 +38,24 @@ graph TD acs[Azure AI Search] aoai[Azure OpenAI] webapp[Web App] - functionapp[Function App] + functionapp[Function Apps] storage[Storage Account] - webapp -->|Generate query embeddings for vector search| aoai + webapp -->|Generate query embeddings for vector search (for external vectorization)| aoai webapp -->|Send chat requests| aoai webapp -->|Send search requests| acs webapp -->|Upload new documents| storage - functionapp -->|Generate embeddings for chunks| aoai + functionapp -->|Generate embeddings for chunks (for external vectorization)| aoai + functionapp -->|Push chunks into search index (for push model)| acs + acs -->|Generate embeddings for chunks and search queries (for integrated vectorization)| aoai acs -->|Populate search index from documents| storage - acs -->|Generate chunks and embeddings to index| functionapp + acs -->|Generate chunks and embeddings to index (for external vectorization)| functionapp aoai -->|Find relevant context to build prompt for Azure OpenAI on your data| acs ``` When you deploy the solution, it creates an [Azure AI Search](https://learn.microsoft.com/azure/search/search-what-is-azure-search) service which indexes document content from a blob storage container. (Note that documents are assumed to be in English.) -The documents in the index are also chunked into smaller pieces, and vector embeddings are created for these chunks using a Function App based on the [Azure OpenAI Embeddings Generator power skill](https://github.com/Azure-Samples/azure-search-power-skills/tree/main/Vector/EmbeddingGenerator). This allows you to easily try out [vector and hybrid search](https://learn.microsoft.com/azure/search/vector-search-overview). With Azure AI Search on its own, the responses *always* come directly from the source data, rather than being generated by an AI model. You can optionally use [semantic ranking](https://learn.microsoft.com/azure/search/semantic-search-overview) which *does* use AI, not to generate content but to increase the relevancy of the results and provide semantic answers and captions. +The documents in the index are also chunked into smaller pieces, and vector embeddings are created for these chunks using either [integrated vectorization](https://learn.microsoft.com/azure/search/vector-search-integrated-vectorization), or external vectorization using a Function App. This allows you to easily try out [vector and hybrid search](https://learn.microsoft.com/azure/search/vector-search-overview). With Azure AI Search on its own, the responses *always* come directly from the source data, rather than being generated by an AI model. You can optionally use [semantic ranking](https://learn.microsoft.com/azure/search/semantic-search-overview) which *does* use AI, not to generate content but to increase the relevancy of the results and provide semantic answers and captions. The solution also deploys an [Azure OpenAI](https://learn.microsoft.com/azure/ai-services/openai/overview) service. It provides an embeddings model to generate the vector representations of the document chunks and search queries, and a GPT model to generate answers to your search queries. If you choose the option to use [Azure OpenAI "on your data"](https://learn.microsoft.com/azure/ai-services/openai/concepts/use-your-data), these AI-generated responses can be grounded in (and even limited to) the information in your Azure AI Search indexes. This option allows you to let Azure OpenAI orchestrate the [Retrieval Augmented Generation (RAG)](https://aka.ms/what-is-rag) pattern. This means your search query will first be used to retrieve the most relevant documents (or preferably *smaller chunks of those documents*) from your private data source. Those search results are then used as context in the prompt that gets sent to the AI model, along with the original search query. This allows the AI model to generate a response based on the most relevant source data, rather than the public data that was used to train the model. Next to letting Azure OpenAI orchestrate the RAG pattern, the web application can also use [Semantic Kernel](https://learn.microsoft.com/semantic-kernel/overview/) to perform that orchestration, using a prompt and other parameters you can control yourself. @@ -109,9 +111,9 @@ This can easily be done by setting up the built-in [authentication and authoriza ## Configuration -The ARM template deploys the services and sets the configuration settings for the Web App and Function App. Most of these shouldn't be changed as they contain connection settings between the various services, but you can change the settings below for the App Service Web App. +The ARM template deploys the services and sets the configuration settings for the Web App and Function Apps. Most of these shouldn't be changed as they contain connection settings between the various services, but you can change the settings below for the App Service Web App. -> Note that the settings of the Function App shouldn't be changed, as the [power skill](https://github.com/Azure-Samples/azure-search-power-skills/tree/main/Vector/EmbeddingGenerator) was tweaked for this project to take any relevant settings from the request sent by the Azure AI Search skillset instead of from configuration (for example, the embedding model and chunk size to use). +> Note that the settings of the Function Apps shouldn't be changed, as the [power skill](https://github.com/Azure-Samples/azure-search-power-skills/tree/main/Vector/EmbeddingGenerator) was tweaked for this project to take any relevant settings from the request sent by the Azure AI Search skillset instead of from configuration (for example, the embedding model and chunk size to use). | Setting | Purpose | Default value | | ------- | ------- | ------------- | @@ -121,12 +123,14 @@ The ARM template deploys the services and sets the configuration settings for th | `OpenAIGptDeployment` | The deployment name of the [Azure OpenAI GPT model](https://learn.microsoft.com/azure/ai-services/openai/concepts/models) to use | `gpt-35-turbo` | | `StorageContainerNameBlobDocuments`* | The name of the storage container that contains the documents | `blob-documents` | | `StorageContainerNameBlobChunks`* | The name of the storage container that contains the document chunks | `blob-chunks` | -| `TextEmbedderNumTokens` | The number of tokens per chunk when splitting documents into smaller pieces | `2048` | -| `TextEmbedderTokenOverlap` | The number of tokens to overlap between consecutive chunks | `0` | -| `TextEmbedderMinChunkSize` | The minimum number of tokens of a chunk (smaller chunks are excluded) | `10` | +| `TextChunkerPageLength` | In case of integrated vectorization, the number of characters per page (chunk) when splitting documents into smaller pieces | `2000` | +| `TextChunkerPageOverlap` | In case of integrated vectorization, the number of characters to overlap between consecutive pages (chunks) | `500` | +| `TextEmbedderNumTokens` | In case of external vectorization, the number of tokens per chunk when splitting documents into smaller pieces | `2048` | +| `TextEmbedderTokenOverlap` | In case of external vectorization, the number of tokens to overlap between consecutive chunks | `0` | +| `TextEmbedderMinChunkSize` | In case of external vectorization, the minimum number of tokens of a chunk (smaller chunks are excluded) | `10` | | `SearchIndexNameBlobDocuments`* | The name of the search index that contains the documents | `blob-documents` | | `SearchIndexNameBlobChunks`* | The name of the search index that contains the document chunks | `blob-chunks` | -| `SearchIndexerSkillType`* | The type of chunking and embedding skill to use as part of the documents indexer: `pull` uses a [knowledge store](https://learn.microsoft.com/azure/search/knowledge-store-concept-intro) to store the chunk data in blobs and a separate indexer to pull these into the document chunks index; `push` directly uploads the data from the custom skill into the document chunks index | `pull` | +| `SearchIndexerSkillType`* | The type of chunking and embedding skill to use as part of the documents indexer: `integrated` uses [integrated vectorization](https://learn.microsoft.com/azure/search/vector-search-integrated-vectorization); `pull` uses a custom skill with a [knowledge store](https://learn.microsoft.com/azure/search/knowledge-store-concept-intro) to store the chunk data in blobs and a separate indexer to pull these into the document chunks index; `push` directly uploads the data from a custom skill into the document chunks index | `integrated` | | `SearchIndexerScheduleMinutes`* | The number of minutes between indexer executions in Azure AI Search | `5` | | `InitialDocumentUrls` | A space-separated list of URLs for the documents to include by default | A [resiliency](https://azure.microsoft.com/mediahandler/files/resourcefiles/resilience-in-azure-whitepaper/Resiliency-whitepaper.pdf) and [compliance](https://azure.microsoft.com/mediahandler/files/resourcefiles/data-residency-data-sovereignty-and-compliance-in-the-microsoft-cloud/Data_Residency_Data_Sovereignty_Compliance_Microsoft_Cloud.pdf) document | | `DefaultSystemRoleInformation` | The default instructions for the AI model | "You are an AI assistant that helps people find information." | diff --git a/azuredeploy.json b/azuredeploy.json index e0addd6..13ee617 100644 --- a/azuredeploy.json +++ b/azuredeploy.json @@ -61,6 +61,8 @@ "openaiApiVersion": "2023-06-01-preview", "storageContainerNameBlobDocuments": "blob-documents", "storageContainerNameBlobChunks": "blob-chunks", + "textChunkerPageLength": 2000, + "textChunkerPageOverlap": 500, "textEmbedderNumTokens": 2048, "textEmbedderTokenOverlap": 0, "textEmbedderMinChunkSize": 10, @@ -534,6 +536,14 @@ "type": "string", "value": "[variables('functionApiKey')]" }, + "textChunkerPageLength": { + "type": "string", + "value": "[variables('textChunkerPageLength')]" + }, + "textChunkerPageOverlap": { + "type": "string", + "value": "[variables('textChunkerPageOverlap')]" + }, "textEmbedderNumTokens": { "type": "int", "value": "[variables('textEmbedderNumTokens')]" diff --git a/src/Azure.AISearch.WebApp/AppSettings.cs b/src/Azure.AISearch.WebApp/AppSettings.cs index b77db8f..2cd8fa3 100644 --- a/src/Azure.AISearch.WebApp/AppSettings.cs +++ b/src/Azure.AISearch.WebApp/AppSettings.cs @@ -14,6 +14,8 @@ public class AppSettings public string? TextEmbedderFunctionEndpointPython { get; set; } public string? TextEmbedderFunctionEndpointDotNet { get; set; } public string? TextEmbedderFunctionApiKey { get; set; } + public int? TextChunkerPageLength { get; set; } // If unspecified, will use 2000 characters per page. + public int? TextChunkerPageOverlap { get; set; } // If unspecified, will use 500 characters overlap. public int? TextEmbedderNumTokens { get; set; } // If unspecified, will use the default as configured in the text embedder Function App. public int? TextEmbedderTokenOverlap { get; set; } // If unspecified, will use the default as configured in the text embedder Function App. public int? TextEmbedderMinChunkSize { get; set; } // If unspecified, will use the default as configured in the text embedder Function App. @@ -22,7 +24,7 @@ public class AppSettings public string? SearchServiceSku { get; set; } public string? SearchIndexNameBlobDocuments { get; set; } public string? SearchIndexNameBlobChunks { get; set; } - public string? SearchIndexerSkillType { get; set; } // If unspecified, will use the "pull" model. + public string? SearchIndexerSkillType { get; set; } // If unspecified, will use the "integrated" model. public int? SearchIndexerScheduleMinutes { get; set; } // If unspecified, will be set to 5 minutes. public string? InitialDocumentUrls { get; set; } public string? DefaultSystemRoleInformation { get; set; } diff --git a/src/Azure.AISearch.WebApp/Constants.cs b/src/Azure.AISearch.WebApp/Constants.cs index dab04b3..39b62eb 100644 --- a/src/Azure.AISearch.WebApp/Constants.cs +++ b/src/Azure.AISearch.WebApp/Constants.cs @@ -7,11 +7,14 @@ public static class Constants public static class ConfigurationNames { public const string SemanticConfigurationNameDefault = "default"; - public const string VectorSearchConfigurationNameDefault = "default"; + public const string VectorSearchProfileNameDefault = "default-profile"; + public const string VectorSearchAlgorithNameDefault = "default-algorithm"; + public const string VectorSearchVectorizerNameDefault = "default-vectorizer"; } public static class SearchIndexerSkillTypes { + public const string Integrated = "integrated"; public const string Pull = "pull"; public const string Push = "push"; } diff --git a/src/Azure.AISearch.WebApp/Models/AppSettingsOverride.cs b/src/Azure.AISearch.WebApp/Models/AppSettingsOverride.cs index fc3cea3..c24c8d3 100644 --- a/src/Azure.AISearch.WebApp/Models/AppSettingsOverride.cs +++ b/src/Azure.AISearch.WebApp/Models/AppSettingsOverride.cs @@ -4,9 +4,11 @@ namespace Azure.AISearch.WebApp.Models; // as they are not used anywhere else and don't depend on other settings. public class AppSettingsOverride { + public int? TextChunkerPageLength { get; set; } // If unspecified, will use 2000 characters per page. + public int? TextChunkerPageOverlap { get; set; } // If unspecified, will use 500 characters overlap. public int? TextEmbedderNumTokens { get; set; } // If unspecified, will use the default as configured in the text embedder Function App. public int? TextEmbedderTokenOverlap { get; set; } // If unspecified, will use the default as configured in the text embedder Function App. public int? TextEmbedderMinChunkSize { get; set; } // If unspecified, will use the default as configured in the text embedder Function App. - public string? SearchIndexerSkillType { get; set; } // If unspecified, will use the "pull" model. + public string? SearchIndexerSkillType { get; set; } // If unspecified, will use the "integrated" model. public int? SearchIndexerScheduleMinutes { get; set; } // If unspecified, will be set to 5 minutes. } \ No newline at end of file diff --git a/src/Azure.AISearch.WebApp/Models/DocumentChunk.cs b/src/Azure.AISearch.WebApp/Models/DocumentChunk.cs index 8c65fe7..1622c11 100644 --- a/src/Azure.AISearch.WebApp/Models/DocumentChunk.cs +++ b/src/Azure.AISearch.WebApp/Models/DocumentChunk.cs @@ -3,9 +3,6 @@ namespace Azure.AISearch.WebApp.Models; public class DocumentChunk { public string? Id { get; set; } - public long ChunkIndex { get; set; } - public long ChunkOffset { get; set; } - public long ChunkLength { get; set; } public string? Content { get; set; } public IReadOnlyList? ContentVector { get; set; } public string? SourceDocumentId { get; set; } diff --git a/src/Azure.AISearch.WebApp/Models/SearchRequest.cs b/src/Azure.AISearch.WebApp/Models/SearchRequest.cs index 8d9b07d..33c9a3b 100644 --- a/src/Azure.AISearch.WebApp/Models/SearchRequest.cs +++ b/src/Azure.AISearch.WebApp/Models/SearchRequest.cs @@ -10,6 +10,7 @@ public class SearchRequest public QuerySyntax QuerySyntax { get; set; } = QuerySyntax.Simple; public DataSourceType DataSource { get; set; } = DataSourceType.None; public string? OpenAIGptDeployment { get; set; } + public bool UseIntegratedVectorization { get; set; } public int? VectorNearestNeighborsCount { get; set; } = Constants.Defaults.VectorNearestNeighborsCount; public bool LimitToDataSource { get; set; } = true; // "Limit responses to your data content" public string? SystemRoleInformation { get; set; } // Give the model instructions about how it should behave and any context it should reference when generating a response. You can describe the assistant’s personality, tell it what it should and shouldn’t answer, and tell it how to format responses. There’s no token limit for this section, but it will be included with every API call, so it counts against the overall token limit. diff --git a/src/Azure.AISearch.WebApp/Models/SearchResult.cs b/src/Azure.AISearch.WebApp/Models/SearchResult.cs index aa93214..e353192 100644 --- a/src/Azure.AISearch.WebApp/Models/SearchResult.cs +++ b/src/Azure.AISearch.WebApp/Models/SearchResult.cs @@ -6,7 +6,6 @@ public class SearchResult public string? SearchIndexKey { get; set; } public string? DocumentId { get; set; } public string? DocumentTitle { get; set; } - public int? ChunkIndex { get; set; } public double? Score { get; set; } public IDictionary> Highlights { get; set; } = new Dictionary>(); public IList Captions { get; set; } = new List(); diff --git a/src/Azure.AISearch.WebApp/Pages/Index.cshtml b/src/Azure.AISearch.WebApp/Pages/Index.cshtml index 3d60063..dca4355 100644 --- a/src/Azure.AISearch.WebApp/Pages/Index.cshtml +++ b/src/Azure.AISearch.WebApp/Pages/Index.cshtml @@ -35,7 +35,7 @@ // Update the sequence diagram whenever relevant properties change. watch( - () => [settings.value.showExplanation, searchRequest.value.engine, searchRequest.value.searchIndex, searchRequest.value.queryType, searchRequest.value.dataSource], + () => [settings.value.showExplanation, searchRequest.value.engine, searchRequest.value.searchIndex, searchRequest.value.queryType, searchRequest.value.dataSource, searchRequest.value.useIntegratedVectorization], () => showSequenceDiagram() ); @@ -49,9 +49,26 @@ }); }; - const getAzureCognitiveSearchSequenceDiagram = function (sourceParticipant, queryType, searchIndex) { + const getAzureCognitiveSearchSequenceDiagram = function (sourceParticipant, queryType, searchIndex, useIntegratedVectorization, createAzureOpenAIParticipant) { var mermaidDiagram = ''; mermaidDiagram += 'create participant ACS as Azure AI Search\n'; + + var mermaidDiagramIntegratedVectorization = ''; + if (useIntegratedVectorization) { + // In case of integrated vectorization, the vector is generated by Azure AI Search first. + if (createAzureOpenAIParticipant) { + mermaidDiagramIntegratedVectorization += 'create participant AOAI as Azure OpenAI\n'; + } + mermaidDiagramIntegratedVectorization += 'ACS->>AOAI: Generate vector for search query\n'; + mermaidDiagramIntegratedVectorization += getAzureOpenAIVectorSequenceDiagram(); + if (createAzureOpenAIParticipant) { + mermaidDiagramIntegratedVectorization += 'destroy AOAI\n'; + mermaidDiagramIntegratedVectorization += 'AOAI-xACS: Vector for search query\n'; + } else { + mermaidDiagramIntegratedVectorization += 'AOAI->>ACS: Vector for search query\n'; + } + } + if (queryType == '@QueryType.TextStandard' || queryType == '@QueryType.TextSemantic') { // Pure keyword search. mermaidDiagram += `${sourceParticipant}->>ACS: Search using keywords\n`; @@ -63,6 +80,7 @@ else if (queryType == '@QueryType.Vector') { // Pure vector search. mermaidDiagram += `${sourceParticipant}->>ACS: Search using vector\n`; + mermaidDiagram += mermaidDiagramIntegratedVectorization; mermaidDiagram += `create participant Index as ${searchIndex} Index\n`; mermaidDiagram += `ACS->>Index: Search using vector\n`; mermaidDiagram += 'destroy Index\n'; @@ -71,6 +89,7 @@ else if (queryType == '@QueryType.HybridStandard' || queryType == '@QueryType.HybridSemantic') { // Hybrid search. mermaidDiagram += `${sourceParticipant}->>ACS: Search using vector and keywords\n`; + mermaidDiagram += mermaidDiagramIntegratedVectorization; mermaidDiagram += `create participant Index as ${searchIndex} Index\n`; mermaidDiagram += `ACS->>Index: Search using vector\n`; mermaidDiagram += 'Index->>ACS: Vector search results\n'; @@ -106,6 +125,7 @@ var searchIndex = searchRequest.value.searchIndex; var dataSource = searchRequest.value.dataSource; var queryType = searchRequest.value.queryType; + var useIntegratedVectorization = searchRequest.value.useIntegratedVectorization; var needsVector = (queryType == '@QueryType.Vector' || queryType == '@QueryType.HybridStandard' || queryType == '@QueryType.HybridSemantic'); var mermaidDiagram = 'sequenceDiagram\n'; @@ -114,14 +134,14 @@ if (engine == '@EngineType.AzureCognitiveSearch') { // Azure AI Search. mermaidDiagram += 'User->>App: Search query\n'; - if (needsVector) { + if (needsVector && !useIntegratedVectorization) { mermaidDiagram += 'create participant AOAI as Azure OpenAI\n'; mermaidDiagram += 'App->>AOAI: Generate vector for search query\n'; mermaidDiagram += getAzureOpenAIVectorSequenceDiagram(); mermaidDiagram += 'destroy AOAI\n'; mermaidDiagram += 'AOAI-xApp: Vector for search query\n'; } - mermaidDiagram += getAzureCognitiveSearchSequenceDiagram('App', queryType, searchIndex); + mermaidDiagram += getAzureCognitiveSearchSequenceDiagram('App', queryType, searchIndex, useIntegratedVectorization, true); mermaidDiagram += 'App->>User: Search results\n'; } else if (engine == '@EngineType.AzureOpenAI') { // Azure OpenAI. @@ -133,7 +153,7 @@ if (needsVector) { mermaidDiagram += getAzureOpenAIVectorSequenceDiagram(); } - mermaidDiagram += getAzureCognitiveSearchSequenceDiagram('AOAI', queryType, searchIndex); + mermaidDiagram += getAzureCognitiveSearchSequenceDiagram('AOAI', queryType, searchIndex, false, false); mermaidDiagram += 'AOAI->>AOAI: Build a prompt using the search results to ground the model\n'; } // Chat completion. @@ -149,12 +169,12 @@ // Custom orchestration. mermaidDiagram += 'User->>App: Search query\n'; mermaidDiagram += 'create participant AOAI as Azure OpenAI\n'; - if (needsVector) { + if (needsVector && !useIntegratedVectorization) { mermaidDiagram += 'App->>AOAI: Generate vector for search query\n'; mermaidDiagram += getAzureOpenAIVectorSequenceDiagram(); mermaidDiagram += 'AOAI->>App: Vector for search query\n'; } - mermaidDiagram += getAzureCognitiveSearchSequenceDiagram('App', queryType, searchIndex); + mermaidDiagram += getAzureCognitiveSearchSequenceDiagram('App', queryType, searchIndex, useIntegratedVectorization, false); mermaidDiagram += 'App->>App: Build a prompt using the search results to ground the model\n'; mermaidDiagram += `App->>AOAI: Generate chat response\n`; mermaidDiagram += 'create participant GPT as GPT Model\n'; @@ -338,6 +358,14 @@ +
+
+ + + + +
+
diff --git a/src/Azure.AISearch.WebApp/Pages/Manage.cshtml b/src/Azure.AISearch.WebApp/Pages/Manage.cshtml index 75ee746..c69cb31 100644 --- a/src/Azure.AISearch.WebApp/Pages/Manage.cshtml +++ b/src/Azure.AISearch.WebApp/Pages/Manage.cshtml @@ -112,7 +112,12 @@
- + + + +
+
+
@@ -124,17 +129,27 @@
- + + + +
+
+ + + +
+
+
- +
- +
diff --git a/src/Azure.AISearch.WebApp/Pages/Shared/_SearchResponse.cshtml b/src/Azure.AISearch.WebApp/Pages/Shared/_SearchResponse.cshtml index cd8fb0e..1438acc 100644 --- a/src/Azure.AISearch.WebApp/Pages/Shared/_SearchResponse.cshtml +++ b/src/Azure.AISearch.WebApp/Pages/Shared/_SearchResponse.cshtml @@ -55,10 +55,6 @@
@searchResult.DocumentTitle - @if (searchResult.ChunkIndex != null) - { - (chunk #@searchResult.ChunkIndex) - }
@if (searchResult.Captions.Any()) { diff --git a/src/Azure.AISearch.WebApp/Services/AzureCognitiveSearchConfigurationService.cs b/src/Azure.AISearch.WebApp/Services/AzureCognitiveSearchConfigurationService.cs index cb686a4..60276dd 100644 --- a/src/Azure.AISearch.WebApp/Services/AzureCognitiveSearchConfigurationService.cs +++ b/src/Azure.AISearch.WebApp/Services/AzureCognitiveSearchConfigurationService.cs @@ -28,14 +28,14 @@ public async Task InitializeAsync(AppSettingsOverride? settingsOverride = null) ArgumentNullException.ThrowIfNull(this.settings.StorageContainerNameBlobDocuments); ArgumentNullException.ThrowIfNull(this.settings.StorageContainerNameBlobChunks); ArgumentNullException.ThrowIfNull(this.settings.SearchIndexNameBlobChunks); - if (!await SearchIndexExistsAsync(this.settings.SearchIndexNameBlobDocuments)) - { - await CreateDocumentsIndex(settingsOverride, this.settings.SearchIndexNameBlobDocuments, this.settings.StorageContainerNameBlobDocuments, this.settings.StorageContainerNameBlobChunks); - } if (!await SearchIndexExistsAsync(this.settings.SearchIndexNameBlobChunks)) { await CreateChunksIndex(settingsOverride, this.settings.SearchIndexNameBlobChunks, this.settings.StorageContainerNameBlobChunks); } + if (!await SearchIndexExistsAsync(this.settings.SearchIndexNameBlobDocuments)) + { + await CreateDocumentsIndex(settingsOverride, this.settings.SearchIndexNameBlobDocuments, this.settings.StorageContainerNameBlobDocuments, this.settings.StorageContainerNameBlobChunks); + } } public async Task UninitializeAsync() @@ -148,16 +148,16 @@ private async Task CreateDocumentsIndex(AppSettingsOverride? settingsOverride, s { Schedule = new IndexingSchedule(GetIndexingSchedule(settingsOverride)), FieldMappings = - { - // Map the full blob URL to the document ID, base64 encoded to ensure it has only valid characters for a document ID. - new FieldMapping("metadata_storage_path") { TargetFieldName = nameof(Document.Id), MappingFunction = new FieldMappingFunction("base64Encode") }, - // Map the file name to the document title. - new FieldMapping("metadata_storage_name") { TargetFieldName = nameof(Document.Title) }, - // Map the file content to the document content. - new FieldMapping("content") { TargetFieldName = nameof(Document.Content) }, - // Map the full blob URL as the document file path. - new FieldMapping("metadata_storage_path") { TargetFieldName = nameof(Document.FilePath) } - }, + { + // Map the full blob URL to the document ID, base64 encoded to ensure it has only valid characters for a document ID. + new FieldMapping("metadata_storage_path") { TargetFieldName = nameof(Document.Id), MappingFunction = new FieldMappingFunction("base64Encode") }, + // Map the file name to the document title. + new FieldMapping("metadata_storage_name") { TargetFieldName = nameof(Document.Title) }, + // Map the file content to the document content. + new FieldMapping("content") { TargetFieldName = nameof(Document.Content) }, + // Map the full blob URL as the document file path. + new FieldMapping("metadata_storage_path") { TargetFieldName = nameof(Document.FilePath) } + }, // Use the skillset for chunking and embedding. SkillsetName = skillset.Name }; @@ -198,120 +198,182 @@ private static SearchIndex GetDocumentsSearchIndex(string documentsIndexName) private SearchIndexerSkillset GetDocumentsSearchIndexerSkillset(AppSettingsOverride? settingsOverride, string indexName, string knowledgeStoreContainerName) { - var usePullModel = UsePullModel(settingsOverride); - var textEmbedderFunctionEndpoint = usePullModel ? this.settings.TextEmbedderFunctionEndpointPython : this.settings.TextEmbedderFunctionEndpointDotNet; - var skillset = new SearchIndexerSkillset(GetSkillsetName(indexName), Array.Empty()) + var searchIndexerSkillType = GetSearchIndexerSkillType(settingsOverride); + var skillset = new SearchIndexerSkillset(GetSkillsetName(indexName), Array.Empty()); + + if (string.Equals(searchIndexerSkillType, Constants.SearchIndexerSkillTypes.Integrated, StringComparison.InvariantCultureIgnoreCase)) { - Skills = + // Use integrated vectorization (no custom skills required). + ArgumentNullException.ThrowIfNull(this.settings.OpenAIEndpoint); + ArgumentNullException.ThrowIfNull(this.settings.SearchIndexNameBlobChunks); + var textChunkerPageLength = settingsOverride?.TextChunkerPageLength ?? this.settings.TextChunkerPageLength ?? 2000; + var textChunkerPageOverlap = settingsOverride?.TextChunkerPageOverlap ?? this.settings.TextChunkerPageOverlap ?? 500; + + skillset.Skills.Add(new SplitSkill(Array.Empty(), Array.Empty()) { - new WebApiSkill(Array.Empty(), Array.Empty(), textEmbedderFunctionEndpoint) + TextSplitMode = TextSplitMode.Pages, + MaximumPageLength = textChunkerPageLength, + PageOverlapLength = textChunkerPageOverlap, + MaximumPagesToTake = 0, // Don't limit the number of pages to take, split the entire source document. + DefaultLanguageCode = "en", + Context = "/document", + Inputs = { - Name = "chunking-embedding-skill", - Context = $"/document/{nameof(Document.Content)}", - HttpMethod = "POST", - Timeout = TimeSpan.FromMinutes(3), - BatchSize = 1, - DegreeOfParallelism = 1, - HttpHeaders = - { - { "Authorization", this.settings.TextEmbedderFunctionApiKey } - }, - Inputs = - { - // Pass the document ID. - new InputFieldMappingEntry("document_id") { Source = $"/document/{nameof(Document.Id)}" }, - // Pass the document content as the text to chunk and created the embeddings for. - new InputFieldMappingEntry("text") { Source = $"/document/{nameof(Document.Content)}" }, - // Pass the document title. - new InputFieldMappingEntry("title") { Source = $"/document/{nameof(Document.Title)}" }, - // Pass the document file path. - new InputFieldMappingEntry("filepath") { Source = $"/document/{nameof(Document.FilePath)}" }, - // Pass the field name as a string literal. - new InputFieldMappingEntry("fieldname") { Source = $"='{nameof(Document.Content)}'" }, - // Pass the embedding deployment to use as a string literal. - new InputFieldMappingEntry("embedding_deployment_name") { Source = $"='{this.settings.OpenAIEmbeddingDeployment}'" } - }, - Outputs = + // Pass the document content as the text to chunk. + new InputFieldMappingEntry("text") { Source = $"/document/{nameof(Document.Content)}" }, + }, + Outputs = + { + // Store the chunks output under "/document/chunks". + new OutputFieldMappingEntry("textItems") { TargetName = "chunks" }, + } + }); + skillset.Skills.Add(new AzureOpenAIEmbeddingSkill(Array.Empty(), Array.Empty()) + { + ResourceUri = new Uri(this.settings.OpenAIEndpoint), + DeploymentId = this.settings.OpenAIEmbeddingDeployment, + ApiKey = this.settings.OpenAIApiKey, + Context = "/document/chunks/*", // Call the Azure OpenAI skill for each chunk individually. + Inputs = + { + // Pass the chunk text to Azure OpenAI to generate the embedding for it. + new InputFieldMappingEntry("text") { Source = $"/document/chunks/*" }, + }, + Outputs = + { + // Store the chunk's embedding under "/document/chunks/*/content_vector". + new OutputFieldMappingEntry("embedding") { TargetName = "content_vector" }, + } + }); + + skillset.IndexProjections = new SearchIndexerIndexProjections(Array.Empty()) + { + Parameters = new SearchIndexerIndexProjectionsParameters + { + // Project the chunks into their own "chunks" search index, while still indexing the full + // parent document into the main "documents" index. + ProjectionMode = IndexProjectionMode.IncludeIndexingParentDocuments + }, + Selectors = + { + new SearchIndexerIndexProjectionSelector(this.settings.SearchIndexNameBlobChunks, nameof(DocumentChunk.SourceDocumentId), "/document/chunks/*", Array.Empty()) { - // Store the chunks output under "/document/Content/chunks". - new OutputFieldMappingEntry("chunks") { TargetName = "chunks" } + Mappings = + { + new InputFieldMappingEntry(nameof(DocumentChunk.Content)) { Source = $"/document/chunks/*" }, + new InputFieldMappingEntry(nameof(DocumentChunk.ContentVector)) { Source = $"/document/chunks/*/content_vector" }, + new InputFieldMappingEntry(nameof(DocumentChunk.SourceDocumentTitle)) { Source = $"/document/metadata_storage_name" }, + new InputFieldMappingEntry(nameof(DocumentChunk.SourceDocumentFilePath)) { Source = $"/document/metadata_storage_path" } + } } } - } - }; - - if (usePullModel) + }; + } + else { - skillset.KnowledgeStore = new KnowledgeStore(this.settings.StorageAccountConnectionString, Array.Empty()) + // Use the push or pull-based model with a custom Web API skill. + var usePullModel = string.Equals(searchIndexerSkillType, Constants.SearchIndexerSkillTypes.Pull, StringComparison.InvariantCultureIgnoreCase); + var textEmbedderFunctionEndpoint = usePullModel ? this.settings.TextEmbedderFunctionEndpointPython : this.settings.TextEmbedderFunctionEndpointDotNet; + skillset.Skills.Add(new WebApiSkill(Array.Empty(), Array.Empty(), textEmbedderFunctionEndpoint) { - Projections = + Name = "chunking-embedding-skill", + Context = $"/document/{nameof(Document.Content)}", + HttpMethod = "POST", + Timeout = TimeSpan.FromMinutes(3), + BatchSize = 1, + DegreeOfParallelism = 1, + HttpHeaders = + { + { "Authorization", this.settings.TextEmbedderFunctionApiKey } + }, + Inputs = { - new KnowledgeStoreProjection + // Pass the document ID. + new InputFieldMappingEntry("document_id") { Source = $"/document/{nameof(Document.Id)}" }, + // Pass the document content as the text to chunk and created the embeddings for. + new InputFieldMappingEntry("text") { Source = $"/document/{nameof(Document.Content)}" }, + // Pass the document title. + new InputFieldMappingEntry("title") { Source = $"/document/{nameof(Document.Title)}" }, + // Pass the document file path. + new InputFieldMappingEntry("filepath") { Source = $"/document/{nameof(Document.FilePath)}" }, + // Pass the field name as a string literal. + new InputFieldMappingEntry("fieldname") { Source = $"='{nameof(Document.Content)}'" }, + // Pass the embedding deployment to use as a string literal. + new InputFieldMappingEntry("embedding_deployment_name") { Source = $"='{this.settings.OpenAIEmbeddingDeployment}'" } + }, + Outputs = + { + // Store the chunks output under "/document/Content/chunks". + new OutputFieldMappingEntry("chunks") { TargetName = "chunks" } + } + }); + + if (usePullModel) + { + skillset.KnowledgeStore = new KnowledgeStore(this.settings.StorageAccountConnectionString, Array.Empty()) + { + Projections = { - // Project the chunks to a knowledge store container, where each chunk will be its own JSON document that can be indexed later. - Objects = + new KnowledgeStoreProjection { - new KnowledgeStoreObjectProjectionSelector(knowledgeStoreContainerName) + // Project the chunks to a knowledge store container, where each chunk will be its own JSON document that can be indexed later. + Objects = { - GeneratedKeyName = nameof(DocumentChunk.Id), - // Iterate over each chunk in "/document/Content/chunks". - SourceContext = $"/document/{nameof(Document.Content)}/chunks/*", - Inputs = + new KnowledgeStoreObjectProjectionSelector(knowledgeStoreContainerName) { - // Map the document ID. - new InputFieldMappingEntry(nameof(DocumentChunk.SourceDocumentId)) { Source = $"/document/{nameof(Document.Id)}" }, - // Map the document file path. - new InputFieldMappingEntry(nameof(DocumentChunk.SourceDocumentFilePath)) { Source = $"/document/{nameof(Document.FilePath)}" }, - // Map the Content field name. - new InputFieldMappingEntry(nameof(DocumentChunk.SourceDocumentContentField)) { Source = $"/document/{nameof(Document.Content)}/chunks/*/embedding_metadata/fieldname" }, - // Map the document title. - new InputFieldMappingEntry(nameof(DocumentChunk.SourceDocumentTitle)) { Source = $"/document/{nameof(Document.Title)}" }, - // Map the chunked content. - new InputFieldMappingEntry(nameof(DocumentChunk.Content)) { Source = $"/document/{nameof(Document.Content)}/chunks/*/content" }, - // Map the embedding vector. - new InputFieldMappingEntry(nameof(DocumentChunk.ContentVector)) { Source = $"/document/{nameof(Document.Content)}/chunks/*/embedding_metadata/embedding" }, - // Map the chunk index. - new InputFieldMappingEntry(nameof(DocumentChunk.ChunkIndex)) { Source = $"/document/{nameof(Document.Content)}/chunks/*/embedding_metadata/index" }, - // Map the chunk offset. - new InputFieldMappingEntry(nameof(DocumentChunk.ChunkOffset)) { Source = $"/document/{nameof(Document.Content)}/chunks/*/embedding_metadata/offset" }, - // Map the chunk length. - new InputFieldMappingEntry(nameof(DocumentChunk.ChunkLength)) { Source = $"/document/{nameof(Document.Content)}/chunks/*/embedding_metadata/length" } + GeneratedKeyName = nameof(DocumentChunk.Id), + // Iterate over each chunk in "/document/Content/chunks". + SourceContext = $"/document/{nameof(Document.Content)}/chunks/*", + Inputs = + { + // Map the document ID. + new InputFieldMappingEntry(nameof(DocumentChunk.SourceDocumentId)) { Source = $"/document/{nameof(Document.Id)}" }, + // Map the document file path. + new InputFieldMappingEntry(nameof(DocumentChunk.SourceDocumentFilePath)) { Source = $"/document/{nameof(Document.FilePath)}" }, + // Map the document title. + new InputFieldMappingEntry(nameof(DocumentChunk.SourceDocumentTitle)) { Source = $"/document/{nameof(Document.Title)}" }, + // Map the chunked content. + new InputFieldMappingEntry(nameof(DocumentChunk.Content)) { Source = $"/document/{nameof(Document.Content)}/chunks/*/content" }, + // Map the embedding vector. + new InputFieldMappingEntry(nameof(DocumentChunk.ContentVector)) { Source = $"/document/{nameof(Document.Content)}/chunks/*/embedding_metadata/embedding" }, + } } } } } - } - }; - } + }; + } - // Configure any optional settings that can be overridden by the indexer rather than depending on the default - // values in the text embedder Function App. - var textEmbedderNumTokens = settingsOverride?.TextEmbedderNumTokens ?? this.settings.TextEmbedderNumTokens; - if (textEmbedderNumTokens != null) - { - skillset.Skills[0].Inputs.Add(new InputFieldMappingEntry("num_tokens") { Source = $"={textEmbedderNumTokens}" }); - } - var textEmbedderTokenOverlap = settingsOverride?.TextEmbedderTokenOverlap ?? this.settings.TextEmbedderTokenOverlap; - if (textEmbedderTokenOverlap != null) - { - skillset.Skills[0].Inputs.Add(new InputFieldMappingEntry("token_overlap") { Source = $"={textEmbedderTokenOverlap}" }); - } - var textEmbedderMinChunkSize = settingsOverride?.TextEmbedderMinChunkSize ?? this.settings.TextEmbedderMinChunkSize; - if (textEmbedderMinChunkSize != null) - { - skillset.Skills[0].Inputs.Add(new InputFieldMappingEntry("min_chunk_size") { Source = $"={textEmbedderMinChunkSize}" }); + // Configure any optional settings that can be overridden by the indexer rather than depending on the default + // values in the text embedder Function App. + var textEmbedderNumTokens = settingsOverride?.TextEmbedderNumTokens ?? this.settings.TextEmbedderNumTokens; + if (textEmbedderNumTokens != null) + { + skillset.Skills[0].Inputs.Add(new InputFieldMappingEntry("num_tokens") { Source = $"={textEmbedderNumTokens}" }); + } + var textEmbedderTokenOverlap = settingsOverride?.TextEmbedderTokenOverlap ?? this.settings.TextEmbedderTokenOverlap; + if (textEmbedderTokenOverlap != null) + { + skillset.Skills[0].Inputs.Add(new InputFieldMappingEntry("token_overlap") { Source = $"={textEmbedderTokenOverlap}" }); + } + var textEmbedderMinChunkSize = settingsOverride?.TextEmbedderMinChunkSize ?? this.settings.TextEmbedderMinChunkSize; + if (textEmbedderMinChunkSize != null) + { + skillset.Skills[0].Inputs.Add(new InputFieldMappingEntry("min_chunk_size") { Source = $"={textEmbedderMinChunkSize}" }); + } } + return skillset; } private async Task CreateChunksIndex(AppSettingsOverride? settingsOverride, string chunksIndexName, string chunksContainerName) { // Create the index which represents the chunked data from the main indexer's knowledge store. - var chunkSearchIndex = GetChunksSearchIndex(chunksIndexName); + var chunkSearchIndex = GetChunksSearchIndex(chunksIndexName, settingsOverride); await this.indexClient.CreateIndexAsync(chunkSearchIndex); - var usePullModel = UsePullModel(settingsOverride); - if (usePullModel) + var searchIndexerSkillType = GetSearchIndexerSkillType(settingsOverride); + if (string.Equals(searchIndexerSkillType, Constants.SearchIndexerSkillTypes.Pull, StringComparison.InvariantCultureIgnoreCase)) { // Create the Storage data source for the chunked data. var chunksDataSourceConnection = new SearchIndexerDataSourceConnection(GetDataSourceName(chunksIndexName), SearchIndexerDataSourceType.AzureBlob, this.settings.StorageAccountConnectionString, new SearchIndexerDataContainer(chunksContainerName)); @@ -333,20 +395,16 @@ private async Task CreateChunksIndex(AppSettingsOverride? settingsOverride, stri } } - private SearchIndex GetChunksSearchIndex(string chunkIndexName) + private SearchIndex GetChunksSearchIndex(string chunkIndexName, AppSettingsOverride? settingsOverride) { - return new SearchIndex(chunkIndexName) + var chunksSearchIndex = new SearchIndex(chunkIndexName) { Fields = { - new SearchField(nameof(DocumentChunk.Id), SearchFieldDataType.String) { IsKey = true, IsFilterable = true, IsSortable = true, IsFacetable = false, IsSearchable = false }, - new SearchField(nameof(DocumentChunk.ChunkIndex), SearchFieldDataType.Int64) { IsFilterable = true, IsSortable = true, IsFacetable = false, IsSearchable = false }, - new SearchField(nameof(DocumentChunk.ChunkOffset), SearchFieldDataType.Int64) { IsFilterable = true, IsSortable = true, IsFacetable = false, IsSearchable = false }, - new SearchField(nameof(DocumentChunk.ChunkLength), SearchFieldDataType.Int64) { IsFilterable = true, IsSortable = true, IsFacetable = false, IsSearchable = false }, + new SearchField(nameof(DocumentChunk.Id), SearchFieldDataType.String) { IsKey = true, IsFilterable = true, IsSortable = true, IsFacetable = false, IsSearchable = true, AnalyzerName = LexicalAnalyzerName.Keyword }, new SearchField(nameof(DocumentChunk.Content), SearchFieldDataType.String) { IsFilterable = false, IsSortable = false, IsFacetable = false, IsSearchable = true, AnalyzerName = LexicalAnalyzerName.EnMicrosoft }, - new SearchField(nameof(DocumentChunk.ContentVector), SearchFieldDataType.Collection(SearchFieldDataType.Single)) { IsFilterable = false, IsSortable = false, IsFacetable = false, IsSearchable = true, VectorSearchDimensions = this.settings.OpenAIEmbeddingVectorDimensions, VectorSearchProfile = Constants.ConfigurationNames.VectorSearchConfigurationNameDefault }, + new SearchField(nameof(DocumentChunk.ContentVector), SearchFieldDataType.Collection(SearchFieldDataType.Single)) { IsFilterable = false, IsSortable = false, IsFacetable = false, IsSearchable = true, VectorSearchDimensions = this.settings.OpenAIEmbeddingVectorDimensions, VectorSearchProfile = Constants.ConfigurationNames.VectorSearchProfileNameDefault }, new SearchField(nameof(DocumentChunk.SourceDocumentId), SearchFieldDataType.String) { IsFilterable = true, IsSortable = true, IsFacetable = false, IsSearchable = false }, - new SearchField(nameof(DocumentChunk.SourceDocumentContentField), SearchFieldDataType.String) { IsFilterable = true, IsSortable = true, IsFacetable = false, IsSearchable = false }, new SearchField(nameof(DocumentChunk.SourceDocumentTitle), SearchFieldDataType.String) { IsFilterable = true, IsSortable = true, IsFacetable = false, IsSearchable = true, AnalyzerName = LexicalAnalyzerName.EnMicrosoft }, new SearchField(nameof(DocumentChunk.SourceDocumentFilePath), SearchFieldDataType.String) { IsFilterable = true, IsSortable = true, IsFacetable = false, IsSearchable = true, AnalyzerName = LexicalAnalyzerName.StandardLucene } }, @@ -373,9 +431,13 @@ private SearchIndex GetChunksSearchIndex(string chunkIndexName) }, VectorSearch = new VectorSearch { + Profiles = + { + new VectorSearchProfile(Constants.ConfigurationNames.VectorSearchProfileNameDefault, Constants.ConfigurationNames.VectorSearchAlgorithNameDefault) + }, Algorithms = { - new HnswVectorSearchAlgorithmConfiguration(Constants.ConfigurationNames.VectorSearchConfigurationNameDefault) + new HnswVectorSearchAlgorithmConfiguration(Constants.ConfigurationNames.VectorSearchAlgorithNameDefault) { Parameters = new HnswParameters { @@ -388,6 +450,25 @@ private SearchIndex GetChunksSearchIndex(string chunkIndexName) } } }; + + var searchIndexerSkillType = GetSearchIndexerSkillType(settingsOverride); + if (string.Equals(searchIndexerSkillType, Constants.SearchIndexerSkillTypes.Integrated, StringComparison.InvariantCultureIgnoreCase)) + { + // For integrated vectorization, use the OpenAI vectorizer to generate the embeddings for the search query itself. + ArgumentNullException.ThrowIfNull(this.settings.OpenAIEndpoint); + chunksSearchIndex.VectorSearch.Vectorizers.Add(new AzureOpenAIVectorizer(Constants.ConfigurationNames.VectorSearchVectorizerNameDefault) + { + AzureOpenAIParameters = new AzureOpenAIParameters + { + ResourceUri = new Uri(this.settings.OpenAIEndpoint), + DeploymentId = this.settings.OpenAIEmbeddingDeployment, + ApiKey = this.settings.OpenAIApiKey + } + }); + chunksSearchIndex.VectorSearch.Profiles[0].Vectorizer = Constants.ConfigurationNames.VectorSearchVectorizerNameDefault; + } + + return chunksSearchIndex; } private TimeSpan GetIndexingSchedule(AppSettingsOverride? settingsOverride) @@ -397,6 +478,11 @@ private TimeSpan GetIndexingSchedule(AppSettingsOverride? settingsOverride) return TimeSpan.FromMinutes(minutes); } + private string GetSearchIndexerSkillType(AppSettingsOverride? settingsOverride) + { + return settingsOverride?.SearchIndexerSkillType ?? this.settings.SearchIndexerSkillType ?? Constants.SearchIndexerSkillTypes.Integrated; + } + private static string GetIndexerName(string indexName) { return $"{indexName}-indexer"; @@ -411,10 +497,4 @@ private static string GetSkillsetName(string indexName) { return $"{indexName}-skillset"; } - - private bool UsePullModel(AppSettingsOverride? settingsOverride) - { - var searchIndexerSkillType = settingsOverride?.SearchIndexerSkillType ?? this.settings.SearchIndexerSkillType; - return !string.Equals(searchIndexerSkillType, Constants.SearchIndexerSkillTypes.Push, StringComparison.InvariantCultureIgnoreCase); - } } \ No newline at end of file diff --git a/src/Azure.AISearch.WebApp/Services/AzureCognitiveSearchService.cs b/src/Azure.AISearch.WebApp/Services/AzureCognitiveSearchService.cs index 8e7d4f3..9fbc81b 100644 --- a/src/Azure.AISearch.WebApp/Services/AzureCognitiveSearchService.cs +++ b/src/Azure.AISearch.WebApp/Services/AzureCognitiveSearchService.cs @@ -59,16 +59,25 @@ public async Task SearchAsync(SearchRequest request) { ArgumentNullException.ThrowIfNull(request.Query); - // Generate an embedding vector for the search query text. - var queryEmbeddings = await this.embeddingService.GetEmbeddingAsync(request.Query); - - // Pass the vector as part of the search options. - searchOptions.VectorQueries.Add(new RawVectorQuery + var vectorQuery = default(VectorQuery); + if (request.UseIntegratedVectorization) + { + // Pass the original search query as part of the search options so that Azure AI Search + // can generate the embedding directly using integrated vectorization. + vectorQuery = new VectorizableTextQuery { Text = request.Query }; + } + else { - KNearestNeighborsCount = request.VectorNearestNeighborsCount ?? Constants.Defaults.VectorNearestNeighborsCount, - Fields = { nameof(DocumentChunk.ContentVector) }, - Vector = queryEmbeddings - }); + // Generate an embedding vector for the search query text. + var queryEmbeddings = await this.embeddingService.GetEmbeddingAsync(request.Query); + + // Pass the vector itself as part of the search options. + vectorQuery = new RawVectorQuery { Vector = queryEmbeddings }; + } + + vectorQuery.KNearestNeighborsCount = request.VectorNearestNeighborsCount ?? Constants.Defaults.VectorNearestNeighborsCount; + vectorQuery.Fields.Add(nameof(DocumentChunk.ContentVector)); + searchOptions.VectorQueries.Add(vectorQuery); } // Don't pass the search query text for vector-only search. @@ -120,7 +129,6 @@ private void SetSearchOptionsForChunksIndex(SearchOptions searchOptions, QueryTy searchOptions.Select.Add(nameof(DocumentChunk.SourceDocumentId)); searchOptions.Select.Add(nameof(DocumentChunk.SourceDocumentTitle)); searchOptions.Select.Add(nameof(DocumentChunk.Content)); - searchOptions.Select.Add(nameof(DocumentChunk.ChunkIndex)); if (queryType != QueryType.Vector) { // Don't request highlights for vector-only search, as that doesn't make @@ -136,7 +144,6 @@ private SearchResult GetSearchResultForChunksIndex(SearchResult searchResult.SearchIndexKey = result.Document.GetString(nameof(DocumentChunk.Id)); searchResult.DocumentId = result.Document.GetString(nameof(DocumentChunk.SourceDocumentId)); searchResult.DocumentTitle = result.Document.GetString(nameof(DocumentChunk.SourceDocumentTitle)); - searchResult.ChunkIndex = result.Document.GetInt32(nameof(DocumentChunk.ChunkIndex)); if (queryType == QueryType.Vector) { diff --git a/src/Azure.AISearch.WebApp/Services/SearchScenarioProvider.cs b/src/Azure.AISearch.WebApp/Services/SearchScenarioProvider.cs index 0ef2eb3..f4aaa77 100644 --- a/src/Azure.AISearch.WebApp/Services/SearchScenarioProvider.cs +++ b/src/Azure.AISearch.WebApp/Services/SearchScenarioProvider.cs @@ -74,7 +74,8 @@ public IList GetSearchScenarios() { Engine = EngineType.AzureCognitiveSearch, SearchIndex = SearchIndexType.Chunks, - QueryType = QueryType.Vector + QueryType = QueryType.Vector, + UseIntegratedVectorization = true } }, new SearchScenario @@ -86,7 +87,8 @@ public IList GetSearchScenarios() { Engine = EngineType.AzureCognitiveSearch, SearchIndex = SearchIndexType.Chunks, - QueryType = QueryType.HybridSemantic + QueryType = QueryType.HybridSemantic, + UseIntegratedVectorization = true } }, new SearchScenario @@ -115,7 +117,8 @@ public IList GetSearchScenarios() SystemRoleInformation = this.settings.GetDefaultSystemRoleInformation(), SearchIndex = SearchIndexType.Chunks, // As a built-in scenario, always use the chunks index for best results QueryType = QueryType.HybridSemantic, // As a built-in scenario, always use semantic ranking for best results - LimitToDataSource = true + LimitToDataSource = true, + UseIntegratedVectorization = true } }, new SearchScenario @@ -129,8 +132,8 @@ public IList GetSearchScenarios() OpenAIGptDeployment = this.settings.OpenAIGptDeployment, CustomOrchestrationPrompt = this.settings.GetDefaultCustomOrchestrationPrompt(), SearchIndex = SearchIndexType.Chunks, // As a built-in scenario, always use the chunks index for best results - QueryType = QueryType.HybridSemantic // As a built-in scenario, always use semantic ranking for best results - } + QueryType = QueryType.HybridSemantic, // As a built-in scenario, always use semantic ranking for best results + UseIntegratedVectorization = true } } }; }