Typesense 30.2 查询语言详解

Typesense 30.2 查询语言详解:q/query_by/filter_by/facet_by/sort_by 参数体系逐项实测,覆盖 typo 数量层级、split_join、丢词回退、group_by、_eval 条件排序与高亮,回显全部照抄。

TypesenseTypesense2026/10/18

Typesense 30.2 查询语言详解

‍‍​​‌​> 目标:系统讲解 Typesense 30.2 的搜索查询语言——以 q/query_by/filter_by/facet_by/sort_by 为主干的参数化查询体系,覆盖过滤表达式、分面统计、typo 容错、连写拆词、丢词回退、分组、条件排序与高亮,全部请求与响应 JSON 均在 Ubuntu 22.04 服务器实测照抄。

一、环境信息

本文全部查询与回显在以下环境实测照抄:

项目内容
操作系统Ubuntu 22.04 服务器(x86_64)
Typesense 版本30.2(服务端回执与 /health 实测确认)
服务端口8108(服务端监听),本文经反向映射端口 13909 访问
API 密钥xyz(本文演示服务的主密钥,实测值)
测试集合 products12 条电商商品文档(含嵌套字段 specs、geopoint 字段 location)
测试集合 books8 条图书文档(含 string[] 字段 authors/genres)
测试集合 books-facet8 条图书文档(publication_year 开启 facet 的数值分面演示集合)
部署方式见《Typesense Docker Compose 部署指南》

二、前置条件

2.1 服务就绪验证

查询语言依赖一个运行中的 Typesense 服务。先确认服务健康:

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

实测回显:

{"ok":true}

2.2 实验数据集导入

本文所有查询基于两个数据集。图书集合按如下 schema 创建(文件 books-schema.json):

{
  "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"
}

创建集合并导入 JSONL 文档(每行一个 JSON 文档,共 8 条《The Great Gatsby》《Pride and Prejudice》《Harry Potter and the Philosopher's Stone》等):

curl -s -X POST -H 'X-TYPESENSE-API-KEY: xyz' \
  -H 'Content-Type: application/json' \
  --data @books-schema.json 'http://localhost:13909/collections'

curl -s -X POST -H 'X-TYPESENSE-API-KEY: xyz' \
  --data-binary @books.jsonl -H 'Content-Type: text/plain' \
  'http://localhost:13909/collections/books/documents/import?action=create'

实测回显(导入接口对每行文档返回一行结果,8 行全部成功):

{"success":true}
{"success":true}
{"success":true}
{"success":true}
{"success":true}
{"success":true}
{"success":true}
{"success":true}

验证集合就绪(关注 num_documents):

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

实测回显:

books 8

商品集合 products(12 条)同理导入,字段结构为:

name(string) brand(string,facet) category(string,facet) price(float,sort)
stock(int32,sort) rating(float,sort) tags(string[],facet) in_stock(bool,sort)
location(geopoint,sort) description(string,optional) specs(object,optional)
specs.ram / specs.cpu / specs.size / specs.panel(嵌套子字段)

其 default_sorting_field 为 stock,enable_nested_fields 为 true。12 条文档的 id、名称与关键字段:

prod-001 | MacBook Pro 14 M4          | Apple   | Laptop   | 15999 | stock 42  | rating 4.9 | in_stock true  | 上海
prod-002 | ThinkPad X1 Carbon Gen 12  | Lenovo  | Laptop   | 12499 | stock 18  | rating 4.6 | in_stock true  | 北京
prod-003 | iPhone 16 Pro              | Apple   | Phone    |  8999 | stock 120 | rating 4.8 | in_stock true  | 上海
prod-004 | AirPods Pro 2              | Apple   | Audio    |  1899 | stock 300 | rating 4.7 | in_stock true  | 上海
prod-005 | WH-1000XM5 Headphones      | Sony    | Audio    |  2399 | stock 0   | rating 4.8 | in_stock false | 深圳
prod-006 | MX Master 3S Mouse         | Logitech| Accessory|   649 | stock 250 | rating 4.9 | in_stock true  | 深圳
prod-007 | Air Force 1 Sneakers       | Nike    | Shoes    |   799 | stock 88  | rating 4.5 | in_stock true  | 广州
prod-008 | Pegasus 41 Running Shoes   | Nike    | Shoes    |   899 | stock 55  | rating 4.6 | in_stock true  | 广州
prod-009 | Ultraboost Light           | Adidas  | Shoes    |  1099 | stock 0   | rating 4.4 | in_stock false | 广州
prod-010 | Kindle Paperwhite          | Amazon  | E-Reader |  1068 | stock 64  | rating 4.7 | in_stock true  | 成都
prod-011 | Echo Dot 5                 | Amazon  | Smart Home|  349 | stock 210 | rating 4.3 | in_stock true  | 成都
prod-012 | ThinkVision P27h Monitor   | Lenovo  | Monitor  |  2299 | stock 33  | rating 4.5 | in_stock true  | 北京

2.3 数值分面演示集合

分面统计要求字段在 schema 中声明 facet: true(见 5.4 节实测)。为演示数值字段的分面统计,另建 books-facet 集合(schema 与 books 相同,仅 publication_year 增加 "facet": true),导入同样的 8 条文档:

curl -s -X POST -H 'X-TYPESENSE-API-KEY: xyz' \
  -H 'Content-Type: application/json' \
  --data @books-facet-schema.json 'http://localhost:13909/collections'

curl -s -X POST -H 'X-TYPESENSE-API-KEY: xyz' \
  --data-binary @books.jsonl -H 'Content-Type: text/plain' \
  'http://localhost:13909/collections/books-facet/documents/import?action=create'

三、语言总览

Typesense 没有独立的查询 CLI 或脚本解释器:它的查询语言就是一组 HTTP 搜索参数。一条搜索 = GET /collections/{collection}/documents/search 端点 + 若干查询参数,其中 q、query_by 是检索主干,filter_by 是一段嵌入 URL 的过滤表达式子语言,facet_by/sort_by 负责统计与排序。所有能力都通过同一个服务端点对外提供,curl、SDK、管理台只是调用方式不同。

💡 技术拓展:主流搜索/检索组件的查询语言形态对比——

组件查询语言形态表达载体
ElasticsearchQuery DSL(JSON 领域专用语言)请求体 JSON,语句嵌套组合
Typesense参数化查询 + 表达式子语言URL 查询参数 + filter_by/sort_by 表达式
MeiliSearch参数化查询URL 查询参数(filter/sort/facets)
PromQL独立查询语言时间序列表达式,自有语法解析器

Typesense 与 MeiliSearch 同属「URL 参数形态」:学习成本低、可读性强、天然适配 GET 缓存;代价是复杂嵌套逻辑的表达能力弱于 ES 的 JSON DSL。本文实测的 filter_by 表达式即是 Typesense 中最接近「语言」的部分(支持 &&/||/比较运算/数组/地理半径等)。

一次最小可用查询(在 name 字段中搜 phone):

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",
                "description": "Flagship phone with titanium frame",
                "id": "prod-003",
                "in_stock": true,
                "location": [31.2304, 121.4737],
                "name": "iPhone 16 Pro",
                "price": 8999.0,
                "rating": 4.8,
                "specs": {"cpu": "A18 Pro", "ram": "8GB"},
                "stock": 120,
                "tags": ["phone", "5g", "titanium"]
            },
            "highlight": {
                "name": {
                    "matched_tokens": ["iPhone"],
                    "snippet": "<mark>iPhone</mark> 16 Pro"
                }
            },
            "highlights": [
                {
                    "field": "name",
                    "matched_tokens": ["iPhone"],
                    "snippet": "<mark>iPhone</mark> 16 Pro"
                }
            ],
            "text_match": 578730054645713017,
            "text_match_info": {
                "best_field_score": "1108057784321",
                "best_field_weight": 15,
                "fields_matched": 1,
                "num_tokens_dropped": 0,
                "score": "578730054645713017",
                "tokens_matched": 1,
                "typo_prefix_score": 2
            }
        }
    ],
    "out_of": 12,
    "page": 1,
    "request_params": {
        "collection_name": "products",
        "first_q": "phone",
        "per_page": 10,
        "q": "phone"
    },
    "search_cutoff": false,
    "search_time_ms": 2
}

响应骨架解读:

字段含义
found / out_of本次命中数 / 集合文档总数
hits[].document原始文档
hits[].highlight命中字段高亮(snippet 默认用 <mark> 包裹命中词)
hits[].text_match / text_match_info相关性打分及构成(tokens_matched、typo 计分等)
facet_counts分面统计(仅传 facet_by 时出现)
grouped_hits分组结果(仅传 group_by 时出现,替代 hits)
page / request_params当前页 / 服务端回显的请求参数
search_cutoff / search_time_ms是否触发查询超时 / 耗时毫秒

四、语法基础

4.1 q 查询串的四类写法

q 是必填参数,支持四种形态(官方文档 30.2 版「Search Parameters」明确):

  1. 关键词:q=phone,按空格分词后逐词匹配;
  2. 通配符:q=*,返回全部文档(通常与 filter_by 组合做"纯过滤查询");
  3. 排除词:q=running -pegasus,- 前缀把该词从结果中排除;
  4. 短语精确:q="running shoes",双引号包裹表示整段按序精确匹配,关闭 typo 容错。

排除词实测(description 中含 running 的两条,排除 Pegasus 后只剩 Ultraboost):

curl -s -H 'X-TYPESENSE-API-KEY: xyz' --get \
  --data-urlencode 'q=running -pegasus' \
  --data-urlencode 'query_by=name,description' \
  'http://localhost:13909/collections/products/documents/search' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('found:', d['found'], [h['document']['name'] for h in d['hits']])"

实测回显:

found: 1 ['Ultraboost Light']

短语匹配需要词序一致——把两条 shoes 文档的名称反序查询可看到明显差异:

# 短语(双引号,词序反了)
curl -s -H 'X-TYPESENSE-API-KEY: xyz' --get \
  --data-urlencode 'q="shoes running"' \
  --data-urlencode 'query_by=name,description' \
  'http://localhost:13909/collections/products/documents/search' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('phrase(反序) found:', d['found'], [h['document']['name'] for h in d['hits']])"

# 普通多词(无引号,词序无关)
curl -s -H 'X-TYPESENSE-API-KEY: xyz' --get \
  --data-urlencode 'q=shoes running' \
  --data-urlencode 'query_by=name,description' \
  'http://localhost:13909/collections/products/documents/search' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('plain(反序) found:', d['found'], [h['document']['name'] for h in d['hits']])"

实测回显:

phrase(反序) found: 0 []
plain(反序) found: 2 ['Pegasus 41 Running Shoes', 'Ultraboost Light']

4.2 query_by 与多字段权重

query_by 指定检索哪些字段,逗号分隔。字段顺序即权重顺序:排在前的字段命中被认为更相关。可用 query_by_weights 为每个字段赋显式权重(与 query_by 顺序一一对应)。

curl -s -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/search?q=phone&query_by=name,description&query_by_weights=2,1' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); [print(h['document']['id'], h['document']['name'], '|', h['document']['description'][:40], '| text_match:', h['text_match']) for h in d['hits']]"

实测回显:

prod-003 iPhone 16 Pro | Flagship phone with titanium frame | text_match: 578730123365189641

4.3 前缀匹配规则(prefix)

prefix 控制前缀匹配。实测结论(重要,网上常见误解在这里):

  • 默认(不传):仅对最后一个查询词做前缀匹配,中间词必须精确匹配;
  • prefix=false:连最后一个词也不做前缀;
  • prefix=true:与默认行为一致(仍只作用于最后一个词)——多词查询的中间词不会因 prefix=true 获得前缀能力。

依次实测三种情形:

# 单词前缀(默认行为):q=mac 命中 MacBook
curl -s -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/search?q=mac&query_by=name&drop_tokens_threshold=0' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('found:', d['found'], [h['highlights'] for h in d['hits']])"

# 关闭前缀:q=iph 本应命中 iPhone,关闭后为 0
curl -s -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/search?q=iph&query_by=name&prefix=false' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('found:', d['found'])"

实测回显:

found: 1 [{'field': 'name', 'matched_tokens': ['Mac'], 'snippet': '<mark>Mac</mark>Book Pro 14 M4'}]
found: 0

多词中间词不做前缀的验证(mac pro 两个词,mac 是 macbook 的前缀但不是完整词;先禁用丢词回退以便观察纯前缀行为):

# 默认参数:中间词 mac 无精确匹配 → 丢词回退接手(结果由 pro 单词决定)
curl -s -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/search?q=mac%20pro&query_by=name' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('found:', d['found'], [(h['document']['name'], h['text_match_info']['num_tokens_dropped']) for h in d['hits']])"

# prefix=true + drop_tokens_threshold=0:中间词不做前缀 → 0 结果
curl -s -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/search?q=macb%20pro&query_by=name&prefix=true&drop_tokens_threshold=0' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('found:', d['found'])"

# 对照:两词都精确 → 命中
curl -s -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/search?q=macbook%20pro&query_by=name&prefix=true&drop_tokens_threshold=0' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('found:', d['found'], [h['document']['name'] for h in d['hits']])"

实测回显:

found: 3 [('AirPods Pro 2', 1), ('iPhone 16 Pro', 1), ('MacBook Pro 14 M4', 1)]
found: 0
found: 1 ['MacBook Pro 14 M4']

第一条中 num_tokens_dropped: 1 表明命中来自丢词回退(5.7 节):mac 被丢弃、用剩余词 pro 完成检索。第二条禁用丢词后 macb 作为中间词既不精确匹配也不前缀匹配,直接 0 结果。

五、核心查询能力详解

5.1 过滤表达式 filter_by

filter_by 是 Typesense 查询语言中表达式能力最强的部分:支持数值比较、布尔、字符串、数组、嵌套字段与地理半径,条件间用 &&(与)、||(或)组合,可用空格分隔多个条件(空格即"与")。下面逐类实测。

数值比较(>=、< 同理,另有 >、<=、!=):

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

实测回显:

found: 2 [('MacBook Pro 14 M4', 15999.0), ('ThinkPad X1 Carbon Gen 12', 12499.0)]

多条件 && 组合(注意 URL 中 :、&&、<> 需转义,推荐用 --data-urlencode):

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

实测回显:

found: 1 ['ThinkPad X1 Carbon Gen 12']

|| 或条件:

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

实测回显:

found: 3 [('Air Force 1 Sneakers', 'Nike'), ('Pegasus 41 Running Shoes', 'Nike'), ('Ultraboost Light', 'Adidas')]

不等于 !=(排除 Shoes 类目,12-3=9 条):

curl -s -H 'X-TYPESENSE-API-KEY: xyz' --get \
  --data-urlencode 'q=*' --data-urlencode 'query_by=name' \
  --data-urlencode 'filter_by=category:!=Shoes' \
  'http://localhost:13909/collections/products/documents/search' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('found:', d['found'])"

实测回显:

found: 9

多值匹配 [v1, v2](等价于多个 ||):

curl -s -H 'X-TYPESENSE-API-KEY: xyz' --get \
  --data-urlencode 'q=*' --data-urlencode 'query_by=name' \
  --data-urlencode 'filter_by=brand:[Nike, Adidas, Amazon]' \
  'http://localhost:13909/collections/products/documents/search' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('found:', d['found'], sorted({h['document']['brand'] for h in d['hits']}))"

实测回显:

found: 5 ['Adidas', 'Amazon', 'Nike']

数组字段的成员匹配(tags 是 string[],值包含即命中):

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

实测回显:

found: 2 ['Air Force 1 Sneakers', 'Pegasus 41 Running Shoes']

嵌套字段过滤(specs.cpu 属于 object 字段 specs 的子字段,集合需开启 enable_nested_fields):

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

实测回显:

found: 1 [('MacBook Pro 14 M4', {'cpu': 'M4 Pro', 'ram': '24GB'})]

地理半径过滤(geopoint 字段,语法 字段:(纬度, 经度, 半径 单位),单位支持 km/mi 等):

curl -s -H 'X-TYPESENSE-API-KEY: xyz' --get \
  --data-urlencode 'q=*' --data-urlencode 'query_by=name' \
  --data-urlencode 'filter_by=location:(31.2304, 121.4737, 300 km)' \
  'http://localhost:13909/collections/products/documents/search' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('found:', d['found'], [h['document']['name'] for h in d['hits']])"

实测回显(300km 内只有上海仓的 3 条):

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

5.2 分面统计 facet_by

分面(facet)对过滤后的命中集按字段值做计数统计,是筛选面板(左侧类目/品牌导航)的数据来源。前提:字段必须在 schema 中声明 facet: true。对未声明字段传 facet_by 直接报错(实测):

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

实测回显:

{"message":"Could not find a facet field named `price` in the schema."}

对已声明的 brand 分面:

curl -s -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/search?q=*&query_by=name&facet_by=brand' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); fc=d['facet_counts'][0]; print('field:', fc['field_name'], 'sampled:', fc['sampled'], 'total_values:', fc['stats']['total_values']); [print(' ', c['value'], c['count']) for c in fc['counts']]"

实测回显:

field: brand sampled: False total_values: 7
  Apple 3
  Nike 2
  Lenovo 2
  Amazon 2
  Sony 1
  Adidas 1
  Logitech 1

多字段分面用逗号并列:

curl -s -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/search?q=*&query_by=name&facet_by=brand%2Ccategory' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); [print(fc['field_name'], '->', {c['value']:c['count'] for c in fc['counts']}) for fc in d['facet_counts']]"

实测回显:

brand -> {'Apple': 3, 'Nike': 2, 'Lenovo': 2, 'Amazon': 2, 'Sony': 1, 'Adidas': 1, 'Logitech': 1}
category -> {'Shoes': 3, 'Audio': 2, 'Laptop': 2, 'Phone': 1, 'Monitor': 1, 'E-Reader': 1, 'Accessory': 1, 'Smart Home': 1}

与 filter_by 组合时,分面计数基于过滤后的命中集(类目限定 Laptop 后品牌只剩 Apple/Lenovo):

curl -s -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/search?q=*&query_by=name&filter_by=category%3ALaptop&facet_by=brand' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('found:', d['found'], 'facets:', {c['value']:c['count'] for c in d['facet_counts'][0]['counts']})"

实测回显:

found: 2 facets: {'Apple': 1, 'Lenovo': 1}

max_facet_values 限制每个字段的返回条数(按计数降序截断):

curl -s -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/search?q=*&query_by=name&facet_by=brand&max_facet_values=3' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); fc=d['facet_counts'][0]; print('counts:', {c['value']:c['count'] for c in fc['counts']}, 'stats:', fc['stats'])"

实测回显:

counts: {'Apple': 3, 'Lenovo': 2, 'Amazon': 2} stats: {'total_values': 3}

facet_query 在分面值内部再做一次搜索(筛选面板的"搜索品牌"输入框)。brand:ap 只返回值含/前缀匹配 ap 的分面项:

curl -s -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/search?q=*&query_by=name&facet_by=brand&facet_query=brand%3Aap' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); fc=d['facet_counts'][0]; print('field:', fc['field_name'], {c['value']:c['count'] for c in fc['counts']})"

实测回显:

field: brand {'Apple': 3}

数值字段分面额外返回 stats(min/max/sum/avg)。对 books-facet(publication_year 声明 facet)实测:

curl -s -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/books-facet/documents/search?q=*&query_by=title&facet_by=publication_year' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print(json.dumps(d['facet_counts'][0]['stats'], indent=1)); print([c['value'] for c in d['facet_counts'][0]['counts']])"

实测回显:

{
 "avg": 1945.375,
 "max": 1997.0,
 "min": 1813.0,
 "sum": 15563.0,
 "total_values": 8
}
['1997', '1996', '1967', '1960', '1954', '1951', '1925', '1813']

5.3 typo 容错与数量层级

Typesense 的拼写容错由四个参数构成体系,且存在按词长的两级门槛(这是"数量层级"的含义):

参数默认值作用
num_typos2允许的最大编辑距离(typo 数);0 关闭容错
min_len_1typo4词长达到该值才应用 1 级容错
min_len_2typo7词长达到该值才应用 2 级容错
typo_tokens_threshold15命中结果数低于该值时才触发 typo 修正(直到凑够);0 彻底禁用容错

基础验证(macbok 与 macbook 编辑距离 1):

# 默认 num_typos=2:命中
curl -s -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/search?q=macbok&query_by=name' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('found:', d['found'], [(h['document']['name'], h['highlights'][0]['matched_tokens'], h['text_match_info']['typo_prefix_score']) for h in d['hits']])"

# num_typos=0:不命中
curl -s -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/search?q=macbok&query_by=name&num_typos=0' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('found:', d['found'])"

# typo_tokens_threshold=0:同样不命中(触发条件被关闭)
curl -s -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/search?q=macbok&query_by=name&typo_tokens_threshold=0' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('found:', d['found'])"

实测回显:

found: 1 [('MacBook Pro 14 M4', ['MacBook'], 2)]
found: 0
found: 0

1 级容错门槛实测:prk(3 字符)是 pro 的 1 次编辑变体。默认 min_len_1typo=4,3 字符词不享受 1-typo 容错;降到 3 后命中:

# 默认:0 结果
curl -s -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/search?q=prk&query_by=name' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('found:', d['found'])"

# min_len_1typo=3:命中全部 3 条 Pro
curl -s -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/search?q=prk&query_by=name&num_typos=1&min_len_1typo=3' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('found:', d['found'], [(h['document']['name'], h['highlights'][0]['matched_tokens']) for h in d['hits']])"

实测回显:

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

2 级容错门槛实测:macbk 与 macbook 编辑距离 2,作为中间词(与 pro 组合且禁用丢词)时逐级验证:

# num_typos=2 + 默认 min_len_2typo=7:5 字符词不享受 2-typo → 0
curl -s -H 'X-TYPESENSE-API-KEY: xyz' --get \
  --data-urlencode 'q=macbk pro' --data-urlencode 'query_by=name' \
  --data-urlencode 'num_typos=2' --data-urlencode 'drop_tokens_threshold=0' \
  'http://localhost:13909/collections/products/documents/search' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('found:', d['found'])"

# min_len_2typo=5 放行:命中
curl -s -H 'X-TYPESENSE-API-KEY: xyz' --get \
  --data-urlencode 'q=macbk pro' --data-urlencode 'query_by=name' \
  --data-urlencode 'num_typos=2' --data-urlencode 'min_len_2typo=5' --data-urlencode 'drop_tokens_threshold=0' \
  'http://localhost:13909/collections/products/documents/search' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('found:', d['found'], [h['document']['name'] for h in d['hits']])"

# min_len_2typo=6:5 字符仍低于门槛 → 0
curl -s -H 'X-TYPESENSE-API-KEY: xyz' --get \
  --data-urlencode 'q=macbk pro' --data-urlencode 'query_by=name' \
  --data-urlencode 'num_typos=2' --data-urlencode 'min_len_2typo=6' --data-urlencode 'drop_tokens_threshold=0' \
  'http://localhost:13909/collections/products/documents/search' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('found:', d['found'])"

实测回显:

found: 0
found: 1 ['MacBook Pro 14 M4']
found: 0

⚠️ 注意:单词查询中最后一个词同时享受前缀匹配(4.3 节),macbk 的 1-typo 变体 macbo 恰是 macbook 的前缀,因此单词查询 q=macbk&num_typos=1 也能命中——这是"typo + 前缀"叠加的效果,不代表编辑距离门槛失效。演示层级差异时建议像上面一样把容错词放在中间词位置并禁用丢词,以隔离变量。

5.4 空格容错 split_join_tokens

split_join_tokens 把空格当作可容错对象:查询词连写(airforce)时可尝试拆成 air force,查询词分写(air force)时可尝试合并。三个取值:

取值行为
fallback(默认)仅当原始查询零结果时才尝试拆/并
off完全禁用
always无视原始查询是否命中,一律尝试

实测三态:

# 默认 fallback:airforce 无精确命中 → 拆词后命中 Air Force 1
curl -s -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/search?q=airforce&query_by=name' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('found:', d['found'], [(h['document']['name'], h['highlights'][0]['matched_tokens']) for h in d['hits']])"

# off:禁用后 0 结果
curl -s -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/search?q=airforce&query_by=name&split_join_tokens=off' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('found:', d['found'])"

# always:即使两词原文已能命中,也执行拆/并尝试(结果不变但语义强制)
curl -s -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/search?q=air%20force&query_by=name&split_join_tokens=always' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('found:', d['found'], [(h['document']['name'], h['highlights'][0]['matched_tokens']) for h in d['hits']])"

实测回显:

found: 1 [('Air Force 1 Sneakers', ['Air', 'Force'])]
found: 0
found: 1 [('Air Force 1 Sneakers', ['Air', 'Force'])]

5.5 丢词回退 drop_tokens

多词查询中,若"所有词同时出现在同一文档"的结果数低于 drop_tokens_threshold(默认 15),Typesense 会丢弃个体结果最少的词继续检索,直到凑够该数量;0 表示禁用。被丢的词数记录在每个命中的 text_match_info.num_tokens_dropped。

实测(kindle reader echo 三词,无文档同时包含):

# 默认:丢 2 个词,用 kindle 完成检索
curl -s -H 'X-TYPESENSE-API-KEY: xyz' --get \
  --data-urlencode 'q=kindle reader echo' --data-urlencode 'query_by=name' \
  'http://localhost:13909/collections/products/documents/search' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('found:', d['found'], [(h['document']['name'], h['text_match_info']['num_tokens_dropped']) for h in d['hits']])"

# 禁用丢词:0 结果
curl -s -H 'X-TYPESENSE-API-KEY: xyz' --get \
  --data-urlencode 'q=kindle reader echo' --data-urlencode 'query_by=name' \
  --data-urlencode 'drop_tokens_threshold=0' \
  'http://localhost:13909/collections/products/documents/search' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('found:', d['found'])"

实测回显:

found: 1 [('Kindle Paperwhite', 2)]
found: 0

drop_tokens_mode 控制丢词方向(right_to_left 默认、left_to_right、both_sides)。换 echo 在最前的查询验证 left_to_right(从左往右丢:保留最左词 echo):

curl -s -H 'X-TYPESENSE-API-KEY: xyz' --get \
  --data-urlencode 'q=echo kindle reader' --data-urlencode 'query_by=name' \
  --data-urlencode 'drop_tokens_mode=left_to_right' \
  'http://localhost:13909/collections/products/documents/search' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('found:', d['found'], [(h['document']['name'], h['text_match_info']['num_tokens_dropped']) for h in d['hits']])"

实测回显:

found: 1 [('Echo Dot 5', 2)]

books 集合同样验证(prejudice keyboard,丢 keyboard 留 prejudice):

curl -s -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/books/documents/search?q=prejudice%20keyboard&query_by=title' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('found:', d['found'], [(h['document']['title'], h['text_match_info']['num_tokens_dropped']) for h in d['hits']])"

实测回显:

found: 1 [('Pride and Prejudice', 1)]

5.6 分组 group_by

group_by 按字段值分组返回(每组默认按相关性取组内 top),group_limit 控制每组内条数,group_missing_values(默认 true)决定含空值的文档是否聚成 null 组。分组后响应中 hits 被 grouped_hits 取代,且 found(组数)与 found_docs(文档数)分离——这是分组查询最容易被误读的一点。

curl -s -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/search?q=pro&query_by=name&group_by=category&group_limit=3' \
  | python3 -c "import json,sys; d=json.load(sys.stdin)
print('found(组数):', d['found'], 'found_docs(文档数):', d['found_docs'])
for g in d['grouped_hits']:
    print(' group:', g['group_key'], '组内命中:', g['found'], '->', [h['document']['name'] for h in g['hits']])"

实测回显:

found(组数): 3 found_docs(文档数): 3
 group: ['Audio'] 组内命中: 1 -> ['AirPods Pro 2']
 group: ['Phone'] 组内命中: 1 -> ['iPhone 16 Pro']
 group: ['Laptop'] 组内命中: 1 -> ['MacBook Pro 14 M4']

多字段分组(brand,category 以二元组为键,Nike×Shoes 组内 2 条、group_limit=2 全保留):

curl -s -H 'X-TYPESENSE-API-KEY: xyz' --get \
  --data-urlencode 'q=*' --data-urlencode 'query_by=name' \
  --data-urlencode 'group_by=brand,category' --data-urlencode 'group_limit=2' \
  'http://localhost:13909/collections/products/documents/search' \
  | python3 -c "import json,sys; d=json.load(sys.stdin)
print('found(组数):', d['found'], 'found_docs(文档数):', d['found_docs'])
for g in d['grouped_hits']:
    if g['found'] > 1: print(' group:', g['group_key'], '组内命中:', g['found'], '->', [h['document']['name'] for h in g['hits']])"

实测回显:

found(组数): 11 found_docs(文档数): 12
 group: ['Nike', 'Shoes'] 组内命中: 2 -> ['Air Force 1 Sneakers', 'Pegasus 41 Running Shoes']

5.7 排序 sort_by 与 _eval 表达式

sort_by 语法为 字段:方向,逗号分隔最多 3 个字段做依次决胜(tie-breaker);若只指定 1–2 个字段,_text_match 会作为最后一个决胜字段自动追加。不传 sort_by 时默认按 _text_match:desc,default_sorting_field:desc(default_sorting_field 即建集合时声明的字段,products 为 stock)。

三个特殊排序键:

排序键含义
_text_match:desc/ascBM25 相关性得分
_rand() / _rand(42)随机排序(可带正整数种子固定序列)
_eval(表达式):desc/asc条件排序("可选过滤"),表达式语法与 filter_by 相同;命中表达式为 true 的文档排前

多字段排序(先在售优先,再按评分降序):

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

实测回显(前 5 条全部 in_stock=true 且评分递减):

found: 12 [('MX Master 3S Mouse', True, 4.9), ('MacBook Pro 14 M4', True, 4.9), ('iPhone 16 Pro', True, 4.8), ('Kindle Paperwhite', True, 4.7), ('AirPods Pro 2', True, 4.7)]

纯价格排序:

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

实测回显:

found: 12 [15999.0, 12499.0, 8999.0, 2399.0, 2299.0, 1899.0]

字段未开启排序时直接报错(name 建集合时 sort: false):

curl -s -H 'X-TYPESENSE-API-KEY: xyz' --get \
  --data-urlencode 'q=shoes' --data-urlencode 'query_by=name,description' \
  --data-urlencode 'sort_by=_text_match:desc,name:asc' \
  'http://localhost:13909/collections/products/documents/search'

实测回显:

{"message":"Could not find a field named `name` in the schema for sorting."}

单独用 _text_match 排序(观察两条 shoes 文档的相关性差异):

curl -s -H 'X-TYPESENSE-API-KEY: xyz' --get \
  --data-urlencode 'q=shoes' --data-urlencode 'query_by=name,description' \
  --data-urlencode 'sort_by=_text_match:desc' \
  'http://localhost:13909/collections/products/documents/search' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('found:', d['found'], [(h['document']['name'], h['text_match']) for h in d['hits']])"

实测回显:

found: 2 [('Pegasus 41 Running Shoes', 578730123365189753), ('Ultraboost Light', 578730123365189745)]

地理排序(location(纬度, 经度):asc 按距该坐标由近到远):

curl -s -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/search?q=*&query_by=name&sort_by=location(31.2304%2C121.4737)%3Aasc' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('found:', d['found'], [h['document']['name'] for h in d['hits'][:6]])"

实测回显(上海仓 3 条最近在前,随后北京、广州按距离展开):

found: 12 ['AirPods Pro 2', 'iPhone 16 Pro', 'MacBook Pro 14 M4', 'ThinkVision P27h Monitor', 'ThinkPad X1 Carbon Gen 12', 'Ultraboost Light']

_eval 条件排序(官方语法:表达式不带方括号直接写在 _eval() 内):

curl -s -H 'X-TYPESENSE-API-KEY: xyz' --get \
  --data-urlencode 'q=*' --data-urlencode 'query_by=name' \
  --data-urlencode 'sort_by=_eval(in_stock:true):desc,stock:desc' \
  'http://localhost:13909/collections/products/documents/search' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('found:', d['found'], [(h['document']['in_stock'], h['document']['stock']) for h in d['hits'][:8]])"

实测回显(在售文档按库存降序占据前 8 位,售罄的垫底):

found: 12 [(True, 300), (True, 250), (True, 210), (True, 120), (True, 88), (True, 64), (True, 55), (True, 42)]

_eval 还支持给多个条件表达式赋不同权重分数(语法 [(条件):分值, (条件):分值],命中高分者排前):

curl -s -H 'X-TYPESENSE-API-KEY: xyz' --get \
  --data-urlencode 'q=*' --data-urlencode 'query_by=name' \
  --data-urlencode 'sort_by=_eval([ (brand:Nike):3, (brand:Adidas):2 ]):desc,rating:desc' \
  'http://localhost:13909/collections/products/documents/search' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('found:', d['found'], [(h['document']['brand'], h['document']['rating']) for h in d['hits'][:5]])"

实测回显(Nike 全部 2 条在前 → Adidas → 其余按 rating):

found: 12 [('Nike', 4.6), ('Nike', 4.5), ('Adidas', 4.4), ('Logitech', 4.9), ('Apple', 4.9)]

随机排序 _rand()(同一请求两次,顺序不同;固定种子 _rand(42) 可复现):

for i in 1 2; do
  curl -s -H 'X-TYPESENSE-API-KEY: xyz' --get \
    --data-urlencode 'q=*' --data-urlencode 'query_by=name' \
    --data-urlencode 'sort_by=_rand():desc' \
    'http://localhost:13909/collections/products/documents/search' \
    | python3 -c "import json,sys; d=json.load(sys.stdin); print('run:', [h['document']['id'] for h in d['hits'][:6]])"
done

实测回显:

run: ['prod-012', 'prod-003', 'prod-010', 'prod-002', 'prod-007', 'prod-006']
run: ['prod-003', 'prod-009', 'prod-008', 'prod-002', 'prod-004', 'prod-007']

💡 技术拓展:_eval 之外的"可选过滤"思路——

方式原理适用
_eval(expr):desc布尔条件提升排序简单的"满足者优先"
_eval([(c1):3,(c2):2]):desc条件赋分值加权和多档营销加权
filter_by 先过滤 + sort_by 后排序先缩窄集合条件为硬性要求(必须满足)

filter_by 是硬过滤(不满足即排除),_eval 是软排序(不满足仅降位)。本文 6.4 节模板即用 _eval 实现"售罄垫底"而非"售罄隐藏"。

5.8 字段裁剪 include_fields / exclude_fields

响应文档可能很大(含 description、嵌套 specs 等),用这两个参数裁剪返回字段(不影响打分,只影响返回):

curl -s -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/search?q=kindle&query_by=name&include_fields=name%2Cprice' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print(json.dumps(d['hits'][0]['document'], ensure_ascii=False))"

curl -s -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/search?q=iphone&query_by=name&exclude_fields=description%2Cspecs%2Ctags%2Clocation' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print(json.dumps(d['hits'][0]['document'], ensure_ascii=False))"

实测回显:

{"name": "Kindle Paperwhite", "price": 1068.0}
{"brand": "Apple", "category": "Phone", "id": "prod-003", "in_stock": true, "name": "iPhone 16 Pro", "price": 8999.0, "rating": 4.8, "stock": 120}

5.9 高亮控制

高亮默认用 <mark></mark> 包裹命中片段。相关参数:

参数默认作用
highlight_start_tag / highlight_end_tag<mark> / </mark>自定义包裹标签
highlight_affix_num_tokens4命中词前后保留的上下文词数
highlight_full_fields无指定字段返回全文高亮(value 字段),不做片段截断
snippet_threshold30字段词数超过该值才截断为片段
highlight_fields同 query_by只在这些字段上计算高亮

自定义标签实测:

curl -s -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/search?q=pro&query_by=name&highlight_start_tag=%5Bhl%5D&highlight_end_tag=%5B%2Fhl%5D' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['hits'][0]['highlight'])"

实测回显:

{'name': {'matched_tokens': ['Pro'], 'snippet': 'AirPods [hl]Pro[/hl] 2'}}

全文高亮(highlight_full_fields=name 时高亮结构多出 value 全文字段):

curl -s -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/search?q=pro&query_by=name&highlight_full_fields=name' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['hits'][0]['highlight']['name'])"

实测回显:

{'matched_tokens': ['Pro'], 'snippet': 'AirPods <mark>Pro</mark> 2', 'value': 'AirPods <mark>Pro</mark> 2'}

5.10 分页

两套分页参数等价可用:

  • 页码式:page(默认 1)+ per_page(默认 10);
  • 偏移式:offset + limit(深翻页场景推荐 offset/limit,语义与 SQL 一致)。
curl -s -H 'X-TYPESENSE-API-KEY: xyz' \
  'http://localhost:13909/collections/products/documents/search?q=*&query_by=name&page=2&per_page=3&sort_by=price%3Aasc' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('page:', d['page'], 'found:', d['found'], [(h['document']['name'], h['document']['price']) for h in d['hits']])"

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

实测回显(价格升序 12 条,第 2 页取 4-6 位;offset=9 取第 10-12 位):

page: 2 found: 12 [('Pegasus 41 Running Shoes', 899.0), ('Kindle Paperwhite', 1068.0), ('Ultraboost Light', 1099.0)]
[('iPhone 16 Pro', 8999.0), ('ThinkPad X1 Carbon Gen 12', 12499.0), ('MacBook Pro 14 M4', 15999.0)]

六、场景化查询模式

6.1 电商商品搜索模板(过滤 + 分面 + 排序组合)

筛选面板场景一次请求同时拿到结果页与分面计数:

curl -s -H 'X-TYPESENSE-API-KEY: xyz' --get \
  --data-urlencode 'q=shoes' --data-urlencode 'query_by=name' \
  --data-urlencode 'filter_by=in_stock:true' \
  --data-urlencode 'facet_by=brand' \
  --data-urlencode 'sort_by=price:asc' \
  --data-urlencode 'per_page=2' \
  'http://localhost:13909/collections/products/documents/search' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('found:', d['found'], 'hits:', [h['document']['name'] for h in d['hits']], 'facets:', {c['value']:c['count'] for c in d['facet_counts'][0]['counts']})"

实测回显(在售 shoes 只有 Pegasus 一条且为 Nike;Ultraboost 售罄被过滤):

found: 1 hits: ['Pegasus 41 Running Shoes'] facets: {'Nike': 1}

6.2 容错搜索模板

面向用户输入规范差的 C 端搜索框,典型参数组合:

curl -s -H 'X-TYPESENSE-API-KEY: xyz' --get \
  --data-urlencode 'q=macbok pro' --data-urlencode 'query_by=name,description' \
  --data-urlencode 'num_typos=2' --data-urlencode 'typo_tokens_threshold=10' \
  --data-urlencode 'drop_tokens_threshold=10' \
  --data-urlencode 'split_join_tokens=fallback' \
  'http://localhost:13909/collections/products/documents/search' \
  | 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']

6.3 分组去重展示模板

同一品牌只想展示 top N 商品时(不重复刷屏):

curl -s -H 'X-TYPESENSE-API-KEY: xyz' --get \
  --data-urlencode 'q=*' --data-urlencode 'query_by=name' \
  --data-urlencode 'group_by=brand' --data-urlencode 'group_limit=1' \
  --data-urlencode 'sort_by=rating:desc' \
  'http://localhost:13909/collections/products/documents/search' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('组数:', d['found']); [print(' ', g['group_key'], '->', g['hits'][0]['document']['name']) for g in d['grouped_hits']]"

实测回显(7 个品牌各展示评分最高的 1 款):

组数: 7
  ['Apple'] -> MacBook Pro 14 M4
  ['Logitech'] -> MX Master 3S Mouse
  ['Amazon'] -> Kindle Paperwhite
  ['Nike'] -> Pegasus 41 Running Shoes
  ['Lenovo'] -> ThinkPad X1 Carbon Gen 12
  ['Sony'] -> WH-1000XM5 Headphones
  ['Adidas'] -> Ultraboost Light

6.4 售罄垫底排序模板(_eval 实战)

不隐藏售罄商品但让它排最后(保曝光、不伤体验):

curl -s -H 'X-TYPESENSE-API-KEY: xyz' --get \
  --data-urlencode 'q=*' --data-urlencode 'query_by=name' \
  --data-urlencode 'sort_by=_eval(in_stock:true):desc,rating:desc' \
  'http://localhost:13909/collections/products/documents/search' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print([(h['document']['name'], h['document']['in_stock']) for h in d['hits'][-3:]])"

实测回显(最后 3 位中售罄的 WH-1000XM5/Ultraboost 按评分次序垫底):

[('AirPods Pro 2', True), ('WH-1000XM5 Headphones', False), ('Ultraboost Light', False)]

七、函数与运算符速查

7.1 filter_by 操作符

操作符示例说明
:category:Laptop等值匹配(字符串/数值/布尔/数组成员)
!=category:!=Shoes不等于
> >= < <=price:>=12499数值比较
&&a:1 && b:2与(空格分隔默认同为与)
||brand:Nike || brand:Adidas或
[v1, v2]brand:[Nike, Adidas]多值 in 匹配
(lat, lon, r km)location:(31.23, 121.47, 300 km)地理半径(单位 km/mi/cm/mm/ft/in/yd)
嵌套路径specs.cpu:M4object 子字段(需 enable_nested_fields)

7.2 sort_by 特殊键

键示例说明
_text_match_text_match:descBM25 相关性得分(默认排序首键)
_rand() / _rand(种子)_rand():desc随机序;种子为正整数时序列可复现
_eval(条件)_eval(in_stock:true):desc条件布尔排序,语法同 filter_by
_eval([(c):分值])_eval([(brand:Nike):3]):desc条件加权评分排序
geo字段(lat,lon)location(31.23,121.47):asc按距坐标远近排序

7.3 核心参数默认值(30.2 实测/官方标注)

参数默认值说明
per_page / page10 / 1页大小 / 页码
num_typos2最大容错编辑距离
typo_tokens_threshold15结果不足才触发容错;0 禁用
drop_tokens_threshold15联合结果不足才丢词;0 禁用
min_len_1typo / min_len_2typo4 / 71/2 级容错的词长门槛
split_join_tokensfallback空格容错模式
drop_tokens_moderight_to_left丢词方向
prefix仅最后一个词生效前缀匹配开关
highlight_start_tag / highlight_end_tag<mark> / </mark>高亮包裹标签
highlight_affix_num_tokens4高亮上下文词数
snippet_threshold30片段截断词数阈值
group_limit3每组返回条数
max_facet_values10每字段分面项上限
search_cutoff_ms30000查询超时(触发后 search_cutoff=true)

八、性能与陷阱

  1. 丢词回退改变 found 语义:默认 drop_tokens_threshold=15 下,多词查询命中数不足 15 就会丢词,found 反映的是丢词后的结果集(5.5 节 num_tokens_dropped: 2)。要求"必须全词匹配"的场景(如后台精确筛选)应设 drop_tokens_threshold=0,否则结果可能比预期宽。
  2. prefix=true 不是全词前缀:实测多词查询的中间词不做前缀(4.3 节 macb pro 案例)。需要输入到一半就有结果的搜索框,应把未完词放在末尾(交互上即如此),不要指望 prefix=true 补偿。
  3. 容错层级误判:num_typos=2 不代表短词能享受 2 级容错——词长门槛(默认 4/7)先于编辑距离生效。小词表上"明明 1 个字母之差却搜不到"多半是词长不足 4;对短词场景(SKU、型号)显式调低 min_len_1typo。
  4. facet 字段必须预声明:facet_by 对未声明 facet: true 的字段直接 4xx 报错(5.2 节实测)。schema 设计期就要确定哪些字段上筛选面板,事后只能通过 alter 接口加新字段。
  5. _eval 方括号语法差异:布尔条件排序是 _eval(in_stock:true):desc(无方括号),多条件加权才是 _eval([ (c):3 ]):desc(有方括号)。两者混用会报 Parameter sort_by is malformed(实测)。
  6. search_cutoff 的触发条件:search_cutoff_ms 默认 30000,超时后返回已收集的部分结果并把 search_cutoff 置 true。本文 12 文档数据集实测 search_cutoff_ms=1 也未触发(search_cutoff: false, search_time_ms: 0),该参数面向百万级文档的深检索兜底,而非小数据集调参项。
  7. 分面与排序的资源开销:分面统计和 _rand() 都在命中集上额外计算,max_facet_values 过大或分面字段基数过高会放大内存与耗时;筛选面板应配合 filter_by 缩窄命中集后再分面。

九、常见问题

Q1:facet_by 报 Could not find a facet field named xxx in the schema. 字段未在 schema 中声明 "facet": true。重建集合时声明,或用 collections alter 接口新增一个 facet 字段后重建数据。

Q2:sort_by 报 Parameter sort_by is malformed. _eval 语法写错:布尔条件不要加方括号;加权评分必须加方括号且每个条件自带括号。参照 5.7 节两种实测写法。

Q3:sort_by 报 Could not find a field named xxx in the schema for sorting. 目标字段建集合时未声明 "sort": true。string 字段默认不可排序,数值/布尔/geopoint 可在 schema 中开启。

Q4:多词查询结果比预期多,少了某个词也能命中。 丢词回退生效(num_tokens_dropped 大于 0)。设 drop_tokens_threshold=0 禁用,或接受回退但用 text_match_info.num_tokens_dropped 在业务层过滤。

Q5:双引号短语查询不生效。 URL 中引号需转义传递:用 --data-urlencode 'q="running shoes"'(curl)或客户端库的参数编码;直接拼 URL 会把引号丢掉退化成普通多词查询。

Q6:短查询词(如 3 字符型号)错一个字母搜不到。 默认 min_len_1typo=4 对短词关闭 1 级容错。显式传 min_len_1typo=2(或 3)放行,并配合 num_typos=1。

十、参考链接