Skip to content

Toponymy Architecture

John Healy edited this page Aug 17, 2026 · 2 revisions

Overview

Toponymy builds hierarchical topic models from a collection of objects and a layered clustering structure.

At a high level, it:

  1. Clusters objects into a hierarchy of increasingly broad topic layers.
  2. Extracts useful features for each cluster, such as exemplar texts and keyphrases.
  3. Processes layers from the most detailed layer upward.
  4. Builds an LLM prompt for each topic using the available per-cluster features.
  5. Names topics with an LLM and optionally disambiguates similar names.
  6. Stores learned topics and inspectable intermediate results in a TopicModel.

The architecture separates reusable clustering from topic-model-specific enrichment. A Clusterer describes the hierarchy; a TopicModel describes the resulting topics.

flowchart TD
    Objects[Objects] --> FeatureExtractors[Feature extractors]
    ObjectVectors[Embedding vectors] --> FeatureExtractors
    ClusterableVectors[Clusterable vectors] --> Clusterer

    Clusterer -->|cluster layers and tree| TopicModel
    FeatureExtractors -->|per-cluster features| TopicModel

    TopicModel --> CurrentLayer[Current topic layer]
    CurrentLayer --> DependentFeatures[Optional layer-dependent feature extractors]
    DependentFeatures --> Template
    Template --> LLMNamer
    LLMNamer -->|topic names and results| TopicModel
Loading

The architecture processes Objects (the raw data items or documents) using two different vector formats. Embedded Vectors are the rich, high-dimensional machine learning representations of these objects that retain deep semantic meaning used during the information extraction and exemplar selection stages. These are often created via neural embedding techniques but can be generated by any vectorization process. Clusterable Vectors are low-dimensional representations of these objects optimized for fast, multi-scale grouping. These are often created by performing dimension reduction on the high dimensional Embedding Vectors.

Core Data Types

Toponymy processes layers from most detailed to broadest. The optional layer-dependent feature-extraction step is skipped for the base layer, because no lower layers have yet been named. For later layers, configured LayerDependentFeatureExtractor instances read completed lower-layer results from the TopicModel and add further per-cluster features, such as subtopics.

Feature extractors store their results in the TopicModel. Templates do not communicate with extractors directly; when constructing a prompt for a topic, a template reads the relevant cluster data and features from the TopicModel.

Design Principles

Clear ownership

Each component has a focused responsibility:

  • Clusterer owns clustering structure.
  • FeatureExtractor owns per-cluster feature extraction before topic naming.
  • LayerDependentFeatureExtractor owns per-cluster features requiring completed lower layers.
  • Template owns prompt construction.
  • LLMNamer owns communication with a language model.
  • Embedder owns generation of new semantic vectors.
  • TopicModel owns learned topic information and intermediate outputs.
  • Toponymy orchestrates these components.

Inspectable intermediate state

A fitted model should expose more than final topic names. Users and developers should be able to inspect:

  • cluster layers and hierarchy;
  • per-cluster extracted features;
  • rendered prompts;
  • generated topic names;
  • name embeddings, when generated;
  • summaries and explanations, when enabled;
  • disambiguation inputs and results.

This makes debugging, evaluation, serialization, and custom workflows practical.

Controlled mutation

A fitted Clusterer should remain reusable. Passing it to Toponymy must not add prompts, names, features, or other topic-model runtime state to the clusterer.

Topic-specific state belongs in TopicModel. Stage outputs should be explicit records and treated as immutable once produced.

Standard estimator conventions

Components follow scikit-learn-style conventions where practical:

  • Constructor arguments configure an object.
  • fit(...) supplies data and learns state.
  • Learned attributes use an underscore suffix, such as features_ or cluster_layers_.
  • A component may be fitted before it is supplied to Toponymy.

Core Objects

Toponymy

Toponymy is the primary user-facing estimator and workflow orchestrator.

It is configured with:

It is configured with:

  • a Clusterer;
  • FeatureExtractor instances;
  • optional LayerDependentFeatureExtractor instances;
  • a Template;
  • an LLMNamer;
  • an optional Embedder.

FeatureExtractor instances derive per-cluster features before topic naming begins. LayerDependentFeatureExtractor instances derive per-cluster features after lower topic layers have been named.

During construction, Toponymy validates its configured components. In particular, it determines whether any configured extractor or post-processing component requires an embedder. If one is required but no embedder was supplied, construction fails with a useful error before clustering or LLM calls begin.

During fit(...), Toponymy coordinates clustering, per-cluster feature extraction, prompt construction, topic naming, and optional disambiguation. Its principal learned result is a TopicModel.

model = Toponymy(
    clusterer=PLSCANClusterer(),
    feature_extractors=[
        ExemplarTextExtractor(),
    ],
    template=DefaultTemplate(),
    llm_namer=llm_namer,
)

model.fit(
    objects=documents,
    embedding_vectors=document_embeddings,
    clusterable_vectors=reduced_embeddings,
)

topic_model = model.topic_model_

The exact public constructor may evolve during the v0.6 refactor, but these responsibilities are the intended architecture.

Clusterer

A Clusterer produces a hierarchical clustering structure from vectors.

Its responsibilities are deliberately narrow:

  • accept clusterable vectors in fit(...);
  • produce cluster_layers_;
  • produce cluster_tree_;
  • validate that the hierarchy is internally consistent.

A clusterer does not construct prompts, extract topic features, call LLMs, or store topic names.

Cluster Layers

cluster_layers_ is a sequence of label arrays, one for each hierarchy layer.

  • Layer 0 is the most detailed layer.
  • Higher layer indices represent broader groupings.
  • Each label array has one entry per input object.
  • Non-negative integers identify clusters.
  • The label -1 identifies background noise, following the HDBSCAN convention.

For example:

cluster_layers_ = [
    [0, 0, 1, 1, -1],  # detailed topics
    [0, 0, 0, 0, -1],  # broader topic
]

Cluster Tree

cluster_tree_ describes inclusion relationships between clusters in adjacent layers. It allows Toponymy to understand which detailed topics contribute to each broader topic.

Built-in Clusterers

The intended built-in clusterers are:

  • PLSCANClusterer, based on fast_hdbscan, and the default clusterer;
  • EVoCClusterer, based on EVoC, where dependency compatibility permits;
  • a precomputed or label-based clusterer for user-supplied layer labels.

A clusterer may be fitted independently before it is supplied to Toponymy. This enables inspection of topic counts and hierarchy shape before any LLM calls occur.

TopicModel

TopicModel is the learned, topic-oriented result of running Toponymy.

It is initialized from a cluster hierarchy and progressively enriched as the workflow runs. It is the primary place for users and developers to inspect a fitted model.

A TopicModel owns:

  • the topic hierarchy and layer structure;
  • topic-level metadata;
  • per-cluster feature outputs produced before naming;
  • per-cluster feature outputs dependent on previously named layers;
  • prompts;
  • generated names;
  • name embeddings, where applicable;
  • summaries and explanations, when used;
  • disambiguation results;
  • serialization-friendly representations of the learned model.

This separates the question “how were these objects clustered?” from the question “what topics did we learn from those clusters?”

The existing serialization-oriented TopicModel API is part of the public compatibility surface. The runtime topic model should evolve that type rather than create an unrelated competing representation.

FeatureExtractor

A FeatureExtractor derives useful information for each cluster before topic naming begins.

Typical examples include:

  • exemplar texts;
  • keyphrases;
  • contrastive terms;
  • TreeSHAP-derived feature descriptions;
  • image captions or visual exemplars;
  • structured metadata summaries.

A feature extractor produces a learned features_ output aligned to clusters and layers. It should retain derived features rather than large raw input objects whenever possible. This keeps memory use and serialized models manageable.

Standard and Custom Inputs

Some extractors can be fitted using standard inputs available to Toponymy, such as:

  • objects;
  • embedding_vectors;
  • clusterable_vectors;
  • the cluster hierarchy.

These extractors may be automatically fitted during Toponymy.fit(...).

Other extractors require custom data, such as a DataFrame containing domain-specific metadata. Those extractors should be fitted explicitly before being passed to Toponymy.

metadata_extractor = MetadataFeatureExtractor()
metadata_extractor.fit(
    metadata=metadata_dataframe,
    clusterer=clusterer,
)

model = Toponymy(
    clusterer=clusterer,
    feature_extractors=[metadata_extractor],
    template=DefaultTemplate(),
    llm_namer=llm_namer,
)

This preserves flexibility without forcing Toponymy to store arbitrary user data.

LayerDependentFeatureExtractor

A LayerDependentFeatureExtractor derives per-cluster features that require results from previously named layers.

Unlike ordinary FeatureExtractor instances, it cannot run before topic naming begins. It is skipped for layer 0, then runs once per higher layer after its required lower-layer results are available.

The central example is SubtopicExtractor:

  • detailed topics are named first;
  • their names and embeddings become evidence for broader topics;
  • the extractor derives relevant subtopics for each broader cluster;
  • those subtopics are stored in the TopicModel;
  • the template reads them when constructing prompts for the higher layer.

Both extractor types produce per-cluster results. The difference is temporal:

  • FeatureExtractor results do not depend on generated topic names.
  • LayerDependentFeatureExtractor results do depend on outputs from earlier naming stages.

Template

A Template defines how topic-model data and extracted features become prompts for an LLM.

A template receives typed information for a particular topic from the TopicModel, such as:

  • the current layer and cluster;
  • exemplar texts;
  • keyphrases;
  • subtopics;
  • corpus and object descriptions;
  • requested detail level;
  • prior topic names, summaries, and explanations.

It produces a prompt record that can be inspected and stored in the TopicModel.

Templates should arrange information; they should not perform clustering, derive features, or call an LLM.

LLMNamer

An LLMNamer wraps a language-model API or local language model used to generate topic names.

It receives rendered prompts and returns structured naming results, such as:

  • a topic name;
  • a summary;
  • an explanation;
  • optional provider metadata or raw response data.

The wrapper isolates provider-specific behavior from the topic-model workflow.

Embedder

An Embedder produces vector representations for new text or other objects.

It is separate from LLMNamer because an embedding model may be local, API-backed, or entirely different from the model used for naming.

An embedder is optional for Toponymy itself. It is required only when a selected component needs to embed newly created content, such as keyphrases or generated topic names.

Existing embedding_vectors supplied to fit(...) represent the original objects. They do not replace an embedder when a workflow must embed new strings produced during fitting.

Components That May Require an Embedder

An Embedder is optional for Toponymy itself. It is required only when a configured component must create vector representations for newly generated text, such as keyphrases or topic names.

The embedding_vectors supplied to fit(...) represent the original objects. They do not replace an embedder when the selected pipeline needs vectors for new strings produced during fitting.

Component Embedder requirement Reason
ExemplarTextExtractor No It can use supplied object embeddings or other selection methods.
KeyphraseExtractor Yes Semantic keyphrase selection and cluster-level keyphrase ranking use keyphrase vectors.
SubtopicExtractor Yes It compares prior topic names using topic-name embeddings.
Disambiguator Declared by the implementation Some methods identify similar names through embeddings; others can use text matching or LLM-only comparison.
Naming from exemplars only No The LLM can name topics directly from exemplar text.

Toponymy validates these requirements during initialization. If a configured component requires an embedder but none was supplied, it raises a clear error before clustering or LLM calls begin.

Toponymy(
    clusterer=PLSCANClusterer(),
    feature_extractors=[
        ExemplarTextExtractor(),
        KeyphraseExtractor(),
    ],
    template=DefaultTemplate(),
    llm_namer=llm_namer,
)

This configuration should raise an error similar to:

KeyphraseExtractor requires an embedder. Pass embedder=... or remove
KeyphraseExtractor from feature_extractors.

When an embedder-dependent component needs representations of generated topic names, Toponymy creates and retains those embeddings in the TopicModel as part of that component’s workflow.

Common Configurations

Exemplar-Driven Naming

This is the intended lightweight default. It requires a clusterer and an LLM namer, but no embedder for newly generated text.

Toponymy(
    clusterer=PLSCANClusterer(),
    feature_extractors=[ExemplarTextExtractor()],
    template=DefaultTemplate(),
    llm_namer=llm_namer,
)

Keyphrase-Enhanced Naming

Keyphrases are optional rather than default-enabled. They can be useful for distinctiveness and may reduce later disambiguation work, but current evaluation evidence does not show a general topic-quality benefit sufficient to require their cost in every workflow.

Toponymy(
    clusterer=PLSCANClusterer(),
    feature_extractors=[
        ExemplarTextExtractor(),
        KeyphraseExtractor(),
    ],
    template=DefaultTemplate(),
    llm_namer=llm_namer,
    embedder=embedder,
)

Hierarchical Naming With Subtopics

Subtopics are layer-dependent per-cluster features. They require a clusterer, detailed-layer names, and an embedder for those names.

Toponymy(
    clusterer=PLSCANClusterer(),
    feature_extractors=[ExemplarTextExtractor()],
    layer_dependent_feature_extractors=[SubtopicExtractor()],
    template=DefaultTemplate(),
    llm_namer=llm_namer,
    embedder=embedder,
)

Layer Processing Order

Toponymy processes topic layers from the most detailed layer upward.

For each layer:

  1. Obtain the current layer’s clusters from the TopicModel.
  2. For layers above the base layer, run configured LayerDependentFeatureExtractor instances.
  3. Read the current cluster’s available features from the TopicModel.
  4. Use the selected Template to render prompts.
  5. Use the LLMNamer to generate topic names and related outputs.
  6. Optionally disambiguate names that are too similar.
  7. Generate topic-name embeddings when selected later stages require them.
  8. Store all results in the TopicModel.

The ordering matters because a broad topic may be named using completed names from detailed topics beneath it.

Extending Toponymy

Add a Clusterer

A new clusterer should:

  1. Implement the clusterer interface.
  2. Produce valid cluster_layers_ and cluster_tree_.
  3. Preserve -1 as the noise label.
  4. Avoid topic-model-specific state.
  5. Include tests for hierarchy validity and pre-fitted reuse.

Add a Feature Extractor

A new feature extractor should:

  1. Implement the feature extractor interface.
  2. Define its per-cluster fitted features_ output clearly.
  3. State whether Toponymy can fit it from standard inputs.
  4. Declare whether it requires an embedder.
  5. Avoid retaining unnecessary raw input data.
  6. Include unit tests for feature shape, cluster alignment, and fitted-state behavior.

Add a Layer-Dependent Feature Extractor

A layer-dependent extractor should:

  1. Define which prior-layer outputs it requires.
  2. Accept the current TopicModel and target layer.
  3. Declare whether it requires an embedder.
  4. Produce a layer-scoped, per-cluster immutable feature result.
  5. Include a multi-layer test proving it runs only after prerequisites are available.

Add a Template

A new template should:

  1. Accept the documented per-topic prompt inputs from the TopicModel.
  2. Produce deterministic prompt records for deterministic inputs.
  3. Avoid direct LLM calls.
  4. Include fixture-based tests for rendered prompt content.

Migration Notes

This architecture is the target direction for the v0.6 refactor. During the transition, compatibility adapters may preserve older interfaces such as ToponymyClusterer, ClusterLayer behavior, existing prompt-template dictionaries, and serialization methods.

New development should prefer these ownership boundaries:

  • clustering structure belongs to Clusterer;
  • learned topic information belongs to TopicModel;
  • orchestration belongs to Toponymy;
  • prompt formatting belongs to Template;
  • pre-naming and layer-dependent per-cluster feature derivation belongs to extractor classes;
  • optional vector generation belongs to Embedder.

Clone this wiki locally