feat(crm): route crm search through meilisearch

This commit is contained in:
2026-06-05 04:16:49 +08:00
parent 7a7bc4197a
commit 202da194e1
11 changed files with 318 additions and 18 deletions
+2 -1
View File
@@ -14,5 +14,6 @@ type SearchRepoInterface interface {
SearchConversations(ctx context.Context, accountID uint, query string, filter *SearchFilter) ([]model.Conversation, int64, error)
SearchMessages(ctx context.Context, accountID uint, query string, filter *SearchFilter) ([]model.Message, int64, error)
SearchContacts(ctx context.Context, accountID uint, query string, filter *SearchFilter) ([]model.Contact, int64, error)
SearchCompanies(ctx context.Context, accountID uint, query string, filter *SearchFilter) ([]model.Company, int64, error)
SearchArticles(ctx context.Context, accountID uint, query string, filter *SearchFilter) ([]model.Article, int64, error)
}
}
+45
View File
@@ -126,6 +126,27 @@ func (s *SearchService) GlobalSearch(ctx context.Context, accountID uint, query
}
}
// Search companies
if filter.ShouldSearchType(ResultTypeCompany) {
companies, companyCount, err := s.searchRepo.SearchCompanies(ctx, accountID, query, filter)
if err != nil {
applogger.L().Warnf("search companies error: %v", err)
} else {
byType["company"] = companyCount
totalCount += companyCount
for _, company := range companies {
allResults = append(allResults, SearchResult{
Type: ResultTypeCompany,
ID: company.ID,
AccountID: company.AccountID,
Snippet: company.Name,
Score: 1,
Data: company,
})
}
}
}
// Search articles (Knowledge Base)
if filter.ShouldSearchType(ResultTypeArticle) {
articles, articleCount, err := s.searchRepo.SearchArticles(ctx, accountID, query, filter)
@@ -235,6 +256,30 @@ func (s *SearchService) SearchContacts(ctx context.Context, accountID uint, quer
return results, total, nil
}
// SearchCompanies performs a filtered company search.
func (s *SearchService) SearchCompanies(ctx context.Context, accountID uint, query string, filter *SearchFilter) ([]SearchResult, int64, error) {
if s.engine != nil {
return s.searchWithEngineForType(ctx, accountID, query, filter, ResultTypeCompany)
}
companies, total, err := s.searchRepo.SearchCompanies(ctx, accountID, query, filter)
if err != nil {
return nil, 0, fmt.Errorf("search companies: %w", err)
}
results := make([]SearchResult, len(companies))
for i, company := range companies {
results[i] = SearchResult{
Type: ResultTypeCompany,
ID: company.ID,
AccountID: company.AccountID,
Snippet: company.Name,
Score: 1,
Data: company,
}
}
return results, total, nil
}
// SearchArticles performs a filtered knowledge base article search.
// Convenience method for article-only search with full filter support.
func (s *SearchService) SearchArticles(ctx context.Context, accountID uint, query string, filter *SearchFilter) ([]SearchResult, int64, error) {
+41 -9
View File
@@ -15,20 +15,24 @@ import (
// mockSearchRepo implements SearchRepoInterface for testing.
type mockSearchRepo struct {
conversations []model.Conversation
convCount int64
convErr error
convCount int64
convErr error
messages []model.Message
msgCount int64
msgErr error
messages []model.Message
msgCount int64
msgErr error
contacts []model.Contact
contactCount int64
contactErr error
articles []model.Article
articleCount int64
articleErr error
companies []model.Company
companyCount int64
companyErr error
articles []model.Article
articleCount int64
articleErr error
}
func (m *mockSearchRepo) SearchConversations(ctx context.Context, accountID uint, query string, filter *SearchFilter) ([]model.Conversation, int64, error) {
@@ -43,6 +47,10 @@ func (m *mockSearchRepo) SearchContacts(ctx context.Context, accountID uint, que
return m.contacts, m.contactCount, m.contactErr
}
func (m *mockSearchRepo) SearchCompanies(ctx context.Context, accountID uint, query string, filter *SearchFilter) ([]model.Company, int64, error) {
return m.companies, m.companyCount, m.companyErr
}
func (m *mockSearchRepo) SearchArticles(ctx context.Context, accountID uint, query string, filter *SearchFilter) ([]model.Article, int64, error) {
return m.articles, m.articleCount, m.articleErr
}
@@ -75,6 +83,15 @@ func makeContact(id uint, accountID uint, name string, email string, phone strin
}
}
func makeCompany(id uint, accountID uint, name string, domain string) model.Company {
return model.Company{
AccountID: accountID,
ID: id,
Name: name,
Domain: domain,
}
}
func makeArticle(id uint, accountID uint, title string, description string, content string, status string) model.Article {
return model.Article{
Base: model.Base{ID: id},
@@ -277,6 +294,21 @@ func TestSearchContacts_ConvenienceMethod(t *testing.T) {
assert.Equal(t, ResultTypeContact, results[0].Type)
}
func TestSearchCompanies_ConvenienceMethod(t *testing.T) {
repo := &mockSearchRepo{}
repo.companies = []model.Company{makeCompany(7, 1, "Acme", "acme.example")}
repo.companyCount = 1
svc := NewSearchService(repo)
filter := &SearchFilter{Page: 1, PerPage: 25}
results, total, err := svc.SearchCompanies(context.Background(), 1, "acme", filter)
require.NoError(t, err)
assert.Equal(t, int64(1), total)
assert.Len(t, results, 1)
assert.Equal(t, ResultTypeCompany, results[0].Type)
}
func TestSearchArticles_ConvenienceMethod(t *testing.T) {
repo := &mockSearchRepo{}
repo.articles = []model.Article{makeArticle(1, 1, "FAQ", "Common questions", "Details here", "published")}
@@ -555,4 +587,4 @@ func TestSortResultsByScore_StableOrder(t *testing.T) {
assert.Equal(t, uint(1), results[0].ID)
assert.Equal(t, uint(2), results[1].ID)
assert.Equal(t, uint(3), results[2].ID)
}
}