Skip to content

Getting started with pgokf

This is an end-to-end, copy-pasteable first run. By the end you will have installed the extension, fixed the one permission gotcha every new operator hits, confined bundle registration to a directory you control, registered a bundle of OKF concepts, searched it full-text, walked its link graph, and retrieved a concept's original source bytes.

Everything below was run against a live PostgreSQL 18 cluster with pgokf 0.2.0 installed; the output blocks are the real output, lightly trimmed for width. If your numbers differ, it is because your bundle differs, the shapes will match.


Prerequisites

  • PostgreSQL 15, 16, 17, 18, or 19 with the pgokf extension files installed into the cluster's SHAREDIR/extension and the shared library into PKGLIBDIR (see packaging.md for how the artifacts get there).
  • A superuser connection for the one-time install and role setup.
  • A directory of OKF Markdown files to register - this walkthrough uses the bundle shipped at examples/sample-bundle, which contains four concepts (a service, a runbook, a reference appendix, and a dashboard) that cross-link each other so the graph and search steps have something to show.

Copy the sample bundle somewhere the PostgreSQL server process can read:

mkdir -p /srv/okf
cp -r examples/sample-bundle /srv/okf/knowledge

The server reads the path, not your shell. register_bundle is executed inside the PostgreSQL backend, so the path must be readable by the OS user that runs postgres (commonly postgres), not by you. A path that works in your terminal but lives under /home/you will typically fail with a permission error from the server side. Put bundles somewhere the server owns - /srv/okf/... here.


1. Install the extension (superuser)

Connect as a superuser and create the extension. This runs the bootstrap SQL, which creates the pgokf (public API) and pgokf_private (internal state) schemas, the catalog tables and composite types, and the three cluster-wide roles pgokf_reader, pgokf_writer, and pgokf_admin.

CREATE EXTENSION pgokf;
SELECT pgokf.version();
CREATE EXTENSION
 version
---------
 0.2.0
(1 row)

pgokf.version() reports the loaded shared library's version; after an upgrade, compare it against the installed SQL version to confirm the two agree (see operations.md).

Make the GUCs cluster-wide. The pgokf.* server settings (resource ceilings such as pgokf.max_file_bytes) register when the shared library loads. The library loads lazily the first time a session calls a pgokf function, so a brand-new session sees unrecognized configuration parameter until it touches the extension. To make the ceilings visible in every session from connection start - and to be able to set them in postgresql.conf - add the library to shared_preload_libraries and restart:

# postgresql.conf
shared_preload_libraries = 'pgokf'

This is optional for a first run but recommended for any real deployment.


2. The role model, and the 42501 gotcha

pgokf ships three NOLOGIN roles in a tier. You never log in as them; you GRANT them to your real login users. Each tier inherits the one below it.

Role May do
pgokf_reader Search and read: concept_search, concept_neighbors, list_bundles, bundle_info, get_config, get_concept_source, and SELECT on the catalog tables.
pgokf_writer Everything a reader can, plus ingestion: register_bundle, register_bundle_content, refresh_bundle, unregister_bundle, set_bundle_enabled, retire_bundle, unretire_bundle, and set_concept_embedding. This is the tier an ingestion pipeline runs as. pgokf_writer inherits pgokf_reader.
pgokf_admin Everything a writer can, plus configuration (set_config, reset_config), the file-writing exports (export_parquet, export_sources), purge_retired, the index rebuilds (rebuild_search_index, rebuild_embedding_index), and list_access_log. pgokf_admin inherits pgokf_writer.

The bootstrap deliberately does not grant any of these roles to PUBLIC. Schema USAGE on pgokf is granted only to those three roles. So a fresh login role, even one you just created, gets SQLSTATE 42501, permission denied for schema pgokf on its very first query, before it even reaches a function's own privilege check:

CREATE ROLE okf_app LOGIN;
SET ROLE okf_app;
SELECT * FROM pgokf.list_bundles();
CREATE ROLE
SET
ERROR:  permission denied for schema pgokf
LINE 1: SELECT * FROM pgokf.list_bundles();
                      ^

This is not a misconfiguration, it is the least-privilege default working as designed. The fix is a single grant. Back in your superuser session:

RESET ROLE;                       -- leave the okf_app role
GRANT pgokf_admin TO okf_app;     -- this operator also configures policy and exports

Ingesting bundles needs only pgokf_writer, so an ingestion pipeline user should be granted that tier; grant pgokf_reader for a user that should only search and read. This walkthrough grants pgokf_admin to okf_app because the same operator will also set allowed_roots (step 3) and run the exports (step 8), which are admin-only. See security.md for the full model, including why the in-function checks use session_user.


3. Confine registration with allowed_roots

register_bundle makes the PostgreSQL backend read arbitrary files from the host filesystem. Before registering anything, set allowed_roots so a bundle path must resolve inside a directory you have blessed. Any path outside every configured root - including one reached through a symlink - is rejected with SQLSTATE 22023.

set_config requires pgokf_admin, so do this as okf_app (or any admin):

SET ROLE okf_app;
SELECT pgokf.set_config('allowed_roots', '["/srv/okf"]'::jsonb);
SELECT pgokf.get_config();
SET
 set_config
------------

(1 row)

                                   get_config
--------------------------------------------------------------------------------
 {"store_source": false, "allowed_roots": ["/srv/okf"], "default_strict": true,
  "default_exclude": [], "search_backend": "native", "bm25_provider": "auto",
  "require_tenant": false, "notify_channel": "",
  "okf_version_policy": "warn", "embedding_dim": 1536, "track_history": false,
  "history_retention_days": 0, "sync_log_retention_days": 30,
  "default_text_search_config": "pg_catalog.english"}
(1 row)

get_config returns the whole effective policy as one JSON object. The keys and their meaning are documented in configuration.md; the two you will care about most on day one are allowed_roots (just set) and store_source (covered in step 7).

When allowed_roots is empty, the interim policy accepts any absolute, traversal-free, canonical path. Setting at least one root is strongly recommended for any shared or production cluster - see security.md.


4. Register the bundle

Now register the copied bundle. The second argument is an optional human name.

SET ROLE okf_app;
SELECT * FROM pgokf.register_bundle('/srv/okf/knowledge', 'knowledge');
 bundle_id |        path         | added | updated | removed | unchanged | total
-----------+---------------------+-------+---------+---------+-----------+-------
         1 | /srv/okf/knowledge  |     4 |       0 |       0 |         0 |     4

The returned bundle_sync_result is the per-bucket file accounting for the sync: four files discovered, all four newly added. Note the returned bundle_id - you pass it to refresh_bundle, bundle_info, export_parquet, and (optionally) the search and graph functions. The reserved files index.md and log.md at every directory level are not counted as concepts (each log.md activity log is instead projected into pgokf.bundle_log, readable via pgokf.list_bundle_log).

Confirm what is registered:

SELECT id, name, okf_version, file_count, enabled FROM pgokf.list_bundles();
 id |   name    | okf_version | file_count | enabled
----+-----------+-------------+------------+---------
  1 | knowledge |             |          4 | t

okf_version is blank here because the sample bundle's root index.md carries no okf_version frontmatter key. Add okf_version: "0.2" to a bundle-root index.md and the catalog will populate this column on the next sync - see okf-authoring.md and the reserved-file rules there.


concept_search(query, bundle_id DEFAULT NULL, limit_count DEFAULT 20) returns ranked hits. The query is a plain phrase; it is parsed with the configured text search configuration (pg_catalog.english by default) and matched against a weighted tsvector built from each concept's title (weight A), tags / type / description (B), and body (D).

SELECT bundle_id, concept_id, title, type, round(rank::numeric, 4) AS rank
  FROM pgokf.concept_search('failover');
 bundle_id |         concept_id         |       title        |   type    |  rank
-----------+----------------------------+--------------------+-----------+--------
         1 | runbooks/appendix          | Failover appendix  | Reference | 1.6000
         1 | runbooks/database-failover | Database failover  | Runbook   | 1.2000
         1 | services/postgresql        | PostgreSQL service | Reference | 0.3000

concept_id is the path-derived identity: the bundle-relative path without its .md suffix (runbooks/database-failover.mdrunbooks/database-failover). rank is ts_rank_cd, higher is more relevant. The full result also carries a headline snippet with the matching terms highlighted. concept_search also takes optional structured filters (concept type, tags, status, trust tier) and an after_cursor argument for keyset pagination, with search_facets for faceted counts; see search-guide.md for query syntax, ranking, filters, pagination, the optional BM25 / semantic / hybrid backends, and performance characteristics.

Constrain a search to one bundle by passing its id, and cap the result count (1–500) with the third argument:

SELECT concept_id, title FROM pgokf.concept_search('postgresql', 1, 5);

Concepts link to each other (in the sample bundle the service points at its runbook and dashboard, the runbook at its appendix). concept_neighbors(concept_id, max_hops DEFAULT 2, bundle_id DEFAULT NULL) returns every concept reachable within max_hops, with the shortest hop count and the path taken:

SELECT source_id, neighbor_id, hops, title
  FROM pgokf.concept_neighbors('services/postgresql', 2);
      source_id      |        neighbor_id         | hops |       title
---------------------+----------------------------+------+-------------------
 services/postgresql | dashboards/health          |    1 | Service health
 services/postgresql | runbooks/database-failover |    1 | Database failover
 services/postgresql | runbooks/appendix          |    2 | Failover appendix

max_hops is capped by the pgokf.max_graph_hops GUC (default 5), so a caller cannot ask for an unbounded traversal. The full concept_neighbor row also returns path, the array of concept ids from the start to that neighbor.


7. Retrieve a concept's source

Whether the original file bytes live inside PostgreSQL is governed by the store_source policy key, and it is the single biggest deployment decision - see deployment-topologies.md.

store_source defaults to false (the enterprise/data-lake tier: PostgreSQL holds metadata and the search index, the files stay in their external store). With it off, get_concept_source tells you plainly that no bytes are stored:

SELECT octet_length(pgokf.get_concept_source(1, 'runbooks/database-failover'));
ERROR:  no source is stored for concept runbooks/database-failover in bundle 1;
        the bundle was synced with store_source disabled

To make PostgreSQL self-contained (the small tier), enable store_source and re-index. store_source is not retroactive, and refresh_bundle re-projects only files whose content changed - so on an unchanged bundle a plain refresh will not backfill the sources:

SELECT pgokf.set_config('store_source', 'true'::jsonb);
SELECT * FROM pgokf.refresh_bundle(1);
 bundle_id |        path         | added | updated | removed | unchanged | total
-----------+---------------------+-------+---------+---------+-----------+-------
         1 | /srv/okf/knowledge  |     0 |       0 |       0 |         4 |     4

All four unchanged - nothing was re-projected, so still no stored bytes. To force a full re-projection of an unchanged bundle, unregister and register it again (or edit the files). Re-registering assigns a fresh bundle_id:

SELECT id FROM pgokf.unregister_bundle(1);
SELECT bundle_id FROM pgokf.register_bundle('/srv/okf/knowledge', 'knowledge');
-- bundle_id is now 2
SELECT octet_length(pgokf.get_concept_source(2, 'runbooks/database-failover'))
       AS source_bytes;
 source_bytes
--------------
         1096

The stored bytes are the exact, unmodified source file (they hash to pgokf.concepts.file_hash). Decode them as text with convert_from(..., 'UTF8'). The retrieval nuance above is exactly the kind of thing operations.md covers for day-2 work.


8. (Optional) Export for analytics or DR

An admin can snapshot a bundle's catalog projection to Parquet, and - when sources are stored - export the original files back to disk:

-- concepts / metadata / links / provenance as four Parquet files
SELECT bundle_id, concepts_rows, metadata_rows, links_rows,
       provenance_rows, bytes_written
  FROM pgokf.export_parquet(2, '/srv/okf/export');

-- the stored source files, reconstructed under dest_dir (store_source only)
SELECT bundle_id, concepts_rows, bytes_written
  FROM pgokf.export_sources(2, '/srv/okf/export');
 bundle_id | concepts_rows | metadata_rows | links_rows | provenance_rows | bytes_written
-----------+---------------+---------------+------------+-----------------+---------------
         2 |             4 |             9 |         12 |               4 |         15272

 bundle_id | concepts_rows | bytes_written
-----------+---------------+---------------
         2 |             4 |          2615

dest_dir must already exist, be writable by the server, and - when allowed_roots is set - resolve inside one of the roots, exactly like a bundle path. The Parquet files are interoperable with tools such as DuckDB. See operations.md for using exports in backup/DR and analytics pipelines.


Where to go next

You want to… Read
Write your own OKF concepts okf-authoring.md, templates/
Decide where catalog and files live deployment-topologies.md
Run it day-to-day operations.md
Get better search results search-guide.md
Every function, table, and type sql-api.md
Tune ceilings and policy configuration.md
Understand the security model security.md
Diagnose an error troubleshooting.md

Tear down (for a scratch run)

DROP EXTENSION pgokf;   -- drops the pgokf / pgokf_private schemas and their objects

The three roles are cluster-wide and survive DROP EXTENSION (they may be shared across databases); drop them explicitly with DROP ROLE pgokf_reader, pgokf_writer, pgokf_admin; only if nothing else uses them.