-- SPDX-FileCopyrightText: © 2026 Vladimir Zorin -- SPDX-License-Identifier: OWL-1.0 or later local testimony = require("testimony") local std = require("std") local mneme = require("mneme") local testify = testimony.new("== mneme: ft bulk loading ==") local tmp_path = function() return "/tmp/mneme_ft_bulk_" .. std.nanoid() .. ".mneme" end testify:that("pause_indexing prevents index updates", function() local path = tmp_path() local db = mneme.open(path, { sync = "none" }) local ft = db:ft_keyspace("corpus") ft:pause_indexing() ft:put("d1", "hello world test") ft:put("d2", "hello again friend") -- Document text should be stored testimony.assert_equal("hello world test", ft:get("d1")) testimony.assert_equal("hello again friend", ft:get("d2")) -- But search should fail while paused local results, err = ft:search("hello") testimony.assert_nil(results) testimony.assert_equal("indexing paused", err) db:close() std.fs.remove(path) end) testify:that("rebuild_index creates correct index from stored docs", function() local path = tmp_path() local db = mneme.open(path, { sync = "none" }) local ft = db:ft_keyspace("corpus") ft:pause_indexing() ft:put("d1", "tcp congestion window control") ft:put("d2", "udp datagram protocol") ft:put("d3", "tcp flow control mechanism") ft:rebuild_index() -- Search should now work local results = ft:search("tcp") testimony.assert_equal(2, #results) local keys = {} for _, r in ipairs(results) do keys[r.key] = true end testimony.assert_true(keys["d1"] ~= nil) testimony.assert_true(keys["d3"] ~= nil) -- Stats should be correct local stats = ft:stats() testimony.assert_equal(3, stats.doc_count) testimony.assert_true(stats.unique_terms > 0) db:close() std.fs.remove(path) end) testify:that("rebuild matches auto-indexed equivalent", function() local path1 = tmp_path() local path2 = tmp_path() -- Auto-indexed local db1 = mneme.open(path1, { sync = "none" }) local ft1 = db1:ft_keyspace("corpus") -- Bulk-loaded local db2 = mneme.open(path2, { sync = "none" }) local ft2 = db2:ft_keyspace("corpus") ft2:pause_indexing() local docs = { { "rfc1", "congestion control for real time traffic" }, { "rfc2", "flow control and window management" }, { "rfc3", "congestion avoidance algorithm cubic" }, { "rfc4", "network congestion and flow analysis" }, { "rfc5", "reliable transport protocol design" }, } for _, doc in ipairs(docs) do ft1:put(doc[1], doc[2]) ft2:put(doc[1], doc[2]) end ft2:rebuild_index() -- Compare search results local queries = { "congestion", "flow", "congestion flow", "protocol", "control" } for _, q in ipairs(queries) do local r1 = ft1:search(q) local r2 = ft2:search(q) testimony.assert_equal(#r1, #r2, "result count mismatch for query: " .. q) for i = 1, #r1 do testimony.assert_equal(r1[i].key, r2[i].key, "key mismatch at pos " .. i .. " for query: " .. q) testimony.assert_true( math.abs(r1[i].score - r2[i].score) < 0.001, "score mismatch at pos " .. i .. " for query: " .. q ) end end -- Compare stats local s1 = ft1:stats() local s2 = ft2:stats() testimony.assert_equal(s1.doc_count, s2.doc_count) testimony.assert_equal(s1.unique_terms, s2.unique_terms) testimony.assert_true(math.abs(s1.avg_doc_len - s2.avg_doc_len) < 0.01) db1:close() db2:close() std.fs.remove(path1) std.fs.remove(path2) end) testify:that("rebuild with many documents uses chunked commits", function() local path = tmp_path() local db = mneme.open(path, { sync = "none" }) local ft = db:ft_keyspace("corpus") ft:pause_indexing() -- Insert enough docs to trigger chunked processing for i = 1, 60 do ft:put( "doc" .. string.format("%03d", i), "document number " .. i .. " with some words for indexing test content" ) end local ok, err = ft:rebuild_index() testimony.assert_not_nil(ok, err) local stats = ft:stats() testimony.assert_equal(60, stats.doc_count) -- Verify search works local results = ft:search("document") testimony.assert_true(#results > 0) db:close() std.fs.remove(path) end) testify:that("rebuild resumes auto-indexing", function() local path = tmp_path() local db = mneme.open(path, { sync = "none" }) local ft = db:ft_keyspace("corpus") ft:pause_indexing() ft:put("d1", "initial content") ft:rebuild_index() -- Auto-indexing should be resumed ft:put("d2", "new content added after rebuild") -- Both should be searchable local r1 = ft:search("initial") testimony.assert_equal(1, #r1) local r2 = ft:search("rebuild") testimony.assert_equal(1, #r2) testimony.assert_equal("d2", r2[1].key) db:close() std.fs.remove(path) end) testify:that("delete paused-inserted document succeeds", function() local path = tmp_path() local db = mneme.open(path, { sync = "none" }) local ft = db:ft_keyspace("corpus") ft:pause_indexing() local ok, err = ft:put("a", "alpha beta") testimony.assert_not_nil(ok, err) ok, err = ft:del("a") testimony.assert_not_nil(ok, err) testimony.assert_nil(ft:get("a")) ok, err = ft:rebuild_index() testimony.assert_not_nil(ok, err) local results = ft:search("alpha") testimony.assert_equal(0, #results) db:close() std.fs.remove(path) end) testify:that("delete indexed document while paused removes it", function() local path = tmp_path() local db = mneme.open(path, { sync = "none" }) local ft = db:ft_keyspace("corpus") local ok, err = ft:put("z", "zeta theta") testimony.assert_not_nil(ok, err) local before = ft:search("zeta") testimony.assert_equal(1, #before) ft:pause_indexing() ok, err = ft:del("z") testimony.assert_not_nil(ok, err) testimony.assert_nil(ft:get("z")) ok, err = ft:rebuild_index() testimony.assert_not_nil(ok, err) local after = ft:search("zeta") testimony.assert_equal(0, #after) db:close() std.fs.remove(path) end) testify:conclude()