跳到主要内容

72 篇博文 含有标签「Rust」

查看所有标签

LanceDB是一种使用持久存储构建的用于矢量搜索的开源数据库,极大简化了检索、过滤和嵌入管理。 LanceDB的主要功能包括:

  1. 生产规模的矢量搜索,无需管理服务器。
  2. 存储、查询和过滤矢量、元数据和多模态数据(文本、图像、视频、点云等)。
  3. 支持矢量相似性搜索、全文搜索和SQL。
  4. 本地支持Python和Javascript/Typescript。
  5. 零拷贝、自动版本管理,管理数据版本而无需额外基础设施。
  6. GPU支持构建矢量索引(*)。
  7. 生态系统集成
  • LangChain🦜️🔗
  • LlamaIndex🦙
  • Apache-Arrow
  • Pandas
  • Polars
  • DuckDB

关于LanceDB中Table介绍

Table方法

  • name(): 获取表的名称
  • schema(): 获取表的模式
  • count_rows(): 获取表中的行数
  • add(): 添加记录添加到表中,但传入的参数是需要实现IntoArrow的类型
  • query(): 查询表中的记录
  • update(): 更新表中的记录
  • delete(): 删除表中的记录
  • create_index(): 创建索引
  • merge_insert(): 合并插入
  • vector_search(): 矢量搜索
  • optimize(): 优化表
  • add_columns(): 添加(多)列
  • alter_columns(): 修改(多)列
  • drop_columns(): 删除(多)列
  • version(): 获取表的版本,由于LanceDB使用版本控制变化
  • checkpoint(): 根据指定版本获取检查点
  • checkpoint_latest(): 获取最新检查点
  • restore(): 恢复到指定版本
  • list_indices(): 列出表的索引

Schema模式定义Table定义方式

LanceDB Schema关系图

LanceDB Table依赖关系图

创建空表

1 完整代码

use arrow_schema::{DataType, Field, Schema};
use lancedb::{connect, Result};
use std::sync::Arc;

#[tokio::main]
async fn main() -> Result<()> {
let created_empty_talbe = create_empty_table().await?;
println!(
"Created empty table: {}, Table name: {}",
created_empty_talbe,
created_empty_talbe.name()
);
Ok(())
}

#[allow(unused)]
async fn create_empty_table() -> Result<LanceDbTable> {
// 创建模式定义
let schema = Arc::new(Schema::new(vec![
Field::new("id", DataType::Int32, false),
Field::new("name", DataType::Utf8, false),
]));
// 创建数据库URI目录
let uri = "data/sample-lancedb";
// 连接数据库
let db = connect(uri).execute().await?;
// 创建一个空表
let table = db
.create_empty_table("empty_talbe", schema)
.execute()
.await?;
Ok(table)
}
  1. 包依赖文件 Cargo.toml文件内容如下:
lancedb = "0.7.0"
tokio = {version = "1.38.0", features = ["rt-multi-thread"]}
arrow-schema = "51.0"
  1. 运行结果如下: LanceDB创建空表

创建带初始化数据的表

  1. 完整代码如下:
use arrow_schema::{DataType, Field, Schema};
use arrow_array::{Int32Array, RecordBatch, RecordBatchIterator, StringArray};
use lancedb::{connect, Result, Table as LanceDbTable};
use std::sync::Arc;

#[tokio::main]
async fn main() -> Result<()> {
let created_table_with_data = create_table_with_data().await?;
println!(
"Created table with data: {}, Table name: {}",
created_table_with_data,
created_table_with_data.name()
);
Ok(())
}

#[allow(unused)]
async fn create_table_with_data() -> Result<LanceDbTable> {
// 创建本地数据库URI目录
let uri = "data/sample-lancedb";
// 连接数据库
let db = connect(uri).execute().await?;

// 创建模式定义
let schema = Arc::new(Schema::new(vec![
Field::new("id", DataType::Int32, false),
Field::new("name", DataType::Utf8, false),
]));

// 初始化`ids`列的数据
let ids = Int32Array::from(vec![1, 2, 3]);
// 初始化`name`列的数据
let names = StringArray::from(vec!["Alice", "Bob", "Lily"]);
// 使用`Schema`以及列数据创建`RecordBatch`
let batch = RecordBatch::try_new(schema.clone(), vec![Arc::new(ids), Arc::new(names)])?;
// 使用`RecordBatch`创建`RecordBatchIterator`
let batchs = RecordBatchIterator::new(vec![batch].into_iter().map(Ok), schema);
// 创建表,并插入初始化数据
let table = db
.create_table("table_with_person", batchs)
.execute()
.await?;
Ok(table)
}
  1. 包依赖文件 Cargo.toml文件内容如下:
lancedb = "0.7.0"
tokio = {version = "1.38.0", features = ["rt-multi-thread"]}
arrow-schema = "51.0"
  1. 运行结果如下: LanceDB创建带初始化数据的表

初始化记录并创建表

  1. 完整代码
use arrow_array::types::Float32Type;
use arrow_array::{FixedSizeListArray, Int32Array, RecordBatch, RecordBatchIterator, StringArray};
use arrow_schema::{DataType, Field, Schema};
use lancedb::arrow::IntoArrow;
use lancedb::{connect, Result, Table as LanceDbTable};
use std::sync::Arc;

#[tokio::main]
async fn main() -> Result<()> {
let created_table_with_records = create_table_with_records().await?;
println!(
"Created table with records: {}, Table name: {}",
created_table_with_records,
created_table_with_records.name()
);
Ok(())
}

#[allow(unused)]
async fn create_table_with_records() -> Result<LanceDbTable> {
let uri = "data/sample-lancedb";
let db = connect(uri).execute().await?;

let initial_data = create_some_records()?;
let tbl = db.create_table("my_table", initial_data).execute().await?;

let new_data = create_some_records()?;
// NOTICE: 只有实现了 IntoArrow 的类型才能使用`add`方法,即`create_some_records`返回的类型
tbl.add(new_data).execute().await?;
Ok(tbl)
}

#[allow(unused)]
fn create_some_records() -> Result<impl IntoArrow> {
const TOTAL: usize = 1000;
const DIM: usize = 128;

let schema = Arc::new(Schema::new(vec![
Field::new("id", DataType::Int32, false),
Field::new(
"vector",
DataType::FixedSizeList(
Arc::new(Field::new("item", DataType::Float32, true)),
DIM as i32,
),
true,
),
]));

let batch = RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(Int32Array::from_iter_values(0..TOTAL as i32)),
Arc::new(
FixedSizeListArray::from_iter_primitive::<Float32Type, _, _>(
(0..TOTAL).map(|_| Some(vec![Some(1.0); DIM])),
DIM as i32,
),
),
],
)
.unwrap();
let batches = RecordBatchIterator::new(vec![batch].into_iter().map(Ok), schema.clone());
Ok(Box::new(batches))
}
  1. 包依赖文件 Cargo.toml文件内容如下:
lancedb = "0.7.0"
tokio = {version = "1.38.0", features = ["rt-multi-thread"]}
arrow-schema = "51.0"
arrow-array = "51.0"
  1. 运行结果如下: LanceDB初始化记录并创建表

打开已存在的表

  1. 完整代码
use lancedb::{connect, Result, Table as LanceDbTable};

async fn main() -> Result<()> {
let opened_table = open_with_existing_table().await?;
println!(
"Opened table: {}, Table name: {}",
opened_table,
opened_table.name()
);
}

#[allow(unused)]
async fn open_with_existing_table() -> Result<LanceDbTable> {
let uri = "data/sample-lancedb";
let db = connect(uri).execute().await?;
let table = db.open_table("my_table").execute().await?;
Ok(table)
}
  1. 包依赖文件
lancedb = "0.7.0"
tokio = {version = "1.38.0", features = ["rt-multi-thread"]}
  1. 运行结果如下: LanceDB打开已存在的表

删除表记录

  1. 完整代码
use lancedb::{connect, Result};

#[tokio::main]
async fn main() -> Result<()> {
let queried_result = query_table().await?;
println!("Query result: {:?}", queried_result);

delete_table_records().await?; // 根据条件删除表中的记录
Ok(())
}

#[allow(unused)]
async fn delete_table_records() -> Result<()> {
let uri = "data/sample-lancedb";
let db = connect(uri).execute().await?;
let table = db.open_table("my_table").execute().await?;
table.delete("id > 24").await?;
Ok(())
}
  1. 包依赖文件
lancedb = "0.7.0"
tokio = {version = "1.38.0", features = ["rt-multi-thread"]}

删除表

  1. 完整代码
use lancedb::{connect, Result};

#[tokio::main]
async fn main() -> Result<()> {
let queried_result = query_table().await?;
println!("Query result: {:?}", queried_result);

drop_table().await?; // 删除 data/sample-lancedb/my_table
Ok(())
}

#[allow(unused)]
async fn drop_table() -> Result<()> {
let uri = "data/sample-lancedb";
let db = connect(uri).execute().await?;
db.drop_table("my_table").await?;
Ok(())
}
  1. 包依赖文件
lancedb = "0.7.0"
tokio = {version = "1.38.0", features = ["rt-multi-thread"]}

删除数据库

  1. 完整代码
use lancedb::{connect, Result};

#[tokio::main]
async fn main() -> Result<()> {
let queried_result = query_table().await?;
println!("Query result: {:?}", queried_result);

drop_database().await?; // 删除 data/sample-lancedb
Ok(())
}

#[allow(unused)]
async fn drop_database() -> Result<()> {
let uri = "data/sample-lancedb";
let db = connect(uri).execute().await?;
db.drop_db().await?;
Ok(())
}
  1. 包依赖文件
lancedb = "0.7.0"
tokio = {version = "1.38.0", features = ["rt-multi-thread"]}

查询表记录

  1. 完整代码
use lancedb::{connect, Result};

#[tokio::main]
async fn main() -> Result<()> {

let queried_result = query_table().await?;
println!("Query result: {:?}", queried_result);
Ok(())
}

#[allow(unused)]
async fn query_table() -> Result<VectorQuery> {
let uri = "data/sample-lancedb";
let db = connect(uri).execute().await?;
let table = db.open_table("my_table").execute().await?;
let result = table.query().nearest_to(&[1.0; 128])?;
Ok(result)
}
  1. 包依赖文件
lancedb = "0.7.0"
tokio = {version = "1.38.0", features = ["rt-multi-thread"]}
  1. 运行结果如下: LanceDB查询表记录

更新表记录

  1. 完整代码
use lancedb::{connect, Result};

#[tokio::main]
async fn main() -> Result<()> {

update_table().await?;
Ok(())
}

#[allow(unused)]
async fn update_table() -> Result<()> {
let uri = "data/sample-lancedb";
let db = connect(uri).execute().await?;
let table = db.open_table("table_with_person").execute().await?;
println!("Before update: {:?}", table.query());
table
.update()
.only_if("id=0")
.column("name", "Bob")
.execute()
.await?; // Bob -> Halzzz

Ok(())
}
  1. 包依赖文件
lancedb = "0.7.0"
tokio = {version = "1.38.0", features = ["rt-multi-thread"]}

总结

本文详细介绍了如何使用Rust编程语言与LanceDB进行交互, 包括表的创建、插入数据、查询、更新和删除操作。 通过这些示例,展示了LanceDB在处理矢量数据和支持多模态数据方面的强大功能, 以及如何通过Rust代码实现这些操作。 LanceDB提供了丰富的API接口,简化了数据库操作, 使得开发者能够高效地管理和查询数据。

希望通过本文的讲解,您能够更好地理解并应用LanceDB来解决实际问题。

链接

鱼雪

本文将详细介绍如何从头开始使用LanceDB。每个步骤都附有详细的说明和图示,帮助您快速上手LanceDB。

LanceDB创建数据库基本调用关系图示

环境搭建

在开始之前,请确保您的开发环境中安装了以下工具:

  • Rust: 用于编写和编译代码。
  • Tokio: 异步运行时。
  • LanceDB: 数据库库。

Rust安装

使用以下命令安装Rust:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

创建Rust项目

创建一个新的Rust项目:

cargo new lancedb-example
cd lancedb-example

安装Tokio

在项目中添加Tokio依赖:

cargo add tokio --features rt-multi-thread

安装LanceDB

在项目中添加LanceDB依赖:

cargo add lancedb

代码解析

以下是一个简单的LanceDB数据库连接示例:

use lancedb::{connect, Result};

#[tokio::main]
async fn main() -> Result<()> {
let uri = "data/example-lancedb";
let db_builder = connect(uri);
let db_connect = connect(uri).execute().await?;
println!("LanceDB builder: {:?}", db_builder);
println!("LanceDB connect: {}", db_connect);
Ok(())
}

代码说明

  • URI: 数据库的URI。
  • connect: 接收URI参数,返回ConnectBuilder
  • execute: 返回Connection,在执行目录创建数据库。

connect函数

connect函数用于创建一个ConnectBuilder实例:

pub fn connect(uri: &str) -> ConnectBuilder {
ConnectBuilder::new(uri)
}

ConnectBuilder结构体

ConnectBuilder用于配置和建立与LanceDB数据库的连接。

主要字段和方法包括:

  • uri: String: 数据库的URI。
  • execute(self) -> Result<Connection>: 执行连接建立。

Database结构体

Database封装了与实际数据库交互的逻辑。

Connection结构体

Connection表示与LanceDB数据库的连接实例。

总结

通过本文,您可以了解如何在Rust中使用LanceDB进行数据库操作。希望这篇指南能帮助您快速上手LanceDB。

相关链接

鱼雪

这两天想用待办事项的番茄钟,以前也用过很多其他的产品,包括免费的、付费的,在手机上的还有在电脑上的都有。

然后最近都一直在学Rust的内容,以及之前学过一点Tauri,所以就想着自己写一个番茄钟的应用,然后就有了这个项目。

项目初衷

  1. 用Rust写一个待办事项番茄钟的应用,练习Rust的开发
  2. 足够简单好用,只专注于今天的事情, 不要有太多的功能(以往用的番茄钟都有很多功能,比如统计、报表等等,但是我只想专注于今天的事情, 比如四象限等还要想怎么安排,然后就容易导致事情拖延,托着拖着就不做了)
  3. 既有待办事项,又有番茄钟,两者结合,不要分开,这样就不用来回切换了
  4. 只专注于电脑端,不要有手机端,因为手机端的话,就容易分心,不专注于今天的事情了
  5. 只专注于电脑端,只专注于办公

项目地址

Github Repo: https://github.com/yuxuetr/todo-pomodoro

安装依赖

  1. 安装Rust
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
  1. 安装Node.js 安装链接和方法

  2. 安装pnpm

npm i -g pnpm

构建方式

  1. 克隆仓库
git clone https://github.com/yuxuetr/todo-pomodoro.git
  1. 安装依赖
cd todo-pomodoro
pnpm install
  1. 构建运行项目
pnpm run tauri dev
  1. 运行结果如图 待办事项番茄钟

设置应用的图标

图标文件存放在src-tauri/icons下,在这下面有针对不同平台的应用图标格式。

图标的设置在tauri.conf.json文件中,如下所示

"bundle": {
"active": true,
"targets": "all",
"identifier": "com.yuxuetr.dev",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}

在MacOS下,应用的图标是icon.icns,所以需要将图标文件放到icons目录下,然后在tauri.conf.json文件中设置图标的路径。

MacOS平台应用的图标大小是1024x1024,所以需要将图标文件转换成1024x1024的大小,然后修改成icon.icns放到icons目录下。

如果需要将图标由直角转换成圆角,可以使用在线工具。也可以试试我写的工具。

有三个不同的版本:

应用发布

第一次发布应用,需要先构建应用需要使用--target参数,指定目标平台

cargo tauri build --target aarch64-apple-darwin
# 或者
pnpm tauri build --target aarch64-apple-darwin

之后每次发布应用,只需要执行下面的命令即可

pnpm tauri build

应用构建与发布

执行命令后,会弹出下面的窗口

应用安装

然后按照Mac应用的方式安装应用,将应用拖到应用文件夹即可

项目后续

  1. 项目后期增加统计功能(如果有必要的话)
  2. 项目后期增加命令行功能(Tauri支持clip命令,可以在命令行中使用)
鱼雪

在Axum中,中间件是处理请求和响应的强大工具。

它们可以在请求到达实际处理程序之前或响应发回客户端之前对其进行处理

Axum提供了多种方式来实现中间件。

本文将介绍两种常用的方法:使用from_fntower::Layer

方式一:from_fn

from_fn是一种简便的方法,可以快速创建中间件。它适用于那些不需要复杂逻辑的中间件。

示例代码: request_id

以下是一个通过from_fn实现请求ID中间件的示例:

// request_id.rs
use super::REQUEST_ID_HEADER;
use axum::{extract::Request, http::HeaderValue, middleware::Next, response::Response};
use tracing::warn;

pub async fn set_request_id(mut req: Request, next: Next) -> Response {
let id = match req.headers().get(REQUEST_ID_HEADER) {
Some(v) => Some(v.clone()),
None => {
let request_id = uuid::Uuid::new_v4().to_string();
match HeaderValue::from_str(&request_id) {
Ok(v) => {
req.headers_mut().insert(REQUEST_ID_HEADER, v.clone());
Some(v)
}
Err(e) => {
warn!("parse generated request id failed: {}", e);
None
}
}
}
};

let mut res = next.run(req).await;

if let Some(id) = id {
res.headers_mut().insert(REQUEST_ID_HEADER, id);
}
res
}

在这个示例中,中间件会检查请求头中是否包含请求ID。 如果没有,则生成一个新的UUID并将其添加到请求头中。 然后,继续处理请求并在响应头中添加相同的请求ID。

Token验证中间件

这里再提供一个示例,展示如何实现一个验证Token的中间件:

// auth.rs
use crate::AppState;
use axum::{
extract::{FromRequestParts, Request, State},
http::StatusCode,
middleware::Next,
response::{IntoResponse, Response},
};
use axum_extra::{
headers::{authorization::Bearer, Authorization},
TypedHeader;
use tracing::warn;

pub async fn verify_token(State(state): State<AppState>, req: Request, next: Next) -> Response {
let (mut parts, body) = req.into_parts();
let req = match TypedHeader::<Authorization<Bearer>>::from_request_parts(&mut parts, &state).await {
Ok(TypedHeader(Authorization(bearer))) => {
let token = bearer.token();
match state.dk.verify(token) {
Ok(user) => {
let mut req = Request::from_parts(parts, body);
req.extensions_mut().insert(user);
req
}
Err(e) => {
let msg = format!("verify token failed: {}", e);
warn!(msg);
return (StatusCode::FORBIDDEN, msg).into_response();
}
}
}
Err(e) => {
let msg = format!("parse Authorization header failed: {}", e);
warn!(msg);
return (StatusCode::UNAUTHORIZED, msg).into_response();
}
};
next.run(req).await
}

在这个示例中,中间件会检查请求头中的Authorization Bearer token,并验证其有效性。 如果验证失败,将返回相应的错误响应。

方式二: tower::Layer

tower::Layer是创建复杂中间件的推荐方式。

它提供了更多的灵活性和控制,可以处理更复杂的逻辑。

示例代码

以下是一个通过tower::Layer实现服务器时间中间件的示例:

// server_time.rs
use super::{REQUEST_ID_HEADER, SERVER_TIME_HEADER};
use axum::{extract::Request, response::Response};
use std::{
future::Future,
pin::Pin,
task::{Context, Poll},
};
use tokio::time::Instant;
use tower::{Layer, Service};
use tracing::warn;

#[derive(Clone)]
pub struct ServerTimeLayer;

impl<S> Layer<S> for ServerTimeLayer {
type Service = ServerTimeMiddleware<S>;

fn layer(&self, inner: S) -> Self::Service {
ServerTimeMiddleware { inner }
}
}

#[derive(Clone)]
pub struct ServerTimeMiddleware<S> {
inner: S,
}

impl<S> Service<Request> for ServerTimeMiddleware<S>
where
S: Service<Request, Response = Response> + Send + 'static,
S::Future: Send + 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;

fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}

fn call(&mut self, req: Request) -> Self::Future {
let start = Instant::now();
let future = self.inner.call(req);
Box::pin(async move {
let mut res: Response = future.await?;
let elapsed = format!("{}us", start.elapsed().as_micros());
match elapsed.parse() {
Ok(v) => {
res.headers_mut().insert(SERVER_TIME_HEADER, v);
}
Err(e) => {
warn!(
"Parse elapsed time failed: {} for request {:?}",
e,
res.headers().get(REQUEST_ID_HEADER)
);
}
}
Ok(res)
})
}
}

在这个示例中,中间件会记录请求处理的时间,并将其添加到响应头中。 它使用tower::Layer来创建一个自定义的中间件结构体ServerTimeMiddleware

通过这些示例,可以看到在Axum中实现中间件的灵活性和强大功能。 根据需求选择合适的方法,可以帮助你更好地管理和处理HTTP请求和响应。

总结

  1. 中间件概述
  • 中间件是处理请求和响应的工具,可以在请求到达处理程序之前或响应发回客户端之前对其进行处理。
  • Axum提供了多种实现中间件的方法,主要有from_fntower::Layer两种方式。
  1. 方式一:from_fn

    • from_fn方法适用于快速创建简单的中间件。
  • 示例代码展示了如何通过from_fn实现请求ID中间件:

    • 检查请求头是否包含请求ID,如果没有则生成一个新的UUID并添加到请求头中。
    • 继续处理请求,并在响应头中添加相同的请求ID。
  1. 方式二:tower::Layer

    • tower::Layer方法适用于创建复杂的中间件,提供更多灵活性和控制。
  • 示例代码展示了如何通过tower::Layer实现服务器时间中间件:

    • 记录请求处理时间,并将其添加到响应头中。
    • 创建一个自定义的中间件结构体ServerTimeMiddleware
  1. Token验证中间件

    • 示例代码展示了如何实现一个验证Token的中间件:

      • 检查请求头中的Authorization Bearer token,并验证其有效性。
    • 如果验证失败,返回相应的错误响应。

鱼雪

在Rust中,struct StructName(DemoName); 这种用法定义的事一个元组结构体(tuple struct), 它是一种特殊的结构体它的字段没有名称,只有索引

它并不会直接继承DemoName中所有pub内容,而是将DemoName作为一个单独的字段封装在结构体中。

为什么要这样做呢?这样做的好处是可以在不改变DemoName的情况下,为其添加额外的功能。

元组结构体的定义和使用

struct DemoName {
pub index: i32,
pub value: String,
}

struct StructName(DemoName);

在上面的例子中,StructName是一个元组结构体,它包含一个DemoName类型的字段。 这个字段可以通过索引0来访问

fn main() {
let demo = DemoName {
index: 1,
value: "Hello".to_string(),
};
let struct_name = StructName(demo);

// 访问内部的`DemoName`结构体
println!("Index: {}", struct_name.0.index);
println!("Value: {}", struct_name.0.value);
}

在这个例子中,我们创建了一个DemoName实例, 并将其作为一个字段封装在StructName结构体中。 要访问DemoName中的字段,我们使用struct_name.0来访问。

自动解引用和Deref

通过实现Deref trait,可以使元组结构体在使用时表现的像它所包含的值一样,从而简化代码。

use std::ops::Deref;

struct DemoName {
pub index: i32,
pub value: String,
}

struct StructName(DemoName);

impl Deref for StructName {
type Target = DemoName;

fn deref(&self) -> &Self::Target {
&self.0
}
}

fn main() {
let demo = DemoName {
index: 1,
value: "Hello".to_string(),
};
let struct_name = StructName(demo);

// 访问内部的`DemoName`结构体
println!("Index: {}", struct_name.index);
println!("Value: {}", struct_name.value);
}

在这个例子中,通过实现Deref trait,我们可以直接访问StructName中的DemoName字段, 而不需要显式地使用索引0

结构体的创建和使用方式

普通结构体

普通结构体的字段有名字,可以通过名字来访问。

struct DemoName {
pub index: i32,
pub value: String,
}

fn main() {
let demo = DemoName {
index: 1,
value: "Hello".to_string(),
};

// 访问结构体的字段
println!("Index: {}", demo.index);
println!("Value: {}", demo.value);
}

元素结构体

元组结构体的字段没有名字,只有索引,可以通过索引来访问。

struct DemoName (i32, String);

fn main() {
let demo = DemoName(1, "Hello".to_string());

// 访问元组结构体的字段
println!("Index: {}", demo.0);
println!("Value: {}", demo.1);
}

单元结构体

单元结构体没有字段,通常用于标记或实现某些trait。

struct UnitStruct;

fn main() {
let unit = UnitStruct;
}

实际使用案例: JWT签名和验证

use crate::{AppError, User};
use jwt_simple::prelude::*;
use std::ops::Deref;

const JWT_DURATION: u64 = 60 * 60 * 24 * 7;
const JWT_ISS: &str = "chat_server";
const JWT_AUD: &str = "chat_web";

pub struct EncodingKey(Ed25519KeyPair);
pub struct DecodingKey(Ed25519PublicKey);

impl EncodingKey {
pub fn load(pem: &str) -> Result<Self, AppError> {
Ok(Self(Ed25519KeyPair::from_pem(pem)?))
}

pub fn sign(&self, user: impl Into<User>) -> Result<String, AppError> {
let claims = Claims::with_custom_claims(
user.into(), Duration::from_secs(JWT_DURATION));
let claims = claims.with_issuer(JWT_ISS).with_audience(JWT_AUD);
Ok(self.0.sign(claims)?)
}
}

impl DecodingKey {
pub fn load(pem: &str) -> Result<Self, AppError> {
Ok(Self(Ed25519PublicKey::from_pem(pem)?))
}

pub fn verify(&self, token: &str) -> Result<User, AppError> {
let mut options = VerificationOptions::default();
options.allowed_issuers = Some(HashSet::from_strings(&[JWT_ISS]));
options.allowed_audiences = Some(HashSet::from_strings(&[JWT_AUD]));
let claims = self.0.verify_token::<User>(token, Some(options))?;
Ok(claims.custom)
}
}

impl Deref for EncodingKey {
type Target = Ed25519KeyPair;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl Deref for DecodingKey {
type Target = Ed25519PublicKey;

fn deref(&self) -> &Self::Target {
&self.0
}
}

#[cfg(test)]
mod tests {
use anyhow::Result;
use super::*;
use crate::User;

#[tokio::test]
async fn test_jwt() -> Result<()> {
let encoding_pem = include_str!("../../fixtures/encoding.pem");
let decoding_pem = include_str!("../../fixtures/decoding.pem");
let ek = EncodingKey::load(encoding_pem)?;
let dk = DecodingKey::load(decoding_pem)?;

let user = User::new(1, "Hal", "halzzz@gmail.com");

let token = ek.sign(user.clone())?;
let user2 = dk.verify(&token)?;
assert_eq!(user, user2);
Ok(())
}
}

在这个例子中,我们定义了两个结构体EncodingKeyDecodingKey, 它们分别用于签名和验证JWT令牌。 这里使用EncodingKey封装了Ed25519KeyPair,使其方便加载密钥签名。 这里使用DecodingKey封装了Ed25519PublicKey,使其方便加载公钥验证

通过实现Deref trait,我们可以直接访问Ed25519KeyPairEd25519PublicKey中的方法, 而不需要显式地使用self.0来访问。

总结

本文介绍了元组结构体(tuple struct)的定义和使用,以及如何通过实现Deref trait来简化代码。 元组结构体是一种特殊的结构体,它的字段没有名称,只有索引。 通过实现Deref trait,可以使元组结构体在使用时表现的像它所包含的值一样,从而简化代码。

鱼雪

Deref trait 是 Rust 中的一个特性,它允许我们重载解引用运算符 *。 这个特性在 Rust 中非常有用, 因为它允许我们在自定义类型上使用解引用运算符,而不需要手动调用 *

在Rust中,Deref trait 是一个非常强大的工具, 它允许你通过解引用运算符*来访问底层数据

Deref trait 最常见的用途之一 是将自定义智能指针类型转换为其内部持有的数据类型。

通过实现 Deref trait,你可以使你的自定义类型与标准库中的类型(如引用和智能指针)具有相同的行为。

使用 Deref 的情形

  • 自定义智能指针:如果你创建了一个自定义智能指针类型,可以通过实现 Deref trait 来使其像标准指针一样工作。
  • 类型转换:当你希望你的类型在某些上下文中像另一个类型一样工作时,可以使用 Deref 进行隐式转换。
  • 函数调用简化:当你希望你的类型能够自动解引用以便调用底层类型的方法时,Deref 非常有用。

情形1: 自定义智能指针

情形说明

  • 如果你创建了一个自定义智能指针类型,可以通过实现 Deref trait 来使其像标准指针一样工作。
use std::ops::Deref;

struct MyBox<T> {
value: T,
}

impl<T> MyBox<T> {
fn new(value: T) -> MyBox<T> {
MyBox { value }
}
}

impl<T> Deref for MyBox<T> {
type Target = T;

fn deref(&self) -> &T {
&self.value
}
}

fn main() {
let x = 5;
let y = MyBox::new(x);

// 通过 Deref trait 自动解引用
println!("Value in MyBox: {}", *y);
}

情形2: 类型转换

情形说明

  • 当你希望你的类型在某些上下文中像另一个类型一样工作时,可以使用 Deref 进行隐式转换
use std::ops::Deref;

struct MyBox<T> {
value: T,
}

impl<T> MyBox<T> {
fn new(value: T) -> MyBox<T> {
MyBox { value }
}
}

impl<T> Deref for MyBox<T> {
type Target = T;

fn deref(&self) -> &T {
&self.value
}
}

fn print_value(value: &i32) {
println!("Value: {}", value);
}

fn main() {
let x = 5;
let y = MyBox::new(x);

// 通过 Deref trait 自动解引用
print_value(&y);
}

情形3: 函数调用简化

情形说明

  • 当你希望你的类型能够自动解引用以便调用底层类型的方法时,Deref 非常有用
use crate::{AppError, User};
use jwt_simple::prelude::*;
use std::ops::Deref;

const JWT_DURATION: u64 = 60 * 60 * 24 * 7;

pub struct EncodingKey(Ed25519KeyPair);
pub struct DecodingKey(Ed25519PublicKey);

impl EncodingKey {
pub fn load(pem: &str) -> Result<Self, AppError> {
Ok(Self(Ed25519KeyPair::from_pem(pem)?))
}

pub fn sign(user: User, key: &EncodingKey) -> Result<String, AppError> {
let claims = Claims::with_custom_claims(user, Duration::from_secs(JWT_DURATION));
Ok(key.sign(claims)?) // 这里key为引用,但是我们想要像操作结构体本身一样使用
}
}

impl DecodingKey {
pub fn load(pem: &str) -> Result<Self, AppError> {
Ok(Self(Ed25519PublicKey::from_pem(pem)?))
}

pub fn verify(token: &str, key: &DecodingKey) -> Result<User, AppError> {
let claims = key.verify_token::<User>(token, None)?; // 这里key为引用,但是我们想要像操作结构体本身一样使用
Ok(claims.custom)
}
}

impl Deref for EncodingKey {
type Target = Ed25519KeyPair;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl Deref for DecodingKey {
type Target = Ed25519PublicKey;

fn deref(&self) -> &Self::Target {
&self.0
}
}

如果参数传递的是结构体的引用,并且你想要在使用该引用时像操作结构体本身一样使用

那么实现 Deref trait 是一个很好的选择

通过实现 Deref trait,你可以让结构体的引用表现得像结构体本身一样,从而简化代码并提高可读性。

总结

Deref trait 主要用在智能指针类型和自定义类型上, 它可以帮助我们简化代码、减少重复、提高可读性。

Rust中传参,优先使用引用,而不是值, 实现 Deref trait 可以让你在使用引用时像操作结构体本身一样使用, 从而提高代码的可读性和简洁性。

鱼雪

Burn 是一个新型的、全面的 动态深度学习框架,使用 Rust 构建,目标是提供极高的 灵活性计算效率可移植性。通过自动内核融合、异步执行和多硬件后端的支持,Burn 为开发者提供了前所未有的高性能深度学习体验。

Burn 的主要特性

  • 自动内核融合:通过动态生成 GPU 内核,提升模型执行效率。
  • 异步执行架构:保证框架与计算的独立性,提升系统性能。
  • 多后端集成:支持多种硬件架构,跨平台高效执行。
  • 线程安全与智能内存管理:利用 Rust 的所有权系统,确保训练过程中的安全与效率。

性能优化:自动内核融合与异步执行

自动内核融合 💥

Burn 提供自动内核融合,意味着在任何后端都能对模型进行优化。在可能的情况下,Burn 自动创建自定义低级内核,特别适用于需要最小化内存迁移的场景,从而显著提升模型的计算效率。

以下是一个 Rust 代码片段,展示如何实现自定义 GELU 激活函数:

fn gelu_custom<B: Backend, const D: usize>(x: Tensor<B, D>) -> Tensor<B, D> {
let x = x.clone() * ((x / SQRT_2).erf() + 1);
x / 2
}

在运行时,Burn 会为这个自定义实现自动创建内核,与手工编写的 GPU 实现性能相媲美。目前,自动内核融合仅支持 WGPU 后端,未来会扩展到其他后端。

异步执行 ❤️‍🔥

Burn 的后端采用 异步执行风格,这使得模型的计算不会阻塞框架的正常执行,确保系统的高响应性。这种异步架构还为自动内核融合等优化技术提供了支持,进一步提高执行效率。更多关于异步执行的信息,请访问 Burn 博客

线程安全的模块 🦞

Burn 利用 Rust 的所有权系统,使每个模块成为其权重的唯一所有者。这样可以安全地将模块发送到不同线程进行计算,适合多设备训练,避免了传统框架中的同步问题。

智能内存管理 🦀

内存管理 是深度学习框架的关键之一。Burn 使用内存池减少频繁的内存分配和释放,从而提升吞吐量,并通过跟踪张量的所有权在适当时候进行就地突变,进一步减少内存使用。有关内存管理的更多详细信息,请参考 Burn 博客

自动内核选择 🎯

Burn 会自动运行基准测试并为硬件选择最佳内核配置,以确保在所有硬件上高效执行。这稍微增加了热身时间,但会在几次前向和后向传递后稳定下来,大大提高长期执行效率。

硬件特定优化 🔥

Burn 支持多种硬件特性,例如 Nvidia Tensor Cores。目前,Tensor Cores 支持通过 LibTorch 和 Candle 后端进行优化,但还未支持其他加速器。我们计划在未来为 WGPU 后端引入类似的硬件支持。

扩展与灵活性:后端扩展与自定义实现

Burn 提供了丰富的扩展能力,使开发者能够轻松地为模型添加自定义操作或编写特定后端的内核。例如,可以手动编写快闪注意力的实现来提升性能。更多关于后端扩展的信息,请参考 Burn Book

Burn 的深度学习工作流程

培训与推理

Burn 从一开始就考虑了深度学习的培训和推理过程,提供了符合人体工程学的仪表板来监控训练进展,并能够在从嵌入式设备到大型 GPU 集群的任意设备上运行推理。

  • 培训仪表板 📈:Burn 提供基于 Ratatui 框架的终端 UI 仪表板,用户可以方便地实时跟踪训练和验证指标。
  • ONNX 支持 🐫:Burn 支持导入符合 ONNX 标准的模型,便于将其他框架(如 TensorFlow 或 PyTorch)的模型移植到 Burn。
  • 浏览器推理 🌐:通过编译为 Web Assembly,Burn 支持直接在浏览器内运行推理。您可以查看以下示例:

多后端支持

Burn 致力于成为一个 跨平台、支持多后端的深度学习框架,旨在满足不同硬件和开发需求的灵活性。

支持的后端

  1. WGPU 后端 🌐:基于 Rust 图形库 WGPU,支持跨平台 GPU 加速,适用于从 Vulkan 到 WebGPU 等多种环境。更多信息,请参考 WGPU 后端 README
  2. Candle 后端 🕯:基于 Hugging Face 的 Candle,支持 Web Assembly 和 CUDA,适合极简的高性能模型。更多信息,请参考 Candle 后端 README
  3. LibTorch 后端 🎆:基于 PyTorch 的 Rust 绑定,支持 CPU、CUDA 和 Metal,加速深度学习模型训练和推理。更多信息,请参考 LibTorch 后端 README
  4. NdArray 后端 🦐:轻量级 CPU 后端,唯一支持 no_std 的后端,可在无操作系统环境下运行。更多信息,请参考 NdArray 后端 README
  5. Autodiff 后端 🔄:作为后端修饰器,为任何基础后端增加自动微分支持,用于模型训练。更多信息,请参考 Autodiff 后端 README
  6. Fusion 后端 💥:为支持内核融合的后端增加内核融合特性,目前仅 WGPU 后端支持融合。更多信息,请参考 Fusion 后端 README

入门指南

The Burn Book 🔥

要有效地开始使用 Burn,建议阅读 The Burn Book 的前几章,了解 Burn 的关键组成部分和哲学,涵盖了张量、模块和优化器等构建模块的详细示例。

示例代码 🙏

以下是一个简单的代码片段,展示 Burn 的模块声明和前向传播实现:

use burn::nn;
use burn::module::Module;
use burn::tensor::backend::Backend;

#[derive(Module, Debug)]
pub struct PositionWiseFeedForward<B: Backend> {
linear_inner: nn::Linear<B>,
linear_outer: nn::Linear<B>,
dropout: nn::Dropout,
gelu: nn::Gelu,
}

impl<B: Backend> PositionWiseFeedForward<B> {
pub fn forward<const D: usize>(&self,

input: Tensor<B, D>) -> Tensor<B, D> {
let x = self.linear_inner.forward(input);
let x = self.gelu.forward(x);
let x = self.dropout.forward(x);

self.linear_outer.forward(x)
}
}

我们还提供了丰富的 代码示例,展示如何在不同情景中使用 Burn。

为什么选择 Rust 进行深度学习 🦀

Rust 提供了 零成本抽象 和强大的 内存管理,是深度学习开发的理想选择。Rust 提供高性能和安全性,同时使用 Cargo 作为包管理器,可以轻松地构建、测试和部署应用程序。

Rust 的学习曲线可能较为陡峭,但一旦掌握,它可以提供更可靠、无 bug 的解决方案,极大地提升开发效率和代码质量。

结论

Burn 是一个功能强大的 Rust 深度学习框架,支持自动内核融合、异步执行、多后端集成,适用于从嵌入式设备到大型 GPU 集群的各种场景。如果您对深度学习的性能和灵活性有高要求,并且希望探索 Rust 的强大能力,那么 Burn 会是一个绝佳的选择。

鱼雪

Rust 中,特性(features)是一种用于条件编译的机制,能够让开发者根据不同的需求启用或禁用某些代码块,实现模块化和灵活性。Cargo 提供了工具 cargo add --featurescargo run --features,用于方便地管理项目的特性。

本文将详细介绍 如何使用这两个命令,并深入分析它们的区别、优缺点,以及应用场景和示例代码。

什么是特性(features)?

在 Rust 中,特性是一种条件编译的工具,可以根据需求为项目启用或禁用特定功能。通过使用特性,可以减少编译时间、减小最终二进制文件的大小,或者为代码添加可选的依赖。

特性使得项目的依赖项、模块和功能的启用变得更加灵活,可以根据特定的配置编译不同的功能模块,满足开发过程中的各种需求。

1. 使用 cargo add --features

什么是 cargo add --features

cargo add 是一个用于将依赖项添加到项目 Cargo.toml 文件的命令。通过使用 cargo add crate_name --features features_name,您可以在添加依赖项时,直接启用该依赖项的特性,这样特性配置会被保存在 Cargo.toml 中,以便未来使用。

使用方法

cargo add crate_name --features feature_name

示例:添加 serde 并启用特性

cargo add serde --features derive

上述命令将 serde 库添加到项目的 Cargo.toml 中,并启用其 derive 特性。生成的 Cargo.toml 文件内容如下:

[dependencies]
serde = { version = "1.0", features = ["derive"] }

优点

  • 便于管理:特性配置保存在 Cargo.toml 中,集中管理,便于查看和维护。
  • 自动启用:通过配置,特性会被持久保存,无需每次运行项目时手动指定。

缺点

  • 修改配置文件:需要修改 Cargo.toml 文件,对于短期需求或临时特性,显得比较繁琐。

2. 使用 cargo run --features

什么是 cargo run --features

cargo run 是一个用于编译并运行当前项目的命令。通过 cargo run --features feature_name,您可以在运行项目时临时启用某些特性,而无需修改 Cargo.toml,这些特性仅在当前编译和运行过程中有效。

使用方法

cargo run --features feature_name

示例:使用 cargo run 启用特性

假设 Cargo.toml 文件如下所示:

[dependencies]
serde = "1.0"

[features]
special_feature = ["serde/derive"]

运行命令:

cargo run --features special_feature

这会在编译和运行时启用 special_feature,从而使项目可以使用相应的功能。

优点

  • 灵活性:无需修改配置文件,可以根据需要临时启用某些特性。
  • 快速测试:适用于需要快速启用某些功能的测试场景,而不希望永久更改项目的配置。

缺点

  • 需显式指定:每次运行命令时都需要显式指定特性,手动输入特性名称可能会显得冗长。

3. 区别与使用场景

配置方式

  • cargo add --features:通过 Cargo.toml 配置特性,适合长期启用的功能。
  • cargo run --features:在运行命令时临时指定特性,适合短期、临时启用功能的场景。

持久性

  • cargo add --features:永久配置,特性将一直有效,直到被手动修改或删除。
  • cargo run --features:临时配置,仅在当前编译和运行时有效。

使用场景

  • 长期启用:使用 cargo add --features,例如,您需要持续使用某个功能,可以将其配置在 Cargo.toml 中。
  • 临时启用:使用 cargo run --features,例如,进行一次性测试或某些功能验证,不希望修改项目文件。

4. 示例代码与用法详解

示例一:使用 cargo add --features

  1. 使用 cargo add 添加依赖并启用特性:

    cargo add serde --features derive
  2. Cargo.toml 文件:

    [dependencies]
    serde = { version = "1.0", features = ["derive"] }
  3. Rust 代码示例:

    use serde::{Serialize, Deserialize};

    #[derive(Serialize, Deserialize)]
    struct MyStruct {
    name: String,
    age: u32,
    }

    fn main() {
    let my_struct = MyStruct {
    name: "Alice".to_string(),
    age: 30,
    };

    let serialized = serde_json::to_string(&my_struct).unwrap();
    println!("Serialized: {}", serialized);
    }

示例二:使用 cargo run --features

  1. Cargo.toml 文件:

    [dependencies]
    serde = "1.0"

    [features]
    special_feature = ["serde/derive"]
  2. Rust 代码示例:

    #[cfg(feature = "special_feature")]
    #[macro_use]
    extern crate serde_derive;

    #[cfg(feature = "special_feature")]
    #[derive(Serialize, Deserialize)]
    struct MyStruct {
    name: String,
    age: u32,
    }

    fn main() {
    #[cfg(feature = "special_feature")]
    {
    let my_struct = MyStruct {
    name: "Alice".to_string(),
    age: 30,
    };

    let serialized = serde_json::to_string(&my_struct).unwrap();
    println!("Serialized: {}", serialized);
    }

    println!("Hello, world!");
    }
  3. 运行命令:

    cargo run --features special_feature

5. 总结

  • cargo add --features:适用于需要长期启用的特性,通过在 Cargo.toml 中进行配置,特性会持久有效,便于管理和维护。
  • cargo run --features:适用于临时启用的特性,通过运行时指定,避免了修改项目配置的繁琐,提供了很大的灵活性。

通过合理使用这两种命令,您可以更加灵活地管理和控制 Rust 项目中的功能特性,提升项目的可维护性和开发效率。

鱼雪