ef.exploration¶
Layer L5 — explore a corpus: project to 2-D/3-D, cluster, label clusters.
This is ef’s visualization heritage, inherited from imbed and
re-presented as a secondary surface. The primary surface of ef is
search / RAG / corpus-indexing (see ef.ingest() and
SourceManager); explore is the “see the shape of
the corpus” tool — the backend an app_ef corpus map consumes.
Four free functions, each accepting a corpus or an already-computed vector
matrix (whatever _resolve_vectors() understands):
project()— reduce embeddings to 2-D/3-D coordinates (PCA → UMAP).cluster()— group embeddings into clusters (k-means / HDBSCAN).label_clusters()— name clusters with an LLM (viaimbed).explore()— the orchestrator: project + cluster (+ optionally label) in one call, returning a structured, JSON-friendlyExploreResult(ids/coords/labels/cluster_titles) — the surface anapp_efcorpus map (oref.service.EfService.explore_corpus()) consumes directly.
The module imports with numpy only. UMAP, HDBSCAN and imbed are
optional: each is imported lazily, inside the function that needs it, and only
that heavy path requires the corresponding extra (ef[explore] /
ef[imbed]). The numpy-only paths — PCA projection and k-means clustering —
work out of the box with no install.
Example — project a small set of vectors to the plane (numpy only):
>>> import numpy as np
>>> vectors = np.random.RandomState(0).rand(12, 16)
>>> coords = project(vectors, dims=2, method='pca')
>>> coords.shape
(12, 2)
>>> labels = cluster(vectors, method='kmeans', n_clusters=3, random_state=0)
>>> labels.shape
(12,)
Each returned array’s rows are in the iteration order of the input — for a
SearchableCorpus that is list(searchable.collection),
which the caller zips back against to recover {id: coords}. explore()
does that zip-back for you — its ExploreResult keeps ids aligned
with coords and labels.
Functions
|
Project a corpus's embeddings to |
|
Cluster a corpus's embeddings (use cases §G2). |
|
Name each cluster with a short LLM-generated title (use cases §G3). |
|
Project, cluster and (optionally) label a corpus in one call. |
Classes
A structured, JSON-friendly corpus-exploration result. |
- class ef.exploration.ExploreResult[source]¶
Bases:
TypedDictA structured, JSON-friendly corpus-exploration result.
What
explore()returns andef.service.EfService.explore_corpus()serves. Every list is row-aligned:ids[i],coords[i]andlabels[i]all describe the same item.- Keys:
- ids: the per-item identifiers — a corpus’s keys, or positional
"0", "1", … for an id-less vector matrix.- coords: the projected coordinates — one
[x, y](or[x, y, z]) row per item.
labels: the cluster id of each item (HDBSCAN marks noise
-1). cluster_titles:{cluster_id: title}— empty unlessexplore()was called with
label=True.- ids: the per-item identifiers — a corpus’s keys, or positional
- ef.exploration.cluster(data, *, method='kmeans', n_clusters=8, min_cluster_size=5, embedder=None, normalize=True, random_state=42)[source]¶
Cluster a corpus’s embeddings (use cases §G2).
method='kmeans'is the default and is dependency-free — a numpy Lloyd’s algorithm with k-means++ seeding.method='hdbscan'finds a density-based clustering (and a noise label-1) and needs theef[explore]extra.- Parameters:
data (
Any) – what to cluster — seeproject()/_resolve_vectors().method (
Literal['kmeans','hdbscan']) –'kmeans'(numpy-only) or'hdbscan'(optional extra).n_clusters (
int) – number of clusters for k-means (clamped ton_samples).min_cluster_size (
int) – smallest cluster HDBSCAN will form.embedder (
Any) – embedder used whendatais raw text.normalize (
bool) – L2-normalize the vectors first, so Euclidean distance behaves like cosine similarity — the right default for semantic embeddings. SetFalseto cluster raw vectors.random_state (
int) – seed for k-means initialization.
- Return type:
ndarray- Returns:
An
ndarrayof integer cluster labels, shape(n_samples,), in input order. HDBSCAN labels unclustered points-1.
>>> import numpy as np >>> vectors = np.random.RandomState(2).rand(15, 8) >>> labels = cluster(vectors, method='kmeans', n_clusters=3, random_state=0) >>> labels.shape (15,) >>> set(labels.tolist()) <= {0, 1, 2} True
- ef.exploration.explore(data, *, dims=2, projection_method='auto', cluster_method='kmeans', n_clusters=8, min_cluster_size=5, label=False, context=' ', n_words=4, embedder=None, random_state=42)[source]¶
Project, cluster and (optionally) label a corpus in one call.
The layer-L5 orchestrator —
project()+cluster()(+ optionallylabel_clusters()) wired into one structured result. Whereproject()/cluster()return bare arrays in input order,explorekeeps every row tied to its id: it returns anExploreResultwithids/coords/labelsrow-aligned (pluscluster_titles) — the JSON-friendly shape anapp_efcorpus map (oref.service.EfService.explore_corpus()) consumes directly.- Parameters:
data (
Any) – the corpus to explore — anything_resolve_explorable()understands (aSourceManager/SearchableCorpus, aCorpusmapping, an iterable of texts orSegments, or a vector matrix).dims (
int) – projection target dimensionality —2or3.projection_method (
Literal['auto','umap','pca']) – forwarded toproject()—'auto'/'umap'/'pca'.cluster_method (
Literal['kmeans','hdbscan']) – forwarded tocluster()—'kmeans'/'hdbscan'.n_clusters (
int) – number of k-means clusters.min_cluster_size (
int) – smallest cluster HDBSCAN will form.label (
bool) – whenTrue, name each cluster withlabel_clusters()(needs theef[imbed]extra, an LLM key, and text-bearingdata); whenFalse(the default)cluster_titlesis empty.context (
str) – the corpus topic passed tolabel_clusters().n_words (
int) – maximum cluster-title length, in words.embedder (
Any) – embedder used whendatais raw text.random_state (
int) – seed — projection and clustering are reproducible.
- Return type:
- Returns:
an
ExploreResult—ids,coordsandlabelsrow-aligned, andcluster_titles(empty unlesslabel=True).- Raises:
ValueError – if the corpus is empty, has fewer than 2 samples, or
label=Trueis asked of a text-less vector matrix.
>>> import numpy as np >>> vectors = np.random.RandomState(0).rand(12, 16) >>> result = explore(vectors, projection_method='pca', n_clusters=3, random_state=0) >>> len(result['ids']), len(result['coords']), len(result['labels']) (12, 12, 12) >>> result['ids'][:3] ['0', '1', '2'] >>> len(result['coords'][0]) # each row has `dims` coordinates 2 >>> set(result['labels']) <= {0, 1, 2} True >>> result['cluster_titles'] # empty unless label=True {}
- ef.exploration.label_clusters(segments, labels, *, context=' ', n_words=4, n_samples=None, **labeler_kwargs)[source]¶
Name each cluster with a short LLM-generated title (use cases §G3).
A thin wrapper over
imbed’simbed.tools.ClusterLabeler: it samples each cluster’s segments, asks an LLM for a few-word title, and returns{cluster_id: title}.efdoes not reimplement this — it reusesimbed(design notes §7).- Parameters:
segments (
Iterable[Any]) – the segment texts — plain strings orSegmentmappings (the"text"field is used). Aligned withlabels.labels (
Iterable[Any]) – the cluster id of each segment — typically the output ofcluster().context (
str) – the corpus’s overall topic, given to the LLM so titles describe how a cluster differs from the rest, not the shared topic.n_words (
int) – maximum title length, in words.n_samples (
int|None) – how many segments to sample per cluster when prompting;NonekeepsClusterLabeler’s default.**labeler_kwargs (
Any) – forwarded toimbed.tools.ClusterLabeler(e.g.max_unique_clusters,prompt).
- Return type:
- Returns:
{cluster_id: title}for every distinct label.- Raises:
ValueError – if
segmentsandlabelsdiffer in length.ImportError – if
imbed(and itspandas/oadeps) is missing.
>>> # needs ef[imbed] + an LLM key — illustrative only: >>> # label_clusters(['neural net training ...', ...], [0, 1, 0, ...]) >>> # -> {0: 'Neural network training', 1: 'Dataset preprocessing'}
- ef.exploration.project(data, *, dims=2, method='auto', embedder=None, n_neighbors=15, min_dist=0.1, metric='cosine', pca_components=50, random_state=42)[source]¶
Project a corpus’s embeddings to
dimscoordinates for visualization.The canonical “show me the shape of the corpus” operation (use cases §G1). The default
method='auto'runs the design-notes §8.4 recipe — **PCA → UMAP**,cosinemetric, seeded: PCA first compresses high-dimensional embeddings topca_components(cheap denoising), then UMAP lays them out indimsdimensions.- Parameters:
data (
Any) – what to project — anything_resolve_vectors()understands: anndarray(or nested sequence) of vectors, an iterable of texts (embedded withembedder), aCorpusmapping, or aSearchableCorpus/SourceManager(vectors pulled from the index).dims (
int) – target dimensionality —2or3for plotting.method (
Literal['auto','umap','pca']) –'auto'(UMAP when available and the corpus is large enough, else PCA),'umap'(force UMAP — needs theef[explore]extra), or'pca'(numpy-only, always available).embedder (
Any) – embedder used whendatais raw text; defaults to the dependency-freeDEFAULT_EMBEDDER.n_neighbors (
int) – UMAP neighborhood size (clamped ton_samples - 1).min_dist (
float) – UMAP minimum point separation in the embedding.metric (
str) – UMAP distance metric —'cosine'for semantic embeddings.pca_components (
int) – PCA width for the pre-UMAP compression step; PCA is skipped when the source dimensionality is already smaller.random_state (
int) – seed — projection is reproducible.
- Return type:
ndarray- Returns:
An
ndarrayof shape(n_samples, dims), rows in input order.- Raises:
ValueError – if there are fewer than 2 samples.
ImportError – if
method='umap'andumap-learnis not installed.
>>> import numpy as np >>> vectors = np.random.RandomState(1).rand(20, 32) >>> project(vectors, dims=3, method='pca').shape (20, 3)