Skip to content

V 2.0.0#3

Merged
pro100andrey merged 20 commits into
mainfrom
v2-deepening
Jul 7, 2026
Merged

V 2.0.0#3
pro100andrey merged 20 commits into
mainfrom
v2-deepening

Conversation

@pro100andrey

Copy link
Copy Markdown
Owner

No description provided.

…rors

BREAKING CHANGE: Model2Vec is now a stateless namespace of static methods.

Native library seam (Candidates 1 & 4)
- Switch ffigen to @Native code-assets mode (assetId
  package:model2vec/model2vec.so); the SDK resolver — fed by the build hook —
  now locates the native library. Delete _resolveLibPath, boot(), the singleton
  instance, both DynamicLibrary constructors, and the _cachedDimension cache.
  Fixes Flutter bundling and the boot()-across-isolates bug.

Native ABI and error seam (Candidate 3)
- Rust now owns output allocation: generate_* return ptr+dim(+count), freed via
  free_floats — removing the dim/model-switch buffer-overflow race by
  construction. Every fallible function returns a stable code plus a
  char** out_error, and every #[no_mangle] is wrapped in catch_unwind so a
  panic (including from a malformed model) becomes a typed error instead of UB.
  Length params use size_t (fixes a 64-bit-Windows usize mismatch). Dart
  surfaces a single Model2VecException with an exhaustive Model2VecErrorKind.

Streaming worker (Candidate 2)
- Rebuild generateEmbeddingStream on small, tested modules: batched() (pure
  transformer), Channel (isolate-port seam + in-memory fake), WorkerProtocol
  (serialized-queue state machine) and a private EmbeddingWorker. Worker errors
  cross the isolate boundary as typed exceptions; a dying worker fails pending
  requests via onExit instead of hanging.

Catalog (Candidate 5)
- getRecommendedModels() -> typed Model2Vec.recommendedModels constant.

Tests
- 60 tests. The model-backed suite is migrated to the static API; new
  no-model/no-network suites cover batching, the worker protocol (fake channel),
  the worker over a real isolate (incl. death/hang robustness) and native error
  paths. dart_test.yaml pins concurrency: 1 because the native model is a single
  process-global.

Full migration guide is in CHANGELOG.md.
Adds the retrieval/RAG layer on top of the embedding engine, plus lifecycle
and parallelism helpers.

- EmbeddingIndex: in-memory vector store (add/search/remove) with cosine
  ranking, optional int8-quantized storage (~4x less memory), and binary
  toBytes/fromBytes persistence — a local retrieval engine for RAG.
- RAG helpers: chunkText (overlapping character chunker, maxChars-bounded),
  Model2VecUtils.similaritySearchWithScores (index + score), and
  maximalMarginalRelevance (MMR reranking, O(k*n*dim)).
- Lifecycle & DX: Model2Vec.isInitialized (non-throwing), unloadModel()
  (frees native memory via a new free_embedder FFI), modelInfo (metadata in
  one ModelInfo), Model2VecUtils.dequantizeInt8.
- EmbeddingPool: N worker isolates embedding concurrently (the Rust RwLock
  allows concurrent readers), with order-preserving embedBatches and
  leak-safe startup.
- Native: is_model_loaded + free_embedder FFI functions; bindings regenerated.

92 tests, including no-model/no-network suites for the index (persistence,
quantization, corrupt-blob handling), the chunker, MMR, and the pool (fake
entry point). README recipes and CHANGELOG updated.
Loading is the heaviest, most download-prone step and had no async door,
while microsecond generate* calls did — inverted from cost. On Flutter the
synchronous initEmbedder freezes the UI during the first model download.

Add initEmbedderAsync and initEmbedderAdvancedAsync, which load on a
background isolate via Isolate.run. The native model is a process-global
(the worker pool already relies on this), so the model loaded off-thread is
visible on every isolate, including the one that awaited the call.
The index stored id->vector only, so every retrieval caller kept a parallel
id->text map and re-joined it on each hit (the RAG example and README recipe
both did exactly this).

Add an optional String payload per entry: add(id, vector, {payload}),
surfaced as SearchResult.payload and persisted through toBytes. The blob
format bumps to v2 (a per-entry payload section with a null sentinel distinct
from an empty string); v1 blobs still decode. payload is a String (not a
generic) so the index stays a pure, serializable data structure — hold JSON
for structured metadata. The RAG example drops its parallel passages map.
Model2VecUtils had three near-identical searches returning three shapes:
similaritySearch and similaritySearchWithThreshold handed back bare List<int>
indices (forcing callers to re-associate), while similaritySearchWithScores
returned (index, score) records.

Make similaritySearchWithScores the single scored search — it now takes an
optional score threshold alongside topK (idiomatic topK + min-score), so it
subsumes both query modes. The two bare-index methods become @deprecated
one-line delegates over it, so existing callers keep working for one release.
Removes the duplicated scoring loops and the private _IndexedSimilarity type;
migrates the example and README to the scored shape.
The public lifecycle verbs mixed two nouns — initEmbedder* to load vs
unloadModel/modelInfo — and 'Embedder' is a term CONTEXT.md explicitly says to
avoid; the load/unload inverse was hidden in the names.

Rename the five loaders to loadModel, loadModelAdvanced, loadModelFromBytes,
loadModelAsync and loadModelAdvancedAsync, so the surface reads loadModel <->
unloadModel with one noun (Model). The old initEmbedder* names stay as
@deprecated delegating aliases for one release (removed in 3.0.0). The native
init_embedder entry points stay internal. Adds a Load/Unload term to CONTEXT.md
and migrates the example, benchmark, tests and README.
generateBatchEmbeddings and its async form exposed a batchSize parameter that
the README itself described as 'internal chunks sent to the FFI layer' — a
native throughput detail the caller has no basis to choose, and one that never
affects the result.

Remove it from both signatures and pass a fixed internal chunk size (1024, the
former default, so results are unchanged) to the native call. The stream's
batchSize stays: there it is a meaningful, user-facing chunk size. Internal
callers stop threading batch.length through.
Loading a model is a single blocking FFI call, and the first fetch pulls
tens to hundreds of MB of weights from Hugging Face with no way to observe
it — a Flutter app can only show an indeterminate spinner during the
heaviest step of all.

Add loadModelWithProgress, which loads on a background isolate and returns a
Stream<LoadProgress> reporting the weights download (bytesDownloaded /
totalBytes / fraction) and a coarse phase: resolving -> downloading ->
parsing -> done. The Rust side routes the weights fetch through hf-hub's
download_with_progress into process-global atomics (a cache hit skips it
entirely), read over FFI via new get_load_progress / reset_load_progress.
A cached model or local path streams straight to done; the stream always
ends on done and surfaces a load failure as an error event. Adds an
integration test that downloads into a fresh cache, plus README and
CHANGELOG entries.

Also reorganize example/ so each file teaches one story: main.dart is now a
small quickstart (load -> embed -> cosine similarity), scaling_example.dart
covers the at-scale patterns (progress-bar load, batch, EmbeddingPool,
streaming), and rag_example.dart keeps the retrieval pipeline. Adds
example/README.md indexing the three.
@pro100andrey pro100andrey merged commit d1857cb into main Jul 7, 2026
3 checks passed
@pro100andrey pro100andrey deleted the v2-deepening branch July 7, 2026 13:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant