▶ Cinematic fable · Watch on YouTube ▶ 影片版寓言 · 在 YouTube 观看

万井城的清晨是从气味开始的。

城挂在两道山脊之间,九万九千四百户人家,一户一口井,一口井一种水。水不一样,酿出来的香就不一样:北坡的井水带铁,酿出的香沉,压得住桌案;南坡的井水软,酿出的香浮,一开盖就往梁上跑;西头老槐树底下那口井,据说能让桂皮闻起来像刚下过雨。

城里有香市。香市里有个行当,叫寻香人。

客人递上一只小瓶,瓶里装着一缕说不清来路的香——可能是三十年前某户人家的嫁妆匣底,可能是从沉船木板上刮下来的漆。寻香人要把这缕香拿到鼻子底下闻一闻,然后告诉他:城里哪户人家的香,跟这一瓶最像。

早年间这活儿是跪着干的。

香谱阁里有本大账簿,一户一页,页上记着那户人家的香谱:三十六味底料各几钱几厘,火候几分。客人来了,老司香沈九就把账簿摊开,从第一页读到第九万九千四百页,一页一页地对。三天读一本。读完,把最像的那一页指给客人看。

对得准。可城还在长。每年新起的院子有一千多户,账簿每年厚一叠,读一本的时间往后推。到阿棠进阁那年,读一遍要五天。

阿棠识字不多。她是阁里扫地的。

可她天天在城里跑——替沈九送香样、取井水、跑腿买炭。跑得多了,她记得的不是字,是路。

那年冬天她对沈九说:师父,别读账了。让院子和院子之间牵绳子。

沈九问:牵绳子做什么?

阿棠说:从任意一户进门,闻一闻,再走到它牵着的最像的那一户,一直走。走到一户,它的邻居里没有一个比它更像客人那瓶香——就停。停下的那户,就是答案。

沈九算了一夜,说:好。可是绳子怎么牵?

第一版是这样的:每户人家牵十六根绳子,牵给城里香最像它的十六户。

这一版在城东试了三个月。

三个月后,阿棠把绳子全解了。

问题不在绳子,在路太远。九万九千四百户,每户十六条绳子,城太大,绳子太短。从一个陌生的门口起步,你要沿着”更像一点、再更像一点”的小碎步,走过上千户人家,才勉强摸到客人的那一片香。

更糟的是它会走丢。城东有条窄巷,十七户人家,香都极像——像到十七户的绳子全都牵在自己人身上,一根都不往外。阿棠那次走到巷口,进了巷子,就再没出来。她在里面转了四十多步,每一户都比上一户更像,可每一户都不是。

绳子太短,是因为只连了近亲。

那天夜里她坐在井台上,看城里的灯。

城是依山起的。从山脚往上看,灯是下面密、上面疏:山脚一层挨一层,亮成一片;半山腰稀稀落落几点;山顶只有三盏。

她忽然想通了一件事:要是城里也能分几层就好了。

底层,所有人家都在——九万九千四百户,绳子短,一户一户地挪。 上面一层,只留几百户,绳子长,一跨能跨半个城。 再上面一层,几十户,绳子更长。 最顶上,三户。

在顶上走一小步,底下就是十里地。

可麻烦立刻来了:谁上二楼?

城里每天都有新院子起梁。阿棠不可能先看完整座城再决定谁上楼——院子是一户一户冒出来的,城的样子要等城长完了才知道。这是这个法子最要命的地方。

她的办法是:抽签。

每户人家起梁那天,在门槛上掷一次骰子。

大多数人家的骰子落在”一层”,这辈子只在地面待着,牵地面的短绳。 少数落在”二层”——它在地面有一副绳子,在二楼还有一副绳子,二楼那副牵给同样抽中二层的几百户。 更少数落在”三层”。 极少落在”四层”。

骰子是偏的:每上一层,中签的人家少掉一大半。落到三层四层的,全城也就几十户、几户。

抽中几层,是起梁那天定死的,此后一生不改。院子不知道城将来会变成什么样,可只要骰子是偏的,楼层就自己长成了上疏下密的形状。

而且这不需要谁去统筹。城东新起的一户人家,不认识城西任何一户,它只管掷骰子、牵绳子。九万户这么各干各的,楼层还是那个形状。

于是寻香变成了一次下楼。

客人递瓶。阿棠先上山顶那三户中的一户——那是入口。 在顶层,她只做一件事:看看三户邻居里哪一户的香更像客人这瓶,走过去;再看新到这一户的邻居里哪一户更像,走过去。顶层人少,几步就到头。 然后她顺着这户人家自己的楼梯,下一层。 在下一层,还是同样的走法——只往更像的那边走。这一层人多些,绳子短些,步子小些。 再下一层。 最后一层是地面,九万九千四百户,她在这里走最后几十步。

那一天她走了九步,敲开了城南一户做陈皮的人家的门。

沈九把账簿翻到那一页,对了一遍,抬头看她,很久没说话。

—

可是第二版还是栽了一次。

栽在一个字上:亲。

第二版里,每户人家牵绳子,牵的是”香最像我的那十六户”。这听起来天经地义,其实是个陷阱。

一层楼里,如果每户都只牵自己最像的邻居,那么香相近的一群人家会互相牵成一团:你牵我,我牵你,绳子全在圈里,谁也不往外。这一团就成了一座孤岛。从外面走进来的人,进了圈就只能在圈里打转;圈外更近的答案,一根绳子都够不着。

阿棠在城北的染坊街见过这样一团:二十九户人家,绳子加起来四百多根,往圈外去的只有两根。

改了规矩:牵绳子的时候,不牵”最像我的”,牵”只有我牵得着的”。

具体是这样。一户人家要牵十六根绳子,来了个新邻居想牵它,先别急着答应。看看这根绳子指的方向,是不是已经有一根绳子指着了。如果新邻居比”已经牵上的那户”更靠近自己,而且比”已经牵上的那户”更远离那些已牵的人——换句话说,它指向一片还没有绳子的地方——那就牵上。

有时候,这意味着要砍掉一根更近的绳子,换上一根更远的。

看着是亏了。其实是把孤岛拆了。

一根更远的绳子,往往正是通往另一个街区的那扇门。

—

最后一个难题,是阿棠手里那只篮子。

起初她只记一件事:目前最像的那一户。走到那儿,再找比它更像的。这个走法干脆,可它会停在假山顶上——那户人家的邻居都不如它像,于是停下;可只要往旁边挪一户、忍一步”变差”,再往前就是真正的那片香。

所以出门要带篮子。

篮子装得下十六户人家——她一路上见过的、最像的前十六户,全揣着。每走一步,从篮子里挑最像的那一户还没敲过门的去敲;敲完,把新见到的人家也丢进篮子,把篮子里最不像的挤出去。什么时候停?当篮子里剩下的人家,没有一户能比篮子底那户更像客人的香时,停。

篮子大,走得准,也走得慢。篮子小,走得快,可能错过。

阿棠最后定的是十六。她说,够用了。

—

万井城后来没有烧掉那本大账簿。账簿还留在香谱阁里,一年开一次,用来校新法子有没有走偏。

阿棠在城门口立了块木牌,上面刻了她这辈子唯一写下的两行字:

这是张猜出来的图。 它不保证找到最像的那一户。它保证,几乎每次都找到最像的那几户里的一个。

沈九看了半天,说:你少刻了一句。

阿棠问:哪句?

沈九说:猜得快,才有人用。

— —

By now you’ve probably recognized it: this is HNSW.

这是什么

HNSW(Hierarchical Navigable Small World,分层可导航小世界)是目前最主流的近似最近邻(approximate nearest neighbor,ANN)索引之一。它要解决的问题是:给定一个查询向量,在上亿条向量里找出最相似的 k 条——而不必把每一条都算一遍距离。

它的结构就是几层邻近图(proximity graph)叠在一起:

  • 第 0 层装全部向量。每个点最多连 M_max0 = 2M 条边,边只连给距离近的点。
  • 第 1 层及以上只装一部分点,越往上越稀疏,上层每个点最多连 M_max = M 条边。层数按指数衰减分配。

一个点被分到几层,是插入时随机决定的,公式是 l = floor(-ln(U) * mL),其中 U ~ Uniform(0,1),mL = 1/ln(M)。这意味着大约 1/M 的点能上到第 1 层,1/M² 到第 2 层——不需要任何全局信息,也不需要建完之后再重排。新点进来时只管自己掷骰子、连边,整体就自然长成”下密上疏”的形状。

查询分两段。上层的目标不是找答案,是找入口:从最顶层的入口点开始,每层做一次贪心下降(ef = 1),只走向距离查询更近的邻居,走到局部最小,然后从当前点落到下一层继续。到了第 0 层,改用候选堆搜索(best-first search):维护一个大小为 ef(efSearch)的候选集,每次展开候选中距离最小的那个,收集到的邻居塞回候选集,剪掉最差的;当候选集里最优点都不比已收集结果的最差点更好时停止,返回前 k 个。

插入是查询的复用:先从顶层贪心找到该层入口,然后从该点被分配到的最高层一路向下,每一层都用 efConstruction 大小的候选堆找出 M 个最近邻,双向连边,再用启发式剪枝(HNSW 论文 Algorithm 4)把超额的边裁掉。

那个启发式剪枝,就是阿棠改的那条规矩:候选邻居 e 只有在”它到新点的距离 < 它到任何一个已选邻居的距离”时才保留。换成人话——只有当这条边指向一个现有边没覆盖的方向时,才留它。这条规则允许为了更远的、方向不同的邻居而砍掉一个更近的、方向重复的邻居。没有它,图会退化成一簇簇互不连通的密集小团,贪心走进去就出不来。

复杂度上,HNSW 的查询是 O(log N) 量级,插入也是;代价是内存:每个点要存 M_max0 + M × (层数-1) 条边,每条约 4 字节。

为什么重要

嵌入向量(embedding)已经是语义搜索、RAG 检索、推荐召回、图片去重、异常检测的通用接口。但这些系统的规模让暴力检索失效:一千万条 768 维向量,一次全量扫描是 76.8 亿次浮点乘加——单次查询几百毫秒到几秒,QPS 上不去,成本也压不下来。

ANN 索引就是拿一点召回率(recall)换几个数量级的延迟。HNSW 在这个交换里长期占据甜点位置:在内存里、低延迟、高召回,不需要像 IVF 那样先聚类再探测若干桶,也不需要像 PQ 那样把向量压成码本近似距离。Faiss、hnswlib、Qdrant、Weaviate、Milvus、pgvector、Elasticsearch 的向量检索,默认或主力索引都是它。

它的重要之处,也是它的坑:

  • 它是近似的,而且这个”近似”是产品决策,不是 bug。 衡量指标是 recall@k——返回的 k 条里有多少真在前 k 里。调 efSearch 就是在延迟和召回之间滑动;多数线上系统接受 0.95 而不是 0.999,因为最后那 5 个点要再花一倍时间。
  • 参数不是玄学,但也没有普适值。 M 决定图的连通度和内存;efConstruction 决定建图质量(只影响建索引,不影响查询);efSearch 是运行时旋钮。M 太小,图断成孤岛;M 太大,内存和建图时间线性上涨,召回却很快饱和。
  • 贪心会困在局部最优。 这就是为什么上层要稀疏:稀疏层的长边提供了跨区域的跳跃能力,让下降过程先粗后细。这也是”small world”的含义——少数长程连接把图的直径压得很小。
  • 删除很别扭。 图结构不支持干净地摘掉一个节点(会有别的点的边指过来),工程上一般是打墓碑、或者定期重建整张图。频繁增删的场景,维护成本比查询成本更值得关注。
  • 过滤检索是真正的难点。 “在满足 tenant_id = 42 且 created_at > ... 的向量里找最近邻”——先过滤再搜会把图打断(过滤后子图可能不连通),先搜再过滤会漏。这就是带元数据过滤的向量库普遍更慢、召回更差的原因。
  • 它是一张图,所以很难分片。 图没有天然的哈希边界,跨分片的边要么被切断(召回掉),要么被复制(内存涨)。这也是大规模向量检索系统往往在”分片 + 每片独立 HNSW + 结果归并”和”单机大内存”之间反复权衡的原因。

隐喻对应表

  • 城里九万九千四百户人家 → 向量库里的全部向量(N 个点)
  • 一户人家的”香” → 一个向量(embedding)
  • 两户人家的香有多像 → 向量之间的距离/相似度(余弦、内积、L2)
  • 客人递来的小瓶 → 查询向量(query vector)
  • 寻香人要找”最像的几户” → 最近邻检索(k-NN)
  • 香谱阁的大账簿,逐页对 → 暴力检索(brute force / flat 索引),O(N·d)
  • 读一遍要五天,城还在长大 → 全量扫描的延迟与成本随规模线性上涨
  • 院子之间牵绳子 → 邻近图的一条边(edge)
  • 每户最多十六根绳子 → 每点最大出度 M(第 0 层为 2M)
  • 从任意一户起步,只走向”更像”的邻居 → 图上的贪心搜索(greedy search)
  • 走进窄巷再也出不来 → 局部最优 / 图不连通导致的搜索失败
  • 山脚灯密、山顶只有三盏 → 分层结构,上层按指数衰减保留少量点
  • 起梁那天掷一次骰子 → 插入时随机分配层数 l = floor(-ln(U) · mL)
  • 每上一层少掉一大半人家 → 几何分布,约 1/M 的点进入上一层
  • 骰子偏,但没人统筹 → 局部随机决策自然产生全局的分层形状
  • 顺着这户人家自己的楼梯下一层 → 从当前点下降到下一层同一节点继续搜索
  • 顶层几步跨半个城 → 稀疏层上的长边提供跨区域跳跃(小世界性质)
  • 地面走最后几十步 → 第 0 层上的 ef 候选堆精细搜索
  • 只往”更像”的那边走 → ef = 1 的贪心下降
  • 香相近的人家互相牵成一团,谁也不往外 → 只连最近邻会让图退化成密集孤岛
  • 牵”最像我的” → 朴素的 k 近邻连边(nearest-neighbor graph)
  • 牵”只有我牵得着的” → 启发式邻居选择:只保留指向新方向的边
  • 砍掉更近的绳子,换一根更远的 → 为多样性牺牲距离,换取图的连通与可导航性
  • 篮子揣着十六户 → 候选集大小 efSearch
  • 忍一步”变差”再往前走 → ef > 1 的搜索能越过局部最优
  • 篮子大走得准也走得慢 → efSearch 的召回/延迟权衡
  • 停在”篮子里没人能更好”的那一刻 → 候选集耗尽即终止条件
  • 九步敲对门 → O(log N) 量级的查询复杂度
  • 木牌上”这是张猜出来的图” → HNSW 是近似算法,不保证精确最近邻
  • “不保证找到最像的,保证找到最像的几个之一” → recall@k:以召回率而非精确性衡量
  • 账簿一年开一次,用来校偏 → 用精确检索做离线评估,校准 ANN 的召回
  • “猜得快,才有人用” → ANN 的全部价值在于用可控的召回损失换取数量级的延迟下降
  • 城东新户不认识城西任何户,自己掷骰子就行 → 插入是纯局部的,不需要全局协调
  • 拆孤岛的那两根外指绳子 → 少量长程边决定了整张图能否被导航

The City of Ten Thousand Wells wakes to smell.

The city hangs between two ridges. Ninety-nine thousand four hundred households, one well apiece, one water apiece. The water differs, so the scent differs: the north slope’s well water carries iron and the scents it distills sit low and heavy; the south slope’s water is soft and its scents float up to the rafters; the well beneath the old locust tree at the west end is said to make cassia smell like rain just fallen.

There is a scent market in the city, and in the scent market there is a trade: scent finding.

A customer hands you a small phial holding a smell of unclear origin — the bottom of a dowry chest thirty years gone, or lacquer scraped off the planks of a sunken ship. You hold it under your nose, breathe once, and then tell them: which household in this city smells most like this.

For years the work was done on your knees.

The Pavilion of Scent Records kept a great ledger, one page per household, each page recording that household’s scent recipe: thirty-six base notes in exact qian and li, with firing times. When a customer came, old master Shen Jiu opened the ledger at page one and read to page ninety-nine thousand four hundred, comparing, page by page, for three days. Then he pointed at the closest page.

It was accurate. But the city kept growing. More than a thousand new courtyards rose each year, the ledger thickened by a sheaf each year, and every reading took longer. By the year A-Tang came to the pavilion, one reading took five days.

A-Tang could barely read. She swept the floors.

But she ran the city all day — carrying samples for Shen Jiu, fetching well water, buying charcoal. What she remembered was not pages. It was roads.

That winter she said to him: Master, stop reading the ledger. String ropes between the courtyards instead.

Shen Jiu asked: ropes for what?

A-Tang said: walk into any household, smell once, then step to whichever household it is roped to that smells closer. Keep walking. Stop at a household whose neighbors include nothing that smells closer to the customer’s phial than it does itself. That household is your answer.

Shen Jiu worked at it all night and said: fine. But how do you decide which ropes to string?

The first version went like this: every household strings sixteen ropes, each to the sixteen households whose scent is closest to its own.

They tried it in the east quarter for three months.

After three months, A-Tang untied every rope.

The trouble was not the ropes. It was the reach. Ninety-nine thousand four hundred households, sixteen ropes apiece; the city was too wide, and the ropes were too short. Starting from a doorway you had never seen, you would take a thousand shuffling half-steps — slightly closer, slightly closer — just to graze the district where the customer’s scent lived.

Worse, you got lost. There was a narrow lane in the east quarter, seventeen households, their scents so alike that all seventeen had roped themselves to each other and not one rope went out. A-Tang walked to the mouth of that lane, stepped in, and never came out again. She circled inside for forty-odd steps. Every household was closer than the last. None of them was right.

The ropes were too short because they only tied kin.

That night she sat on the well curb and watched the city’s lamps.

The city climbs a hillside. Looking up from the foot, the lights are dense below and thin above: at the base, row upon row, a single sheet of brightness; halfway up, scattered points; at the summit, three lamps.

And she understood one thing: the city needed floors.

At the bottom, everyone — ninety-nine thousand four hundred households, short ropes, one household at a time. One floor up, only a few hundred remained, and their ropes were long: one strand could cross half the city. Another floor up, a few dozen, ropes longer still. At the very top, three.

One small step at the top is ten li on the ground.

But then came the trouble: who gets a second floor?

New courtyards raise their beams every day. A-Tang could not survey the whole city first and then decide who goes upstairs — the courtyards appeared one at a time, and the shape of the city was only knowable once the city was finished. That was the fatal flaw in the whole notion.

Her answer was: draw lots.

On the day a household raises its beam, it throws a die on the threshold.

Most households land on “ground” and stay at street level their whole lives, stringing short ropes. A few land on “second” — they keep one set of ropes on the ground and another set on the second floor, where they are roped to the few hundred households that also drew a second floor. Fewer still land on “third.” Very rarely, a fourth.

The die is weighted: with every floor up, better than half of the remaining households drop out. The third and fourth floors together hold a few dozen households in the whole city, then a handful.

The floor you draw is fixed on beam-raising day and never changes. The courtyard cannot know what the city will become, but as long as the die is weighted, the floors grow themselves into a shape that is dense below and sparse above.

And none of it needs a coordinator. A new household in the east quarter knows nobody in the west quarter; it throws its die and strings its ropes. Ninety thousand households each doing only that, and the floors come out the same shape.

So scent finding became a descent.

The customer hands over the phial. A-Tang climbs to one of the three households at the summit — that is the entrance. On the top floor she does exactly one thing: look at the three neighbors, step to whichever smells closer to the customer’s phial, then again from that one. The top floor is nearly empty; a few steps and you are done. Then she takes that household’s own staircase down one floor. On the next floor, the same walk — only toward what smells closer. More households here, shorter ropes, smaller steps. Down again. The last floor is the ground, all ninety-nine thousand four hundred, and there she walks the final few dozen steps.

That day she took nine steps and knocked on the door of a dried-tangerine-peel household in the south of the city.

Shen Jiu turned the ledger to that page, checked it, looked up at her, and said nothing for a long while.

—

But the second version still failed once.

It failed on a single word: kin.

In the second version, when a household strung its ropes, it strung them to “the sixteen households that smell most like me.” That sounds self-evident and is in fact a trap.

On a single floor, if every household ropes only to its closest neighbors, then a group of households with similar scents ropes itself into a knot: you to me, me to you, every rope inside the ring, nothing out. The knot becomes an island. Someone arriving from outside can only circle within it; the closer answer outside is not reachable by a single rope.

A-Tang saw one on the dyers’ street in the north quarter: twenty-nine households, more than four hundred ropes among them, and exactly two going out.

So the rule changed: do not string to “the one most like me.” String to “the one only I can reach.”

Concretely: a household is allowed sixteen ropes. A new neighbor asks to be roped. Do not simply agree. Look at the direction that rope would point — is a rope already pointing there? If the newcomer is closer to me than the one already roped, yet farther from that already-roped one than it is from me — in other words, if it points somewhere no rope yet points — then tie it.

Sometimes this means cutting a nearer rope and keeping a farther one.

It looks like a loss. It is how you dismantle an island.

A farther rope is often exactly the door into the next district.

—

The last problem was the basket on A-Tang’s arm.

At first she remembered only one thing: the household that currently smelled closest. Walk there, then look for something closer. That walk is decisive, and it stops on false summits — a household whose every neighbor smells worse, so you halt, when one household to the side, one tolerated step of “worse,” leads on to the true district.

So you carry a basket.

The basket holds sixteen households — the sixteen closest she has met so far on the whole walk. At every step she takes the closest household in the basket whose door she has not yet knocked on; knocks; drops the new households she meets into the basket; and evicts the ones that smell least like the customer’s phial. When does she stop? When nothing left in the basket can beat the household at the bottom of it.

A big basket walks accurately and slowly. A small basket walks fast and may miss.

A-Tang settled on sixteen. Good enough, she said.

—

The city never burned the great ledger. It stayed in the Pavilion of Scent Records, opened once a year, to check whether the new method had drifted.

At the city gate A-Tang raised a wooden board, carved with the only two lines she ever wrote:

This is a map made of guesses. It does not promise the household that smells most like yours. It promises that, nearly every time, it finds one of the few that do.

Shen Jiu studied it for a while and said: you left out a line.

Which one? she asked.

He said: A guess is only worth anything if it is fast.

— —

By now you’ve probably recognized it: this is HNSW.

What it is

HNSW — Hierarchical Navigable Small World — is one of the most widely deployed approximate nearest neighbor (ANN) indexes. The problem it solves: given a query vector, find the k most similar vectors among hundreds of millions, without computing the distance to every single one.

Its structure is several proximity graphs stacked in layers:

  • Layer 0 holds every vector. Each node keeps at most M_max0 = 2M edges, and edges only go to nearby points.
  • Layers 1 and up hold a subset of points, sparser the higher you go, and each node keeps at most M_max = M edges. Levels are assigned with exponential decay.

Which layers a point occupies is decided at insertion time, randomly: l = floor(-ln(U) * mL), with U ~ Uniform(0,1) and mL = 1/ln(M). Roughly 1/M of points reach layer 1, 1/M² reach layer 2 — no global knowledge required, and no re-layout after the fact. A new point simply throws its die and strings its ropes, and the whole structure naturally grows dense below and sparse above.

Search happens in two phases. The upper layers are not there to find the answer; they are there to find the entry point. Starting from the entry node at the top layer, each layer runs a greedy descent (ef = 1): step only to a neighbor closer to the query, until you hit a local minimum, then drop through the same node to the next layer down. At layer 0 the search switches to a best-first candidate heap: maintain a candidate set of size ef (efSearch), always expand the closest unexpanded candidate, push its neighbors back into the set, evict the worst; stop when the best remaining candidate is no better than the worst result already collected, and return the top k.

Insertion reuses the search: greedy descent from the top to find the entry at each layer, then from the new point’s assigned top level downward, run an efConstruction-sized candidate search at each layer to find M neighbors, add bidirectional edges, and prune the excess with the paper’s heuristic neighbor selection (Algorithm 4).

That heuristic is precisely the rule A-Tang changed: keep candidate neighbor e only if e is closer to the new point than it is to any already-selected neighbor. In plain words — keep this edge only if it points somewhere no existing edge points. The rule permits cutting a nearer, redundant-direction neighbor to keep a farther one that opens a new direction. Without it, the graph degenerates into dense, mutually disconnected clumps, and greedy search walks in and never walks out.

Query and insert both cost O(log N). The price is memory: each point stores M_max0 + M × (levels - 1) edges, roughly 4 bytes per edge.

Why it matters

Embeddings are now the common interface for semantic search, RAG retrieval, recommendation recall, image deduplication, and anomaly detection. But at production scale, brute force collapses: ten million 768-dimensional vectors is 7.68 billion floating-point multiply-adds per query — hundreds of milliseconds to seconds each, which caps QPS and inflates cost.

ANN indexes trade a little recall for orders of magnitude in latency. HNSW has long occupied the sweet spot in that trade: in-memory, low-latency, high-recall, without IVF’s cluster-then-probe-buckets structure or PQ’s compressed-codebook distance approximation. Faiss, hnswlib, Qdrant, Weaviate, Milvus, pgvector, and Elasticsearch’s vector search all ship it as the default or primary index.

That importance is also where the sharp edges are:

  • It is approximate, and that approximation is a product decision, not a bug. The metric is recall@k — how many of the true top k you actually returned. Turning efSearch up slides along the latency/recall curve; most production systems accept 0.95 rather than 0.999, because the last few points cost another multiple of the query time.
  • The parameters are not mysticism, but there is no universal setting. M sets graph connectivity and memory; efConstruction sets build quality (it affects indexing only, never query time); efSearch is the runtime dial. Too small an M and the graph fragments into islands; too large and memory and build time rise linearly while recall saturates almost immediately.
  • Greedy search gets trapped in local optima. That is exactly why the upper layers must be sparse: long edges in sparse layers provide the cross-region jumps that let the descent go coarse-to-fine. This is the “small world” part — a handful of long-range links collapse the graph’s diameter.
  • Deletion is awkward. A graph does not let you cleanly remove a node, because other nodes’ edges point at it. In practice you tombstone it or periodically rebuild the whole index. In write-heavy, delete-heavy workloads, maintenance cost matters more than query cost.
  • Filtered search is the real hard problem. “Nearest neighbors where tenant_id = 42 and created_at > ...” — filter first and the subgraph may be disconnected; search first and you miss. This is why vector databases with metadata filtering are consistently slower and lower-recall than the unfiltered case.
  • It is a graph, so it shards badly. Graphs have no natural hash boundary: cross-shard edges either get cut (recall drops) or duplicated (memory grows). Large-scale vector search systems therefore keep re-litigating “shard + independent HNSW per shard + merge results” versus “one big box with a lot of RAM.”

Metaphor mapping

  • Ninety-nine thousand four hundred households → every vector in the store (N points)
  • A household’s scent → a vector (embedding)
  • How alike two scents are → distance or similarity between vectors (cosine, inner product, L2)
  • The customer’s phial → the query vector
  • Finding the few households that smell most alike → k-nearest-neighbor search
  • The great ledger, read page by page → brute-force / flat search, O(N·d)
  • Five days per reading, and the city still growing → scan cost rising linearly with corpus size
  • Ropes strung between courtyards → edges of a proximity graph
  • At most sixteen ropes per household → maximum out-degree M (2M at layer 0)
  • Start anywhere, step only toward a closer-smelling neighbor → greedy search on the graph
  • Walking into the narrow lane and never getting out → local optimum / disconnected graph
  • Lights dense at the base, three lamps at the summit → layered structure, exponentially fewer points per layer
  • One die throw on beam-raising day → random level assignment at insertion, l = floor(-ln(U) · mL)
  • Better than half the households lost per floor up → geometric distribution, about 1/M reach the next layer
  • Weighted die, no coordinator → local randomness producing a global layered shape
  • Taking that household’s own staircase down → descending from the current node to the next layer
  • A few top-floor steps crossing half the city → long edges in sparse layers enabling cross-region jumps
  • The last few dozen steps on the ground → fine-grained ef search at layer 0
  • Stepping only toward what smells closer → greedy descent with ef = 1
  • Similar households roped into a knot with nothing pointing out → nearest-neighbor-only graphs degenerating into dense islands
  • Roping to “the ones most like me” → naive k-nearest-neighbor edge selection
  • Roping to “the one only I can reach” → heuristic neighbor selection keeping only new-direction edges
  • Cutting a nearer rope for a farther one → sacrificing distance for diversity and navigability
  • A basket holding sixteen households → candidate set size efSearch
  • Tolerating one step that smells worse → ef > 1 escaping local optima
  • A big basket is accurate and slow → the efSearch recall/latency tradeoff
  • Stopping when nothing in the basket can do better → termination condition when the candidate set is exhausted
  • Nine steps to the right door → O(log N)-scale query complexity
  • “This is a map made of guesses” → HNSW is approximate, with no exactness guarantee
  • “Not the closest, but one of the closest few” → recall@k as the metric of record
  • Opening the ledger once a year to check for drift → offline exact search used to calibrate ANN recall
  • “A guess is only worth anything if it is fast” → the entire value of ANN is trading bounded recall loss for order-of-magnitude latency wins
  • A new household needs no knowledge of the far city → insertion is purely local, with no global coordination
  • The two outward ropes that dismantle an island → a small number of long-range edges determine whether the graph is navigable at all
Daily Fables每日寓言 2026-10-28