Redis实战之通讯录自动补全(php)

Redis 实战之通讯录补全

前言

自动补全的例子在很多场景中都能看到,如浏览器输入框的常用网址补全,或是百度搜索框的自动补全

实现功能

第一阶段:需要保留最新的100个联系人,并且能够根据前缀自动弹出关联联系人名称 ,如:to 要出现 tom,toms

第二阶段:使用有序集合来存储联系人,且不限制100人,但是只允许向同一个群组的人员发送

// 公共代码部分,以便后面不在重复写
$redis = new Redis();
$redis->connect('127.0.0.1','6379','3');
$redis->select(9);

前提条件

联系人名称都是要26个字母组成无任何特殊符号.

第一阶段功能实现

思路:最新的100个在考虑到负载的情况下,使用list列表来存储,消耗最少的存储空间,自动补全的部分由后端语言来实现,如PHP(别的也不会)

// 添加/更新联系人
function add_update_contact($conn, $user, $contact) {
    $ac_list = "recent:" . $user;
    $pipe = $conn->multi(Redis::PIPLINE);
    $pipe->lRem($ac_list, $contact, 0);		// 移除所有相同的联系人
    $pipe->lpush($ac_list, $contact);
    $pipe->ltrim($ac_list, 0, 99);			// 删除所有100个之后的联系人
    $pipe->exec();
    echo "联系人添加成功";
    return true;
}

// 移除联系人
function remove_contact($conn, $user, $contact) {
    $ac_list = "recent:" . $user;
    return $conn->lRem($ac_list, $contact, 0);
}

// $prefix 搜索前缀 
function fetch_autoComplete_list($conn, $user, $prefix) {
    $ac_list = "recent:" . $user;
    $candidates = $conn->lRange($ac_list, 0, -1);
    $matches = [];
    foreach ($candidates as $candidate) {
        if (strpos(strtolower($candidate), strtolower($prefix)) === 0) {
         	$matches[] = strtolower($candidate);   
        }
    }
    return $matches;
}

// 测试代码
add_update_contact($redis, 'mowang', 'john');
add_update_contact($redis, 'mowang', 'jojo');
add_update_contact($redis, 'mowang', 'join');
add_update_contact($redis, 'mowang', 'johns');
var_dump(fetch_autoComplete_list($redis, 'mowang', 'jo'));

第二阶段功能实现

// 加入群组
function join_guild($conn, $guild, $user) {
    $members_list = "members:" . $guild;
    $conn->zAdd($members_list, 0, $user);
    echo "加入群组成功";
    return true;
}

// 离开群组
function leave_guild($conn, $guild, $user) {
    $members_list = "members:" . $guild;
    $conn->zRem($members_list, $user);
    echo "离开群组成功";
    return true;
}

function find_prefix_range($prefix) {
    $valid_chars = "`abcdefghijklmnopqrstuvwxyz{";
    // $prefix = 'dqc'  // 输出dq
    $position = strpos($valid_chars, substr($prefix, 0, -1));
    $suffix = $valid_chars[$position > 0 ? $position - 1 : 0];
    return [
        substr($prefix, 0, -1) . $suffix . "{",
        $prefix . "{"
    ];
}

function autocomplete_on_prefix($conn, $guild, $user) {
    list($start, $end) = find_prefix_range($prefix);
    $identifier = Uuid::uuid4()->toString();
    $start .= $identifier;
    $end .= $identifier;
    $zset_name = 'members:'. $guild;
    $conn->zAdd($zset_name, 0, $start);
    $conn->zAdd($zset_name, 0, $end);
    // zrange 是闭合区间,即0,0也是有值的
    while (1) {
        try {
            $conn->watch($zset_name);
            $sindex = $conn->zRank($zset_name, $start);
            $eindex = $conn->zRank($zset_name, $end);
            $erange = min($sindex + 9, $eindex - 2);		// 最多取10条
            $trans = $conn->multi(Redis::PIPELINE);
            $trans->zrem($zset_name, $start);
            $trans->zrem($zset_name, $end);
            $trans->zRange($zset_name, $sindex, $erange);
            $baidu = $trans->exec();
            $items = end($baidu);
            break;
        } catch (\Exception $e) {
            continue;
        }
    }
    // 过滤掉带有`{`符号的数据
    return array_filter(
        $items,
        function ($item) {
            return strpos($item, '{') === false;
        }
    )
}

结语

后续会补充支持中文等不同方式的自动补全,此博文只作为个人记录用

posted @ 2021-09-23 00:15  Dan_eil  阅读(141)  评论(0编辑  收藏  举报