feat(monitoring): match citations by canonical link and scope summary by brand/keyword/platform/date
Replace the domain+title fuzzy alias scoring with a canonical candidate-key index built from published-link aliases — only exact matches now resolve to a SaaS source, eliminating false positives across articles sharing a host (e.g. m.163.com vs www.163.com). Surface the summary scoping that already existed on loadCitationRanking / loadCitedArticles by accepting brand_id, keyword_id, ai_platform_id and business_date on CitationSummary, plumbing them through the handler and admin tracking view. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -284,6 +284,7 @@ type monitoringAliasResolution struct {
|
||||
|
||||
type monitoringAliasLookupInput struct {
|
||||
NormalizedURL string
|
||||
MatchKeys []string
|
||||
RegistrableDomain string
|
||||
SiteKey string
|
||||
Host string
|
||||
@@ -293,6 +294,7 @@ type monitoringAliasLookupInput struct {
|
||||
|
||||
type monitoringAliasCandidate struct {
|
||||
NormalizedURL string
|
||||
MatchKeys []string
|
||||
ArticleID *int64
|
||||
PublishRecordID *int64
|
||||
RegistrableDomain string
|
||||
@@ -2444,6 +2446,7 @@ func (s *MonitoringCallbackService) buildCitationFacts(ctx context.Context, tx p
|
||||
}
|
||||
lookupInputs = append(lookupInputs, monitoringAliasLookupInput{
|
||||
NormalizedURL: key,
|
||||
MatchKeys: monitoringCitationCandidateKeys(item.URL, key),
|
||||
RegistrableDomain: strings.ToLower(strings.TrimSpace(registrableDomain)),
|
||||
SiteKey: strings.ToLower(strings.TrimSpace(siteKey)),
|
||||
Host: strings.ToLower(strings.TrimSpace(host)),
|
||||
@@ -2557,6 +2560,7 @@ func (s *MonitoringCallbackService) loadAliasResolutions(ctx context.Context, tx
|
||||
candidate.Host = strings.ToLower(strings.TrimSpace(nullableStringText(host)))
|
||||
candidate.LastPathSegment = strings.TrimSpace(nullableStringText(lastPathSegment))
|
||||
candidate.TitleKey = normalizeMonitoringAliasTitle(nullableStringText(articleTitle))
|
||||
candidate.MatchKeys = monitoringCitationCandidateKeys(candidate.NormalizedURL, candidate.NormalizedURL)
|
||||
candidates = append(candidates, candidate)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
@@ -2567,11 +2571,7 @@ func (s *MonitoringCallbackService) loadAliasResolutions(ctx context.Context, tx
|
||||
bestScore := -1
|
||||
bestAmbiguous := false
|
||||
var best monitoringAliasCandidate
|
||||
domainMatchedArticleIDs := map[int64]struct{}{}
|
||||
for _, candidate := range candidates {
|
||||
if monitoringAliasDomainMatches(input, candidate) && candidate.ArticleID != nil {
|
||||
domainMatchedArticleIDs[*candidate.ArticleID] = struct{}{}
|
||||
}
|
||||
score := scoreMonitoringAliasCandidate(input, candidate)
|
||||
if score < 0 {
|
||||
continue
|
||||
@@ -2586,27 +2586,16 @@ func (s *MonitoringCallbackService) loadAliasResolutions(ctx context.Context, tx
|
||||
bestAmbiguous = true
|
||||
}
|
||||
}
|
||||
if bestScore == 40 && len(domainMatchedArticleIDs) == 1 {
|
||||
bestScore = 60
|
||||
}
|
||||
|
||||
if bestScore < 60 || bestAmbiguous {
|
||||
if bestScore < 100 || bestAmbiguous {
|
||||
continue
|
||||
}
|
||||
confidence := "medium"
|
||||
status := "domain_matched"
|
||||
if bestScore >= 100 {
|
||||
confidence = "high"
|
||||
status = "resolved"
|
||||
} else if bestScore < 80 {
|
||||
confidence = "low"
|
||||
}
|
||||
result[input.NormalizedURL] = monitoringAliasResolution{
|
||||
ArticleID: cloneInt64(best.ArticleID),
|
||||
PublishRecordID: cloneInt64(best.PublishRecordID),
|
||||
NormalizedURLKey: best.NormalizedURL,
|
||||
ResolutionStatus: status,
|
||||
ResolutionConfidence: confidence,
|
||||
ResolutionStatus: "resolved",
|
||||
ResolutionConfidence: "high",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2618,23 +2607,19 @@ func scoreMonitoringAliasCandidate(input monitoringAliasLookupInput, candidate m
|
||||
return 100
|
||||
}
|
||||
|
||||
if !monitoringAliasDomainMatches(input, candidate) {
|
||||
return -1
|
||||
}
|
||||
|
||||
score := 40
|
||||
if input.LastPathSegment != nil && strings.TrimSpace(*input.LastPathSegment) != "" && candidate.LastPathSegment == strings.TrimSpace(*input.LastPathSegment) {
|
||||
score += 30
|
||||
}
|
||||
if input.TitleKey != "" && candidate.TitleKey != "" {
|
||||
switch {
|
||||
case input.TitleKey == candidate.TitleKey:
|
||||
score += 30
|
||||
case strings.Contains(input.TitleKey, candidate.TitleKey) || strings.Contains(candidate.TitleKey, input.TitleKey):
|
||||
score += 15
|
||||
for _, inputKey := range input.MatchKeys {
|
||||
inputKey = strings.TrimSpace(inputKey)
|
||||
if inputKey == "" {
|
||||
continue
|
||||
}
|
||||
for _, candidateKey := range candidate.MatchKeys {
|
||||
if inputKey == strings.TrimSpace(candidateKey) && candidateKey != "" {
|
||||
return 100
|
||||
}
|
||||
}
|
||||
}
|
||||
return score
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func monitoringAliasDomainMatches(input monitoringAliasLookupInput, candidate monitoringAliasCandidate) bool {
|
||||
|
||||
Reference in New Issue
Block a user