» Rust:使用ElasticSearch构建全文检索API » 3. 搜索文档 » 3.1 发送 Search 请求

发送 Search 请求

添加配置项,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 {

更新 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>>;
 }

更新 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)
+    }
 }

此处我们使用 multi_match 来进行多字段查询:“title”,“author”和“content”。

更新 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?)
+    }
 }

更新 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)
 }

重启后再次使用 curl 测试:

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

样例响应:

[
  {
    "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."
  }
]

PostmanInsomnia 之类工具中尝试:

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

在 URL 编码中,%20+ 都可以表示空格,但是它们的使用场景稍有区别。

Insomnia Screenshot

赞!你刚刚完成了一个全文检索。

注意:
全文检索需要语言分词器。默认的标准分词器对中、日、韩文等语言处理效果不理想。 可以使用语言特定的分词器以获取更佳效果。