model2vec 2.0.3
model2vec: ^2.0.3 copied to clipboard
On-device Model2Vec text embeddings for Dart & Flutter — a self-contained Rust core via FFI and Native Assets. Fast, local, static, minimal memory.
2.0.3 #
Fixes the other half of 2.0.2: the hook stopped declaring the build tree as an input, but still BUILT into it — and the build tree is in the pub cache, which every project on the machine shares.
-
Cargo now builds into the directory the invoker hands out (
out_dir_shared), passed as--target-dir, instead of defaulting to<crate>/native/target. That directory is unique per hook per project and the runner serialises concurrent invocations into it, which is what it is documented for: "shared output and intermediate artifacts".The old default put one cargo build tree under
~/.pub-cache/hosted/pub.dev/model2vec-*/native/targetfor every consumer at once. The hook runner tracks the artifact it was handed, and macOS stamps a freshLC_UUIDinto every link, so a relink is a content change even when no source moved. Two consumers building under different configs —dart testruns withlinking_enabled: falseanddart buildwithtrue, and a second checkout or a Flutter workspace counts too — therefore invalidated each other's hook cache merely by building.What that looked like downstream: the runner deletes
output.jsonbefore re-running a hook, so whatever a paralleldart testhad already scheduled failed withNo asset with id 'package:model2vec/model2vec.so'. The diagnosis is hard because the failure names neither this package nor the build that caused it, and re-running after a warm-up makes it disappear — it had been living in a dependent repository's contributor guide as "run this command first when the suite goes red", which is a workaround with a documentation entry rather than a fix.Writing into the pub cache at all is the underlying mistake; a package directory there is shared, and on some setups read-only.
-
test/build_hook_test.dartpins it: the registered asset must live under the invoker'sout_dir_shared, and no path component may benative. Verified discriminating — with the previous line restored it fails naming the pub-cache path it built into.
Also a performance pass over the native core. Embedding output is bit-identical up to float rounding (verified ≤ 1.5e-7 against the previous code on a real model); the pooling semantics are now pinned by 12 Rust unit tests.
-
Model loading got measurably faster and lighter. The unk-token id is read straight off the tokenizer's model instead of serializing the entire tokenizer — vocabulary included — to a JSON tree to look up one field, and the weights file is memory-mapped instead of read into RAM, so parsing no longer holds both the raw file and the decoded f32 table at once (for
potion-multilingual-128Mthat second copy alone was ~250 MB). Warm-cache load ofpotion-base-8M: 31 ms → 21 ms;potion-base-32M: 83 ms → 52 ms. -
Batch pooling now runs in parallel (rayon, already in the dependency tree via
tokenizers): each sentence pools into its own disjoint chunk of the one flat output buffer. Single-sentence calls keep the sequential path and skip the thread pool. On short-text batches the gain is invisible — tokenization dominates — it shows on long texts and grows with embedding dimension. -
Assorted per-call overhead removed from the encode path: sentences are no longer copied into fresh
Strings before tokenization (encode_batch_fasttakes&str), token ids are pooled straight off the encoding instead of being copied to aVecfor unk-filtering and truncation, and the final mean-and-normalize is one multiply pass instead of two division passes. -
Rust dependencies raised to current:
safetensors0.7 → 0.8 (no API change for our usage),ureq3.4,rayon1.12, plusanyhow/serde_jsonpatch bumps.hf-hubdeliberately stays on 0.5, the lastureq-based line: 1.0 is a reqwest/tokio redesign with a mandatoryhf-xetdependency that grows the dependency tree from 200 to ~350 crates, and itsrustls-tlsflavour pullsaws-lc-sys, which needscmakeon every machine that builds the crate — this package builds on the consumer's machine via the native-assets hook, so that would become an install requirement for every user of the package (thenative-tlsflavour trades that for OpenSSL headers on Linux).
2.0.2 #
Fixes a build hook that could never be cached, so every build of a dependent package paid to rebuild this crate.
- The hook no longer declares its own build output as its input. It looked
for cargo's dep-info beside the artifact as
'$binaryPath.d'—libm2v_ffi.dylib.d. Cargo writes one dep-info per target, named after the target: a crate built as["staticlib", "cdylib"]produceslibm2v_ffi.a,libm2v_ffi.dyliband a singlelibm2v_ffi.d. The requested name is written on no platform, so the parse returned nothing every time and the fallback —output.dependencies.add(.directory(nativeDir))— was not a fallback but the only path ever taken.native/contains cargo's owntarget/, so the hook declared as its input a tree it rewrites on every run. Anything that writes there — a second consumer building the same crate — marks the hook dirty, and the runner reportsFile modified during build. Build must be rerun.and invokes it again. Measured on this repository againsthooks 2.1.0, a file written intotarget/without touchingsrc/: 1 re-run and 1.55 s before, 0 and 0.85 s after. The re-run is cheap only while cargo itself has nothing to do; when the re-run coincides with a real rebuild the cost is the rebuild, and in a dependent workspace that measured 68 s against 3.5 s settled, with the spawning tests of a paralleldart testtiming out. See dart-lang/native#1998 for the same shape reported againstnative_toolchain_c, and dart.dev/tools/hooks: dependencies are the inputs a hook reads, never the outputs it produces. - The dep-info parser no longer breaks on Windows paths. It split the file
on the first
':', which on Windows is the drive letter — soC:\out\m2v_ffi.dll: C:\src\lib.rsyielded\out\m2v_ffi.dllas a "dependency": a path that exists nowhere, with the hook still reporting success. That bug was dormant only because the filename above meant the parser was never reached, so fixing the filename alone would have shipped it. The separator is now the first": ". While there, the parser reads every artifact line rather than only the first, and honours cargo's\escape so a path containing a space stays one path. - When the dep-info really is missing, the fallback now names the crate's
actual inputs —
native/src/,Cargo.lock,rust-toolchain.tomland the manifest — instead of the directory that holds the build tree.
test/build_hook_test.dart covers both: an integration test reads the
dependencies the hook actually writes for the host, and unit tests pin the
Windows, multi-line and escaped-space cases that a run on one platform cannot
produce. No API change; this is a build-time fix only.
2.0.1 #
Fixes a build hook that broke every Flutter app depending on this package.
flutter runno longer fails with "Building native assets failed". The hook readinput.config.codewithout first askinginput.config.buildCodeAssets. That is fine fordart build, which only ever invokes hooks with code assets requested — but Flutter also runs them from its asset-bundling pass (buildCodeAssets: null), and with data assets behind a feature flag that pass arrives withbuild_asset_typesempty. Reading.codethere throwsStateError, the hook exits 255, and Flutter reports the whole app's native-asset build as failed, including the pass that would have built the library. The hook now returns early when no code assets are asked for.test/build_hook_test.dartpins the empty case, which no localdartcommand reaches on its own.
2.0.0 #
Major release reworking the FFI boundary and public surface for testability and correctness. This release is breaking — see migration below.
Breaking changes:
- Static API.
Model2Vecis now a stateless namespace of static methods.Model2Vec.instance, theModel2Vec(DynamicLibrary)constructor andModel2Vec.boot(...)were removed — the native library is resolved automatically through Native Assets (@Nativecode assets). ReplaceModel2Vec.instance.foo(...)withModel2Vec.foo(...). - Recommended models.
getRecommendedModels()(returningList<Map<String, dynamic>>) is replaced by the typed constantModel2Vec.recommendedModels(List<RecommendedModel>). - Typed errors.
Model2VecExceptionnow carries aModel2VecErrorKind kind; its constructor is(kind, message, [code])and thefromCodefactory is replaced byfromNative(code, message). Native failures surface the message produced by the Rust layer, each with an exhaustively-switchablekind. - Lifecycle naming. The
initEmbedder*methods are renamed toloadModel*, pairingloadModel⇄unloadModelover the model.initEmbedder,initEmbedderAdvanced,initEmbedderFromBytesand their async forms are removed.Model2VecUtils.similaritySearch/similaritySearchWithThresholdare removed in favour ofsimilaritySearchWithScores(read.index). - Batch signature.
generateBatchEmbeddingsno longer takesbatchSize(its signature is now(List<String> texts, {int maxLength})). The native layer batches internally;batchSizeremains only ongenerateEmbeddingStream, which still controls its per-batch size.
Improvements:
- Native memory safety. The
generate_*FFI functions now allocate their output inside the native call (returned as a pointer the caller frees), removing a dimension/model-switch race that could overflow the output buffer. Every native entry point is wrapped incatch_unwind, so a panic (including from a malformed model) surfaces as a typed error instead of undefined behaviour. - Windows ABI fix. FFI length parameters use
size_t(wasunsigned long, 32-bit on 64-bit Windows and mismatched against Rust'susize). - Streaming rework.
generateEmbeddingStreamis rebuilt on small, tested modules — a batching transformer, a transport-agnostic worker protocol, and a worker isolate. Worker errors cross the isolate boundary as typedModel2VecExceptions (kind + code preserved) rather than stringified errors.
New capabilities:
- Local vector index.
EmbeddingIndex— store embeddings by id, thensearchthe nearest by cosine similarity. Optional int8-quantized storage (~4x less memory) and binarytoBytes/fromBytespersistence. Turns the package into a local retrieval engine for RAG. - RAG pipeline helpers.
chunkText(overlapping character chunker),Model2VecUtils.similaritySearchWithScores(index + score), andModel2VecUtils.maximalMarginalRelevance(MMR reranking for diverse results). - Lifecycle & DX.
Model2Vec.isInitialized(non-throwing check),Model2Vec.unloadModel()(free the native model),Model2Vec.modelInfo(all metadata in oneModelInfo), andModel2VecUtils.dequantizeInt8(the inverse ofquantizeToInt8). - Load progress.
Model2Vec.loadModelWithProgress()loads on a background isolate and returns aStream<LoadProgress>reporting the HF weights download (bytesDownloaded/totalBytes/fraction) plus a coarseLoadPhase(resolving → downloading → parsing → done). A cached model or local path streams straight todone. - Parallel worker pool.
EmbeddingPoolfans batches across N worker isolates to embed concurrently across CPU cores.
Migration:
| 1.x | 2.0.0 |
|---|---|
Model2Vec.instance.generateEmbedding(t) |
Model2Vec.generateEmbedding(t) |
Model2Vec.boot(lib) / Model2Vec(lib) |
removed — resolution is automatic |
Model2Vec.instance.getRecommendedModels() |
Model2Vec.recommendedModels (typed) |
Model2Vec.instance.initEmbedder(path) |
Model2Vec.loadModel(path) |
Model2VecUtils.similaritySearch(q, c) |
similaritySearchWithScores(q, c).map((r) => r.index) |
catch (e) { e.code } |
still works; add e.kind for exhaustive handling |
1.2.0 #
- Lowered minimum Dart SDK requirement to
3.10.0to support a wider range of environments.
1.1.0 #
New Features:
-
getRecommendedModels()no longer calls FFI — now returns a hardcoded list of 7 models -
Removed
get_model_listfrom FFI bindings (Rust, Dart,.h) -
generateEmbedding()now acceptsmaxLengthparameter — signature changed -
generateBatchEmbeddings()now acceptsmaxLengthandbatchSizeparameters — signature changed -
Streaming API —
generateEmbeddingStream()for processing large datasets with batching and optional worker isolate -
Async API —
generateEmbeddingAsync()andgenerateBatchEmbeddingsAsync()withmaxLength/batchSizesupport -
Advanced init —
initEmbedderAdvanced()withhfToken,cacheDirectory,normalize,subfolder -
In-memory init —
initEmbedderFromBytes()for loading models from raw bytes -
boot()— manual initialization with a customDynamicLibrary -
isNormalized— getter for L2-normalization check -
medianTokenLength— getter for median token length -
maxLength— token truncation parameter forgenerateEmbedding() -
batchSize— internal batching control forgenerateBatchEmbeddings() -
Model2VecUtils— vector math:cosineSimilarity,dotProduct,euclideanDistance,similaritySearch,similaritySearchWithThreshold,cosineDistance,normalize,meanPooling,quantizeToInt8,toBase64,fromBase64,pairwiseSimilarity
Improvements:
- Streaming API Performance:
generateEmbeddingStream()now utilizes a single long-lived worker isolate instead of spawning one per batch, dramatically reducing IPC and memory overhead for large datasets. - Inter-Isolate Communication: Switched from
Map<String, dynamic>to Dart 3 Records for significantly faster and strictly typed isolate communication. - FFI Optimization:
generateEmbedding()in Rust rewritten to avoid array pointer allocations and correctly respectmax_length. - Refactored
quantizeToInt8()to use Dart's native.clamp(). - Added clear documentation for zero-vector handling in
cosineSimilarityandnormalize. - Added documentation warning about IPC overhead in
generateEmbeddingStreamfor CLI/Server applications. - Better error messages when loading the native library fails, explaining possible missing Rust builds.
- Cleaned up FFI bindings: removed dead
get_model_listsymbol from.hand bindings. generate_embeddingin Rust now returns-5on empty results instead of silently corrupting data.generate_batch_embeddings_advancedvalidates result count matches input count.- Benchmark updated to run all 5 models.
- README fully rewritten with API reference and accurate model dimensions.
1.0.0 #
- Initial version.