simmetry
simmetry is a Python library for computing similarity between strings, vectors, geo points, and sets — using a single, consistent API.
Whether you're deduplicating company records, ranking documents against a query, searching a million embeddings, or checking how close two GPS coordinates are, simmetry gives you the same interface: similarity(a, b, metric=...).
Why simmetry?
Most similarity tasks in Python require juggling multiple libraries: rapidfuzz for strings, scikit-learn for vectors, geopy for coordinates, and custom code for everything else. Each has a different API, different output conventions, and no way to combine them.
simmetry unifies this under one interface — and adds batch operations, ANN indexing, and composite record matching on top.
The basics
One function for everything
similarity(a, b) automatically picks the right metric based on input type:
from simmetry import similarity
# Strings → jaro_winkler
similarity("New York", "New York City")
# 0.962
# Vectors → cosine
similarity([1.0, 0.0, 0.5], [0.9, 0.1, 0.5])
# 0.993
# Geo coordinates → haversine_sim (0 = antipodal, 1 = same point)
similarity((41.0082, 28.9784), (41.0122, 28.9760))
# 0.9998
# Sets → jaccard
similarity({"python", "ml", "nlp"}, {"python", "nlp", "cv"})
# 0.5
You can always specify the metric explicitly:
similarity("kitten", "sitting", metric="levenshtein") # 0.571
similarity("kitten", "sitting", metric="jaro_winkler") # 0.746
similarity("kitten", "sitting", metric="ngram_jaccard") # 0.286
Checking which metric will be used
from simmetry import infer_metric
infer_metric("hello", "world") # "jaro_winkler"
infer_metric((51.5, -0.1), (48.8, 2.3)) # "haversine_sim"
infer_metric({1, 2, 3}, {3, 4, 5}) # "jaccard"
infer_metric([0.1, 0.9], [0.8, 0.2]) # "cosine"
Tuple vs list for geo coordinates
Only tuple inputs trigger the geo heuristic, not list.
(41.0, 29.0) → haversine_sim, but [41.0, 29.0] → cosine.
This avoids ambiguity with 2D numeric vectors. Pass metric="haversine_sim" explicitly when in doubt.
String similarity
Choosing the right string metric
Each string metric is suited to different tasks:
| Metric | Best for | Example |
|---|---|---|
jaro_winkler |
Names, company names, short strings with typos | "McDonald's" vs "McDonalds" |
levenshtein |
General edit distance, codes, identifiers | "colour" vs "color" |
ngram_jaccard |
Longer text, OCR output, transliterations | character-level overlap |
token_jaccard |
Multi-word phrases, documents | token set overlap |
hamming_str |
Fixed-length codes, DNA sequences | "ATCG" vs "ATGG" |
bm25 |
Ranking documents against a query | search relevance |
Finding the best match in a list
topk_strings returns the k closest matches from a corpus, sorted by score:
from simmetry.strings import topk_strings
# Scenario: you have a canonical list of company names and a noisy input
canonical = [
"Apple Inc.",
"Google LLC",
"Microsoft Corporation",
"Amazon.com Inc.",
"Meta Platforms Inc.",
]
query = "Microsft Corp" # typo + truncation
idx, scores = topk_strings(query, canonical, k=3, metric="jaro_winkler")
for i, score in zip(idx, scores):
print(f" {canonical[i]:<25} {score:.3f}")
# Microsoft Corporation 0.931
# Apple Inc. 0.545
# Google LLC 0.542
All-vs-all pairwise matrix
pairwise_strings computes similarity between every pair across two lists:
from simmetry.strings import pairwise_strings
import numpy as np
list_a = ["New York", "Los Angeles", "Chicago"]
list_b = ["New York City", "LA", "Chicago IL"]
S = pairwise_strings(list_a, list_b, metric="jaro_winkler")
# S.shape == (3, 3)
# S[i, j] = similarity between list_a[i] and list_b[j]
print(S.round(3))
# [[0.962 0.506 0.547] ← "New York" vs each
# [0.486 0.711 0.506] ← "Los Angeles" vs each
# [0.593 0.433 0.944]] ← "Chicago" vs each
Self-similarity (pass only one list):
tags = ["machine-learning", "deep-learning", "reinforcement-learning", "nlp"]
S = pairwise_strings(tags, metric="ngram_jaccard")
# diagonal is 1.0; off-diagonal shows character-level overlap
Or use the top-level pairwise() which detects the input type automatically:
Text ranking with BM25
bm25 treats the first argument as a query and the second as a document, returning a relevance score in [0, 1]. It's designed for ranking — use it with topk_strings to search a corpus without building an index:
from simmetry.strings import topk_strings
corpus = [
"introduction to machine learning with python",
"deep learning for natural language processing",
"python string similarity and distance metrics",
"vector databases and embedding search",
"fuzzy matching for data deduplication",
"similarity search at scale with FAISS",
]
query = "python similarity search"
idx, scores = topk_strings(query, corpus, k=3, metric="bm25")
for i, score in zip(idx, scores):
print(f" {corpus[i]:<48} {score:.3f}")
# python string similarity and distance metrics 1.000
# similarity search at scale with FAISS 0.500
# fuzzy matching for data deduplication 0.333
BM25 is asymmetric — it measures how well a document satisfies a query, not general string closeness. For symmetric string comparison, use jaro_winkler or levenshtein.
Vector similarity
Pairwise matrix for embeddings
If you have a set of embeddings (from a model, TF-IDF, or anything else), pairwise() gives you the full similarity matrix:
import numpy as np
from simmetry import pairwise
# 1000 sentence embeddings, 384 dimensions
embeddings = np.random.randn(1000, 384).astype("float32")
S = pairwise(embeddings, metric="cosine")
# S.shape == (1000, 1000)
# S[i, j] = cosine similarity between embedding i and embedding j
# diagonal is 1.0
Cross-similarity between two sets:
queries = np.random.randn(10, 384) # query embeddings
documents = np.random.randn(500, 384) # document embeddings
S = pairwise(queries, documents, metric="cosine")
# S.shape == (10, 500)
Top-k nearest neighbours (exact)
from simmetry import topk
q = embeddings[0]
idx, scores = topk(q, embeddings, k=5, metric="cosine")
# Results are sorted descending — highest similarity first
# idx[0] == 0, scores[0] == 1.0 (the query itself)
All vector metrics
from simmetry import similarity
import numpy as np
a = np.array([1.0, 0.3, 0.8])
b = np.array([0.9, 0.4, 0.7])
similarity(a, b, "cosine") # 0.998 direction similarity
similarity(a, b, "euclidean_sim") # 0.874 1 / (1 + euclid_dist)
similarity(a, b, "manhattan_sim") # 0.769 1 / (1 + manhattan_dist)
similarity(a, b, "pearson") # 0.995 correlation
similarity(a, b, "dot") # 1.49 raw inner product
similarity(a, b, "cosine_distance") # 0.002 1 - cosine (for distance-based pipelines)
Geo / location similarity
haversine_sim returns a similarity in [0, 1] — 1.0 for the same point, approaching 0 for antipodal points:
from simmetry import similarity
from simmetry.points import topk_points, pairwise_points
istanbul = (41.0082, 28.9784)
paris = (48.8566, 2.3522)
london = (51.5074, -0.1278)
new_york = (40.7128, -74.006)
singapore = (1.3521, 103.8198)
# How similar are the locations?
similarity(istanbul, london, "haversine_sim") # 0.966
similarity(istanbul, new_york, "haversine_sim") # 0.933
similarity(london, singapore, "haversine_sim") # 0.889
# Find the closest cities to istanbul
cities = [istanbul, paris, london, new_york, singapore]
names = ["Istanbul", "Paris", "London", "New York", "Singapore"]
idx, scores = topk_points(istanbul, cities, k=3, metric="haversine_sim")
for i, s in zip(idx, scores):
print(f" {names[i]:<12} {s:.4f}")
# Istanbul 1.0000 (itself)
# London 0.9660
# Paris 0.9657
# Pairwise distance matrix (raw km, not similarity)
from simmetry.points import haversine_km
D = pairwise_points(cities[:3], metric="haversine_km")
# D[i, j] = km between city i and city j
Set similarity
Sets, lists, or any iterables work as input:
from simmetry import similarity
from simmetry.sets import tversky
a = {"python", "machine-learning", "nlp", "transformers"}
b = {"python", "deep-learning", "nlp", "computer-vision"}
similarity(a, b, "jaccard") # 0.333 |intersection| / |union|
similarity(a, b, "dice") # 0.5 harmonic-mean weighting
similarity(a, b, "overlap") # 0.5 |intersection| / |smaller set|
Tversky is a generalisation with asymmetric weights — useful when false positives and false negatives have different costs:
# Standard Jaccard (alpha=beta=1)
tversky(a, b, alpha=1.0, beta=1.0) # 0.333
# Standard Dice (alpha=beta=0.5)
tversky(a, b, alpha=0.5, beta=0.5) # 0.5
# Penalise items in b but not in a more heavily
tversky(a, b, alpha=0.2, beta=0.8) # 0.556
Composite record matching
When you have structured records with multiple fields, you can specify a metric per field and a weight for each:
from simmetry import similarity
# Entity resolution: are these two records the same company?
record_a = {
"name": "Acme Corporation",
"city": "New York",
"location": (40.7128, -74.006),
"tags": {"manufacturing", "b2b", "industrial"},
}
record_b = {
"name": "ACME Corp.",
"city": "New York City",
"location": (40.7200, -74.010),
"tags": {"b2b", "industrial", "wholesale"},
}
score = similarity(
record_a,
record_b,
metric={
"name": "jaro_winkler",
"city": "token_jaccard",
"location": "haversine_sim",
"tags": "jaccard",
},
weights={
"name": 0.4,
"city": 0.1,
"location": 0.2,
"tags": 0.3,
},
)
# score → 0.883 → likely the same entity
Each field can use any registered metric — strings, vectors, points, and sets can all appear in the same record.
Large-scale vector search with SimIndex
For large corpora (100k+ vectors), exact pairwise search becomes too slow. SimIndex wraps exact and ANN backends behind a single interface, always returning (indices, similarities) — not raw distances:
import numpy as np
from simmetry import SimIndex
# 500k product embeddings, 128 dimensions
X = np.random.randn(500_000, 128).astype("float32")
# Exact search — fine up to ~50k
index = SimIndex(metric="cosine", backend="exact").add(X)
# HNSW — approximate, ~10-50x faster (pip install "simmetry[ann-hnsw]")
index = SimIndex(metric="cosine", backend="hnsw").add(X)
# FAISS — GPU-ready, best for very large corpora (pip install "simmetry[ann-faiss]")
index = SimIndex(metric="cosine", backend="faiss").add(X)
# Query is identical regardless of backend
query = X[0]
idx, scores = index.query(query, k=10)
print(idx) # array([ 0, 42317, 91823, ...])
print(scores) # array([1.000, 0.923, 0.917, ...]) ← similarities, not distances
Switch backends to benchmark or scale without changing any query code.
Custom metrics
Register any function as a named metric and use it with the full API:
from simmetry import register, similarity, pairwise
def jaro(a: str, b: str) -> float:
"""Example: plug in your own implementation."""
...
register("my_jaro", jaro, kind="string")
# Now works with the full API
similarity("hello", "helo", metric="my_jaro")
from simmetry.strings import pairwise_strings
S = pairwise_strings(["hello", "world"], metric="my_jaro")
All available metrics
from simmetry import available
available() # all registered metrics
available("vector") # cosine, cosine_distance, dot, euclidean_sim, hamming, manhattan_sim, pearson
available("string") # bm25, hamming_str, jaro_winkler, levenshtein, ngram_jaccard, token_jaccard
available("point") # euclidean_2d, haversine_sim
available("set") # dice, jaccard, overlap, tversky