feat(search): align conversation identity search

This commit is contained in:
2026-06-07 08:05:12 +08:00
parent 9a7e455e1c
commit 450c8673d8
9 changed files with 171 additions and 42 deletions
+29 -4
View File
@@ -41,7 +41,14 @@ func (i *DurableSearchIndexer) IndexConversation(ctx context.Context, conversati
return nil
}
return i.enqueueOrIndex(ctx, "conversation", conversation.AccountID, conversation.ID, func() error {
return i.delegate.IndexConversation(ctx, conversation)
enriched, err := i.loadConversation(ctx, conversation.AccountID, conversation.ID)
if err != nil {
return err
}
if enriched == nil {
return nil
}
return i.delegate.IndexConversation(ctx, enriched)
})
}
@@ -144,11 +151,14 @@ func (i *DurableSearchIndexer) perform(ctx context.Context, job *model.Backgroun
func (i *DurableSearchIndexer) performIndex(ctx context.Context, payload searchIndexJob) error {
switch payload.Entity {
case "conversation":
var item model.Conversation
if err := i.load(ctx, payload, &item); err != nil {
item, err := i.loadConversation(ctx, payload.AccountID, payload.ID)
if err != nil {
return err
}
return i.delegate.IndexConversation(ctx, &item)
if item == nil {
return nil
}
return i.delegate.IndexConversation(ctx, item)
case "message":
var item model.Message
if err := i.load(ctx, payload, &item); err != nil {
@@ -178,6 +188,21 @@ func (i *DurableSearchIndexer) performIndex(ctx context.Context, payload searchI
}
}
func (i *DurableSearchIndexer) loadConversation(ctx context.Context, accountID, id uint) (*model.Conversation, error) {
var item model.Conversation
err := i.db.WithContext(ctx).
Preload("Contact").
Where("id = ? AND account_id = ?", id, accountID).
First(&item).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, i.performDelete(ctx, searchIndexJob{Operation: "delete", Entity: "conversation", AccountID: accountID, ID: id})
}
if err != nil {
return nil, err
}
return &item, nil
}
func (i *DurableSearchIndexer) load(ctx context.Context, payload searchIndexJob, dest any) error {
err := i.db.WithContext(ctx).Where("id = ? AND account_id = ?", payload.ID, payload.AccountID).First(dest).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
+34 -3
View File
@@ -11,12 +11,16 @@ import (
)
type recordingDurableSearchIndexer struct {
indexedContacts []uint
deletedContacts []uint
err error
indexedConversations []model.Conversation
indexedContacts []uint
deletedContacts []uint
err error
}
func (r *recordingDurableSearchIndexer) IndexConversation(ctx context.Context, conversation *model.Conversation) error {
if conversation != nil {
r.indexedConversations = append(r.indexedConversations, *conversation)
}
return r.err
}
@@ -91,6 +95,33 @@ func TestDurableSearchIndexerQueuesAndReplaysContactIndex(t *testing.T) {
}
}
func TestDurableSearchIndexerPreloadsConversationContact(t *testing.T) {
db := setupServiceTestDB(t)
account := createTestAccount(t, db)
contact := &model.Contact{AccountID: account.ID, Name: "Ada Lovelace", Email: "ada@example.com"}
if err := db.Create(contact).Error; err != nil {
t.Fatalf("create contact: %v", err)
}
conversation := &model.Conversation{AccountID: account.ID, ContactID: contact.ID, InboxID: 1, ChannelType: "web_widget", Channel: "web_widget", Status: "open"}
if err := db.Create(conversation).Error; err != nil {
t.Fatalf("create conversation: %v", err)
}
delegate := &recordingDurableSearchIndexer{}
indexer := NewDurableSearchIndexer(db, nil, delegate)
if err := indexer.IndexConversation(context.Background(), conversation); err != nil {
t.Fatalf("index conversation: %v", err)
}
if len(delegate.indexedConversations) != 1 {
t.Fatalf("expected one indexed conversation, got %#v", delegate.indexedConversations)
}
indexed := delegate.indexedConversations[0]
if indexed.Contact == nil || indexed.Contact.Name != "Ada Lovelace" || indexed.Contact.Email != "ada@example.com" {
t.Fatalf("expected preloaded contact in indexed conversation, got %#v", indexed.Contact)
}
}
func TestDurableSearchIndexerRetriesDelegateFailure(t *testing.T) {
db := setupServiceTestDB(t)
account := createTestAccount(t, db)