github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/teams/opensearch/scoring.go (about)

     1  package opensearch
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  	"time"
     7  
     8  	"github.com/keybase/client/go/protocol/keybase1"
     9  )
    10  
    11  const (
    12  	minScoringMemberCount   = 0
    13  	maxScoringMemberCount   = 100000
    14  	minScoringActivityHours = 7 * 24      // one week
    15  	maxScoringActivityHours = 4 * 30 * 24 // one month
    16  	memberCountWeight       = 400
    17  	lastActiveWeight        = 20
    18  )
    19  
    20  type RankedSearchItem interface {
    21  	Score(query string) float64
    22  	String() string
    23  }
    24  
    25  type rankedSearchItem struct {
    26  	item  keybase1.TeamSearchItem
    27  	score float64
    28  }
    29  
    30  func (i rankedSearchItem) String() string {
    31  	description := ""
    32  	if i.item.Description != nil {
    33  		description = *i.item.Description
    34  	}
    35  	return fmt.Sprintf(
    36  		"Name: %s Description: %s MemberCount: %d LastActive: %v Score: %.2f isDemoted: %v",
    37  		i.item.Name, description, i.item.MemberCount,
    38  		i.item.LastActive.Time(), i.score, i.item.IsDemoted)
    39  }
    40  
    41  func (i rankedSearchItem) Score(query string) (score float64) {
    42  	query = strings.ToLower(query)
    43  	name := strings.ToLower(i.item.Name)
    44  	// demoted teams require an exact name match to be returned
    45  	if i.item.IsDemoted && query != name {
    46  		return 0
    47  	}
    48  	for _, qtok := range strings.Split(query, " ") {
    49  		score += ScoreName(name, qtok)
    50  		if i.item.Description != nil {
    51  			score += ScoreDescription(*i.item.Description, qtok)
    52  		}
    53  	}
    54  	if FilterScore(score) {
    55  		return score
    56  	}
    57  	return score + normalizeMemberCount(i.item.MemberCount)*memberCountWeight +
    58  		NormalizeLastActive(minScoringActivityHours,
    59  			maxScoringActivityHours, i.item.LastActive)*lastActiveWeight
    60  }
    61  
    62  func normalizeMemberCount(memberCount int) float64 {
    63  	if memberCount < minScoringMemberCount {
    64  		return 0
    65  	} else if memberCount > maxScoringMemberCount {
    66  		return 1
    67  	}
    68  	return float64(memberCount) / float64(maxScoringMemberCount-minScoringMemberCount)
    69  }
    70  
    71  func NormalizeLastActive(minHrs, maxHrs float64, lastActive keybase1.Time) float64 {
    72  	hours := time.Since(lastActive.Time()).Hours()
    73  	if hours > maxHrs {
    74  		return 0
    75  	} else if hours < minHrs {
    76  		return 1
    77  	}
    78  	return 1 - hours/(maxHrs-minHrs)
    79  }
    80  
    81  func FilterScore(score float64) bool {
    82  	return score-.0001 < 0
    83  }
    84  
    85  func ScoreName(name, qtok string) (score float64) {
    86  	name = strings.ToLower(name)
    87  	if qtok == name || strings.HasPrefix(name, qtok) || strings.HasSuffix(name, qtok) {
    88  		score += 1000
    89  	} else if strings.Contains(name, qtok) {
    90  		score += 100
    91  	}
    92  	return score
    93  }
    94  
    95  func ScoreDescription(desc, qtok string) (score float64) {
    96  	desc = strings.ToLower(desc)
    97  	for _, dtok := range strings.Split(desc, " ") {
    98  		if dtok == qtok {
    99  			score += 25
   100  		}
   101  	}
   102  	return score
   103  }