Readme
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
Boolean ā
nsfw,landscapeā document either has it or doesn'tCategorical ā
genre:fantasy,rating:safeā field:value pairs, multiple values per fieldNumeric ā
score,token_countā integer values with range queries
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:
meta.jsonā tag keys, numeric fields, filename mappingsbitmaps/*.crbā CRoaring serialized bitmapsnumeric/*.jsonā sorted index databitmaps/__universe__.crbā universe bitmap
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 <[email protected]>
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.