Skip to content

Commit fd9564f

Browse files
committed
Moving lambda execution to the leaf.
1 parent 93cd5b9 commit fd9564f

17 files changed

Lines changed: 351 additions & 234 deletions

File tree

quickwit/quickwit-config/src/node_config/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,11 @@ pub struct LambdaConfig {
320320
/// Maximum number of splits per Lambda invocation.
321321
#[serde(default = "LambdaConfig::default_max_splits_per_invocation")]
322322
pub max_splits_per_invocation: usize,
323+
/// When the number of pending split searches exceeds this threshold,
324+
/// new splits are offloaded to Lambda instead of being queued locally.
325+
/// A value of 0 disables offloading (all splits are processed locally).
326+
#[serde(default = "LambdaConfig::default_offload_threshold")]
327+
pub offload_threshold: usize,
323328
/// Auto-deploy configuration. If set, Quickwit will automatically deploy
324329
/// the Lambda function at startup.
325330
/// If deploying a lambda fails, Quickwit will log an error and fail.
@@ -356,6 +361,7 @@ impl Default for LambdaConfig {
356361
Self {
357362
function_name: Self::default_function_name(),
358363
max_splits_per_invocation: Self::default_max_splits_per_invocation(),
364+
offload_threshold: Self::default_offload_threshold(),
359365
auto_deploy: None,
360366
}
361367
}
@@ -368,6 +374,9 @@ impl LambdaConfig {
368374
fn default_max_splits_per_invocation() -> usize {
369375
10
370376
}
377+
fn default_offload_threshold() -> usize {
378+
20
379+
}
371380
}
372381

373382
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]

quickwit/quickwit-lambda-client/src/invoker.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use aws_sdk_lambda::types::InvocationType;
2121
use base64::prelude::*;
2222
use prost::Message;
2323
use quickwit_lambda_server::{LeafSearchPayload, LeafSearchResponsePayload};
24-
use quickwit_proto::search::{LeafSearchRequest, LeafSearchResponse};
24+
use quickwit_proto::search::{LeafSearchRequest, LeafSearchResponse, LeafSearchResponses};
2525
use quickwit_search::{LambdaLeafSearchInvoker, SearchError};
2626
use tracing::{debug, info, instrument};
2727

@@ -91,7 +91,7 @@ impl LambdaLeafSearchInvoker for AwsLambdaInvoker {
9191
async fn invoke_leaf_search(
9292
&self,
9393
request: LeafSearchRequest,
94-
) -> Result<LeafSearchResponse, SearchError> {
94+
) -> Result<Vec<LeafSearchResponse>, SearchError> {
9595
let start = std::time::Instant::now();
9696

9797
let result = self.invoke_leaf_search_inner(request).await;
@@ -115,7 +115,7 @@ impl AwsLambdaInvoker {
115115
async fn invoke_leaf_search_inner(
116116
&self,
117117
request: LeafSearchRequest,
118-
) -> Result<LeafSearchResponse, SearchError> {
118+
) -> Result<Vec<LeafSearchResponse>, SearchError> {
119119
// Serialize request to protobuf bytes, then base64 encode
120120
let request_bytes = request.encode_to_vec();
121121
let payload = LeafSearchPayload {
@@ -178,14 +178,14 @@ impl AwsLambdaInvoker {
178178
.decode(&lambda_response.payload)
179179
.map_err(|e| SearchError::Internal(format!("Base64 decode error: {}", e)))?;
180180

181-
let leaf_response = LeafSearchResponse::decode(&response_bytes[..])
181+
let leaf_responses = LeafSearchResponses::decode(&response_bytes[..])
182182
.map_err(|e| SearchError::Internal(format!("Protobuf decode error: {}", e)))?;
183183

184184
debug!(
185-
num_hits = leaf_response.num_hits,
185+
num_responses = leaf_responses.responses.len(),
186186
"Lambda invocation completed"
187187
);
188188

189-
Ok(leaf_response)
189+
Ok(leaf_responses.responses)
190190
}
191191
}

quickwit/quickwit-lambda-server/src/context.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ impl LambdaSearcherContext {
3434

3535
let config = LambdaSearcherConfig::try_from_env()?;
3636
let searcher_config = create_searcher_config(&config);
37-
let searcher_context = Arc::new(SearcherContext::new(searcher_config, None));
37+
let searcher_context = Arc::new(SearcherContext::new(searcher_config, None, None));
3838
let storage_resolver = StorageResolver::configured(&Default::default());
3939

4040
Ok(Self {

quickwit/quickwit-lambda-server/src/handler.rs

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@
1414

1515
use base64::prelude::*;
1616
use prost::Message;
17-
use quickwit_proto::search::{LeafSearchRequest, LeafSearchResponse};
17+
use quickwit_proto::search::{
18+
LeafSearchRequest, LeafSearchResponse, LeafSearchResponses, SplitIdAndFooterOffsets,
19+
};
1820
use quickwit_search::leaf::multi_index_leaf_search;
1921
use serde::{Deserialize, Serialize};
22+
use tokio::task::JoinSet;
2023
use tracing::{info, instrument};
2124

2225
use crate::context::LambdaSearcherContext;
@@ -32,11 +35,14 @@ pub struct LeafSearchPayload {
3235
/// Response from leaf search Lambda invocation.
3336
#[derive(Debug, Serialize, Deserialize)]
3437
pub struct LeafSearchResponsePayload {
35-
/// Base64-encoded serialized LeafSearchResponse protobuf.
38+
/// Base64-encoded serialized `LeafSearchResponses` protobuf (one per split).
3639
pub payload: String,
3740
}
3841

3942
/// Handle a leaf search request in Lambda.
43+
///
44+
/// Returns one `LeafSearchResponse` per split. Each split is processed
45+
/// independently so that the caller can cache and merge results individually.
4046
#[instrument(skip(ctx), fields(request_id))]
4147
pub async fn handle_leaf_search(
4248
event: LeafSearchPayload,
@@ -50,30 +56,73 @@ pub async fn handle_leaf_search(
5056
// Deserialize LeafSearchRequest
5157
let leaf_search_request = LeafSearchRequest::decode(&request_bytes[..])?;
5258

53-
let num_splits: usize = leaf_search_request
59+
let all_splits: Vec<(usize, SplitIdAndFooterOffsets)> = leaf_search_request
5460
.leaf_requests
5561
.iter()
56-
.map(|leaf_request_ref| leaf_request_ref.split_offsets.len())
57-
.sum();
62+
.enumerate()
63+
.flat_map(|(leaf_req_idx, leaf_request_ref)| {
64+
leaf_request_ref
65+
.split_offsets
66+
.iter()
67+
.cloned()
68+
.map(move |split_id_and_footer_offsets| (leaf_req_idx, split_id_and_footer_offsets))
69+
})
70+
.collect();
5871

59-
info!(num_splits, "processing leaf search request");
72+
let num_splits = all_splits.len();
73+
info!(num_splits, "processing leaf search request (per-split)");
6074

61-
// Execute leaf search
62-
let leaf_search_response = multi_index_leaf_search(
63-
ctx.searcher_context.clone(),
64-
leaf_search_request,
65-
&ctx.storage_resolver,
66-
)
67-
.await?;
75+
// Process each split in parallel. The SearchPermitProvider inside
76+
// SearcherContext gates concurrency based on memory budget.
77+
let mut join_set = JoinSet::new();
78+
for (split_idx, (leaf_req_idx, split)) in all_splits.into_iter().enumerate() {
79+
let leaf_request_ref = &leaf_search_request.leaf_requests[leaf_req_idx];
80+
let single_split_request = LeafSearchRequest {
81+
search_request: leaf_search_request.search_request.clone(),
82+
doc_mappers: leaf_search_request.doc_mappers.clone(),
83+
index_uris: leaf_search_request.index_uris.clone(),
84+
leaf_requests: vec![quickwit_proto::search::LeafRequestRef {
85+
index_uri_ord: leaf_request_ref.index_uri_ord,
86+
doc_mapper_ord: leaf_request_ref.doc_mapper_ord,
87+
split_offsets: vec![split],
88+
}],
89+
};
90+
91+
let searcher_context = ctx.searcher_context.clone();
92+
let storage_resolver = ctx.storage_resolver.clone();
93+
join_set.spawn(async move {
94+
let response = multi_index_leaf_search(
95+
searcher_context,
96+
single_split_request,
97+
&storage_resolver,
98+
)
99+
.await;
100+
(split_idx, response)
101+
});
102+
}
103+
104+
// Collect results, preserving split order.
105+
let mut indexed_responses: Vec<(usize, LeafSearchResponse)> =
106+
Vec::with_capacity(num_splits);
107+
while let Some(join_result) = join_set.join_next().await {
108+
let (split_idx, search_result) = join_result
109+
.map_err(|e| LambdaError::Internal(format!("split search task failed: {e}")))?;
110+
let response = search_result
111+
.map_err(|e| LambdaError::Internal(format!("leaf search failed: {e}")))?;
112+
indexed_responses.push((split_idx, response));
113+
}
114+
indexed_responses.sort_by_key(|(idx, _)| *idx);
115+
let responses: Vec<LeafSearchResponse> =
116+
indexed_responses.into_iter().map(|(_, r)| r).collect();
68117

69118
info!(
70-
num_hits = leaf_search_response.num_hits,
71-
num_successful_splits = leaf_search_response.num_successful_splits,
72-
"leaf search completed"
119+
num_responses = responses.len(),
120+
"leaf search completed (per-split)"
73121
);
74122

75-
// Serialize response
76-
let response_bytes = leaf_search_response.encode_to_vec();
123+
// Serialize as LeafSearchResponses wrapper
124+
let wrapper = LeafSearchResponses { responses };
125+
let response_bytes = wrapper.encode_to_vec();
77126
let payload = BASE64_STANDARD.encode(&response_bytes);
78127

79128
Ok(LeafSearchResponsePayload { payload })

quickwit/quickwit-proto/protos/quickwit/search.proto

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -182,14 +182,6 @@ message ListFields {
182182
}
183183
// -- Search -------------------
184184

185-
// Execution mode for leaf search operations.
186-
enum ExecutionMode {
187-
// Default: use gRPC to searcher nodes (existing behavior).
188-
EXECUTION_MODE_GRPC = 0;
189-
// Execute leaf search via remote serverless functions (e.g., AWS Lambda).
190-
EXECUTION_MODE_REMOTE_FUNCTION = 1;
191-
}
192-
193185
message SearchRequest {
194186
// Index ID patterns
195187
repeated string index_id_patterns = 1;
@@ -255,11 +247,6 @@ message SearchRequest {
255247
// When an exact index ID is provided (not a pattern), the query fails only if
256248
// that index is not found and this parameter is set to `false`.
257249
bool ignore_missing_indexes = 18;
258-
259-
// Execution mode for leaf search operations.
260-
// When set to EXECUTION_MODE_REMOTE_FUNCTION, leaf search is executed via
261-
// serverless functions (e.g., AWS Lambda) instead of gRPC to searcher nodes.
262-
ExecutionMode execution_mode = 19;
263250
}
264251

265252
enum CountHits {
@@ -506,6 +493,12 @@ message LeafSearchResponse {
506493
ResourceStats resource_stats = 8;
507494
}
508495

496+
// Wrapper for multiple LeafSearchResponse messages, used by Lambda to return
497+
// per-split results.
498+
message LeafSearchResponses {
499+
repeated LeafSearchResponse responses = 1;
500+
}
501+
509502
message SnippetRequest {
510503
repeated string snippet_fields = 1;
511504
string query_ast_resolved = 2;

quickwit/quickwit-proto/src/codegen/quickwit/quickwit.search.rs

Lines changed: 8 additions & 36 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

quickwit/quickwit-search/src/invoker.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ use crate::SearchError;
2626
#[async_trait]
2727
pub trait LambdaLeafSearchInvoker: Send + Sync + 'static {
2828
/// Invoke the remote function with a LeafSearchRequest.
29+
///
30+
/// Returns one `LeafSearchResponse` per split in the request.
2931
async fn invoke_leaf_search(
3032
&self,
3133
request: LeafSearchRequest,
32-
) -> Result<LeafSearchResponse, SearchError>;
34+
) -> Result<Vec<LeafSearchResponse>, SearchError>;
3335
}

0 commit comments

Comments
 (0)