Skip to documentation
SLOP

tiny.sql.Search

Reference tiny.sql Search

Defined in tiny.sql.

API (27)

Actions

Public operations.

Fields and members

Public fields and members.

No direct callersNo direct callstiny.sqlSearch
Static calls · unresolved targets: unknown · external targets: unknown.

Source

Source: lib/sql/src/search/engine.zig:119

zig
pub const Search = struct {    space: space_mod.Space,    documents: table_mod.Table,    terms: index_mod.Index,    index_prefixes: bool,    index_phrase_pairs: bool,    omit_phrase_pair_singletons: bool,    field_names: []const []const u8,    pub fn open(database: *file.Database, options: Options) Error!Search {        const roots = [_]space_mod.RootSpec{            .{ .root_page = options.documents_root },            .{ .root_page = options.terms_root },        };        const space = try space_mod.Space.open(database, .{            .meta_page = options.meta_page,            .roots = &roots,            .reserved_page_max = options.reserved_page_max,        });        return .{            .space = space,            .documents = try space.rowidTable(options.documents_root),            .terms = try space.index(options.terms_root, &.{.{ .collation = .binary }}),            .index_prefixes = options.index_prefixes,            .index_phrase_pairs = options.index_phrase_pairs,            .omit_phrase_pair_singletons = options.omit_phrase_pair_singletons,            .field_names = options.field_names,        };    }    pub fn put(self: *Search, allocator: Allocator, rowid: i64, text: []const u8, options: file.CommitOptions) Error!file.Commit {        const phase = trace.scope("search.put");        defer phase.end();        var write = try self.space.beginWrite();        defer write.deinit();        try self.putIn(&write, allocator, rowid, text);        return try write.commit(options);    }    pub fn putForSource(        self: *Search,        allocator: Allocator,        rowid: i64,        text: []const u8,        expected: Source,        next: Source,        options: file.CommitOptions,    ) Error!?file.Commit {        const phase = trace.scope("search.put_for_source");        defer phase.end();        if (!try self.sourceMatches(allocator, expected)) return null;        var write = try self.space.beginWrite();        defer write.deinit();        try self.putIn(&write, allocator, rowid, text);        try self.putSourceIn(&write, next);        return try write.commit(options);    }    pub fn advanceSource(self: *Search, allocator: Allocator, expected: Source, next: Source, options: file.CommitOptions) Error!?file.Commit {        const phase = trace.scope("search.advance_source");        defer phase.end();        if (!try self.sourceMatches(allocator, expected)) return null;        var write = try self.space.beginWrite();        defer write.deinit();        try self.putSourceIn(&write, next);        return try write.commit(options);    }    pub fn putAll(self: *Search, allocator: Allocator, documents: []const Document, options: file.CommitOptions) Error!file.Commit {        const phase = trace.scope("search.put_all");        defer phase.end();        var write = try self.space.beginWrite();        defer write.deinit();        var stats = try self.corpusStatsOrEmpty(allocator);        try self.ensureRequestedCapabilitiesIn(&write, allocator, stats);        for (documents) |document| {            const indexed_text = token_mod.documentText(document.text);            const old = try self.documents.get(allocator, document.rowid);            defer if (old) |bytes| allocator.free(bytes);            if (old) |bytes| {                try self.deletePostingsIn(&write, allocator, document.rowid, try token_mod.textFromRow(bytes));                const old_length = try self.documentLength(allocator, document.rowid);                try self.deleteDocumentLengthIn(&write, document.rowid);                stats = try posting_mod.subtractDocumentStats(stats, old_length);            }            try self.putTextIn(&write, allocator, document.rowid, indexed_text);            try self.putPostingsIn(&write, allocator, document.rowid, indexed_text);            const length = token_mod.documentTokenLength(indexed_text);            try self.putDocumentLengthIn(&write, document.rowid, length);            stats = try posting_mod.addDocumentStats(stats, length);        }        try self.putCorpusStatsIn(&write, stats);        return try write.commit(options);    }    pub fn loadAll(self: *Search, allocator: Allocator, documents: []const Document, options: file.CommitOptions) Error!file.Commit {        const phase = trace.scope("search.load_all");        defer phase.end();        return try self.loadAllWithSource(allocator, documents, null, options);    }    pub fn loadAllForSource(self: *Search, allocator: Allocator, documents: []const Document, source_value: Source, options: file.CommitOptions) Error!file.Commit {        const phase = trace.scope("search.load_all_for_source");        defer phase.end();        return try self.loadAllWithSource(allocator, documents, source_value, options);    }    fn loadAllWithSource(self: *Search, allocator: Allocator, documents: []const Document, source_value: ?Source, options: file.CommitOptions) Error!file.Commit {        var postings = try self.collectPostings(allocator, documents);        var postings_live = true;        defer if (postings_live) postings.deinit(allocator);        std.mem.sort(posting_mod.Posting, postings.items, postings.keys, posting_mod.postingLess);        var write = try self.space.beginWrite();        defer write.deinit();        try self.clearIn(&write, allocator);        try self.putRequestedCapabilitiesIn(&write);        try self.putSortedPostingsIn(&write, allocator, postings.items, postings.keys);        postings.deinit(allocator);        postings_live = false;        var stats: posting_mod.CorpusStats = .{ .documents = 0, .total_tokens = 0 };        {            var lengths: std.ArrayList(posting_mod.DocumentLength) = .empty;            defer lengths.deinit(allocator);            for (documents) |document| {                const indexed_text = token_mod.documentText(document.text);                try self.putTextIn(&write, allocator, document.rowid, indexed_text);                const length = token_mod.documentTokenLength(indexed_text);                try lengths.append(allocator, .{ .rowid = document.rowid, .length = length });                stats = try posting_mod.addDocumentStats(stats, length);            }            try self.putSortedDocumentLengthsIn(&write, allocator, lengths.items);        }        try self.putCorpusStatsIn(&write, stats);        if (source_value) |value| try self.putSourceIn(&write, value);        return try write.commit(options);    }    pub fn loadAllNew(self: *Search, allocator: Allocator, documents: []const Document, options: file.CommitOptions) Error!file.Commit {        const phase = trace.scope("search.load_all_new");        defer phase.end();        var postings = try self.collectPostings(allocator, documents);        var postings_live = true;        defer if (postings_live) postings.deinit(allocator);        std.mem.sort(posting_mod.Posting, postings.items, postings.keys, posting_mod.postingLess);        var write = try self.space.beginWrite();        defer write.deinit();        var stats = try self.corpusStatsOrEmpty(allocator);        try self.ensureRequestedCapabilitiesIn(&write, allocator, stats);        try self.putSortedPostingsIn(&write, allocator, postings.items, postings.keys);        postings.deinit(allocator);        postings_live = false;        var lengths: std.ArrayList(posting_mod.DocumentLength) = .empty;        defer lengths.deinit(allocator);        for (documents) |document| {            const indexed_text = token_mod.documentText(document.text);            try self.putTextIn(&write, allocator, document.rowid, indexed_text);            const length = token_mod.documentTokenLength(indexed_text);            try lengths.append(allocator, .{ .rowid = document.rowid, .length = length });            stats = try posting_mod.addDocumentStats(stats, length);        }        try self.putSortedDocumentLengthsIn(&write, allocator, lengths.items);        try self.putCorpusStatsIn(&write, stats);        return try write.commit(options);    }    pub fn clear(self: *Search, allocator: Allocator, options: file.CommitOptions) Error!file.Commit {        const phase = trace.scope("search.clear");        defer phase.end();        var write = try self.space.beginWrite();        defer write.deinit();        try self.clearIn(&write, allocator);        try self.putRequestedCapabilitiesIn(&write);        return try write.commit(options);    }    pub fn delete(self: *Search, allocator: Allocator, rowid: i64, options: file.CommitOptions) Error!file.Commit {        const phase = trace.scope("search.delete");        defer phase.end();        const old = (try self.documents.get(allocator, rowid)) orelse return error.KeyNotFound;        defer allocator.free(old);        var write = try self.space.beginWrite();        defer write.deinit();        var stats = try self.corpusStatsOrEmpty(allocator);        try self.ensureRequestedCapabilitiesIn(&write, allocator, stats);        try self.deletePostingsIn(&write, allocator, rowid, try token_mod.textFromRow(old));        const old_length = try self.documentLength(allocator, rowid);        try self.deleteDocumentLengthIn(&write, rowid);        stats = try posting_mod.subtractDocumentStats(stats, old_length);        try self.putCorpusStatsIn(&write, stats);        try self.documents.deleteIn(&write, rowid);        return try write.commit(options);    }    fn getDocumentText(self: anytype, allocator: Allocator, rowid: i64) Error!?text_mod.DocumentText {        const bytes = (try self.documents.get(allocator, rowid)) orelse return null;        errdefer allocator.free(bytes);        return .{            .bytes = bytes,            .text = try token_mod.textFromRow(bytes),        };    }    pub fn getText(self: anytype, allocator: Allocator, rowid: i64) Error!?[]u8 {        var document = (try self.getDocumentText(allocator, rowid)) orelse return null;        defer document.deinit(allocator);        return try allocator.dupe(u8, document.text);    }    pub fn capabilities(self: anytype, allocator: Allocator) Error!posting_mod.Capabilities {        return try self.indexCapabilities(allocator);    }    pub fn indexedSource(self: anytype, allocator: Allocator) Error!?Source {        var lookup: index_mod.Scan = undefined;        try self.terms.lookupPayloads(&lookup, allocator, &posting_mod.sourceValues());        defer lookup.deinit();        const entry = (try lookup.next()) orelse return null;        if (entry.rowid != posting_mod.internal_meta_rowid) return error.InvalidSearchIndex;        const value = try posting_mod.sourcePayloadValue(entry.payload);        if (try lookup.next() != null) return error.InvalidSearchIndex;        return value;    }    pub fn sourceMatches(self: anytype, allocator: Allocator, expected: Source) Error!bool {        const current = (try self.indexedSource(allocator)) orelse return false;        return current.same(expected);    }    pub fn sourceSchemaMatches(self: anytype, allocator: Allocator, schema: u64) Error!bool {        const current = (try self.indexedSource(allocator)) orelse return false;        return current.schema == schema;    }    pub fn postingStats(self: anytype, allocator: Allocator) Error!posting_mod.PostingStats {        var scan: index_mod.Scan = undefined;        try self.terms.scanPayloads(&scan, allocator, null, null);        defer scan.deinit();        var stats: posting_mod.PostingStats = .{};        var previous_key: [posting_mod.phrase_pair_posting_key_max_bytes]u8 = undefined;        var previous_key_len: usize = 0;        var have_previous_key = false;        var values: [1]row.Value = undefined;        var scratch: [page.size]u8 = undefined;        while (try scan.next()) |entry| {            stats.term_index_entries += 1;            stats.payload_bytes += entry.payload.len;            const decoded = try key_mod.decodeIndex(&values, &scratch, entry.key);            if (decoded.values.len != 1) return error.InvalidSearchIndex;            const key_text = switch (decoded.values[0]) {                .text => |text| text,                else => return error.InvalidSearchIndex,            };            if (std.mem.eql(u8, key_text, posting_mod.document_length_key)) {                stats.document_length_entries += 1;                if (posting_mod.lengthSegmentPayloadCount(entry.payload)) |count| {                    stats.document_length_segment_entries += 1;                    stats.document_length_segment_postings += count;                } else {                    _ = try posting_mod.documentLengthPayloadMaybe(entry.payload);                    stats.document_length_direct_entries += 1;                }                continue;            }            if (std.mem.eql(u8, key_text, posting_mod.corpus_stats_key) or                std.mem.eql(u8, key_text, posting_mod.capabilities_key) or                std.mem.eql(u8, key_text, posting_mod.source_key))            {                stats.metadata_entries += 1;                continue;            }            if (key_text.len == 0 or key_text.len > previous_key.len) return error.InvalidSearchIndex;            if (!have_previous_key or !std.mem.eql(u8, previous_key[0..previous_key_len], key_text)) {                stats.posting_term_keys += 1;                stats.posting_term_key_text_bytes += key_text.len;                switch (key_text[0]) {                    '=' => {                        stats.exact_term_keys += 1;                        stats.exact_term_key_text_bytes += key_text.len;                    },                    '*' => {                        stats.prefix_term_keys += 1;                        stats.prefix_term_key_text_bytes += key_text.len;                    },                    posting_mod.phrase_pair_posting_kind => {                        stats.phrase_pair_term_keys += 1;                        stats.phrase_pair_term_key_text_bytes += key_text.len;                    },                    else => return error.InvalidSearchIndex,                }                @memcpy(previous_key[0..key_text.len], key_text);                previous_key_len = key_text.len;                have_previous_key = true;            }            stats.posting_entries += 1;            stats.posting_payload_bytes += entry.payload.len;            stats.posting_key_text_bytes += key_text.len;            const phrase_pair = key_text[0] == posting_mod.phrase_pair_posting_kind;            switch (key_text[0]) {                '=' => {                    stats.exact_posting_key_text_bytes += key_text.len;                    stats.exact_posting_payload_bytes += entry.payload.len;                },                '*' => {                    stats.prefix_posting_key_text_bytes += key_text.len;                    stats.prefix_posting_payload_bytes += entry.payload.len;                },                posting_mod.phrase_pair_posting_kind => {                    stats.phrase_pair_posting_key_text_bytes += key_text.len;                    stats.phrase_pair_posting_payload_bytes += entry.payload.len;                },                else => return error.InvalidSearchIndex,            }            if (posting_mod.segmentPayloadCount(entry.payload)) |count| {                stats.segment_entries += 1;                stats.segment_postings += count;                if (phrase_pair) {                    stats.phrase_pair_segment_entries += 1;                    stats.phrase_pair_segment_postings += count;                }            } else {                const count = try posting_mod.postingPayloadCount(entry.payload);                if (count == 0) {                    stats.direct_posting_tombstones += 1;                    if (phrase_pair) stats.phrase_pair_direct_posting_tombstones += 1;                } else {                    stats.direct_posting_entries += 1;                    if (phrase_pair) stats.phrase_pair_direct_posting_entries += 1;                }            }        }        return stats;    }    fn putIn(self: *Search, write: *tree.Write, allocator: Allocator, rowid: i64, text: []const u8) Error!void {        const indexed_text = token_mod.documentText(text);        const old = try self.documents.get(allocator, rowid);        defer if (old) |bytes| allocator.free(bytes);        var stats = try self.corpusStatsOrEmpty(allocator);        try self.ensureRequestedCapabilitiesIn(write, allocator, stats);        if (old) |bytes| {            const old_text = try token_mod.textFromRow(bytes);            if (std.mem.eql(u8, old_text, indexed_text)) return;            const old_length = try self.documentLength(allocator, rowid);            try self.replacePostingsIn(write, allocator, rowid, old_text, indexed_text);            try self.putTextIn(write, allocator, rowid, indexed_text);            const length = token_mod.documentTokenLength(indexed_text);            if (old_length != length) {                try self.putDocumentLengthIn(write, rowid, length);                stats = try posting_mod.subtractDocumentStats(stats, old_length);                stats = try posting_mod.addDocumentStats(stats, length);                try self.putCorpusStatsIn(write, stats);            }            return;        }        try self.putTextIn(write, allocator, rowid, indexed_text);        try self.putPostingsIn(write, allocator, rowid, indexed_text);        const length = token_mod.documentTokenLength(indexed_text);        try self.putDocumentLengthIn(write, rowid, length);        stats = try posting_mod.addDocumentStats(stats, length);        try self.putCorpusStatsIn(write, stats);    }    fn requestedCapabilities(self: anytype) posting_mod.Capabilities {        return .{            .phrase_pair_postings = self.index_phrase_pairs,            .phrase_pair_singletons_omitted = self.index_phrase_pairs and self.omit_phrase_pair_singletons,        };    }    fn indexCapabilities(self: anytype, allocator: Allocator) Error!posting_mod.Capabilities {        var lookup: index_mod.Scan = undefined;        try self.terms.lookupPayloads(&lookup, allocator, &posting_mod.capabilitiesValues());        defer lookup.deinit();        const entry = (try lookup.next()) orelse return .{};        if (entry.rowid != posting_mod.internal_meta_rowid) return error.InvalidSearchIndex;        const value = try posting_mod.capabilitiesPayloadValue(entry.payload);        if (try lookup.next() != null) return error.InvalidSearchIndex;        return value;    }    fn canUsePhrasePairPostings(self: anytype, allocator: Allocator) Error!bool {        if (!self.index_phrase_pairs) return false;        return (try self.indexCapabilities(allocator)).phrase_pair_postings;    }    fn phrasePairPostingsArePartial(self: anytype, allocator: Allocator) Error!bool {        if (!self.index_phrase_pairs) return false;        return (try self.indexCapabilities(allocator)).phrase_pair_singletons_omitted;    }    fn ensureRequestedCapabilitiesIn(self: *Search, write: *tree.Write, allocator: Allocator, stats: posting_mod.CorpusStats) Error!void {        const requested = self.requestedCapabilities();        if (posting_mod.capabilitiesEmpty(requested)) return;        const current = try self.indexCapabilities(allocator);        if (posting_mod.capabilitiesInclude(current, requested)) return;        if (stats.documents != 0) return error.SearchIndexRebuildRequired;        try self.putCapabilitiesIn(write, posting_mod.mergeCapabilities(current, requested));    }    fn putRequestedCapabilitiesIn(self: *Search, write: *tree.Write) Error!void {        const requested = self.requestedCapabilities();        if (posting_mod.capabilitiesEmpty(requested)) return;        try self.putCapabilitiesIn(write, requested);    }    fn putCapabilitiesIn(self: *Search, write: *tree.Write, capabilities_value: posting_mod.Capabilities) Error!void {        var payload: [posting_mod.capabilities_payload_size]u8 = undefined;        try self.terms.putPayloadIn(write, posting_mod.internal_meta_rowid, &posting_mod.capabilitiesValues(), try posting_mod.capabilitiesPayload(&payload, capabilities_value));    }    fn putSourceIn(self: *Search, write: *tree.Write, source_value: Source) Error!void {        var payload: [posting_mod.source_payload_size]u8 = undefined;        try self.terms.putPayloadIn(write, posting_mod.internal_meta_rowid, &posting_mod.sourceValues(), try posting_mod.sourcePayload(&payload, source_value));    }    pub fn query(self: anytype, allocator: Allocator, text: []const u8, limit: usize) Error!Results {        const phase = trace.scope("search.query");        defer phase.end();        var parsed = try query_mod.parseQuery(allocator, text, self.field_names);        defer parsed.deinit(allocator);        return try self.queryParsed(allocator, parsed, limit);    }    pub fn queryScoreBand(self: anytype, allocator: Allocator, text: []const u8, limit: usize) Error!Results {        const phase = trace.scope("search.query_score_band");        defer phase.end();        var parsed = try query_mod.parseQuery(allocator, text, self.field_names);        defer parsed.deinit(allocator);        return try self.queryParsedScoreBand(allocator, parsed, limit);    }    pub fn queryRanked(self: anytype, allocator: Allocator, text: []const u8, limit: usize, options: rank_mod.RankingOptions) Error!RankedResults {        const phase = trace.scope("search.query_ranked");        defer phase.end();        var parsed = try query_mod.parseQuery(allocator, text, self.field_names);        defer parsed.deinit(allocator);        return try self.queryParsedRanked(allocator, parsed, limit, options);    }    pub fn prepare(self: anytype, allocator: Allocator, text: []const u8) Error!Prepared {        const phase = trace.scope("search.prepare");        defer phase.end();        return .{ .parsed = try query_mod.parseQuery(allocator, text, self.field_names) };    }    fn queryParsed(self: anytype, allocator: Allocator, parsed: query_mod.Query, limit: usize) Error!Results {        if (parsed.clauses.len == 0) return .{ .allocator = allocator, .hits = &.{}, .total = 0 };        if (parsed.clauses.len == 1) {            if (limit != 0) {                if (try self.clauseBoundedIndexHits(allocator, parsed.clauses[0], limit)) |bounded_hits| {                    std.mem.sort(hit_mod.Hit, bounded_hits.hits, {}, hit_mod.hitLess);                    return .{ .allocator = allocator, .hits = bounded_hits.hits, .total = bounded_hits.total };                }            }            if (try self.clauseIndexHits(allocator, parsed.clauses[0])) |index_hits| {                const total = index_hits.len;                return .{ .allocator = allocator, .hits = try hit_mod.sortAndLimitOwnedHits(allocator, index_hits, limit), .total = total };            }        }        if (limit != 0) {            if (try self.queryBoundedExactOrHits(allocator, parsed, limit)) |bounded_hits| {                std.mem.sort(hit_mod.Hit, bounded_hits.hits, {}, hit_mod.hitLess);                return .{ .allocator = allocator, .hits = bounded_hits.hits, .total = bounded_hits.total };            }        }        var hits: []hit_mod.Hit = &.{};        var have_hits = false;        errdefer if (have_hits) allocator.free(hits);        for (parsed.clauses) |clause| {            const clause_hits = (try self.clauseIndexHits(allocator, clause)) orelse try self.scoredClauseHits(allocator, clause);            var clause_hits_owned = true;            errdefer if (clause_hits_owned) allocator.free(clause_hits);            if (!have_hits) {                hits = clause_hits;                have_hits = true;                clause_hits_owned = false;                continue;            }            const merged = try hit_mod.unionHits(allocator, hits, clause_hits);            allocator.free(hits);            allocator.free(clause_hits);            clause_hits_owned = false;            hits = merged;        }        const total = hits.len;        const owned = try hit_mod.sortAndLimitOwnedHits(allocator, hits, limit);        return .{ .allocator = allocator, .hits = owned, .total = total };    }    fn queryParsedScoreBand(self: anytype, allocator: Allocator, parsed: query_mod.Query, limit: usize) Error!Results {        if (limit == 0) return try self.queryParsed(allocator, parsed, 0);        if (parsed.clauses.len == 0) return .{ .allocator = allocator, .hits = &.{}, .total = 0 };        if (parsed.clauses.len == 1) {            if (try self.clauseScoreBandIndexHits(allocator, parsed.clauses[0], limit)) |band_hits| {                std.mem.sort(hit_mod.Hit, band_hits.hits, {}, hit_mod.hitLess);                return .{ .allocator = allocator, .hits = band_hits.hits, .total = band_hits.total };            }            if (try self.clauseIndexHits(allocator, parsed.clauses[0])) |index_hits| {                const total = index_hits.len;                return .{ .allocator = allocator, .hits = try hit_mod.sortAndScoreBandOwnedHits(allocator, index_hits, limit), .total = total };            }        }        if (try self.queryScoreBandExactOrHits(allocator, parsed, limit)) |band_hits| {            std.mem.sort(hit_mod.Hit, band_hits.hits, {}, hit_mod.hitLess);            return .{ .allocator = allocator, .hits = band_hits.hits, .total = band_hits.total };        }        var full = try self.queryParsed(allocator, parsed, 0);        errdefer full.deinit();        full.hits = try hit_mod.sortAndScoreBandOwnedHits(allocator, full.hits, limit);        return full;    }    fn queryParsedRanked(self: anytype, allocator: Allocator, parsed: query_mod.Query, limit: usize, options: rank_mod.RankingOptions) Error!RankedResults {        if (try self.queryParsedRankedExactOr(allocator, parsed, limit, options)) |ranked| return ranked;        if (query_mod.singlePositivePrefixRankingAtom(parsed)) |atom| {            return try self.rankPrefixAtom(allocator, atom, limit, options);        }        if (query_mod.singlePositivePhrasePairRankingAtom(parsed)) |atom| {            return try self.rankPhrasePairAtom(allocator, atom, limit, options);        }        const rows = try self.queryParsedCandidateRows(allocator, parsed);        defer if (rows.len != 0) allocator.free(rows);        if (rows.len == 0) return .{ .allocator = allocator, .hits = &.{}, .total = 0 };        const stats = try self.corpusStats(allocator);        if (query_mod.singlePositiveNonExactRankingAtom(parsed)) |atom| {            return try self.rankSingleAtomCandidateRows(allocator, rows, atom, stats, limit, options);        }        const terms = try self.collectRankingTerms(allocator, parsed, stats.documents);        defer rank_mod.deinitRankingTerms(allocator, terms);        return try self.rankCandidateRows(allocator, rows, terms, stats, limit, options);    }    fn queryParsedCandidateRows(self: anytype, allocator: Allocator, parsed: query_mod.Query) Error![]i64 {        var rows: []i64 = &.{};        var have_rows = false;        errdefer if (have_rows) allocator.free(rows);        for (parsed.clauses) |clause| {            const clause_rows = try self.clauseCandidates(allocator, clause);            var clause_rows_owned = true;            errdefer if (clause_rows_owned) allocator.free(clause_rows);            if (!have_rows) {                rows = clause_rows;                have_rows = true;                clause_rows_owned = false;                continue;            }            const merged = try hit_mod.unionRows(allocator, rows, clause_rows);            allocator.free(rows);            allocator.free(clause_rows);            clause_rows_owned = false;            rows = merged;        }        return if (have_rows) rows else &.{};    }    fn rankSingleAtomCandidateRows(self: anytype, allocator: Allocator, rows: []const i64, atom: *const query_mod.Atom, stats: posting_mod.CorpusStats, limit: usize, options: rank_mod.RankingOptions) Error!RankedResults {        var lengths = try rank_mod.DocumentLengthCursor.init(self, allocator);        defer lengths.deinit();        var candidates: std.ArrayList(rank_mod.SingleAtomCandidate) = .empty;        defer candidates.deinit(allocator);        var document_frequency: usize = 0;        if (text_mod.useDocumentTextScanRows(rows)) {            var documents: text_mod.DocumentTextCursor = undefined;            try documents.init(self, allocator, rows[0]);            defer documents.deinit();            for (rows) |rowid| {                const frequency = (try documents.atomFrequencyFor(rowid, atom.*)) orelse continue;                try rank_mod.appendSingleAtomCandidate(allocator, &candidates, &lengths, &document_frequency, rowid, frequency);            }        } else {            for (rows) |rowid| {                var document = (try self.getDocumentText(allocator, rowid)) orelse continue;                const frequency = text_mod.atomFrequency(document.text, atom.*);                document.deinit(allocator);                try rank_mod.appendSingleAtomCandidate(allocator, &candidates, &lengths, &document_frequency, rowid, frequency);            }        }        const idf = try rank_mod.bm25Idf(stats.documents, document_frequency);        var hits: std.ArrayList(hit_mod.RankedHit) = .empty;        errdefer hits.deinit(allocator);        for (candidates.items) |candidate| {            try hit_mod.appendRankedHit(allocator, &hits, limit, .{                .rowid = candidate.rowid,                .rank = -rank_mod.bm25Score(idf, candidate.frequency, candidate.length, stats, options),            });        }        std.mem.sort(hit_mod.RankedHit, hits.items, {}, hit_mod.rankedHitLess);        return .{            .allocator = allocator,            .hits = try hits.toOwnedSlice(allocator),            .total = document_frequency,        };    }    fn rankPrefixAtom(self: anytype, allocator: Allocator, atom: *const query_mod.Atom, limit: usize, options: rank_mod.RankingOptions) Error!RankedResults {        const hits = try self.prefixHits(allocator, atom.tokens[0]);        defer if (hits.len != 0) allocator.free(hits);        if (hits.len == 0) return .{ .allocator = allocator, .hits = &.{}, .total = 0 };        const stats = try self.corpusStats(allocator);        const idf = try rank_mod.bm25Idf(stats.documents, hits.len);        var lengths = try rank_mod.DocumentLengthCursor.init(self, allocator);        defer lengths.deinit();        var ranked: std.ArrayList(hit_mod.RankedHit) = .empty;        errdefer ranked.deinit(allocator);        if (text_mod.useDocumentTextScanHits(hits)) {            var documents: text_mod.DocumentTextCursor = undefined;            try documents.init(self, allocator, hits[0].rowid);            defer documents.deinit();            for (hits) |hit| {                const frequency = (try documents.atomFrequencyFor(hit.rowid, atom.*)) orelse continue;                try rank_mod.appendPrefixRankedHit(allocator, &ranked, &lengths, hit.rowid, frequency, idf, stats, options, limit);            }        } else {            for (hits) |hit| {                var document = (try self.getDocumentText(allocator, hit.rowid)) orelse continue;                const frequency = text_mod.atomFrequency(document.text, atom.*);                document.deinit(allocator);                try rank_mod.appendPrefixRankedHit(allocator, &ranked, &lengths, hit.rowid, frequency, idf, stats, options, limit);            }        }        std.mem.sort(hit_mod.RankedHit, ranked.items, {}, hit_mod.rankedHitLess);        return .{            .allocator = allocator,            .hits = try ranked.toOwnedSlice(allocator),            .total = hits.len,        };    }    fn rankPhrasePairAtom(self: anytype, allocator: Allocator, atom: *const query_mod.Atom, limit: usize, options: rank_mod.RankingOptions) Error!RankedResults {        std.debug.assert(query_mod.isPhrasePairAtom(atom.*));        const hits = if (try self.canUsePhrasePairPostings(allocator) and !try self.phrasePairPostingsArePartial(allocator))            try self.phrasePairHits(allocator, atom.tokens[0], atom.tokens[1])        else            try self.phrasePairVerifiedHits(allocator, atom.*, 1);        defer if (hits.len != 0) allocator.free(hits);        if (hits.len == 0) return .{ .allocator = allocator, .hits = &.{}, .total = 0 };        const stats = try self.corpusStats(allocator);        const idf = try rank_mod.bm25Idf(stats.documents, hits.len);        var lengths = try rank_mod.DocumentLengthCursor.init(self, allocator);        defer lengths.deinit();        var ranked: std.ArrayList(hit_mod.RankedHit) = .empty;        errdefer ranked.deinit(allocator);        for (hits) |hit| {            try hit_mod.appendRankedHit(allocator, &ranked, limit, .{                .rowid = hit.rowid,                .rank = -rank_mod.bm25Score(idf, hit.score, try lengths.lengthFor(hit.rowid), stats, options),            });        }        std.mem.sort(hit_mod.RankedHit, ranked.items, {}, hit_mod.rankedHitLess);        return .{            .allocator = allocator,            .hits = try ranked.toOwnedSlice(allocator),            .total = hits.len,        };    }    fn queryParsedRankedExactOr(self: anytype, allocator: Allocator, parsed: query_mod.Query, limit: usize, options: rank_mod.RankingOptions) Error!?RankedResults {        if (parsed.clauses.len == 0) return .{ .allocator = allocator, .hits = &.{}, .total = 0 };        for (parsed.clauses) |clause| _ = query_mod.exactClauseTerm(clause) orelse return null;        const stats = try self.corpusStats(allocator);        const cursors = try allocator.alloc(rank_mod.ExactRankingCursor, parsed.clauses.len);        var cursor_count: usize = 0;        defer {            for (cursors[0..cursor_count]) |*cursor| cursor.deinit();            allocator.free(cursors);        }        for (parsed.clauses) |clause| {            const term_text = query_mod.exactClauseTerm(clause).?;            cursors[cursor_count] = .{                .cursor = try rank_mod.ExactTermCursor.init(self, allocator, term_text),                .idf = 0.0,            };            cursor_count += 1;            const document_frequency = try cursors[cursor_count - 1].cursor.documentFrequency();            cursors[cursor_count - 1].idf = try rank_mod.bm25Idf(stats.documents, document_frequency);            try cursors[cursor_count - 1].cursor.advance();        }        var lengths = try rank_mod.DocumentLengthCursor.init(self, allocator);        defer lengths.deinit();        var hits: std.ArrayList(hit_mod.RankedHit) = .empty;        errdefer hits.deinit(allocator);        var total: usize = 0;        while (try rank_mod.nextExactRankedOrHit(cursors[0..cursor_count], &lengths, stats, options)) |hit| {            total += 1;            try hit_mod.appendRankedHit(allocator, &hits, limit, hit);        }        std.mem.sort(hit_mod.RankedHit, hits.items, {}, hit_mod.rankedHitLess);        return .{            .allocator = allocator,            .hits = try hits.toOwnedSlice(allocator),            .total = total,        };    }    fn rankCandidateRows(self: anytype, allocator: Allocator, rows: []const i64, terms: []const rank_mod.RankingTerm, stats: posting_mod.CorpusStats, limit: usize, options: rank_mod.RankingOptions) Error!RankedResults {        var lengths = try rank_mod.DocumentLengthCursor.init(self, allocator);        defer lengths.deinit();        var hits: std.ArrayList(hit_mod.RankedHit) = .empty;        errdefer hits.deinit(allocator);        var document_text: ?text_mod.DocumentText = null;        defer text_mod.deinitDocumentText(allocator, &document_text);        var document_text_rowid: ?i64 = null;        var total: usize = 0;        for (rows) |rowid| {            const length = try lengths.lengthFor(rowid);            var score: f64 = 0.0;            for (terms) |term| {                const frequency = if (term.posting_hits.len != 0)                    hit_mod.postingHitFrequency(term.posting_hits, rowid)                else frequency: {                    if (document_text_rowid == null or document_text_rowid.? != rowid) {                        text_mod.deinitDocumentText(allocator, &document_text);                        document_text_rowid = null;                        document_text = (try self.getDocumentText(allocator, rowid)) orelse break :frequency 0;                        document_text_rowid = rowid;                    }                    break :frequency text_mod.atomFrequency(document_text.?.text, term.atom.*);                };                if (frequency == 0) continue;                score += rank_mod.bm25Score(term.idf, frequency, length, stats, options);            }            total += 1;            try hit_mod.appendRankedHit(allocator, &hits, limit, .{ .rowid = rowid, .rank = -score });        }        std.mem.sort(hit_mod.RankedHit, hits.items, {}, hit_mod.rankedHitLess);        return .{            .allocator = allocator,            .hits = try hits.toOwnedSlice(allocator),            .total = total,        };    }    fn putPostingsIn(self: *Search, write: *tree.Write, allocator: Allocator, rowid: i64, text: []const u8) Error!void {        const counts = try posting_mod.documentTermCounts(allocator, text, self.index_prefixes, self.index_phrase_pairs and !self.omit_phrase_pair_singletons);        defer posting_mod.freeTermCounts(allocator, counts);        for (counts) |term_count| try self.putPostingIn(write, rowid, term_count);    }    fn replacePostingsIn(self: *Search, write: *tree.Write, allocator: Allocator, rowid: i64, old_text: []const u8, new_text: []const u8) Error!void {        const old_counts = try posting_mod.documentTermCounts(allocator, old_text, self.index_prefixes, self.index_phrase_pairs);        defer posting_mod.freeTermCounts(allocator, old_counts);        const new_counts = try posting_mod.documentTermCounts(allocator, new_text, self.index_prefixes, self.index_phrase_pairs and !self.omit_phrase_pair_singletons);        defer posting_mod.freeTermCounts(allocator, new_counts);        for (old_counts) |old_count| {            const new_count = termCount(new_counts, old_count.key) orelse {                try self.deletePostingIn(write, rowid, old_count.key);                continue;            };            if (old_count.count != new_count.count) try self.putPostingIn(write, rowid, new_count);        }        for (new_counts) |new_count| {            if (termCount(old_counts, new_count.key) == null) try self.putPostingIn(write, rowid, new_count);        }    }    fn putPostingIn(self: *Search, write: *tree.Write, rowid: i64, term_count: posting_mod.TermCount) Error!void {        var payload: [page.size]u8 = undefined;        try self.terms.putPayloadIn(write, rowid, &posting_mod.postingValues(term_count.key), try posting_mod.postingPayload(&payload, term_count.count));    }    fn deletePostingIn(self: *Search, write: *tree.Write, rowid: i64, key: []const u8) Error!void {        self.terms.deleteIn(write, rowid, &posting_mod.postingValues(key)) catch |err| switch (err) {            error.KeyNotFound => {},            else => return err,        };        var payload: [page.size]u8 = undefined;        try self.terms.putPayloadIn(write, rowid, &posting_mod.postingValues(key), try posting_mod.postingPayload(&payload, 0));    }    fn putDocumentLengthIn(self: *Search, write: *tree.Write, rowid: i64, length: usize) Error!void {        var payload: [posting_mod.document_length_payload_size]u8 = undefined;        try self.terms.putPayloadIn(write, rowid, &posting_mod.documentLengthValues(), try posting_mod.documentLengthPayload(&payload, length));    }    fn deleteDocumentLengthIn(self: *Search, write: *tree.Write, rowid: i64) Error!void {        var payload: [posting_mod.document_length_payload_size]u8 = undefined;        try self.terms.putPayloadIn(write, rowid, &posting_mod.documentLengthValues(), posting_mod.documentLengthTombstonePayload(&payload));    }    fn putCorpusStatsIn(self: *Search, write: *tree.Write, stats: posting_mod.CorpusStats) Error!void {        var payload: [posting_mod.corpus_stats_payload_size]u8 = undefined;        try self.terms.putPayloadIn(write, posting_mod.internal_meta_rowid, &posting_mod.corpusStatsValues(), try posting_mod.corpusStatsPayload(&payload, stats));    }    fn putSortedDocumentLengthsIn(self: *Search, write: *tree.Write, allocator: Allocator, lengths: []posting_mod.DocumentLength) Error!void {        std.mem.sort(posting_mod.DocumentLength, lengths, {}, posting_mod.documentLengthLess);        if (posting_mod.canSegmentDocumentLengths(lengths)) {            const payload = try posting_mod.documentLengthSegmentPayload(allocator, lengths);            defer allocator.free(payload);            try self.terms.putPayloadIn(write, try posting_mod.segmentRowid(lengths[0].rowid), &posting_mod.documentLengthValues(), payload);            return;        }        for (lengths) |document_length| try self.putDocumentLengthIn(write, document_length.rowid, document_length.length);    }    fn putSortedPostingsIn(self: *Search, write: *tree.Write, allocator: Allocator, postings: []const posting_mod.Posting, keys: []const []const u8) Error!void {        var start: usize = 0;        while (start < postings.len) {            var end = start + 1;            while (end < postings.len and postings[start].key_index == postings[end].key_index) : (end += 1) {}            const group = postings[start..end];            if (group[0].key_index >= keys.len) return error.InvalidSearchIndex;            const key = keys[group[0].key_index];            if (posting_mod.canSegmentPostings(group)) {                const payload = try posting_mod.postingSegmentPayload(allocator, group);                defer allocator.free(payload);                try self.terms.putPayloadIn(write, try posting_mod.segmentRowid(group[0].rowid), &posting_mod.postingValues(key), payload);            } else if (self.omit_phrase_pair_singletons and posting_mod.isPhrasePairPostingKey(key)) {                start = end;                continue;            } else {                for (group) |posting| {                    var payload: [page.size]u8 = undefined;                    try self.terms.putPayloadIn(write, posting.rowid, &posting_mod.postingValues(key), try posting_mod.postingPayload(&payload, posting.count));                }            }            start = end;        }    }    fn putTextIn(self: *Search, write: *tree.Write, allocator: Allocator, rowid: i64, text: []const u8) Error!void {        const values = [_]row.Value{.{ .text = text }};        const size = try row.encodedSize(&values);        if (size <= page.size) {            var row_bytes: [page.size]u8 = undefined;            try self.documents.putEncodedIn(write, rowid, try row.encode(&row_bytes, &values));            return;        }        const row_bytes = try allocator.alloc(u8, size);        defer allocator.free(row_bytes);        try self.documents.putEncodedIn(write, rowid, try row.encode(row_bytes, &values));    }    fn deletePostingsIn(self: *Search, write: *tree.Write, allocator: Allocator, rowid: i64, text: []const u8) Error!void {        const counts = try posting_mod.documentTermCounts(allocator, text, self.index_prefixes, self.index_phrase_pairs);        defer posting_mod.freeTermCounts(allocator, counts);        for (counts) |term_count| try self.deletePostingIn(write, rowid, term_count.key);    }    fn clauseIndexHits(self: anytype, allocator: Allocator, clause: query_mod.Clause) Error!?[]hit_mod.Hit {        if (clause.positive.len == 0) return null;        const first = try self.atomIndexHits(allocator, clause.positive[0]);        var current = first orelse return null;        errdefer allocator.free(current);        var index: usize = 1;        while (index < clause.positive.len) : (index += 1) {            const next_maybe = try self.atomIndexHits(allocator, clause.positive[index]);            const next = next_maybe orelse {                allocator.free(current);                return null;            };            defer allocator.free(next);            const merged = try hit_mod.intersectHits(allocator, current, next);            allocator.free(current);            current = merged;        }        for (clause.negative) |atom| {            const excluded = try self.atomDocuments(allocator, atom);            defer allocator.free(excluded);            const filtered = try hit_mod.subtractHits(allocator, current, excluded);            allocator.free(current);            current = filtered;        }        return current;    }    fn clauseBoundedIndexHits(self: anytype, allocator: Allocator, clause: query_mod.Clause, limit: usize) Error!?hit_mod.HitSet {        if (clause.negative.len != 0 or clause.positive.len != 1) return null;        return try self.atomBoundedIndexHits(allocator, clause.positive[0], limit);    }    fn clauseScoreBandIndexHits(self: anytype, allocator: Allocator, clause: query_mod.Clause, limit: usize) Error!?hit_mod.HitSet {        if (clause.negative.len != 0 or clause.positive.len != 1) return null;        return try self.atomScoreBandIndexHits(allocator, clause.positive[0], limit);    }    fn queryBoundedExactOrHits(self: anytype, allocator: Allocator, parsed: query_mod.Query, limit: usize) Error!?hit_mod.HitSet {        if (parsed.clauses.len < 2) return null;        for (parsed.clauses) |clause| _ = query_mod.exactClauseTerm(clause) orelse return null;        const cursors = try allocator.alloc(rank_mod.ExactTermCursor, parsed.clauses.len);        var cursor_count: usize = 0;        defer {            for (cursors[0..cursor_count]) |*cursor| cursor.deinit();            allocator.free(cursors);        }        for (parsed.clauses) |clause| {            const term = query_mod.exactClauseTerm(clause).?;            cursors[cursor_count] = try rank_mod.ExactTermCursor.init(self, allocator, term);            cursor_count += 1;            try cursors[cursor_count - 1].advance();        }        var hits: std.ArrayList(hit_mod.Hit) = .empty;        errdefer hits.deinit(allocator);        var total: usize = 0;        while (try rank_mod.nextExactOrHit(cursors)) |hit| {            total += 1;            try hit_mod.appendBoundedHit(allocator, &hits, limit, hit);        }        return .{ .hits = try hits.toOwnedSlice(allocator), .total = total };    }    fn queryScoreBandExactOrHits(self: anytype, allocator: Allocator, parsed: query_mod.Query, limit: usize) Error!?hit_mod.HitSet {        var bounded_hits = (try self.queryBoundedExactOrHits(allocator, parsed, limit)) orelse return null;        errdefer if (bounded_hits.hits.len != 0) allocator.free(bounded_hits.hits);        if (bounded_hits.total <= limit) return bounded_hits;        const cutoff = hit_mod.minHitScore(bounded_hits.hits);        allocator.free(bounded_hits.hits);        bounded_hits.hits = &.{};        bounded_hits.hits = try self.queryExactOrHitsAtLeastScore(allocator, parsed, cutoff);        return bounded_hits;    }    fn queryExactOrHitsAtLeastScore(self: anytype, allocator: Allocator, parsed: query_mod.Query, cutoff: usize) Error![]hit_mod.Hit {        if (parsed.clauses.len < 2) return &.{};        const cursors = try allocator.alloc(rank_mod.ExactTermCursor, parsed.clauses.len);        var cursor_count: usize = 0;        defer {            for (cursors[0..cursor_count]) |*cursor| cursor.deinit();            allocator.free(cursors);        }        for (parsed.clauses) |clause| {            const term = query_mod.exactClauseTerm(clause).?;            cursors[cursor_count] = try rank_mod.ExactTermCursor.init(self, allocator, term);            cursor_count += 1;            try cursors[cursor_count - 1].advance();        }        var hits: std.ArrayList(hit_mod.Hit) = .empty;        errdefer hits.deinit(allocator);        while (try rank_mod.nextExactOrHit(cursors)) |hit| {            if (hit.score >= cutoff) try hits.append(allocator, hit);        }        return try hits.toOwnedSlice(allocator);    }    fn atomIndexHits(self: anytype, allocator: Allocator, atom: query_mod.Atom) Error!?[]hit_mod.Hit {        if (atom.tokens.len == 0 or atom.prefix or atom.near_window != null or atom.field != null) return null;        if (atom.phrase) {            if (!query_mod.isPhrasePairAtom(atom)) return null;            const hits = if (try self.canUsePhrasePairPostings(allocator) and !try self.phrasePairPostingsArePartial(allocator))                try self.phrasePairHits(allocator, atom.tokens[0], atom.tokens[1])            else                try self.phrasePairVerifiedHits(allocator, atom, 1);            errdefer if (hits.len != 0) allocator.free(hits);            for (hits) |*hit| hit.score = std.math.mul(usize, hit.score, atom.tokens.len) catch return error.InvalidSearchIndex;            return hits;        }        var current = try self.termHits(allocator, atom.tokens[0]);        errdefer allocator.free(current);        var index: usize = 1;        while (index < atom.tokens.len) : (index += 1) {            const next = try self.termHits(allocator, atom.tokens[index]);            defer allocator.free(next);            const merged = try hit_mod.intersectHits(allocator, current, next);            allocator.free(current);            current = merged;        }        return current;    }    fn atomBoundedIndexHits(self: anytype, allocator: Allocator, atom: query_mod.Atom, limit: usize) Error!?hit_mod.HitSet {        if (atom.tokens.len != 1 or atom.prefix or atom.phrase or atom.near_window != null or atom.field != null) return null;        return try self.termHitsBounded(allocator, atom.tokens[0], limit);    }    fn atomScoreBandIndexHits(self: anytype, allocator: Allocator, atom: query_mod.Atom, limit: usize) Error!?hit_mod.HitSet {        if (atom.tokens.len != 1 or atom.prefix or atom.phrase or atom.near_window != null or atom.field != null) return null;        return try self.termHitsScoreBand(allocator, atom.tokens[0], limit);    }    fn clauseCandidates(self: anytype, allocator: Allocator, clause: query_mod.Clause) Error![]i64 {        var current = if (clause.positive.len == 0) try self.allDocuments(allocator) else try self.atomDocuments(allocator, clause.positive[0]);        errdefer allocator.free(current);        var index: usize = if (clause.positive.len == 0) 0 else 1;        while (index < clause.positive.len) : (index += 1) {            const next = try self.atomDocuments(allocator, clause.positive[index]);            defer allocator.free(next);            const merged = try hit_mod.intersectRows(allocator, current, next);            allocator.free(current);            current = merged;        }        for (clause.negative) |atom| {            const excluded = try self.atomDocuments(allocator, atom);            defer allocator.free(excluded);            const filtered = try hit_mod.subtractRows(allocator, current, excluded);            allocator.free(current);            current = filtered;        }        return current;    }    fn scoredClauseHits(self: anytype, allocator: Allocator, clause: query_mod.Clause) Error![]hit_mod.Hit {        const candidates = try self.clauseCandidates(allocator, clause);        defer allocator.free(candidates);        var hits: std.ArrayList(hit_mod.Hit) = .empty;        errdefer hits.deinit(allocator);        for (candidates) |rowid| {            const doc = (try self.documents.get(allocator, rowid)) orelse continue;            defer allocator.free(doc);            const score = text_mod.clauseScore(try token_mod.textFromRow(doc), clause);            if (score == 0) continue;            try hits.append(allocator, .{ .rowid = rowid, .score = score });        }        return try hits.toOwnedSlice(allocator);    }    fn atomDocuments(self: anytype, allocator: Allocator, atom: query_mod.Atom) Error![]i64 {        if (atom.tokens.len == 0) return &.{};        if (query_mod.isPhrasePairAtom(atom)) {            const hits = if (try self.canUsePhrasePairPostings(allocator) and !try self.phrasePairPostingsArePartial(allocator))                try self.phrasePairHits(allocator, atom.tokens[0], atom.tokens[1])            else                try self.phrasePairVerifiedHits(allocator, atom, 1);            defer if (hits.len != 0) allocator.free(hits);            var rows: std.ArrayList(i64) = .empty;            errdefer rows.deinit(allocator);            try rows.ensureUnusedCapacity(allocator, hits.len);            for (hits) |hit| rows.appendAssumeCapacity(hit.rowid);            return try rows.toOwnedSlice(allocator);        }        var current = try self.termDocuments(allocator, atom.tokens[0], atom.prefix);        errdefer allocator.free(current);        if (atom.prefix) {            if (atom.field == null) return current;            const filtered = try self.verifiedRowsFromCandidates(allocator, current, atom);            allocator.free(current);            return filtered;        }        var index: usize = 1;        while (index < atom.tokens.len) : (index += 1) {            const next = try self.termDocuments(allocator, atom.tokens[index], false);            defer allocator.free(next);            const merged = try hit_mod.intersectRows(allocator, current, next);            allocator.free(current);            current = merged;        }        if (atom.phrase or atom.near_window != null or atom.field != null) {            const filtered = try self.verifiedRowsFromCandidates(allocator, current, atom);            allocator.free(current);            return filtered;        }        return current;    }    fn verifiedRowsFromCandidates(self: anytype, allocator: Allocator, rows: []const i64, atom: query_mod.Atom) Error![]i64 {        var out: std.ArrayList(i64) = .empty;        errdefer out.deinit(allocator);        for (rows) |rowid| {            var document = (try self.getDocumentText(allocator, rowid)) orelse continue;            const frequency = text_mod.atomFrequency(document.text, atom);            document.deinit(allocator);            if (frequency != 0) try out.append(allocator, rowid);        }        return try out.toOwnedSlice(allocator);    }    fn phrasePairVerifiedHits(self: anytype, allocator: Allocator, atom: query_mod.Atom, score_multiplier: usize) Error![]hit_mod.Hit {        std.debug.assert(query_mod.isPhrasePairAtom(atom));        var left = try rank_mod.ExactTermCursor.init(self, allocator, atom.tokens[0]);        defer left.deinit();        try left.advance();        var right = try rank_mod.ExactTermCursor.init(self, allocator, atom.tokens[1]);        defer right.deinit();        try right.advance();        var indexed: ?rank_mod.ExactTermCursor = if (try self.canUsePhrasePairPostings(allocator))            try rank_mod.ExactTermCursor.initPhrasePair(self, allocator, atom.tokens[0], atom.tokens[1])        else            null;        defer if (indexed) |*cursor| cursor.deinit();        if (indexed) |*cursor| try cursor.advance();        var hits: std.ArrayList(hit_mod.Hit) = .empty;        errdefer hits.deinit(allocator);        var documents: ?text_mod.DocumentTextCursor = null;        defer if (documents) |*cursor| cursor.deinit();        while (try rank_mod.nextPhrasePairCandidateRow(&left, &right)) |rowid| {            const indexed_frequency = if (indexed) |*cursor| try rank_mod.postingCursorFrequencyAt(cursor, rowid) else 0;            const frequency = if (indexed_frequency != 0) indexed_frequency else frequency: {                if (documents == null) {                    documents = @as(text_mod.DocumentTextCursor, undefined);                    errdefer documents = null;                    try documents.?.init(self, allocator, rowid);                }                if (documents) |*cursor| break :frequency (try cursor.atomFrequencyFor(rowid, atom)) orelse 0;                unreachable;            };            if (frequency != 0) try hits.append(allocator, .{                .rowid = rowid,                .score = std.math.mul(usize, frequency, score_multiplier) catch return error.InvalidSearchIndex,            });        }        return try hits.toOwnedSlice(allocator);    }    fn termDocuments(self: anytype, allocator: Allocator, term: []const u8, prefix: bool) Error![]i64 {        if (prefix and !self.index_prefixes) return try self.prefixDocuments(allocator, term);        const hits = try self.termHitsForKind(allocator, if (prefix) '*' else '=', term);        defer allocator.free(hits);        var rows: std.ArrayList(i64) = .empty;        errdefer rows.deinit(allocator);        try rows.ensureUnusedCapacity(allocator, hits.len);        for (hits) |hit| rows.appendAssumeCapacity(hit.rowid);        return try hit_mod.sortedUniqueRows(allocator, &rows);    }    fn termHits(self: anytype, allocator: Allocator, term: []const u8) Error![]hit_mod.Hit {        return try self.termHitsForKind(allocator, '=', term);    }    fn termHitsForKind(self: anytype, allocator: Allocator, kind: u8, term: []const u8) Error![]hit_mod.Hit {        var key_buffer: [token_mod.max_token_bytes + 1]u8 = undefined;        const key_text = posting_mod.postingKeyBuffer(&key_buffer, kind, term);        return try self.postingHits(allocator, key_text);    }    fn postingHits(self: anytype, allocator: Allocator, key_text: []const u8) Error![]hit_mod.Hit {        var lookup: index_mod.Scan = undefined;        try self.terms.lookupPayloads(&lookup, allocator, &.{.{ .text = key_text }});        defer lookup.deinit();        var raw: std.ArrayList(hit_mod.RawHit) = .empty;        errdefer raw.deinit(allocator);        while (try lookup.next()) |entry| try hit_mod.appendRawHits(allocator, &raw, entry.rowid, entry.payload);        return try hit_mod.coalescedHits(allocator, &raw);    }    fn phrasePairHits(self: anytype, allocator: Allocator, first: []const u8, second: []const u8) Error![]hit_mod.Hit {        var key_buffer: [posting_mod.phrase_pair_posting_key_max_bytes]u8 = undefined;        const key_text = posting_mod.phrasePairPostingKeyBuffer(&key_buffer, first, second);        return try self.postingHits(allocator, key_text);    }    fn termHitsBounded(self: anytype, allocator: Allocator, term: []const u8, limit: usize) Error!hit_mod.HitSet {        const hits = try self.termHits(allocator, term);        errdefer if (hits.len != 0) allocator.free(hits);        const total = hits.len;        return .{ .hits = try hit_mod.sortAndLimitOwnedHits(allocator, hits, limit), .total = total };    }    fn termHitsScoreBand(self: anytype, allocator: Allocator, term: []const u8, limit: usize) Error!hit_mod.HitSet {        if (limit == 0) {            const hits = try self.termHits(allocator, term);            return .{ .hits = hits, .total = hits.len };        }        var cursor = try rank_mod.ExactTermCursor.init(self, allocator, term);        defer cursor.deinit();        var hits: std.ArrayList(hit_mod.Hit) = .empty;        errdefer hits.deinit(allocator);        var total: usize = 0;        while (true) {            try cursor.advance();            const hit = cursor.current orelse break;            total += 1;            try hit_mod.appendBoundedHit(allocator, &hits, limit, hit);        }        if (total <= limit) {            const owned = try hits.toOwnedSlice(allocator);            std.mem.sort(hit_mod.Hit, owned, {}, hit_mod.hitLess);            return .{ .hits = owned, .total = total };        }        const cutoff = hit_mod.minHitScore(hits.items);        hits.clearRetainingCapacity();        cursor.reset();        while (true) {            try cursor.advance();            const hit = cursor.current orelse break;            if (hit.score >= cutoff) try hits.append(allocator, hit);        }        const owned = try hits.toOwnedSlice(allocator);        std.mem.sort(hit_mod.Hit, owned, {}, hit_mod.hitLess);        return .{ .hits = owned, .total = total };    }    fn termHitsAtLeastScore(self: anytype, allocator: Allocator, term: []const u8, cutoff: usize) Error![]hit_mod.Hit {        const source = try self.termHits(allocator, term);        defer allocator.free(source);        var hits: std.ArrayList(hit_mod.Hit) = .empty;        errdefer hits.deinit(allocator);        for (source) |hit| {            const score = hit.score;            if (score < cutoff) continue;            try hits.append(allocator, hit);        }        return try hits.toOwnedSlice(allocator);    }    fn prefixHits(self: anytype, allocator: Allocator, term: []const u8) Error![]hit_mod.Hit {        if (self.index_prefixes) return try self.termHitsForKind(allocator, '*', term);        var start_buffer: [token_mod.max_token_bytes + 1]u8 = undefined;        const start = posting_mod.postingKeyBuffer(&start_buffer, '=', term);        var end_buffer: [token_mod.max_token_bytes + 2]u8 = undefined;        const end = posting_mod.prefixEndBuffer(&end_buffer, start);        var lookup: index_mod.Scan = undefined;        try self.terms.scanPayloads(            &lookup,            allocator,            &.{.{ .text = start }},            &.{.{ .text = end }},        );        defer lookup.deinit();        var raw: std.ArrayList(hit_mod.RawHit) = .empty;        errdefer raw.deinit(allocator);        while (try lookup.next()) |entry| try hit_mod.appendRawHits(allocator, &raw, entry.rowid, entry.payload);        const hits = try hit_mod.coalescedHits(allocator, &raw);        return hits;    }    fn prefixDocuments(self: anytype, allocator: Allocator, term: []const u8) Error![]i64 {        const hits = try self.prefixHits(allocator, term);        defer allocator.free(hits);        var rows: std.ArrayList(i64) = .empty;        errdefer rows.deinit(allocator);        try rows.ensureUnusedCapacity(allocator, hits.len);        for (hits) |hit| rows.appendAssumeCapacity(hit.rowid);        return try hit_mod.sortedUniqueRows(allocator, &rows);    }    fn allDocuments(self: anytype, allocator: Allocator) Error![]i64 {        var scan: table_mod.Scan = undefined;        try self.documents.scan(&scan, allocator, null, null);        defer scan.deinit();        var rows: std.ArrayList(i64) = .empty;        errdefer rows.deinit(allocator);        while (try scan.next()) |entry| try rows.append(allocator, entry.rowid);        return try hit_mod.sortedUniqueRows(allocator, &rows);    }    fn corpusStats(self: anytype, allocator: Allocator) Error!posting_mod.CorpusStats {        var lookup: index_mod.Scan = undefined;        try self.terms.lookupPayloads(&lookup, allocator, &posting_mod.corpusStatsValues());        defer lookup.deinit();        const entry = (try lookup.next()) orelse return error.InvalidSearchIndex;        if (entry.rowid != posting_mod.internal_meta_rowid) return error.InvalidSearchIndex;        const stats = try posting_mod.corpusStatsPayloadValue(entry.payload);        if (try lookup.next() != null) return error.InvalidSearchIndex;        return stats;    }    fn corpusStatsOrEmpty(self: anytype, allocator: Allocator) Error!posting_mod.CorpusStats {        return self.corpusStats(allocator) catch |err| switch (err) {            error.InvalidSearchIndex => stats: {                var scan: table_mod.Scan = undefined;                try self.documents.scan(&scan, allocator, null, null);                defer scan.deinit();                if (try scan.next() != null) return error.InvalidSearchIndex;                break :stats .{ .documents = 0, .total_tokens = 0 };            },            else => return err,        };    }    fn documentLength(self: anytype, allocator: Allocator, rowid: i64) Error!usize {        var cursor = try rank_mod.DocumentLengthCursor.init(self, allocator);        defer cursor.deinit();        return try cursor.lengthFor(rowid);    }    fn documentLengths(self: anytype, allocator: Allocator) Error![]posting_mod.DocumentLength {        var cursor = try rank_mod.DocumentLengthCursor.init(self, allocator);        defer cursor.deinit();        var lengths: std.ArrayList(posting_mod.DocumentLength) = .empty;        errdefer lengths.deinit(allocator);        while (cursor.rowid) |rowid| {            try lengths.append(allocator, .{                .rowid = rowid,                .length = cursor.length,            });            try cursor.advance();        }        return try lengths.toOwnedSlice(allocator);    }    fn collectRankingTerms(self: anytype, allocator: Allocator, parsed: query_mod.Query, document_count: usize) Error![]rank_mod.RankingTerm {        var terms: std.ArrayList(rank_mod.RankingTerm) = .empty;        errdefer {            for (terms.items) |*term| term.deinit(allocator);            terms.deinit(allocator);        }        for (parsed.clauses) |*clause| {            for (clause.positive) |*atom| {                if (atom.tokens.len == 0) continue;                if (query_mod.isExactRankingAtom(atom.*)) {                    const hits = try self.termHits(allocator, atom.tokens[0]);                    const idf = rank_mod.bm25Idf(document_count, hits.len) catch |err| {                        if (hits.len != 0) allocator.free(hits);                        return err;                    };                    terms.append(allocator, .{                        .atom = atom,                        .posting_hits = hits,                        .document_frequency = hits.len,                        .idf = idf,                    }) catch |err| {                        if (hits.len != 0) allocator.free(hits);                        return err;                    };                } else if (query_mod.isPhrasePairAtom(atom.*)) {                    const hits = if (try self.canUsePhrasePairPostings(allocator) and !try self.phrasePairPostingsArePartial(allocator))                        try self.phrasePairHits(allocator, atom.tokens[0], atom.tokens[1])                    else                        try self.phrasePairVerifiedHits(allocator, atom.*, 1);                    const idf = rank_mod.bm25Idf(document_count, hits.len) catch |err| {                        if (hits.len != 0) allocator.free(hits);                        return err;                    };                    terms.append(allocator, .{                        .atom = atom,                        .posting_hits = hits,                        .document_frequency = hits.len,                        .idf = idf,                    }) catch |err| {                        if (hits.len != 0) allocator.free(hits);                        return err;                    };                } else {                    const frequency = try self.atomDocumentFrequency(allocator, atom.*);                    try terms.append(allocator, .{                        .atom = atom,                        .posting_hits = &.{},                        .document_frequency = frequency,                        .idf = try rank_mod.bm25Idf(document_count, frequency),                    });                }            }        }        return try terms.toOwnedSlice(allocator);    }    fn atomDocumentFrequency(self: anytype, allocator: Allocator, atom: query_mod.Atom) Error!usize {        const rows = try self.atomDocuments(allocator, atom);        defer allocator.free(rows);        if (!atom.phrase) return rows.len;        var count: usize = 0;        for (rows) |rowid| {            var document = (try self.getDocumentText(allocator, rowid)) orelse continue;            const frequency = text_mod.atomFrequency(document.text, atom);            document.deinit(allocator);            if (frequency != 0) count += 1;        }        return count;    }    fn clearIn(self: *Search, write: *tree.Write, allocator: Allocator) Error!void {        _ = allocator;        try write.clear(&self.terms.entries);        try write.clear(&self.documents.rows);    }    fn collectTermKeys(self: anytype, allocator: Allocator) Error![]const []const u8 {        var scan: index_mod.Scan = undefined;        try self.terms.scan(&scan, allocator, null, null);        defer scan.deinit();        var keys: std.ArrayList([]const u8) = .empty;        errdefer posting_mod.freeStringSlice(allocator, keys.items);        while (try scan.next()) |entry| try keys.append(allocator, try allocator.dupe(u8, entry.key));        return try keys.toOwnedSlice(allocator);    }    fn collectPostings(self: anytype, allocator: Allocator, documents: []const Document) Error!posting_mod.PostingSet {        var postings: std.ArrayList(posting_mod.Posting) = .empty;        errdefer postings.deinit(allocator);        var keys: std.ArrayList([]const u8) = .empty;        errdefer posting_mod.deinitStringList(allocator, &keys);        var key_map = posting_mod.PostingKeyMap.init(allocator);        defer key_map.deinit();        const phrase_pairs = self.index_phrase_pairs and !self.omit_phrase_pair_singletons;        if (!self.index_prefixes) {            const postings_capacity = bulkCapacity(documents.len, if (phrase_pairs) 32 else 16, 0);            try postings.ensureTotalCapacity(allocator, postings_capacity);            const key_capacity = bulkCapacity(documents.len, if (phrase_pairs) 2 else 1, 64);            try keys.ensureTotalCapacity(allocator, key_capacity);            try key_map.ensureTotalCapacity(hashCapacityHint(key_capacity));        }        const term_count_capacity = try documentTermCountScratchCapacity(documents, self.index_prefixes, phrase_pairs);        const term_counts: []posting_mod.TermCount = if (term_count_capacity == 0)            &.{}        else            try allocator.alloc(posting_mod.TermCount, term_count_capacity);        defer if (term_counts.len != 0) allocator.free(term_counts);        for (documents) |document| {            const counts = try posting_mod.documentInternedTermCounts(allocator, token_mod.documentText(document.text), self.index_prefixes, phrase_pairs, &key_map, &keys, term_counts);            try postings.ensureUnusedCapacity(allocator, counts.len);            for (counts) |term_count| {                const key_index = key_map.get(term_count.key) orelse return error.InvalidSearchIndex;                const count = std.math.cast(u32, term_count.count) orelse return error.InvalidSearchIndex;                postings.appendAssumeCapacity(.{ .rowid = document.rowid, .key_index = key_index, .count = count });            }        }        if (self.index_phrase_pairs and self.omit_phrase_pair_singletons) try posting_mod.collectPartialPhrasePairPostings(allocator, documents, &postings, &keys);        const items = try postings.toOwnedSlice(allocator);        errdefer if (items.len != 0) allocator.free(items);        return .{            .items = items,            .keys = try keys.toOwnedSlice(allocator),        };    }};

Source: lib/sql/src/root.zig:251

zig
pub const Search = search.Search;
Called byCallsNo direct callersprivate sourcelib.sql.src.search.engine.SearchputSourceInSearchsourceMatchesSearchadvanceSource
Static calls · unresolved targets: 0 · external targets: 5.
Called byCallsNo direct callersprivate sourcelib.sql.src.search.engine.SearchindexCapabilitiesSearchcapabilities
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callersprivate sourcelib.sql.src.search.engine.SearchclearInprivate sourcelib.sql.src.search.engine.SearchputRequestedCapabilitiesInSearchclear
Static calls · unresolved targets: 0 · external targets: 5.
Called byCallsNo direct callersprivate sourcelib.sql.src.search.engine.SearchcorpusStatsOrEmptyprivate sourcelib.sql.src.search.engine.SearchdeleteDocumentLengthInprivate sourcelib.sql.src.search.engine.SearchdeletePostingsInprivate sourcelib.sql.src.search.engine.SearchdocumentLengthprivate sourcelib.sql.src.search.engine.SearchensureRequestedCapabilitiesInprivate sourcelib.sql.src.search.engine.SearchputCorpusStatsInSearchdelete
Static calls · unresolved targets: 1 · external targets: 9.
Called byCallsNo direct callersprivate sourcelib.sql.src.search.engine.SearchgetDocumentTextSearchgetText
Static calls · unresolved targets: 0 · external targets: 2.
Called byCallsNo direct callsSearchsourceMatchesSearchsourceSchemaMatchesSearchindexedSource
Static calls · unresolved targets: 0 · external targets: 5.
Called byCallsNo direct callersprivate sourcelib.sql.src.search.engine.SearchloadAllWithSourceSearchloadAll
Static calls · unresolved targets: 0 · external targets: 2.
Called byCallsNo direct callersprivate sourcelib.sql.src.search.engine.SearchloadAllWithSourceSearchloadAllForSource
Static calls · unresolved targets: 0 · external targets: 2.
Called byCallsNo direct callersprivate sourcelib.sql.src.search.engine.SearchcollectPostingsprivate sourcelib.sql.src.search.engine.SearchcorpusStatsOrEmptyprivate sourcelib.sql.src.search.engine.SearchensureRequestedCapabilitiesInprivate sourcelib.sql.src.search.engine.SearchputCorpusStatsInprivate sourcelib.sql.src.search.engine.SearchputSortedDocumentLengthsIn+2 moreSearchloadAllNew
Static calls · unresolved targets: 1 · external targets: 10.
Called byCallsNo direct callstest sourcelib.sql.src.search.enginetest: search OR unions exact hits and...test sourcelib.sql.src.search.enginetest: search bounds oversized documen...test sourcelib.sql.src.search.enginetest: search bulk load can rebuild id...test sourcelib.sql.src.search.enginetest: search bulk load rebuilds chang...test sourcelib.sql.src.search.enginetest: search bulk load replaces exist...+50 moreSearchopen
Static calls · unresolved targets: 0 · external targets: 3.
Called byCallsNo direct callersprivate sourcelib.sql.src.search.engine.SearchputInSearchput
Static calls · unresolved targets: 0 · external targets: 5.
Called byCallsNo direct callersprivate sourcelib.sql.src.search.engine.SearchcorpusStatsOrEmptyprivate sourcelib.sql.src.search.engine.SearchdeleteDocumentLengthInprivate sourcelib.sql.src.search.engine.SearchdeletePostingsInprivate sourcelib.sql.src.search.engine.SearchdocumentLengthprivate sourcelib.sql.src.search.engine.SearchensureRequestedCapabilitiesIn+4 moreSearchputAll
Static calls · unresolved targets: 1 · external targets: 11.
Called byCallsNo direct callersprivate sourcelib.sql.src.search.engine.SearchputInprivate sourcelib.sql.src.search.engine.SearchputSourceInSearchsourceMatchesSearchputForSource
Static calls · unresolved targets: 0 · external targets: 5.
Called byCallsNo direct callersprivate sourcelib.sql.src.search.engine.SearchqueryParsedSearchquery
Static calls · unresolved targets: 0 · external targets: 4.
Called byCallsNo direct callersprivate sourcelib.sql.src.search.engine.SearchqueryParsedRankedSearchqueryRanked
Static calls · unresolved targets: 0 · external targets: 4.
Called byCallsNo direct callersprivate sourcelib.sql.src.search.engine.SearchqueryParsedScoreBandSearchqueryScoreBand
Static calls · unresolved targets: 0 · external targets: 4.
Called byCallsSearchadvanceSourceSearchputForSourceSearchindexedSourceSearchsourceMatches
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallsNo direct callersSearchindexedSourceSearchsourceSchemaMatches
Static calls · unresolved targets: 0 · external targets: 0.

Also reachable as

search.Search.

Complete call list for Search.loadAllNew

7 direct calls.

Complete caller list for Search.open

55 direct callers.

Complete call list for Search.putAll

9 direct calls.

Audit

Definitions21
Public names42
Members7
Version26.7.0
Revisiondaab053ee433