fix for semantic search
This commit is contained in:
parent
e42cc5d8eb
commit
65737c73d8
@ -9,7 +9,6 @@ using RMuseum.Models.Auth.Memory;
|
||||
using RMuseum.Models.Auth.ViewModel;
|
||||
using RMuseum.Models.Ganjoor;
|
||||
using RMuseum.Models.Ganjoor.PublicExport;
|
||||
using RMuseum.Models.Ganjoor.SemanticSearch;
|
||||
using RMuseum.Models.Ganjoor.ViewModels;
|
||||
using RMuseum.Models.GanjoorAudio.ViewModels;
|
||||
using RMuseum.Models.GanjoorIntegration;
|
||||
@ -5023,30 +5022,6 @@ namespace RMuseum.Controllers
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// semantic ("find a poem about...") search
|
||||
/// </summary>
|
||||
[HttpPost("search/semantic")]
|
||||
[ProducesResponseType((int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
|
||||
public async Task<IActionResult> SemanticSearch([FromBody] SemanticSearchRequestDto request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _semanticSearchService.SearchAsync(request.Query, request.TopK);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (ArgumentException exp)
|
||||
{
|
||||
return BadRequest(exp.Message);
|
||||
}
|
||||
catch (Exception exp)
|
||||
{
|
||||
return BadRequest(exp.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
@ -5117,8 +5092,6 @@ namespace RMuseum.Controllers
|
||||
protected IConfiguration Configuration { get; }
|
||||
|
||||
|
||||
protected ISemanticSearchService _semanticSearchService;
|
||||
|
||||
/// <summary>
|
||||
/// constructor
|
||||
/// </summary>
|
||||
@ -5128,15 +5101,13 @@ namespace RMuseum.Controllers
|
||||
/// <param name="imageFileService"></param>
|
||||
/// <param name="memoryCache"></param>
|
||||
/// <param name="configuration"></param>
|
||||
/// <param name="semanticSearchService"></param>
|
||||
public GanjoorController(
|
||||
IGanjoorService ganjoorService,
|
||||
IAppUserService appUserService,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IImageFileService imageFileService,
|
||||
IMemoryCache memoryCache,
|
||||
IConfiguration configuration,
|
||||
ISemanticSearchService semanticSearchService
|
||||
IConfiguration configuration
|
||||
)
|
||||
{
|
||||
_ganjoorService = ganjoorService;
|
||||
@ -5145,7 +5116,6 @@ namespace RMuseum.Controllers
|
||||
_imageFileService = imageFileService;
|
||||
_memoryCache = memoryCache;
|
||||
Configuration = configuration;
|
||||
_semanticSearchService = semanticSearchService;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
65
RMuseum/Controllers/SemanticSearchController.cs
Normal file
65
RMuseum/Controllers/SemanticSearchController.cs
Normal file
@ -0,0 +1,65 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using RMuseum.Models.Ganjoor.SemanticSearch;
|
||||
using RMuseum.Services.Implementation;
|
||||
using RMuseum.Utils.SemanticSearch;
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace RMuseum.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Semantic ("find a poem about...") search — deliberately its own controller, not a method
|
||||
/// on GanjoorController, after a production incident: GanjoorController's constructor took
|
||||
/// ISemanticSearchService (indirectly requiring EmbeddingIndex/QueryEmbedder to load
|
||||
/// successfully), so a resource-loading failure prevented the ENTIRE controller from being
|
||||
/// constructed — a 503 on every endpoint under /api/ganjoor, not just this feature. Same
|
||||
/// route prefix as before (api/ganjoor), so the endpoint's URL is unchanged
|
||||
/// (POST /api/ganjoor/search/semantic) — only which controller class hosts it changed.
|
||||
/// A future failure in this feature's own dependencies can now only ever affect this one
|
||||
/// controller/endpoint, never GanjoorController or anything else.
|
||||
/// </summary>
|
||||
[Produces("application/json")]
|
||||
[Route("api/ganjoor")]
|
||||
[ApiController]
|
||||
public class SemanticSearchController : ControllerBase
|
||||
{
|
||||
protected readonly ISemanticSearchService _semanticSearchService;
|
||||
|
||||
public SemanticSearchController(ISemanticSearchService semanticSearchService)
|
||||
{
|
||||
_semanticSearchService = semanticSearchService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// semantic ("find a poem about...") search
|
||||
/// </summary>
|
||||
[HttpPost("search/semantic")]
|
||||
[ProducesResponseType((int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
|
||||
[ProducesResponseType((int)HttpStatusCode.ServiceUnavailable, Type = typeof(string))]
|
||||
public async Task<IActionResult> SemanticSearch([FromBody] SemanticSearchRequestDto request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _semanticSearchService.SearchAsync(request.Query, request.TopK);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (SemanticSearchUnavailableException exp)
|
||||
{
|
||||
// distinct from a plain 400/500 - lets a client (or a person reading logs) tell
|
||||
// "this feature isn't loaded/configured right now" apart from a bad query or an
|
||||
// actual crash
|
||||
return StatusCode((int)HttpStatusCode.ServiceUnavailable, exp.Message);
|
||||
}
|
||||
catch (ArgumentException exp)
|
||||
{
|
||||
return BadRequest(exp.Message);
|
||||
}
|
||||
catch (Exception exp)
|
||||
{
|
||||
return BadRequest(exp.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2644,11 +2644,6 @@
|
||||
<param name="correction"></param>
|
||||
<returns></returns>
|
||||
</member>
|
||||
<member name="M:RMuseum.Controllers.GanjoorController.SemanticSearch(RMuseum.Models.Ganjoor.SemanticSearch.SemanticSearchRequestDto)">
|
||||
<summary>
|
||||
semantic ("find a poem about...") search
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:RMuseum.Controllers.GanjoorController.ReadOnlyMode">
|
||||
<summary>
|
||||
readonly mode
|
||||
@ -2689,7 +2684,7 @@
|
||||
Configuration
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:RMuseum.Controllers.GanjoorController.#ctor(RMuseum.Services.IGanjoorService,RSecurityBackend.Services.IAppUserService,Microsoft.AspNetCore.Http.IHttpContextAccessor,RSecurityBackend.Services.IImageFileService,Microsoft.Extensions.Caching.Memory.IMemoryCache,Microsoft.Extensions.Configuration.IConfiguration,RMuseum.Services.Implementation.ISemanticSearchService)">
|
||||
<member name="M:RMuseum.Controllers.GanjoorController.#ctor(RMuseum.Services.IGanjoorService,RSecurityBackend.Services.IAppUserService,Microsoft.AspNetCore.Http.IHttpContextAccessor,RSecurityBackend.Services.IImageFileService,Microsoft.Extensions.Caching.Memory.IMemoryCache,Microsoft.Extensions.Configuration.IConfiguration)">
|
||||
<summary>
|
||||
constructor
|
||||
</summary>
|
||||
@ -2699,7 +2694,6 @@
|
||||
<param name="imageFileService"></param>
|
||||
<param name="memoryCache"></param>
|
||||
<param name="configuration"></param>
|
||||
<param name="semanticSearchService"></param>
|
||||
</member>
|
||||
<member name="M:RMuseum.Controllers.GanjoorPoetSuggestedPhotosController.GetPoetSuggestedPhotosAsync(System.Int32)">
|
||||
<summary>
|
||||
@ -3436,6 +3430,24 @@
|
||||
</summary>
|
||||
<param name="roleService"></param>
|
||||
</member>
|
||||
<member name="T:RMuseum.Controllers.SemanticSearchController">
|
||||
<summary>
|
||||
Semantic ("find a poem about...") search — deliberately its own controller, not a method
|
||||
on GanjoorController, after a production incident: GanjoorController's constructor took
|
||||
ISemanticSearchService (indirectly requiring EmbeddingIndex/QueryEmbedder to load
|
||||
successfully), so a resource-loading failure prevented the ENTIRE controller from being
|
||||
constructed — a 503 on every endpoint under /api/ganjoor, not just this feature. Same
|
||||
route prefix as before (api/ganjoor), so the endpoint's URL is unchanged
|
||||
(POST /api/ganjoor/search/semantic) — only which controller class hosts it changed.
|
||||
A future failure in this feature's own dependencies can now only ever affect this one
|
||||
controller/endpoint, never GanjoorController or anything else.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:RMuseum.Controllers.SemanticSearchController.SemanticSearch(RMuseum.Models.Ganjoor.SemanticSearch.SemanticSearchRequestDto)">
|
||||
<summary>
|
||||
semantic ("find a poem about...") search
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:RMuseum.Controllers.SiteBannersController.AddSiteBanner">
|
||||
<summary>
|
||||
add site banner (send form) with these fields: alt, url and an image attachment
|
||||
@ -23077,15 +23089,22 @@
|
||||
<param name="roleManager"></param>
|
||||
</member>
|
||||
<member name="T:RMuseum.Services.Implementation.SemanticSearchService">
|
||||
<summary>
|
||||
Deliberately NOT a GanjoorService partial, unlike everything else in this project.
|
||||
EmbeddingIndex (~530MB in memory) and QueryEmbedder (a loaded ONNX model) both need to be
|
||||
true singletons — constructed once at startup, never per-request — while GanjoorService
|
||||
and RMuseumDbContext are scoped per-request throughout this codebase. Injecting a
|
||||
singleton's dependencies into a per-request class (or vice versa) is a real DI lifetime
|
||||
bug, not just an inconsistency, so this stays a separate service registered as a
|
||||
singleton itself (see INTEGRATION.md for the exact registration).
|
||||
</summary>
|
||||
<summary>
|
||||
Deliberately NOT a GanjoorService partial, unlike everything else in this project.
|
||||
EmbeddingIndex (~530MB in memory) and QueryEmbedder (a loaded ONNX model) both need to be
|
||||
true singletons — constructed once at startup, never per-request — while GanjoorService
|
||||
and RMuseumDbContext are scoped per-request throughout this codebase. Injecting a
|
||||
singleton's dependencies into a per-request class (or vice versa) is a real DI lifetime
|
||||
bug, not just an inconsistency, so this stays a separate service registered as a
|
||||
singleton itself (see INTEGRATION.md for the exact registration).
|
||||
|
||||
Depends on LazySemanticSearchResources rather than EmbeddingIndex/QueryEmbedder directly —
|
||||
deliberately, after a production incident where eager, throwing DI factories for those two
|
||||
meant a load failure (wrong/missing file paths) prevented GanjoorController itself from
|
||||
being constructed, taking down every endpoint under /api/ganjoor with a 503, not just
|
||||
semantic search. This class must never let a resource-loading failure become an unhandled
|
||||
exception that propagates past SearchAsync — see the catch below.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:RMuseum.Services.Implementation.SiteBannersService">
|
||||
<summary>
|
||||
@ -24451,6 +24470,46 @@
|
||||
L2-normalized the same way the indexed vectors are — see QueryEmbedder.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:RMuseum.Utils.SemanticSearch.LazySemanticSearchResources">
|
||||
<summary>
|
||||
Wraps EmbeddingIndex + QueryEmbedder loading so a failure (missing files, wrong paths,
|
||||
corrupt data) can NEVER take down anything else in the app.
|
||||
|
||||
The original design registered EmbeddingIndex/QueryEmbedder as singletons whose DI
|
||||
factories called EmbeddingIndex.Load(...)/`new QueryEmbedder(...)` directly — both of
|
||||
which throw on failure. Because GanjoorController's constructor (indirectly, through
|
||||
ISemanticSearchService) depended on them, a load failure meant the controller itself
|
||||
couldn't be constructed — taking down EVERY endpoint under /api/ganjoor, not just semantic
|
||||
search, with a 503. That's exactly what happened in production. This class exists so that
|
||||
can't happen again: the actual load is deferred to first real use (not app/controller
|
||||
construction), attempted at most once, and a failure is caught, logged, and remembered —
|
||||
SearchAsync() then reports "search unavailable" as an ordinary result, not an exception
|
||||
that propagates into breaking anything else.
|
||||
|
||||
Also worth knowing if this server runs multiple IIS worker processes for the same app
|
||||
pool: each process gets its own instance of this (and everything it loads) — "singleton"
|
||||
only means one instance per process, not per server. See the migration notes for the
|
||||
memory math this implies at scale.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:RMuseum.Utils.SemanticSearch.LazySemanticSearchResources.TryGetResources(RMuseum.Utils.SemanticSearch.EmbeddingIndex@,RMuseum.Utils.SemanticSearch.QueryEmbedder@,System.String@)">
|
||||
<summary>
|
||||
Attempts to load the resources on first call (subsequent calls reuse the same result,
|
||||
success or failure — this never retries automatically; a fresh app start is required
|
||||
to try again after a config/file fix, which is the expected deploy-and-restart flow
|
||||
anyway). Returns true and populates both out parameters if available; returns false
|
||||
and populates <paramref name="error"/> otherwise. NEVER THROWS — that guarantee is the
|
||||
entire point of this class.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:RMuseum.Utils.SemanticSearch.SemanticSearchUnavailableException">
|
||||
<summary>
|
||||
Thrown by SemanticSearchService when the underlying resources aren't available — the
|
||||
controller catches this specifically and returns HTTP 503 with the message, distinct from
|
||||
a plain 400/500, so a client (or a person checking logs) can tell "this feature isn't
|
||||
configured/loaded right now" apart from "the query itself was bad" or "something crashed".
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:RMuseum.Utils.SemanticSearch.QueryEmbedder">
|
||||
<summary>
|
||||
Embeds a single user-typed search query using the SAME ONNX model + tokenizer as
|
||||
|
||||
@ -22,19 +22,24 @@ namespace RMuseum.Services.Implementation
|
||||
/// singleton's dependencies into a per-request class (or vice versa) is a real DI lifetime
|
||||
/// bug, not just an inconsistency, so this stays a separate service registered as a
|
||||
/// singleton itself (see INTEGRATION.md for the exact registration).
|
||||
///
|
||||
/// Depends on LazySemanticSearchResources rather than EmbeddingIndex/QueryEmbedder directly —
|
||||
/// deliberately, after a production incident where eager, throwing DI factories for those two
|
||||
/// meant a load failure (wrong/missing file paths) prevented GanjoorController itself from
|
||||
/// being constructed, taking down every endpoint under /api/ganjoor with a 503, not just
|
||||
/// semantic search. This class must never let a resource-loading failure become an unhandled
|
||||
/// exception that propagates past SearchAsync — see the catch below.
|
||||
/// </summary>
|
||||
public class SemanticSearchService : ISemanticSearchService
|
||||
{
|
||||
private const int DefaultTopK = 10;
|
||||
private const int MaxTopK = 50;
|
||||
|
||||
private readonly EmbeddingIndex _embeddingIndex;
|
||||
private readonly QueryEmbedder _queryEmbedder;
|
||||
private readonly LazySemanticSearchResources _resources;
|
||||
|
||||
public SemanticSearchService(EmbeddingIndex embeddingIndex, QueryEmbedder queryEmbedder)
|
||||
public SemanticSearchService(LazySemanticSearchResources resources)
|
||||
{
|
||||
_embeddingIndex = embeddingIndex;
|
||||
_queryEmbedder = queryEmbedder;
|
||||
_resources = resources;
|
||||
}
|
||||
|
||||
public async Task<SemanticSearchResponseDto> SearchAsync(string query, int? topK)
|
||||
@ -42,12 +47,18 @@ namespace RMuseum.Services.Implementation
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
throw new ArgumentException("query must not be empty", nameof(query));
|
||||
|
||||
if (!_resources.TryGetResources(out var embeddingIndex, out var queryEmbedder, out var error))
|
||||
{
|
||||
throw new SemanticSearchUnavailableException(
|
||||
"Semantic search is not available right now" + (error != null ? $": {error}" : "."));
|
||||
}
|
||||
|
||||
int k = topK.GetValueOrDefault(DefaultTopK);
|
||||
if (k <= 0 || k > MaxTopK)
|
||||
k = DefaultTopK;
|
||||
|
||||
float[] queryVector = _queryEmbedder.EmbedQuery(query);
|
||||
List<(int PoemId, float Score)> topMatches = _embeddingIndex.FindTopSimilar(queryVector, k);
|
||||
float[] queryVector = queryEmbedder.EmbedQuery(query);
|
||||
List<(int PoemId, float Score)> topMatches = embeddingIndex.FindTopSimilar(queryVector, k);
|
||||
|
||||
var response = new SemanticSearchResponseDto { Query = query };
|
||||
|
||||
|
||||
@ -327,23 +327,14 @@ namespace RMuseum
|
||||
|
||||
services.AddOpenAIService();
|
||||
|
||||
services.AddSingleton<EmbeddingIndex>(sp =>
|
||||
{
|
||||
var config = sp.GetRequiredService<IConfiguration>();
|
||||
string dir = config["SemanticSearch:EmbeddingsDirectory"];
|
||||
return EmbeddingIndex.Load(dir);
|
||||
});
|
||||
|
||||
services.AddSingleton<QueryEmbedder>(sp =>
|
||||
{
|
||||
var config = sp.GetRequiredService<IConfiguration>();
|
||||
int dimension = int.Parse(config["SemanticSearch:Dimension"] ?? "1024");
|
||||
return new QueryEmbedder(
|
||||
config["SemanticSearch:ModelPath"],
|
||||
config["SemanticSearch:VocabPath"],
|
||||
config["SemanticSearch:MergesPath"],
|
||||
dimension);
|
||||
});
|
||||
// See LazySemanticSearchResources.cs: this is deliberately NOT
|
||||
// services.AddSingleton<EmbeddingIndex>(sp => EmbeddingIndex.Load(...)) anymore. That
|
||||
// eager, throwing factory is what caused a production 503 on the whole /api/ganjoor
|
||||
// surface when the configured paths were wrong — GanjoorController's constructor
|
||||
// (via ISemanticSearchService) couldn't be built, so nothing under that route could
|
||||
// run. LazySemanticSearchResources defers the actual load to first real use and
|
||||
// never throws; a failure there disables semantic search only.
|
||||
services.AddSingleton<LazySemanticSearchResources>();
|
||||
|
||||
services.AddSingleton<ISemanticSearchService, SemanticSearchService>();
|
||||
|
||||
|
||||
111
RMuseum/Utils/SemanticSearch/LazySemanticSearchResources.cs
Normal file
111
RMuseum/Utils/SemanticSearch/LazySemanticSearchResources.cs
Normal file
@ -0,0 +1,111 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
|
||||
namespace RMuseum.Utils.SemanticSearch
|
||||
{
|
||||
/// <summary>
|
||||
/// Wraps EmbeddingIndex + QueryEmbedder loading so a failure (missing files, wrong paths,
|
||||
/// corrupt data) can NEVER take down anything else in the app.
|
||||
///
|
||||
/// The original design registered EmbeddingIndex/QueryEmbedder as singletons whose DI
|
||||
/// factories called EmbeddingIndex.Load(...)/`new QueryEmbedder(...)` directly — both of
|
||||
/// which throw on failure. Because GanjoorController's constructor (indirectly, through
|
||||
/// ISemanticSearchService) depended on them, a load failure meant the controller itself
|
||||
/// couldn't be constructed — taking down EVERY endpoint under /api/ganjoor, not just semantic
|
||||
/// search, with a 503. That's exactly what happened in production. This class exists so that
|
||||
/// can't happen again: the actual load is deferred to first real use (not app/controller
|
||||
/// construction), attempted at most once, and a failure is caught, logged, and remembered —
|
||||
/// SearchAsync() then reports "search unavailable" as an ordinary result, not an exception
|
||||
/// that propagates into breaking anything else.
|
||||
///
|
||||
/// Also worth knowing if this server runs multiple IIS worker processes for the same app
|
||||
/// pool: each process gets its own instance of this (and everything it loads) — "singleton"
|
||||
/// only means one instance per process, not per server. See the migration notes for the
|
||||
/// memory math this implies at scale.
|
||||
/// </summary>
|
||||
public class LazySemanticSearchResources
|
||||
{
|
||||
private readonly object _lock = new object();
|
||||
private EmbeddingIndex _embeddingIndex;
|
||||
private QueryEmbedder _queryEmbedder;
|
||||
private string _loadError;
|
||||
private bool _attempted;
|
||||
|
||||
private readonly string _embeddingsDirectory;
|
||||
private readonly string _modelPath;
|
||||
private readonly string _vocabPath;
|
||||
private readonly string _mergesPath;
|
||||
private readonly int _dimension;
|
||||
private readonly ILogger<LazySemanticSearchResources> _logger;
|
||||
|
||||
public LazySemanticSearchResources(IConfiguration configuration, ILogger<LazySemanticSearchResources> logger)
|
||||
{
|
||||
_embeddingsDirectory = configuration["SemanticSearch:EmbeddingsDirectory"];
|
||||
_modelPath = configuration["SemanticSearch:ModelPath"];
|
||||
_vocabPath = configuration["SemanticSearch:VocabPath"];
|
||||
_mergesPath = configuration["SemanticSearch:MergesPath"];
|
||||
_dimension = int.TryParse(configuration["SemanticSearch:Dimension"], out var d) ? d : 1024;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to load the resources on first call (subsequent calls reuse the same result,
|
||||
/// success or failure — this never retries automatically; a fresh app start is required
|
||||
/// to try again after a config/file fix, which is the expected deploy-and-restart flow
|
||||
/// anyway). Returns true and populates both out parameters if available; returns false
|
||||
/// and populates <paramref name="error"/> otherwise. NEVER THROWS — that guarantee is the
|
||||
/// entire point of this class.
|
||||
/// </summary>
|
||||
public bool TryGetResources(out EmbeddingIndex embeddingIndex, out QueryEmbedder queryEmbedder, out string error)
|
||||
{
|
||||
if (!_attempted)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_attempted)
|
||||
{
|
||||
try
|
||||
{
|
||||
_embeddingIndex = EmbeddingIndex.Load(_embeddingsDirectory);
|
||||
_queryEmbedder = new QueryEmbedder(_modelPath, _vocabPath, _mergesPath, _dimension);
|
||||
_logger.LogInformation(
|
||||
"Semantic search resources loaded: {Count} poems, dimension {Dimension}.",
|
||||
_embeddingIndex.Metadata.Count, _embeddingIndex.Metadata.Dimension);
|
||||
}
|
||||
catch (Exception exp)
|
||||
{
|
||||
_loadError = exp.Message;
|
||||
_embeddingIndex = null;
|
||||
_queryEmbedder = null;
|
||||
_logger.LogError(exp,
|
||||
"Semantic search resources failed to load from '{EmbeddingsDirectory}' / '{ModelPath}' " +
|
||||
"— semantic search will report unavailable, but this must not affect anything else.",
|
||||
_embeddingsDirectory, _modelPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_attempted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
embeddingIndex = _embeddingIndex;
|
||||
queryEmbedder = _queryEmbedder;
|
||||
error = _loadError;
|
||||
return _embeddingIndex != null && _queryEmbedder != null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Thrown by SemanticSearchService when the underlying resources aren't available — the
|
||||
/// controller catches this specifically and returns HTTP 503 with the message, distinct from
|
||||
/// a plain 400/500, so a client (or a person checking logs) can tell "this feature isn't
|
||||
/// configured/loaded right now" apart from "the query itself was bad" or "something crashed".
|
||||
/// </summary>
|
||||
public class SemanticSearchUnavailableException : Exception
|
||||
{
|
||||
public SemanticSearchUnavailableException(string message) : base(message) { }
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user