问题及现状
闲鱼搜索很多场景基于集团搜索中台能力,纵观闲鱼搜索链路,存在多角色(工程、算法工程、算法等)、多业务(闲鱼无忧购、租房、帖子等)、多节点(离线数据源聚合、在线召回、URF Rank等),具有明显的复杂性。并且闲鱼主搜仅存在一条链路支持搜索多业务发展,各角色、各业务、各节点处于高耦合串行迭代模式。在大数据量、多业务、多角色并行场景下,以下问题日益明显:
1、迭代效率低、排期长,无法满足新业务快速迭代诉求:主要体现在数据量大,单次迭代周期长,以及多业务、多角色串行操作,耦合严重;
2、风险高:不同业务、不同特性均在主引擎召回链路执行修改,侵入大,风险高;
3、干预能力弱,业务间混排能力不足:缺少干预扶持能力,无法有效为创新业务提供定制化孵化能力。
闲鱼一次请求完整流程如下:
注:QP,即Query Planner,主要用于预测用户query搜索意图。
SP在测试环节下调用拓扑示例如下:
关键节点:
blender是SP服务核心入口,主要职责包括解析用户query意图、引擎分页召回、搜索数据补齐等功能。
uniq_session是召回核心入口,通过引擎召回、分页处理、URF Rank等操作,获取最终商品列表
ha3_searcher是引擎召回入口,翻译用户query为HA3引擎查询query,请求引擎在线服务,召回满足条件商品信息
uniq_summary是商品信息补充入口,依据商品id列表,补齐商品详细信息并返回,最终在搜索结果页呈现给用户
背景概述
闲鱼搜索很多场景基于集团搜索中台能力,搜索引擎分为数据源聚合(离线dump)、全量/增量/实时索引构建及在线服务等部分,通过集团内部一系列处理阶段,对客户提供高可用高性能的搜索服务。服务架构如下:
其中:
SP/SPL是集团内一套构建于Wunder上的开发工具,提供开发测试打包上线的视图界面以及一套业务函数库。HA3是一套基于suez框架的全文检索引擎,提供丰富的在线查询子句,过滤子句,排序子句,聚合子句且支持用户自定义开发排序插件。
Qrs用于接收用户查询,将用户查询分发给Searcher,收集Searcher返回的结果作整合,最终返回给用户。Searcher是搜索查询的执行者,主要包括倒排索引召回、统计、条件过滤、文档打分及排序及摘要生成。在实际的部署中,Qrs和Searcher都是采用多行部署的方式,可以根据业务的流量变化作调整。Searcher还可以根据业务的数据量调整列数,解决单机内存或磁盘放不下所有数据的问题。
剖开HA3引擎,我们可以粗略看下引擎侧索引的构建以及在线召回链路:
蓝色部分表示HA3在线服务,提供在线召回商品服务;绿色部分表示引擎数据源聚合(离线dump)逻辑,将商品信息构建成对应的引擎数据源。下图为一个离线引擎dump的数据源图示例:
设计思路
由于我们要解决的问题是如何安全、灵活、高效地支撑多业务接入闲鱼搜索,并提供不同业务间混排能力,我们的设计想法如下:
1、支持业务间互相隔离,从引擎侧深层次隔离,提高整体迭代效率
2、支持多引擎并发召回能力,解决搜索RT恶化问题
3、支持多引擎召回结果合并能力,并统一传递给URF Rank执行混排
4、支持实时干预能力,满足不同业务孵化诉求
调研集团内部已有实现模式,并结合闲鱼搜索业务发展诉求,我们重新设计并执行闲鱼底层引擎召回逻辑升级流程,从单引擎单路召回架构升级为多引擎多路并发召回架构。
Search Planner及搜索引擎层升级如下图所示:
主要升级节点如下:
1、步骤1中解析用户query,获取多引擎标识,解析并缓存引擎配置信息
2、步骤3流控模块从配置中心获取引擎定制化配置,并执行query改写逻辑,生成对应的HA3请求串
3、步骤4依据query中携带的多引擎标识,并发请求不同引擎在线服务召回商品,并合并召回结果,统一调用步骤5的精排服务能力
4、步骤5算法侧升级支持多引擎结果,支持多业务混排能力
实现方案
依照上节的设计思路,实现方案核心主要关注以下几点:
1、如何解析用户query,并缓存用户query相关引擎配置信息
每一个引擎均存在唯一的标识id(便于说明:主搜引擎标识为0,闲鱼无忧购引擎标识为1,向量引擎标识为2),上层搜索业务层依据不同业务场景组装引擎查询query,包括组装引擎标识列表(比如bizType=0,1,2,表示同时从主搜引擎、闲鱼无忧购引擎、向量引擎召回商品)。
Search Planner核心解析逻辑如下:
-- 构建请求的biz列表
function get_search_biz_type_list(query, param)
-- 搜索biz配置列表
local search_biz_type_list = {}
if query.bizType then
if type(query.bizType) == "string" then
table.insert(search_biz_type_list, query.bizType)
else
search_biz_type_list = query.bizType
end
end
if search_biz_type_list == nil or #search_biz_type_list < 1 then
table.insert(search_biz_type_list, param.default_biz_type)
end
local search_list = {}
if not query._enable_multiplexed then
table.insert(search_list, {biz = param.default_biz_type, biz_type = search_biz_type_list})
return search_list
end
for _, type in ipairs(search_biz_type_list) do
table.insert(search_list, {biz = type, biz_type=type})
end
return search_list
end
function parse_diamond_config(query, param)
query._cluster_info = {}
-- 多引擎配置
local cluster_info_str = param.cluster_info
local cluster_info = setup.split(cluster_info_str, ";")
if not cluster_info then
return
end
for _, info in ipairs(cluster_info) do
local single_cluster_info = setup.split(info, ":")
if single_cluster_info and #single_cluster_info > 1 then
query._cluster_info[single_cluster_info[1]] = single_cluster_info[2]
end
end
end
-- 改写向量召回query
function rewrite_embedding_recall(query, switch, param)
-- 创建query
local que = {}
que._biz = query._biz
que._cluster_name = query._cluster_name
que.q = ""
if really_need_search_embedding_recall(query) then
local qp_idlefish_table_qp_reserve_str_4rs_query_vector = query._qp_idlefish_table_qp_reserve_str_4rs_query_vector
local q_str = qp_idlefish_table_qp_reserve_str_4rs_query_vector .. "&n=" .. param.embedding_recall_search_hit
que.q = "sim_vec:" .. url_encode(q_str)
end
-- 改写attribues
if not switch.full_phase then
que.attribute = "item_id,bizType"
end
-- 改写rank
que.rank = { -- rank 子句
rank_profile = 'RecallScorer'
}
return que
end
-- 解析入口
function multi_parse(res_vec)
local items_search_lua_tables = {}
for _, p in ipairs(res_vec) do
-- p.res:value()执行异步并发请求
local res = p and p.res and p.res:value()
local biz = p and p.biz
... ...
local ret_table = res:getTable()
table.insert(items_search_lua_tables, {biz=biz, tbl = ret_table})
end
end
/bin/sp?outfmt=json2&src=idlefishwireless&app=spl_secondHand_new&q=Zimmermann%20%E8%A3%99&isForbiden=0&wlsort=35&s=0&n=10&enable_rank=true&enable_advsort=true&new_rs=true&ha3_version=v2&sellStatus=0&auctionType=a,b&se=tis2&bizType=0,1,2