如何使用Redis将搜索文本与其他条件组合?

4

我成功地使用Redis编写了一个文本搜索和其他条件的交集。为了实现这一点,我使用了Lua脚本。问题在于,我不仅从该脚本中读取值,还要写入值。从Redis 3.2开始,可以通过调用redis.replicate_commands()来实现,但在3.2之前不行。

以下是我存储值的方式。

名称

> HSET product:name 'Cool product' 1
> HSET product:name 'Nice product' 2

价格

> ZADD product:price 49.90 1
> ZADD product:price 54.90 2

例如,为了获取所有匹配'ice'的产品,我需要调用以下代码:

> HSCAN product:name 0 MATCH *ice*

然而,由于HSCAN使用游标,我需要多次调用它来获取所有结果。这就是我使用Lua脚本的地方:

local cursor = 0
local fields = {}
local ids = {}
local key = 'product:name'
local value = '*' .. ARGV[1] .. '*'

repeat
    local result = redis.call('HSCAN', key, cursor, 'MATCH', value)
    cursor = tonumber(result[1])
    fields = result[2]
    for i, id in ipairs(fields) do
        if i % 2 == 0 then
            ids[#ids + 1] = id
        end
    end
until cursor == 0
return ids

由于不可能在另一个调用中使用脚本的结果,例如 SADD key EVAL(SHA) ...。而且,在脚本中也不能使用全局变量。我已更改字段循环内部的部分,以访问脚本外部的ID列表:

if i % 2 == 0 then
    ids[#ids + 1] = id
    redis.call('SADD', KEYS[1], id)
end

我必须在第一行添加redis.replicate_commands()。通过这个改变,当我调用脚本时可以从传递的键中获取所有ID(参见KEYS[1])。

最后,要获取价格在40到50之间、名称包含“ice”的100个产品 ID 列表,我会执行以下操作:

> ZUNIONSTORE tmp:price 1 product:price WEIGHTS 1
> ZREMRANGEBYSCORE tmp:price 0 40
> ZREMRANGEBYSCORE tmp:price 50 +INF
> EVALSHA b81c2b... 1 tmp:name ice
> ZINTERSTORE tmp:result tmp:price tmp:name
> ZCOUNT tmp:result -INF +INF
> ZRANGE tmp:result 0 100

我使用ZCOUNT命令提前知道有多少结果页,以count / 100计算。

如我之前所说,这在Redis 3.2上运行得很好。但是当我尝试在只支持Redis 2.8的AWS上运行代码时,它就无法工作了。我不确定如何在没有使用脚本或从脚本中编写的情况下迭代HSCAN游标。有没有办法使其在Redis 2.8上运行?

一些注意事项:

  1. 我知道可以在Redis外部进行部分处理(例如迭代游标或交集匹配),但这会影响应用程序的整体性能。
  2. 我不想部署自己的Redis实例以使用版本3.2。
  3. 以上标准(价格范围和名称)仅是为了保持简单起见的示例。我有其他字段和类型的匹配,不仅仅是这些。
  4. 我不确定我存储数据的方式是否是最佳方式。我愿意听取建议。

@KevinChristopherHenry,以下调用需要复制:redis.call('SADD', KEYS[1], id) - Gustavo Straube
2个回答

2

我发现这里唯一的问题是在lua脚本中存储值。因此,不要将它们存储在lua中,而是将该值取出(返回字符串数组的值)。使用sadd(key,members [])在不同的调用中将其存储在集合中。然后进行交集操作并返回结果。

> ZUNIONSTORE tmp:price 1 product:price WEIGHTS 1
> ZREVRANGEBYSCORE tmp:price 0 40
> ZREVRANGEBYSCORE tmp:price 50 +INF
> nameSet[] = EVALSHA b81c2b... 1 ice 
> SADD tmp:name nameSet
> ZINTERSTORE tmp:result tmp:price tmp:name
> ZCOUNT tmp:result -INF +INF
> ZRANGE tmp:result 0 100

我认为你的设计是最优秀的。我的建议是尽可能使用pipeline,因为它可以一次性处理所有内容。

希望这能有所帮助。

更新 在lua中没有像数组([ ])这样的东西,您必须使用lua表来实现它。在您的脚本中,您正在返回ids,那本身就是一个数组,您可以将其用作单独的调用以实现sadd。

String [] nameSet = (String[]) evalsha b81c2b... 1 ice -> This is in java
SADD tmp:name nameSet

相应的Lua脚本与您的第一个脚本相同。

local cursor = 0
local fields = {}
local ids = {}
local key = 'product:name'
local value = '*' .. ARGV[1] .. '*'

repeat
    local result = redis.call('HSCAN', key, cursor, 'MATCH', value)
    cursor = tonumber(result[1])
    fields = result[2]
    for i, id in ipairs(fields) do
        if i % 2 == 0 then
            ids[#ids + 1] = id
        end
    end
until cursor == 0
return ids

我不知道在变量中存储脚本结果是可能的。太棒了!我今天稍后会尝试一下并告诉你它是否有效。 - Gustavo Straube
是的,这是可能的。我已经尝试过一个与你类似的样例场景,并且它可以正常工作。我使用了Java中的Jedis库来实现这一点。 - Karthikeyan Gopall
嘿!当我试图将脚本结果分配给那个变量时,出现了以下错误: ERR unknown command 'nameSet []' - Gustavo Straube
嗨,我更新了答案供您参考。@GustavoStraube - Karthikeyan Gopall
我明白了。但是这样的话,我将不得不在客户端执行部分过程——存储集合并将其转发到下一步——我知道这是可能的。我想要实现的是全部在Redis/Lua上完成。 - Gustavo Straube
这是不可能的。这就是我们从一开始就在谈论的内容。 - Karthikeyan Gopall

1
问题不在于你正在向数据库写入数据,而是你在进行HSCAN之后执行了一次写操作,这是一个非确定性命令。
在我看来,在Lua脚本中很少有使用SCAN命令的好理由。该命令的主要目的是允许您分批处理,以便您不会锁定服务器处理巨大的键空间(或哈希键空间)。但是,由于脚本是原子性的,使用HSCAN并没有帮助——直到整个过程完成,你仍然会锁定服务器。
以下是我能看到的选项:
如果您不能冒险使用耗时的命令锁定服务器:
  1. 在客户端上使用HSCAN。这是最安全的选择,但也是最慢的选择。
如果您想尽可能多地在单个原子Lua命令中进行处理:
  1. 使用Redis 3.2并进行脚本效果复制。
  2. 在脚本中进行扫描,但将值返回给客户端并从那里启动写操作。 (即Karthikeyan Gopall的答案。)
  3. 不要使用HSCAN,而是在脚本中使用HKEYS并使用Lua的模式匹配过滤结果。由于HKEYS是确定性的,因此您不会遇到后续写入时的问题。当然,缺点是您必须首先读取所有密钥,而不管它们是否与您的模式匹配。 (尽管HSCAN在哈希大小方面也是O(N)。)

我不确定如何使用 HKEYS 执行类似于 HSCANMATCH - Gustavo Straube
@GustavoStraube:你需要在Lua中进行过滤。我已经更新了答案,并列出了我能看到的选项。 - Kevin Christopher Henry

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接