diff --git a/src/InboxIntel.Infrastructure/Migrations/20260701235112_TrigramIndexesSenderDomain.Designer.cs b/src/InboxIntel.Infrastructure/Migrations/20260701235112_TrigramIndexesSenderDomain.Designer.cs
new file mode 100644
index 0000000..81e5395
--- /dev/null
+++ b/src/InboxIntel.Infrastructure/Migrations/20260701235112_TrigramIndexesSenderDomain.Designer.cs
@@ -0,0 +1,758 @@
+//
+using System;
+using InboxIntel.Infrastructure.Persistence;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using NpgsqlTypes;
+
+#nullable disable
+
+namespace InboxIntel.Infrastructure.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ [Migration("20260701235112_TrigramIndexesSenderDomain")]
+ partial class TrigramIndexesSenderDomain
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.4")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "pg_trgm");
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("InboxIntel.Domain.Entities.AnalyticsAggregate", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAtUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Day")
+ .HasColumnType("date");
+
+ b.Property("HourHistogramJson")
+ .HasColumnType("text");
+
+ b.Property("NewsletterCount")
+ .HasColumnType("integer");
+
+ b.Property("TotalReceived")
+ .HasColumnType("integer");
+
+ b.Property("TotalSizeBytes")
+ .HasColumnType("bigint");
+
+ b.Property("TotalUnread")
+ .HasColumnType("integer");
+
+ b.Property("UpdatedAtUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.Property("WithAttachments")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Day")
+ .IsUnique();
+
+ b.ToTable("analytics_aggregates", (string)null);
+ });
+
+ modelBuilder.Entity("InboxIntel.Domain.Entities.Attachment", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAtUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("EmailId")
+ .HasColumnType("uuid");
+
+ b.Property("FileName")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)");
+
+ b.Property("GmailAttachmentId")
+ .HasColumnType("text");
+
+ b.Property("MimeType")
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("SizeBytes")
+ .HasColumnType("bigint");
+
+ b.Property("UpdatedAtUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("EmailId");
+
+ b.HasIndex("UserId", "MimeType");
+
+ b.ToTable("attachments", (string)null);
+ });
+
+ modelBuilder.Entity("InboxIntel.Domain.Entities.Email", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("BodyText")
+ .HasColumnType("text");
+
+ b.Property("Category")
+ .HasColumnType("integer");
+
+ b.Property("CreatedAtUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("GmailMessageId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("HasAttachments")
+ .HasColumnType("boolean");
+
+ b.Property("HasListUnsubscribe")
+ .HasColumnType("boolean");
+
+ b.Property("IsImportant")
+ .HasColumnType("boolean");
+
+ b.Property("IsInInbox")
+ .HasColumnType("boolean");
+
+ b.Property("IsStarred")
+ .HasColumnType("boolean");
+
+ b.Property("IsTrashed")
+ .HasColumnType("boolean");
+
+ b.Property("IsUnread")
+ .HasColumnType("boolean");
+
+ b.Property("ListUnsubscribeRaw")
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b.Property("ReceivedAtUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("SearchVector")
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("tsvector")
+ .HasComputedColumnSql("setweight(to_tsvector('english', coalesce(\"Subject\",'')), 'A') || setweight(to_tsvector('english', coalesce(\"BodyText\",'')), 'B')", true);
+
+ b.Property("SenderId")
+ .HasColumnType("uuid");
+
+ b.Property("SentAtUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("SizeEstimateBytes")
+ .HasColumnType("bigint");
+
+ b.Property("Snippet")
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b.Property("Subject")
+ .HasMaxLength(1024)
+ .HasColumnType("character varying(1024)");
+
+ b.Property("SupportsOneClickUnsubscribe")
+ .HasColumnType("boolean");
+
+ b.Property("ThreadId")
+ .HasColumnType("uuid");
+
+ b.Property("UpdatedAtUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("SearchVector");
+
+ NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("SearchVector"), "GIN");
+
+ b.HasIndex("SenderId");
+
+ b.HasIndex("ThreadId");
+
+ b.HasIndex("UserId", "Category");
+
+ b.HasIndex("UserId", "GmailMessageId")
+ .IsUnique();
+
+ b.HasIndex("UserId", "IsInInbox");
+
+ b.HasIndex("UserId", "IsUnread");
+
+ b.HasIndex("UserId", "SenderId");
+
+ b.HasIndex("UserId", "SentAtUtc");
+
+ b.ToTable("emails", (string)null);
+ });
+
+ modelBuilder.Entity("InboxIntel.Domain.Entities.EmailLabel", b =>
+ {
+ b.Property("EmailId")
+ .HasColumnType("uuid");
+
+ b.Property("LabelId")
+ .HasColumnType("uuid");
+
+ b.HasKey("EmailId", "LabelId");
+
+ b.HasIndex("LabelId");
+
+ b.ToTable("email_labels", (string)null);
+ });
+
+ modelBuilder.Entity("InboxIntel.Domain.Entities.Label", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("ColorHex")
+ .HasColumnType("text");
+
+ b.Property("CreatedAtUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("GmailLabelId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("UpdatedAtUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "GmailLabelId")
+ .IsUnique();
+
+ b.ToTable("labels", (string)null);
+ });
+
+ modelBuilder.Entity("InboxIntel.Domain.Entities.MailDomain", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAtUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("EmailCount")
+ .HasColumnType("integer");
+
+ b.Property("IsBulkSender")
+ .HasColumnType("boolean");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("UpdatedAtUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name");
+
+ NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Name"), "gin");
+ NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex("Name"), new[] { "gin_trgm_ops" });
+
+ b.HasIndex("UserId", "Name")
+ .IsUnique();
+
+ b.ToTable("domains", (string)null);
+ });
+
+ modelBuilder.Entity("InboxIntel.Domain.Entities.MailThread", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAtUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("FirstMessageUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("GmailThreadId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("LastMessageUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("MessageCount")
+ .HasColumnType("integer");
+
+ b.Property("Snippet")
+ .HasColumnType("text");
+
+ b.Property("Subject")
+ .HasMaxLength(1024)
+ .HasColumnType("character varying(1024)");
+
+ b.Property("UpdatedAtUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "GmailThreadId")
+ .IsUnique();
+
+ b.ToTable("threads", (string)null);
+ });
+
+ modelBuilder.Entity("InboxIntel.Domain.Entities.Sender", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Address")
+ .IsRequired()
+ .HasMaxLength(320)
+ .HasColumnType("character varying(320)");
+
+ b.Property("CreatedAtUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("DisplayName")
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("DomainId")
+ .HasColumnType("uuid");
+
+ b.Property("EmailCount")
+ .HasColumnType("integer");
+
+ b.Property("HasUnsubscribe")
+ .HasColumnType("boolean");
+
+ b.Property("LastReceivedUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("TotalSizeBytes")
+ .HasColumnType("bigint");
+
+ b.Property("UnreadCount")
+ .HasColumnType("integer");
+
+ b.Property("UpdatedAtUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Address");
+
+ NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Address"), "gin");
+ NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex("Address"), new[] { "gin_trgm_ops" });
+
+ b.HasIndex("DisplayName");
+
+ NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("DisplayName"), "gin");
+ NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex("DisplayName"), new[] { "gin_trgm_ops" });
+
+ b.HasIndex("DomainId");
+
+ b.HasIndex("UserId", "Address")
+ .IsUnique();
+
+ b.HasIndex("UserId", "EmailCount");
+
+ b.ToTable("senders", (string)null);
+ });
+
+ modelBuilder.Entity("InboxIntel.Domain.Entities.SyncState", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CompletedUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ConsecutiveFailures")
+ .HasColumnType("integer");
+
+ b.Property("CreatedAtUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("LastError")
+ .HasMaxLength(4000)
+ .HasColumnType("character varying(4000)");
+
+ b.Property("LastHistoryId")
+ .HasColumnType("text");
+
+ b.Property("LastSuccessfulSyncUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("LastSyncType")
+ .HasColumnType("integer");
+
+ b.Property("MessagesProcessed")
+ .HasColumnType("integer");
+
+ b.Property("ResumePageToken")
+ .HasColumnType("text");
+
+ b.Property("StartedUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Status")
+ .HasColumnType("integer");
+
+ b.Property("TotalMessagesEstimate")
+ .HasColumnType("integer");
+
+ b.Property("UpdatedAtUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("sync_states", (string)null);
+ });
+
+ modelBuilder.Entity("InboxIntel.Domain.Entities.UnsubscribeItem", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Confidence")
+ .HasColumnType("double precision");
+
+ b.Property("CreatedAtUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("EmailCount")
+ .HasColumnType("integer");
+
+ b.Property("LastAttemptUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Method")
+ .HasColumnType("integer");
+
+ b.Property("ResultMessage")
+ .HasColumnType("text");
+
+ b.Property("SenderId")
+ .HasColumnType("uuid");
+
+ b.Property("Status")
+ .HasColumnType("integer");
+
+ b.Property("UnsubscribeTarget")
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b.Property("UpdatedAtUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("SenderId");
+
+ b.HasIndex("UserId", "SenderId")
+ .IsUnique();
+
+ b.ToTable("unsubscribe_items", (string)null);
+ });
+
+ modelBuilder.Entity("InboxIntel.Domain.Entities.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AccessTokenExpiresAtUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedAtUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("DigestEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("DisplayName")
+ .HasColumnType("text");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasMaxLength(320)
+ .HasColumnType("character varying(320)");
+
+ b.Property("EncryptedRefreshToken")
+ .HasColumnType("bytea");
+
+ b.Property("GoogleSubjectId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("LastDigestSentUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("LastLoginUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("PictureUrl")
+ .HasColumnType("text");
+
+ b.Property("UpdatedAtUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Email")
+ .IsUnique();
+
+ b.HasIndex("GoogleSubjectId")
+ .IsUnique();
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("InboxIntel.Domain.Entities.WidgetLayout", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAtUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("H")
+ .HasColumnType("integer");
+
+ b.Property("SettingsJson")
+ .HasColumnType("text");
+
+ b.Property("SortOrder")
+ .HasColumnType("integer");
+
+ b.Property("UpdatedAtUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.Property("Visible")
+ .HasColumnType("boolean");
+
+ b.Property("W")
+ .HasColumnType("integer");
+
+ b.Property("WidgetKey")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("X")
+ .HasColumnType("integer");
+
+ b.Property("Y")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "WidgetKey")
+ .IsUnique();
+
+ b.ToTable("widget_layouts", (string)null);
+ });
+
+ modelBuilder.Entity("InboxIntel.Domain.Entities.Attachment", b =>
+ {
+ b.HasOne("InboxIntel.Domain.Entities.Email", "Email")
+ .WithMany("Attachments")
+ .HasForeignKey("EmailId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Email");
+ });
+
+ modelBuilder.Entity("InboxIntel.Domain.Entities.Email", b =>
+ {
+ b.HasOne("InboxIntel.Domain.Entities.Sender", "Sender")
+ .WithMany("Emails")
+ .HasForeignKey("SenderId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("InboxIntel.Domain.Entities.MailThread", "Thread")
+ .WithMany("Emails")
+ .HasForeignKey("ThreadId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("InboxIntel.Domain.Entities.User", null)
+ .WithMany("Emails")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Sender");
+
+ b.Navigation("Thread");
+ });
+
+ modelBuilder.Entity("InboxIntel.Domain.Entities.EmailLabel", b =>
+ {
+ b.HasOne("InboxIntel.Domain.Entities.Email", "Email")
+ .WithMany("EmailLabels")
+ .HasForeignKey("EmailId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("InboxIntel.Domain.Entities.Label", "Label")
+ .WithMany("EmailLabels")
+ .HasForeignKey("LabelId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Email");
+
+ b.Navigation("Label");
+ });
+
+ modelBuilder.Entity("InboxIntel.Domain.Entities.Sender", b =>
+ {
+ b.HasOne("InboxIntel.Domain.Entities.MailDomain", "Domain")
+ .WithMany("Senders")
+ .HasForeignKey("DomainId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Domain");
+ });
+
+ modelBuilder.Entity("InboxIntel.Domain.Entities.UnsubscribeItem", b =>
+ {
+ b.HasOne("InboxIntel.Domain.Entities.Sender", "Sender")
+ .WithMany()
+ .HasForeignKey("SenderId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Sender");
+ });
+
+ modelBuilder.Entity("InboxIntel.Domain.Entities.WidgetLayout", b =>
+ {
+ b.HasOne("InboxIntel.Domain.Entities.User", null)
+ .WithMany("WidgetLayouts")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("InboxIntel.Domain.Entities.Email", b =>
+ {
+ b.Navigation("Attachments");
+
+ b.Navigation("EmailLabels");
+ });
+
+ modelBuilder.Entity("InboxIntel.Domain.Entities.Label", b =>
+ {
+ b.Navigation("EmailLabels");
+ });
+
+ modelBuilder.Entity("InboxIntel.Domain.Entities.MailDomain", b =>
+ {
+ b.Navigation("Senders");
+ });
+
+ modelBuilder.Entity("InboxIntel.Domain.Entities.MailThread", b =>
+ {
+ b.Navigation("Emails");
+ });
+
+ modelBuilder.Entity("InboxIntel.Domain.Entities.Sender", b =>
+ {
+ b.Navigation("Emails");
+ });
+
+ modelBuilder.Entity("InboxIntel.Domain.Entities.User", b =>
+ {
+ b.Navigation("Emails");
+
+ b.Navigation("WidgetLayouts");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/InboxIntel.Infrastructure/Migrations/20260701235112_TrigramIndexesSenderDomain.cs b/src/InboxIntel.Infrastructure/Migrations/20260701235112_TrigramIndexesSenderDomain.cs
new file mode 100644
index 0000000..a58e968
--- /dev/null
+++ b/src/InboxIntel.Infrastructure/Migrations/20260701235112_TrigramIndexesSenderDomain.cs
@@ -0,0 +1,51 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace InboxIntel.Infrastructure.Migrations
+{
+ ///
+ public partial class TrigramIndexesSenderDomain : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateIndex(
+ name: "IX_senders_Address",
+ table: "senders",
+ column: "Address")
+ .Annotation("Npgsql:IndexMethod", "gin")
+ .Annotation("Npgsql:IndexOperators", new[] { "gin_trgm_ops" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_senders_DisplayName",
+ table: "senders",
+ column: "DisplayName")
+ .Annotation("Npgsql:IndexMethod", "gin")
+ .Annotation("Npgsql:IndexOperators", new[] { "gin_trgm_ops" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_domains_Name",
+ table: "domains",
+ column: "Name")
+ .Annotation("Npgsql:IndexMethod", "gin")
+ .Annotation("Npgsql:IndexOperators", new[] { "gin_trgm_ops" });
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropIndex(
+ name: "IX_senders_Address",
+ table: "senders");
+
+ migrationBuilder.DropIndex(
+ name: "IX_senders_DisplayName",
+ table: "senders");
+
+ migrationBuilder.DropIndex(
+ name: "IX_domains_Name",
+ table: "domains");
+ }
+ }
+}
diff --git a/src/InboxIntel.Infrastructure/Migrations/AppDbContextModelSnapshot.cs b/src/InboxIntel.Infrastructure/Migrations/AppDbContextModelSnapshot.cs
index ffcdab8..7ad75dc 100644
--- a/src/InboxIntel.Infrastructure/Migrations/AppDbContextModelSnapshot.cs
+++ b/src/InboxIntel.Infrastructure/Migrations/AppDbContextModelSnapshot.cs
@@ -301,6 +301,11 @@ namespace InboxIntel.Infrastructure.Migrations
b.HasKey("Id");
+ b.HasIndex("Name");
+
+ NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Name"), "gin");
+ NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex("Name"), new[] { "gin_trgm_ops" });
+
b.HasIndex("UserId", "Name")
.IsUnique();
@@ -395,6 +400,16 @@ namespace InboxIntel.Infrastructure.Migrations
b.HasKey("Id");
+ b.HasIndex("Address");
+
+ NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Address"), "gin");
+ NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex("Address"), new[] { "gin_trgm_ops" });
+
+ b.HasIndex("DisplayName");
+
+ NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("DisplayName"), "gin");
+ NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex("DisplayName"), new[] { "gin_trgm_ops" });
+
b.HasIndex("DomainId");
b.HasIndex("UserId", "Address")
diff --git a/src/InboxIntel.Infrastructure/Persistence/AppDbContext.cs b/src/InboxIntel.Infrastructure/Persistence/AppDbContext.cs
index 8acdfee..d71c65e 100644
--- a/src/InboxIntel.Infrastructure/Persistence/AppDbContext.cs
+++ b/src/InboxIntel.Infrastructure/Persistence/AppDbContext.cs
@@ -75,6 +75,16 @@ public class AppDbContext : DbContext, IAppDbContext
"setweight(to_tsvector('english', coalesce(\"BodyText\",'')), 'B')",
stored: true);
modelBuilder.Entity().HasIndex(e => e.SearchVector).HasMethod("GIN");
+
+ // Trigram GIN indexes so the sender/domain `.Contains()` filters in SearchService
+ // (which translate to `LIKE '%x%'`, non-sargable on a btree) become index-accelerated.
+ // Needs pg_trgm (enabled by the EnablePgTrgm migration).
+ modelBuilder.Entity().HasIndex(s => s.Address)
+ .HasMethod("gin").HasOperators("gin_trgm_ops");
+ modelBuilder.Entity().HasIndex(s => s.DisplayName)
+ .HasMethod("gin").HasOperators("gin_trgm_ops");
+ modelBuilder.Entity().HasIndex(d => d.Name)
+ .HasMethod("gin").HasOperators("gin_trgm_ops");
}
else
{