semantic search
This commit is contained in:
parent
7f3bcebad7
commit
64fdb6318c
@ -311,9 +311,12 @@
|
||||
' <a href="javascript:void(0);" onclick="ssWidgetSearchGlobally()" class="ss-widget-scope-clear">(جستجوی سراسری)</a></p>';
|
||||
}
|
||||
|
||||
response.results.forEach(function (item) {
|
||||
response.results.forEach(function (item, index) {
|
||||
var rank = index + 1;
|
||||
html += '<div class="ss-widget-result">';
|
||||
html += '<a class="ss-widget-result-title" href="' + ssWidgetEscapeHtml(item.fullUrl) + '">' + ssWidgetEscapeHtml(item.fullTitle || item.title) + '</a>';
|
||||
html += '<a class="ss-widget-result-title" href="' + ssWidgetEscapeHtml(item.fullUrl) + '" onclick="ssWidgetReportClick(' +
|
||||
JSON.stringify(response.logId) + ',' + JSON.stringify(item.poemId) + ',' + rank + ')">' +
|
||||
ssWidgetEscapeHtml(item.fullTitle || item.title) + '</a>';
|
||||
html += ssWidgetRenderVerses(item.verses);
|
||||
html += '</div>';
|
||||
});
|
||||
@ -321,6 +324,29 @@
|
||||
resultsEl.innerHTML = html;
|
||||
}
|
||||
|
||||
// Fire-and-forget - does not preventDefault or delay navigation in any way, just queues the
|
||||
// click report in the background while the browser proceeds to the poem page normally.
|
||||
// sendBeacon is specifically designed for "report something right as the page is about to
|
||||
// unload" - a plain fetch can get cancelled mid-flight by the navigation; sendBeacon (or a
|
||||
// keepalive fetch as fallback for older browsers) doesn't have that problem.
|
||||
function ssWidgetReportClick(logId, poemId, rank) {
|
||||
if (!logId) return; // the search itself failed to log - nothing to correlate a click to
|
||||
|
||||
var payload = JSON.stringify({ logId: logId, poemId: poemId, rank: rank });
|
||||
var url = ssWidgetApiRoot + "/api/ganjoor/search/semantic/click";
|
||||
|
||||
if (navigator.sendBeacon) {
|
||||
navigator.sendBeacon(url, new Blob([payload], { type: "application/json" }));
|
||||
} else {
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: payload,
|
||||
keepalive: true
|
||||
}).catch(function () { });
|
||||
}
|
||||
}
|
||||
|
||||
// Same hemistich-pairing logic as mini-ganjoor's renderVerses (https://ganjoor.github.io/mini/)
|
||||
// and SemanticSearch.cshtml - reused as-is rather than reinvented a third time.
|
||||
function ssWidgetRenderVerses(verses) {
|
||||
|
||||
@ -61,5 +61,21 @@ namespace RMuseum.Controllers
|
||||
return BadRequest(exp.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fire-and-forget click reporting — called via navigator.sendBeacon (or a keepalive
|
||||
/// fetch as fallback) right as a result link is clicked, so it can complete even as the
|
||||
/// browser navigates away. ReportClickAsync itself is fully best-effort (never throws in
|
||||
/// a way that matters here), so this always returns 200 regardless of whether the
|
||||
/// underlying write actually succeeded — the caller isn't listening for the response
|
||||
/// either way.
|
||||
/// </summary>
|
||||
[HttpPost("search/semantic/click")]
|
||||
[ProducesResponseType((int)HttpStatusCode.OK)]
|
||||
public async Task<IActionResult> SemanticSearchClick([FromBody] SemanticSearchClickDto click)
|
||||
{
|
||||
await _semanticSearchService.ReportClickAsync(click);
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ using RSecurityBackend.DbContext;
|
||||
using RSecurityBackend.Models.Auth.Db;
|
||||
using System;
|
||||
using RMuseum.Models.Ganjoor;
|
||||
using RMuseum.Models.Ganjoor.SemanticSearch;
|
||||
using RMuseum.Models.MusicCatalogue;
|
||||
using RMuseum.Models.Accounting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@ -353,6 +354,12 @@ namespace RMuseum.DbContext
|
||||
/// </summary>
|
||||
public DbSet<GanjoorPoem> GanjoorPoems { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Semantic search query log — see SemanticSearchQueryLog for what is (and deliberately
|
||||
/// isn't) recorded
|
||||
/// </summary>
|
||||
public DbSet<SemanticSearchQueryLog> SemanticSearchQueryLogs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Ganjoor Verses
|
||||
/// </summary>
|
||||
|
||||
6174
RMuseum/Migrations/20260911114747_AddSemanticSearchQueryLog.Designer.cs
generated
Normal file
6174
RMuseum/Migrations/20260911114747_AddSemanticSearchQueryLog.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RMuseum.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddSemanticSearchQueryLog : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SemanticSearchQueryLogs",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
Query = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
DateTimeUtc = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
RequestedTopK = table.Column<int>(type: "int", nullable: false),
|
||||
ResultCount = table.Column<int>(type: "int", nullable: false),
|
||||
TopResultScore = table.Column<float>(type: "real", nullable: true),
|
||||
ScopeDetected = table.Column<bool>(type: "bit", nullable: false),
|
||||
DetectedPoetName = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
DetectedCategoryName = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
ScopeDetectionDisabled = table.Column<bool>(type: "bit", nullable: false),
|
||||
ClickedPoemId = table.Column<int>(type: "int", nullable: true),
|
||||
ClickedResultRank = table.Column<int>(type: "int", nullable: true),
|
||||
ClickedAtUtc = table.Column<DateTime>(type: "datetime2", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SemanticSearchQueryLogs", x => x.Id);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "SemanticSearchQueryLogs");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2651,6 +2651,55 @@ namespace RMuseum.Migrations
|
||||
b.ToTable("GanjoorPoemMusicTracks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RMuseum.Models.Ganjoor.SemanticSearch.SemanticSearchQueryLog", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime?>("ClickedAtUtc")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int?>("ClickedPoemId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("ClickedResultRank")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("DateTimeUtc")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("DetectedCategoryName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("DetectedPoetName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Query")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("RequestedTopK")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("ResultCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<bool>("ScopeDetected")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("ScopeDetectionDisabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<float?>("TopResultScore")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("SemanticSearchQueryLogs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RMuseum.Models.Ganjoor.UpdatingRelSectsLog", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
||||
@ -83,6 +83,27 @@ namespace RMuseum.Models.Ganjoor.SemanticSearch
|
||||
/// </summary>
|
||||
public string DetectedPoetName { get; set; }
|
||||
public string DetectedCategoryName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Id of the SemanticSearchQueryLog row written for this request, or null if logging
|
||||
/// itself failed (best-effort — see SemanticSearchService.SearchAsync — a logging
|
||||
/// failure must never fail the search itself). The UI passes this back with
|
||||
/// POST search/semantic/click if/when a result gets clicked, so the click can be
|
||||
/// correlated to the search that produced it.
|
||||
/// </summary>
|
||||
public int? LogId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reported by the UI, fire-and-forget, if and when a person actually clicks through to one
|
||||
/// of the results — see SemanticSearchQueryLog for what this updates.
|
||||
/// </summary>
|
||||
public class SemanticSearchClickDto
|
||||
{
|
||||
public int LogId { get; set; }
|
||||
public int PoemId { get; set; }
|
||||
|
||||
/// <summary>1-based position of the clicked result in the list that was returned</summary>
|
||||
public int Rank { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
using System;
|
||||
|
||||
namespace RMuseum.Models.Ganjoor.SemanticSearch
|
||||
{
|
||||
/// <summary>
|
||||
/// A lightweight, privacy-conscious log of semantic search usage — query text and result
|
||||
/// metadata only. Deliberately NO user identifiers: no IP address, no user agent, no
|
||||
/// session/user id. Exists so ranking decisions (the AI-summary down-ranking, a future
|
||||
/// couplet-level search, anything else) can be evaluated against real usage instead of a
|
||||
/// handful of manually spot-checked queries.
|
||||
/// </summary>
|
||||
public class SemanticSearchQueryLog
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Query { get; set; }
|
||||
public DateTime DateTimeUtc { get; set; }
|
||||
public int RequestedTopK { get; set; }
|
||||
public int ResultCount { get; set; }
|
||||
public float? TopResultScore { get; set; }
|
||||
public bool ScopeDetected { get; set; }
|
||||
public string DetectedPoetName { get; set; }
|
||||
public string DetectedCategoryName { get; set; }
|
||||
public bool ScopeDetectionDisabled { get; set; }
|
||||
|
||||
// Filled in later, via POST search/semantic/click, if and only if a result actually gets
|
||||
// clicked. Null/null/null (the expected common case — most searches presumably end
|
||||
// without a click, or the person is just browsing results) is itself useful signal, not
|
||||
// missing data.
|
||||
public int? ClickedPoemId { get; set; }
|
||||
public int? ClickedResultRank { get; set; }
|
||||
public DateTime? ClickedAtUtc { get; set; }
|
||||
}
|
||||
}
|
||||
@ -3448,6 +3448,16 @@
|
||||
semantic ("find a poem about...") search
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:RMuseum.Controllers.SemanticSearchController.SemanticSearchClick(RMuseum.Models.Ganjoor.SemanticSearch.SemanticSearchClickDto)">
|
||||
<summary>
|
||||
Fire-and-forget click reporting — called via navigator.sendBeacon (or a keepalive
|
||||
fetch as fallback) right as a result link is clicked, so it can complete even as the
|
||||
browser navigates away. ReportClickAsync itself is fully best-effort (never throws in
|
||||
a way that matters here), so this always returns 200 regardless of whether the
|
||||
underlying write actually succeeded — the caller isn't listening for the response
|
||||
either way.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:RMuseum.Controllers.SiteBannersController.AddSiteBanner">
|
||||
<summary>
|
||||
add site banner (send form) with these fields: alt, url and an image attachment
|
||||
@ -3681,6 +3691,12 @@
|
||||
Ganjoor Poems
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:RMuseum.DbContext.RMuseumDbContext.SemanticSearchQueryLogs">
|
||||
<summary>
|
||||
Semantic search query log — see SemanticSearchQueryLog for what is (and deliberately
|
||||
isn't) recorded
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:RMuseum.DbContext.RMuseumDbContext.GanjoorVerses">
|
||||
<summary>
|
||||
Ganjoor Verses
|
||||
@ -4695,6 +4711,18 @@
|
||||
<member name="M:RMuseum.Migrations.RSecNewFields.BuildTargetModel(Microsoft.EntityFrameworkCore.ModelBuilder)">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="T:RMuseum.Migrations.AddSemanticSearchQueryLog">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:RMuseum.Migrations.AddSemanticSearchQueryLog.Up(Microsoft.EntityFrameworkCore.Migrations.MigrationBuilder)">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:RMuseum.Migrations.AddSemanticSearchQueryLog.Down(Microsoft.EntityFrameworkCore.Migrations.MigrationBuilder)">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:RMuseum.Migrations.AddSemanticSearchQueryLog.BuildTargetModel(Microsoft.EntityFrameworkCore.ModelBuilder)">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="T:RMuseum.Models.Accounting.DonationExpenditure">
|
||||
<summary>
|
||||
donation expenditures
|
||||
@ -11385,6 +11413,16 @@
|
||||
explicit PoetId/CatId narrows further, it doesn't replace detection.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:RMuseum.Models.Ganjoor.SemanticSearch.SemanticSearchRequestDto.DisableScopeDetection">
|
||||
<summary>
|
||||
Skips auto-detection entirely for this request — the "search globally instead"
|
||||
escape hatch. Needed because substring-based detection is genuinely ambiguous
|
||||
sometimes: "شمع و پروانه" is both a common poetic theme AND the literal title of a
|
||||
book by a specific poet, so a query using it as a theme could get silently locked to
|
||||
that one book with no way out otherwise. An explicit PoetId/CatId still applies even
|
||||
with this set — that's a deliberate scope the caller asked for, not something guessed.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:RMuseum.Models.Ganjoor.SemanticSearch.SemanticSearchVerseDto.Position">
|
||||
<summary>
|
||||
matches GanjoorVerse.VersePosition.ToString() (e.g. "Right"/"Left") - same convention
|
||||
@ -11414,6 +11452,33 @@
|
||||
without any name being recognized in the free text).
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:RMuseum.Models.Ganjoor.SemanticSearch.SemanticSearchResponseDto.LogId">
|
||||
<summary>
|
||||
Id of the SemanticSearchQueryLog row written for this request, or null if logging
|
||||
itself failed (best-effort — see SemanticSearchService.SearchAsync — a logging
|
||||
failure must never fail the search itself). The UI passes this back with
|
||||
POST search/semantic/click if/when a result gets clicked, so the click can be
|
||||
correlated to the search that produced it.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:RMuseum.Models.Ganjoor.SemanticSearch.SemanticSearchClickDto">
|
||||
<summary>
|
||||
Reported by the UI, fire-and-forget, if and when a person actually clicks through to one
|
||||
of the results — see SemanticSearchQueryLog for what this updates.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:RMuseum.Models.Ganjoor.SemanticSearch.SemanticSearchClickDto.Rank">
|
||||
<summary>1-based position of the clicked result in the list that was returned</summary>
|
||||
</member>
|
||||
<member name="T:RMuseum.Models.Ganjoor.SemanticSearch.SemanticSearchQueryLog">
|
||||
<summary>
|
||||
A lightweight, privacy-conscious log of semantic search usage — query text and result
|
||||
metadata only. Deliberately NO user identifiers: no IP address, no user agent, no
|
||||
session/user id. Exists so ranking decisions (the AI-summary down-ranking, a future
|
||||
couplet-level search, anything else) can be evaluated against real usage instead of a
|
||||
handful of manually spot-checked queries.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:RMuseum.Models.Ganjoor.UpdatingRelSectsLog">
|
||||
<summary>
|
||||
Updating related sections logs
|
||||
@ -23119,6 +23184,12 @@
|
||||
</summary>
|
||||
<param name="roleManager"></param>
|
||||
</member>
|
||||
<member name="M:RMuseum.Services.Implementation.ISemanticSearchService.ReportClickAsync(RMuseum.Models.Ganjoor.SemanticSearch.SemanticSearchClickDto)">
|
||||
<summary>
|
||||
Best-effort — see the implementation for why this should never throw in a way the
|
||||
caller needs to handle.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:RMuseum.Services.Implementation.SemanticSearchService">
|
||||
<summary>
|
||||
Deliberately NOT a GanjoorService partial, unlike everything else in this project.
|
||||
@ -23142,6 +23213,14 @@
|
||||
one must not be able to disable the other.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:RMuseum.Services.Implementation.SemanticSearchService.IsAiGeneratedSummary(System.String)">
|
||||
<summary>
|
||||
ganjoor-data's own editing workflow requires this exact prefix be removed once a
|
||||
human has reviewed/edited a poem's summary — its continued presence is a direct,
|
||||
already-existing signal for "not yet human-reviewed," not something this project
|
||||
invented or has to infer.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="F:RMuseum.Services.Implementation.SemanticSearchService.PersianStopWords">
|
||||
<summary>
|
||||
Common Persian function words stripped out before keyword-matching a query against
|
||||
@ -23162,15 +23241,25 @@
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:RMuseum.Services.Implementation.SemanticSearchService.SelectPreviewVerses(System.Collections.Generic.List{RMuseum.Models.Ganjoor.GanjoorVerse},System.Collections.Generic.List{System.String},System.Int32)">
|
||||
<summary>
|
||||
Looks for the couplet (Right/Left verse pair) whose combined text contains the most
|
||||
query keywords, and returns it (plus, if there's room within previewVerseCount, the
|
||||
couplet immediately following it, for a little reading continuity rather than a
|
||||
single isolated pair). Falls back to the poem's opening verses — the previous,
|
||||
always-the-same-lines behavior — if no keyword appears anywhere in the scanned
|
||||
verses, or if there were no real keywords to search for at all (a query that was
|
||||
entirely stopwords, or empty after stripping them).
|
||||
</summary>
|
||||
<summary>
|
||||
Looks for the couplet (Right/Left verse pair) whose combined text contains the most
|
||||
query keywords, and returns it (plus, if there's room within previewVerseCount, the
|
||||
couplet immediately following it, for a little reading continuity rather than a
|
||||
single isolated pair). Falls back to the poem's opening verses — the previous,
|
||||
always-the-same-lines behavior — if no keyword appears anywhere in the scanned
|
||||
verses, or if there were no real keywords to search for at all (a query that was
|
||||
entirely stopwords, or empty after stripping them).
|
||||
|
||||
Matches against CoupletSummary (when present) as well as the raw verse text — the
|
||||
summary is clean, modern-language prose, while the verses themselves are archaic and
|
||||
metaphorical and often won't literally contain a query's keywords even when the
|
||||
couplet is genuinely on-topic. CoupletSummary is only ever read from the FIRST verse
|
||||
of the pair (the "Right"/anchor position) — ganjoor-data stores it once per couplet
|
||||
there, never on the second ("Left") verse; a small number of "Left" rows do carry a
|
||||
stray value (a data anomaly, not a second legitimate copy), and this deliberately
|
||||
never reads it from that position, matching how the data is actually meant to be laid
|
||||
out rather than how a few rows happen to look.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:RMuseum.Services.Implementation.SemanticSearchService.GetDescendantCategoryIdsAsync(RMuseum.DbContext.RMuseumDbContext,System.Int32)">
|
||||
<summary>
|
||||
@ -24604,6 +24693,20 @@
|
||||
SUPPOSED to have it (ganjgah.ir) needs the key explicitly set to "True", not left implicit.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:RMuseum.Utils.SemanticSearch.LazySemanticSearchResources.AiSummaryScorePenalty">
|
||||
<summary>
|
||||
A gentle re-ranking multiplier applied to results whose PoemSummary is still
|
||||
AI-generated and un-reviewed (detected by the "هوش مصنوعی:" prefix ganjoor-data's own
|
||||
editing workflow requires removing once a human has reviewed/edited a summary — see
|
||||
SemanticSearchService for how this is actually applied). A soft nudge, not a filter:
|
||||
~95% of summaries currently carry this prefix, so excluding them outright would gut
|
||||
coverage for most queries. Configurable (SemanticSearch:AiSummaryScorePenalty) rather
|
||||
than hardcoded, since the right effect size here is a judgment call worth tuning
|
||||
without a redeploy. Read here (not directly in SemanticSearchService) purely to reuse
|
||||
the config-reading this class already does — this value has nothing to do with the
|
||||
lazy-loaded embeddings/model themselves and is available even when Enabled is false.
|
||||
</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,
|
||||
|
||||
@ -13,6 +13,12 @@ namespace RMuseum.Services.Implementation
|
||||
public interface ISemanticSearchService
|
||||
{
|
||||
Task<SemanticSearchResponseDto> SearchAsync(SemanticSearchRequestDto request);
|
||||
|
||||
/// <summary>
|
||||
/// Best-effort — see the implementation for why this should never throw in a way the
|
||||
/// caller needs to handle.
|
||||
/// </summary>
|
||||
Task ReportClickAsync(SemanticSearchClickDto click);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -216,11 +222,66 @@ namespace RMuseum.Services.Implementation
|
||||
}).ToList(),
|
||||
});
|
||||
}
|
||||
|
||||
// Best-effort logging, using the same context already open for this request - a
|
||||
// logging failure must never fail a real search, so any exception here is
|
||||
// swallowed and response.LogId simply stays null (ReportClickAsync already
|
||||
// no-ops safely if it's ever called with a null/0 LogId from a search that
|
||||
// couldn't be logged).
|
||||
try
|
||||
{
|
||||
var log = new SemanticSearchQueryLog
|
||||
{
|
||||
Query = request.Query,
|
||||
DateTimeUtc = DateTime.UtcNow,
|
||||
RequestedTopK = k,
|
||||
ResultCount = response.Results.Count,
|
||||
TopResultScore = response.Results.Count > 0 ? (float?)response.Results[0].Score : null,
|
||||
ScopeDetected = !string.IsNullOrEmpty(response.DetectedPoetName) || !string.IsNullOrEmpty(response.DetectedCategoryName),
|
||||
DetectedPoetName = response.DetectedPoetName,
|
||||
DetectedCategoryName = response.DetectedCategoryName,
|
||||
ScopeDetectionDisabled = request.DisableScopeDetection,
|
||||
};
|
||||
context.SemanticSearchQueryLogs.Add(log);
|
||||
await context.SaveChangesAsync();
|
||||
response.LogId = log.Id;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// logging is best-effort only - never worth failing a real search over
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public async Task ReportClickAsync(SemanticSearchClickDto click)
|
||||
{
|
||||
if (click == null || click.LogId <= 0)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
using (RMuseumDbContext context = new RMuseumDbContext(new DbContextOptions<RMuseumDbContext>()))
|
||||
{
|
||||
var log = await context.SemanticSearchQueryLogs.FindAsync(click.LogId);
|
||||
if (log != null)
|
||||
{
|
||||
log.ClickedPoemId = click.PoemId;
|
||||
log.ClickedResultRank = click.Rank;
|
||||
log.ClickedAtUtc = DateTime.UtcNow;
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// click reporting is best-effort only - the click/navigation has already
|
||||
// happened regardless of whether this write succeeds, never worth surfacing an
|
||||
// error to the (fire-and-forget) caller for this
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ganjoor-data's own editing workflow requires this exact prefix be removed once a
|
||||
/// human has reviewed/edited a poem's summary — its continued presence is a direct,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user