Relations: The Graph Database Layer of Qbix Streams
The Qbix Platform treats every stream as a node in a graph. Relations between streams form the edges of this graph, enabling developers to build categories, indexes, playlists, threads, memberships, gamification mechanics, recommendations, and arbitrary higher-level structures. All of this is powered by two-directional, typed, weighted, queryable relations.
For a high-level overview of how Qbix Streams act as a full graph database, see:
Qbix Streams as a Graph Database
.
Directional Relations
Each relation connects two streams in a specific direction:
[fromPublisherId/fromStreamName] --(type, weight, extra)--> [toPublisherId/toStreamName]
The type determines the purpose of the relation – such as category, photo, Streams/participating, featured, or any custom value you define. The weight controls ordering, and the extra field stores arbitrary metadata for the edge.
Bidirectional Indexing
Every relation is stored twice for fast lookups:
- RelatedTo: find items related to a given category
- RelatedFrom: find categories a given item is related from
Both tables are indexed, allowing efficient O(log N) queries from either side without expensive graph traversal.
Relations as a Graph Database
Relations form a directed, typed, weighted multigraph. Developers can:
- filter by relation type
- filter by min/max weight
- limit, offset, and paginate results
- search by prefixes of stream names
- randomize ordering or insert at arbitrary weight positions
- use template prefixes (ending with "/") to build hierarchies
This allows Qbix apps to support complex structures such as threaded discussions, playlists, topic graphs, event-attendee connections, and dynamic content indexes.
The Streams::related() Function
The primary way to retrieve relations is:
list($relations, $streams, $stream) = Streams::related( $asUserId, $publisherId, $streamName, $isCategory, $options );
The $isCategory flag determines whether to fetch streams related to the category or fetch categories related from the stream. Options let you filter by type, prefix, weight range, and even sort randomly.
Adding Relations: relate()
You can relate streams in either direction using:
Streams::relate( $asUserId, $toPublisherId, $toStreamName, $type, $fromPublisherId, $fromStreamName, $options );
The system stores both directions, posts related-to and related-from messages, updates aggregate totals, and optionally inherits access from the related category.
Weights can be specified explicitly, incremented from the maximum, or computed from relations already present.
Removing Relations: unrelate()
To remove a relation:
Streams::unrelate( $asUserId, $toPublisherId, $toStreamName, $type, $fromPublisherId, $fromStreamName );
This removes both entries, adjusts weights as needed, and posts the appropriate update messages.
Updating Relations
The platform provides:
- updateRelations() – bulk updates
- updateRelation() – single-edge updates
Both support updating weights, shifting neighbors, and modifying the extra metadata.
Randomized Insertion and Consumption
Several helper functions support gamified or randomized use cases:
- insertRandomly() – insert a relation in a random position
- removeInsertedRandomly() – remove while maintaining consistency
- consume() – treat relations as a consumable ordered list
Relation Metadata ("Extras")
Each relation includes an extra field – an arbitrary JSON object. Common uses include scores, badges, configuration, or extended display options.
Utility methods include:
- getAllExtras()
- getExtra()
- setExtra()
- clearExtra()
- clearAllExtras()
Filtering by Access
Relations respect all access control rules. When retrieving related streams, the system filters results using:
- testReadLevel()
- testWriteLevel()
- testAdminLevel()
- testPermission()
Automatic Relations: Streams/participating
When a user joins a stream, the platform automatically maintains a relation of type Streams/participating from the user to the stream. This allows fast lookup of participants, live presence updates, and accurate participant counts.
When they leave, the relation is removed automatically.
Maximum Relation Limits
Developers can enforce quotas through checkAvailableRelations(). This is useful for limiting:
- maximum featured items
- NFT minting counts
- index size limits
- rate-limiting categories
Relation Totals
The platform maintains fast-lookup tables:
- Streams_RelatedToTotal
- Streams_RelatedFromTotal
These allow pagination and analytics without expensive COUNT(*) queries, making relations scalable even with millions of edges.
Practical Use Cases
The relations system can represent:
- hierarchical categories
- tagging and many-to-many indexing
- playlists and ordered collections
- threaded discussions
- event-attendee relationships
- user likes and favorites
- recommendation graphs
- game mechanics and scoreboards
- content cross-referencing
Further Reading
For diagrams and deeper conceptual explanations, read:
Qbix Streams as a Graph Database
Dynamic Relationships via syncRelations
In addition to user-generated relations (such as liking, following, and manual reordering), streams can declare relations automatically based on their type, their attributes, or templates. This mechanism is implemented through the syncRelations() method, which keeps a stream’s outgoing relations synchronized with its internal fields.
Each stream type may define a syncRelations configuration, typically listing the direction "from". When enabled, the system inspects the stream’s changed attributes and automatically updates relations of the form attribute/<name>=<value>. Scalars and arrays are treated uniformly, allowing attributes such as "tags", "categories", or "difficulty" to express relations directly.
The sync process works as follows:
- The framework detects which attributes were added, removed, or modified.
- Type-level and stream-specific templates (e.g. "Type/" and "StreamName") are consulted to determine which relation types apply. More specific templates override less specific ones.
- For each changed attribute, the system computes which relations to unrelate and which to add. Values are normalized (including number formatting).
- The framework then issues Streams::unrelate() and Streams::relate() calls automatically, all within a transaction, without generating user-facing messages.
This makes it easy for developers to define large-scale automatic categorization or tagging systems without writing boilerplate to maintain relation tables.
Streams_Category: High-Performance Category Indexes
While relations describe the full graph between streams, some experiences require ultra-fast “top N” listings — such as featured posts, gallery items, curated collections, or product categories. For this purpose, Qbix provides a compact, optimized index per stream: Streams_Category.
Each category row stores a JSON snapshot of related items organized by relation type and integer weight. Each entry includes the related stream’s publisherId, streamName, title, and icon. This denormalized index avoids hydration, joins, and access checks, allowing high-traffic category pages to render instantly.
You can fetch the index with:
$results = Streams_Category::getRelatedTo( $publisherId, $streamName, $relationType );
When enabled by configuration, category rows are updated automatically whenever relations of the corresponding type are added, removed, or reordered.
Automatic Maintenance Through Hooks
Both the syncRelations() mechanism and Streams_Category benefit from real-time maintenance performed by Streams message hooks. These hooks execute after relation-changing messages are posted, ensuring the relation graph and its category indexes remain fully synchronized.
Whenever a stream becomes related to or unrelated from another stream, or when a relation’s weight changes, the following handlers run:
- Streams_relatedTo
- Streams_unrelatedTo
- Streams_updateRelateTo
These hooks:
- ensure availability of new relations based on configuration
- update the appropriate Streams_Category row
- insert, reindex, or remove entries using integer-floored weights
- preserve ordering and apply optional weight adjustments
- run inside a single database transaction for consistency
Because syncRelations() may generate relations internally, its results flow naturally into these same hooks, keeping the category index accurate without any extra work. The system therefore unifies:
- manual relations posted by users
- automatically generated relations from templates
- attribute-driven relations via syncRelations()
- fast category retrieval via Streams_Category
When to Use syncRelations and Category Indexes
Use syncRelations() when you want changes in a stream’s fields or attributes to automatically propagate as structured relationships — for example filtering people on dating sites or products in catalogs based on their attributes.
Use Streams_Category when you need a stable, pre-sorted, high-performance index for public or semi-public listings, such as:
- featured content
- gallery views
- top stories or most recent items
- curated collections
- product or service categories
Together, these systems give you the flexibility of a rich relation graph and the performance of instant category lookups — all maintained automatically by the Qbix Platform.