Topic Registry

academy

Topic Registry

Definition & Overview

The Topic Registry is an ETS-backed GenServer within the Prismatic Academy application that serves as the central catalog for all self-registering learning topics. It follows the same metaprogramming pattern used by the OSINT ToolRegistry and DD SourceRegistry, where topic modules declare their configuration via macros and automatically register themselves at compile time through @after_compile hooks.

The registry maintains three ETS tables: a primary topics table for O(1) slug-based lookups, an interconnections table that maps semantic relationships between topics using a directed graph, and a search index that enables full-text discovery across topic titles, descriptions, and tags. This tri-table architecture ensures that common access patterns, whether looking up a single topic, navigating related content, or searching across the curriculum, all complete in sub-millisecond time.

What distinguishes the Topic Registry from a simple key-value store is its active role in maintaining pedagogical coherence. The InterconnectionEngine, which works in tandem with the registry, automatically discovers relationships between topics based on shared categories, overlapping tags, prerequisite chains, and content similarity. This transforms the flat list of topics into a navigable knowledge graph that supports intelligent learning path recommendations.

Technical Deep Dive

The Topic Registry's implementation follows the established self-registering pattern with academy-specific enhancements for learning path management:

defmodule PrismaticAcademy.TopicRegistry do
  @moduledoc """
  ETS-backed registry for self-registered Academy topics.
  Maintains topics, interconnections, and search index
  across three dedicated ETS tables.
  """

  use GenServer

  @topics_table :academy_topics
  @interconnections_table :academy_interconnections
  @search_index_table :academy_search_index

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  @impl true
  def init(_opts) do
    :ets.new(@topics_table, [:set, :named_table, :public, read_concurrency: true])
    :ets.new(@interconnections_table, [:bag, :named_table, :public, read_concurrency: true])
    :ets.new(@search_index_table, [:bag, :named_table, :public, read_concurrency: true])

    {:ok, %{initialized_at: DateTime.utc_now()}}
  end

  @spec register(map()) :: :ok
  def register(%{slug: slug} = config) do
    :ets.insert(@topics_table, {slug, config})
    index_topic(slug, config)
    :ok
  end

  @spec get_topic(String.t()) :: {:ok, map()} | {:error, :not_found}
  def get_topic(slug) do
    case :ets.lookup(@topics_table, slug) do
      [{^slug, config}] -> {:ok, config}
      [] -> {:error, :not_found}
    end
  end

  @spec list_topics(keyword()) :: [map()]
  def list_topics(filters \\ []) do
    category = Keyword.get(filters, :category)
    difficulty = Keyword.get(filters, :difficulty)

    :ets.foldl(fn {_slug, config}, acc ->
      matches_category = is_nil(category) or config.category == category
      matches_difficulty = is_nil(difficulty) or config.difficulty == difficulty

      if matches_category and matches_difficulty do
        [config | acc]
      else
        acc
      end
    end, [], @topics_table)
  end

  @spec search(String.t()) :: [map()]
  def search(query) do
    normalized = String.downcase(query)

    @search_index_table
    |> :ets.foldl(fn {term, slug}, acc ->
      if String.contains?(term, normalized) do
        MapSet.put(acc, slug)
      else
        acc
      end
    end, MapSet.new())
    |> Enum.flat_map(fn slug ->
      case get_topic(slug) do
        {:ok, config} -> [config]
        _ -> []
      end
    end)
  end

  defp index_topic(slug, config) do
    terms = [
      String.downcase(config.name),
      String.downcase(config.description || ""),
      Enum.join(config.tags || [], " ") |> String.downcase()
    ]

    Enum.each(terms, fn term ->
      term
      |> String.split(~r/\s+/)
      |> Enum.each(fn word ->
        :ets.insert(@search_index_table, {word, slug})
      end)
    end)
  end
end

The Topic behaviour and registration macro mirror the OSINT Tool pattern:

defmodule PrismaticAcademy.Topic do
  @moduledoc """
  Behaviour and registration macro for Academy topics.
  Modules using this behaviour self-register at compile time.
  """

  @callback content() :: map()
  @callback exercises() :: [map()]
  @callback assessment() :: map()

  defmacro __using__(_opts) do
    quote do
      @behaviour PrismaticAcademy.Topic
      import PrismaticAcademy.Topic, only: [register_topic: 1]

      @after_compile __MODULE__

      def __after_compile__(env, _bytecode) do
        if function_exported?(env.module, :__topic_config__, 0) do
          config = env.module.__topic_config__()
          PrismaticAcademy.TopicRegistry.register(config)
        end
      end
    end
  end

  defmacro register_topic(config) do
    quote do
      def __topic_config__ do
        unquote(config)
        |> Map.put(:module, __MODULE__)
        |> Map.put(:registered_at, DateTime.utc_now())
      end
    end
  end
end

Architecture & Implementation

The Topic Registry operates within a broader Academy OTP supervision tree that includes five GenServers working in coordination:

TopicRegistry: The central catalog described above, holding all topic configurations and providing filtered queries. It starts first in the supervision tree since other components depend on it.

InterconnectionEngine: Uses Erlang's :digraph module to maintain a directed graph of topic relationships. After all topics are registered, it scans for shared categories, prerequisite declarations, overlapping tags, and content similarity to build semantic edges. This graph powers the "Related Topics" sidebar in the Academy LiveView dashboard.

ProgressTracker: Tracks per-user completion state for each topic. Reads the topic list from the registry and maintains progress records in ETS with PostgreSQL persistence for durability.

SessionManager: Manages active learning sessions, tracking which topics a user is currently studying and enforcing prerequisite chains by consulting the interconnection graph.

Telemetry: Emits events for topic views, exercise completions, assessment scores, and learning path progress, enabling time series analysis of educational outcomes.

The registry's read_concurrency: true ETS option ensures that concurrent LiveView processes serving the Academy dashboard can read topic data without contention, even under high load.

Usage in Prismatic Platform

The Topic Registry integrates with the Academy LiveView dashboard to provide dynamic filtering, search, and navigation across all registered topics:

defmodule PrismaticWeb.AcademyLive.Dashboard do
  use PrismaticWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    topics = PrismaticAcademy.TopicRegistry.list_topics()
    categories = topics |> Enum.map(& &1.category) |> Enum.uniq() |> Enum.sort()

    {:ok, assign(socket,
      topics: topics,
      categories: categories,
      selected_category: nil,
      search_query: ""
    )}
  end

  @impl true
  def handle_event("filter_category", %{"category" => category}, socket) do
    filter = if category == "all", do: [], else: [category: String.to_existing_atom(category)]
    topics = PrismaticAcademy.TopicRegistry.list_topics(filter)
    {:noreply, assign(socket, topics: topics, selected_category: category)}
  end

  @impl true
  def handle_event("search", %{"query" => query}, socket) do
    topics =
      if String.length(query) >= 2 do
        PrismaticAcademy.TopicRegistry.search(query)
      else
        PrismaticAcademy.TopicRegistry.list_topics()
      end

    {:noreply, assign(socket, topics: topics, search_query: query)}
  end
end

The registry pattern, shared across OSINT (ToolRegistry), Academy (TopicRegistry), and DD (SourceRegistry), represents one of the platform's most powerful architectural patterns. It enables each subsystem to grow by simply adding new modules without modifying any infrastructure code.

Cross-References

  • Tool - OSINT self-registering adapter using the same pattern
  • ETS - In-memory storage backing the registry
  • Metaprogramming - Compile-time code generation technique
  • Academy - Learning platform subsystem
  • Track - Learning path built from registered topics

Created by Tomas Korcak (korczis) | GitHub