NumPy-first similarity scores for strings, vectors, points, and sets.
Project description
simmetry
Similarity scores for strings, vectors, points, and sets with a small, NumPy-first API.
PyPI · GitHub · Docs · Changelog
Install
pip install simmetry
pip install "simmetry[fast]" # Numba acceleration for euclidean_sim / manhattan_sim
pip install "simmetry[ann-hnsw]" # hnswlib ANN backend
pip install "simmetry[ann-faiss]" # FAISS ANN backend
pip install "simmetry[ann]" # both ANN backends
Project Status
- Maturity: Beta (API stabilising; pin to minor versions in production)
- Versioning: semantic versioning; breaking changes bump the minor version until
2.0
Quickstart
One function
from simmetry import similarity
similarity("kitten", "sitting", metric="levenshtein")
similarity([1, 2, 3], [1, 2, 4], metric="cosine")
similarity((41.1, 29.0), (41.2, 29.1), metric="haversine_sim") # returns [0, 1]
similarity({1, 2, 3}, {2, 3, 4}, metric="jaccard")
Pairwise matrices
import numpy as np
from simmetry import pairwise
# Vectors
X = np.random.randn(1000, 128)
S = pairwise(X, metric="cosine") # (1000, 1000)
D = pairwise(X, metric="cosine_distance") # 1 - cosine, sklearn-compatible
# Strings
S = pairwise(["cat", "car", "bar"], metric="levenshtein") # (3, 3)
# Points
pts = [(41.0, 29.0), (41.1, 29.1), (40.9, 28.9)]
S = pairwise(pts, metric="haversine_sim") # (3, 3)
Top-k search (exact)
import numpy as np
from simmetry import topk
X = np.random.randn(5000, 64)
q = np.random.randn(64)
idx, scores = topk(q, X, k=10, metric="cosine")
# idx and scores are sorted descending (highest similarity first)
Available Metrics
from simmetry import available
available() # all registered metrics
available("vector")
available("string")
available("point")
available("set")
Vectors
| Metric | Returns |
|---|---|
cosine |
[-1, 1] |
cosine_distance |
[0, 2] · 1 - cosine |
dot |
unbounded inner product |
euclidean_sim |
(0, 1] · 1 / (1 + dist) |
manhattan_sim |
(0, 1] · 1 / (1 + dist) |
pearson |
[-1, 1] |
hamming |
[0, 1] · normalized for equal-length sequences |
Strings
| Metric | Notes |
|---|---|
levenshtein |
normalized edit distance |
jaro_winkler |
prefix-weighted character matching |
ngram_jaccard |
character trigram Jaccard (default n=3) |
token_jaccard |
whitespace-token Jaccard |
hamming_str |
normalized Hamming for equal-length strings |
bm25 |
BM25 relevance score normalized to [0, 1] |
Points / Geo
| Metric | Returns |
|---|---|
euclidean_2d |
(0, 1] · 2D Cartesian similarity |
haversine_sim |
[0, 1] · geographic similarity (antipodal ≈ 0) |
haversine_km is available as a utility function (not a registered metric) that returns raw kilometers:
from simmetry.points import haversine_km
km = haversine_km((40.7, -74.0), (51.5, -0.1)) # ~5 570 km
Sets
| Metric | Formula |
|---|---|
jaccard |
|A∩B| / |A∪B| |
dice |
2|A∩B| / (|A| + |B|) |
overlap |
|A∩B| / min(|A|, |B|) |
tversky |
|A∩B| / (|A∩B| + α|A\B| + β|B\A|) |
tversky with alpha=beta=1 equals Jaccard; alpha=beta=0.5 equals Dice.
Call it directly with custom weights: tversky(A, B, alpha=0.3, beta=0.7).
Auto Metric Selection
Auto mode applies fixed type-based rules — no learning, no randomness.
from simmetry import infer_metric, similarity
infer_metric("samplecorp", "sample corp") # "jaro_winkler"
infer_metric((41.0, 29.0), (41.1, 29.1)) # "haversine_sim"
infer_metric({1, 2, 3}, {2, 3, 4}) # "jaccard"
similarity("samplecorp", "sample corp") # uses inferred metric
Selection order:
list[str]/tuple[str](including empty) →jaro_winklerstr+str→jaro_winklertupleof 2 numbers with valid lat/lon range ([-90, 90] × [-180, 180]) →haversine_simset/frozenset→jaccard- numeric vectors →
cosine - fallback →
cosine
Note: Only
tupleinputs (notlist) trigger the geo heuristic to avoid ambiguity with 2D numeric vectors.[1.0, 2.0]routes tocosine;(1.0, 2.0)routes tohaversine_sim. Passmetric="haversine_sim"explicitly when in doubt.
Batch String APIs
from simmetry.strings import pairwise_strings, topk_strings
S = pairwise_strings(
["item_one", "item_two"],
["item_one", "item_alt"],
metric="jaro_winkler",
)
idx, scores = topk_strings(
"samplecorp",
["samplecorp", "examplefinance", "testgroup"],
k=2,
metric="levenshtein",
)
Batch Point APIs (Geo / 2D)
from simmetry.points import pairwise_points, topk_points
pts = [(41.0, 29.0), (41.01, 29.01), (40.9, 28.9)]
S = pairwise_points(pts, metric="haversine_sim") # similarity matrix
idx, scores = topk_points((41.0, 29.0), pts, k=2, metric="haversine_sim")
haversine_km is also accepted by pairwise_points and topk_points for raw-distance
use cases. topk_points(..., metric="haversine_km") ranks by ascending distance (nearest first).
ANN Top-k (Optional)
For large vector corpora (100k+), use approximate nearest neighbour backends.
hnswlib
import numpy as np
from simmetry.ann import build_hnsw
X = np.random.randn(200_000, 128).astype("float32")
X /= np.linalg.norm(X, axis=1, keepdims=True)
index = build_hnsw(X, space="cosine")
labels, distances = index.query(X[0], k=10)
faiss
import numpy as np
from simmetry.ann import build_faiss
X = np.random.randn(200_000, 128).astype("float32")
X /= np.linalg.norm(X, axis=1, keepdims=True)
index = build_faiss(X, metric="ip")
labels, scores = index.query(X[0], k=10)
SimIndex (Exact or ANN)
SimIndex provides a unified interface across all backends.
query() always returns (indices, similarities) — ANN distances are converted
to similarities internally so results are directly comparable across backends.
import numpy as np
from simmetry import SimIndex
X = np.random.randn(50_000, 128).astype("float32")
index = SimIndex(metric="cosine", backend="exact").add(X)
idx, scores = index.query(X[0], k=10)
# scores are cosine similarities, sorted descending
Switch backends without changing the query code:
index_hnsw = SimIndex(metric="cosine", backend="hnsw").add(X)
index_faiss = SimIndex(metric="cosine", backend="faiss").add(X)
Composite Records
Weight multiple fields with different metrics:
from simmetry import similarity
a = {"name": "Entity One", "city": "CityAlpha", "loc": (41.0, 29.0)}
b = {"name": "Entity One Extended", "city": "CityAlpha", "loc": (41.01, 28.99)}
score = similarity(
a,
b,
metric={"name": "jaro_winkler", "loc": "haversine_sim"},
weights={"name": 0.7, "loc": 0.3},
)
Fields missing from either record raise KeyError with a descriptive message.
Custom Metrics
from simmetry import register, similarity
def my_metric(a, b):
return 1.0 if a == b else 0.0
register("exact_match", my_metric, kind="generic")
similarity("foo", "foo", metric="exact_match") # 1.0
Scope and Roadmap
Planned additions:
pairwise()cross-type dispatch for composite inputs- BM25 corpus-level ranking (multi-document IDF)
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file simmetry-1.2.0.tar.gz.
File metadata
- Download URL: simmetry-1.2.0.tar.gz
- Upload date:
- Size: 25.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0d26853a62a63b28147cddedda4775d244c0c607b07b343e3963d92cd3a4131f
|
|
| MD5 |
73694b7b7496b316eb807342a4fafa79
|
|
| BLAKE2b-256 |
94bc9abac08809cdf80f39e749aa5bc8282d8abc2da2ace6a1da8683d605ab15
|
File details
Details for the file simmetry-1.2.0-py3-none-any.whl.
File metadata
- Download URL: simmetry-1.2.0-py3-none-any.whl
- Upload date:
- Size: 23.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0669e64c0d217ee0604d7d1b5c7718c4a9f1284b48530d85a247974a526f0187
|
|
| MD5 |
daf590603f75a31abf63fec49ef26c69
|
|
| BLAKE2b-256 |
0e46efce1f3fde3514105125d1fd4b3cc88216fc3d5f7bb3abf190d08b0e9a3d
|