docs/reference/query-languages/esql/esql-cross-serverless-projects.md
Cross-project search (CPS) enables you to run queries across multiple linked {{serverless-short}} projects from a single request.
There are several ways to control which projects a query runs against:
This page covers {{esql}}-specific CPS behavior. Before continuing, make sure you are familiar with the following:
The default behavior is to query across the origin project and all linked projects automatically.
The following example queries the data index and includes the _index metadata field to identify which project each result came from:
GET /_query
{
"query": "FROM data METADATA _index", <1>
"include_execution_metadata": true <2>
}
METADATA _index returns the fully-qualified index name for each document. Documents from linked projects include the project alias prefix, for example linked-project-1:data._clusters object in the response. Defaults to false.The response includes:
_clusters object showing the status of each participating projectvalues array where each row includes the qualified index name identifying which project the document came from:::{dropdown} Example response
{
"took": 329,
"is_partial": false,
"columns": [
{ "name": "_index", "type": "keyword" }
],
"values": [
["data"], <1>
["linked-project-1:data"] <2>
],
"_clusters": {
"total": 2,
"successful": 2,
"running": 0,
"skipped": 0,
"partial": 0,
"failed": 0,
"details": {
"_origin": { <3>
"status": "successful",
"indices": "data",
"took": 328,
"_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }
},
"linked-project-1": { <4>
"status": "successful",
"indices": "data",
"took": 256,
"_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }
}
}
}
}
_origin is the reserved identifier for the origin project.Project routing limits the scope of a query to specific projects, based on tag values. Project routing happens before query execution, so excluded projects are never queried. This can help reduce cost and latency.
:::{note}
Project routing expressions use Lucene query syntax. The : operator matches a tag value, equivalent to = in other query languages. For example, _alias:my-project matches projects whose alias is my-project.
:::
You can specify project routing in two ways:
SET: This approach works wherever you can write an {{esql}} query._query API request body: You can pass a project_routing field to keep project routing logic separate from the query string.:::{important}
If both options are combined, SET project_routing takes precedence.
:::
SET source commandSET project_routing embeds project routing directly within the {{esql}} query. You can use this approach wherever you write {{esql}}. SET must appear before other {{esql}} commands. The semicolon after the last parameter separates it from the rest of the query. The order of parameters within SET does not matter.
SET project_routing="_alias:my-project"; <1>
FROM data
| STATS COUNT(*)
my-project.project_routing in the API request bodyIf you are constructing the full _query request, you can pass the project_routing field in the request body. This keeps project routing logic separate from the query string:
GET /_query
{
"query": "FROM data | STATS COUNT(*)",
"project_routing": "_alias:my-project" <1>
}
my-project.Both options support referencing a named project routing expression using the @ prefix.
Before you can reference a named expression, you must create it using the _project_routing API.
For instructions, refer to Using named project routing expressions.
::::{tab-set}
:::{tab-item} Request body
GET /_query
{
"query": "FROM logs | STATS COUNT(*)",
"project_routing": "@custom-expression"
}
:::
:::{tab-item} SET directive
SET project_routing="@custom-expression";
FROM logs
| STATS COUNT(*)
:::
::::
{{esql}} supports two types of index expressions:
logs*.project1:logs*.Use _origin: to target only the project from which the query is run:
FROM _origin:data <1>
| STATS COUNT(*)
_origin always refers to the origin project, regardless of its alias.Prefix the index name with the linked project's alias:
FROM linked-project-1:data <1>
| STATS COUNT(*)
linked-project-1 with the actual project alias.Prefix an index expression with - to exclude it from the resolved set.
The following example uses -_origin:* to exclude all indices from the origin project:
FROM data,-_origin:* <1>
| STATS COUNT(*)
data is resolved across all projects except the origin project.::::{note}
*: in CPS does not behave like *: in cross-cluster search (CCS) (which is used to query across clusters in non-serverless deployments):
*: targets all remote clusters and excludes the local cluster.*: resolves against all projects including the origin, the same as an unqualified expression.
::::You can mix unqualified and qualified expressions in the same query:
FROM data, _origin:logs <1>
| LIMIT 100
data is resolved across all projects. _origin:logs is resolved only in the origin project.::::{tip} Error handling differs between expression types. Unqualified expressions fail only if the index exists in none of the searched projects. Qualified expressions fail if the index is missing from the targeted project, regardless of whether it exists elsewhere. For a detailed explanation, refer to Unqualified expression behavior. ::::
Use the METADATA keyword in a FROM command to include project-level information alongside query results.
Project metadata fields use the _project. prefix to distinguish them from document fields.
You can use project metadata fields in two ways:
WHERE, STATS, and KEEP, to filter, aggregate, or sort results by project. Note: WHERE filters results after all projects are queried and does not limit query scope.Available fields include all predefined tags and any custom tags you have defined.
You can also use wildcard patterns such as _project.my-prefix* or _project.*.
For a full list of predefined tags, refer to Tags in CPS.
::::{important}
You must declare a project metadata field in the METADATA clause to use it anywhere in the query, including in WHERE, STATS, KEEP, and other downstream commands.
::::
Include _project._alias in METADATA to add the project alias as a column on each result row:
FROM logs* METADATA _project._alias <1>
| KEEP @timestamp, message, _project._alias
_project._alias in METADATA makes it available in KEEP and other downstream commands.:::{dropdown} Example response
{
"took": 47,
"is_partial": false,
"columns": [
{ "name": "@timestamp", "type": "date" },
{ "name": "message", "type": "keyword" },
{ "name": "_project._alias", "type": "keyword" }
],
"values": [
["2025-01-15T10:23:00.000Z", "connection established", "origin-project"], <1>
["2025-01-15T10:24:00.000Z", "request timeout", "linked-project-1"], <2>
["2025-01-15T10:25:00.000Z", "disk full", "linked-project-1"]
]
}
Include _project._alias in METADATA to group and count results by project:
FROM logs* METADATA _project._alias <1>
| STATS doc_count = COUNT(*) BY _project._alias
_project._alias must be in METADATA to use it in STATS ... BY.A project tag in a WHERE clause filters the result set after the query runs across all projects. It does not limit which projects are queried.
The following examples show the difference between filtering with WHERE and restricting the query scope with project routing.
WHERE (post-query)FROM logs* METADATA _project._csp <1>
| WHERE _project._csp == "aws" <2>
METADATA to use it in downstream commands.::::{important}
Filtering with WHERE on a project tag happens after all projects are queried. To optimize a query, use project routing to select projects before execution.
::::
SET project_routing="_alias:aws-project"; <1>
FROM logs*
| STATS COUNT(*)
aws-project is queried. No data is fetched from other projects. For supported project routing tags, refer to Limitations.Project routing and project metadata serve different purposes and are independent of each other. Project routing determines which projects are queried, before execution. METADATA makes tag values available in query results and downstream commands, at query time.
Using a tag in METADATA does not route the query. Using project routing does not populate METADATA fields.
To both restrict queried projects and include tag values in results, specify both:
SET project_routing="_alias:*linked*"; <1>
FROM logs METADATA _project._alias <2>
| STATS COUNT(*) BY _project._alias
*linked*. Only those projects are queried._project._alias so it can be used in STATS. Results show a count per matched project.Initially, project routing only supports the _alias tag.
Other predefined tags (_csp, _region, and so on) and custom tags are not yet supported as project routing criteria.
{{esql}} LOOKUP JOIN follows the same constraints as {{esql}} cross-cluster LOOKUP JOIN.
The lookup index must exist on every project being queried, because each project uses its own local copy of the lookup index data.
FROM command: full reference for index expressions and METADATA syntax.SET directive: full reference for the SET directive in {{esql}}.LOOKUP JOIN: details on LOOKUP JOIN constraints, including cross-cluster and cross-project support.