X-Git-Url: https://git.sesse.net/?a=blobdiff_plain;f=plocate-build.cpp;h=6d393e444c830bb396db9f873c5f1015e3352af2;hb=9cdb6c7988b2ab73a4536da1d8ec33d4083bb2d1;hp=40ce2ff893ad455e6816b73c43e567c0aea1ac16;hpb=c41f998855416a8619751653f3cedd9bc6f0f4c0;p=plocate diff --git a/plocate-build.cpp b/plocate-build.cpp index 40ce2ff..6d393e4 100644 --- a/plocate-build.cpp +++ b/plocate-build.cpp @@ -1,34 +1,38 @@ -#include "vp4.h" +#include "db.h" +#include "dprintf.h" +#include "turbopfor-encode.h" #include -#include #include #include -#include -#include +#include +#include #include #include +#include +#include #include +#include #include #include -#include +#include #include -#include -#include -#include +#include #include +#include #include -#include "db.h" - #define P4NENC_BOUND(n) ((n + 127) / 128 + (n + 32) * sizeof(uint32_t)) -#define dprintf(...) -//#define dprintf(...) fprintf(stderr, __VA_ARGS__); + +#define NUM_TRIGRAMS 16777216 using namespace std; using namespace std::chrono; -string zstd_compress(const string &src, string *tempbuf); +string zstd_compress(const string &src, ZSTD_CDict *cdict, string *tempbuf); + +constexpr unsigned num_overflow_slots = 16; +bool use_debug = false; static inline uint32_t read_unigram(const string_view s, size_t idx) { @@ -70,7 +74,7 @@ struct db_directory { class PostingListBuilder { public: - void add_docid(uint32_t docid); + inline void add_docid(uint32_t docid); void finish(); string encoded; @@ -80,34 +84,31 @@ private: void write_header(uint32_t docid); void append_block(); - vector pending_docids; + vector pending_deltas; - uint32_t last_block_end; + uint32_t last_block_end, last_docid = -1; }; void PostingListBuilder::add_docid(uint32_t docid) { // Deduplicate against the last inserted value, if any. - if (pending_docids.empty()) { - if (encoded.empty()) { - // Very first docid. - write_header(docid); - ++num_docids; - last_block_end = docid; - return; - } else if (docid == last_block_end) { - return; - } - } else { - if (docid == pending_docids.back()) { - return; - } + if (docid == last_docid) { + return; } - pending_docids.push_back(docid); - if (pending_docids.size() == 128) { + if (num_docids == 0) { + // Very first docid. + write_header(docid); + ++num_docids; + last_block_end = last_docid = docid; + return; + } + + pending_deltas.push_back(docid - last_docid - 1); + last_docid = docid; + if (pending_deltas.size() == 128) { append_block(); - pending_docids.clear(); + pending_deltas.clear(); last_block_end = docid; } ++num_docids; @@ -115,7 +116,7 @@ void PostingListBuilder::add_docid(uint32_t docid) void PostingListBuilder::finish() { - if (pending_docids.empty()) { + if (pending_deltas.empty()) { return; } @@ -123,40 +124,152 @@ void PostingListBuilder::finish() // No interleaving for partial blocks. unsigned char buf[P4NENC_BOUND(128)]; - unsigned char *end = p4d1enc32(pending_docids.data(), pending_docids.size(), buf, last_block_end); + unsigned char *end = encode_pfor_single_block<128>(pending_deltas.data(), pending_deltas.size(), /*interleaved=*/false, buf); encoded.append(reinterpret_cast(buf), reinterpret_cast(end)); } void PostingListBuilder::append_block() { unsigned char buf[P4NENC_BOUND(128)]; - assert(pending_docids.size() == 128); - unsigned char *end = p4d1enc128v32(pending_docids.data(), 128, buf, last_block_end); + assert(pending_deltas.size() == 128); + unsigned char *end = encode_pfor_single_block<128>(pending_deltas.data(), 128, /*interleaved=*/true, buf); encoded.append(reinterpret_cast(buf), reinterpret_cast(end)); } void PostingListBuilder::write_header(uint32_t docid) { unsigned char buf[P4NENC_BOUND(1)]; - size_t bytes = p4nd1enc128v32(&docid, 1, buf); - encoded.append(reinterpret_cast(buf), bytes); + unsigned char *end = write_baseval(docid, buf); + encoded.append(reinterpret_cast(buf), end - buf); +} + +class DatabaseReceiver { +public: + virtual ~DatabaseReceiver() = default; + virtual void add_file(string filename) = 0; + virtual void flush_block() = 0; +}; + +class DictionaryBuilder : public DatabaseReceiver { +public: + DictionaryBuilder(size_t blocks_to_keep, size_t block_size) + : blocks_to_keep(blocks_to_keep), block_size(block_size) {} + void add_file(string filename) override; + void flush_block() override; + string train(size_t buf_size); + +private: + const size_t blocks_to_keep, block_size; + string current_block; + uint64_t block_num = 0; + size_t num_files_in_block = 0; + + std::mt19937 reservoir_rand{ 1234 }; // Fixed seed for reproducibility. + bool keep_current_block = true; + int64_t slot_for_current_block = -1; + + vector sampled_blocks; + vector lengths; +}; + +void DictionaryBuilder::add_file(string filename) +{ + if (keep_current_block) { // Only bother saving the filenames if we're actually keeping the block. + if (!current_block.empty()) { + current_block.push_back('\0'); + } + current_block += filename; + } + if (++num_files_in_block == block_size) { + flush_block(); + } +} + +void DictionaryBuilder::flush_block() +{ + if (keep_current_block) { + if (slot_for_current_block == -1) { + lengths.push_back(current_block.size()); + sampled_blocks.push_back(move(current_block)); + } else { + lengths[slot_for_current_block] = current_block.size(); + sampled_blocks[slot_for_current_block] = move(current_block); + } + } + current_block.clear(); + num_files_in_block = 0; + ++block_num; + + if (block_num < blocks_to_keep) { + keep_current_block = true; + slot_for_current_block = -1; + } else { + // Keep every block with equal probability (reservoir sampling). + uint64_t idx = uniform_int_distribution(0, block_num)(reservoir_rand); + keep_current_block = (idx < blocks_to_keep); + slot_for_current_block = idx; + } +} + +string DictionaryBuilder::train(size_t buf_size) +{ + string dictionary_buf; + sort(sampled_blocks.begin(), sampled_blocks.end()); // Seemingly important for decompression speed. + for (const string &block : sampled_blocks) { + dictionary_buf += block; + } + + string buf; + buf.resize(buf_size); + size_t ret = ZDICT_trainFromBuffer(&buf[0], buf_size, dictionary_buf.data(), lengths.data(), lengths.size()); + dprintf("Sampled %zu bytes in %zu blocks, built a dictionary of size %zu\n", dictionary_buf.size(), lengths.size(), ret); + buf.resize(ret); + + sampled_blocks.clear(); + lengths.clear(); + + return buf; } -class Corpus { +class Corpus : public DatabaseReceiver { public: - Corpus(size_t block_size) - : block_size(block_size) {} - void add_file(string filename); - void flush_block(); + Corpus(FILE *outfp, size_t block_size, ZSTD_CDict *cdict) + : invindex(new PostingListBuilder *[NUM_TRIGRAMS]), outfp(outfp), block_size(block_size), cdict(cdict) + { + fill(invindex.get(), invindex.get() + NUM_TRIGRAMS, nullptr); + } + ~Corpus() override + { + for (unsigned i = 0; i < NUM_TRIGRAMS; ++i) { + delete invindex[i]; + } + } - vector filename_blocks; - unordered_map invindex; + void add_file(string filename) override; + void flush_block() override; + + vector filename_blocks; size_t num_files = 0, num_files_in_block = 0, num_blocks = 0; + bool seen_trigram(uint32_t trgm) + { + return invindex[trgm] != nullptr; + } + PostingListBuilder &get_pl_builder(uint32_t trgm) + { + if (invindex[trgm] == nullptr) { + invindex[trgm] = new PostingListBuilder; + } + return *invindex[trgm]; + } + size_t num_trigrams() const; private: + unique_ptr invindex; + FILE *outfp; string current_block; string tempbuf; const size_t block_size; + ZSTD_CDict *cdict; }; void Corpus::add_file(string filename) @@ -186,86 +299,146 @@ void Corpus::flush_block() if (s.size() >= 3) { for (size_t j = 0; j < s.size() - 2; ++j) { uint32_t trgm = read_trigram(s, j); - invindex[trgm].add_docid(docid); + get_pl_builder(trgm).add_docid(docid); } } ptr += s.size() + 1; } // Compress and add the filename block. - filename_blocks.push_back(zstd_compress(current_block, &tempbuf)); + filename_blocks.push_back(ftell(outfp)); + string compressed = zstd_compress(current_block, cdict, &tempbuf); + if (fwrite(compressed.data(), compressed.size(), 1, outfp) != 1) { + perror("fwrite()"); + exit(1); + } current_block.clear(); num_files_in_block = 0; ++num_blocks; } -const char *handle_directory(const char *ptr, Corpus *corpus) +size_t Corpus::num_trigrams() const { - ptr += sizeof(db_directory); + size_t num = 0; + for (unsigned trgm = 0; trgm < NUM_TRIGRAMS; ++trgm) { + if (invindex[trgm] != nullptr) { + ++num; + } + } + return num; +} + +string read_cstr(FILE *fp) +{ + string ret; + for (;;) { + int ch = getc(fp); + if (ch == -1) { + perror("getc"); + exit(1); + } + if (ch == 0) { + return ret; + } + ret.push_back(ch); + } +} + +void handle_directory(FILE *fp, DatabaseReceiver *receiver) +{ + db_directory dummy; + if (fread(&dummy, sizeof(dummy), 1, fp) != 1) { + if (feof(fp)) { + return; + } else { + perror("fread"); + } + } - string dir_path = ptr; - ptr += dir_path.size() + 1; + string dir_path = read_cstr(fp); if (dir_path == "/") { dir_path = ""; } for (;;) { - uint8_t type = *ptr++; + int type = getc(fp); if (type == DBE_NORMAL) { - string filename = ptr; - corpus->add_file(dir_path + "/" + filename); - ptr += filename.size() + 1; + string filename = read_cstr(fp); + receiver->add_file(dir_path + "/" + filename); } else if (type == DBE_DIRECTORY) { - string dirname = ptr; - corpus->add_file(dir_path + "/" + dirname); - ptr += dirname.size() + 1; + string dirname = read_cstr(fp); + receiver->add_file(dir_path + "/" + dirname); } else { - return ptr; + return; // Probably end. } } } -void read_mlocate(const char *filename, Corpus *corpus) +void read_plaintext(FILE *fp, DatabaseReceiver *receiver) { - int fd = open(filename, O_RDONLY); - if (fd == -1) { - perror(filename); + if (fseek(fp, 0, SEEK_SET) != 0) { + perror("fseek"); exit(1); } - off_t len = lseek(fd, 0, SEEK_END); - if (len == -1) { - perror("lseek"); - exit(1); + + while (!feof(fp)) { + char buf[1024]; + if (fgets(buf, sizeof(buf), fp) == nullptr) { + break; + } + string s(buf); + assert(!s.empty()); + while (s.back() != '\n' && !feof(fp)) { + // The string was longer than the buffer, so read again. + if (fgets(buf, sizeof(buf), fp) == nullptr) { + break; + } + s += buf; + } + if (!s.empty() && s.back() == '\n') + s.pop_back(); + receiver->add_file(move(s)); } - const char *data = (char *)mmap(nullptr, len, PROT_READ, MAP_SHARED, fd, /*offset=*/0); - if (data == MAP_FAILED) { - perror("mmap"); +} + +void read_mlocate(FILE *fp, DatabaseReceiver *receiver) +{ + if (fseek(fp, 0, SEEK_SET) != 0) { + perror("fseek"); exit(1); } - const db_header *hdr = (const db_header *)data; + db_header hdr; + if (fread(&hdr, sizeof(hdr), 1, fp) != 1) { + perror("short read"); + exit(1); + } // TODO: Care about the base path. - string path = data + sizeof(db_header); - uint64_t offset = sizeof(db_header) + path.size() + 1 + ntohl(hdr->conf_size); - - const char *ptr = data + offset; - while (ptr < data + len) { - ptr = handle_directory(ptr, corpus); + string path = read_cstr(fp); + while (!feof(fp)) { + handle_directory(fp, receiver); } - - munmap((void *)data, len); - close(fd); } -string zstd_compress(const string &src, string *tempbuf) +string zstd_compress(const string &src, ZSTD_CDict *cdict, string *tempbuf) { + static ZSTD_CCtx *ctx = nullptr; + if (ctx == nullptr) { + ctx = ZSTD_createCCtx(); + } + size_t max_size = ZSTD_compressBound(src.size()); if (tempbuf->size() < max_size) { tempbuf->resize(max_size); } - size_t size = ZSTD_compress(&(*tempbuf)[0], max_size, src.data(), src.size(), /*level=*/6); + size_t size; + if (cdict == nullptr) { + size = ZSTD_compressCCtx(ctx, &(*tempbuf)[0], max_size, src.data(), src.size(), /*level=*/6); + } else { + size = ZSTD_compress_usingCDict(ctx, &(*tempbuf)[0], max_size, src.data(), src.size(), cdict); + } return string(tempbuf->data(), size); } @@ -294,7 +467,7 @@ uint32_t next_prime(uint32_t x) return x; } -unique_ptr create_hashtable(const Corpus &corpus, const vector &all_trigrams, uint32_t ht_size, uint32_t num_overflow_slots) +unique_ptr create_hashtable(Corpus &corpus, const vector &all_trigrams, uint32_t ht_size, uint32_t num_overflow_slots) { unique_ptr ht(new Trigram[ht_size + num_overflow_slots + 1]); // 1 for the sentinel element at the end. for (unsigned i = 0; i < ht_size + num_overflow_slots + 1; ++i) { @@ -304,7 +477,7 @@ unique_ptr create_hashtable(const Corpus &corpus, const vectorsecond.num_docids), 0}; + Trigram to_insert{ trgm, uint32_t(corpus.get_pl_builder(trgm).num_docids), 0 }; uint32_t bucket = hash_trigram(trgm, ht_size); unsigned distance = 0; @@ -326,56 +499,107 @@ unique_ptr create_hashtable(const Corpus &corpus, const vector(steady_clock::now() - start).count()); - // Sort the trigrams, mostly to get a consistent result every time - // (the hash table will put things in random order anyway). + // Find the used trigrams. vector all_trigrams; - for (auto &[trigram, pl_builder] : corpus.invindex) { - all_trigrams.push_back(trigram); + for (unsigned trgm = 0; trgm < NUM_TRIGRAMS; ++trgm) { + if (corpus.seen_trigram(trgm)) { + all_trigrams.push_back(trgm); + } } - sort(all_trigrams.begin(), all_trigrams.end()); + // Create the hash table. unique_ptr hashtable; uint32_t ht_size = next_prime(all_trigrams.size()); - constexpr unsigned num_overflow_slots = 16; - for ( ;; ) { + for (;;) { hashtable = create_hashtable(corpus, all_trigrams, ht_size, num_overflow_slots); if (hashtable == nullptr) { dprintf("Failed creating hash table of size %u, increasing by 5%% and trying again.\n", ht_size); @@ -386,69 +610,43 @@ void do_build(const char *infile, const char *outfile, int block_size) } } - umask(0027); - FILE *outfp = fopen(outfile, "wb"); - // Find the offsets for each posting list. size_t bytes_for_hashtable = (ht_size + num_overflow_slots + 1) * sizeof(Trigram); - uint64_t offset = sizeof(Header) + bytes_for_hashtable; + uint64_t offset = ftell(outfp) + bytes_for_hashtable; for (unsigned i = 0; i < ht_size + num_overflow_slots + 1; ++i) { hashtable[i].offset = offset; // Needs to be there even for empty slots. if (hashtable[i].num_docids == 0) { continue; } - const PostingListBuilder &pl_builder = corpus.invindex[hashtable[i].trgm]; - offset += pl_builder.encoded.size(); + const string &encoded = corpus.get_pl_builder(hashtable[i].trgm).encoded; + offset += encoded.size(); } - // Write the header. - Header hdr; - memcpy(hdr.magic, "\0plocate", 8); - hdr.version = 0; - hdr.hashtable_size = ht_size; - hdr.extra_ht_slots = num_overflow_slots; - hdr.hash_table_offset_bytes = sizeof(hdr); // This member is just there for flexibility. - hdr.filename_index_offset_bytes = offset; - fwrite(&hdr, sizeof(hdr), 1, outfp); - // Write the hash table. + hdr.hash_table_offset_bytes = ftell(outfp); + hdr.hashtable_size = ht_size; fwrite(hashtable.get(), ht_size + num_overflow_slots + 1, sizeof(Trigram), outfp); // Write the actual posting lists. - size_t bytes_for_posting_lists = 0; for (unsigned i = 0; i < ht_size + num_overflow_slots + 1; ++i) { if (hashtable[i].num_docids == 0) { continue; } - const string &encoded = corpus.invindex[hashtable[i].trgm].encoded; + const string &encoded = corpus.get_pl_builder(hashtable[i].trgm).encoded; fwrite(encoded.data(), encoded.size(), 1, outfp); - bytes_for_posting_lists += encoded.size(); - } - - // Stick an empty block at the end as sentinel. - corpus.filename_blocks.push_back(""); - - // Write the offsets to the filenames. - size_t bytes_for_filename_index = 0, bytes_for_filenames = 0; - offset = hdr.filename_index_offset_bytes + corpus.filename_blocks.size() * sizeof(offset); - for (const string &filename : corpus.filename_blocks) { - fwrite(&offset, sizeof(offset), 1, outfp); - offset += filename.size(); - bytes_for_filename_index += sizeof(offset); - bytes_for_filenames += filename.size(); - } - - // Write the actual filenames. - for (const string &filename : corpus.filename_blocks) { - fwrite(filename.data(), filename.size(), 1, outfp); } + // Rewind, and write the updated header. + hdr.version = 1; + fseek(outfp, 0, SEEK_SET); + fwrite(&hdr, sizeof(hdr), 1, outfp); fclose(outfp); - size_t total_bytes __attribute__((unused)) = (bytes_for_hashtable + bytes_for_posting_lists + bytes_for_filename_index + bytes_for_filenames); + size_t total_bytes = (bytes_for_hashtable + bytes_for_posting_lists + bytes_for_filename_index + bytes_for_filenames); dprintf("Block size: %7d files\n", block_size); + dprintf("Dictionary: %'7.1f MB\n", dictionary.size() / 1048576.0); dprintf("Hash table: %'7.1f MB\n", bytes_for_hashtable / 1048576.0); dprintf("Posting lists: %'7.1f MB\n", bytes_for_posting_lists / 1048576.0); dprintf("Filename index: %'7.1f MB\n", bytes_for_filename_index / 1048576.0); @@ -457,8 +655,76 @@ void do_build(const char *infile, const char *outfile, int block_size) dprintf("\n"); } +void usage() +{ + printf( + "Usage: plocate-build MLOCATE_DB PLOCATE_DB\n" + "\n" + "Generate plocate index from mlocate.db, typically /var/lib/mlocate/mlocate.db.\n" + "Normally, the destination should be /var/lib/mlocate/plocate.db.\n" + "\n" + " -b, --block-size SIZE number of filenames to store in each block (default 32)\n" + " -p, --plaintext input is a plaintext file, not an mlocate database\n" + " --help print this help\n" + " --version print version information\n"); +} + +void version() +{ + printf("plocate-build %s\n", PLOCATE_VERSION); + printf("Copyright 2020 Steinar H. Gunderson\n"); + printf("License GPLv2+: GNU GPL version 2 or later .\n"); + printf("This is free software: you are free to change and redistribute it.\n"); + printf("There is NO WARRANTY, to the extent permitted by law.\n"); +} + int main(int argc, char **argv) { - do_build(argv[1], argv[2], 32); + static const struct option long_options[] = { + { "block-size", required_argument, 0, 'b' }, + { "plaintext", no_argument, 0, 'p' }, + { "help", no_argument, 0, 'h' }, + { "version", no_argument, 0, 'V' }, + { "debug", no_argument, 0, 'D' }, // Not documented. + { 0, 0, 0, 0 } + }; + + int block_size = 32; + bool plaintext = false; + + setlocale(LC_ALL, ""); + for (;;) { + int option_index = 0; + int c = getopt_long(argc, argv, "b:hpVD", long_options, &option_index); + if (c == -1) { + break; + } + switch (c) { + case 'b': + block_size = atoi(optarg); + break; + case 'p': + plaintext = true; + break; + case 'h': + usage(); + exit(0); + case 'V': + version(); + exit(0); + case 'D': + use_debug = true; + break; + default: + exit(1); + } + } + + if (argc - optind != 2) { + usage(); + exit(1); + } + + do_build(argv[optind], argv[optind + 1], block_size, plaintext); exit(EXIT_SUCCESS); }