Rand Stats

Roaring::Tags

zef:apogee

Actions Status

NAME

Roaring::Tags - High-level tag system built on CRoaring bitmaps

SYNOPSIS

use Roaring::Tags;

my $tags = Roaring::Tags.new;

# Boolean tags
$tags.tag('nsfw', 42);
$tags.tag-many('landscape', [1, 2, 3, 4, 5]);
$tags.untag('nsfw', 42);
$tags.has-tag('landscape', 3);     # True

# Categorical tags (field:value)
$tags.tag('genre:fantasy', 42);
$tags.tag('genre', 'romance', 42);   # Two-arg form

# Numeric properties with range queries
$tags.set-value('score', 42, 150);
$tags.set-values('score', ([1, 100], [2, 200]));

# Query — returns owned CRoaring bitmaps
my $results = $tags.query('landscape');
my $fantasy = $tags.query('genre:fantasy');
my $high    = $tags.range('score', :gt(100));

# Compose with CRoaring set operations
my $filtered = $tags.query('landscape')
    .and($tags.range('score', :gte(50)))
    .andnot($tags.query('violent'));

# Search with booru-style query syntax
my $results = $tags.search('landscape, hdr, score:>100, -violent');
my $complex = $tags.search('(landscape, hdr) OR (portrait, bokeh), score:>50');
my $negonly = $tags.search('-nsfw, -violent');  # Uses universe bitmap

# Pagination (newest first)
my @page1 = $results.slice-reverse(0, 20);
my @page2 = $results.slice-reverse(20, 20);

# Introspection
$tags.tags;                        # All tag keys
$tags.boolean-tags;                # Keys without ':'
$tags.fields;                      # Categorical + numeric field names
$tags.field-values('genre');       # ('fantasy', 'romance')
$tags.numeric-fields;              # ('score',)
$tags.count('landscape');          # Cardinality
$tags.doc-tags(42);                # Tags for a document
$tags.doc-values(42);              # Numeric values for a document

# Document removal
$tags.remove-doc(42);              # Removes from all tags, indices, and universe

# Persistence
$tags.save('my-tags'.IO);
my $loaded = Roaring::Tags.load('my-tags'.IO);

DESCRIPTION

Roaring::Tags provides a high-level tag system for millions of documents with sub-millisecond set operations. Built on CRoaring compressed bitmaps.

Tag types

Query syntax

The search method accepts booru-style query strings:

landscape                   # Boolean tag (AND)
-violent                    # Negation (ANDNOT)
genre:fantasy               # Categorical tag
score:>100                  # Numeric: greater than
score:>=100                 # Numeric: greater than or equal
score:<50                   # Numeric: less than
score:<=50                  # Numeric: less than or equal
score:100..500              # Numeric: inclusive range
(a, b) OR (c, d)           # OR groups with parentheses
a OR b                      # Bare OR
a OR b, c                   # AND binds tighter: a OR (b AND c)
(a OR b), c                 # Parens override: (a OR b) AND c

Terms are comma-separated (AND). OR binds tighter than comma. Negative-only queries (-nsfw, -violent) use the universe bitmap to return everything except the excluded tags.

The execution planner sorts positive terms by ascending cardinality (smallest bitmap first) to minimize intermediate set sizes.

Universe bitmap

Every document that is tagged or given a numeric value is automatically tracked in a universe bitmap. This enables negative-only queries and is persisted alongside the tag data.

Persistence

save writes a directory with:

load restores the full state. Compatible with v1 format (rebuilds universe from bitmaps).

Query explain

my $q = Roaring::Tags::Query.new(:tags($db), :query-string('(a, b) OR c, -d'));
say $q.explain;
# Query: (a, b) OR c, -d
#
# Execution plan:
#   AND
#     OR
#       AND
#         a (cardinality: 500)
#         b (cardinality: 200)
#       c (cardinality: 1000)
#     -d (cardinality: 50)

AUTHOR

Matt Doughty matt@apogee.guru

COPYRIGHT AND LICENSE

Copyright 2026 Matt Doughty

This library is free software; you can redistribute it and/or modify it under the Artistic License 2.0.