Skip to main content

src/barrel_postings.erl

%% @doc Postings Resource API for barrel_docdb
%%
%% This module wraps the rocksdb postings_* resource API for efficient
%% posting list operations. The resource API parses posting list binaries
%% once and provides O(1) or O(log n) lookups for repeated access.
%%
%% Key benefits:
%% - Parse once, lookup many times (~0.1 us per lookup)
%% - Native set operations (intersection, union, difference)
%% - Pre-sorted keys in V2 format
%% - Roaring64 bitmap for fast existence checks
%%
%% @end
-module(barrel_postings).

-export([
    %% Resource lifecycle
    open/1,
    to_binary/1,

    %% Lookups
    contains/2,
    bitmap_contains/2,

    %% Metadata
    count/1,
    keys/1,

    %% Set operations (return {ok, Resource} or error)
    intersection/2,
    union/2,
    difference/2,
    intersect_all/1,
    intersection_count/2
]).

-export_type([postings/0]).

-opaque postings() :: reference().

%% @doc Parse a posting list binary into a resource for fast repeated lookups.
%% The binary must be a valid V1 or V2 posting list format.
-spec open(binary()) -> {ok, postings()} | {error, term()}.
open(Binary) when is_binary(Binary) ->
    rocksdb:postings_open(Binary).

%% @doc Convert a postings resource back to binary format.
-spec to_binary(postings()) -> binary().
to_binary(Postings) ->
    rocksdb:postings_to_binary(Postings).

%% @doc Check if a key exists in the posting list (exact match).
%% Uses O(log n) binary search on sorted keys.
-spec contains(postings(), binary()) -> boolean().
contains(Postings, Key) when is_binary(Key) ->
    rocksdb:postings_contains(Postings, Key).

%% @doc Check if a key exists using the embedded roaring bitmap.
%% O(1) hash-based lookup, may have rare false positives.
%% Use contains/2 for exact checks when false positives are unacceptable.
-spec bitmap_contains(postings(), binary()) -> boolean().
bitmap_contains(Postings, Key) when is_binary(Key) ->
    rocksdb:postings_bitmap_contains(Postings, Key).

%% @doc Get the number of active keys in the posting list.
-spec count(postings()) -> non_neg_integer().
count(Postings) ->
    rocksdb:postings_count(Postings).

%% @doc Get all active keys in lexicographic (sorted) order.
%% Keys are pre-sorted in V2 format, so this is O(n).
-spec keys(postings()) -> [binary()].
keys(Postings) ->
    rocksdb:postings_keys(Postings).

%% @doc Compute intersection (AND) of two posting lists.
%% Both arguments can be postings resources or binaries.
-spec intersection(postings() | binary(), postings() | binary()) ->
    {ok, postings()} | {error, term()}.
intersection(A, B) ->
    rocksdb:postings_intersection(A, B).

%% @doc Compute union (OR) of two posting lists.
%% Both arguments can be postings resources or binaries.
-spec union(postings() | binary(), postings() | binary()) ->
    {ok, postings()} | {error, term()}.
union(A, B) ->
    rocksdb:postings_union(A, B).

%% @doc Compute difference (A - B) of two posting lists.
%% Returns keys in A that are not in B.
-spec difference(postings() | binary(), postings() | binary()) ->
    {ok, postings()} | {error, term()}.
difference(A, B) ->
    rocksdb:postings_difference(A, B).

%% @doc Compute intersection of multiple posting lists.
%% Efficiently processes smallest lists first.
%% All list elements can be postings resources or binaries.
-spec intersect_all([postings() | binary()]) -> {ok, postings()} | {error, term()}.
intersect_all(Postings) when is_list(Postings), length(Postings) > 0 ->
    rocksdb:postings_intersect_all(Postings);
intersect_all([]) ->
    {error, empty_list}.

%% @doc Get the cardinality of intersection without materializing the result.
%% Uses roaring bitmap for fast computation.
-spec intersection_count(postings() | binary(), postings() | binary()) ->
    non_neg_integer().
intersection_count(A, B) ->
    rocksdb:postings_intersection_count(A, B).