simstring_rust
A native Rust implementation of the CPMerge algorithm, designed for approximate string matching. This crate is particularly useful for natural language processing tasks that require the retrieval of strings/texts from very large corpora (big amounts of texts). Currently, this crate supports both character and word-based N-grams feature generation, with plans to allow custom user-defined feature generation methods.
Features
- ✅ Fast algorithm for string matching
- ✅ 100% exact retrieval
- ✅ Support for Unicode
- Support for building databases directly from text files
- Mecab-based tokenizer support
Supported String Similarity Measures
- ✅ Dice coefficient
- ✅ Jaccard coefficient
- ✅ Cosine coefficient
- ✅ Overlap coefficient
- ✅ Exact match
Installation
Requires Rust 1.89 or newer (edition 2024). Python bindings support Python 3.10+. See Rust API migration for the read-only query engine's trait changes.
Add simstring_rust to your Cargo.toml:
[dependencies]
simstring_rust = "0.3.0" # change version accordingly
For the latest features, you can add the master branch by specifying the Git repository:
[dependencies]
simstring_rust = { git = "https://github.com/PyDataBlog/simstring_rs.git", branch = "main" }
Note: Using the master branch may include experimental features and potential breakages. Use with caution!
To revert to a stable version, ensure your Cargo.toml specifies a specific version number instead of the Git repository.
Usage
Here is a basic example of how to use simstring_rs in your Rust project:
use simstring_rust::database::HashDb;
use simstring_rust::extractors::CharacterNgrams;
use simstring_rust::measures::Cosine;
use simstring_rust::Searcher;
use std::sync::Arc;
fn main() {
// 1. Setup the database
let feature_extractor = Arc::new(CharacterNgrams::new(2, "$"));
let mut db = HashDb::new(feature_extractor);
// 2. Index some strings
db.insert("hello".to_string());
db.insert("help".to_string());
db.insert("halo".to_string());
db.insert("world".to_string());
// 3. Search for strings
let measure = Cosine;
let searcher = Searcher::new(&db, measure);
let query = "hell";
let alpha = 0.5;
if let Ok(results) = searcher.ranked_search(query, alpha) {
println!("Found {} results for query '{}'", results.len(), query);
for (item, score) in results {
println!("- Match: '{}', Score: {:.4}", item, score);
}
}
}
Saving and loading databases
Snapshots store the full index and its CharacterNgrams or WordNgrams
configuration. Loading preserves insertion order and duplicates, and returns a
mutable database without extracting corpus features again.
use simstring_rust::{CharacterNgrams, HashDb};
use std::sync::Arc;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut db = HashDb::new(Arc::new(CharacterNgrams::new(2, "$")));
db.insert("hello".into());
db.save_to_file(".", "names.ssdb")?;
let mut restored = HashDb::load_from_file(".", "names.ssdb")?;
restored.insert("help".into());
Ok(())
}
from pathlib import Path
from simstring_rust.database import HashDb
from simstring_rust.extractors import CharacterNgrams
db = HashDb(CharacterNgrams(2, "$"))
db.insert("hello")
db.save_to_file(Path("."), "names.ssdb")
restored = HashDb.load_from_file(Path("."), "names.ssdb")
restored.insert("help")
The directory must exist and file_name must be a single filename. Saving
atomically replaces an existing file after a successful write. Rust returns
PersistenceError; Python raises OSError for filesystem errors, ValueError
for invalid snapshots or filenames, and TypeError for unsupported extractors.
The v1 format uses a versioned header, CRC32 checksum, and Postcard payload with portable integer IDs. Rust and Python share the format across supported platforms. Unsupported format versions are rejected explicitly; v1 readers and extractor semantics are covered by shared compatibility fixtures. Loading temporarily holds the encoded payload and decoded data in memory.
Feature IDs use 32-bit Spur keys in memory and zero-based u32 values in v1
snapshots. The maximum feature ID is u32::MAX - 1, allowing up to
4,294,967,295 distinct interned features. This limit applies to the shared
feature dictionary. String IDs and explicit sizes/counts use u64 in snapshots
and are converted to native usize values with capacity checks when loaded.
Custom Rust extractors default to unsupported persistence. A wrapper that
exactly matches a built-in extractor can delegate persistence_config();
arbitrary custom behavior and Python CustomExtractor objects cannot be saved.
See examples/persistence.rs and examples/python_persistence.py for runnable
examples. Measure indexing, saving, and loading with
cargo bench --bench persistence -- unigrams (medians of seven runs; warm file cache).
Releasing
Release Please manages stable
releases from main. Its Rust strategy updates Cargo.toml, Cargo.lock,
CHANGELOG.md, and .release-please-manifest.json in a release PR. Maturin derives
the Python package version from Cargo, so crates.io and PyPI share one version.
Release process
- Squash-merge changes into
mainusing Conventional Commit PR titles. After CI passes, Release Please creates or updates the pending release PR. - Review the proposed version and release notes after the release PR passes CI. Merging this PR is the approval to release.
- After CI passes, Release Please creates the
vMAJOR.MINOR.PATCHtag and GitHub release. The shared build/test workflow runs against that exact release commit, producing separately named release wheel artifacts. - The crate is verified and published to crates.io, followed by the tested release wheels to PyPI. TestPyPI receives the same wheels independently of production publishing. Publication is automatic after the release tests pass.
Ordinary merges update the proposed release and run CI. Registry publishing is
driven by Release Please's release_created output.
The configuration and manifest start from
v0.3.5-beta.1. The prerelease versioning strategy with prerelease: false
promotes beta/RC versions to stable versions. With the pending feature changes,
the first release is 0.4.0. Subsequent versions follow Conventional Commits:
fix and perf bump the patch, feat bumps the minor, and breaking changes bump
the minor while the project is below 1.0.0. Include the
Rust API migration when reviewing the first release notes.
Repository setup
- Install a release GitHub App on this repository with Contents, Issues,
and Pull requests read/write permissions. Add its App ID and private key as
repository secrets
RELEASE_APP_IDandRELEASE_APP_PRIVATE_KEY. The App token allows CI to run on the release PR that it creates. - Configure
mainto require the CI and PR title checks before merging. Enable squash merging and use the PR title as the squash commit title so Release Please can determine the version from the merged history. - Configure trusted publishers for
PyDataBlog/simstring_rs, workflowCI.yml, and environmenttestpypion TestPyPI /pypion PyPI. These environments use automatic publishing, with no required reviewers. - Provide
CARGO_REGISTRY_TOKENwith permission to publishsimstring_rust.
Retrying publication
Use Re-run failed jobs on the original release workflow run. This reuses Release Please's outputs and the successful build artifacts. Already-published crate versions and wheel files are skipped. Check the publishing jobs to confirm that the packages are available in their registries.
Rust API migration
Version 0.4.0 introduces breaking changes for custom Rust trait implementations.
The read-only query engine preserves the usual HashDb and Searcher calls,
as well as Python method signatures. Custom Rust implementations need these updates:
- FeatureExtractor: implement
visit_features(&self, text: &str, visitor: &mut dyn FnMut(&str)), emitting occurrence-qualified features. The defaultfeaturesmethod interns, sorts, and deduplicates them for insertion; search visits them without interning. - Database:
interner()returns&Rodeorather than a lockedArc.lookup_strings()returns sorted, uniqueOption<&[StringId]>postings, andget_features()returnsOption<&[Spur]>. Implementfeature_sizes()as a sorted, unique slice of populated sizes. Each ID belongs to exactly one size bucket; stored features must be sorted and unique, using the same interner. Optionally overridevisit_postings()to enumerate one feature's postings across a size range directly; the default callslookup_strings()once per size. - Measure: implement
similarity_from_counts(x_len, y_len, common) -> f64. The defaultsimilarityintersects sorted feature sets and delegates to it. Search uses counts directly, so this method must agree with the pruning bounds.
Unknown query features still contribute to similarity denominators. Duplicate insertions remain distinct results, and queries with no features return no results. Both search methods now apply exact score filtering at threshold boundaries.
Search performance
Most search time goes to gathering postings and counting overlaps.
cargo bench --bench micro measures end-to-end search (100 queries over
benches/data/company_names.txt, Cosine, minimum of 30 runs). Measured on one x86_64
machine, before and after the ScanCount and per-feature posting index changes:
| n-gram | alpha | before | now |
|---|---|---|---|
| 2 | 0.6 | 5.22 ms | 4.14 ms |
| 2 | 0.9 | 0.55 ms | 0.47 ms |
| 3 | 0.6 | 2.93 ms | 1.66 ms |
| 3 | 0.9 | 0.47 ms | 0.34 ms |
| 4 | 0.6 | 2.28 ms | 1.07 ms |
| 4 | 0.9 | 0.46 ms | 0.32 ms |
Contributing
Contributions are welcome! Please open an issue or submit a pull request on GitHub.
Use Conventional Commits for PR titles,
for example fix(search): handle empty queries or feat(bindings): add an extractor.
Mark breaking changes with !, such as refactor(api)!: change the Database trait,
and describe the migration in the PR. PR titles are checked automatically and
become the squash commit titles used by Release Please.
License
This project is licensed under the MIT License.
Benchmarks
The benches/run_benches.py harness compares several language bindings (Rust, Python, Julia, Ruby, C++).
The CI job summary and benchmark PR comment also report Rust and Python database build,
save, and load medians, snapshot sizes, and sample counts for both corpora with
2-, 3-, and 4-grams. Run cargo bench --bench persistence -- --json (Rust) or
python benches/bench_persistence.py (Python bindings) for the persistence
measurements in machine-readable form.
CI also checks insert and unranked Cosine search against the latest stable
published crate, rebuilding release and PR sources with matching settings for
Rust and Python. It uses company names, character/word 2–4 grams, and thresholds
0.6/0.9. After three paired runs, cases slower by more than 5% are rechecked:
confirmed >5% warns and confirmed >10% fails. The median and every recheck pair
must exceed the threshold. Correctness mismatches and benchmark errors fail
immediately. Raw samples and build metadata are uploaded as the
release-regression artifact, with results in the existing benchmark comment.
Run .venv/bin/python benches/check_release.py --baseline 0.5.0 locally; Maturin
must be installed, and each run needs a fresh --output directory.
git,autoconf,automake,libtool,make,python,uvand a C++ compiler (g++) to build the C++ CLI.
The C++ sources are cloned into benches/.simstring_cpp/ and a local copy of the simstring binary is installed
under that directory. If you need to rebuild from scratch, remove benches/.simstring_cpp/ before re-running the benchmark suite.
Acknowledgements
Inspired by the SimString project.
Metadata
Release files for simstring-rust 0.6.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distributions (wheels)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| simstring_rust-0.6.0-cp310-abi3-win_amd64.whl | CPython 3.10 | abi3 | Windows x86-64 | Details for simstring_rust-0.6.0-cp310-abi3-win_amd64.whl |
| simstring_rust-0.6.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | CPython 3.10 | abi3 | Linux glibc 2.17+ x86-64 | Details for simstring_rust-0.6.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
| simstring_rust-0.6.0-cp310-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl | CPython 3.10 | abi3 | macOS 11.0+ ARM64, macOS 10.12+ universal2 (ARM64, x86-64), macOS 10.12+ x86-64 | Details for simstring_rust-0.6.0-cp310-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl |
Total release size: 5.7 MB
Release files / simstring_rust-0.6.0-cp310-abi3-win_amd64.whl
| Download URL | simstring_rust-0.6.0-cp310-abi3-win_amd64.whl |
|---|---|
| Size | 316.2 kB |
| Tags | CPython 3.10 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
b0165a7345b18cc8e0004db783748ad03f91fadeb4382928f538570cf06ea687
|
|
BLAKE2b-256 checksum How to use checksums |
510fd98be066b93d7efbfc73541c5a3e3a33b3529fb4f8bf5d67cd3af718459a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
maturin/1.15.0
|
Release files / simstring_rust-0.6.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | simstring_rust-0.6.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 4.5 MB |
| Tags | CPython 3.10 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
a1e21ad30c8448a6c07bb9acf694621366c1d410f2774ddb84052dc93f888cf3
|
|
BLAKE2b-256 checksum How to use checksums |
a884b338aa9babad86f48e80f578967d23710b252419001a20379f327842c110
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
maturin/1.15.0
|
Release files / simstring_rust-0.6.0-cp310-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl
| Download URL | simstring_rust-0.6.0-cp310-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl |
|---|---|
| Size | 901.4 kB |
| Tags | CPython 3.10 abi3 macOS 10.12+ universal2 (ARM64, x86-64) macOS 10.12+ x86-64 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
792e4778aca4bb88ac7537f5e9f12dcf14d48107e19743ed0c1f0d85de0cc975
|
|
BLAKE2b-256 checksum How to use checksums |
39235743f40cd6f1bc0faec9b5ebae6e7055c6d2848e0a3e3d50cc5f28a5e66c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
maturin/1.15.0
|
Security for simstring-rust 0.6.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.