基于Hologres的达人圈选系统实战:从分钟级到秒级的标签筛选与向量召回
2026/9/8 19:41:48 网站建设 项目流程

1. 项目背景:从“找得到”到“找得准”,达人圈选到底难在哪

做运营和增长的同学应该都有体会,达人圈选这件事,听起来就是“从库里捞人”,但真正落地的时候完全不是那么回事。早期我们用离线表跑批,一个圈选任务动辄跑半小时,运营同学拿着提数需求排队等结果,等拿到名单的时候,热点早过去了。后来换成了 Elasticsearch,检索速度快了不少,但碰到“有消费能力、美妆垂类、近 30 天活跃、去年同期买过精华但今年没买”这种复杂条件,得写一堆 bool 查询,性能扛不住不说,圈出来的结果也经常不够准。

这个项目的起点很朴素:帮运营团队把达人圈选从“找得到”升级到“找得准”。“找得到”意味着能从千万级达人池里把满足基本条件的用户捞出来,比如性别、年龄、城市这种硬性标签。“找得准”则要复杂得多,它要求系统能理解“垂类偏好”“内容调性”“消费潜力”“复购节奏”这些模糊但决定投放效果的特征,同时在圈选结果返回的速度上不能拖后腿。

我们最终选择用 Hologres 来承载这套系统。说实话,最开始团队内部是有争议的,因为 Hologres 在很多人印象里是个 OLAP 数仓,拿来做圈选场景总觉得有点“不务正业”。但实际做下来,它的几个特性对达人圈选场景几乎是量身定做的:原生支持向量检索、全文检索、列存高效过滤,还能把这些能力在一条 SQL 里融合起来。这套系统的核心思路,就是不再区分“清洗层—标签层—圈选层”三个割裂的阶段,而是把达人特征、行为序列、向量 embedding 统一沉淀到 Hologres 的一张宽表里,圈选就是一次 SQL 交互。

适合谁来参考这篇内容?如果你是负责用户增长、达人运营的数据开发或后端工程师,正被“标签筛选效率低、圈选结果不够精准、实时性跟不上活动节奏”这几个问题困扰,那这篇实操记录应该能给你一些直接可落地的思路。整个系统上线后的效果是:单次复杂圈选从分钟级降到秒级返回,运营自助圈选的覆盖率提升了 60% 以上,这是我最初没想到的收获。

2. 整体架构与核心思路:为什么 Hologres 适合做这件事

2.1 圈选系统的传统实现方式,问题出在哪

在动手写 Hologres 方案之前,我觉得有必要把传统方式的三个坑讲透,这是后面所有设计决策的背景。

第一个坑是数据链路太长。传统的达人圈选链路通常是:原始日志入 Kafka,经过 Flink 清洗后落到离线数仓 Hive,再加工成标签宽表,最后同步到 HBase 或者 Elasticsearch 供圈选查询。链路一长,数据时效性就差。运营上午要圈“昨晚直播互动过的达人”,数据要到下午才能用,这还算快的。如果涉及跨月行为对比,T+1 都未必够用,标签数据至少得攒两三天。

第二个坑是查询模型割裂。标签筛选在 Elasticsearch 里做,行为序列在 ClickHouse 里算,文本召回靠 ES 的 match 查询,向量召回又单独接一套 Milvus。听起来挺合理,但业务上你很难把一个复杂的圈选逻辑拆得这么干净。比如“美妆垂类、粉丝量级在 50 万到 200 万之间、近 30 天视频完播率高于 60%、同时和某品牌历史合作过 3 次以上”这个条件,要跨三四个存储引擎做 join。开发成本是一方面,更麻烦的是,运营同学要的是一个兜底全部条件的最终名单,而每个引擎返回的候选集做交并集时,很容易出现数据口径不一致的问题。

第三个坑是圈选条件无法复用。同一个“高购买力人群”的标签,在 A 场景是“客单价高于 300 元”,在 B 场景又变成了“近 90 天有 2 次以上千元订单”。每个场景各写各的 SQL 和查询逻辑,标签管理混乱,最后谁也说不清一套圈选结果是怎么算出来的。

2.2 Hologres 在圈选场景的独特定位

我第一次接触 Hologres 的时候,先是把它当普通数仓用——建几张表、跑跑聚合、出报表,没什么特别的感觉。直到后来做到实时特征查询,才意识到它的底层存储和索引设计跟传统数仓不一样。

Hologres 本质上是一个 HSAP 系统,翻译成人话就是:它既能当数仓做离线批量写入,又能当在线服务引擎支撑高并发点查和实时写入,同时还能跑复杂的分析 SQL。这个特性对达人圈选系统来说非常关键,因为圈选场景天然就是“离线的数据、在线的查询”。达人标签数据是批量加工的,但圈选请求是一个个实时进来的,而且每次圈选条件的组合方式都不一样,没法预先物化结果集。

项目里我们用到的核心能力有三块:

第一,列存和自定义索引的结合。Hologres 底层用列存,配合 Bitmap 索引可以高效处理多条件过滤。这在圈选场景里价值很大,因为圈选条件往往是一堆等值、范围、IN、LIKE 的混合体。传统数仓对这种查询要么全表扫,要么靠分区裁剪,而 Hologres 的位图索引能做到“按需”扫数据,相当于给每列建了一个倒排。

第二,原生向量检索。这是后来我们敢把算法团队产出的人设 embedding 放到同一个引擎里的原因。Hologres 内部集成了 Proxima 向量引擎,建表时声明一个 vector 字段,写入向量数据,查询的时候用 order by distance 排序就行。不需要额外部署一套向量数据库,也不用维护两套数据的一致性。

第三,一条 SQL 融合多维检索。标签过滤、全文检索、向量召回可以在同一张表上做交并集,优化器会自动选择执行策略。这在系统落地阶段帮了大忙,因为不需要在应用层编排多个引擎的查询逻辑,所有条件全部下推到 Hologres 这一层完成。

2.3 整体架构设计与数据流转

整套系统的数据流转分为五层,这里我画个简化的逻辑说明:

  • 数据接入层:达人基础信息、内容数据、互动行为日志,统一通过 Flink 实时写入 Hologres,同时离线数据用 DataWorks 批量同步。
  • 标签加工层:用 Hologres 本身的 SQL 能力做特征加工,产出达人核心标签表,包括自然属性、内容属性、商业属性、行为偏好。
  • 向量生成层:算法团队用预训练模型产出达人内容调性向量、人设相似度向量,写入另外一张宽表。
  • 圈选服务层:对外提供一组 Restful API,接收圈选条件,拼装成 SQL 后请求 Hologres,返回命中达人列表。
  • 应用层:运营端圈选后台、投放系统、数据分析看板。

这套结构和很多公司做用户画像圈选的架构没什么本质区别,唯一的差别是,我们没拆微服务,也没有多引擎 join,圈选服务就一个瘦瘦的查询网关,核心逻辑全在 SQL 里。

3. 核心表结构设计与索引选择

3.1 达人宽表:把标签做成列

圈选系统最核心的物理模型是一张达人宽表,我们把达人相关的维度、标签、行为指标、向量全塞进去。这里有一个很重要的设计原则:宁可宽,不要窄。

为什么?因为圈选的查询条件组合是无限的,如果拆成多张表,用户每次圈选都要做 join,性能损耗非常大,而且 SQL 写起来也很痛苦。把它做成宽表,每个标签一列,查询的时候只有用到的列会被加载。列存嘛,这是基础认知。

我们最终的表结构大致长这样:

CREATE TABLE daren_profile ( daren_id TEXT NOT NULL, gender TEXT, age_group TEXT, city_level TEXT, province TEXT, fans_cnt BIGINT, avg_play_rate FLOAT8, avg_like_cnt BIGINT, avg_comment_cnt BIGINT, avg_share_cnt BIGINT, category_tags TEXT[], -- 垂类标签数组 cooperate_brands TEXT[], -- 合作品牌列表 first_active_date DATE, last_active_date DATE, active_days_30 BIGINT, purchase_ability TEXT, is_verified BOOLEAN, embedding VECTOR(128), -- 内容调性向量 ext_info JSONB, PRIMARY KEY (daren_id) );

几个关键的选型思考:

  • 标签列尽量用数组类型。比如 category_tags、cooperate_brands,用 text[] 比用字符串拼接好得多,查询时可以直接用 array_contains 之类的语义,配合 GIN 索引也能加速。
  • embedding 字段直接放在宽表里,不单独建表。这样圈选条件里如果同时有标签过滤和向量召回,只需要在同一张表上做一次操作,不会跨表扫描。
  • ext_info 用 JSONB 存高度个性化的字段,比如不同垂类的专属属性,这样不用频繁改表结构。但是要注意,JSONB 字段能不用就不用,查询性能不如普通列。
  • 主键是 daren_id,Hologres 里会基于主键建行存,实现快速点查。但圈选场景更多是分析型查询,所以把 segment 属性设成 COLUMNAR 存储模式,兼顾两种场景。

3.2 行为聚合表:跑不动的指标提前算好

除了宽表,我们还建了一张行为聚合表,专门存达人最近 7 天、14 天、30 天、90 天的互动指标。为什么单独建这张表,而不是把列加到宽表里?因为行为指标是周期性的,今天算完明天又变,如果都放宽表,更新压力会很大,每次 Flink 写入都要 rewrite 整行,资源浪费很严重。

行为聚合表的粒度是“达人 + 统计周期 + 指标类型”,用行存存储,key 是 daren_id,按周期聚合成多列。查询的时候,先根据圈选条件里的时间范围找到对应的周期列,再从宽表里过滤。这样既保证了指标时效性,又避免了大宽表的频繁更新。

3.3 索引选型:位图索引是圈选查询的加速器

这里重点说位图索引。Hologres 的列存默认是压缩存储,但如果不建索引,查询密集值列(比如性别、城市等级)时还是要做全列扫描。我们测试下来,在两千万达人数据量下,单列等值过滤需要 200 到 400 毫秒,加上两三个这样的条件,查询时间轻松超过一秒,完全达不到我们要的“秒级返回”。

加了位图索引之后,单列等值过滤基本能压在 30 毫秒以内,多条件组合也只多了不到 50 毫秒。原理很简单,位图索引会把每个取值映射成一段位图,过滤时直接做位运算,相当于把磁盘扫描变成了内存位图运算,进度完全不一样。

具体建索引时我们踩了一个坑:给所有列都建位图索引。后来发现低基数列(比如 gender 只有三个值)建索引效果还行,但高基数列(比如 fans_cnt)建位图索引反而变慢了,因为每个取值对应位图很小,但值太多导致位图索引本身膨胀得厉害。经过反复测试,我们的策略是:低基数列(性别、城市等级、认证状态)建位图索引;范围查询列(粉丝数、互动量)不建位图,用聚类索引;数组标签列建 GIN 索引;embedding 列走 Proxima 索引。

注意:位图索引不是建得越多越好,低基数、重复度高的列收益最大,高基数列可能适得其反。建完索引后一定要用 explain 看执行计划,确认优化器真的选中了索引。

4. 圈选查询的实现细节:把复杂条件翻译成 SQL

4.1 基础标签圈选:一段真实的圈选 SQL

把需求和 SQL 对应起来,是圈选系统开发日常占比最高的工作。运营同学提需求的时候不会说 SQL,他们说“我想要美妆垂类、女性、粉丝数 50 万以上、近 30 天活跃的达人”。翻译成 SQL 就是:

SELECT daren_id, daren_name, fans_cnt FROM daren_profile WHERE category_tags @> ARRAY['美妆'] AND gender = 'female' AND fans_cnt >= 500000 AND active_days_30 > 0 LIMIT 200;

这条 SQL 走下来,如果 category_tags 建了 GIN 索引、gender 建了位图索引,执行时间大概在 100 毫秒左右。但在实际过程中,我们发现只做这种基础圈选,运营同学并不满意。他们抱怨两点:第一,圈出来的名单虽然符合条件,但看起来“不像达人”,很多是发了几条内容就没动静的僵尸号;第二,条件一松一紧就不好把握,松了名单大而杂,紧了名单又太小。

这就是“找得到”和“找得准”的分水岭。基础标签只能证明“这个人有这些属性”,但证明不了“这个人是这类人里值得合作的”。所以后面我们引入了行为质量分和向量召回,来回答“凭什么选他不选别人”的问题。

4.2 行为质量分排序:让圈选结果更有说服力

我们定义了一个行为质量分 quality_score,综合考虑达人的内容打开率、完播率、互动率、粉丝增长趋势,权重由运营团队和算法团队共同商定。打分的 SQL 逻辑如下:

CREATE OR REPLACE FUNCTION calc_quality_score( avg_play_rate FLOAT8, avg_like_cnt BIGINT, fans_cnt BIGINT ) RETURNS FLOAT8 AS $$ SELECT 0.4 * avg_play_rate + 0.3 * ln(avg_like_cnt + 1) / ln(fans_cnt + 1) + 0.3 * CASE WHEN avg_comment_cnt > 100 THEN 1.0 WHEN avg_comment_cnt > 50 THEN 0.8 ELSE 0.5 END $$ LANGUAGE sql;

这个打分不是严格意义上的复杂算法,但至少能筛选掉“数据虚高但互动极差”的达人。圈选的时候,我们用它做排序,优先展示质量分高的达人。改动后的圈选 SQL 变成:

SELECT daren_id, daren_name, fans_cnt, quality_score FROM daren_profile WHERE category_tags @> ARRAY['美妆'] AND gender = 'female' AND fans_cnt >= 500000 AND active_days_30 > 0 ORDER BY quality_score DESC LIMIT 200;

同一个条件,圈出来的名单质量提升非常明显。原来按粉丝量倒序取出来的头部达人,虽然粉丝多但很多是早年做微商囤的粉,内容互动惨不忍睹。换成质量分排序后,前 200 名几乎都是真正能撬动转化的人。

4.3 向量召回:从“满足条件”到“调性一致”

标签圈选解决的是“硬条件”,但达人营销里还有一项软指标——内容调性。一个高端护肤品牌要找达人,硬条件可以是“女性、粉丝 50 万以上、美妆垂类”,但调性上还得匹配,不能找天天发九块九包邮种草号的博主。

我们把达人的历史内容标题、评论、视频脚本过一遍预训练模型,生成一个 128 维的内容调性向量,存入 embedding 字段。圈选时,用户上传一个参考文本或拿一个种子达人编码成 query 向量,用向量相似度找内容调性最接近的一批达人。

SELECT daren_id, daren_name, fans_cnt, embedding <=> :query_vector AS distance FROM daren_profile WHERE category_tags @> ARRAY['美妆'] AND fans_cnt >= 500000 ORDER BY embedding <=> :query_vector ASC LIMIT 200;

在 Hologres 里,向量距离操作符是<=>,代表余弦距离(值越小越相似)。这个查询表面上看也是 order by,但执行计划里会走 Proxima 索引,不会全表计算距离,所以性能不用太担心。

实际测试中,在一张一千万行的表里做“标签过滤 + 向量排序”的组合查询,返回前 200 条相似达人,耗时大概在 300 毫秒左右。相比纯扫全表做暴力计算——那种方式保守估计也得两秒以上——这个优化效果很理想。

4.4 全文检索:当圈选条件变成一句描述

运营同学的另一种常见操作是,不选标签,直接输入一句“爱用国货美妆的学生党精致女孩”。这种非结构化的圈选描述,以往只能靠运气碰标签组合,现在我们借用 Hologres 的全文检索能力来兜底。

给达人表增加一个 profile_text 字段,内容拼接达人的简介、历史内容标题、热门评论标签,建全文检索索引。查询时直接用中文分词检索:

SELECT daren_id, daren_name FROM daren_profile WHERE profile_text @@ '爱用 & 国货 & 美妆 & 学生' ORDER BY ts_rank(profile_text, query) DESC LIMIT 200;

这里关键是中文分词。Hologres 内置的分词器对中文支持还可以,但在处理“国货美妆”这种复合词时偶尔会拆得不够好。我们试过挂 IK 分词插件,效果有提升,尤其是对品牌词和垂类词。如果项目里中文检索需求重,建议从第一天就上自定义分词词典,把垂类关键词、品牌名、达人常用语加进去。

提示:全文检索和向量召回是很配的组合。向量召回擅长语义相似,全文检索擅长关键词匹配,两者结合能充分照顾“语义派”和“关键词派”两种运营习惯。Hologres 一条 SQL 可以同时写两个条件,不冲突。

5. 性能优化实践:从百万级到千万级数据量,查询不衰减

5.1 预计算和物化视图,永远不过时的优化手段

如果是固定组合的圈选条件,比如“一线城市女性美妆达人”,每次都实时跑全量数据,收益很低。我们针对运营同学使用频率最高的三十多个固定圈选条件组合,建了物化视图,Hologres 自动维护刷新。

物化视图的本质是空间换时间,尤其在数据量持续膨胀的阶段,这个策略帮我们把高频查询的耗时从秒级进一步压低到了几十毫秒。低频的长尾条件则直接走实时查询,反正数据分层合理的话,响应时间也可控。

5.2 数据分布优化:让查询只在必要的分片上跑

Hologres 的分布式架构里,数据会根据分布键分散到不同的 shard。分布键选得好不好,对查询性能影响非常明显。

我们踩过一个坑:用 daren_id 做 distribution key,结果圈选高频条件都是按 category_tags 和 city_level 来过滤的,导致查询要么广播到所有 shard,要么得等最慢的 shard 跑完才能汇总,性能很磕碜。

后来把分布键改成 category_tags 的 hash 值,同时把查询条件里最常见的 category_tags 过滤下推到存储层,这样每个 shard 只需要扫自己本地的一部分数据,不需要全量广播。调整之后,查询耗时稳定下降了 40% 左右。

5.3 写入链路避坑:批量写资源争抢问题

Hologres 能扛高并发查询,但写入和查询如果同时压在同一批计算资源上,容易出现相互抢占。我们上线初期就撞上过:Flink 实时写达人行为数据,一旦写入峰值上来,线上圈选查询的 p99 延迟直接涨了两倍多。

解决方法是把读写资源拆开,在 Hologres 上建了两组 shard,一组承担实时写入和批处理,另一组只服务圈选查询。同时控制 Flink 的写入批次大小,从原来每两秒刷一次改成每五秒刷一次,单批次数据量翻倍,整体资源争抢明显缓解。

5.4 慢查询排查三板斧

我这里整理一份 Hologres 慢查询排查的方法论,亲测有效:

  • 打开慢查询日志,Hologres 控制台有查询诊断页面,直接看每条查询的耗时和扫描行数。先定位是不是扫描行数太高,如果是,大概率是索引没生效。
  • 用 explain 看执行计划,确认每个过滤条件下推到哪个算子,有没有走索引,有没有出现 TableScan 扫全表。
  • 针对性建索引或改 SQL。如果单列过滤慢,加位图索引;如果 join 慢,检查分布键和数据倾斜。

6. 常见问题速查表与避坑经验

这块我按问题、原因、解决方案三个维度整理,都是我们项目里真实碰到过的。

现象根本原因解决思路
加了多个过滤条件后查询反而变慢优化器选错了索引路径,可能走了全列扫描再过滤查看执行计划,给高频过滤列建位图或 GIN 索引
向量召回结果不相关向量本身质量不行,可能模型用错了领域语料重新训练 embedding 模型,或者换用下游任务做微调
物化视图刷新不及时增量更新的触发条件设置不合理手动触发刷新策略,高频表缩短刷新间隔
相同 SQL,不同时间执行耗时差异巨大查询可能打到了不同的 shard 上,数据分布不均匀检查分布键,确认高频过滤条件是否和分布键契合
中文全文检索效果差自带分词器分词粒度不匹配业务词汇挂 IK 分词器,配置自定义词典
圈选结果里混入低质达人过于依赖硬性标签,没考虑行为质量引入 quality_score 排序,结合互动指标
同一达人出现在多个圈选名单里造成重复触达圈选条件交叉重叠在应用层维护排除名单,或圈选时显式带上排除 ID 列表

7. 上线之后的运营反馈优化闭环

系统上线之后,运营同学的使用方式也在发生变化。刚开始他们习惯从标签面板一层层点选,出来的结果就是一张表。后来我们把质量分、向量相似度这些非语义结果做了可视化展示,选人逻辑慢慢变成了“先用标签捞池子,再用相似度排序找人”。

这里有一个从运营侧反馈过来的例子,我觉得很有代表性。某护肤品牌要做新品上市推广,运营在系统里圈选达人,条件大概是“女性、粉丝 50 万以上、美妆垂类、近 30 天有内容更新”。按旧逻辑,这批名单大概有 8000 人,运营要从里面人工挑 200 个,工作量巨大,而且挑出来的大多是自己熟悉的那批熟人。用了新系统之后,运营基于一个过往合作效果最好的种子达人做向量召回,再加上标签过滤,直接拿到了内容和调性都贴近的 200 人,整个圈选过程三分钟搞定。

从“找得到”到“找得准”,系统的价值不只是把 SQL 跑快了,而是让运营能把“我觉得这个达人合适”变成可复现、可解释的规则。这也是为什么后来我们把每次圈选的条件、排序分数和入选名单全部记录下来,沉淀成历史分析数据。现在反过来看,这批数据本身就是一套很宝贵的达人分群方法论。

8. 一点延伸:这套系统的边界和可复用的思考方式

说实话,Hologres 并不是万能钥匙。我们做这套系统时也评估过 ClickHouse 和专门向量数据库的方案,各自都有优势。Hologres 最合适的场景,是数据规模在千万到亿级、查询模式复杂多样、希望一份数据服务多种业务的中间态。如果数据量到了十亿级以上,或者要支撑千人千面的实时个性化推荐,那可能需要引入更底层的分布式存储和计算引擎。

但这个项目最让我觉得有价值的地方不完全是技术选型,而是把“圈选”从一个单点查询问题拆成了数据组织、索引设计、特征加工、效果排序这样一个有层次的系统工程。在动手写 SQL 之前,先把要解决的问题拆到位,比选什么引擎重要得多。

个人在实际操作中的体会是:Hologres 的上手门槛不高,但要用好,得把它的索引机制和存储模型琢磨透。不要把它当普通 MySQL 用,也别把它当纯数仓用。它的正确打开方式,是理解它擅长在哪些场景里把“分析”和“在线”融合起来——达人圈选只是一个代表,换成商品选品、用户分层、线索打分,底层思路都是通用的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询