Documentation
Find stored pages, query their text and links, and export results.
Explore with SQL
Open the signed-in app to query stored page text, structure and links. Sign in and select your organisation, then write SQL in its workspace. Review and run it, then download the returned rows as CSV or JSON.
To add sources, submit a discovery and follow its progress.
Run your first query
Periplus exposes a ClickHouse catalogue through native ClickHouse SQL. Run queries against the configured catalogue or through the query service. Each row in captures has retained HTML. The same URL can appear more than once, and retained HTML HTTP error responses are included. Captured HTML may reflect browser rendering rather than the original HTTP response body.
Recent captures
The query API defaults to public_v1: use captures or public_v1.captures. Requests may specify schema_version: "public_v1"; unsupported versions are rejected. Responses report the resolved schema version separately from the data snapshot. Each query uses one consistent materialization version while new captures continue arriving.
Collection and coverage
Periplus observes a page in a browser after configured loading and interaction steps. That document records one observed state of a page, rather than every possible interactive or personalised state.
Each capture owns an immutable archive of its exact HTML. Retention policies protect capture history; identical bytes share a content identity but retain independent archives. It records captures, then builds HTML and link projections separately. Collection can finish before those projections are queryable.
Coverage includes selected sources. It offers neither a representative sample of the web nor a uniform refresh schedule. Collection dates can be missing; the latest date for a site does not establish that all its pages are fresh.
Submit finite discovery through the control API. A link found in a page does not mean its destination has been collected. Check available captures and projections before relying on a source in your dataset.
Discovery and page policies
Discovery starts from a list of URLs, with maximum depth and a total page limit. Depth zero captures starting pages only; deeper discovery follows links within their sites.
A page limit means “up to N”, including starting pages. Discoveries can share acquisitions while retaining their own selection and accounting. Discovery is one-off and does not create retention or refresh policies.
Request membership and readiness are available in coverage APIs and the request view. The public SQL schema contains page, capture and content evidence.
Independent page policies
Use Retention to protect exact capture IDs until a fixed expiry measured from claim activation. The latest expiry applies on overlapping claims, with a seven-day service baseline. No future captures are automatically included. This does not fetch pages or restore deleted history.
Use Refresh to request another capture every N days, without following links. The shortest active interval applies; successful captures from other work also satisfy freshness. Failures retry with backoff. Refresh does not extend retention.
Freshness accepts returned URLs in Explore, or SQL returning a url column. SQL runs once and saves up to 1,000 fixed pages. Retention accepts up to 64 exact capture IDs per claim. Review the draft before activating it. Later SQL results do not change membership. Pausing or removing policies does not immediately delete captures; already-started refresh captures may finish.
Reproducibility
SQL preserves the extraction method. Identical results also require the same input data, schema, and deterministic semantics. A source snapshot reference records the input version; it does not pin a future rerun.
Search documents
search(query) returns distinct documents with a score and an optional plain-text excerpt. Repeated captures of identical documents do not duplicate search results. Join captures for URLs, observation times and provenance.
Search ranks all matching candidates by distinct query-term coverage, with smaller title and phrase bonuses. It returns at most 100 documents, ordered by score and then document identity. Query tokenization follows the text index and removes a small English stopword list. Empty or stopword-only queries return no matches.
Search uses prose excluding head, script, style, template and explicitly hidden subtrees. It does not evaluate CSS or identify a main article. The exact parsed text remains available through documents and nodes. Snippets are excerpts, not summaries.
Explore links
walk(page_id, max_depth = 2, direction = 'out') visits each reachable page once at its minimum hop count, including the starting page at depth zero. Use direction = 'in' to follow backlinks. For example, audit documentation reachable within three clicks, or find pages affected by a broken destination.
paths(from_page_id, to_page_id, max_depth = 6, limit = 10) returns up to the requested number of shortest distinct outgoing routes. Each route visits a page at most once. For example, compare navigation routes from a university homepage to its scholarship application.
Supply page IDs as positional parameters, or replace ? with quoted page_id values from captures or pages. Arguments must be literals or parameters. Named options accept = or :=. Depth is 0–8; path limit is 1–20. SQL ordering is explicit: order walks by depth and paths by path_id, step.
| Columns | Meaning |
|---|---|
| page_id, url | Reached page. |
| depth, predecessor_page_id | Walk distance and previous page on one deterministic shortest route. |
| path_id, path_length, step | Route number starting at 1, total hop count, and position starting at 0. |
| capture_id, document_id, node_index | Source link used for this step. Join nodes or call node_text to read its anchor. Root rows have NULL evidence. For incoming walks, the link points from the reached page toward its predecessor. |
Traversal selects the latest visible complete HTML capture of each page. Dated captures precede undated ones; capture ID descending breaks ties. All steps use one published corpus version. This combines observations from different times; it is not a simultaneous snapshot of the live web. A linked destination without a retained capture is an outgoing leaf. Unknown starting pages return no rows.
Repeated anchors between the same two pages count as one connection, using the lowest node index as evidence. Walks avoid repeated pages; paths distinguish page sequences rather than individual anchor occurrences. A start equal to its destination yields one zero-hop route when that page exists.
At most two graph calls share a per-request budget of 5,000 pages, 50,000 link occurrences, 16 MiB of lookup responses and 1,000,000 search steps, alongside the query deadline. Exceeding a traversal budget fails the query rather than silently omitting routes. An outer LIMIT only limits returned rows. Normal result truncation can still cut a route; check the response’s truncation flag.
Graph results are ordinary table sources: join them to captures, documents or nodes. Outer filters apply after traversal; they do not restrict which edges are explored. Preparation checks the output schema without traversing; execution performs the bounded search.
Identity and joins
Different captures can share one document. A document_id identifies the retained bytes together with their HTML interpretation. Joining a document to captures repeats it for each matching capture.
| From → to | Join on |
|---|---|
| Document → original bytes | document_id |
| Capture → HTML relations | document_id |
| Capture → link occurrences | capture_id |
| Link occurrence → capture → HTML element | document_id + node_index |
Use a LEFT JOIN to retain captures without matching HTML structure. An INNER JOIN keeps only matches. Counting joined HTML rows counts elements, not pages; choose distinct URLs, capture IDs, or document IDs according to your question.
For a parent element, match both document_id and child.parent_index = parent.node_index. A subtree occupies node indices from node_index inclusive to subtree_end_index exclusive.
The complete parsed tree includes document, element, text, comment and template-fragment nodes. Indices are contiguous preorder positions. Sibling positions count every node kind, and parent_index references the immediate parent. Node 0 is the document root at depth 0. Attributes remain attached to elements as an object. Filter nodes by kind, name and namespace to inspect headings, metadata or scripts. Node positions belong to the selected materialization; original byte syntax remains in the archived HTML.
Retrieve original captured bytes with GET /api/documents/{document_id}/content. The download returns the original bytes for that document. The catalogue exposes capture metadata and parsed HTML. A source URL identifies the live site; its current content may differ from the retained capture.
SQL patterns
Replace the illustrative document IDs below with document_id values from your own captures. Retention and catalogue coverage affect whether an example returns rows. Exact document filters keep extraction focused; a final LIMIT alone does not.
Choose one capture per URL
Explore outgoing links
Find pages without a capture
Choose pages to explore
Find matching HTML headings
Read a page outline
Inspect image declarations
Text relations preserve source text order without automatic trimming or inserted separators. Apply normalization explicitly. Missing projections can mean indexing is still pending; missing rows alone do not prove a page has no headings, forms, or tables.
Public schema
The public_v1 namespace exposes these five views. A page is a normalized URL, including linked destinations without captures. Redirects and canonical declarations do not merge page identities. These are public views. Their keys describe logical identity; they are not declarations of database-enforced primary keys on the views. The SQL console schema explorer shows the deployed relations.
public_v1.pages
page_id| Column / type | Meaning |
|---|---|
page_idString | SHA-256 identity of the normalized URL. |
urlString | Normalized URL. Redirects and canonical declarations do not merge addresses. |
scheme, host, path, queryString | URL components; query parameter order is preserved. |
SELECT * FROM public_v1.pages LIMIT 1;public_v1.captures
capture_id| Column / type | Meaning |
|---|---|
capture_idUUID | Observation identity. |
page_id, urlString | Observed address; references pages. |
requested_urlString | Address requested for this observation. |
effective_urlNullable(String) | Final address, when available. |
observed_atNullable(DateTime64(6, 'UTC')) | Observation time; unknown times remain null. |
timestamp_precisionString | microsecond, second, or unknown. |
http_statusNullable(UInt16) | Received status, including retained HTML error responses. |
document_idString | Shared document identity. |
source_provider, source_dataset, source_record_idString / Nullable(String) | Archival source provenance when available. |
SELECT * FROM public_v1.captures LIMIT 1;public_v1.documents
document_id| Column / type | Meaning |
|---|---|
document_idString | Identity of retained bytes and their HTML interpretation. |
content_idString | SHA-256 of the exact retained payload bytes. |
representationString | rendered_html or response_body. |
encodingString | Character encoding used for parsing. |
byte_lengthUInt64 | Payload size before storage compression. |
textString | All parsed text in document order, preserving whitespace without added separators. |
node_count, element_countUInt64 | Complete tree size and its element subset. |
SELECT * FROM public_v1.documents LIMIT 1;public_v1.nodes
document_id + node_index| Column / type | Meaning |
|---|---|
document_idString | Owning shared document. |
node_indexUInt32 | Zero-based preorder position. Node 0 is the document root. |
parent_indexNullable(UInt32) | Immediate parent node; null at the root. |
subtree_end_indexUInt32 | Exclusive end of this node's subtree. |
sibling_index, depthUInt32 | Position among all sibling nodes, and depth from the document root. |
kindString | document, element, text, comment, doctype, document_fragment, or processing_instruction. |
name, namespaceNullable(String) | Node name and namespace where applicable. |
valueNullable(String) | Character data, or a parsed doctype declaration. Elements have null values. |
attributesMap(String, String) | Element attributes as an object. Use mapContains to distinguish missing from empty values. Namespaced keys use {namespace}name. |
text_directString | Immediate text children of an element. |
textString | All text nodes within this subtree, with original parsed whitespace. |
SELECT * FROM public_v1.nodes LIMIT 1;public_v1.links
capture_id + node_index| Column / type | Meaning |
|---|---|
capture_idUUID | Source observation; URL resolution belongs to this capture. |
document_id, node_indexString, UInt32 | Source anchor node, preserving repeated links. |
target_page_id, target_urlString | Destination page identity and normalized URL, excluding the fragment. |
raw_hrefString | Literal parsed href attribute. |
resolved_urlString | Normalized destination including its fragment when present. |
fragmentNullable(String) | Destination fragment when present. |
SELECT * FROM public_v1.links LIMIT 1;Select HTML elements
query_selector(document_id, selector) returns the first matching element’s node index, or NULL when no element matches. query_selector_all(document_id, selector) returns a table of matching document_id and node_index pairs. Use these identities with the document-slice functions below.
Selectors support tags, IDs, classes, attributes, combinators, selector lists, structural pseudo-classes and :is(), :where(), and :not(). They inspect the stored HTML tree; they do not evaluate live browser state, CSS visibility, or :has().
Document IDs may be literals, parameters, or column expressions. Selector strings must be literals or parameters. Select candidate documents before expanding elements. Use ORDER BY node_index for document order; the table helper returns no rows for missing matches. LEFT JOIN LATERAL with ON TRUE retains a document when nothing matches.
Extract a document slice
Select a node, then use node_text(document_id, node_index), node_html(document_id, node_index), or node_tree(document_id, node_index). Node 0 selects the entire document. Missing nodes return NULL.
Text preserves whitespace and includes script, style and template text without inserting separators. HTML is serialized from the parsed tree. The tree result is a JSON string containing node identities, attributes and ordered children, including individual text and comment nodes. Parse it as JSON in your client, or inspect it with ClickHouse JSON functions.
These helpers run through the query service with the same selected publication and resource limits as the rest of the statement. Filter documents before reconstructing subtrees. Use mapContains(attributes, 'alt') to distinguish a missing attribute from an explicitly empty one.
Query limits and behavior
The query API accepts SQL and positional parameters through POST /api/v1/sql. The public website does not proxy SQL or schema requests. Set operation to prepare to explain a SELECT; execution returns rows. GET /api/v1/schema describes the installed helpers. Responses preserve submitted SQL and report any applied compiler optimizations. ClickHouse optimizes the execution plan.
These limits apply to the query service. Direct ClickHouse connections use their connection settings and do not inherit the service’s HTTP result caps.
| Capability | Behavior |
|---|---|
| SQL | One read-only statement over the public views. Use operation: prepare to inspect the plan. |
| Execution | Operator-configured interrupt deadline (300 seconds maximum). ClickHouse enforces memory and read-work limits in addition to the request deadline. |
| Results | Operator-configured row and result-size limits (1,000 rows and 8 MiB by default). Truncation is reported explicitly; returned rows may be incomplete. |
| Busy service | One operation per query process. Excess requests receive 429 with Retry-After; there is currently no queue. |
| Parameters | Use ? placeholders and supply positional values in the request parameters array. |
| Numbers | Decimals and integers outside JavaScript’s safe range arrive as strings, alongside SQL type information. |
A LIMIT bounds output, not necessarily work. Sorting, aggregation, and joins may still scan substantial data. Filter captures and select the columns you need before expanding HTML. Preparation also consumes a bounded execution slot.
Acquisitions without retained HTML do not appear in captures. A published capture references a completed document tree. A missing matching element means the selection found no match in that tree. HTML projections may become available after collection finishes. Capture times describe captured data, not a guarantee of freshness; live website links can show different content.
Python SDK
Use the SDK from the same Periplus source checkout as this deployment, with Python 3.11 or newer. These public_v1 examples require the updated source SDK; the previously published 0.2.0 release predates this contract. Configure the query API origin, not the public website URL. See the source SDK README for its authentication contract.
python -m pip install ./clients/periplus-python-sdkfrom periplus_sdk import Client
with Client("https://your-periplus-host") as client:
result = client.execute(
"SELECT capture_id FROM public_v1.captures LIMIT ?", [10]
)
print(result.columns, result.rows)
print(result.schema_version, result.source_snapshot, result.truncated)prepare(sql, parameters) returns diagnostics and a plan. execute(sql, parameters) independently validates and runs the query. helpers() describes installed SQL helpers. Use AsyncClient with an async context manager and await these methods for asynchronous Python.
The same public access settings and query limits apply. Results preserve SQL types and explicit truncation; the SDK never retries or fetches more rows automatically. SQL and parameters are recorded as SDK operations in private, best-effort query history for 30 days; result rows are not stored.