diff --git a/app/rpc.go b/app/rpc.go index f1800642..69905373 100644 --- a/app/rpc.go +++ b/app/rpc.go @@ -45,6 +45,28 @@ func (app *CyberdApp) Search(cid string, page, perPage int) ([]RankedCid, int, e return result, size, nil } +func (app *CyberdApp) Backlinks(cid string, page, perPage int) ([]RankedCid, int, error) { + + ctx := app.RpcContext() + cidNumber, exists := app.cidNumKeeper.GetCidNumber(ctx, link.Cid(cid)) + if !exists || cidNumber > app.rankStateKeeper.GetLastCidNum() { + return nil, 0, errors.New("no such cid found") + } + + rankedCidNumbers, size, err := app.rankStateKeeper.Backlinks(cidNumber, page, perPage) + + if err != nil { + return nil, size, err + } + + result := make([]RankedCid, 0, len(rankedCidNumbers)) + for _, c := range rankedCidNumbers { + result = append(result, RankedCid{Cid: app.cidNumKeeper.GetCid(ctx, c.GetNumber()), Rank: c.GetRank()}) + } + + return result, size, nil +} + func (app *CyberdApp) Top(page, perPage int) ([]RankedCid, int, error) { ctx := app.RpcContext() @@ -88,6 +110,25 @@ func (app *CyberdApp) AccountBandwidth(address sdk.AccAddress) bw.AccountBandwid return app.bandwidthMeter.GetCurrentAccBandwidth(app.RpcContext(), address) } +func (app *CyberdApp) AccountLinks(address sdk.AccAddress, page, perPage int) ([]link.Link, int, error) { + ctx := app.RpcContext() + + acc := app.accountKeeper.GetAccount(ctx, address) + + if acc != nil { + accNumber := cbd.AccNumber(acc.GetAccountNumber()) + links, total, _ := app.rankStateKeeper.Accounts(uint64(accNumber), page, perPage) + + result := make([]link.Link, 0, len(links)) + for j, c := range links { + result = append(result, link.Link{From: app.cidNumKeeper.GetCid(ctx, j), To: app.cidNumKeeper.GetCid(ctx, c)}) + } + return result, total, nil + } else { + return nil, 0, nil + } +} + func (app *CyberdApp) IsLinkExist(from link.Cid, to link.Cid, address sdk.AccAddress) bool { ctx := app.RpcContext() diff --git a/cmd/cyberd/rpc/account_links.go b/cmd/cyberd/rpc/account_links.go new file mode 100644 index 00000000..fe250351 --- /dev/null +++ b/cmd/cyberd/rpc/account_links.go @@ -0,0 +1,32 @@ +package rpc + +import ( + "github.com/cosmos/cosmos-sdk/types" + + "github.com/cybercongress/go-cyber/x/link" + + rpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types" +) + +type ResultAccountLinks struct { + Links []link.Link `json:"links"` + TotalCount int `json:"total"` + Page int `json:"page"` + PerPage int `json:"perPage"` +} + +func AccountLinks(ctx *rpctypes.Context, address string, page, perPage int) (*ResultAccountLinks, error) { + if perPage == 0 { + perPage = 100 + } + + accAddress, err := types.AccAddressFromBech32(address) + if err != nil { + return nil, err + } + + links, totalSize, err := cyberdApp.AccountLinks(accAddress, page, perPage) + return &ResultAccountLinks{links, totalSize, page, perPage}, err +} + + diff --git a/cmd/cyberd/rpc/account_test.go b/cmd/cyberd/rpc/account_test.go deleted file mode 100644 index 9ab1e3e8..00000000 --- a/cmd/cyberd/rpc/account_test.go +++ /dev/null @@ -1 +0,0 @@ -package rpc diff --git a/cmd/cyberd/rpc/link.go b/cmd/cyberd/rpc/link.go index 2702d353..a89a96a2 100644 --- a/cmd/cyberd/rpc/link.go +++ b/cmd/cyberd/rpc/link.go @@ -1,11 +1,10 @@ package rpc import ( - "encoding/json" "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/auth" - "github.com/cybercongress/go-cyber/x/link" rpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types" + + "github.com/cybercongress/go-cyber/x/link" ) func IsLinkExist(ctx *rpctypes.Context, from string, to string, address string) (bool, error) { @@ -21,30 +20,3 @@ func IsLinkExist(ctx *rpctypes.Context, from string, to string, address string) return cyberdApp.IsLinkExist(link.Cid(from), link.Cid(to), accAddress), nil } - -type LinkRequest struct { - Fee auth.StdFee `json:"fee"` - Msgs []link.Msg `json:"msgs"` - Signatures []Signature `json:"signatures"` - Memo string `json:"memo"` -} - -func (r LinkRequest) GetFee() auth.StdFee { return r.Fee } -func (r LinkRequest) GetSignatures() []Signature { return r.Signatures } -func (r LinkRequest) GetMemo() string { return r.Memo } -func (r LinkRequest) GetMsgs() []types.Msg { - msgs := make([]types.Msg, 0, len(r.Msgs)) - for _, msg := range r.Msgs { - msgs = append(msgs, msg) - } - return msgs -} - -func UnmarshalLinkRequestFn(reqBytes []byte) (TxRequest, error) { - var req LinkRequest - err := json.Unmarshal(reqBytes, &req) - if err != nil { - return nil, err - } - return req, nil -} diff --git a/cmd/cyberd/rpc/rank.go b/cmd/cyberd/rpc/rank.go index 977b541a..673ed5ea 100644 --- a/cmd/cyberd/rpc/rank.go +++ b/cmd/cyberd/rpc/rank.go @@ -1,6 +1,8 @@ package rpc import ( + rpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types" + "github.com/cybercongress/go-cyber/merkle" ) @@ -9,7 +11,7 @@ type RankAndProofResult struct { Rank float64 `amino:"unsafe" json:"rank"` } -func Rank(cid string, proof bool) (*RankAndProofResult, error) { +func Rank(ctx *rpctypes.Context, cid string, proof bool) (*RankAndProofResult, error) { rankValue, proofs, err := cyberdApp.Rank(cid, proof) return &RankAndProofResult{proofs, rankValue}, err } diff --git a/cmd/cyberd/rpc/routes.go b/cmd/cyberd/rpc/routes.go index c546cd4b..64846941 100644 --- a/cmd/cyberd/rpc/routes.go +++ b/cmd/cyberd/rpc/routes.go @@ -15,22 +15,16 @@ func SetCyberdApp(cApp *app.CyberdApp) { var Routes = map[string]*rpcserver.RPCFunc{ "search": rpcserver.NewRPCFunc(Search, "cid,page,perPage"), + "backlinks": rpcserver.NewRPCFunc(Backlinks, "cid,page,perPage"), "top": rpcserver.NewRPCFunc(Top, "page,perPage"), "rank": rpcserver.NewRPCFunc(Rank, "cid,proof"), "account": rpcserver.NewRPCFunc(Account, "address"), "account_bandwidth": rpcserver.NewRPCFunc(AccountBandwidth, "address"), + "account_links": rpcserver.NewRPCFunc(AccountLinks, "address,page,perPage"), "is_link_exist": rpcserver.NewRPCFunc(IsLinkExist, "from,to,address"), "current_bandwidth_price": rpcserver.NewRPCFunc(CurrentBandwidthPrice, ""), "current_network_load": rpcserver.NewRPCFunc(CurrentNetworkLoad, ""), "index_stats": rpcserver.NewRPCFunc(IndexStats, ""), - - // TODO remove this for euler-6 release - "staking/validators": rpcserver.NewRPCFunc(StakingValidators, "page,limit,status"), - "staking/pool": rpcserver.NewRPCFunc(StakingPool, ""), - - // TODO remove this for euler-6 release - "submit_signed_link": rpcserver.NewRPCFunc(SignedMsgHandler(UnmarshalLinkRequestFn), "data"), - "submit_signed_send": rpcserver.NewRPCFunc(SignedMsgHandler(UnmarshalSendRequestFn), "data"), } func init() { diff --git a/cmd/cyberd/rpc/search.go b/cmd/cyberd/rpc/search.go index c04ba62b..14d625d1 100644 --- a/cmd/cyberd/rpc/search.go +++ b/cmd/cyberd/rpc/search.go @@ -19,3 +19,12 @@ func Search(ctx *rpctypes.Context, cid string, page, perPage int) (*ResultSearch links, totalSize, err := cyberdApp.Search(cid, page, perPage) return &ResultSearch{links, totalSize, page, perPage}, err } + +func Backlinks(ctx *rpctypes.Context, cid string, page, perPage int) (*ResultSearch, error) { + if perPage == 0 { + perPage = 100 + } + links, totalSize, err := cyberdApp.Backlinks(cid, page, perPage) + return &ResultSearch{links, totalSize, page, perPage}, err +} + diff --git a/cmd/cyberd/rpc/staking.go b/cmd/cyberd/rpc/staking.go deleted file mode 100644 index 83c8e935..00000000 --- a/cmd/cyberd/rpc/staking.go +++ /dev/null @@ -1,46 +0,0 @@ -package rpc - -import ( - "github.com/cosmos/cosmos-sdk/x/staking" - sdk "github.com/cosmos/cosmos-sdk/x/staking/types" - abci "github.com/tendermint/tendermint/abci/types" - rpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types" -) - -func StakingValidators(ctx *rpctypes.Context, page, limit int, status string) ([]sdk.Validator, error) { - - queryValsParams := staking.NewQueryValidatorsParams(page, limit, status) - bz, err := codec.MarshalJSON(queryValsParams) - if err != nil { - return nil, err - } - - respQuery := cyberdApp.Query(abci.RequestQuery{ - Path: "custom/staking/validators", - Data: bz, - }) - - validators := make([]sdk.Validator, 0) - err = codec.UnmarshalJSON(respQuery.Value, &validators) - if err != nil { - return nil, err - } - - return validators, nil -} - -func StakingPool(ctx *rpctypes.Context) (staking.Pool, error) { - - respQuery := cyberdApp.Query(abci.RequestQuery{ - Path: "custom/staking/pool", - Prove: false, - }) - - pool := staking.Pool{} - err := codec.UnmarshalJSON(respQuery.Value, &pool) - if err != nil { - return pool, err - } - - return pool, nil -} diff --git a/cmd/cyberd/rpc/tokens.go b/cmd/cyberd/rpc/tokens.go deleted file mode 100644 index b0864367..00000000 --- a/cmd/cyberd/rpc/tokens.go +++ /dev/null @@ -1,35 +0,0 @@ -package rpc - -import ( - "encoding/json" - "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/auth" - "github.com/cosmos/cosmos-sdk/x/bank" -) - -type SendRequest struct { - Fee auth.StdFee `json:"fee"` - Msgs []bank.MsgMultiSend `json:"msgs"` - Signatures []Signature `json:"signatures"` - Memo string `json:"memo"` -} - -func (r SendRequest) GetFee() auth.StdFee { return r.Fee } -func (r SendRequest) GetSignatures() []Signature { return r.Signatures } -func (r SendRequest) GetMemo() string { return r.Memo } -func (r SendRequest) GetMsgs() []types.Msg { - msgs := make([]types.Msg, 0, len(r.Msgs)) - for _, msg := range r.Msgs { - msgs = append(msgs, msg) - } - return msgs -} - -func UnmarshalSendRequestFn(reqBytes []byte) (TxRequest, error) { - var req SendRequest - err := json.Unmarshal(reqBytes, &req) - if err != nil { - return nil, err - } - return req, nil -} diff --git a/x/rank/exported/exported.go b/x/rank/exported/exported.go index 259a5f79..7ac52dd9 100644 --- a/x/rank/exported/exported.go +++ b/x/rank/exported/exported.go @@ -19,6 +19,8 @@ type StateKeeper interface { EndBlocker(sdk.Context, log.Logger) Search(cidNumber link.CidNumber, page, perPage int) ([]types.RankedCidNumber, int, error) + Backlinks(cidNumber link.CidNumber, page, perPage int) ([]types.RankedCidNumber, int, error) + Accounts(account uint64, page, perPage int) (map[link.CidNumber]link.CidNumber, int, error) Top(page, perPage int) ([]types.RankedCidNumber, int, error) GetRankValue(link.CidNumber) float64 diff --git a/x/rank/internal/keeper/keeper.go b/x/rank/internal/keeper/keeper.go index 57a5e00f..cf53765c 100644 --- a/x/rank/internal/keeper/keeper.go +++ b/x/rank/internal/keeper/keeper.go @@ -152,10 +152,18 @@ func (s *StateKeeper) Search(cidNumber link.CidNumber, page, perPage int) ([]typ return s.index.Search(cidNumber, page, perPage) } +func (s *StateKeeper) Backlinks(cidNumber link.CidNumber, page, perPage int) ([]types.RankedCidNumber, int, error) { + return s.index.Backlinks(cidNumber, page, perPage) +} + func (s *StateKeeper) Top(page, perPage int) ([]types.RankedCidNumber, int, error) { return s.index.Top(page, perPage) } +func (s *StateKeeper) Accounts(account uint64, page, perPage int) (map[link.CidNumber]link.CidNumber, int, error) { + return s.index.Accounts(account, page, perPage) +} + func (s *StateKeeper) GetRankValue(cidNumber link.CidNumber) float64 { return s.index.GetRankValue(cidNumber) } diff --git a/x/rank/internal/types/index.go b/x/rank/internal/types/index.go index 891cfe71..a5a2dd41 100644 --- a/x/rank/internal/types/index.go +++ b/x/rank/internal/types/index.go @@ -7,20 +7,23 @@ import ( "github.com/tendermint/tendermint/libs/log" + "github.com/cybercongress/go-cyber/types" "github.com/cybercongress/go-cyber/x/link" ) type BaseSearchIndex struct { - links []cidLinks - rank Rank + links []cidLinks + backlinks []cidLinks + accounts map[types.AccNumber]map[link.CidNumber]link.CidNumber + rank Rank - linksChan chan link.CompactLink - rankChan chan Rank - errChan chan error + linksChan chan link.CompactLink + rankChan chan Rank + errChan chan error - locked bool + locked bool - logger log.Logger + logger log.Logger } func NewBaseSearchIndex(log log.Logger) *BaseSearchIndex { @@ -45,15 +48,30 @@ func (i *BaseSearchIndex) Load(links link.Links) { startTime := time.Now() i.lock() // lock index for read + i.links = make([]cidLinks, 0, 1000000) + i.backlinks = make([]cidLinks, 0, 1000000) + i.accounts = make(map[types.AccNumber]map[link.CidNumber]link.CidNumber) + for from, toCids := range links { i.extendIndex(uint64(from)) - for to := range toCids { + for to, acc := range toCids { + for j := range acc { + if i.accounts[j] == nil { + i.accounts[j] = make(map[link.CidNumber]link.CidNumber) + } + i.accounts[j][from] = to + i.logger.Info("[ACC]: ", i.accounts[j]) + } i.putLinkIntoIndex(from, to) + + i.extendReverseIndex(uint64(to)) + i.putBacklinkIntoIndex(from, to) } } - i.logger.Info("Search index loaded!", "time", time.Since(startTime)) + + i.logger.Info("The node search index is loaded", "time", time.Since(startTime)) } func (i *BaseSearchIndex) PutNewLinks(links []link.CompactLink) { @@ -99,9 +117,78 @@ func (i *BaseSearchIndex) Search(cidNumber link.CidNumber, page, perPage int) ([ return resultSet, totalSize, nil } +func (i *BaseSearchIndex) Backlinks(cidNumber link.CidNumber, page, perPage int) ([]RankedCidNumber, int, error) { + + i.logger.Info("Backlinks query", "cid", cidNumber, "page", page, "perPage", perPage) + + if i.locked { + return nil, 0, errors.New("The search index is currently unavailable after node restart") + } + + if uint64(cidNumber) >= uint64(len(i.backlinks)) { + return []RankedCidNumber{}, 0, nil + } + + cidLinks := i.backlinks[cidNumber] + if cidLinks.sortedLinks == nil || len(cidLinks.sortedLinks) == 0 { + return []RankedCidNumber{}, 0, nil + } + + totalSize := len(cidLinks.sortedLinks) + startIndex := page * perPage + if startIndex >= totalSize { + return nil, totalSize, errors.New("page not found") + } + + endIndex := startIndex + perPage + if endIndex > totalSize { + endIndex = startIndex + (totalSize % perPage) + } + + resultSet := cidLinks.sortedLinks[startIndex:endIndex] + + return resultSet, totalSize, nil +} + +func (i *BaseSearchIndex) Accounts(account uint64, page, perPage int) (map[link.CidNumber]link.CidNumber, int, error) { + + i.logger.Info("Accounts links query", "account", account, "page", page, "perPage", perPage) + + if i.locked { + return nil, 0, errors.New("The search index is currently unavailable after node restart") + } + + //if uint64(cidNumber) >= uint64(len(i.backlinks)) { + // return []RankedCidNumber{}, 0, nil + //} + // + //cidLinks := i.backlinks[cidNumber] + //if cidLinks.sortedLinks == nil || len(cidLinks.sortedLinks) == 0 { + // return []RankedCidNumber{}, 0, nil + //} + // + //totalSize := len(cidLinks.sortedLinks) + //startIndex := page * perPage + //if startIndex >= totalSize { + // return nil, totalSize, errors.New("page not found") + //} + // + //endIndex := startIndex + perPage + //if endIndex > totalSize { + // endIndex = startIndex + (totalSize % perPage) + //} + // + //resultSet := cidLinks.sortedLinks[startIndex:endIndex] + // + //return resultSet, totalSize, nil + totalSize := len(i.accounts[types.AccNumber(account)]) + + return i.accounts[types.AccNumber(account)], totalSize, nil +} + func (i *BaseSearchIndex) Top(page, perPage int) ([]RankedCidNumber, int, error) { if i.locked { - return nil, 0, errors.New("search index currently unavailable after node restart") + return nil, 0, errors.New("The search index is currently unavailable after node restart") } totalSize := len(i.rank.TopCIDs) @@ -137,6 +224,22 @@ func (i *BaseSearchIndex) handleLink(link link.CompactLink) { } } +func (i *BaseSearchIndex) handleBacklink(link link.CompactLink) { + + i.extendReverseIndex(uint64(link.To())) + + toIndex := i.backlinks[link.To()] + // in case unlock signal received we could operate on this index otherwise put link in the end of queue and finish + select { + case _ = <-toIndex.unlockSignal: + i.putBacklinkIntoIndex(link.From(), link.To()) + toIndex.Unlock() + break + default: + i.linksChan <- link + } +} + func (i *BaseSearchIndex) GetRankValue(cid link.CidNumber) float64 { if i.rank.Values == nil || uint64(len(i.rank.Values)) <= uint64(cid) { return 0 @@ -155,9 +258,19 @@ func (i *BaseSearchIndex) extendIndex(fromCidNumber uint64) { } } +func (i *BaseSearchIndex) extendReverseIndex(fromCidNumber uint64) { + indexLen := uint64(len(i.backlinks)) + if fromCidNumber >= indexLen { + for j := indexLen; j <= fromCidNumber; j++ { + backlinks := NewCidLinks() + backlinks.Unlock() // allow operations on this index + i.backlinks = append(i.backlinks, backlinks) + } + } +} + func (i *BaseSearchIndex) putLinkIntoIndex(from link.CidNumber, to link.CidNumber) { fromLinks := i.links[uint64(from)].sortedLinks - // todo: not optimal. replace with some another implementation. may be AVL tree rankedTo := RankedCidNumber{to, i.GetRankValue(to)} pos := sort.Search(len(fromLinks), func(i int) bool { return fromLinks[i].rank < rankedTo.rank }) fromLinks = append(fromLinks, RankedCidNumber{}) @@ -166,6 +279,16 @@ func (i *BaseSearchIndex) putLinkIntoIndex(from link.CidNumber, to link.CidNumbe i.links[uint64(from)].sortedLinks = fromLinks } +func (i *BaseSearchIndex) putBacklinkIntoIndex(from link.CidNumber, to link.CidNumber) { + toLinks := i.backlinks[uint64(to)].sortedLinks + rankedFrom := RankedCidNumber{from, i.GetRankValue(from)} + pos := sort.Search(len(toLinks), func(i int) bool { return toLinks[i].rank < rankedFrom.rank }) + toLinks = append(toLinks, RankedCidNumber{}) + copy(toLinks[pos+1:], toLinks[pos:]) + toLinks[pos] = rankedFrom + i.backlinks[uint64(to)].sortedLinks = toLinks +} + // for parallel usage func (i *BaseSearchIndex) startListenNewLinks() { defer func() { @@ -174,10 +297,11 @@ func (i *BaseSearchIndex) startListenNewLinks() { } }() - i.logger.Info("Search index starting listen new links") + i.logger.Info("The search index is starting to listen to new links") for { link := <-i.linksChan i.handleLink(link) + i.handleBacklink(link) } } @@ -189,9 +313,9 @@ func (i *BaseSearchIndex) startListenNewRank() { } }() - i.logger.Info("Search index starting listen new rank") + i.logger.Info("The search index is starting to listen to new rank") for { - rank := <-i.rankChan //todo: could be problems if recalculation lasts more than rank period + rank := <-i.rankChan // TODO could be problems if recalculation lasts more than rank period i.rank = rank i.recalculateIndices() } @@ -199,9 +323,9 @@ func (i *BaseSearchIndex) startListenNewRank() { func (i *BaseSearchIndex) recalculateIndices() { defer i.unlock() - n := len(i.links) // fix index length to avoid concurrency modification + n := len(i.links) // TODO: fix index length to avoid concurrency modification - // todo: run in parallel + // TODO: run in parallel for j := 0; j < n; j++ { <-i.links[j].unlockSignal // wait till some operations done on this index @@ -216,6 +340,24 @@ func (i *BaseSearchIndex) recalculateIndices() { i.links[j].sortedLinks = newSortedLinks i.links[j].Unlock() } + + // same process for backlinks + n = len(i.backlinks) + + for j := 0; j < n; j++ { + + <-i.backlinks[j].unlockSignal // wait till some operations done on this index + + currentSortedLinks := i.backlinks[j].sortedLinks + newSortedLinks := make(sortableCidNumbers, 0, len(currentSortedLinks)) + for _, cidNumber := range currentSortedLinks { + newRankedCid := RankedCidNumber{cidNumber.number, i.GetRankValue(cidNumber.number)} + newSortedLinks = append(newSortedLinks, newRankedCid) + } + sort.Stable(sort.Reverse(newSortedLinks)) + i.backlinks[j].sortedLinks = newSortedLinks + i.backlinks[j].Unlock() + } } func (i *BaseSearchIndex) lock() { diff --git a/x/rank/internal/types/index_types.go b/x/rank/internal/types/index_types.go index 99946aa4..890fdb28 100644 --- a/x/rank/internal/types/index_types.go +++ b/x/rank/internal/types/index_types.go @@ -48,6 +48,8 @@ type SearchIndex interface { Run() GetError Load(links link.Links) Search(cidNumber link.CidNumber, page, perPage int) ([]RankedCidNumber, int, error) + Backlinks(cidNumber link.CidNumber, page, perPage int) ([]RankedCidNumber, int, error) + Accounts(account uint64, page, perPage int) (map[link.CidNumber]link.CidNumber, int, error) Top(page, perPage int) ([]RankedCidNumber, int, error) GetRankValue(cidNumber link.CidNumber) float64 PutNewLinks(links []link.CompactLink) @@ -64,10 +66,16 @@ func (i NoopSearchIndex) Run() GetError { func (i NoopSearchIndex) Load(links link.Links) {} func (i NoopSearchIndex) Search(cidNumber link.CidNumber, page, perPage int) ([]RankedCidNumber, int, error) { - return nil, 0, errors.New("search is not enabled on this node") + return nil, 0, errors.New("The search API is not enabled on this node") +} +func (i NoopSearchIndex) Backlinks(cidNumber link.CidNumber, page, perPage int) ([]RankedCidNumber, int, error) { + return nil, 0, errors.New("The search API is not enabled on this node") +} +func (i NoopSearchIndex) Accounts(account uint64, page, perPage int) (map[link.CidNumber]link.CidNumber, int, error) { + return nil, 0, errors.New("The search API is not enabled on this node") } func (i NoopSearchIndex) Top(page, perPage int) ([]RankedCidNumber, int, error) { - return nil, 0, errors.New("search and top is not enabled on this node") + return nil, 0, errors.New("The search API is not enabled on this node") } func (i NoopSearchIndex) PutNewLinks(links []link.CompactLink) {} func (i NoopSearchIndex) PutNewRank(rank Rank) {}