Typesense 30.2 API 使用详解

Typesense 30.2 核心 REST API 实测:密钥体系(根/受限/scoped HMAC 派生与级联失效)、collections/documents 全生命周期、批量导入部分成功语义、multi_search 与运行指标端点,附误删父密钥事故复盘。

TypesenseTypesense2026/10/18

Typesense 30.2 API 使用详解

‍‍​​‌​> 目标:以 30.2 实测覆盖 Typesense 核心 REST API——鉴权体系(根密钥/子密钥/scoped 密钥)、collections 与 documents 全生命周期(含批量导入与错误处理)、multi_search 多路搜索、健康与运行指标端点,并复盘一次真实的父密钥误删事故。全部请求与响应 JSON 在 Ubuntu 22.04 服务器实测照抄。

一、环境信息

项目内容
操作系统Ubuntu 22.04 服务器(x86_64)
Typesense 版本30.2(/debug 实测返回 {"state":1,"version":"30.2"})
服务端口8108(服务端监听),本文经反向映射端口 13909 访问
根密钥xyz(本文演示服务的主密钥,实测值)
测试集合 products12 条电商商品文档(books 8 条同前篇)
部署方式见《Typesense Docker Compose 部署指南》

二、前置条件

服务就绪验证(无鉴权端点):

curl -s http://localhost:13909/health

实测回显:

{"ok":true}

确认版本与服务状态(需鉴权):

curl -s -H 'X-TYPESENSE-API-KEY: xyz' http://localhost:13909/debug

实测回显:

{"state":1,"version":"30.2"}

后续实验基于已导入的 products(12 条)与 books(8 条)集合,导入与 schema 细节见《Typesense 30.2 查询语言详解》。

💡 技术拓展:搜索组件的 API 风格对比——

组件API 风格鉴权载体
ElasticsearchREST + JSON 请求体(DSL 内嵌)Basic Auth / API Key / JWT
Typesense纯 REST,动词即意图单一 Header X-TYPESENSE-API-KEY + 密钥层级
MeiliSearchREST + 索引路径Bearer Token + 权限分离

Typesense 的 API 面最窄:一个 Header、四级密钥(根/管理/搜索/scoped 派生),安全模型完全内建于密钥对象本身。

三、鉴权与密钥管理

3.1 密钥体系总览

Typesense 所有端点(除 /health)都要求 Header X-TYPESENSE-API-KEY。密钥分三类:

密钥创建方式典型用途
根密钥(bootstrap key)服务启动参数/配置文件指定全权限管理,仅管理员持有
普通 API KeyPOST /keys 创建按动作(actions)与集合(collections)收权,可含 filter_by 强制约束
Scoped 密钥用「仅搜索权限的父密钥」在客户端 HMAC 派生下发给浏览器/App 终端用户,参数被服务端强制

普通密钥的权限由四个属性限定:actions(如 documents:search、collections:*)、collections(集合名或 *)、filter_by(强制过滤约束)、expires_at(过期时间戳)。

⚠️ 本文 3.6 节记录了一次真实事故:演示脚本误删了演示父密钥,导致依赖它的 scoped 密钥立即 401。请务必先读该节再动手。

3.2 根密钥从哪来

根密钥由服务启动参数或配置文件指定(部署细节见《Typesense Docker Compose 部署指南》《Typesense 30.2 配置参数详解》):

# 查看本演示服务的根密钥(api-key 参数或 TYPESENSE_API_KEY 环境变量)
docker inspect typesense-30.2-compose --format '{{.Args}}'
cat /opt/data/typesense/keys/api-key.ini

实测回显:

[/opt/typesense-server --data=/data --api-key=xyz --enable-cors]
# Typesense API key
api-key = xyz

安全提示:生产环境不要使用 xyz 这类弱值;密钥文件应 chmod 600 并纳入密钥管理。本文 xyz 仅因是已隔离的演示实例。

3.3 鉴权失败实测(401/403)

错误密钥与无密钥的响应(注意 30.2 对非法密钥统一返回 401):

curl -s -w '\nHTTP %{http_code}' -H 'X-TYPESENSE-API-KEY: wrong-key' http://localhost:13909/collections
curl -s -w '\nHTTP %{http_code}' http://localhost:13909/collections

实测回显:

{"message": "Forbidden - a valid `x-typesense-api-key` header must be sent."}
HTTP 401
{"message": "Forbidden - a valid `x-typesense-api-key` header must be sent."}
HTTP 401

低权限密钥的越权访问同样表现为 401(密钥在服务端「视为无效」,不暴露权限模型细节),实测见 3.5 节。资源不存在则是 404:

curl -s -w '\nHTTP %{http_code}' -H 'X-TYPESENSE-API-KEY: xyz' http://localhost:13909/collections/nope

实测回显:

{"message":"Collection not found"}
HTTP 404

3.4 创建与列出密钥(keys API)

创建一个仅搜索权限、限定 products 集合、强制只看 Apple 品牌的演示密钥:

curl -s -X POST -H 'X-TYPESENSE-API-KEY: xyz' \
  -H 'Content-Type: application/json' \
  'http://localhost:13909/keys' \
  -d '{"description":"demo search key","actions":["documents:search"],"collections":["products"],"filter_by":"brand:=Apple"}'

实测回显(value 只在创建响应中出现一次,请立即妥善保存;本文为演示实例,密钥明文照抄展示):

{
  "actions": ["documents:search"],
  "autodelete": false,
  "collections": ["products"],
  "description": "demo search key",
  "expires_at": 64723363199,
  "filter_by": "brand:=Apple",
  "id": 4,
  "value": "4nBEUI9z7dcUrmSLIRldbGE7epIXOTvw"
}

列出全部密钥(列表不返回 value 明文,只有 value_prefix):

curl -s -H 'X-TYPESENSE-API-KEY: xyz' http://localhost:13909/keys

实测回显:

{
  "keys": [
    {
      "actions": ["documents:search"],
      "autodelete": false,
      "collections": ["products"],
      "description": "demo search key",
      "expires_at": 64723363199,
      "filter_by": "brand:=Apple",
      "id": 4,
      "value_prefix": "4nBE"
    }
  ]
}

⚠️ 两点注意:①创建响应是获取密钥明文的唯一机会,错过后只能删除重建;②GET /keys 的列表不包含根密钥(根密钥由启动参数指定、不作为密钥对象存储),列表只含 POST /keys 创建的密钥。value_prefix(默认 4 字符)用于日志中辨识密钥而不泄露明文。

3.5 密钥权限边界实测

用 3.4 的搜索密钥(id=4)验证权限边界。先正向验证——搜索可用,且密钥的 filter_by 与请求参数合并生效(请求里 category:=Laptop,密钥强制 brand:=Apple,结果 = 两者交集,只剩 MacBook):

curl -s -H "X-TYPESENSE-API-KEY: $SEARCH_KEY" \
  'http://localhost:13909/collections/products/documents/search?q=*&query_by=name&filter_by=category:=Laptop' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('found:', d['found'], [h['document']['name'] for h in d['hits']])"

实测回显:

found: 1 ['MacBook Pro 14 M4']

越权写入(401,搜索密钥无 documents:create 动作):

curl -s -w '\nHTTP %{http_code}' -X POST -H "X-TYPESENSE-API-KEY: $SEARCH_KEY" \
  -H 'Content-Type: application/json' \
  'http://localhost:13909/collections/products/documents' -d '{"name":"x"}'

实测回显:

{"message": "Forbidden - a valid `x-typesense-api-key` header must be sent."}
HTTP 401

越权管理密钥(401,搜索密钥不允许 keys:get)与越权创建密钥(401)实测回显均为:

{"message": "Forbidden - a valid `x-typesense-api-key` header must be sent."}
HTTP 401

删除密钥(按 id):

curl -s -X DELETE -H 'X-TYPESENSE-API-KEY: xyz' http://localhost:13909/keys/3

实测回显(删除实验密钥 id=3 后的回执):

{"id":3}

3.6 Scoped 密钥:客户端派生与服务端强制

Scoped 密钥不需要入库,由父密钥(仅 documents:search 权限)在客户端 HMAC-SHA256 派生,把不可篡改的搜索参数「焊死」在密钥里,适合直接下发到浏览器/移动端。官方限制:父密钥必须只有搜索权限;父密钥被删除时所有派生密钥立即失效(3.7 节实测)。

30.2 的派生格式(服务端源码 + 实测双重确认,与旧版客户端算法不同):

scoped_key = base64( base64(HMAC_SHA256(父密钥, params_json)) + 父密钥前4字符 + params_json )

注意两点:HMAC 的消息是 params 的 JSON 明文(不是它的 base64);params 同样以 JSON 明文拼接在末尾(服务端拿到后直接 json::parse)。

Python 派生与实测代码:

import base64, hashlib, hmac, json

def gen_scoped_search_key(parent_key: str, parameters: dict) -> str:
    params_json = json.dumps(parameters, separators=(',', ':'))
    digest = hmac.new(parent_key.encode(), params_json.encode(), hashlib.sha256).digest()
    signature = base64.b64encode(digest).decode()          # 44 字符(含 '=')
    return base64.b64encode(
        f'{signature}{parent_key[0:4]}{params_json}'.encode()
    ).decode()

SCOPED = gen_scoped_search_key(PARENT_KEY, {
    'filter_by': 'brand:=Apple',
    'exclude_fields': 'description,specs'
})

用该 scoped 密钥搜索:

curl -s -H "X-TYPESENSE-API-KEY: $SCOPED" \
  'http://localhost:13909/collections/products/documents/search?q=*&query_by=name' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('found:', d['found'], [h['document']['name'] for h in d['hits']])"

实测回显(只见 Apple 商品,且返回文档没有 description/specs 字段):

found: 3 ['AirPods Pro 2', 'iPhone 16 Pro', 'MacBook Pro 14 M4']

终端用户试图用请求参数覆盖密钥内嵌的约束(filter_by=brand:=Sony),服务端合并而非替换约束,Sony 不在 Apple 集内,结果为 0:

found: 0 []

scoped 密钥同样可用于 multi_search(约束依旧生效),实测回显:found: 1 ['MacBook Pro 14 M4'],文档字段同上被裁剪。用它访问 /keys 则 401(scoped 仅限搜索)。

💡 技术拓展:HMAC 派生密钥的安全原理——

机制说明
HMAC-SHA256 签名服务端用父密钥重算签名比对,伪造 params 需要父密钥本身
前缀路由前 4 字符父密钥前缀让服务端快速定位候选父密钥(同前缀多 key 时逐一验签)
参数焊死filter_by/exclude_fields 等嵌入 params,请求中同名参数只做合并收窄
级联失效父密钥删除即吊销全部派生密钥,无需逐个清理

与 JWT 的区别:scoped key 不携带过期时间与签发者声明(过期靠父密钥 expires_at),也不做 base64 头/载荷分段,是一个「单段、自校验、可即时吊销」的轻量方案。

3.7 事故复盘:误删父密钥导致 scoped 密钥全部失效

事故经过:本文写作前的实验脚本(d5-recap.sh)在收尾清理时执行了 DELETE /keys/2,误删了 scoped 密钥演示所依赖的父密钥;此后所有 scoped 派生与搜索实测一律 401,排查成本远高于预期。

复现实测(本轮再次真实执行,作为留档):先删除父密钥(回执 {"id":3}),再用此前派生的 scoped 密钥搜索:

curl -s -X DELETE -H 'X-TYPESENSE-API-KEY: xyz' http://localhost:13909/keys/3
curl -s -w '\nHTTP %{http_code}' -H "X-TYPESENSE-API-KEY: $SCOPED" \
  'http://localhost:13909/collections/products/documents/search?q=*&query_by=name'

实测回显:

{"message": "Forbidden - a valid `x-typesense-api-key` header must be sent."}
HTTP 401

密钥管理注意事项(提炼自事故):

  1. 删除密钥前先 GET /keys 核对 id 与 description,脚本中删除操作必须带 description 断言,禁止按固定 id 盲删;
  2. 交付 scoped 密钥的父密钥要单独命名(如 parent-for-scoped)并在 description 里写明依赖关系;
  3. value 明文只在创建响应出现一次,务必即时落盘到密钥管理工具;丢失后唯一恢复方式是删除重建,而重建会连累其下所有 scoped 密钥;
  4. 根密钥仅保存在服务端配置(3.2 节),不参与日常 API 调用,日常用受限密钥。

四、collections API

4.1 创建集合(POST /collections)

创建命令(以 books 集合的 schema 为例):

curl -s -X POST -H 'X-TYPESENSE-API-KEY: xyz' \
  -H 'Content-Type: application/json' \
  'http://localhost:13909/collections' \
  -d '{"name":"books","fields":[{"name":"title","type":"string"},{"name":"authors","type":"string[]","facet":true},{"name":"publication_year","type":"int32"},{"name":"ratings_count","type":"int32"},{"name":"genres","type":"string[]","facet":true}],"default_sorting_field":"ratings_count"}'

实测回显(为不重复占用 books 名称,以下为对同一 schema 的临时集合 d5-demo 的创建响应照抄,该集合验证后已删除):

{
    "created_at": 1792295043,
    "curation_sets": [],
    "default_sorting_field": "ratings_count",
    "enable_nested_fields": false,
    "fields": [
        {
            "facet": false, "index": true, "infix": false, "locale": "", "name": "title",
            "optional": false, "sort": false, "stem": false, "stem_dictionary": "", "store": true, "truncate_len": 100, "type": "string"
        },
        {
            "facet": true, "index": true, "infix": false, "locale": "", "name": "authors",
            "optional": false, "sort": false, "stem": false, "stem_dictionary": "", "store": true, "truncate_len": 100, "type": "string[]"
        },
        {
            "facet": false, "index": true, "infix": false, "locale": "", "name": "publication_year",
            "optional": false, "sort": true, "stem": false, "stem_dictionary": "", "store": true, "truncate_len": 100, "type": "int32"
        },
        {
            "facet": false, "index": true, "infix": false, "locale": "", "name": "ratings_count",
            "optional": false, "sort": true, "stem": false, "stem_dictionary": "", "store": true, "truncate_len": 100, "type": "int32"
        },
        {
            "facet": true, "index": true, "infix": false, "locale": "", "name": "genres",
            "optional": false, "sort": false, "stem": false, "stem_dictionary": "", "store": true, "truncate_len": 100, "type": "string[]"
        }
    ],
    "name": "d5-demo",
    "num_documents": 0,
    "symbols_to_index": [],
    "synonym_sets": [],
    "token_separators": []
}

4.2 查看与列出集合

curl -s -H 'X-TYPESENSE-API-KEY: xyz' 'http://localhost:13909/collections/books' | python3 -c "import json,sys; d=json.load(sys.stdin); print('name:', d['name'], '| num_documents:', d['num_documents'], '| default_sorting_field:', d['default_sorting_field'])"
curl -s -H 'X-TYPESENSE-API-KEY: xyz' 'http://localhost:13909/collections?limit=100' | python3 -c "import json,sys; [print(c['name'], c['num_documents']) for c in json.load(sys.stdin)]"

实测回显:

name: books | num_documents: 8 | default_sorting_field: ratings_count
books 8
books-facet 8
products 12

4.3 修改与删除集合

修改集合(PATCH 直接更新集合定义;新增字段即通过此接口提交 fields 数组):

# 为 books 新增可选字段 translator
curl -s -X PATCH -H 'X-TYPESENSE-API-KEY: xyz' -H 'Content-Type: application/json' \
  'http://localhost:13909/collections/books' \
  -d '{"fields":[{"name":"translator","type":"string","optional":true}]}' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('fields:', [f['name'] for f in d.get('fields',[])], d.get('message',''))"

实测回显:

fields: ['translator']

注意:PATCH 响应的 fields 只包含本次变更涉及的字段,并非完整 schema;确认完整定义需再读集合:

curl -s -H 'X-TYPESENSE-API-KEY: xyz' 'http://localhost:13909/collections/books' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('fields:', [f['name'] for f in d['fields']])"

实测回显(translator 已并入,数据无损):

fields: ['title', 'authors', 'publication_year', 'ratings_count', 'genres', 'publisher', 'translator']

另外,对已存在字段重复添加会被拒绝(实测回显):{"message":"Field publisher is already part of the schema: To change this field, drop it first before adding it back to the schema."}——30.2 不支持就地删字段,改字段需先 drop 再加回。

删除集合(连带全部文档,返回被删集合的完整定义,此处截取 name 字段):

curl -s -X DELETE -H 'X-TYPESENSE-API-KEY: xyz' http://localhost:13909/collections/d5-demo

实测回显(截取):

{"name":"d5-demo", "...": "完整集合定义与创建响应相同"}

五、documents API

5.1 单文档 CRUD

# 创建(未带 id 时服务端自动生成)
curl -s -X POST -H 'X-TYPESENSE-API-KEY: xyz' -H 'Content-Type: application/json' \
  'http://localhost:13909/collections/products/documents' \
  -d '{"id":"prod-013","name":"Surface Laptop 7","brand":"Microsoft","category":"Laptop","price":11188.0,"stock":20,"rating":4.5,"tags":["laptop","touch"],"in_stock":true,"location":[31.2304,121.4737]}'

实测回显(30.2 的创建响应返回完整文档):

{"brand":"Microsoft","category":"Laptop","id":"prod-013","in_stock":true,"location":[31.2304,121.4737],"name":"Surface Laptop 7","price":11188.0,"rating":4.5,"stock":20,"tags":["laptop","touch"]}
# 读取(include_fields 裁剪返回)
curl -s -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/prod-013?include_fields=name,price'

实测回显:

{"name":"Surface Laptop 7","price":11188.0}
# 局部更新(PATCH 合并字段,可新增嵌套对象)
curl -s -X PATCH -H 'X-TYPESENSE-API-KEY: xyz' -H 'Content-Type: application/json' \
  'http://localhost:13909/collections/products/documents/prod-013' \
  -d '{"price":10488.0,"specs":{"cpu":"Snapdragon X","ram":"16GB"}}'

实测回显(返回更新后的完整文档;未提交过的字段不出现,不补空值):

{"brand":"Microsoft","category":"Laptop","id":"prod-013","in_stock":true,"location":[31.2304,121.4737],"name":"Surface Laptop 7","price":10488.0,"rating":4.5,"specs":{"cpu":"Snapdragon X","ram":"16GB"},"stock":20,"tags":["laptop","touch"]}
# 删除与删除后读取
curl -s -X DELETE -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/prod-013'
curl -s -w '\nHTTP %{http_code}' -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/prod-013'

实测回显(DELETE 返回被删文档本身,便于审计对账):

{"brand":"Microsoft","category":"Laptop","id":"prod-013","in_stock":true,"location":[31.2304,121.4737],"name":"Surface Laptop 7","price":10488.0,"rating":4.5,"specs":{"cpu":"Snapdragon X","ram":"16GB"},"stock":20,"tags":["laptop","touch"]}
{"message":"Could not find a document with id: prod-013"}
HTTP 404

5.2 批量导入 import

批量导入是 JSONL(每行一个文档,响应同样逐行 JSON),action=create|upsert|update|emplace 四种模式。30.2 实测要点:行级失败不改变 HTTP 状态(仍 200),错误以逐行 JSON 返回。

# upsert:存在则更新,不存在则创建;return_id,return_doc 请求返回 id/文档
printf '%s\n' '{"id":"prod-001","price":15499.0}' \
  '{"id":"prod-new","name":"Test Upsert","brand":"Test","category":"Test","price":1.0,"stock":1,"rating":1.0,"tags":[],"in_stock":false,"location":[0.0,0.0]}' \
  > /tmp/upsert.jsonl

curl -s -w '\nHTTP %{http_code}' -X POST -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/import?action=upsert&return_id,return_doc' \
  --data-binary @/tmp/upsert.jsonl

实测回显(第一行被拒!products 的 stock 是 default_sorting_field,upsert 提交的文档必须包含该字段;第二行字段齐全,成功):

{"code":400,"error":"Field `stock` has been declared as a default sorting field, but is not found in the document.","success":false}
{"success":true}
HTTP 200

验证第一行确实未被更新(价格维持原值 15999):

curl -s -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/prod-001?include_fields=price'

实测回显:

{"price":15999.0}

混合类型错误实测(价格传字符串,行级错误 + HTTP 200):

printf '%s\n' '{"id":"prod-bad","price":"not-a-number","name":"Bad","brand":"T","category":"T","stock":1,"rating":1.0,"tags":[],"in_stock":false,"location":[0,0]}' \
  > /tmp/bad.jsonl
curl -s -w '\nHTTP %{http_code}' -X POST -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/import?action=upsert' \
  --data-binary @/tmp/bad.jsonl

实测回显:

{"code":400,"error":"Field `price` must be a float.","success":false}
HTTP 200

⚠️ 三个实测提炼的要点:①部分成功语义——同一请求内成功行已入库、失败行仅返回错误,HTTP 状态恒 200,脚本必须逐行检查 success 字段,不能只看状态码;②upsert 文档必须带 default_sorting_field 字段(部分字段提交会被整行拒绝),做"只改价格"类更新应改用 action=update(按 PATCH 语义合并,无此要求);③幂等重放统一用 upsert(同 id 不会产生重复文档)。

💡 顺带说明:示例中 return_id,return_doc 在该 upsert 场景的成功行回显里未生效(仍为 {"success":true}),实测照抄;该参数主要用于 action=create 的场景。

清理测试文档:

curl -s -X DELETE -H 'X-TYPESENSE-API-KEY: xyz' 'http://localhost:13909/collections/products/documents/prod-new'

实测回显(返回被删文档本身):

{"brand":"Test","category":"Test","id":"prod-new","in_stock":false,"location":[0.0,0.0],"name":"Test Upsert","price":1.0,"rating":1.0,"stock":1,"tags":[]}

5.3 导出 export

curl -s -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/export' | head -2

实测回显(JSONL,前两行截取):

{"brand":"Apple","category":"Laptop","description":"14 inch professional laptop with M4 Pro chip","id":"prod-001","in_stock":true,"location":[31.2304,121.4737],"name":"MacBook Pro 14 M4","price":15999.0,"rating":4.9,"specs":{"cpu":"M4 Pro","ram":"24GB"},"stock":42,"tags":["laptop","retina","m4"]}
{"brand":"Lenovo","category":"Laptop","description":"Business ultrabook with carbon fiber body","id":"prod-002","in_stock":true,"location":[39.9042,116.4074],"name":"ThinkPad X1 Carbon Gen 12","price":12499.0,"rating":4.6,"specs":{"cpu":"Core Ultra 7","ram":"32GB"},"stock":18,"tags":["laptop","business","lightweight"]}

export 支持 filter_by/include_fields/exclude_fields 等参数做条件导出(与搜索参数一致)。

六、搜索与多路搜索

6.1 单集合搜索

GET /collections/{collection}/documents/search 的参数体系(q/filter_by/facet_by/sort_by/typo 容错/分组/高亮等)已在《Typesense 30.2 查询语言详解》逐项实测,此处仅列请求骨架:

curl -s -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/search?q=phone&query_by=name'

实测回显(关键字段截取):

{
  "facet_counts": [],
  "found": 1,
  "hits": [
    {
      "document": {
        "brand": "Apple",
        "category": "Phone",
        "id": "prod-003",
        "name": "iPhone 16 Pro",
        "price": 8999.0
      },
      "highlight": {
        "name": {
          "matched_tokens": ["iPhone"],
          "snippet": "<mark>iPhone</mark> 16 Pro"
        }
      },
      "text_match": 578730054645713017
    }
  ],
  "out_of": 12,
  "page": 1,
  "search_cutoff": false,
  "search_time_ms": 2
}

一次请求并行搜索多个集合(或同集合不同参数),请求体 searches 数组每个元素与单集合搜索参数一致,响应 results 按序对应:

curl -s -H 'X-TYPESENSE-API-KEY: xyz' -H 'Content-Type: application/json' \
  'http://localhost:13909/multi_search' \
  -d '{"searches":[{"collection":"products","q":"pro","query_by":"name","per_page":1},{"collection":"books","q":"potter","query_by":"title","per_page":1}]}' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); [print(f\"results[{i}] found:\", r.get('found'), [h['document'].get('name') or h['document'].get('title') for h in r.get('hits',[])]) for i,r in enumerate(d['results'])]"

实测回显:

results[0] found: 3 ['AirPods Pro 2']
results[1] found: 1 ["Harry Potter and the Philosopher's Stone"]

multi_search 天然适配搜索框的"商品/内容/用户"多 tab 场景:一次 RTT 拿齐各路结果;也可配合 page/per_page 分别分页。scoped 密钥同样可用(约束逐路生效,见 3.6 节)。

七、健康与运行指标

端点鉴权用途
GET /health否存活探针(负载均衡/健康检查用)
GET /debug是版本与节点状态(state:1 为正常)
GET /metrics.json是CPU/内存/磁盘/网络逐核指标
GET /stats.json是请求速率、延迟分位、缓存命中
curl -s -H 'X-TYPESENSE-API-KEY: xyz' http://localhost:13909/metrics.json | head -8
curl -s -H 'X-TYPESENSE-API-KEY: xyz' http://localhost:13909/stats.json | head -6

实测回显(CPU 数值随采样时刻变化,以下为实测当时的照抄快照):

{
  "system_cpu10_active_percentage": "30.00",
  "system_cpu11_active_percentage": "0.00",
  "system_cpu12_active_percentage": "0.00",
  "system_cpu1_active_percentage": "27.27",
  "system_cpu2_active_percentage": "0.00"
}
{
  "cache_hit_ratio": 0.0,
  "delete_latency_ms": 0,
  "delete_requests_per_second": 0,
  "import_latency_ms": 0,
  "import_requests_per_second": 0
}

⚠️ 30.2 的指标端点是 /metrics.json 与 /stats.json(带 .json 后缀);直接访问 /metrics 会得到 {"message": "Not Found"}(实测)。

八、端点速查表

方法与路径动作权限 action
GET /health存活探针无需鉴权
GET /debug版本与状态debug:*
GET /metrics.json / GET /stats.json运行指标metrics:* / stats:*
POST /collections创建集合collections:create
GET /collections / GET /collections/{name}列出/读取集合collections:get
PATCH /collections/{name}修改集合(含新增字段)collections:update
DELETE /collections/{name}删除集合collections:delete
POST /collections/{name}/documents创建文档documents:create
GET /collections/{name}/documents/{id}读取文档documents:get
PATCH /collections/{name}/documents/{id}局部更新documents:update
DELETE /collections/{name}/documents/{id}删除文档documents:delete
POST /collections/{name}/documents/import批量导入(JSONL)documents:create/update
GET /collections/{name}/documents/export导出(JSONL 流)documents:get
GET /collections/{name}/documents/search搜索documents:search
POST /multi_search多路搜索documents:search
POST /keys / GET /keys / DELETE /keys/{id}密钥管理keys:*

九、错误码与排查

以下均为本文实测出现过的错误形态:

HTTP实测触发场景与响应处理
200 + 行级错误import 部分行失败(5.2 节):HTTP 仍 200,错误逐行 {"code":400,"error":"...","success":false}逐行检查 success;不能依赖状态码
400import 行内错误对象中的 code:400(缺 default_sorting_field 字段、字段类型错误);请求参数格式错误(如 sort_by 表达式不合法,见《Typesense 30.2 查询语言详解》5.7 节)读 error 文本定位;核对 schema 与参数语法
401无密钥/错误密钥/低权限越权(3.3、3.5 节实测统一 401,message 相同)核对 X-TYPESENSE-API-KEY;检查密钥 actions/collections;scoped 失效多为父密钥被删(3.7 节)
404文档不存在:{"message":"Could not find a document with id: xxx"};集合不存在:{"message":"Collection not found"};端点不存在(如 /metrics):{"message": "Not Found"}核对资源 id 与端点拼写;指标端点带 .json

十、常见问题

Q1:scoped 密钥搜索一律 401,父密钥明明还在? 核对三点:父密钥 actions 必须只有 documents:search(3.7 节事故即由父密钥丢失引起,先确认这点);派生算法是否与 30.2 一致(3.6 节格式:HMAC 对 params JSON 明文,params 明文拼接,digest base64 含 =);客户端库版本是否过旧(旧版算法派生的密钥在 30.2 上验签必败)。逐步骤用本文 3.6 节代码本地重算比对。

Q2:import 的响应里明明有错误,HTTP 状态却是 200? 30.2 的行级错误不影响 HTTP 状态(5.2 节实测):错误逐行以 {"code":400,...,"success":false} 返回,成功行已入库。脚本必须逐行判断 success 字段;只有请求体整体非法(如 JSON 解析失败)才会非 200。

Q3:密钥 value 丢了怎么办? 无法找回(GET /keys 只有 value_prefix)。删除重建;若它是 scoped 的父密钥,其下所有 scoped 密钥同步失效,需重新派生下发(3.7 节)。

Q4:低权限密钥访问被拒,为什么响应和错误密钥一模一样? Typesense 对「无效」与「无权」统一返回 401 与同一 message,不区分泄露权限模型(3.5 节实测)。排查靠 GET /keys 核对该密钥的 actions/collections。

Q5:/metrics 返回 Not Found? 30.2 用 /metrics.json 与 /stats.json(第七节实测对照)。

十一、参考链接