X-Git-Url: https://git.sesse.net/?a=blobdiff_plain;f=plocate-build.cpp;h=028a1ee06c620d8274ae707ba1905041ac288a17;hb=fd6198891d6fd9642effc0843fef6f23b991af3e;hp=bde72b51fa3d6da02b59216e34d8dbfe695cca80;hpb=1f07faf71217a56ac538189c202ca118d7c993c0;p=plocate diff --git a/plocate-build.cpp b/plocate-build.cpp index bde72b5..028a1ee 100644 --- a/plocate-build.cpp +++ b/plocate-build.cpp @@ -1,51 +1,29 @@ +#include "database-builder.h" #include "db.h" -#include "vp4.h" +#include "dprintf.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 - -#define P4NENC_BOUND(n) ((n + 127) / 128 + (n + 32) * sizeof(uint32_t)) -#define dprintf(...) -//#define dprintf(...) fprintf(stderr, __VA_ARGS__); using namespace std; using namespace std::chrono; -string zstd_compress(const string &src, string *tempbuf); - -constexpr unsigned num_overflow_slots = 16; - -static inline uint32_t read_unigram(const string_view s, size_t idx) -{ - if (idx < s.size()) { - return (unsigned char)s[idx]; - } else { - return 0; - } -} - -static inline uint32_t read_trigram(const string_view s, size_t start) -{ - return read_unigram(s, start) | - (read_unigram(s, start + 1) << 8) | - (read_unigram(s, start + 2) << 16); -} +bool use_debug = false; enum { DBE_NORMAL = 0, /* A non-directory file */ @@ -69,401 +47,208 @@ struct db_directory { uint8_t pad[4]; }; -class PostingListBuilder { -public: - void add_docid(uint32_t docid); - void finish(); - - string encoded; - size_t num_docids = 0; - -private: - void write_header(uint32_t docid); - void append_block(); - - vector pending_docids; - - uint32_t last_block_end; -}; - -void PostingListBuilder::add_docid(uint32_t docid) +string read_cstr(FILE *fp) { - // 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; + string ret; + for (;;) { + int ch = getc(fp); + if (ch == -1) { + perror("getc"); + exit(1); } - } else { - if (docid == pending_docids.back()) { - return; + if (ch == 0) { + return ret; } - } - - pending_docids.push_back(docid); - if (pending_docids.size() == 128) { - append_block(); - pending_docids.clear(); - last_block_end = docid; - } - ++num_docids; -} - -void PostingListBuilder::finish() -{ - if (pending_docids.empty()) { - return; - } - - assert(!encoded.empty()); // write_header() should already have run. - - // 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); - 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); - 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); -} - -class Corpus { -public: - Corpus(FILE *outfp, size_t block_size) - : outfp(outfp), block_size(block_size) {} - void add_file(string filename); - void flush_block(); - - vector filename_blocks; - unordered_map invindex; - size_t num_files = 0, num_files_in_block = 0, num_blocks = 0; - -private: - FILE *outfp; - string current_block; - string tempbuf; - const size_t block_size; -}; - -void Corpus::add_file(string filename) -{ - ++num_files; - if (!current_block.empty()) { - current_block.push_back('\0'); - } - current_block += filename; - if (++num_files_in_block == block_size) { - flush_block(); + ret.push_back(ch); } } -void Corpus::flush_block() +void handle_directory(FILE *fp, DatabaseReceiver *receiver) { - if (current_block.empty()) { - return; - } - - uint32_t docid = num_blocks; - - // Create trigrams. - const char *ptr = current_block.c_str(); - while (ptr < current_block.c_str() + current_block.size()) { - string_view s(ptr); - 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); - } + db_directory dummy; + if (fread(&dummy, sizeof(dummy), 1, fp) != 1) { + if (feof(fp)) { + return; + } else { + perror("fread"); } - ptr += s.size() + 1; - } - - // Compress and add the filename block. - filename_blocks.push_back(ftell(outfp)); - string compressed = zstd_compress(current_block, &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) -{ - ptr += sizeof(db_directory); - - 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, unknown_dir_time); } 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, unknown_dir_time); } 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), unknown_dir_time); } - 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); + string path = read_cstr(fp); - const char *ptr = data + offset; - while (ptr < data + len) { - ptr = handle_directory(ptr, corpus); + if (fseek(fp, ntohl(hdr.conf_size), SEEK_CUR) != 0) { + perror("skip conf block"); + exit(1); } - munmap((void *)data, len); - close(fd); + while (!feof(fp)) { + handle_directory(fp, receiver); + } } -string zstd_compress(const string &src, string *tempbuf) +void do_build(const char *infile, const char *outfile, int block_size, bool plaintext) { - size_t max_size = ZSTD_compressBound(src.size()); - if (tempbuf->size() < max_size) { - tempbuf->resize(max_size); + FILE *infp = fopen(infile, "rb"); + if (infp == nullptr) { + perror(infile); + exit(1); } - size_t size = ZSTD_compress(&(*tempbuf)[0], max_size, src.data(), src.size(), /*level=*/6); - return string(tempbuf->data(), size); -} -bool is_prime(uint32_t x) -{ - if ((x % 2) == 0 || (x % 3) == 0) { - return false; + // Train the dictionary by sampling real blocks. + // The documentation for ZDICT_trainFromBuffer() claims that a reasonable + // dictionary size is ~100 kB, but 1 kB seems to actually compress better for us, + // and decompress just as fast. + DictionaryBuilder builder(/*blocks_to_keep=*/1000, block_size); + if (plaintext) { + read_plaintext(infp, &builder); + } else { + read_mlocate(infp, &builder); } - uint32_t limit = ceil(sqrt(x)); - for (uint32_t factor = 5; factor <= limit; ++factor) { - if ((x % factor) == 0) { - return false; - } + string dictionary = builder.train(1024); + + DatabaseBuilder db(outfile, /*owner=*/-1, block_size, dictionary, /*check_visibility=*/true); + DatabaseReceiver *corpus = db.start_corpus(/*store_dir_times=*/false); + if (plaintext) { + read_plaintext(infp, corpus); + } else { + read_mlocate(infp, corpus); } - return true; + fclose(infp); + + dprintf("Read %zu files from %s\n", corpus->num_files_seen(), infile); + db.finish_corpus(); } -uint32_t next_prime(uint32_t x) +void usage() { - if ((x % 2) == 0) { - ++x; - } - while (!is_prime(x)) { - x += 2; - } - return x; + 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"); } -unique_ptr create_hashtable(const Corpus &corpus, const vector &all_trigrams, uint32_t ht_size, uint32_t num_overflow_slots) +void version() { - 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) { - ht[i].trgm = uint32_t(-1); - ht[i].num_docids = 0; - ht[i].offset = 0; - } - for (uint32_t trgm : all_trigrams) { - // We don't know offset yet, so set it to zero. - Trigram to_insert{ trgm, uint32_t(corpus.invindex.find(trgm)->second.num_docids), 0 }; - - uint32_t bucket = hash_trigram(trgm, ht_size); - unsigned distance = 0; - while (ht[bucket].num_docids != 0) { - // Robin Hood hashing; reduces the longest distance by a lot. - unsigned other_distance = bucket - hash_trigram(ht[bucket].trgm, ht_size); - if (distance > other_distance) { - swap(to_insert, ht[bucket]); - distance = other_distance; - } - - ++bucket, ++distance; - if (distance > num_overflow_slots) { - return nullptr; - } - } - ht[bucket] = to_insert; - } - return ht; + printf("plocate-build %s\n", PACKAGE_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"); } -void do_build(const char *infile, const char *outfile, int block_size) +int main(int argc, char **argv) { - steady_clock::time_point start __attribute__((unused)) = steady_clock::now(); - - umask(0027); - FILE *outfp = fopen(outfile, "wb"); + 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 } + }; - // Write the header. - Header hdr; - memcpy(hdr.magic, "\0plocate", 8); - hdr.version = -1; // Mark as broken. - hdr.hashtable_size = 0; // Not known yet. - hdr.extra_ht_slots = num_overflow_slots; - hdr.hash_table_offset_bytes = -1; // We don't know these offsets yet. - hdr.filename_index_offset_bytes = -1; - fwrite(&hdr, sizeof(hdr), 1, outfp); + int block_size = 32; + bool plaintext = false; - Corpus corpus(outfp, block_size); - - read_mlocate(infile, &corpus); - if (false) { // To read a plain text file. - FILE *fp = fopen(infile, "r"); - while (!feof(fp)) { - char buf[1024]; - if (fgets(buf, 1024, fp) == nullptr || feof(fp)) { - break; - } - string s(buf); - if (s.back() == '\n') - s.pop_back(); - corpus.add_file(move(s)); - } - fclose(fp); - } - corpus.flush_block(); - dprintf("Read %zu files from %s\n", corpus.num_files, infile); - - // Stick an empty block at the end as sentinel. - corpus.filename_blocks.push_back(ftell(outfp)); - const size_t bytes_for_filenames = corpus.filename_blocks.back() - corpus.filename_blocks.front(); - - // Write the offsets to the filenames. - hdr.filename_index_offset_bytes = ftell(outfp); - const size_t bytes_for_filename_index = corpus.filename_blocks.size() * sizeof(uint64_t); - fwrite(corpus.filename_blocks.data(), corpus.filename_blocks.size(), sizeof(uint64_t), outfp); - corpus.filename_blocks.clear(); - corpus.filename_blocks.shrink_to_fit(); - - // Finish up encoding the posting lists. - size_t trigrams = 0, longest_posting_list = 0; - size_t bytes_for_posting_lists = 0; - for (auto &[trigram, pl_builder] : corpus.invindex) { - pl_builder.finish(); - longest_posting_list = max(longest_posting_list, pl_builder.num_docids); - trigrams += pl_builder.num_docids; - bytes_for_posting_lists += pl_builder.encoded.size(); - } - dprintf("%zu files, %zu different trigrams, %zu entries, avg len %.2f, longest %zu\n", - corpus.num_files, corpus.invindex.size(), trigrams, double(trigrams) / corpus.invindex.size(), longest_posting_list); - dprintf("%zu bytes used for posting lists (%.2f bits/entry)\n", bytes_for_posting_lists, 8 * bytes_for_posting_lists / double(trigrams)); - - dprintf("Building posting lists took %.1f ms.\n\n", 1e3 * duration(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). - vector all_trigrams; - for (auto &[trigram, pl_builder] : corpus.invindex) { - all_trigrams.push_back(trigram); - } - sort(all_trigrams.begin(), all_trigrams.end()); - - // Create the hash table. - unique_ptr hashtable; - uint32_t ht_size = next_prime(all_trigrams.size()); + setlocale(LC_ALL, ""); 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); - ht_size = next_prime(ht_size * 1.05); - } else { - dprintf("Created hash table of size %u.\n\n", ht_size); + int option_index = 0; + int c = getopt_long(argc, argv, "b:hpVD", long_options, &option_index); + if (c == -1) { break; } - } - - // Find the offsets for each posting list. - size_t bytes_for_hashtable = (ht_size + num_overflow_slots + 1) * sizeof(Trigram); - 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; + 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); } - - const string &encoded = corpus.invindex[hashtable[i].trgm].encoded; - offset += encoded.size(); } - // 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. - 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; - fwrite(encoded.data(), encoded.size(), 1, outfp); + if (argc - optind != 2) { + usage(); + exit(1); } - // Rewind, and write the updated header. - hdr.version = 0; - 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); - - dprintf("Block size: %7d files\n", block_size); - 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); - dprintf("Filenames: %'7.1f MB\n", bytes_for_filenames / 1048576.0); - dprintf("Total: %'7.1f MB\n", total_bytes / 1048576.0); - dprintf("\n"); -} - -int main(int argc, char **argv) -{ - do_build(argv[1], argv[2], 32); + do_build(argv[optind], argv[optind + 1], block_size, plaintext); exit(EXIT_SUCCESS); }