» Rust: Building Full-Text Search API with ElasticSearch » 3. Search Documents » 3.1 Send Search Requests

Send Search Requests

Add config item in src/infrastructure/config/mod.rs:

@@ -16,6 +16,7 @@ pub struct SearchConfig {
 #[derive(Debug, Deserialize, Serialize)]
 pub struct ApplicationConfig {
     pub port: i32,
+    pub page_size: u32,
 }
 
 pub fn parse_config(file_name: &str) -> Config {

Update src/domain/gateway/book_manager.rs:

@@ -7,4 +7,5 @@ use crate::domain::model;
 #[async_trait]
 pub trait BookManager: Send + Sync {
     async fn index_book(&self, b: &model::Book) -> Result<String, Box<dyn Error>>;
+    async fn search_books(&self, q: &str) -> Result<Vec<model::Book>, Box<dyn Error>>;
 }

Update src/infrastructure/search/es.rs:

@@ -2,8 +2,8 @@ use std::error::Error;
 
 use async_trait::async_trait;
 use elasticsearch::http::transport::Transport;
-use elasticsearch::{Elasticsearch, IndexParts};
-use serde_json::Value;
+use elasticsearch::{Elasticsearch, IndexParts, SearchParts};
+use serde_json::{json, Value};
 
 use crate::domain::gateway::BookManager;
 use crate::domain::model;
@@ -12,13 +12,14 @@ const INDEX_BOOK: &str = "book_idx";
 
 pub struct ElasticSearchEngine {
     client: Elasticsearch,
+    page_size: u32,
 }
 
 impl ElasticSearchEngine {
-    pub fn new(address: &str) -> Result<Self, Box<dyn Error>> {
+    pub fn new(address: &str, page_size: u32) -> Result<Self, Box<dyn Error>> {
         let transport = Transport::single_node(address)?;
         let client = Elasticsearch::new(transport);
-        Ok(ElasticSearchEngine { client })
+        Ok(ElasticSearchEngine { client, page_size })
     }
 }
 
@@ -34,4 +35,30 @@ impl BookManager for ElasticSearchEngine {
         let response_body = response.json::<Value>().await?;
         Ok(response_body["_id"].as_str().unwrap().into())
     }
+
+    async fn search_books(&self, q: &str) -> Result<Vec<model::Book>, Box<dyn Error>> {
+        let response = self
+            .client
+            .search(SearchParts::Index(&[INDEX_BOOK]))
+            .from(0)
+            .size(self.page_size as i64)
+            .body(json!({
+                "query": {
+                    "multi_match": {
+                        "query": q,
+                        "fields": vec!["title", "author", "content"],
+                    }
+                }
+            }))
+            .send()
+            .await?;
+        let response_body = response.json::<Value>().await?;
+        let mut books: Vec<model::Book> = vec![];
+        for hit in response_body["hits"]["hits"].as_array().unwrap() {
+            let source = hit["_source"].clone();
+            let book: model::Book = serde_json::from_value(source).unwrap();
+            books.push(book);
+        }
+        Ok(books)
+    }
 }

Here we use multi_match to query on mutilple fields “title“, “author“ and “content“.

Update src/application/executor/book_operator.rs:

@@ -16,4 +16,8 @@ impl BookOperator {
     pub async fn create_book(&self, b: model::Book) -> Result<String, Box<dyn Error>> {
         Ok(self.book_manager.index_book(&b).await?)
     }
+
+    pub async fn search_books(&self, q: &str) -> Result<Vec<model::Book>, Box<dyn Error>> {
+        Ok(self.book_manager.search_books(q).await?)
+    }
 }

Update src/adapter/router.rs:

@@ -1,10 +1,11 @@
 use axum::{
-    extract::{Json, State},
+    extract::{Json, Query, State},
     http::StatusCode,
     response::IntoResponse,
     routing::{get, post},
     Router,
 };
+use serde::Deserialize;
 use std::sync::Arc;
 
 use serde_json::json;
@@ -13,6 +14,11 @@ use crate::application;
 use crate::application::executor;
 use crate::domain::model;
 
+#[derive(Deserialize)]
+struct QueryParams {
+    q: String,
+}
+
 pub struct RestHandler {
     book_operator: executor::BookOperator,
 }
@@ -27,6 +33,16 @@ async fn create_book(
     }
 }
 
+async fn search_books(
+    State(rest_handler): State<Arc<RestHandler>>,
+    Query(params): Query<QueryParams>,
+) -> Result<Json<Vec<model::Book>>, impl IntoResponse> {
+    match rest_handler.book_operator.search_books(&params.q).await {
+        Ok(books) => Ok(Json(books)),
+        Err(err) => Err((StatusCode::INTERNAL_SERVER_ERROR, err.to_string())),
+    }
+}
+
 async fn welcome() -> Json<serde_json::Value> {
     Json(json!({
         "status": "ok"
@@ -40,5 +56,6 @@ pub fn make_router(wire_helper: &application::WireHelper) -> Router {
     Router::new()
         .route("/", get(welcome))
         .route("/books", post(create_book))
+        .route("/books", get(search_books))
         .with_state(rest_handler)
 }

Run the server again and try it with curl:

curl 'http://localhost:3000/books?q=katniss+hunger'

Example response:

[
  {
    "title": "The Hunger Games",
    "author": "Suzanne Collins",
    "published_at": "2008-09-14",
    "content": "In a dystopian future, teenagers are forced to participate in a televised death match called the Hunger Games. Katniss Everdeen volunteers to take her sister‘s place and becomes a symbol of rebellion."
  }
]

Try it in tools like Postman or Insomnia:

http://localhost:3000/books?q=new%20york%20circus%20girl

In URL encoding, both %20 and + are used for representing spaces, but they serve slightly different purposes depending on the context.

Insomnia Screenshot

Great! You just completed a full-text search.

Note:
Full text search requires language analyzers. The default analyzer is the standard analyzer, which may not be the best especially for Chinese, Japanese, or Korean text. With language specific analyzers, you can get better search experience.