Fractional indexingã«ãã並ã³æ¿ãAPIã¨ãã¼ã¿æ§é
ð ð» This post is part of the Eureka Advent Calendar 2023 ð ð»
ããã«ã¡ã¯ï¼ææ¥ç´è»äºå®ã§ãããããæ¢ã¾ããªã 23æ°å Eureka Back-end Engineerã®ãºãã¼ã§ãï¼
æ¬è¨äºã§ã¯æ´æ°é »åº¦ã®é«ãã¦ã¼ã¶ã¼ã³ã³ãã³ã(以ä¸UGC)ã®é åºãä¿æããèªç±ã«ä¸¦ã³æ¿ããAPIã¨ãã¼ã¿è¨è¨ã«ã¤ãã¦èãã¦ã¿ããã¨æãã¾ãã
æ¬è¨äºã§ã¯Pairsã®ã¦ã¼ã¶ã¼ã³ã³ãã³ããã½ã¼ãããæ©è½ã®ä»£è¡¨æ ¼ã§ãããããã£ã¼ã«ç»åãä¾ã«èª¬æãã¦ããã¾ãã
Pairsã§ã¯è¤æ°ã®ãããã£ã¼ã«ç»åãè¨å®ãããã¨ãã§ãããã®ä¸ãããã©ãã°&ããããã§ã¡ã¤ã³ç»åã決ãããããµãç»åã®é åºã並ã³æ¿ããããããã¨ãã§ãã¾ãã
Pairsã®ãããã£ã¼ã«ç»åã®ä¸¦ã¹æ¿ãã§ã¯ãä¸åº¦ã®ãã©ãã°&ããããã§çæ³ã®ä¸¦ã³é ã«ãªããã¨ã¯å°ãªããæ°åã«åãã¦ãã©ãã°&ãããããè¡ããã¦ã¼ã¶ã¼ã®çæ³ã®ä¸¦ã³é ã«ãªããã¨ãä¸è¬çã§ãã
1. Background
UGCã®é åºãæ´æ°ããéã®ãã¼ã¿æ´æ°ãéå¹çã ãªã¨æã£ã¦ããã®ããã®è¨äºãæ¸ããã¨æã£ããã£ããã§ãã
Pairsã§ã¯ãã¤ã³ã¯ãªã¡ã³ã¿ã«ãªã¤ã³ããã¯ã¹ãä»ä¸ãããã¨ã§UGCã®é åºãä¿æãã¦ãã¾ãã
type UserImage struct {
ID int
UserID int
Index int // 0ãæå°ã®ä¸¦ã³é ã®ã¤ã³ããã¯ã¹
Path string // ç»åã®ãã¹
}
type UserImages []UserImage
func (e *UserImages) Sort() {
sort.Slice(e, func(i, j int) bool {
return e[i].Index < e[j].Index
})
}
var userImages = UserImages{
{ID: 1, UserID: 200, Index: 1, Path: "image1.png"},
{ID: 2, UserID: 200, Index: 2, Path: "image2.png"},
{ID: 3, UserID: 200, Index: 3, Path: "image3.png"},
{ID: 4, UserID: 200, Index: 4, Path: "image4.png"},
{ID: 5, UserID: 200, Index: 5, Path: "image5.png"},
}
é åºã®ä¸¦ã³æ¿ããè¡ãéã«ã¯ã以ä¸ã®ããã«æ°ãã並ã³é ã®é åãåãåããã¢ããªã±ã¼ã·ã§ã³å´ã§å ¨ã¦ã®ã¤ã³ããã¯ã¹ãæ¯ãç´ãã¦æ´æ°ãè¡ãªã£ã¦ãã¾ãã
ãã®ãããID: 1
ã®è¦ç´ ãæå¾ã®ç»åã«ããéã¯ã¦ã¼ã¶ã¼ç»åã®å
¨ã¤ã³ããã¯ã¹ãæ´æ°ãã¦ãã¾ãã¾ãã
curl -XPUT 'https://api/image'
-d "{\"image_ids\":[2, 3, 4, 5, 1]}"
var userImages = UserImages{
+ {ID: 2, UserID: 200, Index: 1, Path: "image2.png"},
+ {ID: 3, UserID: 200, Index: 2, Path: "image3.png"},
+ {ID: 4, UserID: 200, Index: 3, Path: "image4.png"},
+ {ID: 5, UserID: 200, Index: 4, Path: "image5.png"},
+ {ID: 1, UserID: 200, Index: 5, Path: "image1.png"},
}
æ´æ°é »åº¦ã®å°ãªããã¹ã¿ã¼ãã¼ã¿ãããã¼ã¿éãå¤ããªãå ´åã§ããã°ãç¹ã«åé¡ã¯ãªãããããã¾ããããæ´æ°é »åº¦ãé«ãå ´åã«ã¯ãããå¹ççã«ãã¼ã¿æ´æ°ãè¡ãããã§ãã
調ã¹ã¦ã¿ãã¨Figmaã§ã¯è¤æ°äººã®åæç·¨éãå¯è½ã«ããããã«Fractional indexingãå°å ¥ãã¦ãããã¨ããããã¾ãã
Pairsã®ç¹æ§ä¸ãç»åã®ä¸¦ã³æ¿ãã§ããã®ã¯ãã°ã¤ã³ã¦ã¼ã¶ã¼ã®ç»åã®ã¿ã§1ã¦ã¼ã¶ã¼ãèªèº«ã®ãªã½ã¼ã¹ã«ã·ã¼ã±ã³ã·ã£ã«ã«ã¢ã¯ã»ã¹ãããã¨ããç¹ã§Figmaã¨ã¯å¤§ããç°ãªãã¾ãã
ã¨ã¯ãããå ¨æ´æ°ããå ´åã«ã¯ãããããã¯ãå¼ãèµ·ããããããªã¯ã¨ã¹ããã¤ãã¼ããéä¿¡éã大ãããªããã¡ã§ãã
* ãªãã¯ã©ã¤ã¢ã³ãå´ã®ãªã¯ã¨ã¹ãã¿ã¤ãã³ã°ã®èª¿æ´ã«ãã£ã¦å ¨æ´æ°ã®é »åº¦ãä¸ããè² è·ãä¸ããæ¹æ³ãèãããã¾ãããæ¬ã±ã¼ã¹ã§ã¯ãããèæ ®ãã¾ããã
ãã®ç¹ã«æ³¨æããªãããããå¹ççãªä¸¦ã³æ¿ãã®APIã¨ãã¼ã¿è¨è¨ã«ã¤ãã¦èãã¦ã¿ããã¨æãã¾ãã
ããã¤ãã®ä¸¦ã³æ¿ãã®ã¢ã«ã´ãªãºã ã¨æ¯è¼ããªããæ´çãã¦ããã¾ãã
2. æ§ã ãªä¸¦ã³æ¿ãã¢ã«ã´ãªãºã
2.1. Swap
Swapã¯2è¦ç´ ã®äº¤æããããã¨ã§ä¸¦ã³æ¿ããè¡ãã¾ãã
ãã®ãããå¿
ããã¼ã¿æ´æ°å¯¾è±¡ã¯2ã«ãªããã¨ã¦ãå¹çãè¯ããã¨ããããã¾ãã
ä¸æ¹ã2è¦ç´ ã®äº¤æ以å¤ã«Swapãé©ç¨ããå ´åã¯ãã®éãã§ã¯ããã¾ããã
a, b, c, d
ã®é ã«ä¸¦ãã è¦ç´ ãããã¨ããa
ãæå¾ã«ä¸¦ã¹ããã¨ã§ãb, c, d, a
ã¨ããé åºã«ãªãããã«ããã¨ãã¾ãã
è¦ç´ ãå
¥ãæ¿ãã¦b, c, d, a
ã®é åºã«ããå ´åã¯O(N)
ã¨ãªãããããã¾ãå¹ççã¨ã¯è¨ãã¾ããã
ãã®ããã«Swapã¢ã«ã´ãªãºã ã¯2è¦ç´ ã®äº¤æã«é©ãã¦ãããéã«è¨ãã°UIã«å½±é¿ããããã¨ãå¤ãããã©ãã°&ããããã§ä¸¦ã³æ¿ããã§ããPairsã§ã¯æ¡ç¨ããã¾ããã§ããã
2.2. å ¨æ´æ°
次ã«ä¸åº¦ã§å
¨ã¦ç½®ãæããå ´åãèãã¦ã¿ã¾ãã
ãã®å ´åã«ã¯ãã¹ã¦ã®ã¤ã³ããã¯ã¹ãä¸åº¦ã«æ´æ°ããå¿
è¦ãããã¾ãã
è¦ç´ æ°ãå¤ãå ´åã«ã¯ãããããã¯ã®èªå ã«ãªã£ãããpayloadèªä½ã大ãããªããã¡ã§ãã
1ã¤ã®è¦ç´ ã®é åºãå¤ããã ããªã1ã¤ã ããæ´æ°å¯¾è±¡ã«ããã®ãçæ³çã§ãã
2.3. Fractional indexing
Fractional indexing(é¨åã¤ã³ããã¯ã¹)ã¨ã¯æ´æ°ã¤ã³ããã¯ã¹ã«ä»£ãã¦ãåæ°ãå°æ°ã使ç¨ãããã¨ã§æ¢åã®ã¤ã³ããã¯ã¹ãå¤æ´ããã«ä½ç½®ãæå®ãããã¨ãã§ããã¢ã«ã´ãªãºã ã§ãã
ã¾ãã¯æ°å¤ãã½ã¼ããã¼ã«ããä¾ãåãä¸ãã¾ãã
2.3.1. æ°å¤ãã½ã¼ããã¼ã«ãã
å
¨ã¦ã®ã¤ã³ããã¯ã¹ã0<index<1
ã®å°æ°ã«ãã¦ã0
ã¾ãã¯1
ã§å¹³åå¤ãåãããã«ãã¾ãã
å¯èªæ§ã¯ãã®ã¾ã¾ã§ãæ´æ°å¯¾è±¡ãä¸ã¤ã ãã«ãªãã¾ããã
次ã«è¦ç´ ã¨è¦ç´ ã®éã«è¦ç´ ãå
¥ããä¾ãè¦ã¦ã¿ã¾ãã
b, c
ã®ä¸éã«a
ã移åãããã¨ããb, c
ã®ã¤ã³ããã¯ã¹ã®å¹³åå¤ã«ãªã£ã¦ãããã¨ããããã¾ãã
å
¨ã¦ã®ã¤ã³ããã¯ã¹ã¯ 0.
ããå§ã¾ããããã¢ããªã±ã¼ã·ã§ã³å´ã§ã¯ 0.
ãçç¥ãã¦æååã§æ ¼ç´ãã¾ãã
æååã¨ãã¦æ±ããã¨ã§æ°å¤ãããé«ãå解è½ã§ã¤ã³ããã¯ã¹ã表ç¾ã§ããæ°å¤ã¨æ¯è¼ãã¦ãªã¼ãã¼ããã¼ãã¤ã³ããã¯ã¹ã®è¡çªãå 延ã°ãã«ã§ãã¾ãã
type UserImage struct {
ID int
UserID int
- Index int
+ Index string
Path string
}
type UserImages []UserImage
func (e *UserImages) Sort() {
sort.Slice(e, func(i, j int) bool {
return e[i].Index < e[j].Index
})
}
ãvar userImages = UserImages{
+ {ID: 1, UserID: 200, Index: "45", Path: "image1.png"},
+ {ID: 2, UserID: 200, Index: "4", Path: "image2.png"},
+ {ID: 3, UserID: 200, Index: "5", Path: "image3.png"},
+ {ID: 4, UserID: 200, Index: "6", Path: "image4.png"},
+ {ID: 5, UserID: 200, Index: "7", Path: "image5.png"},
ã}
ä¸æ¹ãæååã§ä¿åããå ´åã«ã¯æ°å¤ã«å¤æããã«å¹³åå¤ãæ±ããå¿ è¦ãããã¾ãã
ã¾ãã並ã³æ¿ããä½åº¦ãç¹°ãè¿ããå ´åã«ã¯ä¸éè¶ éã«ãã£ã¦ä¸¸ããããè¡çªãçºçãã¾ãã
è¡çªããå ´åãã¤ã³ããã¯ã¹ãå度æ¯ãç´ãå¿ è¦ããããããããªãã©ã³ã·ã³ã°ã¨å¼ã³ã¾ãã
ãã¼ã¿ã¬ã³ã¸ãåºããã¨ãªãã©ã³ã·ã³ã°ã®é »åº¦ã¯ä½ããªãã¾ãã
ãªãã©ã³ã·ã³ã°ã¯ã³ã¹ãã®é«ãå¦çã«ãªãããããªãã©ã³ã·ã³ã°ã®é »åº¦ãæããã¹ããæååãã½ã¼ããã¼ã«ãã¦ã¿ã¾ãã
2.3.2. æååãã½ã¼ããã¼ã«ãã
ä¾ãã°16é²æ°ãæ¡ç¨ãã¦ã¿ãã¨10é²æ°ã®æã¨æ¯ã¹ã¦ãä¸æ¡ããã6éãå¢ãããã¨ã«ãªãããã®åãã¼ã¿ã¬ã³ã¸ãåºããã¾ãã
Figmaã§æ¡ç¨ãããã®ã¯base95ã§ãããbase95ãæ¡ç¨ããã¨10é²æ°ã¨æ¯è¼ãã¦ä¸æ¡ããã85éãå¢ãããã¨ã«ãªãããã®åãã¼ã¿ã¬ã³ã¸ãåºãããã¨ã«ãªãã¾ãã
ãã¡ããbase95ã«ã¨ã³ã³ã¼ãããåã®ãã¤ããªã®ã¾ã¾ä¿åãããã¨ãã§ãã¾ãããDatabaseã®ã«ã©ã å¶éããããã°æã®å¯èªæ§ããã¬ã¼ããªãã«ãªãã¾ãã
ã¾ããã¯ã©ã¤ã¢ã³ãã«ã½ã¼ããã¼ãã¤ã³ããã¯ã¹ããã®ã¾ã¾ä½¿ç¨ãã¦è¿ãå ´åãprintable ascii
以å¤ãå«ãbase95以ä¸ãæ¡ç¨ããã¨ã¯ã©ã¤ã¢ã³ãå´ã«ä½ããã®ä¸å
·åãèµ·ãã¦ãã¾ããã¨ãèãããã¾ãã
3. Fractional indexãæ¡ç¨ããAPI
次ã«Fractional indexãæ¡ç¨ããå ´åã®APIã®ã¤ã³ã¿ã¼ãã§ã¼ã¹ãèãã¦ã¿ã¾ãã
Requestã«ã¯æ´æ°å¯¾è±¡ã®IDã¨åå¾ã®IDãåãåãã¾ããPrevID
ãnull
ã®æã¯å
é ãNextID
ãnull
ã®æã¯æ«å°¾ã«è¿½å ãã¾ãã
type ReorderRequest struct {
UserImageID int `json:"user_image_id"`
PrevID *int `json:"prev_id"`
NextID *int `json:"next_id"`
}
a, b, c, d
ã®ID
ããããã1, 2, 3, 4
ã¨ããã¨ããä¸è¨ã®ãããªRequestã§è¡¨ç¾ã§ãã¾ãã
curl -XPUT 'https://api/images/reoder' \
-d "{\"user_image_id\": 1, \"prev_id\": 2, \"next_id\": 3}"
ãã®ããã«å ¨ã¦ã®ä¸¦ã³é ããã¤ãã¼ãã«å¿ è¦ã¨ããªããããRequestæã®ãã¼ã¿éãåæ¸ã§ãã¾ãã
å
é¨ã§ã¯ãPrevID
, NextID
ããããã®ã¤ã³ããã¯ã¹ãåå¾ãã¦æ´æ°å¯¾è±¡ã®æ°ããã¤ã³ããã¯ã¹ãæååã®ã¾ã¾è¨ç®ãã¾ãã
è¡çªããå ´åã®ã¿ããªãã©ã³ã·ã³ã°ãå¿ è¦ã«ãªãã¾ãããã»ã¨ãã©ã®å ´åã¯ãªãã©ã³ã·ã³ã°ãå¿ è¦ãªãã1è¦ç´ ã®ã¿ãæ´æ°ããããã¨ã¦ãå¹ççã§ãã
ã¾ããä¸è¨ä¾ã§ã¯åæçã«ãªãã©ã³ã·ã³ã°ãè¡ã£ã¦ãã¾ãããäºããªãã©ã³ã·ã³ã°ãå¿ è¦ãªå ´åãæ¤ç¥ããéåæå¦çã«éããã¨ã§è² è·ã軽æ¸ãããã¨ãã§ãã¾ãã
func Reorder(ctx context.Context, userID int, req ReorderRequest) error {
// å
é ã«ç§»åããå ´åã¯äºã決ããæå°å¤ã¨req.NextIDã®å¹³åå¤ãè¨ç®ãã
if req.BringTop() {
return bringTop(ctx, userID, req)
}
// æ«å°¾ã«ç§»åããå ´åã¯äºã決ããæ大å¤ã¨req.PrevIDã®å¹³åå¤ãè¨ç®ãã
if req.BringBottom() {
return bringBottom(ctx, userID, req)
}
images, err := repository.GetByIDs(ctx, userID, []int{req.UserImageID, req.PrevID, req.NextID})
if err != nil {
return err
}
// ç´ã¥ãç»åãåå¨ããªãå ´å.
if len(images) != 3 {
return errors.New("not found")
}
imageMap := images.ToIDMap()
midIndex := image.CalculateMidpoint(imageMap[req.PrevID], imageMap[req.NextID])
// è¡çªãã¦ããå ´åã¯å
¨å¯¾è±¡ã«ã¤ã³ããã¯ã¹ãåå²ãå½ã¦ãããªãã©ã³ã¹ãè¡ã.
if midIndex == imageMap[req.PrevID].Index ||
midIndex == imageMap[req.NextID].Index {
return rebalance(ctx, userID)
}
imageMap[req.UserImageID].Index = midIndex
return repository.Update(ctx, imageMap[req.UserImageID])
}
// package image
func CalculateMidpoint(prevIndex, nextIndex string) string {
next := []byte(nextIndex)
prev := make([]byte, len(next))
// æååæä½ã®ããã«nextã®é·ãã«åããã
for i, j := len(prevIndex)-1, len(next)-1; i >= 0; i-- {
prev[j] = prevIndex[i]
j--
}
return add(divideByTwo(prev), divideByTwo(next))
}
ã¾ã¨ã
æ¬è¨äºã§ã¯ã並ã³æ¿ãAPIã¨ãã¼ã¿æ§é ã«Fractional indexingãæ¡ç¨ãããã¨ã§ãã¼ã¿æ´æ°ãæå°éã«ã§ãããã¨ããããã¾ããã
ã¾ã ã¾ã å°å ¥äºä¾ãå°ãªãFractional indexingã§ãããã¢ããªã±ã¼ã·ã§ã³ã®å±æ§ã¨ãªãã©ã³ã·ã³ã°ã®é »åº¦ãèãããã¨ã§ã並ã³æ¿ãã®ã¦ã¼ã¹ã±ã¼ã¹ã«æé©ãªãã¼ã¿æ§é ã¨APIã®ã¤ã³ã¿ã¼ãã§ã¼ã¹ã決ãããã¨ãã§ãããã§ãã
ãµã³ãã«å®è£ ã®ãªãã¸ããªãå ¬éã§ããããã«ãªã£ããæ¹ãã¦ã·ã§ã¢ãããã¨æãã¾ãã