» Rust:使用ElasticSearch构建全文检索API » 2. 索引文档 » 2.3 发送 Index 请求

发送 Index 请求

让我们添加索引图书的 API,然后放入一些测试数据供后续使用。

添加 async_trait 依赖用于在 trait 中使用 async fn

cargo add async_trait

创建 src/domain/gateway/book_manager.rs:

use std::error::Error;

use async_trait::async_trait;

use crate::domain::model;

#[async_trait]
pub trait BookManager: Send + Sync {
    async fn index_book(&self, b: &model::Book) -> Result<String, Box<dyn Error>>;
}

此处也是在使用4层架构。项目越大,该架构的好处越明显。阅读更多

某个类型可以安全地传送到其它线程,那么它就是 Send;某个类型可以在线程间安全共享,那么它就是 Sync

添加 src/domain/gateway/mod.rs:

mod book_manager;

pub use book_manager::BookManager;

记得为其它子目录创建 mod.rs 文件,以导出符号。

创建 src/infrastructure/search/es.rs:

use std::error::Error;

use async_trait::async_trait;
use elasticsearch::http::transport::Transport;
use elasticsearch::{Elasticsearch, IndexParts};
use serde_json::Value;

use crate::domain::gateway::BookManager;
use crate::domain::model;

const INDEX_BOOK: &str = "book_idx";

pub struct ElasticSearchEngine {
    client: Elasticsearch,
}

impl ElasticSearchEngine {
    pub fn new(address: &str) -> Result<Self, Box<dyn Error>> {
        let transport = Transport::single_node(address)?;
        let client = Elasticsearch::new(transport);
        Ok(ElasticSearchEngine { client })
    }
}

#[async_trait]
impl BookManager for ElasticSearchEngine {
    async fn index_book(&self, b: &model::Book) -> Result<String, Box<dyn Error>> {
        let response = self
            .client
            .index(IndexParts::Index(INDEX_BOOK))
            .body(b)
            .send()
            .await?;
        let response_body = response.json::<Value>().await?;
        Ok(response_body["_id"].as_str().unwrap().into())
    }
}

默认情况下,Elasticsearch 允许你将文档索引到尚不存在的索引中。 当你将文档索引到不存在的索引时,Elasticsearch 将会使用默认设置动态地创建索引。这在开发者不想显式地创建索引时会很方便。

如果你遇到了 unexpected transfer-encoding parsed 错误:

  • 尝试禁用代理,包括系统代理。系统代理会被默认启用。
  • 或者,使用 TransportBuilderdisable_proxy() 方法。
let url = Url::parse(address)?;
let conn_pool = SingleNodeConnectionPool::new(url);
let transport = TransportBuilder::new(conn_pool).disable_proxy().build()?;

添加 toml 依赖:

cargo add toml

提示: TOML 全称是 Tom's Obvious Minimal Language。

创建 src/infrastructure/config/mod.rs:

use std::fs;

use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Serialize)]
pub struct Config {
    pub app: ApplicationConfig,
    pub search: SearchConfig,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct SearchConfig {
    pub address: String,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct ApplicationConfig {
    pub port: i32,
}

pub fn parse_config(file_name: &str) -> Config {
    let content = fs::read_to_string(file_name).expect("Failed to open TOML config file");
    toml::from_str(&content).expect("Failed to parse TOML config file")
}

添加 src/infrastructure/mod.rs:

mod config;
pub use config::{parse_config, Config};

pub mod search;

创建 config.toml:

[app]
port = 3000
page_size = 10

[search]
address = "http://localhost:9200"

警醒:
不要直接 git 提交 config.toml。可能会导致敏感数据泄露。如果非要提交的话,建议只提交配置格式模板。
比如:

[app]
port = 3000
page_size = 10

[search]
address = ""

创建 src/application/executor/book_operator.rs:

use std::error::Error;
use std::sync::Arc;

use crate::domain::gateway;
use crate::domain::model;

pub struct BookOperator {
    book_manager: Arc<dyn gateway::BookManager>,
}

impl BookOperator {
    pub fn new(b: Arc<dyn gateway::BookManager>) -> Self {
        BookOperator { book_manager: b }
    }

    pub async fn create_book(&self, b: model::Book) -> Result<String, Box<dyn Error>> {
        Ok(self.book_manager.index_book(&b).await?)
    }
}

创建 src/application/wire_helper.rs:

use std::sync::Arc;

use crate::domain::gateway;
use crate::infrastructure::search;
use crate::infrastructure::Config;

pub struct WireHelper {
    engine: Arc<search::ElasticSearchEngine>,
}

impl WireHelper {
    pub fn new(c: &Config) -> Result<Self, Box<dyn std::error::Error>> {
        let engine = Arc::new(search::ElasticSearchEngine::new(&c.search.address)?);
        Ok(WireHelper { engine })
    }

    pub fn book_manager(&self) -> Arc<dyn gateway::BookManager> {
        Arc::clone(&self.engine) as Arc<dyn gateway::BookManager>
    }
}

创建 src/adapter/router.rs:

use axum::{
    extract::{Json, State},
    http::StatusCode,
    response::IntoResponse,
    routing::{get, post},
    Router,
};
use std::sync::Arc;

use serde_json::json;

use crate::application;
use crate::application::executor;
use crate::domain::model;

pub struct RestHandler {
    book_operator: executor::BookOperator,
}

async fn create_book(
    State(rest_handler): State<Arc<RestHandler>>,
    Json(book): Json<model::Book>,
) -> Result<Json<serde_json::Value>, impl IntoResponse> {
    match rest_handler.book_operator.create_book(book).await {
        Ok(book_id) => Ok(Json(json!({"id": book_id}))),
        Err(err) => Err((StatusCode::INTERNAL_SERVER_ERROR, err.to_string())),
    }
}

async fn welcome() -> Json<serde_json::Value> {
    Json(json!({
        "status": "ok"
    }))
}

pub fn make_router(wire_helper: &application::WireHelper) -> Router {
    let rest_handler = Arc::new(RestHandler {
        book_operator: executor::BookOperator::new(wire_helper.book_manager()),
    });
    Router::new()
        .route("/", get(welcome))
        .route("/books", post(create_book))
        .with_state(rest_handler)
}

在 handlers 之间共享一些 state(状态)是很常见的操作。例如,可能需要共享一个数据库连接池或某服务的客户端。

最常见的三种方法是:

在这里,我们使用 State 来在 handlers 之间共享 rest_handler

用以下代码替换 src/main.rs 中内容:

mod adapter;
mod application;
mod domain;
mod infrastructure;

const CONFIG_FILE: &str = "config.toml";

#[tokio::main]
async fn main() {
    let c = infrastructure::parse_config(CONFIG_FILE);
    let wire_helper = application::WireHelper::new(&c).expect("Failed to create WireHelper");
    let app = adapter::make_router(&wire_helper);
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

记得添加以下 mod.rs 文件:

  • src/adapter/mod.rs
  • src/application/executor/mod.rs
  • src/application/mod.rs
  • src/domain/gateway/mod.rs
  • src/domain/mod.rs
  • src/infrastructure/mod.rs
  • src/infrastructure/search/mod.rs

Cargo.toml 中变化:

@@ -6,7 +6,10 @@ edition = "2021"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
+async-trait = "0.1.80"
 axum = "0.7.5"
+elasticsearch = "8.5.0-alpha.1"
 serde = { version = "1.0.198", features = ["derive"] }
 serde_json = "1.0.116"
 tokio = { version = "1.37.0", features = ["full"] }
+toml = "0.8.12"

再次运行 server,然后使用 curl 进行测试:

cargo run

样例请求:

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"title":"The Da Vinci Code","author":"Dan Brown","published_at":"2003-03-18","content":"In the Louvre, a curator is found dead. Next to his body, an enigmatic message. It is the beginning of a race to discover the truth about the Holy Grail."}' \
  http://localhost:3000/books

样例响应:

{"id":"gnRLFI8Be1nlzJXaEsQR"}

放入测试数据

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"title":"Harry Potter and the Philosopher\u0027s Stone","author":"J.K. Rowling","published_at":"1997-06-26","content":"A young boy discovers he is a wizard and begins his education at Hogwarts School of Witchcraft and Wizardry, where he uncovers the mystery of the Philosopher‘s Stone."}' \
  http://localhost:3000/books

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"title":"To Kill a Mockingbird","author":"Harper Lee","published_at":"1960-07-11","content":"Set in the American South during the Great Depression, the novel explores themes of racial injustice and moral growth through the eyes of young Scout Finch."}' \
  http://localhost:3000/books

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"title":"The Lord of the Rings","author":"J.R.R. Tolkien","published_at":"1954-07-29","content":"A hobbit named Frodo Baggins embarks on a perilous journey to destroy a powerful ring and save Middle-earth from the Dark Lord Sauron."}' \
  http://localhost:3000/books

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"title":"The Catcher in the Rye","author":"J.D. Salinger","published_at":"1951-07-16","content":"Holden Caulfield narrates his experiences in New York City after being expelled from prep school, grappling with themes of alienation, identity, and innocence."}' \
  http://localhost:3000/books

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"title":"The Alchemist","author":"Paulo Coelho","published_at":"1988-01-01","content":"Santiago, a shepherd boy, travels from Spain to Egypt in search of a treasure buried near the Pyramids. Along the way, he learns about the importance of following one‘s dreams."}' \
  http://localhost:3000/books

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"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."}' \
  http://localhost:3000/books

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"title":"1984","author":"George Orwell","published_at":"1949-06-08","content":"Winston Smith lives in a totalitarian society ruled by the Party led by Big Brother. He rebels against the oppressive regime but ultimately succumbs to its control."}' \
  http://localhost:3000/books

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"title":"The Girl with the Dragon Tattoo","author":"Stieg Larsson","published_at":"2005-08-01","content":"Journalist Mikael Blomkvist and hacker Lisbeth Salander investigate the disappearance of a young woman from a wealthy family, uncovering dark secrets and corruption."}' \
  http://localhost:3000/books

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"title":"Gone Girl","author":"Gillian Flynn","published_at":"2012-06-05","content":"On their fifth wedding anniversary, Nick Dunne‘s wife, Amy, disappears. As the media circus ensues and suspicions mount, Nick finds himself in a whirlwind of deception and betrayal."}' \
  http://localhost:3000/books
上页下页