mirror of
https://github.com/marcominerva/SqlDatabaseVectorSearch.git
synced 2026-06-20 12:23:10 +00:00
Merge pull request #7 from marcominerva/content_decoders
Add Content decoders
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
# SQL Database Vector Search Sample
|
# SQL Database Vector Search Sample
|
||||||
A repository that showcases the native VECTOR type in Azure SQL Database to perform embeddings and RAG with Azure OpenAI.
|
A repository that showcases the native VECTOR type in Azure SQL Database to perform embeddings and RAG with Azure OpenAI.
|
||||||
|
|
||||||
The application is a Minimal API that exposes endpoints to load documents, generate embeddings and save them into the database as Vectors, and perform searches using Vector Search and RAG. Currently, only PDF files are supported. Vectors are saved and retrieved with Entity Framework Core using the [EFCore.SqlServer.VectorSearch](https://github.com/efcore/EfCore.SqlServer.VectorSearch) library. Embedding and Chat Completion are integrated with [Semantic Kernel](https://github.com/microsoft/semantic-kernel).
|
The application is a Minimal API that exposes endpoints to load documents, generate embeddings and save them into the database as Vectors, and perform searches using Vector Search and RAG. Currently, PDF, DOCX and TXT files are supported. Vectors are saved and retrieved with Entity Framework Core using the [EFCore.SqlServer.VectorSearch](https://github.com/efcore/EfCore.SqlServer.VectorSearch) library. Embedding and Chat Completion are integrated with [Semantic Kernel](https://github.com/microsoft/semantic-kernel).
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> If you prefer to use straight SQL, check out the [sql branch](https://github.com/marcominerva/SqlDatabaseVectorSearch/tree/sql).
|
> If you prefer to use straight SQL, check out the [sql branch](https://github.com/marcominerva/SqlDatabaseVectorSearch/tree/sql).
|
||||||
@@ -15,4 +15,4 @@ The application is a Minimal API that exposes endpoints to load documents, gener
|
|||||||
- You may need to update the size of the [`VECTOR`](https://github.com/marcominerva/SqlDatabaseVectorSearch/blob/master/Scripts.sql#L17) column to match the size of the embedding model. Currently, the maximum allowed value is 1998.
|
- You may need to update the size of the [`VECTOR`](https://github.com/marcominerva/SqlDatabaseVectorSearch/blob/master/Scripts.sql#L17) column to match the size of the embedding model. Currently, the maximum allowed value is 1998.
|
||||||
- Open the [appsettings.json](https://github.com/marcominerva/SqlDatabaseVectorSearch/blob/master/SqlDatabaseVectorSearch/appsettings.json) file and set the connection string to the database and the other settings required by Azure OpenAI
|
- Open the [appsettings.json](https://github.com/marcominerva/SqlDatabaseVectorSearch/blob/master/SqlDatabaseVectorSearch/appsettings.json) file and set the connection string to the database and the other settings required by Azure OpenAI
|
||||||
- If your embedding model supports shortening, like **text-embedding-3-small** and **text-embedding-3-large**, and you want to use this feature, you need to set the [`Dimensions`](https://github.com/marcominerva/SqlDatabaseVectorSearch/blob/master/SqlDatabaseVectorSearch/appsettings.json#L17) property to match the value you have used in the SQL script. If your model doesn't provide this feature, or do you want to use the default size, just leave the [`Dimensions`](https://github.com/marcominerva/SqlDatabaseVectorSearch/blob/master/SqlDatabaseVectorSearch/appsettings.json#L17) property to NULL. Keep in mind that **text-embedding-3-small** has a dimension of 1536, while **text-embedding-3-large** uses vectors with 3072 elements, so with this latter model it is mandatory to specify a value (that, as said, must be less or equal to 1998).
|
- If your embedding model supports shortening, like **text-embedding-3-small** and **text-embedding-3-large**, and you want to use this feature, you need to set the [`Dimensions`](https://github.com/marcominerva/SqlDatabaseVectorSearch/blob/master/SqlDatabaseVectorSearch/appsettings.json#L17) property to match the value you have used in the SQL script. If your model doesn't provide this feature, or do you want to use the default size, just leave the [`Dimensions`](https://github.com/marcominerva/SqlDatabaseVectorSearch/blob/master/SqlDatabaseVectorSearch/appsettings.json#L17) property to NULL. Keep in mind that **text-embedding-3-small** has a dimension of 1536, while **text-embedding-3-large** uses vectors with 3072 elements, so with this latter model it is mandatory to specify a value (that, as said, must be less or equal to 1998).
|
||||||
- Run the application and start importing your PDF documents.
|
- Run the application and start importing your documents.
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System.Text;
|
||||||
|
using DocumentFormat.OpenXml.Packaging;
|
||||||
|
using DocumentFormat.OpenXml.Wordprocessing;
|
||||||
|
|
||||||
|
namespace SqlDatabaseVectorSearch.ContentDecoders;
|
||||||
|
|
||||||
|
public class DocxContentDecoder : IContentDecoder
|
||||||
|
{
|
||||||
|
public Task<string> DecodeAsync(Stream stream, string contentType)
|
||||||
|
{
|
||||||
|
// Open a Word document for read-only access.
|
||||||
|
using var document = WordprocessingDocument.Open(stream, false);
|
||||||
|
|
||||||
|
var body = document.MainDocumentPart?.Document.Body;
|
||||||
|
var content = new StringBuilder();
|
||||||
|
|
||||||
|
var paragraphs = body?.Descendants<Paragraph>() ?? [];
|
||||||
|
foreach (var p in paragraphs)
|
||||||
|
{
|
||||||
|
content.AppendLine(p.InnerText);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(content.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace SqlDatabaseVectorSearch.ContentDecoders;
|
||||||
|
|
||||||
|
public interface IContentDecoder
|
||||||
|
{
|
||||||
|
Task<string> DecodeAsync(Stream stream, string contentType);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using System.Text;
|
||||||
|
using UglyToad.PdfPig;
|
||||||
|
using UglyToad.PdfPig.DocumentLayoutAnalysis.TextExtractor;
|
||||||
|
|
||||||
|
namespace SqlDatabaseVectorSearch.ContentDecoders;
|
||||||
|
|
||||||
|
public class PdfContentDecoder : IContentDecoder
|
||||||
|
{
|
||||||
|
public Task<string> DecodeAsync(Stream stream, string contentType)
|
||||||
|
{
|
||||||
|
var content = new StringBuilder();
|
||||||
|
|
||||||
|
// Read the content of the PDF document.
|
||||||
|
using var pdfDocument = PdfDocument.Open(stream);
|
||||||
|
|
||||||
|
foreach (var page in pdfDocument.GetPages().Where(x => x is not null))
|
||||||
|
{
|
||||||
|
var pageContent = ContentOrderTextExtractor.GetText(page) ?? string.Empty;
|
||||||
|
content.AppendLine(pageContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(content.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace SqlDatabaseVectorSearch.ContentDecoders;
|
||||||
|
|
||||||
|
public class TextContentDecoder : IContentDecoder
|
||||||
|
{
|
||||||
|
public async Task<string> DecodeAsync(Stream stream, string contentType)
|
||||||
|
{
|
||||||
|
using var readStream = new StreamReader(stream);
|
||||||
|
var content = await readStream.ReadToEndAsync();
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using System.Net.Mime;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Microsoft.AspNetCore.Http.HttpResults;
|
using Microsoft.AspNetCore.Http.HttpResults;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.SemanticKernel;
|
using Microsoft.SemanticKernel;
|
||||||
|
using SqlDatabaseVectorSearch.ContentDecoders;
|
||||||
using SqlDatabaseVectorSearch.DataAccessLayer;
|
using SqlDatabaseVectorSearch.DataAccessLayer;
|
||||||
using SqlDatabaseVectorSearch.Models;
|
using SqlDatabaseVectorSearch.Models;
|
||||||
using SqlDatabaseVectorSearch.Services;
|
using SqlDatabaseVectorSearch.Services;
|
||||||
@@ -50,6 +52,10 @@ builder.Services.AddSingleton<TokenizerService>();
|
|||||||
builder.Services.AddSingleton<ChatService>();
|
builder.Services.AddSingleton<ChatService>();
|
||||||
builder.Services.AddScoped<VectorSearchService>();
|
builder.Services.AddScoped<VectorSearchService>();
|
||||||
|
|
||||||
|
builder.Services.AddKeyedSingleton<IContentDecoder, PdfContentDecoder>(MediaTypeNames.Application.Pdf);
|
||||||
|
builder.Services.AddKeyedSingleton<IContentDecoder, DocxContentDecoder>("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
||||||
|
builder.Services.AddKeyedSingleton<IContentDecoder, TextContentDecoder>(MediaTypeNames.Text.Plain);
|
||||||
|
|
||||||
builder.Services.ConfigureHttpJsonOptions(options =>
|
builder.Services.ConfigureHttpJsonOptions(options =>
|
||||||
{
|
{
|
||||||
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||||
@@ -69,7 +75,15 @@ var app = builder.Build();
|
|||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
app.UseExceptionHandler();
|
app.UseExceptionHandler(new ExceptionHandlerOptions
|
||||||
|
{
|
||||||
|
StatusCodeSelector = exception => exception switch
|
||||||
|
{
|
||||||
|
NotSupportedException => StatusCodes.Status501NotImplemented,
|
||||||
|
_ => StatusCodes.Status500InternalServerError
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.UseStatusCodePages();
|
app.UseStatusCodePages();
|
||||||
|
|
||||||
app.MapOpenApi();
|
app.MapOpenApi();
|
||||||
@@ -113,14 +127,14 @@ documentsApiGroup.MapPost(string.Empty, async (IFormFile file, VectorSearchServi
|
|||||||
[Description("The unique identifier of the document. If not provided, a new one will be generated. If you specify an existing documentId, the corresponding document will be overwritten.")] Guid? documentId = null) =>
|
[Description("The unique identifier of the document. If not provided, a new one will be generated. If you specify an existing documentId, the corresponding document will be overwritten.")] Guid? documentId = null) =>
|
||||||
{
|
{
|
||||||
using var stream = file.OpenReadStream();
|
using var stream = file.OpenReadStream();
|
||||||
documentId = await vectorSearchService.ImportAsync(stream, file.FileName, documentId);
|
documentId = await vectorSearchService.ImportAsync(stream, file.FileName, file.ContentType, documentId);
|
||||||
|
|
||||||
return TypedResults.Ok(new UploadDocumentResponse(documentId.Value));
|
return TypedResults.Ok(new UploadDocumentResponse(documentId.Value));
|
||||||
})
|
})
|
||||||
.DisableAntiforgery()
|
.DisableAntiforgery()
|
||||||
.ProducesProblem(StatusCodes.Status400BadRequest)
|
.ProducesProblem(StatusCodes.Status400BadRequest)
|
||||||
.WithSummary("Uploads a document")
|
.WithSummary("Uploads a document")
|
||||||
.WithDescription("Uploads a document to SQL Database and saves its embedding using the native VECTOR type. The document will be indexed and used to answer questions. Currently, only PDF files are supported.");
|
.WithDescription("Uploads a document to SQL Database and saves its embedding using the native VECTOR type. The document will be indexed and used to answer questions. Currently, PDF, DOCX and TXT files are supported.");
|
||||||
|
|
||||||
documentsApiGroup.MapDelete("{documentId:guid}", async (Guid documentId, VectorSearchService vectorSearchService) =>
|
documentsApiGroup.MapDelete("{documentId:guid}", async (Guid documentId, VectorSearchService vectorSearchService) =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,26 +1,25 @@
|
|||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Text;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.SemanticKernel.Embeddings;
|
using Microsoft.SemanticKernel.Embeddings;
|
||||||
using Microsoft.SemanticKernel.Text;
|
using Microsoft.SemanticKernel.Text;
|
||||||
|
using SqlDatabaseVectorSearch.ContentDecoders;
|
||||||
using SqlDatabaseVectorSearch.DataAccessLayer;
|
using SqlDatabaseVectorSearch.DataAccessLayer;
|
||||||
using SqlDatabaseVectorSearch.Models;
|
using SqlDatabaseVectorSearch.Models;
|
||||||
using SqlDatabaseVectorSearch.Settings;
|
using SqlDatabaseVectorSearch.Settings;
|
||||||
using UglyToad.PdfPig;
|
|
||||||
using UglyToad.PdfPig.DocumentLayoutAnalysis.TextExtractor;
|
|
||||||
using Entities = SqlDatabaseVectorSearch.DataAccessLayer.Entities;
|
using Entities = SqlDatabaseVectorSearch.DataAccessLayer.Entities;
|
||||||
|
|
||||||
namespace SqlDatabaseVectorSearch.Services;
|
namespace SqlDatabaseVectorSearch.Services;
|
||||||
|
|
||||||
public class VectorSearchService(ApplicationDbContext dbContext, ITextEmbeddingGenerationService textEmbeddingGenerationService, ChatService chatService, TokenizerService tokenizerService, TimeProvider timeProvider, IOptions<AppSettings> appSettingsOptions, ILogger<VectorSearchService> logger)
|
public class VectorSearchService(IServiceProvider serviceProvider, ApplicationDbContext dbContext, ITextEmbeddingGenerationService textEmbeddingGenerationService, ChatService chatService, TokenizerService tokenizerService, TimeProvider timeProvider, IOptions<AppSettings> appSettingsOptions, ILogger<VectorSearchService> logger)
|
||||||
{
|
{
|
||||||
private readonly AppSettings appSettings = appSettingsOptions.Value;
|
private readonly AppSettings appSettings = appSettingsOptions.Value;
|
||||||
|
|
||||||
public async Task<Guid> ImportAsync(Stream stream, string name, Guid? documentId)
|
public async Task<Guid> ImportAsync(Stream stream, string name, string contentType, Guid? documentId)
|
||||||
{
|
{
|
||||||
// Extract the contents of the file (currently, only PDF files are supported).
|
// Extract the contents of the file.
|
||||||
var content = await GetContentAsync(stream);
|
var decoder = serviceProvider.GetKeyedService<IContentDecoder>(contentType) ?? throw new NotSupportedException($"Content type '{contentType}' is not supported.");
|
||||||
|
var content = await decoder.DecodeAsync(stream, contentType);
|
||||||
|
|
||||||
await dbContext.Database.BeginTransactionAsync();
|
await dbContext.Database.BeginTransactionAsync();
|
||||||
|
|
||||||
@@ -126,20 +125,4 @@ public class VectorSearchService(ApplicationDbContext dbContext, ITextEmbeddingG
|
|||||||
|
|
||||||
return (reformulatedQuestion, chunks);
|
return (reformulatedQuestion, chunks);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Task<string> GetContentAsync(Stream stream)
|
|
||||||
{
|
|
||||||
var content = new StringBuilder();
|
|
||||||
|
|
||||||
// Read the content of the PDF document.
|
|
||||||
using var pdfDocument = PdfDocument.Open(stream);
|
|
||||||
|
|
||||||
foreach (var page in pdfDocument.GetPages().Where(x => x is not null))
|
|
||||||
{
|
|
||||||
var pageContent = ContentOrderTextExtractor.GetText(page) ?? string.Empty;
|
|
||||||
content.AppendLine(pageContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.FromResult(content.ToString());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="DocumentFormat.OpenXml" Version="3.2.0" />
|
||||||
<PackageReference Include="EFCore.SqlServer.VectorSearch" Version="9.0.0-preview.2" />
|
<PackageReference Include="EFCore.SqlServer.VectorSearch" Version="9.0.0-preview.2" />
|
||||||
<PackageReference Include="EntityFrameworkCore.Exceptions.SqlServer" Version="8.1.3" />
|
<PackageReference Include="EntityFrameworkCore.Exceptions.SqlServer" Version="8.1.3" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.1" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.1" />
|
||||||
@@ -17,7 +18,7 @@
|
|||||||
<PackageReference Include="Microsoft.ML.Tokenizers" Version="1.0.1" />
|
<PackageReference Include="Microsoft.ML.Tokenizers" Version="1.0.1" />
|
||||||
<PackageReference Include="Microsoft.ML.Tokenizers.Data.Cl100kBase" Version="1.0.1" />
|
<PackageReference Include="Microsoft.ML.Tokenizers.Data.Cl100kBase" Version="1.0.1" />
|
||||||
<PackageReference Include="Microsoft.ML.Tokenizers.Data.O200kBase" Version="1.0.1" />
|
<PackageReference Include="Microsoft.ML.Tokenizers.Data.O200kBase" Version="1.0.1" />
|
||||||
<PackageReference Include="Microsoft.SemanticKernel" Version="1.34.0" />
|
<PackageReference Include="Microsoft.SemanticKernel" Version="1.35.0" />
|
||||||
<PackageReference Include="MinimalHelpers.OpenApi" Version="2.1.3" />
|
<PackageReference Include="MinimalHelpers.OpenApi" Version="2.1.3" />
|
||||||
<PackageReference Include="PdfPig" Version="0.1.9" />
|
<PackageReference Include="PdfPig" Version="0.1.9" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="7.2.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="7.2.0" />
|
||||||
|
|||||||
Reference in New Issue
Block a user