diff --git a/common/proto/search.proto b/common/proto/search.proto index 5069110..4d288db 100644 --- a/common/proto/search.proto +++ b/common/proto/search.proto @@ -4,6 +4,7 @@ package komp_ac.search; service Searcher { rpc SearchTable(SearchRequest) returns (SearchResponse); + rpc ExactSearchTable(SearchRequest) returns (SearchResponse); } message SearchRequest { diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index 4a8dc34..3233067 100644 Binary files a/common/src/proto/descriptor.bin and b/common/src/proto/descriptor.bin differ diff --git a/common/src/proto/komp_ac.search.rs b/common/src/proto/komp_ac.search.rs index b0faacb..849d2ba 100644 --- a/common/src/proto/komp_ac.search.rs +++ b/common/src/proto/komp_ac.search.rs @@ -140,6 +140,27 @@ pub mod searcher_client { .insert(GrpcMethod::new("komp_ac.search.Searcher", "SearchTable")); self.inner.unary(req, path, codec).await } + pub async fn exact_search_table( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/komp_ac.search.Searcher/ExactSearchTable", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("komp_ac.search.Searcher", "ExactSearchTable")); + self.inner.unary(req, path, codec).await + } } } /// Generated server implementations. @@ -159,6 +180,10 @@ pub mod searcher_server { &self, request: tonic::Request, ) -> std::result::Result, tonic::Status>; + async fn exact_search_table( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; } #[derive(Debug)] pub struct SearcherServer { @@ -279,6 +304,49 @@ pub mod searcher_server { }; Box::pin(fut) } + "/komp_ac.search.Searcher/ExactSearchTable" => { + #[allow(non_camel_case_types)] + struct ExactSearchTableSvc(pub Arc); + impl tonic::server::UnaryService + for ExactSearchTableSvc { + type Response = super::SearchResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::exact_search_table(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = ExactSearchTableSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } _ => { Box::pin(async move { let mut response = http::Response::new( diff --git a/search/src/lib.rs b/search/src/lib.rs index 365ebbf..228cdc6 100644 --- a/search/src/lib.rs +++ b/search/src/lib.rs @@ -20,6 +20,12 @@ const INDEX_ROOT: &str = "./tantivy_indexes"; const DEFAULT_RESULT_LIMIT: usize = 5; const SEARCH_RESULT_LIMIT: usize = 100; +#[derive(Clone, Copy)] +enum SearchMode { + Fuzzy, + Exact, +} + pub struct SearcherService { pub pool: PgPool, } @@ -165,7 +171,11 @@ async fn resolve_search_targets( } // Query building -fn build_query(index: &Index, normalized_query: &str) -> Result, Status> { +fn build_query( + index: &Index, + normalized_query: &str, + mode: SearchMode, +) -> Result, Status> { let schema = index.schema(); let prefix_edge_field = schema .get_field("prefix_edge") @@ -182,6 +192,24 @@ fn build_query(index: &Index, normalized_query: &str) -> Result)> = Vec::new(); // Layer 1: prefix @@ -276,6 +304,7 @@ async fn search_target( pool: &PgPool, target: &SearchTarget, query_str: &str, + mode: SearchMode, ) -> Result, Status> { if !target.index_path.exists() { return Ok(vec![]); @@ -286,7 +315,7 @@ async fn search_target( register_slovak_tokenizers(&index) .map_err(|e| Status::internal(format!("Failed to register Slovak tokenizers: {}", e)))?; - let Some(master_query) = build_query(&index, &normalize_slovak_text(query_str))? else { + let Some(master_query) = build_query(&index, &normalize_slovak_text(query_str), mode)? else { return Ok(vec![]); }; @@ -360,6 +389,23 @@ impl Searcher for SearcherService { async fn search_table( &self, request: Request, + ) -> Result, Status> { + self.run_search(request, SearchMode::Fuzzy).await + } + + async fn exact_search_table( + &self, + request: Request, + ) -> Result, Status> { + self.run_search(request, SearchMode::Exact).await + } +} + +impl SearcherService { + async fn run_search( + &self, + request: Request, + mode: SearchMode, ) -> Result, Status> { let req = request.into_inner(); let profile_name = req.profile_name.trim(); @@ -404,7 +450,7 @@ impl Searcher for SearcherService { // Merge per-table hits let mut hits = Vec::new(); for target in &targets { - hits.extend(search_target(&self.pool, target, query).await?); + hits.extend(search_target(&self.pool, target, query, mode).await?); } hits.sort_by(|left, right| right.score.total_cmp(&left.score)); @@ -413,7 +459,11 @@ impl Searcher for SearcherService { } info!( - "Processed search for profile '{}' (table scope: {}). Returning {} hits.", + "Processed {} search for profile '{}' (table scope: {}). Returning {} hits.", + match mode { + SearchMode::Fuzzy => "fuzzy", + SearchMode::Exact => "exact", + }, profile_name, requested_table.unwrap_or("*"), hits.len() diff --git a/server b/server index df65bbf..b26adc0 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit df65bbf8f3317128aa4cf162628b52fe257fe84d +Subproject commit b26adc0cb0afeb4379da320ccb3fd6d4d3a241a4