Skip to content

Commit 8412e6d

Browse files
Offload leaf search work to AWS Lambda functions (quickwit-oss#6157)
* Offload leaf search work to AWS Lambda functions The goal is to handle traffic spikes gracefully without provisioning additional searcher nodes: when the local search queue is saturated, overflow splits are transparently routed to Lambda for processing. The offloading decision happens **on the leaf side**, inside the `SearchPermitProvider`. The permit provider already manages a bounded queue of pending split search tasks (gated by memory budget and download slots). When a leaf search request arrives, the provider checks the current queue depth against a configurable `offload_threshold`. If granting permits for all requested splits would exceed this threshold, only enough splits to fill up to the threshold are processed locally — the rest are marked for offloading. The offloaded splits are batched (up to `max_splits_per_invocation` splits per batch, balanced by document count) and sent to Lambda in parallel. Each Lambda invocation runs the same leaf search code path and **returns per-split results individually**. This is important: the per-split responses are fed back into the `IncrementalCollector` and populate the **partial result cache**, so subsequent queries hitting the same splits benefit from cached results regardless of whether the split was searched locally or on Lambda. Depending on the configuration, the Lambda function code can be **deployed automatically** at startup. The `quickwit-lambda-client` crate embeds a compressed Lambda binary at compile time. When `auto_deploy` is configured, Quickwit will: 1. Check if a published Lambda version matching the current binary already exists (identified by a description tag `quickwit:{version}-{hash}`) 2. Create or update the function and publish a new version if needed 3. Garbage-collect old versions (keeping the current one + 5 most recent) This ensures the Lambda function always matches the running Quickwit version without any external deployment tooling. Manual deployment is also supported for users who prefer to manage Lambda functions through Terraform or other IaC tools. Lambda offloading is opt-in. Add a `lambda` section under `searcher` in the node configuration: ```yaml searcher: lambda: offload_threshold: 100 # queue depth before offloading kicks in (0 = always offload) max_splits_per_invocation: 10 auto_deploy: execution_role_arn: arn:aws:iam::123456789012:role/quickwit-lambda-role memory_size: 5 GiB invocation_timeout_secs: 15 ``` - **`quickwit-lambda-client`**: Handles Lambda invocation (with metrics) and auto-deployment logic. Embeds the Lambda binary at build time. - **`quickwit-lambda-server`**: The Lambda function handler itself — receives a `LeafSearchRequest`, runs `multi_index_leaf_search`, and returns per-split `LeafSearchResponse`s. - **`quickwit-search`**: New `LambdaLeafSearchInvoker` trait; `SearchPermitProvider` gains `get_permits_with_offload` to split work between local and offloaded; `leaf.rs` orchestrates local and Lambda tasks in parallel. - **`quickwit-config`**: New `LambdaConfig` and `LambdaDeployConfig` structs under `SearcherConfig`. - **`quickwit-serve`**: Initializes the Lambda invoker at startup when configured. - **`quickwit-proto`**: New `LeafSearchResponses` wrapper message for batched per-split responses.
1 parent 37079b3 commit 8412e6d

45 files changed

Lines changed: 3664 additions & 246 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/actions/cargo-build-macos-binary/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ runs:
6060
path: ./${{ env.ASSET_FULL_NAME }}.tar.gz
6161
retention-days: 3
6262
- name: Deploy archive to GitHub release
63-
uses: quickwit-inc/upload-to-github-release@v1
63+
uses: quickwit-inc/upload-to-github-release@9b2c40fba23bf8dea05b7d2eece24cbc95d4a190
6464
env:
6565
GITHUB_TOKEN: ${{ inputs.token }}
6666
with:

.github/actions/cross-build-binary/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ runs:
5656
path: ./${{ env.ASSET_FULL_NAME }}.tar.gz
5757
retention-days: 3
5858
- name: Upload archive
59-
uses: quickwit-inc/upload-to-github-release@v1
59+
uses: quickwit-inc/upload-to-github-release@9b2c40fba23bf8dea05b7d2eece24cbc95d4a190
6060
env:
6161
GITHUB_TOKEN: ${{ inputs.token }}
6262
with:
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# This workflow creates a new release for a quickwit search aws lambda.
2+
# The artifact is a zip file containing a binary for ARM 64,
3+
# ready to be deployed by the deployer.
4+
#
5+
# See quickwit-lambda-client/README.md
6+
name: Release Lambda binary
7+
8+
on:
9+
push:
10+
tags:
11+
- 'lambda-*'
12+
workflow_dispatch:
13+
inputs:
14+
version:
15+
description: 'Version tag (e.g., v0.8.0)'
16+
required: false
17+
default: 'dev'
18+
19+
permissions:
20+
contents: read
21+
22+
jobs:
23+
build-lambda:
24+
name: Build Lambda ARM64
25+
runs-on: ubuntu-latest
26+
permissions:
27+
contents: write
28+
actions: write
29+
steps:
30+
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
31+
32+
- name: Set version
33+
run: |
34+
if [ "${{ github.ref_type }}" = "tag" ]; then
35+
# Extract version from tag (e.g., lambda-v0.8.0 -> v0.8.0)
36+
echo "ASSET_VERSION=${GITHUB_REF_NAME#lambda-}" >> $GITHUB_ENV
37+
elif [ -n "${{ github.event.inputs.version }}" ] && [ "${{ github.event.inputs.version }}" != "dev" ]; then
38+
echo "ASSET_VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV
39+
else
40+
echo "ASSET_VERSION=dev-$(git rev-parse --short HEAD)" >> $GITHUB_ENV
41+
fi
42+
43+
- name: Install rustup
44+
run: curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain none -y
45+
46+
- name: Install cross
47+
run: cargo install cross
48+
49+
- name: Retrieve and export commit date, hash, and tags
50+
run: |
51+
echo "QW_COMMIT_DATE=$(TZ=UTC0 git log -1 --format=%cd --date=format-local:%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_ENV
52+
echo "QW_COMMIT_HASH=$(git rev-parse HEAD)" >> $GITHUB_ENV
53+
echo "QW_COMMIT_TAGS=$(git tag --points-at HEAD | tr '\n' ',')" >> $GITHUB_ENV
54+
55+
- name: Build Lambda binary
56+
run: cross build --release --features lambda-release --target aarch64-unknown-linux-gnu -p quickwit-lambda-server --bin quickwit-aws-lambda-leaf-search
57+
env:
58+
QW_COMMIT_DATE: ${{ env.QW_COMMIT_DATE }}
59+
QW_COMMIT_HASH: ${{ env.QW_COMMIT_HASH }}
60+
QW_COMMIT_TAGS: ${{ env.QW_COMMIT_TAGS }}
61+
working-directory: ./quickwit
62+
63+
- name: Create Lambda zip
64+
run: |
65+
cd quickwit/target/aarch64-unknown-linux-gnu/release
66+
cp quickwit-aws-lambda-leaf-search bootstrap
67+
zip quickwit-aws-lambda-${{ env.ASSET_VERSION }}-aarch64.zip bootstrap
68+
mv quickwit-aws-lambda-${{ env.ASSET_VERSION }}-aarch64.zip ../../../../
69+
70+
- name: Upload to GitHub release
71+
uses: quickwit-inc/upload-to-github-release@9b2c40fba23bf8dea05b7d2eece24cbc95d4a190
72+
env:
73+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
74+
with:
75+
file: quickwit-aws-lambda-${{ env.ASSET_VERSION }}-aarch64.zip
76+
overwrite: true
77+
draft: true
78+
tag_name: ${{ env.ASSET_VERSION }}

LICENSE-3rdparty.csv

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@ arrayvec,https://github.com/bluss/arrayvec,MIT OR Apache-2.0,bluss
2121
assert-json-diff,https://github.com/davidpdrsn/assert-json-diff,MIT,David Pedersen <david.pdrsn@gmail.com>
2222
async-compression,https://github.com/Nullus157/async-compression,MIT OR Apache-2.0,"Wim Looman <wim@nemo157.com>, Allen Bui <fairingrey@gmail.com>"
2323
async-speed-limit,https://github.com/tikv/async-speed-limit,MIT OR Apache-2.0,The TiKV Project Developers
24+
async-stream,https://github.com/tokio-rs/async-stream,MIT,Carl Lerche <me@carllerche.com>
25+
async-stream-impl,https://github.com/tokio-rs/async-stream,MIT,Carl Lerche <me@carllerche.com>
2426
async-trait,https://github.com/dtolnay/async-trait,MIT OR Apache-2.0,David Tolnay <dtolnay@gmail.com>
2527
atomic-waker,https://github.com/smol-rs/atomic-waker,Apache-2.0 OR MIT,"Stjepan Glavina <stjepang@gmail.com>, Contributors to futures-rs"
2628
aws-config,https://github.com/smithy-lang/smithy-rs,Apache-2.0,"AWS Rust SDK Team <aws-sdk-rust@amazon.com>, Russell Cohen <rcoh@amazon.com>"
2729
aws-credential-types,https://github.com/smithy-lang/smithy-rs,Apache-2.0,AWS Rust SDK Team <aws-sdk-rust@amazon.com>
2830
aws-lc-rs,https://github.com/aws/aws-lc-rs,ISC AND (Apache-2.0 OR ISC),AWS-LibCrypto
2931
aws-lc-sys,https://github.com/aws/aws-lc-rs,ISC AND (Apache-2.0 OR ISC) AND OpenSSL,AWS-LC
3032
aws-runtime,https://github.com/smithy-lang/smithy-rs,Apache-2.0,AWS Rust SDK Team <aws-sdk-rust@amazon.com>
33+
aws-sdk-lambda,https://github.com/awslabs/aws-sdk-rust,Apache-2.0,"AWS Rust SDK Team <aws-sdk-rust@amazon.com>, Russell Cohen <rcoh@amazon.com>"
3134
aws-sdk-s3,https://github.com/awslabs/aws-sdk-rust,Apache-2.0,"AWS Rust SDK Team <aws-sdk-rust@amazon.com>, Russell Cohen <rcoh@amazon.com>"
3235
aws-sdk-sso,https://github.com/awslabs/aws-sdk-rust,Apache-2.0,"AWS Rust SDK Team <aws-sdk-rust@amazon.com>, Russell Cohen <rcoh@amazon.com>"
3336
aws-sdk-ssooidc,https://github.com/awslabs/aws-sdk-rust,Apache-2.0,"AWS Rust SDK Team <aws-sdk-rust@amazon.com>, Russell Cohen <rcoh@amazon.com>"
@@ -222,6 +225,8 @@ itoa,https://github.com/dtolnay/itoa,MIT OR Apache-2.0,David Tolnay <dtolnay@gma
222225
jobserver,https://github.com/rust-lang/jobserver-rs,MIT OR Apache-2.0,Alex Crichton <alex@alexcrichton.com>
223226
js-sys,https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/js-sys,MIT OR Apache-2.0,The wasm-bindgen Developers
224227
json_comments,https://github.com/tmccombs/json-comments-rs,Apache-2.0,Thayne McCombs <astrothayne@gmail.com>
228+
lambda_runtime,https://github.com/awslabs/aws-lambda-rust-runtime,Apache-2.0,"David Calavera <dcalaver@amazon.com>, Harold Sun <sunhua@amazon.com>"
229+
lambda_runtime_api_client,https://github.com/awslabs/aws-lambda-rust-runtime,Apache-2.0,"David Calavera <dcalaver@amazon.com>, Harold Sun <sunhua@amazon.com>"
225230
lazy_static,https://github.com/rust-lang-nursery/lazy-static.rs,MIT OR Apache-2.0,Marvin Löbel <loebel.marvin@gmail.com>
226231
levenshtein_automata,https://github.com/tantivy-search/levenshtein-automata,MIT,Paul Masurel <paul.masurel@gmail.com>
227232
libc,https://github.com/rust-lang/libc,MIT OR Apache-2.0,The Rust Project Developers
@@ -397,6 +402,7 @@ serde_core,https://github.com/serde-rs/serde,MIT OR Apache-2.0,"Erick Tryzelaar
397402
serde_derive,https://github.com/serde-rs/serde,MIT OR Apache-2.0,"Erick Tryzelaar <erick.tryzelaar@gmail.com>, David Tolnay <dtolnay@gmail.com>"
398403
serde_json,https://github.com/serde-rs/json,MIT OR Apache-2.0,"Erick Tryzelaar <erick.tryzelaar@gmail.com>, David Tolnay <dtolnay@gmail.com>"
399404
serde_json_borrow,https://github.com/PSeitz/serde_json_borrow,MIT,Pascal Seitz <pascal.seitz@gmail.com>
405+
serde_path_to_error,https://github.com/dtolnay/path-to-error,MIT OR Apache-2.0,David Tolnay <dtolnay@gmail.com>
400406
serde_qs,https://github.com/samscott89/serde_qs,MIT OR Apache-2.0,Sam Scott <sam@osohq.com>
401407
serde_spanned,https://github.com/toml-rs/toml,MIT OR Apache-2.0,The serde_spanned Authors
402408
serde_urlencoded,https://github.com/nox/serde_urlencoded,MIT OR Apache-2.0,Anthony Ramine <n.oxyde@gmail.com>
@@ -497,9 +503,11 @@ unicode-width,https://github.com/unicode-rs/unicode-width,MIT OR Apache-2.0,"kwa
497503
unit-prefix,https://codeberg.org/commons-rs/unit-prefix,MIT,"Fabio Valentini <decathorpe@gmail.com>, Benjamin Sago <ogham@bsago.me>"
498504
unsafe-libyaml,https://github.com/dtolnay/unsafe-libyaml,MIT,David Tolnay <dtolnay@gmail.com>
499505
untrusted,https://github.com/briansmith/untrusted,ISC,Brian Smith <brian@briansmith.org>
506+
ureq-proto,https://github.com/algesten/ureq-proto,MIT OR Apache-2.0,Martin Algesten <martin@algesten.se>
500507
url,https://github.com/servo/rust-url,MIT OR Apache-2.0,The rust-url developers
501508
urlencoding,https://github.com/kornelski/rust_urlencoding,MIT,"Kornel <kornel@geekhood.net>, Bertram Truong <b@bertramtruong.com>"
502509
username,https://pijul.org/darcs/user,MIT OR Apache-2.0,Pierre-Étienne Meunier <pierre-etienne.meunier@aalto.fi>
510+
utf-8,https://github.com/SimonSapin/rust-utf8,MIT OR Apache-2.0,Simon Sapin <simon.sapin@exyr.org>
503511
utf8-ranges,https://github.com/BurntSushi/utf8-ranges,Unlicense OR MIT,Andrew Gallant <jamslam@gmail.com>
504512
utf8_iter,https://github.com/hsivonen/utf8_iter,Apache-2.0 OR MIT,Henri Sivonen <hsivonen@hsivonen.fi>
505513
utf8parse,https://github.com/alacritty/vte,Apache-2.0 OR MIT,"Joe Wilm <joe@jwilm.com>, Christian Duerr <contact@christianduerr.com>"
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
---
2+
title: Lambda configuration
3+
sidebar_position: 6
4+
---
5+
6+
Quickwit supports offloading leaf search operations to AWS Lambda for horizontal scaling. When the local search queue becomes saturated, overflow splits are automatically sent to Lambda functions for processing.
7+
8+
:::note
9+
Lambda offloading is currently only supported on AWS.
10+
:::
11+
12+
## How it works
13+
14+
Lambda offloading is **only active when a `lambda` configuration section is present** under `searcher` in your node configuration. When configured:
15+
16+
1. Quickwit monitors the local search queue depth
17+
2. When pending searches exceed the `offload_threshold`, new splits are sent to Lambda instead of being queued locally
18+
3. Lambda returns per-split search results that are cached and merged with local results
19+
20+
This allows Quickwit to handle traffic spikes without provisioning additional searcher nodes.
21+
22+
## Startup validation
23+
24+
When a `lambda` configuration is defined, Quickwit performs a **dry run invocation** at startup to verify that:
25+
- The Lambda function exists
26+
- The function version matches the embedded binary
27+
- The invoker has permission to call the function
28+
29+
If this validation fails, **Quickwit will fail to start**. This ensures that Lambda offloading works correctly before the node begins serving traffic.
30+
31+
## Configuration
32+
33+
Add a `lambda` section under `searcher` in your node configuration:
34+
35+
```yaml
36+
searcher:
37+
lambda:
38+
offload_threshold: 100
39+
auto_deploy:
40+
execution_role_arn: arn:aws:iam::123456789012:role/quickwit-lambda-role
41+
memory_size: 5 GiB
42+
invocation_timeout_secs: 15
43+
```
44+
45+
### Lambda configuration options
46+
47+
| Property | Description | Default value |
48+
| --- | --- | --- |
49+
| `function_name` | Name of the AWS Lambda function to invoke. | `quickwit-lambda-search` |
50+
| `max_splits_per_invocation` | Maximum number of splits to send in a single Lambda invocation. Must be at least 1. | `10` |
51+
| `offload_threshold` | Number of pending local searches before offloading to Lambda. A value of `0` offloads everything to Lambda. | `100` |
52+
| `auto_deploy` | Auto-deployment configuration. If set, Quickwit automatically deploys or updates the Lambda function at startup. | (none) |
53+
54+
### Auto-deploy configuration options
55+
56+
| Property | Description | Default value |
57+
| --- | --- | --- |
58+
| `execution_role_arn` | **Required.** IAM role ARN for the Lambda function's execution role. | |
59+
| `memory_size` | Memory allocated to the Lambda function. More memory provides more CPU. | `5 GiB` |
60+
| `invocation_timeout_secs` | Timeout for Lambda invocations in seconds. | `15` |
61+
62+
## Deployment options
63+
64+
### Automatic deployment (recommended)
65+
66+
With `auto_deploy` configured, Quickwit automatically:
67+
1. Creates the Lambda function if it doesn't exist
68+
2. Updates the function code if the embedded binary has changed
69+
3. Publishes a new version with a unique identifier
70+
4. Garbage collects old versions (keeps current + 5 most recent)
71+
72+
This is the recommended approach as it ensures the Lambda function always matches the Quickwit binary version.
73+
74+
### Manual deployment
75+
76+
You can deploy the Lambda function manually without `auto_deploy`:
77+
1. Download the Lambda zip from [GitHub releases](https://github.com/quickwit-oss/quickwit/releases)
78+
2. Create or update the Lambda function using AWS CLI, Terraform, or the AWS Console
79+
3. Publish a version with description format `quickwit_{version}_{sha256}_{timeout}_{deploy_config}"` (e.g., `quickwit_0_8_0_fa940f44_5120_60s_6c3b2`)
80+
81+
The description must match the format Quickwit expects, or it won't find the function version.
82+
83+
## IAM permissions
84+
85+
### Permissions for the Quickwit node
86+
87+
The IAM role or user running Quickwit needs the following permissions to invoke Lambda:
88+
89+
```json
90+
{
91+
"Version": "2012-10-17",
92+
"Statement": [
93+
{
94+
"Effect": "Allow",
95+
"Action": [
96+
"lambda:InvokeFunction"
97+
],
98+
"Resource": "arn:aws:lambda:*:*:function:quickwit-lambda-search:*"
99+
}
100+
]
101+
}
102+
```
103+
104+
If using `auto_deploy`, additional permissions are required for deployment:
105+
106+
```json
107+
{
108+
"Version": "2012-10-17",
109+
"Statement": [
110+
{
111+
"Effect": "Allow",
112+
"Action": [
113+
"lambda:CreateFunction",
114+
"lambda:GetFunction",
115+
"lambda:UpdateFunctionCode",
116+
"lambda:PublishVersion",
117+
"lambda:ListVersionsByFunction",
118+
"lambda:DeleteFunction"
119+
],
120+
"Resource": "arn:aws:lambda:*:*:function:quickwit-lambda-search"
121+
},
122+
{
123+
"Effect": "Allow",
124+
"Action": "iam:PassRole",
125+
"Resource": "arn:aws:iam::*:role/quickwit-lambda-role",
126+
"Condition": {
127+
"StringEquals": {
128+
"iam:PassedToService": "lambda.amazonaws.com"
129+
}
130+
}
131+
}
132+
]
133+
}
134+
```
135+
136+
### Lambda execution role
137+
138+
The Lambda function requires an execution role with S3 read access to your index data.
139+
140+
Example policy:
141+
142+
```json
143+
{
144+
"Version": "2012-10-17",
145+
"Statement": [
146+
{
147+
"Effect": "Allow",
148+
"Action": "s3:GetObject",
149+
"Resource": "arn:aws:s3:::your-index-bucket/*"
150+
}
151+
]
152+
}
153+
```
154+
155+
The execution role must also have a trust policy allowing Lambda to assume it:
156+
157+
```json
158+
{
159+
"Version": "2012-10-17",
160+
"Statement": [
161+
{
162+
"Effect": "Allow",
163+
"Principal": {
164+
"Service": "lambda.amazonaws.com"
165+
},
166+
"Action": "sts:AssumeRole"
167+
}
168+
]
169+
}
170+
```
171+
172+
## CloudWatch logging
173+
174+
The Lambda function emits structured logs (JSON) to stdout. To have these logs captured by CloudWatch, add the following iam permissions to the Lambda execution role:
175+
176+
```json
177+
{
178+
"Version": "2012-10-17",
179+
"Statement": [
180+
{
181+
"Effect": "Allow",
182+
"Action": [
183+
"logs:CreateLogGroup",
184+
"logs:CreateLogStream",
185+
"logs:PutLogEvents"
186+
],
187+
"Resource": "arn:aws:logs:*:*:*"
188+
}
189+
]
190+
}
191+
```
192+
193+
No additional configuration is needed on the Quickwit side.
194+
195+
## Versioning
196+
197+
Quickwit uses content-based versioning for Lambda:
198+
- A SHA256 hash of the Lambda binary is computed at build time
199+
- This hash is embedded in the Lambda function description as `quickwit:{version}-{sha256_short}`
200+
- When Quickwit starts, it searches for a version matching this description
201+
- Different Quickwit builds with the same Lambda binary share the same Lambda version
202+
- Updating the Lambda binary automatically triggers a new deployment
203+
204+
## Example configuration
205+
206+
207+
Minimal configuration (with auto-deployment):
208+
209+
```yaml
210+
searcher:
211+
lambda:
212+
auto_deploy:
213+
execution_role_arn: arn:aws:iam::123456789012:role/quickwit-lambda-role
214+
```
215+
216+
217+
Full configuration (auto-deployment):
218+
219+
```yaml
220+
searcher:
221+
lambda:
222+
function_name: quickwit-lambda-search
223+
max_splits_per_invocation: 10
224+
offload_threshold: 10
225+
auto_deploy:
226+
execution_role_arn: arn:aws:iam::123456789012:role/quickwit-lambda-role
227+
memory_size: 5 GiB
228+
invocation_timeout_secs: 15
229+
```
230+
231+
Aggressive offloading (send everything to Lambda):
232+
233+
```yaml
234+
searcher:
235+
lambda:
236+
function_name: quickwit-lambda-search
237+
offload_threshold: 0
238+
auto_deploy:
239+
execution_role_arn: arn:aws:iam::123456789012:role/quickwit-lambda-role
240+
```

0 commit comments

Comments
 (0)