2、需求分析:用户故事与用例、核心功能(搜索、分词、排序)、非功能需求(性能、可扩展性)

好,咱们正式开始动手之前,得先把「做什么」想清楚。我见过太多项目,代码写了一半才发现需求没对齐,最后推倒重来。搜索引擎尤其如此——你想想看,一个搜索框背后藏着多少逻辑?

这一章,咱们就聊聊需求分析。说白了,就是搞清楚三件事:用户到底想要什么?系统必须提供哪些核心能力?以及,这个系统得扛得住多大的场面?

2.1 用户故事与用例:别猜,去问

我个人习惯,拿到一个项目先写用户故事。不是写那种「用户希望搜索速度快」的废话,而是具体到「谁,在什么场景下,要达成什么目标」。

一个典型的用户故事模板:

作为 [角色],我想要 [功能],以便 [价值]

拿咱们这个搜索引擎项目举例,我通常会列出这么几个核心角色:

  • 普通用户:输入关键词,找到想要的内容。
  • 高级用户:希望用引号、减号等语法精确控制搜索。
  • 系统管理员:监控索引状态、查看搜索日志、调整权重。

对应的用户故事长什么样?我写几个给你看:

故事1:作为普通用户,我想要输入“Python教程”就能看到相关结果,
      以便快速找到学习资料。

故事2:作为高级用户,我想要搜索“Python -爬虫”,
      以便排除掉我不想要的爬虫相关内容。

故事3:作为管理员,我想要查看最近一小时的搜索热词,
      以便了解用户当前最关心什么。

嗯,这里要注意:用户故事不是需求文档,它只是沟通的起点。写完故事之后,我会把它们转化成具体的用例。用例更细致,会描述前置条件、正常流程、异常流程。

举个例子,「搜索」这个用例:

用例名称 执行搜索
参与者 普通用户
前置条件 用户已打开搜索页面,索引数据已就绪
正常流程 1. 用户输入查询词
2. 系统进行分词
3. 系统检索倒排索引
4. 系统按相关性排序
5. 返回结果列表
异常流程 1. 输入为空 → 提示“请输入搜索词”
2. 无结果 → 显示“未找到相关内容”
3. 索引损坏 → 返回错误页面

我的经验:写用例时,一定要把异常流程写清楚。我在项目中遇到过,上线第一天用户搜了个空字符串,系统直接崩了——就是因为没处理这个边界情况。

2.2 核心功能:搜索、分词、排序

核心功能就三个,但每一个都值得单独开一章。这里我先给你个全景图,让你心里有数。

2.2.1 搜索

搜索不只是「输入关键词,返回结果」。它包含几个子功能:

  • 基础搜索:单关键词匹配
  • 短语搜索:用引号包裹,精确匹配整个短语
  • 布尔搜索:支持 AND、OR、NOT 逻辑
  • 模糊搜索:拼写错误时也能找到相近结果

你可能会问:这些功能都要做吗?我的建议是,第一版只做基础搜索和短语搜索。别贪多,贪多嚼不烂。我早期做过一个项目,一上来就想支持 10 种搜索语法,结果光解析器就写了三周,最后用户根本不用。

2.2.2 分词

分词是中文搜索引擎的命门。英文单词天然有空格分隔,中文没有。比如「南京市长江大桥」,怎么切?

  • 「南京 / 市长 / 江大桥」
  • 「南京市 / 长江大桥」

两种切法,意思完全不同。这就是分词的难点。

咱们这个项目,我打算用正向最大匹配法作为基础算法。原理很简单:从前往后,尽可能匹配最长的词。

词典:["南京", "南京市", "长江", "长江大桥", "市长", "大桥"]
句子:"南京市长江大桥"

正向最大匹配过程:
1. 从"南"开始,取最大长度"南京市" → 匹配成功
2. 从"长"开始,取最大长度"长江大桥" → 匹配成功
结果:南京市 / 长江大桥

避坑指南:我曾经在分词上踩过一个坑——没有处理未登录词。比如用户搜「哔哩哔哩」,词典里没有这个词,结果被切成了「哔 / 哩 / 哔 / 哩」。后来我加了一个策略:如果连续单字超过 3 个,就合并成一个新词。效果好了很多。

2.2.3 排序

搜索出来结果,怎么排?不能只按时间倒序,也不能只按关键词出现次数。得综合多个因素。

我常用的排序公式长这样:

得分 = TF-IDF 相关性 × 0.6 + 页面权重 × 0.3 + 时间衰减 × 0.1
  • TF-IDF:衡量关键词在文档中的重要程度
  • 页面权重:类似 PageRank,越重要的页面权重越高
  • 时间衰减:越新的内容得分越高

这三个权重不是固定的。我在项目中习惯先设一个初始值,然后根据用户点击数据不断调整。比如发现用户总是点第三个结果,那说明排序可能有问题。

2.3 非功能需求:性能与可扩展性

功能需求决定「能不能用」,非功能需求决定「好不好用」。搜索引擎这种系统,性能差一点,用户就跑了。

2.3.1 性能

性能指标我重点关注三个:

指标 目标值 说明
搜索响应时间 P99 < 200ms 99% 的请求在 200ms 内返回
索引更新延迟 < 5 分钟 新内容 5 分钟内能被搜到
并发支持 1000 QPS 每秒处理 1000 个查询

怎么达到这些目标?我常用的手段:

  • 倒排索引:提前建好,搜索时直接查,不用遍历所有文档
  • 缓存:热门查询结果缓存到内存,减少重复计算
  • 异步索引:写入和搜索分离,不互相阻塞

一个小技巧:我在项目中经常用「预热缓存」的方式。比如每天早上 8 点,把前一天的热门搜索词提前加载到缓存里。这样早高峰来了,系统能扛得住。

2.3.2 可扩展性

可扩展性说白了就是:数据量翻倍了,系统还能不能跑?

我见过不少项目,初期数据量小,单机跑得飞快。等数据涨到 100G,直接卡死。然后开始加班重构……

所以从一开始,咱们就要考虑扩展方案:

  • 水平分片:按文档 ID 取模,把索引分布到多台机器上
  • 副本机制:每个分片保留 2-3 个副本,既提高可用性,也分担查询压力
  • 无状态设计:搜索服务本身不存数据,所有状态都放在外部存储里

举个例子,假设我们有 4 台机器:

分片策略:文档 ID % 4
- 机器1:ID % 4 == 0 的文档
- 机器2:ID % 4 == 1 的文档
- 机器3:ID % 4 == 2 的文档
- 机器4:ID % 4 == 3 的文档

查询时,把请求广播到所有机器,然后合并结果。

这样做的好处是,如果数据量翻倍,只需要加机器,改一下分片数量就行。代码几乎不用动。

曾经踩过的坑:我做过一个项目,分片策略用的是「按时间分片」。结果某个月数据量暴增,那个分片直接被打爆了。后来我改成按 ID 哈希分片,负载均匀多了。所以分片策略一定要选好,别偷懒。

小结

这一章咱们把需求理清楚了。用户故事告诉我们「为谁做」,用例告诉我们「怎么做」,核心功能和非功能需求告诉我们「做成什么样」。下一章,咱们就开始搭架子,把搜索引擎的骨架立起来。

嗯,记住一句话:需求分析花的时间,会在后面十倍百倍地省回来。别急着写代码,先把需求想透。