Slide 1

Slide 1 text

オフライン対応! Flutterアプリに 全文検索エンジンを 実装する Dreamwalker (@jaichangpark) 1

Slide 2

Slide 2 text

Jai-Chang Park GDE Dart-Flutter GDG Golang Korea Flutter Seoul @jaichangpark 2

Slide 3

Slide 3 text

Overview 3

Slide 4

Slide 4 text

● 多くのAIアプリはネットワークを利用してサーバーのAPIを呼び出して動作する。 ○ ネットワークなしでは利用困難。 ● オンデバイス AI, On-Device AI ○ ネットワークが利用できない環境。 ○ オフラインでもAIを活用可能であることが重要。 ● LLMの制約 ○ ただし、LLMにはKnowledge Cutoffがある。 * オンデバイスAI : On-Device AI * LLM: Large Language Model * オフライン: Offline Problem & Motivation 4

Slide 5

Slide 5 text

AIにおける「知識のカットオフ」とは何か AIが学習した知識に存在する「最終更新日」である。 AIが持つ知識の「有効期限」に例えられる。 この特定の日付以降に発生した出来事や新しい情報は、 原則としてAIの知識に含まれない。 Problem & Motivation 5 AIのKnowledge Cutoff(知識のカットオフ)とは?

Slide 6

Slide 6 text

Dot shorthands Dart 3.10 2025-11-13 6

Slide 7

Slide 7 text

7 AIのKnowledge Cutoff(知識のカットオフ)とは?

Slide 8

Slide 8 text

● 検索拡張生成、 RAG (Retrieval-Augmented Generation) ○ Knowledge Cutoff を解消 ○ 知識追加 ■ 自分専用, カスタム, 社内データ など ○ ネットワーク依存 → オフライン対応が必要 ● 知識追加方法 ○ プロンプトにコンテキストを提供 ■ ベクトルデータベース(Vector Database)を活用 ● 埋め込み(Embedding) + 類似度検索(Similarity Search) ■ ハイブリッド検索 ■ Web検索 Problem & Motivation 8

Slide 9

Slide 9 text

9

Slide 10

Slide 10 text

10 知識追加

Slide 11

Slide 11 text

11

Slide 12

Slide 12 text

● ChromaDB ○ https://www.trychroma.com/home ● Milvus ○ https://milvus.io/ja ● Qdrant ○ https://qdrant.tech/ ● Pinecone ○ https://www.pinecone.io/ サーバーを構築または利用する場合、オンライン(ネットワーク)接続が必要。 ベクトルデータベース ● DuckDB ○ https://duckdb.org/ ● ElasticSearch ○ https://www.elastic.co/jp/elasticsearch ● OpenSearch ○ https://opensearch.org/ 12

Slide 13

Slide 13 text

EmbeddingGemmaやQwenEmbeddingを利用可能 ● EmbeddingGemma ○ 300M parameter ○ Output embedding dimension size of 768, with smaller options available (512, 256, or 128) via Matryoshka Representation Learning (MRL) ○ https://huggingface.co/google/embeddinggemma-300m ● QwenEmbedding ○ 0.6B(600M), 4B, 8B parameter ○ dimension size: 1024, 2560, 4096 ○ https://huggingface.co/Qwen/Qwen3-Embedding-0.6B テキスト埋め込み 13

Slide 14

Slide 14 text

テキスト埋め込み + 類似度検索だけでは限界がある。 ● 多言語対応の観点から、知識検索を改善する。 ○ ハイブリッド方式を採用し、複数の情報を組み合わせてLLMに注入する。 ■ Embeddingによる類似度検索の結果 ■ テキスト全文検索の結果 ● そのため、Flutterで全文検索エンジンを実装する必要あり テキスト埋め込み 14

Slide 15

Slide 15 text

全文検索(Full Text Search, FTS)とは、 文書全体を対象にテキストを検索する仕組みである。 単なるキーワード一致ではなく、 文章の意味や形態素 を解析して、より適切な結果を返す。 代表的な全文検索エンジンには以下がある: • Elasticsearch:オープンソースで高性能な全文検索エンジン。 • OpenSearch:ElasticsearchをベースにAmazonが提供するオープンソース版。 全文検索, Full Text Search 15

Slide 16

Slide 16 text

文章を単語や意味の最小単位(形態素)に分解して解析する手法である。 形態素解析とは? 16 私 は Flutter が 大好き です 私はFlutterが大好きです

Slide 17

Slide 17 text

条件 ● オフライン状態 要件 ● 既存ではネットワーク必須だった機能を、 すべてオフライン環境でも動作可能にする必要がある。 現状 ● ✅ スマートフォン上でLLMを動作させることは可能。 ● ✅ スマートフォン上でテキスト埋め込み、類似度検索は可能。 ● 📍 知識検索の性能向上方法が必要。 ● 📍 Flutterアプリでオフライン状態でも使用可能な全文検索エンジンが必要。 17

Slide 18

Slide 18 text

Goal 18

Slide 19

Slide 19 text

FTS(Full Text Search)対応の検索エンジン・ライブラリを確認 作成済みのFlutterパッケージがあるか確認 CJK(中国語・日本語・韓国語)対応か確認 存在しない場合は自作で実装 結果のテスト&ベンチマーク + + + How to? + + 19

Slide 20

Slide 20 text

● SQLiteはモバイルアプリで広く使用される軽量データベース。 ● SQLiteは拡張機能としてFull-text search(全文検索)機能を提 供。 20 @srouce; https://www.sqlite.org/fts5.html SQLite FTS5 Extension SQLite FTS5

Slide 21

Slide 21 text

21 @srouce https://pub.dev/packages/drift https://drift.simonbinder.eu/sql_api/extensions/ SQLite FTS5 Extension SQLite FTS5

Slide 22

Slide 22 text

22 @srouce;https://pub.dev/packages/sqlite3_flutter_libs SQLite FTS5 Extension SQLite FTS5

Slide 23

Slide 23 text

SQLite FTS5 Extension 23 @srouce; https://www.sqlite.org/fts5.html#tokenizersl SQLite FTS5

Slide 24

Slide 24 text

● CJK 検索が必要。 ● SQLite FTS5 → 英語には強力だが、CJK対応は限定的。 次のステップ 1. CJK対応可能なトークナイザーの調査。 2. 関連するFlutterパッケージが存在するかを調査。 SQLite FTS5 Extension 24 SQLite FTS5

Slide 25

Slide 25 text

形態素解析器 (トークナイザ) 一般的に形態素解析器(トークナイザー)を活用して改善を行 う ● jieba ○ 中国語 ○ https://github.com/fxsjy/jieba ● mecab - ipadic ○ 日本語 ○ https://github.com/taku910/mecab ● mecab - ko & Kiwi ○ 韓国語 25 @source: https://taku910.github.io/mecab/

Slide 26

Slide 26 text

● https://github.com/wangfenjin/simple ○ SQLite3 FTS5 × 中国語トークナイザー対応。 ○ jieba, PinYin ● https://github.com/thino-rma/fts5_mecab ○ SQLite3 FTS5 x mecab ○ Flutterパッケージではない。 ● https://github.com/SageMik/sqlite3_simple ○ Flutterパッケージとして提供されているもの。 ○ simple プロジェクトをFlutterで利用可能にしたパッケージ。 ○ 中国語には対応しているが、日本語・韓国語のトークナイザーは未対応。 SQLite FTS5 Extension 26 SQLite FTS5

Slide 27

Slide 27 text

● Tantivyは、Rustで開発された高速全文検索エンジンライブラリであり、Apache Luceneから着想 を得ている。 ● 組み込み型ライブラリであり、ElasticsearchやOpenSearchのような外部サーバーを必要とせず、 アプリケーション内で直接利用可能である。 特徴 ● 高速なインデックス作成と検索:大規模データでも高速にインデックスを構築可能(例:英語版 Wikipediaを数分で処理)。 ● BM25スコアリング:Luceneと同様のBM25アルゴリズム採用。 ● マルチスレッド対応:インデックス作成・検索を並列処理可能。 ● 軽量で高速起動:10ms未満で起動、CLIツールや小規模サーバーに適合。 Tantivy:Rust製の高速全文検索エンジンライブラリ Tantivy 27 Tantivy

Slide 28

Slide 28 text

● メモリ安全性を保証(ガベージコレクター不 要)。 ● 高速なパフォーマンス。 ● 並行処理(Concurrency)に強い。 ● C/C++との高い互換性。 ● クロスプラットフォーム対応が容易。 https://rust-lang.org 安全で高速なシステムプログラミング言語 🦀 Rust 28 以前 Rust 難しい。 現在 ● AIを使って一緒にできる。 with Gemini CLI, Claude ● AIと勉強できる。 ⇒ 難しくない。

Slide 29

Slide 29 text

自作で実装 29 Tantivyの使い方を理解する。 API設計 + Flutterで使用するTantivy(Rust)ネイティブコードの作成。 FlutterでFFIを使用するためのプラットフォーム別ライブラリファイルのビルド。 FFIを活用したラッパークラスの開発。 + + + + UI 開発。 +

Slide 30

Slide 30 text

実装段階 30 基本から始める。 Rustネイティブコードの開発とビルド。 ライブラリのビルド方法 。 Dart FFI の使い方。 + Tantivy API の設計。 Tantivy Rustネイティブコード開発。 ffigen の活用。 +

Slide 31

Slide 31 text

31 基本から 基本が大切。Rust + FFIの基本から始めてみよう。 [package] name = "flutter_rust_lib" version = "0.1.0" edition = "2025" [dependencies] # Cの動的ライブラリを生成するための追加 [lib] crate-type = ["cdylib"] Rust Native

Slide 32

Slide 32 text

32 基本 Rust + FFIの基本から始めてみよう。 [package] name = "flutter_rust_lib" version = "0.1.0" edition = "2025" [dependencies] # Cの動的ライブラリを生成するための追加 [lib] crate-type = ["cdylib"] FFI(Foreign Function Interface)は、 Rustで書いたコードを他の言語から呼び出すための 仕組みである。 C言語互換を持つFFI用の動的ライ ブラリを生成する設定である。 Rust Native

Slide 33

Slide 33 text

33 基本 Rust + FFIの基本から始めてみよう。 Rust Native /// FFI: シンプルで高速な同期関数 /// 2つの整数を足して返します。 #[unsafe(no_mangle)] pub extern "C" fn add(a: i32, b: i32) -> i32 { a + b }

Slide 34

Slide 34 text

34 基本 Rust + FFIの基本から始めてみましょう。 Rust Native /// FFI: シンプルで高速な同期関数 /// 2つの整数を足して返します。 #[unsafe(no_mangle)] pub extern "C" fn add(a: i32, b: i32) -> i32 { a + b } ● no_mangle: この関数の名前は他の言語から参照できるよう、絶対に変更しないこと。 ● unsafe: この関数は外部と通信するため、Rustの安全規則が適用されない場合がある ことを理解し、責任を持って扱う。

Slide 35

Slide 35 text

35 基本 Rust + FFIの基本から始めてみましょう。 Rust Native /// FFI: シンプルで高速な同期関数 /// 2つの整数を足して返します。 #[unsafe(no_mangle)] pub extern "C" fn add(a: i32, b: i32) -> i32 { a + b } ● pub: Rustの関数・構造体は基本private。 ● extern "C" : C ABIに従ってコンパイル。

Slide 36

Slide 36 text

36 基本 Rust + FFIの基本から始めてみよう。 Rust Native #[unsafe(no_mangle)] pub extern "C" fn heavy_computation(name: *const c_char) -> *mut c_char { let name_str = unsafe { // code .. }; // CPUを多く使用する作業のシミュレーション thread::sleep(Duration::from_secs(3)); // 3秒待機 let result = format!("Hello, {}. Welcome from Rust!", name_str); // RustのStringをC文字列に変換してポインタを返す // .into_raw()はRustがメモリを解放しないように所有権を渡します。 CString::new(result).unwrap().into_raw() }

Slide 37

Slide 37 text

37 基本 Rust Native cargo build --release (macOS)

Slide 38

Slide 38 text

38 基本 Rust Native

Slide 39

Slide 39 text

39 基本 dart ffi class RustFFI { static final _lib = _loadLibrary(); static DynamicLibrary _loadLibrary() { if (Platform.isMacOS) { return DynamicLibrary.open('libflutter_rust_lib.dylib'); } throw UnsupportedError('Unsupported platform'); } }

Slide 40

Slide 40 text

40 基本 dart ffi static final Pointer Function(Pointer) _heavyComputationSync = _lib .lookup Function(Pointer)>>( 'heavy_computation', ) .asFunction(); #[unsafe(no_mangle)] pub extern "C" fn heavy_computation(name: *const c_char) -> *mut c_char {

Slide 41

Slide 41 text

41 基本 dart ffi String _heavyComputationIsolate(String name) { final nameC = name.toNativeUtf8(); final resultC = RustFFI._heavyComputationSync(nameC); final result = resultC.toDartString(); // code .. return result; } static Future heavyComputationAsync(String name) async { return compute(_heavyComputationIsolate, name); } ffi Lookup

Slide 42

Slide 42 text

42 基本 Flutter Future? _heavyTaskFuture; void _runHeavyTask() { setState(() { _heavyTaskFuture = RustFFI.heavyComputationAsync("Flutter"); }); } FutureBuilder( future: _heavyTaskFuture, builder: (context, snapshot) { if (_heavyTaskFuture == null) { // code.. } if (snapshot.connectionState == ConnectionState.waiting) { return const CircularProgressIndicator.adaptive(); } if (snapshot.hasError) { return Text('${snapshot.error}'); } if (snapshot.hasData) { return Text('Result:\n${snapshot.data}'); } return const SizedBox.shrink(); }, ),

Slide 43

Slide 43 text

43 Flutter https://blog.flutter.dev/whats-new-in-flutter-3-29-f90c380c2317

Slide 44

Slide 44 text

44 基本 Flutter /// RustFFI static String heavyComputation(String name) { return _heavyComputationIsolate(name); }

Slide 45

Slide 45 text

45 基本 Flutter Before After

Slide 46

Slide 46 text

Native API 設計 46 ● 初期化処理 → 関数名: tantivy_init ○ Index 設定 ○ Schema 設定 ○ reader 設定 ○ writer 設定 ● ドキュメント追加 → 関数名: tantivy_add_doc ○ writer.add_documentを使用してインデックスにドキュメントを追加可能 ● ドキュメント検索 → 関数名: tantivy_search ○ ユーザーの検索クエリを受け取り検索を実行 ○ 検索結果を返却 ○ 検索結果は n 件を JSON 文字列として処理 Flutter Tantivy 開発

Slide 47

Slide 47 text

ネイティブコード開発 47 [lib] crate-type = ["cdylib", "staticlib"] [dependencies] tantivy = "0.25.0" once_cell = "1.19" serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } thiserror = "2.0.17" [build-dependencies] cbindgen = "0.29.2" Rust コードを基に、C / C++ のヘッダーファイルを自動生成するツー ル コアになる Tantivy ライブラリ Flutter Tantivy 開発

Slide 48

Slide 48 text

48 #[no_mangle] pub extern "C" fn tantivy_init(dir_path: *const c_char) -> c_int { } #[no_mangle] pub extern "C" fn tantivy_add_doc(text_ptr: *const c_char) -> c_int { } #[no_mangle] pub extern "C" fn tantivy_search(query_ptr: *const c_char, top_k: c_int) -> *mut c_char { } Flutter Tantivy 開発 ネイティブコード開発

Slide 49

Slide 49 text

49 use std::env; use std::path::PathBuf; fn main() { let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); let output_file = PathBuf::from(&crate_dir).join("binding.h"); cbindgen::Builder::new() .with_crate(crate_dir) .with_language(cbindgen::Language::C) .generate() .expect("Unable to generate bindings") .write_to_file(output_file); } build.rs ファイルを作成後、 cargo build --release を実行すると、 binding.h ファイルが生成される。 Flutter Tantivy 開発 ネイティブコード開発

Slide 50

Slide 50 text

50 #include #include #include #include char *tantivy_last_error(void); void tantivy_free_str(char *s); int tantivy_init(const char *dir_path); int tantivy_add_doc(const char *text_ptr); char *tantivy_search(const char *query_ptr, int top_k); binding.h ビルド時に自動生成されたファイルを利用 して、その後 ffigenを活用し、コード自動 生成に利用 Flutter Tantivy 開発 ネイティブコード開発

Slide 51

Slide 51 text

ライブラリ ビルド 51 #!/bin/bash set -e cd rust_tantivy echo "🔧 Building for macOS..." cargo build --release echo "🔧 Building for Android..." if command -v cargo-ndk &> /dev/null; then cargo ndk -t arm64-v8a -t armeabi-v7a -o ../android/app/src/main/jniLibs build --release else echo "⚠ Skipping Android (cargo-ndk not installed)" fi make または sh スクリプトを使用し て、プラットフォーム別・アーキテクチャ 別のビルドファイルを生成 Android の場合、ビルドされた出力物 は以下のディレクトリに配置 Flutter Tantivy 開発

Slide 52

Slide 52 text

52 Flutter Tantivy 開発 ライブラリ ビルド

Slide 53

Slide 53 text

ffigenを使用、 FFI コード自動生成 53 dev_dependencies: flutter_test: sdk: flutter # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^6.0.0 ffigen: ^19.1.0 pubspec.yaml ファイル内の dev_dependencies セクションに ffigen を追加 Flutter Tantivy 開発

Slide 54

Slide 54 text

54 name: SearchFFI description: Bindings for search_ffi output: 'lib/ffi.dart' headers: entry-points: - 'rust_tantivy/binding.h' comments: style: any length: full preamble: | // ignore_for_file: camel_case_types, non_constant_identifier_names, unused_import コード生成の成果物の 場所およびファイル名 コード生成の基礎となる ヘッダーファイル Flutter Tantivy 開発 ffigenを使用、 FFI コード自動生成

Slide 55

Slide 55 text

55 > dart run ffigen --config ffigen.yaml Flutter Tantivy 開発 ffigenを使用、 FFI コード自動生成

Slide 56

Slide 56 text

ffi wrapper class 開発 56 class TantivySearch { late final SearchFFI _bindings; late final ffi.DynamicLibrary _dylib; TantivySearch() { _dylib = _loadLibrary(); _bindings = SearchFFI(_dylib); } } Flutter Tantivy 開発 自動生成されたFFIコードをそのまま使用するのではなく、 ラッパークラスを作成して利用す る。

Slide 57

Slide 57 text

ffi wrapper class 開発 Flutter Tantivy 開発 57 List> search(String query, {int topK = 10}) { final queryPtr = query.toNativeUtf8(); try { final resultPtr = _bindings.tantivy_search(queryPtr.cast(), topK); try { final jsonStr = resultPtr.cast().toDartString(); final results = jsonDecode(jsonStr) as List; return results.map((e) => e as Map).toList(); } finally { _bindings.tantivy_free_str(resultPtr); } } finally { malloc.free(queryPtr); } } FFI メソッド FFI Wrapper Method

Slide 58

Slide 58 text

58 Test Device Nothing 3A Offline avg 0.095ms (n=100)

Slide 59

Slide 59 text

● TantivyをFlutterに組み込み、改善点を確認。 ● 英語には対応できたが、日本語・韓国語・中国語のトークナイザー対応が課 題。 ● 形態素解析の追加と既存実装の調査が必要。 課題と改善点 59

Slide 60

Slide 60 text

● https://github.com/yiv/full_search ○ Tantivyをベースにした既存のパッケージがある。 ○ このパッケージでは中国語のトークナイザーに対応している。 ○ 保守性に課題があり、 ■ 最新のFlutterバージョンを使用した場合、 ビルドエラーが発生する問題が確認された。 ● 日本語・韓国語向けのトークナイザーに対応した既存のパッケージは見つからな かった。 ○ そのため、自分で実装することに。 Flutter パッケージはなかったのか? Flutter Tantivy 60

Slide 61

Slide 61 text

● Rustで作られた形態素解析ライブラリ。 ○ 本プロジェクトは kuromoji-rs からフォークされたもの。 ○ https://github.com/lindera/lindera ● Linderaは、インストールが容易で、さまざまなRustアプリケーション向けに簡潔なAPIを提 供するライブラリの構築を目指している。 ● lindera-sqlite, lindera-python, lindera-tantivy など。 ● Flutterで利用可能なパッケージは見つからなかった。 A multilingual morphological analysis library. Lindera 61

Slide 62

Slide 62 text

● Lindera-Tantivyは、Rust製の全文検索エンジンTantivyを他のアプリケーションで利用できるよう にしたライブラリである。 ● 特徴 ○ オフライン全文検索対応:ネットワーク接続がなくても高速に検索可能。 ○ 形態素解析対応:日本語などの複雑な言語のテキストも正確に分割・検索できる。 ○ Rust の高性能を活かす:Tantivy の高速検索性能を Flutter から利用可能。 ○ クロスプラットフォーム対応:モバイルアプリやデスクトップアプリでも使用可能。 ● Flutterで利用可能なパッケージは見つからなかった。 Lindera tokenizer for Tantivy. Lindera-Tantivy 62

Slide 63

Slide 63 text

経験から感じたこと 63 ● Rust ネイティブで実際に開発を行うと、プロセスが非常に複雑で煩雑になる。 ● プラットフォーム別・アーキテクチャ別のビルドに多くの時間を消費。 ● FFIコードとプラットフォーム別・アーキテクチャ別のビルドを自動で処 理した い。 開発前に

Slide 64

Slide 64 text

Flutter Rust Bridge 64 ● シンプル (Simplicity) ○ 簡単なセットアップ:1つのコマンドで既存のプロジェクトに導入できる。 ○ 自然なコーディング:DartとRustで普段通りにコードを書くだけで、複雑な型や非同期処理も簡単に関数連携が可能 である。 ● 強力な機能 (Powerfulness) ○ 多様な型をサポート:シリアライズ不可能な型や&mut参照など、Rustの高度な型をそのまま利用できる。 ○ 双方向・非同期通信:DartからRustを呼び出すだけでなく、RustからDartの関数を呼び出すことも可能である。ま た、async処理にも対応している。 ○ クロスプラットフォーム:iOS, Android, Windows, macOS, Linux, Webの主要プラットフォームをサポートしている。 ● 高い信頼性 (Reliability) ○ Flutter Favorite パッケージです。 ○ 堅牢なテスト: CIによるメモリ安全性や各プラットフォームでの動作が保証されています。 https://github.com/fzyzcjy/flutter_rust_bridge

Slide 65

Slide 65 text

Flutter Rust Bridge 65 ● Rust のセットアップ(ダウンロードとインストール) ○ flutter_rust_bridge_codegen → Code generator ○ > cargo install flutter_rust_bridge_codegen ● プロジェクトの作成( Flutter + Rust) ○ > flutter_rust_bridge_codegen create my_app プロジェクトを作成する

Slide 66

Slide 66 text

Flutter Rust Bridge 66 プロジェクト構造 Rustネイティブコード用フォルダ このパッケージはFlutterの FFI(Foreign Function Interface)プラグインであ る。 Flutterが各プラットフォーム (Android、iOSなど)向けに ビルドされる際、ネイティブ コード(この場合はRust)も 同時にビルドできるように統 合する仕組みを提供する。 flutter_rust_bridge_codegen というコード生成ツールが使 用する設定ファイルである。

Slide 67

Slide 67 text

Flutter Rust Bridge 67 プロジェクト構造 ● flutter_rust_bridge.yaml ○ rust_input: crate::api ■ Rustコードのうち、どの部分(この場合はapiモジュール)をDartに公開するかを指定す る。 ○ rust_root: rust/ ■ Cargo.tomlが存在するRustプロジェクトの場所を指定する。 ○ dart_output: lib/src/rust ■ 生成されたDartコードをどこに保存するかを指定する。

Slide 68

Slide 68 text

Flutter Rust Bridge 68 Rust Native Code #[flutter_rust_bridge::frb(sync)] pub fn greet(name: String) -> String { format!("Hello, {name}!") } マクロ (macro):Rust 関数を Dart から同期呼び出し 可能なコードを生成。

Slide 69

Slide 69 text

Flutter Rust Bridge 69 Rust Native Code pub fn async_greet(name: String) -> String { format!("Hello, {name}!") } > flutter_rust_bridge_codegen generate

Slide 70

Slide 70 text

Flutter Rust Bridge 70 Rust Native Code String greet({required String name}) => RustLib.instance.api.crateApiSimpleGreet(name: name); Future asyncGreet({required String name}) => RustLib.instance.api.crateApiSimpleAsyncGreet(name: name); 自動生成され た FFI コード 自動生成される FFI コードのラッパークラス。 各ネイティブ関数を使いやすくする。

Slide 71

Slide 71 text

Flutter Rust Bridge 71 ビルドメカニズム ● flutter runを実行すると、各プラットフォームのコード内にはライブラリファイルが存在しないのに、 なぜRustコードがビルドされ、実行できるのだろうか? ● 例えば、Androidではsrc/main/jniLibsにライブラリが必要だが、 存在しなくても正常に動作する。 ● それはなぜ? ○ どのような仕組みで可能になっているのでしょうか?

Slide 72

Slide 72 text

Flutter Rust Bridge 72 cargokitディレクトリ内の事前準 備済みのplugin.gradleファイル を、現在のビルドプロセスで実行 することを意味する。 plugin.gradleスクリプトが必要とする 設定値を渡す部分である。 ビルドメカニズム

Slide 73

Slide 73 text

Flutter Rust Bridge 73 def cargoOutputDir = "${project.buildDir}/jniLibs/${buildType}"; def jniLibs = project.android.sourceSets.maybeCreate(buildType).jniLibs; jniLibs.srcDir(new File(cargoOutputDir)) def task = project.tasks.create(taskName, CargoKitBuildTask.class) { buildMode = variant.buildType.name buildDir = cargoBuildDir outputDir = cargoOutputDir ndkVersion = plugin.project.android.ndkVersion sdkDirectory = plugin.project.android.sdkDirectory minSdkVersion = plugin.project.android.defaultConfig.minSdkVersion.apiLevel as int compileSdkVersion = plugin.project.android.compileSdkVersion.substring(8) as int targetPlatforms = platforms pluginFile = CargoKitPlugin.file } ビルドメカニズム `build/jniLibs/debug` 폴더도 네이티브 라이브러리 소스 폴더로 추가해서 사용해!" ● Rustビルドの成果物を保存する一時フォルダ のパスを指定する。 ● cargoOutputDirフォルダはjniLibsフォルダと同 様に扱われる。 build/jniLibs/debugフォルダもネイティブライブラリのソースフォルダと して追加して使用する。

Slide 74

Slide 74 text

Flutter Rust Bridge 74 abstract class CargoKitBuildTask extends DefaultTask { @TaskAction def build() { def executableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "run_build_tool.cmd" : "run_build_tool.sh" project.exec { executable path args "build-gradle" environment "CARGOKIT_ROOT_PROJECT_DIR", rootProjectDir // code ... environment "CARGOKIT_JAVA_HOME", System.properties['java.home'] } } } ビルドメカニズム

Slide 75

Slide 75 text

Flutter Rust Bridge 75 ビルドメカニズム

Slide 76

Slide 76 text

Flutter Lindera Tantivy 76 Crates 追加 [lib] crate-type = ["cdylib", "staticlib"] [dependencies] flutter_rust_bridge = "=2.11.1" tantivy = "0.25.0" lindera = "1.4.1" lindera-tantivy = { version = "1.1.1", features = [ "embedded-ipadic", "embedded-unidic", "embedded-ko-dic", "embedded-cc-cedict" ] } serde_json = "1.0"

Slide 77

Slide 77 text

Flutter Lindera Tantivy 77 Rust ネイティブ API の設計 ● ローカルディスクに保存 ○ 再実行時にデータが初期化されないように。 ○ 関数名 ■ initialize_search_index_with_path ● CRUD 操作 が可能であること。 ○ 文書の追加 ■ 関数名: add_document , add_documents ○ 検索 ■ 関数名: search_documents ○ 更新 ■ 関数名: update_document ○ 削除 ■ 関数名: delete_document, delete_documents

Slide 78

Slide 78 text

Flutter Lindera Tantivy 78 Rust Native Code #[derive(Clone, Debug)] pub enum DictionaryType { Korean, // ko-dic JapaneseIpadic, // ipadic JapaneseUnidic, // unidic Chinese, // cc-cedict } impl DictionaryType { fn to_embedded_path(&self) -> &'static str { match self { DictionaryType::Korean => "embedded://ko-dic", DictionaryType::JapaneseIpadic => "embedded://ipadic", DictionaryType::JapaneseUnidic => "embedded://unidic", DictionaryType::Chinese => "embedded://cc-cedict", } } fn to_tokenizer_name(&self) -> &'static str { match self { DictionaryType::Korean => "lang_ko", DictionaryType::JapaneseIpadic => "lang_ja_ipadic", DictionaryType::JapaneseUnidic => "lang_ja_unidic", DictionaryType::Chinese => "lang_zh", } } }

Slide 79

Slide 79 text

Flutter Lindera Tantivy 79 Rust Native Code #[flutter_rust_bridge::frb(sync)] pub fn initialize_search_index_with_path(dictionary_type: DictionaryType, index_path: String) -> Result { let tokenizer_name = dictionary_type.to_tokenizer_name(); index .tokenizers() .register("ngram_tokenizer", NgramTokenizer::new(2, 3, false).unwrap()); // Tokenizer with selected dictionary let mode = Mode::Normal; let dictionary = load_dictionary(dictionary_type.to_embedded_path()).map_err(|e| e.to_string())?; let user_dictionary = None; let segmenter = Segmenter::new(mode, dictionary, user_dictionary); let tokenizer = LinderaTokenizer::from_segmenter(segmenter); // register Lindera tokenizer index.tokenizers().register(tokenizer_name, tokenizer); }  初期化する。

Slide 80

Slide 80 text

Flutter Lindera Tantivy 80 #[flutter_rust_bridge::frb(sync)] pub fn search_documents(query_str: String, limit: usize) -> Result, String> { // create reader let reader = search_index.index.reader().map_err(|e| e.to_string())?; // create query parser let query_parser = QueryParser::for_index( &search_index.index, vec![title, body, title_ngram, body_ngram], ); // parse query let query = query_parser .parse_query(&query_str) .map_err(|e| e.to_string())?; // create searcher let searcher = reader.searcher(); // search let top_docs = searcher .search(&query, &TopDocs::with_limit(limit)) .map_err(|e| e.to_string())?; } 検索する。 QueryParserを設定する。 title, body, title_ngram, body_ngram

Slide 81

Slide 81 text

Flutter Lindera Tantivy 81 Flutter Code List searchDocuments({ required String queryStr, required BigInt limit, }) => RustLib.instance.api.crateApiSearchSearchDocuments( queryStr: queryStr, limit: limit, ); flutter_rust_bridgeによって 自動生成されたコード。 flutter_rust_bridgeによって 自動生成されたFFI Function。

Slide 82

Slide 82 text

Flutter Lindera Tantivy 82 Flutter Code class SearchPage extends ConsumerStatefulWidget { const SearchPage({super.key}); @override ConsumerState createState() => _SearchPageState(); } class _SearchPageState extends ConsumerState { Future _performSearch() async { final results = searchDocuments( queryStr: _searchController.text.trim(), limit: BigInt.from(10), ); } } UIで使用するコールバックメ ソッド内で利用する。

Slide 83

Slide 83 text

Flutter Lindera Tantivy 83 Flutter Code TextField( controller: _searchController, onSubmitted: (value) => _performSearch(), ), ElevatedButton.icon( onPressed: _isIndexInitialized && !_isLoading ? _performSearch : null, icon: const Icon(Icons.search), label: Text(l10n.search), ),

Slide 84

Slide 84 text

Flutter Lindera Tantivy 84 Flutter プラグイン作成 flutter_rust_bridge_codegen create my_package --template plugin プロジェクト名の入力(設定) プラグインテンプレートから生成 ライブラリビルド 処理に関する設定 Rust Native Code (APIs)

Slide 85

Slide 85 text

Flutter Lindera Tantivy 85 Flutter プラグインをプラットフォーム別に実行する iOS macOS Android

Slide 86

Slide 86 text

86 Benchmark ベンチマーク macOS 100 vs 1000

Slide 87

Slide 87 text

Test Device : Nothing 3A Mem: 12G 87 avg 128ms (n=100)

Slide 88

Slide 88 text

Flutter Lindera Tantivy 88 https://pub.dev/packages/flutter_lindera_tantivy

Slide 89

Slide 89 text

● オフライン状態でオンデバイス AI を実装するために、追加機能の開発が必要であった。 ● オフラインで動作可能な検索エンジンの実装を追加で実施。 ○ 知識ベース検索の性能向上とコンテキスト改善。 ● CJK に対応可能なFlutter パッケージが存在しなかったため、独自に開発。 ● Rust コードを Flutter で使用する方法を検討。 ○ Rust Native + FFIの基本。 ○ Tantivy を Flutterで使用する方法。(Rust Native + ffigen + FFI ) ○ Lindera + Tantivyを Flutter アプリで使用する方法。(flutter_rust_bridge) ● flutter_lindera_tantivy, flutter_tantivy 開発。 Summary 89

Slide 90

Slide 90 text

● Flutter × Rust FFI の基本 ○ https://github.com/JAICHANGPARK/flutter_rust_ffi_basic ● Tantivy: Flutter × Rust: ffigen、FFI ○ https://github.com/JAICHANGPARK/flutter_tantivy_native ● Lindera-Tantivy: flutter_rust_bridge: ○ https://github.com/JAICHANGPARK/flutter_lindera_tantivy ● flutter_tantivy ○ https://pub.dev/packages/flutter_tantivy ● flutter_lindera_tantivy ○ https://pub.dev/packages/flutter_lindera_tantivy Materials flutter_lindera_tantivy 90

Slide 91

Slide 91 text

Thanks オフライン対応!Flutterアプリに全文検索エンジンを実装する Dream Walker GDE Dart-Flutter @jaichangpark 91