您的位置:首页 > 手游攻略 > 基于阿里云 Milvus 复刻“高德扫街榜”

基于阿里云 Milvus 复刻“高德扫街榜”

作者:互联网  时间: 2026-08-13 09:10:59  

作者:周弘懿(锦琛)

业务背景

「附近有什么好吃的?」——这是本地生活场景里最高频的一句发问。高德「扫街榜」这类产品之所以有说服力,核心在于两点:它离你近,且它是被真实吃过的人夸出来的,而不是靠商家刷分堆出来的。

把这类榜单拆开看,它需要回答三个层层递进的问题:

就近:以我当前位置为圆心,方圆几公里内、或者我正身处的这个商圈范围内,有哪些店?

对味:这些店里,哪些符合我此刻的需求?比如「适合亲子的川菜馆」「安静能聊事的咖啡馆」「深夜还开着的苍蝇馆子」——这是一种语义需求,无法用标签精确枚举。

靠谱:在符合前两条的店里,真实评价的口碑如何?好评率多少?大家反复提到的优点/槽点是什么?

它的业务价值也就落在这三层上:帮用户发现附近的好店、把「近 对味 口碑好」融合成一张可解释的榜单、并且基于真实评价的语义与情感而非刷分数据。要把这三层能力捏合到一次查询里,传统技术栈会相当吃力。

技术挑战

要做出这样一张榜单,系统必须同时具备以下能力,缺一不可:

地理空间过滤。既要支持「距离我某坐标 N 公里内」的半径查询,也要支持「是否落在某商圈多边形内」的范围判断,还要能判断门店与配送区域是否相交。这需要把 POI 的经纬度当作真正的几何对象来存储与运算。

评价文本的语义检索。用户输入「适合亲子的川菜馆」,系统要能理解语义、召回语义相近的评价与门店,而不是做关键词精确匹配——「带孩子」「亲子友好」「有儿童椅」都应该被命中。

海量评价的情感与摘要理解。要在店铺维度上把成千上万条评价聚合成一个好评率,并生成一段店铺评价摘要,让榜单结果可读、可信。

相关性精排。语义召回的候选集里,排序还需要按 Query 的真实相关性重排,把「最对味」的店顶到前面。

传统方案通常是三套系统拼接:用 PostGIS / 空间数据库做地理过滤,用向量数据库做语义召回,再用一套 NLP 服务(情感分类、摘要)做评价理解。三套系统各存一份数据,跨系统 JOIN、数据同步、一致性维护的成本极高,一次榜单查询要串起多个服务、多轮网络往返,工程复杂度和延迟都难以接受。

解决方案

阿里云 Milvus 可以把上面四件事收敛到一个 Collection、一次检索里完成:

地理:POI 经纬度用 Milvus 的地理空间类型 GEOMETRY 存储、商圈为 POLYGON。检索时把地理约束写进 filter 表达式,用 ST_WITHIN / ST_CONTAINS / ST_INTERSECTS 等空间关系函数完成范围/相交过滤。Milvus 2.6 为 GEOMETRY 字段提供了基于 R-Tree 的地理位置索引来加速空间查询。

语义:评价文本通过 AI_EMBEDDING在写入时自动向量化,检索时对同一模型生成的 Query 向量做 ANN 检索。

精排:召回候选用 AI_RERANK,按 Query 相关性重排。

口碑:用 AI_SENTIMENT 对评价做情感分类,应用侧聚合出好评率;用 AI_SUMMARIZE 把门店评价压缩成一段评价摘要。

最关键的一点:地理过滤与语义检索融合在同一次 search() 调用里——ANN 向量检索负责「对味」,filter 里的 ST_ 空间函数负责「就近」,二者在一次请求内联合求解,再叠加 rerank 精排,直接产出榜单候选。

先对齐一下文中几个 AI Function 的作用:

函数

作用

在扫街榜里

AI_EMBEDDING

写入时自动把评价文本转成向量(无需应用侧先调模型)

让「适合亲子的川菜馆」这类语义需求能被向量检索命中

AI_RERANK

对召回候选按与查询的相关性重新排序(精排)

把「最对味」的店顶到榜单前面

AI_SENTIMENT

判断每条评价是好评 / 差评 / 中性

聚合出门店「好评率」

AI_SUMMARIZE

把成百上千条评价压成一句话摘要

生成榜单里「一句话点评」

","margin":true,"hideBorder":false,"id":"yh1OW"}">

具体AI Function用法参考官方文档

架构流程如下:

关键概念:GEOMETRY、WKT 与 ST_ 空间函数

在进入实战代码前,先把几个贯穿全文的地理概念讲清楚。

GEOMETRY —— 把「地点」当成几何图形存起来。 普通做法是把经纬度存成两个 double 字段,数据库只把它当两个数字,并不理解「谁在谁里面、两块区域是否相交」。GEOMETRY 是 Milvus 2.6 的地理空间类型,它把「点 / 线 / 面」当作真正的几何对象存储,于是就能用 ST_* 函数做空间运算;Milvus 2.6 还为它提供了基于 R-Tree 的空间索引来加速。

WKT —— 描述几何图形的标准文本写法。 GEOMETRY 字段里存的值用 WKT 表示,常见几种:

WKT 类型

写法示例

通俗含义

扫街榜里对应

POINT

POINT(116.4612 39.9088)

一个点

一家门店的经纬度

LINESTRING

LINESTRING(116.46 39.90, 116.47 39.91)

一条折线

一段道路 / 配送路径

POLYGON

POLYGON((116.448 39.905, ..., 116.448 39.905))

一片闭合区域(首尾坐标相同)

一个商圈 / 配送范围

MULTIPOINT / MULTIPOLYGON

MULTIPOLYGON(((...)),((...)))

多个点 / 多片区域

一个品牌多家门店、多块行政区

","margin":true,"hideBorder":false,"id":"ZG3FR"}">

坐标顺序是「经度在前、纬度在后」(POINT(lng lat)),和口语「北纬…东经…」相反。本文坐标均为 WGS84(GPS 经纬度)。

ST_ 开头的都是空间关系函数:写进 filter 里判断门店几何与给定区域的关系,返回真 / 假。本文重点用到三个:

函数

一句话解释

扫街榜里的用途

ST_WITHIN(A, B)

A 是否完全落在 B 内部

门店是否在商圈多边形内 / 是否在 3km 半径圆内

ST_CONTAINS(A, B)

A 是否完全包住 B(与 WITHIN 相反)

某商圈是否包含这家店,做归属分析

ST_INTERSECTS(A, B)

A 与 B 是否有任意交集 / 接触

门店配送范围是否与用户所在网格相交

","margin":true,"hideBorder":false,"id":"crmEg"}">

Milvus 2.6 还有其余几个函数,可按需选用:ST_EQUALS(完全相同,去重)、ST_TOUCHES(只碰边界,如相邻商圈接壤)、ST_OVERLAPS(部分重叠,如配送区交叉)、ST_CROSSES(穿越式相交,如道路穿过商圈)。一句话记区别:WITHIN=在里面、CONTAINS=把它装住、INTERSECTS=有任何交集(最宽松)、TOUCHES=只碰边、OVERLAPS=叠一部分、CROSSES=穿过去、EQUALS=一模一样。

为了让 POINTPOLYGONST_WITHIN 更直观,下面把上面用到的示意坐标画到真实地图上(底图为高德地图,正文的 WGS84 坐标已做坐标系转换以精确对齐)。图一为 3km 半径圆与商圈范围的总览;图二放大到商圈级别,可见三家门店都落在商圈 POLYGON 内——正是 ST_WITHIN 判定为「真」的情形。

实战操作

下面用代码表示「建库 → 入库 → 地理过滤 → 语义融合 → 精排 → 情感/摘要」全流程。

建带地理字段 embedding Function 的 Collection:locationGEOMETRY 存 POI 点,reviewAI_EMBEDDING,写入即向量化。

POI 与评价数据入库:写入门店 POINT 与评价原文,review_embedding 由 Function 自动生成,无需手工传向量。

地理过滤查询:用 ST_WITHIN 判断门店是否落在商圈 POLYGON 或 3km 近似圆内(半径圆由 circle_to_wkt 近似成多边形)。

语义检索 地理 filter 融合:把「就近」(filter 里的 ST_)与「对味」(ANN 向量检索)融合进同一次 search()

AI_RERANK 重排候选:按 Query 相关性精排,给出 search() 内挂 ranker 与 REST 即时接口两种写法。

AI_SENTIMENT AI_SUMMARIZE:情感分类聚合出好评率,摘要生成店铺一句话点评。

将MILVUS_HOST和MILVUS_TOKEN替换成自己的集群就可以直接看到效果

tuple[int, dict[str, Any]]:n """AI_RERANK / AI_SENTIMENT / AI_SUMMARIZE 的 REST 即时接口统一封装(走大模型,加重试更稳)。"""n last = Nonen for _ in range(retries):n request = Request(n f"{MILVUS_REST_BASE_URL.rstrip('/')}{path}",n data=json.dumps(body, ensure_ascii=False).encode("utf-8"),n headers={"Authorization": f"Bearer {MILVUS_TOKEN}", "Content-Type": "application/json"},n method="POST",n )n try:n with urlopen(request, timeout=timeout) as response:n status, data = response.status, json.loads(response.read().decode("utf-8"))n except HTTPError as exc:n status, data = exc.code, json.loads(exc.read().decode("utf-8"))n last = (status, data)n if status == 200 and data.get("code") == 0:n return status, datan time.sleep(1)n return lastnnndef circle_to_wkt(lng: float, lat: float, radius_km: float, num_seg: int = 32) -> str:n """把以 (lng,lat) 为圆心、radius_km 为半径的圆,近似成正多边形的 WKT(用于半径粗过滤)。n 注意:经纬度是角度,此处按平面近似换算,纬度越高误差越大。"""n d_lat = radius_km / 111.0n d_lng = radius_km / (111.0 * math.cos(math.radians(lat)))nn pts = [ ]nn for i in range(num_seg + 1):n theta = 2 * math.pi * i / num_segn pts.append(f"{lng + d_lng * math.cos(theta):.6f} {lat + d_lat * math.sin(theta):.6f}")n return f"POLYGON(({', '.join(pts)}))"nnn# ==================== 步骤 1:建带地理字段 + embedding Function 的 Collection ====================n# location 用 DataType.GEOMETRY 存 POI 点;review 挂 AI_EMBEDDING,写入时自动生成 1024 维向量nif client.has_collection(collection_name):n client.drop_collection(collection_name)nnschema = MilvusClient.create_schema(auto_id=True, enable_dynamic_field=False)nschema.add_field("id", DataType.INT64, is_primary=True)nschema.add_field("shop_id", DataType.INT64)nschema.add_field("shop_name", DataType.VARCHAR, max_length=256)nschema.add_field("category", DataType.VARCHAR, max_length=64)nschema.add_field("location", DataType.GEOMETRY) # POI 经纬度点(WKT: POINT(lng lat))nschema.add_field("review", DataType.VARCHAR, max_length=4096) # 评价原文,作为 embedding 输入nschema.add_field("review_embedding", DataType.FLOAT_VECTOR, dim=1024) # 评价向量,维度需与 dim 一致nn# 写入时自动把 review 向量化nschema.add_function(n Function(n name="embed_review",n function_type=FunctionType.TEXTEMBEDDING,n input_field_names=["review"],n output_field_names=["review_embedding"],n params={n "provider": "aliyun_milvus",n "model_name": "qwen3.7-text-embedding",n "dim": 1024,n "max_client_batch_size": 20, # qwen3.7-text-embedding 百炼官方文档批量大小为 20n "max_concurrency": 1,n },n )n)nnindex_params = client.prepare_index_params()nindex_params.add_index(n field_name="review_embedding",n index_type="HNSW",n metric_type="COSINE",n params={"M": 16, "efConstruction": 200},n)nclient.create_collection(n collection_name=collection_name,n schema=schema,n index_params=index_params,n)nnn# ==================== 步骤 2:POI 与评价数据入库(review_embedding 由 Function 自动生成,无需手工传向量) ====================nrows = [n {"shop_id": 1001, "shop_name": "巴蜀人家·亲子川菜", "category": "川菜",n "location": "POINT(116.4612 39.9088)",n "review": "带娃来的,有儿童座椅和小碗餐具,毛血旺不算太辣,服务员很有耐心,环境干净。"},n {"shop_id": 1001, "shop_name": "巴蜀人家·亲子川菜", "category": "川菜",n "location": "POINT(116.4612 39.9088)",n "review": "周末人多要等位,但菜品稳定,适合一家人聚餐,孩子吃得开心。"},n {"shop_id": 1002, "shop_name": "麻辣江湖", "category": "川菜",n "location": "POINT(116.4501 39.9101)",n "review": "重口味爱好者天堂,水煮鱼够味,但空间偏小、比较吵,不太适合带小孩。"},n {"shop_id": 1003, "shop_name": "静巷咖啡", "category": "咖啡",n "location": "POINT(116.4550 39.9075)",n "review": "环境安静适合谈事,手冲不错,不过跟川菜需求无关。"},n]nclient.insert(collection_name, rows)nclient.flush(collection_name)nclient.load_collection(collection_name)nnn# ==================== 步骤 3:地理过滤查询(ST_WITHIN 商圈多边形 / 半径近似圆) ====================n# 3.1 查询某商圈多边形范围内的门店ncbd_polygon = "POLYGON((116.448 39.905, 116.465 39.905, 116.465 39.914, 116.448 39.914, 116.448 39.905))"nhits = client.query(n collection_name=collection_name,n filter=f"ST_WITHIN(location, '{cbd_polygon}')",n output_fields=["shop_name", "category", "location"],n limit=100,n)nprint("n" + "=" * 64)nprint("步骤 3 | 地理过滤查询 ST_WITHIN(location, 多边形 / 半径圆)")nprint("=" * 64)nprint("[3.1] CBD 商圈多边形范围内的门店:")nfor h in hits:n print(f" · {h['shop_name']}({h['category']}) {h['location']}")nn# 3.2 附近 3km 半径查询:把圆近似成多边形,再用 ST_WITHIN 判断 POI 是否落在圈内ncenter_lng, center_lat = 116.4600, 39.9090ncircle = circle_to_wkt(center_lng, center_lat, radius_km=3.0)nnearby = client.query(n collection_name=collection_name,n filter=f"ST_WITHIN(location, '{circle}')",n output_fields=["shop_name", "category", "location"],n limit=200,n)nprint(f"[3.2] 中心点 ({center_lng}, {center_lat}) 附近 3km 半径内的门店:")nfor n in nearby:n print(f" · {n['shop_name']}({n['category']}) {n['location']}")nnn# ==================== 步骤 4:语义检索 + 地理 filter 融合进一次 search() ====================n# ANN 走评价向量负责「对味」,filter 里的 ST_ 空间函数负责「就近」,一次请求联合求解nQUERY = "附近适合亲子的川菜馆"ngeo_filter = f"category == '川菜' && ST_WITHIN(location, '{circle}')"nresults = client.search(n collection_name=collection_name,n data=[QUERY],n anns_field="review_embedding",n filter=geo_filter,n limit=20,n output_fields=["shop_id", "shop_name", "category", "location", "review"],n)nprint("n" + "=" * 64)nprint("步骤 4 | 语义检索 + 地理过滤 融合查询(一次 search 联合求解)")nprint("=" * 64)nprint(f"查询语句:{QUERY}")nprint("过滤条件:category == '川菜' 且落在附近 3km 圈内")nprint("-" * 64)nfor rank, hit in enumerate(results[0], 1):n e = hit["entity"]n print(f" {rank}. [相似度 {hit['distance']:.4f}] {e['shop_name']} | {e['review'][:24]}")nnn# ==================== 步骤 5:AI_RERANK 重排候选 ====================nRERANK_MODEL = "qwen3-rerank"nn# 5.1 在 search() 中挂 RERANK ranker(reranker="model"),ANN 召回后由模型二次精排nreranker = Function(n name="rerank_reviews",n function_type=FunctionType.RERANK,n input_field_names=["review"],n params={n "reranker": "model",n "provider": "aliyun_milvus",n "model_name": RERANK_MODEL,n "queries": [QUERY],n "timeout_sec": 10,n },n)nresults = client.search(n collection_name=collection_name,n data=[QUERY],n anns_field="review_embedding",n filter=geo_filter,n limit=20,n output_fields=["shop_id", "shop_name", "review"],n ranker=reranker,n)nprint("n" + "=" * 64)nprint("步骤 5 | AI_RERANK 大模型重排(qwen3-rerank)")nprint("=" * 64)nprint("[5.1] search() 内置 ranker:ANN 召回后由大模型二次精排")nprint("-" * 64)nfor rank, hit in enumerate(results[0], 1):n e = hit["entity"]n print(f" {rank}. [重排分 {hit['distance']:.4f}] {e['shop_name']} | {e['review'][:24]}")nn# 5.2 也可用无状态 REST 即时接口 /v2/vectordb/ai/rerankn# (documents 必须是 string 数组、query 为顶层 string、不支持 top_n,每个 doc 返回一个分)nrerank_docs = [n "带娃来的,有儿童座椅和小碗餐具,服务员很有耐心。",n "重口味爱好者天堂,水煮鱼够味,但空间偏小、比较吵,不太适合带小孩。",n]nstatus, data = post_json(n "/v2/vectordb/ai/rerank",n {n "model_name": RERANK_MODEL,n "query": QUERY,n "documents": rerank_docs,n "params": {"timeout_sec": 10},n },n)nassert status == 200 and data.get("code") == 0, datanprint(f"n[5.2] REST 即时接口 /v2/vectordb/ai/rerank(query:{QUERY})")nprint("-" * 64)nranked = sorted(data["data"]["output"]["results"], key=lambda x: x["relevance_score"], reverse=True)nfor rank, item in enumerate(ranked, 1):n print(f" {rank}. [相关度 {item['relevance_score']:.4f}] {rerank_docs[item['index']][:28]}")nnn# ==================== 步骤 6:AI_SENTIMENT 算好评率 + AI_SUMMARIZE 生成店铺摘要 ====================nshop_id = 1001nreviews = [r["review"] for r in client.query(n collection_name=collection_name,n filter=f"shop_id == {shop_id}",n output_fields=["review"],n limit=1000,n)]nn# 6.1 情感分类 → 好评率nstatus, sent = post_json("/v2/vectordb/ai/sentiment", {n "model_name": "qwen3.7-max",n "texts": reviews,n "params": {"categories": ["positive", "negative", "neutral"], "temperature": 0},n})nassert status == 200 and sent.get("code") == 0, sentnlabels = sent["data"]["output"]["outputs"]nvalid = [x for x in labels if x] # null 计入「未判定」,不参与聚合npositive = sum(1 for x in valid if x == "positive")ngood_rate = positive / len(valid) if valid else 0.0nprint("n" + "=" * 64)nprint(f"步骤 6 | AI_SENTIMENT 好评率 + AI_SUMMARIZE 摘要(shop_id={shop_id})")nprint("=" * 64)nprint(f"[6.1] 好评率:{good_rate:.0%} (有效样本 {len(valid)}/{len(labels)})")nn# 6.2 摘要 → 店铺评价一句话nstatus, summ = post_json("/v2/vectordb/ai/summarize", {n "model_name": "qwen3.7-max",n "texts": ["n".join(reviews)],n "params": {"max_words": 40, "temperature": 0,n "prompt": "用中文概括这家店的整体评价,突出招牌菜、适合人群与环境。"},n})nassert status == 200 and summ.get("code") == 0, summnprint(f"[6.2] 店铺摘要:{summ['data']['output']['outputs'][0]}")","id":"EaStA"}">

实战效果

把上面的地理过滤 → 语义检索 → rerank 精排 → 情感/摘要串起来,就是一次完整的扫街榜生成:地理过滤圈定「就近」候选 → 语义检索命中「对味」→ rerank 精排 → 情感/摘要补齐「口碑」。下面是一份示例榜单输出

扫街榜的排序逻辑由三路信号融合:

地理分:在半径/商圈内为门槛(不满足直接过滤掉),再按距离由近到远给基础权重;

语义相关分:AI_RERANK 输出的 relevance_score,衡量门店评价与用户需求「适合亲子的川菜馆」的贴合度;

口碑分:AI_SENTIMENT 聚合出的好评率。

最终榜位可用形如 w1·相关分 w2·好评率 w3·距离衰减 的加权融合,权重按业务调参。「巴蜀人家」虽然不是最辣,但因「亲子友好」的语义强命中 高好评率 距离最近,综合排到第一——这正是「扫街榜」区别于「按评分排序」的地方。

与传统多系统方案对比,简化非常直观:

维度

传统方案(PostGIS + 向量库 + NLP 服务)

阿里云 Milvus 一站式

系统数量

3+ 套,各存一份数据

1 套,数据同源

地理 + 语义融合

跨系统 JOIN / 多轮回捞

一次 search():ANN + ST_ filter

评价理解

自建/外接情感、摘要模型

AI_SENTIMENT / AI_SUMMARIZE 内置

向量化链路

应用侧调 embedding 再写入

写入即向量化(Collection Function)

一致性/同步

需自行维护

单库天然一致

","margin":true,"hideBorder":false,"id":"u7skJ"}">

总结

这套方案的价值可以收敛为一句话:把「空间」与「语义」放进同一个数据库、同一次检索里求解。地理空间类型让 Milvus 理解「哪里」,AI Function 让它理解「什么」和「好不好」,二者融合直接产出可解释的本地生活榜单,省掉了 PostGIS 向量库 NLP 三套系统的拼接与同步成本。

顺着这条路还有不少想象空间:

多模态门店检索:用 qwen3-vl-embedding 把门店图片/菜品图向量化,支持「附近有这种装修风格的店」的以图搜店,与地理过滤同样融合在一次检索里。

批量口碑加工:门店评价体量大时,用批量方式(如 AI_BATCH / 批量摘要)离线把好评率、评价摘要预计算落库,榜单查询只读结果,兼顾成本与延迟。

更丰富的空间关系:用 ST_INTERSECTS 判断门店配送多边形与用户所在网格是否相交、用 ST_CONTAINS 做商圈归属分析,把「地理 AI」的融合从「找店」扩展到调度、选址、履约等更多本地生活环节。

地理让 AI「落到地面」,AI 让地理「读懂内容」——当这两者在一个系统里协同,本地生活的每一次「附近有什么好吃的」,都能被更快、更准、更可信地回答。

让 AI 落地,不必再从复杂链路开始

阿里云 Milvus 是目前唯一支持 AI Native 的 Milvus 云服务。它不仅提供高性能、全托管的向量检索能力,更将 AI Function 与 AI-Gateway 原生嵌入数据链路,让模型能力真正成为数据库的一部分。

从数据进入阿里云Milvus 的那一刻起,理解、加工、检索和生成便可以在一条原生链路中完成。阿里云 Milvus 正在把“接入 AI”变成“原生拥有 AI”。

最新游戏

更多

Copyright©2010-2019. All rights reserved | 波波三国游戏官网|[email protected]

备案编号:湘ICP备2022015115号-4