# JOIN 关联查询
Typesense 支持基于相关列将一个或多个集合中的文档进行 JOIN 关联查询。
注意: 目前尚不支持通过 alter 操作为现有集合添加引用字段。
# 一对一关系
创建集合时,您可以通过 reference
属性创建一个字段,将文档连接到另一个集合中的字段。
例如,我们可以使用 authors
集合的 id
字段作为引用,将 books
集合连接到 authors
集合:
{
"name": "books",
"fields": [
{"name": "title", "type": "string"},
{"name": "author_id", "type": "string", "reference": "authors.id"}
]
}
当我们搜索 books
集合时,可以通过 include_fields
从 authors
集合获取作者字段。
curl "http://localhost:8108/multi_search" -X POST \
-H "X-TYPESENSE-API-KEY: ${TYPESENSE_API_KEY}" \
-d '{
"searches": [
{
"collection": "books",
"include_fields": "$authors(first_name,last_name)",
"q": "famous"
}
]
}'
通过 $authors(first_name,last_name)
请求 first_name
和 last_name
时,响应会包含一个带有相应作者信息的 authors
对象:
{
"document": {
"id": "0",
"title": "Famous Five",
"author_id": "0",
"authors": {
"first_name": "Enid",
"last_name": "Blyton"
}
}
}
要包含集合中的所有字段,应使用星号 *
:
{
"collection": "books",
"include_fields": "$authors(*)",
"q": "famous"
}
假设我们想查询 authors
集合并获取匹配查询的所有作者的相关书籍。由于 books
集合具有引用字段而我们正在搜索 authors
集合,不能简单地指定 include_fields: $books(*)
来连接相关文档。为此,我们需要像这样指定 filter_by
子句来连接集合:
{
"collection": "authors",
"q": "query",
"filter_by": "$books(id: *)",
"include_fields": "$books(*)",
}
id:*
是一个特殊过滤器,它匹配特定集合的所有文档,因此在连接中使用时,它允许您列出特定作者的所有相关书籍。
int32
、int64
和 string
类型的字段可用于"一对一"引用情况,即一个文档恰好关联一个引用文档。int32[]
、int64[]
和 string[]
类型的字段可用于多引用情况,即一个文档关联另一个集合中的零个或多个文档。
# 一对多关系
# 简单示例
让我们从一个简单示例开始,假设我们有一个 orders
集合,需要跟踪每位客户的不同订单,即每位客户可能拥有多个不同的订单。
customers
集合的 schema 如下所示:
{
"name": "customers",
"fields": [
{"name": "forename", "type": "string"},
{"name": "surname", "type": "string"},
{"name": "email", "type": "string"}
]
}
接着我们创建一个 orders
集合来存储订单详情,并通过引用关联到 customers
集合中对应的用户:
{
"name": "orders",
"fields": [
{"name": "total_price", "type": "float"},
{"name": "initial_date", "type": "int64"},
{"name": "accepted_date", "type": "int64", "optional": true},
{"name": "completed_date", "type": "int64", "optional": true},
{"name": "customer_id", "type": "string", "reference": "customers.id"}
]
}
现在我们可以搜索 customers
集合,并通过 filter_by
参数从 orders
集合中筛选特定客户的订单:
{
"q":"*",
"collection":"customers",
"filter_by":"$orders(customer_id:=customer_a)"
}
也可以根据其他字段进行筛选,例如查询总价低于 100
的订单对应的客户:
{
"q": "*",
"collection": "customers",
"filter_by": "$orders(total_price:<100)"
}
默认情况下,上述查询会包含被引用集合 orders
的所有字段。如果只需要包含被引用集合中的 total_price
字段,可以这样操作:
{
"include_fields": "$orders(total_price)"
}
# 专业示例
为了展示一个更专业的应用场景,假设我们有一个 products
产品集合,需要为不同客户提供个性化定价,即每个产品对每位客户都有不同的价格。此时,联合查询功能就能派上用场。
products
集合的 schema 结构如下:
{
"name": "products",
"fields": [
{"name": "product_id", "type": "string"},
{"name": "product_name", "type": "string"},
{"name": "product_description", "type": "string"}
]
}
我们再创建一个 customer_product_prices
集合,用于存储每位客户的定制价格,同时通过引用关联到产品集合中的对应文档:
{
"name": "customer_product_prices",
"fields": [
{"name": "customer_id", "type": "string"},
{"name": "custom_price", "type": "float"},
{"name": "product_id", "type": "string", "reference": "products.product_id"}
]
}
现在我们可以搜索 products
集合,并通过 filter_by
参数从 customer_product_prices
集合中筛选特定客户的价格:
{
"q":"*",
"collection":"products",
"filter_by":"$customer_product_prices(customer_id:=customer_a)"
}
如果需要查询价格低于 100
的产品?同样可以轻松实现:
{
"q": "*",
"collection": "products",
"filter_by": "$customer_product_prices(customer_id:=customer_a && custom_price:<100)"
}
与简单示例类似,您也可以仅包含引用集合中的 custom_price
字段:
{
"include_fields": "$customer_product_prices(custom_price)"
}
# 多对多关系
考虑一个文档集合,我们希望为用户提供访问权限,使得一个文档可以被多个用户访问,同时一个用户可以访问多个文档。
要实现这一点,我们可以创建三个集合:documents
(文档)、users
(用户)和 user_doc_access
(用户文档访问权限),其模式如下:
{
"name": "documents",
"fields": [
{"name": "id", "type": "string"},
{"name": "title", "type": "string"}
{"name": "content", "type": "string"}
]
}
{
"name": "users",
"fields": [
{"name": "id", "type": "string"},
{"name": "username", "type": "string"}
]
}
{
"name": "user_doc_access",
"fields": [
{"name": "user_id", "type": "string", "reference": "users.id"},
{"name": "document_id", "type": "string", "reference": "documents.id"},
]
}
要获取用户 user_a
可以访问的所有文档,可以这样查询:
{
"q": "*",
"collection": "documents",
"filter_by": "$user_doc_access(user_id:=user_a)"
}
要获取可以访问特定文档的用户ID:
{
"q": "*",
"collection": "documents",
"query_by": "title",
"filter_by": "$user_doc_access(id: *)",
"include_fields": "$users(id) as user_identifier"
}
当引用字段是数组类型(string[], int32[], int64[])时,即使某个文档只与一个文档有关联,我们也会始终以数组形式返回关联文档。由于 user_doc_access
集合使用的是单一引用字段类型,只有当它与多个文档有关联时,才会以数组形式返回关联文档。可以在 include_fields
中指定 strategy: nest_array
来强制 Typesense 始终以数组形式返回关联文档,无论关联数量多少。详情请参阅此处的"强制嵌套数组关联字段"部分。
# 按关联集合字段排序
遵循 $JoinedCollectionName( ... )
的命名约定,我们可以通过以下方式对关联集合中的字段进行 sort_by
排序:
{
"sort_by": "$JoinedCollectionName( 字段名:desc )"
}
同样地,要使用关联集合的字段指定 _eval
操作:
{
"sort_by": "$JoinedCollectionName( _eval(字段名:foo):desc )"
}
# 合并/嵌套关联字段
默认情况下,当我们关联一个集合时,该集合的字段会作为嵌套文档返回。
例如,当我们像上面那样将 books
集合与 authors
集合关联时,注意 authors
集合的字段是如何作为对象出现在响应文档中的:
{
"document": {
"id": "0",
"title": "Famous Five",
"author_id": "0",
"authors": {
"first_name": "Enid",
"last_name": "Blyton"
}
}
}
我们可以通过使用 merge
策略,将 authors
集合的字段与 books
文档的字段合并:
{
"collection": "books",
"include_fields": "$authors(*, strategy: merge)",
"q": "famous"
}
默认行为是 strategy: nest
(嵌套策略)。
# 强制嵌套字段使用数组格式
在一对多关联查询中,您可能希望关联集合的字段始终以对象数组的形式呈现,即使只有一个匹配项。
例如,给定以下作者和书籍数据:
{"id": "0", ",first_name": "Enid", "last_name": "Blyton"}
{"id": "1", ",first_name": "JK", "last_name": "Rowling"}
{"title": "Famous Five", "author_id": "0"}
{"title": "Secret Seven", "author_id": "0"}
{"title": "Harry Potter", "author_id": "1"}
当我们查询 authors
集合并关联 books
集合时,如下所示:
{
"collection": "authors",
"q": "*",
"filter_by": "$books(id:*)",
"include_fields": "$books(*)"
}
根据每个作者匹配的书籍数量(1本或多本),books 字段可能呈现为嵌套对象或嵌套对象数组:
[
{
"document": {
"id": "1",
"first_name": "JK",
"last_name": "Rowling",
"books": {
"author_id": "1",
"id": "2",
"title": "Harry Potter"
}
}
},
{
"document": {
"id": "0",
"first_name": "Enid",
"last_name": "Blyton",
"books": [
{
"author_id": "0",
"id": "0",
"title": "Famous Five"
},
{
"author_id": "0",
"id": "1",
"title": "Secret Seven"
}
]
}
}
]
若要始终使关联的 books
集合字段以对象数组形式返回,可以使用 nest_array
字段合并策略:
{
"collection": "authors",
"q": "*",
"filter_by": "$books(id:*)",
"include_fields": "$books(*, strategy: nest_array)"
}
这将始终为 books
集合的字段返回对象数组:
[
{
"document": {
"id": "1",
"first_name": "JK",
"last_name": "Rowling",
"books": [
{
"author_id": "1",
"id": "2",
"title": "Harry Potter"
}
]
}
},
{
"document": {
"id": "0",
"first_name": "Enid",
"last_name": "Blyton",
"books": [
{
"author_id": "0",
"id": "0",
"title": "Famous Five"
},
{
"author_id": "0",
"id": "1",
"title": "Secret Seven"
}
]
}
}
]
# 异步引用
在创建引用字段时,可以使用 async_reference
选项。这样即使被引用的文档尚不存在,也能成功索引文档。
{
"name": "orders",
"fields": [
{"name": "product_id", "type": "string", "reference": "products.product_id", "optional": true, "async_reference": true}
]
}
通过设置 async_reference: true
,文档将被索引且不会返回以下错误:
在集合 '...' 中未找到具有 '...' 的引用文档。
当被引用的文档后续被索引时,引用关系会自动解析。这在需要按照不同于依赖关系的顺序索引文档的场景中特别有用。
# 对象内的引用
假设 orders
集合中有一个名为 order
的 object
类型字段。我们可以让订单引用 products
集合中的产品,如下所示:
{
"name": "orders",
"fields": [
{"name": "order", "type": "object"},
{"name": "order.product_id", "type": "string", "reference": "products.id"}
]
}
或者,如果我们有一组 order
对象数组,每个订单对象都包含一个引用,那么引用字段的类型也必须是数组。
{
"name": "orders",
"fields": [
{"name": "orders", "type": "object[]"},
{"name": "orders.product_id", "type": "string[]", "reference": "products.id"}
]
}
WARNING
对于嵌套对象内的引用,不支持 async_reference
参数。这些引用必须在索引时解析。
# 在 Join 操作中使用别名
您可以在引用字段定义中使用集合别名。在下面的示例中,products
可以是一个别名:
{"name": "product_id", "type": "string", "reference": "products.product_id"}
但需要注意的是,集合文档存储的是被引用集合文档的内部 ID。这些内部 ID 本质上是顺序分配的,根据文档被索引的顺序进行分配。
因此,必须将被引用的集合视为一个整体来处理,也就是说,如果您打算通过别名更新来切换任何一个集合,您必须同时重新索引所有相关集合。这将确保参与 join 操作的所有集合中的内部 ID 保持同步。
# 左连接 (Left Join)
默认情况下,Typesense 执行的是内连接(inner join)。要执行左连接,可以指定:
{
"filter_by": "id:* || $join_collection_name( <join_condition> )"
}
id:*
会匹配被搜索集合的所有文档。因此,如果存在引用,结果将包含被引用的文档;否则将原样返回文档。
# 嵌套关联 (Nested Joins)
Typesense 支持在涉及多级数据检索和过滤的查询中使用嵌套关联。假设我们正在管理多个零售商的库存,其中某个产品可能有多种变体。数据可以这样建模:
{
"name": "products",
"fields": [
{ "name": "name", "type": "string" }
]
}
{
"name": "product_variants",
"fields": [
{ "name": "title", "type": "string" },
{ "name": "price", "type": "float" },
{ "name": "product_id", "type": "string", "reference": "products.id" }
]
}
{
"name": "retailers",
"fields": [
{ "name": "title", "type": "string" },
{ "name": "location", "type": "geopoint" }
]
}
{
"name": "inventory",
"fields": [
{ "name": "qty", "type": "int32" },
{ "name": "retailer_id", "type": "string", "reference": "retailers.id" },
{ "name": "product_variant_id", "type": "string", "reference": "product_variants.id" }
]
}
嵌套关联语法遵循 $JoinedCollectionName( $NestedJoinedCollectionName(...))
的约定。要搜索产品名称并获取每个零售商的所有库存,可以发送以下查询:
{
"collection": "products",
"q": "shampoo",
"query_by": "name",
"filter_by": "$product_variants( $inventory( $retailers(id:*)))",
"include_fields": "$product_variants(price, $inventory(qty, $retailers(title)))"
}
如果我们想搜索特定地理半径内的产品,可以发送以下查询:
{
"collection": "products",
"q": "shampoo",
"query_by": "name",
"filter_by": "$product_variants( $inventory( $retailers(location:(48.87538726829884, 2.296113163780903,1 km))))",
"include_fields": "$product_variants(price, $inventory(qty, $retailers(title)))"
}
如果我们还想按价格进行过滤,可以发送以下查询:
{
"collection": "products",
"q": "shampoo",
"query_by": "name",
"filter_by": "$product_variants(price:<100 && $inventory( $retailers(location:(48.87538726829884, 2.296113163780903,1 km))))",
"include_fields": "$product_variants(price, $inventory(qty, $retailers(title)))"
}
# 按嵌套关联集合的字段排序
遵循 $JoinedCollectionName( $NestedJoinedCollectionName(...))
的命名约定,我们可以通过以下方式指定对嵌套关联集合中的字段进行排序:
{
"sort_by": "$JoinedCollectionName( $NestedJoinedCollectionName(field_name:desc))"
}
← 对话式搜索(RAG) 分析与查询建议 →