Density Yields Features - discover structure in embedding spaces
Project description
DYF - Density Yields Features
Discover structure in embedding spaces. DYF uses density-based LSH to reveal the natural organization of your data:
- Dense: Core items in well-populated semantic regions
- Bridge: Transitional items connecting different clusters
- Orphan: Unique items with no semantic neighbors
What it does
DYF transforms raw embeddings into navigable semantic maps. Instead of just clustering, it reveals the topology - which regions are dense, which items bridge between concepts, and which are truly unique.
Use cases:
- Semantic navigation: Find paths between concepts
- Structure discovery: Understand how your data organizes itself
- Anomaly detection: Identify orphans and bridges
- Index building: Pre-compute structure for fast queries
Installation
pip install dyf
For serialization (save/load indexes):
pip install dyf[io]
For full features (embedding generation, LLM labeling):
pip install dyf[full]
Quick Start
Discover Structure
import numpy as np
from dyf import DensityClassifier
# Your embeddings (e.g., from sentence-transformers)
embeddings = np.random.randn(10000, 384).astype(np.float32)
# Find structure
classifier = DensityClassifier(embedding_dim=384)
classifier.fit(embeddings)
# What did we find?
print(classifier.report())
# Corpus: 10000 items
# Dense: 9500 (95.0%)
# Bridge: 450 (4.5%)
# Orphan: 50 (0.5%)
# Get indices
bridges = classifier.get_bridge() # Transitional items
orphans = classifier.get_orphans() # Unique items
Build & Search Indexes
from dyf import build_dyf_tree, write_lazy_index, LazyIndex
# Build tree from embeddings
tree = build_dyf_tree(embeddings, max_depth=4, num_bits=3, min_leaf_size=8)
# Write to disk (mmap-friendly, zero startup cost)
write_lazy_index(tree, embeddings, "index.dyf",
quantization="float16", compression="zstd",
stored_fields={"title": titles},
metadata={"model": "nomic-embed-text-v1.5"})
# Search — instant open, LRU-bounded leaf cache, fast Rust backend
with LazyIndex("index.dyf") as idx:
result = idx.search(query_embedding, k=10, nprobe=256, backend="rust")
print(result.indices, result.scores)
print(result.fields["title"]) # stored fields returned with results
For a fully in-memory corpus, DenseSearchIndex builds the tree and searches via the
same Rust kernel (batched queries supported):
from dyf import DenseSearchIndex
idx = DenseSearchIndex(embeddings) # builds tree + flattens
indices, scores = idx.search(query, k=10, nprobe=256)
I, S = idx.search(query_batch, k=10, nprobe=256) # batched -> (nq, k)
Adaptive Probing
Queries near decision boundaries automatically probe more leaves:
from dyf import LazyIndex, AdaptiveProbeConfig
with LazyIndex("index.dyf") as idx:
# Auto mode: margin-based probe count (default thresholds)
result = idx.search(query, k=10, nprobe="auto", return_routing=True)
print(result.routing["adaptive_nprobe"]) # how many leaves were probed
# Custom thresholds
cfg = AdaptiveProbeConfig(margin_lo=0.005, margin_hi=0.2,
min_probes=1, max_probes=8)
result = idx.search(query, k=10, nprobe=cfg)
Full-Featured Usage
from dyf import DensityClassifierFull, EmbedderConfig, LabelerConfig
# From raw texts
classifier = DensityClassifierFull.from_texts(
texts=documents,
categories=categories,
)
# Label clusters with LLM
labels = classifier.label_buckets(**LabelerConfig.MEDIUM.as_kwargs())
print(labels['dense'][1234]['label']) # "Machine Learning Papers"
How It Works
Two-stage PCA-based LSH:
- Initial bucketing: PCA projections create semantic buckets
- Density check: Items in sparse buckets are candidates for reclassification
- Recovery stage: Coarser PCA finds structure among sparse items
- Classification: Dense (core), Bridge (recovered), Orphan (truly unique)
The key insight: items that appear as outliers globally often share structure at coarser resolution. Bridges are these "misplaced" items - they connect different semantic regions.
Performance
Search runs on a Rust multiprobe kernel (dyf-rs >= 0.8.0, PyO3) — the default path for
both LazyIndex.search and DenseSearchIndex. Results are bit-identical to the
pure-Python reference (backend="python"); the kernel handles fixed and adaptive
nprobe and return_routing. MSMARCO MiniLM-L6 (384d), Apple Silicon, batched unless
noted:
| path | corpus / setting | latency / query | vs pure-Python |
|---|---|---|---|
DenseSearchIndex (in-memory, batched) |
8.84M, nprobe=256 | ~0.5 ms | ~100× |
LazyIndex.search (on-disk, batched) |
up to 8.84M, nprobe=256 | ~0.9 ms | ~29× |
LazyIndex.search (on-disk, single query) |
nprobe=256 | ~4 ms | ~6× |
LazyIndex.search (with stored fields) |
immich 35K | ~2 ms | ~15× |
Speedup is largest at low nprobe, batched queries, and without field-gather. Lazy mode
opens in ~5 ms (vs ~0.4 s preload) and bounds memory with an LRU; only PQ-compressed and
overflow indexes fall back to Python.
Scope. These are Rust-vs-pure-Python speedups — recovering the cost of the per-query Python loop. They are not a claim that dyf is the fastest ANN retriever: on the pure recall-vs-latency frontier, mature graph libraries (pynndescent, HNSW) are faster. dyf's strengths are structure discovery, hierarchy, instant-open on-disk indexes, and reaching exact recall on a single index by raising
nprobe.
API
DensityClassifier
DensityClassifier(
embedding_dim: int,
initial_bits: int = 14, # LSH resolution
recovery_bits: int = 8, # Coarser recovery resolution
dense_threshold: int = 10, # Min bucket size for "dense"
seed: int = 31
)
# Methods
classifier.fit(embeddings)
classifier.get_dense() # Dense item indices
classifier.get_bridge() # Bridge item indices
classifier.get_orphans() # Orphan item indices
classifier.get_bucket_id(idx) # Which bucket is item in?
classifier.report() # Summary statistics
LazyIndex
from dyf import LazyIndex
with LazyIndex("index.dyf") as idx:
# Search with fixed or adaptive probing
result = idx.search(query, k=10, nprobe=3) # fixed
result = idx.search(query, k=10, nprobe="auto") # adaptive
# Inspect index structure
idx.tree_summary # metadata, dims, leaf count
idx.total_items # total indexed items
idx.stored_field_names # available stored fields
# Extract all data
data = idx.extract_all_fields()
data['embeddings'] # (n, d) float32
data['fields'] # {field_name: array}
Documentation
- How It Works — the algorithm, metrics, and Dense/Bridge/Orphan explained
- Getting Started — code recipes and examples
- API Reference — full documentation for all classes and functions
License
MIT
Project details
Release history Release notifications | RSS feed
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 dyf-0.11.0.tar.gz.
File metadata
- Download URL: dyf-0.11.0.tar.gz
- Upload date:
- Size: 242.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1a01874cdebffccf49c6c56a2c7d3516aae34c93f7b122986812604e24942757
|
|
| MD5 |
65aa8ae929faedfce7f2482f702586d5
|
|
| BLAKE2b-256 |
1d24c7c8ae8a2390dbe34f7704076acdc29cef9cb0d3aff104d39b675eff8478
|
Provenance
The following attestation bundles were made for dyf-0.11.0.tar.gz:
Publisher:
publish.yml on jdonaldson/dyf
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dyf-0.11.0.tar.gz -
Subject digest:
1a01874cdebffccf49c6c56a2c7d3516aae34c93f7b122986812604e24942757 - Sigstore transparency entry: 1886007737
- Sigstore integration time:
-
Permalink:
jdonaldson/dyf@2dc98594123b961af29a6df8a023f93ff21cdce9 -
Branch / Tag:
refs/tags/v0.11.0 - Owner: https://github.com/jdonaldson
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2dc98594123b961af29a6df8a023f93ff21cdce9 -
Trigger Event:
push
-
Statement type:
File details
Details for the file dyf-0.11.0-py3-none-any.whl.
File metadata
- Download URL: dyf-0.11.0-py3-none-any.whl
- Upload date:
- Size: 193.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
23c4c97bc6a35c1224b5d0e94cf40de9a80686fee0f3cde78a1a076c2c7c42f2
|
|
| MD5 |
36d9b78f903f47b80efb4dced0f1741f
|
|
| BLAKE2b-256 |
b31490597d603911e354cfc6c14fae18a868711b800d71932367f007a3139df9
|
Provenance
The following attestation bundles were made for dyf-0.11.0-py3-none-any.whl:
Publisher:
publish.yml on jdonaldson/dyf
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dyf-0.11.0-py3-none-any.whl -
Subject digest:
23c4c97bc6a35c1224b5d0e94cf40de9a80686fee0f3cde78a1a076c2c7c42f2 - Sigstore transparency entry: 1886007785
- Sigstore integration time:
-
Permalink:
jdonaldson/dyf@2dc98594123b961af29a6df8a023f93ff21cdce9 -
Branch / Tag:
refs/tags/v0.11.0 - Owner: https://github.com/jdonaldson
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2dc98594123b961af29a6df8a023f93ff21cdce9 -
Trigger Event:
push
-
Statement type: