构建专利检索工具时,核心难点通常不在于发送一次HTTP请求,而在于如何把不同专利局的公开数据统一成可比较、可排序的结构化结果。Node.js的非阻塞I/O和成熟的异步控制库,正好适合处理这种高延迟、多来源的聚合场景。本文实现的PatentSearch模块会以USPTO PatentsView API为例,同时保留对接EPO OPS的扩展点,形成一套轻量的检索代理层。

明确PatentSearch的数据来源与字段约束
USPTO PatentsView是目前最容易获取的开放专利数据接口之一,覆盖美国授权专利和专利申请,支持全文关键词、发明人、受让人、CPC分类号等查询。它的免费层级允许一定频率的匿名访问,但会对单次返回条数做限制,默认每页最多返回25条或50条,需要翻页获取完整结果。EPO OPS则需要申请API密钥,并且返回XML或JSON格式的数据,字段映射更复杂。本文为了降低上手成本,先以PatentsView为主要数据源,模块内部通过适配器接口预留其他数据源的实现。
检索专利时,标题关键词和摘要关键词是最常用的入口,但仅靠关键词容易引入噪声。建议在构造查询条件时同时加入CPC分类号、公开日范围或申请日范围来缩小候选集。PatentsView的查询语法允许在q参数中嵌套_and、_or、_gte、_lte等操作符,例如查询2020年以后公开且标题包含battery的专利,可以组合为{ _and: [ { _text_any: { patent_title: 'battery' } }, { _gte: { patent_date: '2020-01-01' } } ] }。字段名必须与官方文档保持一致,否则接口会返回错误提示而不是空结果,这一点在调试时容易忽略。
function buildPatentQuery({ keyword, cpc, fromDate, toDate, page = 1, perPage = 25 }) {
const conditions = [];
if (keyword) {
conditions.push({ _text_any: { patent_title: keyword } });
}
if (cpc) {
conditions.push({ _and: [{ cpc_group_id: cpc }] });
}
if (fromDate || toDate) {
const dateRange = {};
if (fromDate) dateRange._gte = { patent_date: fromDate };
if (toDate) dateRange._lte = { patent_date: toDate };
conditions.push(dateRange);
}
return {
q: conditions.length > 1 ? { _and: conditions } : conditions[0] || {},
f: ['patent_id', 'patent_title', 'patent_date', 'patent_abstract', 'cpc_group_id'],
o: { page, per_page: perPage }
};
}封装带超时与重试的请求层
网络请求是专利检索中最不稳定的环节。官方接口偶尔会返回429状态码提示频率超限,或者在网络抖动时长时间无响应。若不加控制,一个慢请求可能拖住整个Node.js事件循环的后续任务。这里用AbortController实现8秒超时,遇到429时读取Retry-After响应头进行指数退避,从而避免单点故障影响整体检索体验。
async function fetchPatents(url, retries = 3) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 8000);
try {
const res = await fetch(url, { signal: controller.signal, headers: { 'Accept': 'application/json' } });
if (res.status === 429 && retries > 0) {
const retryAfter = Number(res.headers.get('retry-after') || 1) * 1000;
await new Promise(resolve => setTimeout(resolve, retryAfter));
return fetchPatents(url, retries - 1);
}
if (!res.ok) {
throw new Error(`Patent API returned ${res.status}`);
}
return await res.json();
} finally {
clearTimeout(timer);
}
}拿到原始响应后,需要把结果统一成业务模型,方便前端或下游服务使用。PatentsView返回的结构通常包含patents数组、total_patent_count和分页信息。下面的函数将每个专利对象映射为只保留必要字段的扁平结构,并对缺失的摘要给出默认提示。标准化之后,不同数据源接入时只需实现相同的映射函数,上层逻辑无需改动。
function normalizePatent(raw) {
return {
id: raw.patent_id,
title: raw.patent_title || '未命名专利',
date: raw.patent_date || '',
abstract: raw.patent_abstract || '摘要暂缺',
cpc: Array.isArray(raw.cpc_group_id) ? raw.cpc_group_id[0] : raw.cpc_group_id || ''
};
}用缓存和评分提升检索体验
专利检索的响应体动辄几MB,如果每次都穿透到上游接口,既浪费时间也容易触发限流。可以在Node.js进程内实现一个带TTL的LRU缓存,以序列化后的查询条件作为键,缓存标准化后的结果。对于总专利数较多的关键词查询,建议缓存第一页数据并设置5到10分钟的过期时间,这样用户在分页浏览时后续请求可以直接命中内存,避免重复拉取。
上游接口默认按相关度或日期排序,但不同数据源的默认排序并不一致。为了统一体验,可以在本地做轻量级排序。例如给标题精确匹配的词条更高权重,再结合专利被引次数和公开日期进行加权。PatentsView的响应中可能包含patent_num_cited_by_us_patents字段,可将其归一化后作为影响力分数。以下示例将检索结果按照标题匹配度降序、日期降序排列,让最新且更相关的专利优先展示。
function scoreAndSort(items, keyword) {
const lowerKeyword = keyword.toLowerCase();
return items.map(item => {
let score = 0;
const title = (item.title || '').toLowerCase();
if (title === lowerKeyword) score += 10;
else if (title.includes(lowerKeyword)) score += 5;
if (item.abstract && item.abstract.toLowerCase().includes(lowerKeyword)) score += 2;
score += Math.min(item.citations || 0, 20) * 0.1;
return { ...item, score };
}).sort((a, b) => (b.score - a.score) || (b.date > a.date ? 1 : -1));
}当用户需要获取超过第一页的结果时,尤其在做专利地图分析时,需要批量拉取。此时如果简单用Promise.all同时请求所有页面,很可能触发限流。建议引入p-limit或自定义信号量,将并发数控制在2到3个。请求完成后按页码合并结果,再统一交给后续的分析任务。这样既保证了速度,又避免被上游判定为滥用。
通过Express暴露检索接口
模块封装完成后,用一个轻量HTTP服务暴露给前端或脚本调用。下面是一个完整的Express端点,接收关键词、分类号和日期范围,经过校验、查询、缓存和排序后返回统一JSON结构。错误处理上区分请求参数错误和上游服务异常,分别返回400和502状态码,避免前端无法判断失败原因。
const express = require('express');
const app = express();
const cache = new Map();
const CACHE_TTL = 10 * 60 * 1000;
app.get('/api/patents/search', async (req, res) => {
const keyword = (req.query.keyword || '').trim();
const cpc = (req.query.cpc || '').trim();
const fromDate = req.query.from || '';
const toDate = req.query.to || '';
if (!keyword && !cpc) {
return res.status(400).json({ error: 'keyword or cpc is required' });
}
const cacheKey = JSON.stringify({ keyword, cpc, fromDate, toDate });
const cached = cache.get(cacheKey);
if (cached && Date.now() - cached.createdAt < CACHE_TTL) {
return res.json(cached.data);
}
const query = buildPatentQuery({ keyword, cpc, fromDate, toDate, page: 1, perPage: 25 });
const params = new URLSearchParams({
q: JSON.stringify(query.q),
f: JSON.stringify(query.f),
o: JSON.stringify(query.o)
});
const url = `https://api.patentsview.org/patents/query?${params.toString()}`;
try {
const raw = await fetchPatents(url);
const items = raw.patents.map(normalizePatent);
const sorted = scoreAndSort(items, keyword || cpc);
const payload = { total: raw.total_patent_count || items.length, items: sorted };
cache.set(cacheKey, { createdAt: Date.now(), data: payload });
res.json(payload);
} catch (err) {
res.status(502).json({ error: 'upstream patent api failed', detail: err.message });
}
});
app.listen(3000, () => console.log('PatentSearch listening on port 3000'));上述服务适合作为原型或内部工具使用。若要上生产,建议把内存缓存替换为Redis,将API密钥放入环境变量,并对请求参数做更严格的长度限制和正则校验。对于需要检索中国、欧洲或PCT专利的场景,可以在适配器层增加新的数据源实现,统一走相同的缓存和评分逻辑,业务代码无需改动。
Node.jsPatentSearch专利检索修改时间:2026-09-24 18:34:59