项目复盘 · 2018
让金融资料可以被研究:一套段落级搜索系统的演进
从需求工单、产品文档和两座核心代码仓交叉还原:多源金融资料如何经过解析、语义改写、组合过滤、排序和段落定位,最终进入研究工作台。
这套产品真正解决的不是“有没有文档”,而是研究者怎样在公告、研报、新闻、专利和公司资料中迅速找到一段可以引用的证据。通用搜索通常在返回文件列表时结束,这个项目却继续处理了筛选、段落高亮、原文定位、企业关系、收藏、笔记和文章编辑。因此,它更接近一套以搜索为入口的研究工作台。
本文把结论分为四类:文档事实来自需求、测试或操作文档;代码事实来自当前保存的仓库快照;合理推断是多份证据能够相互解释但缺少正式验收记录的判断;缺失证据表示现有材料不足以确认。这样的区分很重要:工单“完成”只能证明当时有人关闭了事项,代码存在只能证明实现意图,二者都不自动等于长期稳定运行。
下面的等价伪代码压缩了主服务的查询构造顺序。它不是原文件逐字复制,但数值与组合方式均由当前源码核对。
等价伪代码
q = smart_enabled ? rewrite_with_synonyms(input.q) : input.q
must = query_string(
query=q,
fields=scope_or(["title", "paragraph.text"]),
operator="AND",
phrase_slop=input.slop
)
phrase_boost = phrase_query(q, slop=50, boost=20)
code_sets = [selected_tickers]
code_sets += codes_for_industry(input.industry)
code_sets += codes_for_market_cap(input.market_cap)
codes = intersection_of_non_empty(code_sets)
filters = [time_range, board_terms, source_terms, codes]
score = text_score * type_factor * gaussian_time_decay
return search(must, phrase_boost, filters, score)时间线:产品先补研究闭环,再逐步固化搜索内核
文档事实|2017 年 12 月。 智能金融工单在一个月内集中创建了 63 条事项。最初的范围同时包括同义词扩展、图谱扩展、上传、账号、标签、书签、笔记与工作台;很快又出现“大粒度分词破坏相关性”“空格导致无结果”“同义词段落排序不合理”“搜索结果高亮不完整”等具体缺陷。12 月下旬,团队完成了从搜索结果统计相关上市公司、由产业节点反向发起搜索,以及文章内摘录、标签、创建文章和多文章自动保存等工作。
文档事实|2018 年 1 月至 4 月。 需求从“能搜”转向“研究时是否好用”:移动端适配、报告来源与类型展示、结果分页、文内搜索、笔记缓存、导出、图谱交互和引导页陆续进入排期。相关工单共 136 条,114 条完成,21 条停留在待办,1 条处理中;这组状态说明核心检索与研究工作流已被反复测试,但自动报告、事件流等更宏大的设想没有在该项目板上收口。
代码事实|2017 年 8 月至 2019 年 1 月。 公告数据仓保存 858 次提交。2018 年 6 月后的历史能看到抓取延时优化、表格检测、社交内容采集、PDF 转文章管线拆分、阻塞任务接入事件循环、失败日志和证券信息更新等连续修正。仓库头部有 64 个主要采集与处理源码文件,以及三千余份按证券代码保存的关系数据快照;后者占文件数的大头,不能与自研代码量混为一谈。
代码事实|2018 年 6 月至 2019 年 9 月。 在线主服务保存 741 次提交。可辨认的演进包括分层查询、筛选项调整、检索分数权重修改、以证券代码代替名称解决公司更名、缓存接入,以及把旧键值存储中的 PDF 页面和图片逐步迁到对象与文档存储。这个时间段晚于前述 Jira 集中期,说明当前代码快照更像后续持续维护的实现,而不是 2017 年末版本的原样归档。
资料进入索引前,已经经历一次结构化加工
代码事实。 采集端不是单一爬虫,而是按公告、研报、证券信息、互动内容、公众号、专利、工商与公共信息拆分的 Scrapy Spider。默认配置将全局并发、同域并发和并发处理项都设为 4,并设置 5 秒下载间隔;个别任务可单独覆盖。Item 模型区分公告、消息队列任务、公司、管理人员、研报、公众号和证券行情等对象。
公告记录的主干字段包括证券代码、证券名称、板块、公告类型、标题、发布时间、附件地址、文件列表与正文。PDF 不是只抽成一整段文本:转换器先生成 HTML,再按页面上的文本块位置重组段落,为每段保存文本、页码、类别、起止位置、段落锚点和总页数。文件地址摘要同时充当下载文件名和校验标识。正因为索引里保留了段落与页码,在线端才能把“搜索命中”继续映射回长文档中的具体位置。
代码事实。 数据管线依次包含重复检查、文件下载、索引写入和关系库入库。重复检查不只是丢弃相同地址:如果同一公告的分类、板块或机构字段变化,代码会回写已有索引和关系记录。耗时的 PDF 转换曾被拆进事件循环的线程任务;失败扫描、重抓和重建索引脚本也与主采集器并存,表明团队已经遇到过“下载成功但解析失败”“元数据变化但正文不变”等中间状态。
文档事实。 当时的数据上线规范要求先确定新的内容类型,临时在主查询中排除新类型,完成入库后再把它加入来源筛选并清理缓存。这是一种降低线上污染风险的人工发布流程。
合理推断。 该流程能支持小团队快速增加来源,但缺少正式的索引别名切换、灰度样本和自动验收。若在“临时排除”与“开放筛选”之间发生失败,数据可能已经写入却无法被用户发现。
一次查询如何变成可执行的搜索表达式
在线调用链可以从控制器追到 RequestUtil::Make:控制器接收查询 JSON,工具类构造正文查询、加分查询和过滤条件,再交给全文引擎执行。主要步骤如下。
getQueryMatch读取查询词;文内查找模式跳过智能扩展,普通搜索则调用smart_ex。- 默认分析器同时处理中文分词与同义词,多个查询单元使用“全部满足”;搜索字段默认为标题和段落正文,也可限制为标题或正文。
- 用户提供的词距进入
phrase_slop。系统另外复制一份查询,移除默认逻辑与分散匹配参数,改成短语查询,固定词距为 50、提升倍数为 20,作为should加分项。 - 股票选择、行业和市值分别转换为证券代码集合;若同时启用多个条件,代码对这些集合取交集。时间、板块与来源则进入不影响文本分数的过滤上下文。
- 默认排除部分市场板块和私人上传内容;“不限”值会使相应筛选被删除,而排除模式会把证券集合放进否定条件。
- 有关键词时先按相关度、再按时间排序;空查询时主要按发布时间返回。
代码事实。 这套过滤不是“公司或行业或市值”的宽松并集。例如,用户选择一组证券、某行业和一个市值区间,最后只保留同时属于三组代码集合的证券。好处是语义清楚,代价是任一映射表过期都会把交集缩成空集。2018 年 10 月的提交把过滤键从证券名称改为证券代码,正是为了避免公司更名后名称映射失效。
代码事实。 排序由函数评分包裹正文查询。现存参数把年报及日常经营类乘以 1.2,研报乘以 0.68,专利乘以 0.36,其他类型保持 1;发布时间再使用高斯衰减,当前时间为中心,7 天内不衰减,尺度约 30 天。可将结果近似理解为:
最终分数 = 文本相关度 × 内容类型权重 × 时间衰减 + 精确短语加分
不同因子在全文引擎中实际组合还受查询结构影响,因此这不是可以跨查询直接比较的业务评分。返回层又把当前页最高分归一为 100,更使不同分页和不同查询的“相关度百分比”不可横向对比。
“智能扩展”的巧思与脆弱性
代码事实。 分词适配器把文本送到专用索引的分析接口,使用区分大小写的智能分析器。智能扩展没有维护另一套语言服务,而是向同一索引发送带诊断信息的查询,从诊断结果中找出同义词表达式,再与分词序列对齐。双引号包围的词不展开;其他词会被改写为“原词或若干同义词”,各词组之间仍保持“全部满足”。
这个实现的价值很实际:索引和查询共用一套词典与分析器,避免了“离线分词与在线分词版本不同”。用户界面还能收到扩展列表并主动删减,而不是让系统暗中改变查询。
但代码通过正则解析全文引擎内部诊断描述,只取第一分片的第一条查询结构。诊断文本格式、分片规划或分析器输出变化时,扩展可能静默失效。另有一个布尔常量拼写错误,分词异常时局部列表也可能未初始化。这些不是理论风险,而是重建时必须先用固定语料复现的分支。
文档事实。 数据与 NLP 项目曾记录:扩大领域词典后,固定测试集得分并未自然提高;多级分词也一度破坏搜索相关性。这与代码事实相互印证——词典规模不是质量指标,扩展召回必须与原词权重、短语匹配和负面样本共同评测。
高亮段落不是前端装饰,而是第二次检索
代码事实。 列表页先请求段落高亮,但主要返回命中段落数量;打开文档时,系统再按文档标识执行一次只针对正文的高亮查询。处理器移除高亮标记后,以原文本作为键映射回结构化段落,跳过隐藏段落,并补入文件锚点。
当多个段落命中时,每段的基础分数是不同高亮片段字符长度之和。截取逻辑从首个和最后一个命中位置向两侧寻找句号,在最大展示长度附近截断上下文;结果先按命中强度降序,再按原文位置排序。研报使用另一套 HTML 锚点前缀,表明同一阅读器实际兼容了至少两类文档结构。
边界。 以“去掉标记后的全文”作为哈希键存在重复段落覆盖风险;中文句号之外的问号、分号和英文标点也未得到同等处理。长表格被当作文本段落时,字符长度容易夸大其相关性。
图谱如何与搜索结果合并
图谱端同时使用图数据库和全文搜索。图数据库先按名称或节点标识找邻接关系;全文搜索再取最多一千条高相关文档,按证券代码聚合命中文档数和分数,只保留单条分数达到阈值、至少在两份文档中出现的公司,按累计分数和文档频次排序,最多补十个“相关上市公司”节点。若候选公司超过 400 个,代码直接放弃补图,避免生成失控的关系网络。
代码事实。 两路节点按业务标识去重,关系边按“起点_终点”拼接标识。接口虽然接收图谱层数,但入口会把层数强制改回 1,实际只查询一跳;查询文本中为更深层准备的分支因此不可达。这解释了为何产品文档讨论多层展开,而现存实现仍停留在一跳图。
研究工作台把一次查询变成可复用资产
文档事实。 工作台流程包括从文章、PDF、研报和表格选中内容创建笔记,为笔记增加标签,把多条笔记组织成文章,以及多文章编辑与自动保存。后续还加入收藏、文章内搜索和导出。搜索、阅读与写作不再是三套割裂工具。
合理推断。 这是项目最值得复用的产品决策:对于研究任务,用户价值并不等于搜索点击率,而是从查询到可引用证据、再到输出物的时间。图谱适合用于发现邻近对象,段落定位适合核验,笔记则承接判断;三者分工比“把所有功能塞进一个智能答案”更可解释。
已确认的技术债与重建顺序
- 代码事实: 筛选接口读取缓存后又把结果强制设为未命中,等于主动关闭缓存;文内接口还保留绕过缓存的调试路径。
- 代码事实: 内容类型权重、短语提升、时间尺度和图谱阈值都写死在源码中,没有参数版本,无法解释某次历史结果使用了哪套规则。
- 代码事实: 通用多列排序工具动态拼接比较表达式;应改成白名单字段和显式比较器。
- 代码事实: 采集配置和接口注释中曾硬编码访问凭据与服务位置,下载还存在关闭证书校验的路径。本文不公开具体值;这些材料应视为已泄露历史凭据并彻底轮换。
- 缺失证据: 未发现离线相关性评测结果、索引文档覆盖率、PDF 成功率、重复率或搜索延迟分位数,无法证明排序参数优于其他组合。
若重建,第一步不是换模型,而是建立可回放语料:保存原始查询、改写结果、分析器版本、过滤集合、各评分因子和用户最终打开的段落;第二步把采集、解析和索引做成可幂等重试的状态机;第三步再分别评测精确查询、语义扩展、公司更名、跨来源去重和长文档定位。只有这些层稳定后,自动摘要或问答才有可信的证据底座。
证据边界
智能金融工单共 136 条,创建集中于 2017 年 12 月至 2018 年 4 月,后续更新时间延伸至 2018 年 9 月。代码侧,主服务与公告数据仓分别保存 741 次和 858 次提交,另有 163 次提交的移动端客户端,可直接证明查询、筛选、图谱、PDF 阅读与移动搜索实现过。
当前证据不能确认历史索引中每一种资料都持续完整更新,也不能确认笔记工作台与后期主服务始终部署在同一版本。情绪识别、事件抽取、完整自动报告等能力主要停留在设想或其他项目中,本文不把它们计入本项目的确定交付。