X-Git-Url: https://git.sesse.net/?a=blobdiff_plain;f=plocate-build.cpp;h=e1e2f644ef2ca891f93d48fdaab15e4f681bf418;hb=c427ecd63267946d66cf15808ed507d4f94c3566;hp=b64d06feffd28c20ec8b700529d6f946010095a5;hpb=19f8ecfe62f0affc1efa022076f5bf5d0c89d858;p=plocate diff --git a/plocate-build.cpp b/plocate-build.cpp index b64d06f..e1e2f64 100644 --- a/plocate-build.cpp +++ b/plocate-build.cpp @@ -1,28 +1,38 @@ +#include "db.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 -#include "vp4.h" - -#define P4NENC_BOUND(n) ((n+127)/128+(n+32)*sizeof(uint32_t)) +#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; - -static inline uint32_t read_unigram(const string &s, size_t idx) + +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]; @@ -31,23 +41,21 @@ static inline uint32_t read_unigram(const string &s, size_t idx) } } -static inline uint32_t read_trigram(const string &s, size_t start) +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); } -enum -{ - DBE_NORMAL = 0, /* A non-directory file */ - DBE_DIRECTORY = 1, /* A directory */ - DBE_END = 2 /* End of directory contents; contains no name */ +enum { + DBE_NORMAL = 0, /* A non-directory file */ + DBE_DIRECTORY = 1, /* A directory */ + DBE_END = 2 /* End of directory contents; contains no name */ }; // From mlocate. -struct db_header -{ +struct db_header { uint8_t magic[8]; uint32_t conf_size; uint8_t version; @@ -56,78 +64,324 @@ struct db_header }; // From mlocate. -struct db_directory -{ +struct db_directory { uint64_t time_sec; uint32_t time_nsec; uint8_t pad[4]; }; -const char *handle_directory(const char *ptr, vector *files) +class PostingListBuilder { +public: + inline 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_deltas; + + uint32_t last_block_end, last_docid = -1; +}; + +void PostingListBuilder::add_docid(uint32_t docid) +{ + // Deduplicate against the last inserted value, if any. + if (docid == last_docid) { + return; + } + + 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_deltas.clear(); + last_block_end = docid; + } + ++num_docids; +} + +void PostingListBuilder::finish() +{ + if (pending_deltas.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 = 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_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)]; + unsigned char *end = write_baseval(docid, buf); + encoded.append(reinterpret_cast(buf), end - buf); +} + +class Corpus { +public: + Corpus(FILE *outfp, size_t block_size) + : invindex(new PostingListBuilder *[NUM_TRIGRAMS]), outfp(outfp), block_size(block_size) + { + fill(invindex.get(), invindex.get() + NUM_TRIGRAMS, nullptr); + } + ~Corpus() + { + for (unsigned i = 0; i < NUM_TRIGRAMS; ++i) { + delete invindex[i]; + } + } + + void add_file(string filename); + void flush_block(); + + 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]; + } + +private: + unique_ptr invindex; + 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(); + } +} + +void Corpus::flush_block() +{ + 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); + get_pl_builder(trgm).add_docid(docid); + } + } + 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; +} + +string read_cstr(FILE *fp) { - ptr += sizeof(db_directory); + string ret; + for (;;) { + int ch = getc(fp); + if (ch == -1) { + perror("getc"); + exit(1); + } + if (ch == 0) { + return ret; + } + ret.push_back(ch); + } +} - string dir_path = ptr; - ptr += dir_path.size() + 1; +void handle_directory(FILE *fp, Corpus *corpus) +{ + db_directory dummy; + if (fread(&dummy, sizeof(dummy), 1, fp) != 1) { + if (feof(fp)) { + return; + } else { + perror("fread"); + } + } + + string dir_path = read_cstr(fp); if (dir_path == "/") { dir_path = ""; } - - for ( ;; ) { - uint8_t type = *ptr++; + + for (;;) { + int type = getc(fp); if (type == DBE_NORMAL) { - string filename = ptr; - files->push_back(dir_path + "/" + filename); - ptr += filename.size() + 1; + string filename = read_cstr(fp); + corpus->add_file(dir_path + "/" + filename); } else if (type == DBE_DIRECTORY) { - string dirname = ptr; - files->push_back(dir_path + "/" + dirname); - ptr += dirname.size() + 1; + string dirname = read_cstr(fp); + corpus->add_file(dir_path + "/" + dirname); } else { - return ptr; + return; // Probably end. } } } -void read_mlocate(const char *filename, vector *files) +void read_mlocate(const char *filename, Corpus *corpus) { - int fd = open(filename, O_RDONLY); - if (fd == -1) { + FILE *fp = fopen(filename, "rb"); + if (fp == nullptr) { perror(filename); exit(1); } - off_t len = lseek(fd, 0, SEEK_END); - if (len == -1) { - perror("lseek"); + + db_header hdr; + if (fread(&hdr, sizeof(hdr), 1, fp) != 1) { + perror("short read"); exit(1); } - const char *data = (char *)mmap(nullptr, len, PROT_READ, MAP_SHARED, fd, /*offset=*/0); - if (data == MAP_FAILED) { - perror("mmap"); - exit(1); + + // TODO: Care about the base path. + string path = read_cstr(fp); + while (!feof(fp)) { + handle_directory(fp, corpus); + } + fclose(fp); +} + +string zstd_compress(const string &src, string *tempbuf) +{ + 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); + return string(tempbuf->data(), size); +} - const db_header *hdr = (const db_header *)data; +bool is_prime(uint32_t x) +{ + if ((x % 2) == 0 || (x % 3) == 0) { + return false; + } + uint32_t limit = ceil(sqrt(x)); + for (uint32_t factor = 5; factor <= limit; ++factor) { + if ((x % factor) == 0) { + return false; + } + } + return true; +} - // 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); +uint32_t next_prime(uint32_t x) +{ + if ((x % 2) == 0) { + ++x; + } + while (!is_prime(x)) { + x += 2; + } + return x; +} - const char *ptr = data + offset; - while (ptr < data + len) { - ptr = handle_directory(ptr, files); +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) { + 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.get_pl_builder(trgm).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; + } - munmap((void *)data, len); - close(fd); + ++bucket, ++distance; + if (distance > num_overflow_slots) { + return nullptr; + } + } + ht[bucket] = to_insert; + } + return ht; } -void do_build(const char *infile, const char *outfile) +void do_build(const char *infile, const char *outfile, int block_size) { - //steady_clock::time_point start = steady_clock::now(); + steady_clock::time_point start __attribute__((unused)) = steady_clock::now(); + + umask(0027); + FILE *outfp = fopen(outfile, "wb"); + + // 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.num_docids = 0; + 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); + + Corpus corpus(outfp, block_size); - vector files; - read_mlocate(infile, &files); + read_mlocate(infile, &corpus); if (false) { // To read a plain text file. FILE *fp = fopen(infile, "r"); while (!feof(fp)) { @@ -136,104 +390,104 @@ void do_build(const char *infile, const char *outfile) break; } string s(buf); - if (s.back() == '\n') s.pop_back(); - files.push_back(move(s)); + if (s.back() == '\n') + s.pop_back(); + corpus.add_file(move(s)); } fclose(fp); } - dprintf("Read %zu files from %s\n", files.size(), infile); - - unordered_map pl; - size_t trigrams = 0, longest_posting_list = 0; - unordered_map> invindex; - for (size_t i = 0; i < files.size(); ++i) { - const string &s = files[i]; - if (s.size() >= 3) { - for (size_t j = 0; j < s.size() - 2; ++j) { - uint32_t trgm = read_trigram(s, j); - invindex[trgm].push_back(i); - } - } - } - string buf; - size_t bytes_used = 0; - for (auto &[trigram, docids] : invindex) { - auto last = unique(docids.begin(), docids.end()); - docids.erase(last, docids.end()); - longest_posting_list = max(longest_posting_list, docids.size()); - trigrams += docids.size(); + corpus.flush_block(); + dprintf("Read %zu files from %s\n", corpus.num_files, infile); + hdr.num_docids = corpus.filename_blocks.size(); + + // 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(); - size_t bytes_needed = P4NENC_BOUND(docids.size()); - if (buf.size() < bytes_needed) buf.resize(bytes_needed); - size_t bytes = p4nd1enc128v32(&docids[0], docids.size(), reinterpret_cast(&buf[0])); - pl[trigram] = string(buf.data(), bytes); - bytes_used += bytes; + // Finish up encoding the posting lists. + size_t trigrams = 0, longest_posting_list = 0; + size_t bytes_for_posting_lists = 0; + for (unsigned trgm = 0; trgm < NUM_TRIGRAMS; ++trgm) { + if (!corpus.seen_trigram(trgm)) + continue; + PostingListBuilder &pl_builder = corpus.get_pl_builder(trgm); + 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", - files.size(), invindex.size(), trigrams, double(trigrams) / invindex.size(), longest_posting_list); + 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("%zu bytes used for posting lists (%.2f bits/entry)\n", bytes_used, 8 * bytes_used / double(trigrams)); - //steady_clock::time_point end = steady_clock::now(); - dprintf("Building posting lists took %.1f ms.\n\n", 1e3 * duration(end - start).count()); + dprintf("Building posting lists took %.1f ms.\n\n", 1e3 * duration(steady_clock::now() - start).count()); + // Find the used trigrams. vector all_trigrams; - for (auto &[trigram, docids] : 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()); - - umask(0027); - FILE *outfp = fopen(outfile, "wb"); - // Write the header. - uint64_t num_trigrams = invindex.size(); // 64 to get alignment. - fwrite(&num_trigrams, sizeof(num_trigrams), 1, outfp); - - // Find out where the posting lists will end. - uint64_t offset = sizeof(uint64_t) * 2 + 16 * all_trigrams.size(); - uint64_t filename_index_offset = offset; - for (uint32_t trgm : all_trigrams) { - filename_index_offset += pl[trgm].size(); + // Create the hash table. + unique_ptr hashtable; + uint32_t ht_size = next_prime(all_trigrams.size()); + 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); + break; + } } - fwrite(&filename_index_offset, sizeof(filename_index_offset), 1, outfp); - size_t bytes_for_trigrams = 0, bytes_for_posting_lists = 0, bytes_for_filename_index = 0, bytes_for_filenames = 0; + // 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; + } - for (uint32_t trgm : all_trigrams) { - // 16 bytes: trgm (4), number of docids (4), byte offset (8) - fwrite(&trgm, sizeof(trgm), 1, outfp); // One byte wasted, but OK. - uint32_t num_docids = invindex[trgm].size(); - fwrite(&num_docids, sizeof(num_docids), 1, outfp); - fwrite(&offset, sizeof(offset), 1, outfp); - offset += pl[trgm].size(); - bytes_for_trigrams += 16; + const string &encoded = corpus.get_pl_builder(hashtable[i].trgm).encoded; + offset += encoded.size(); } - // Write the actual posting lists. - for (uint32_t trgm : all_trigrams) { - fwrite(pl[trgm].data(), pl[trgm].size(), 1, outfp); - bytes_for_posting_lists += pl[trgm].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 offsets to the filenames. - offset = filename_index_offset + files.size() * sizeof(offset); - for (const string &filename : files) { - fwrite(&offset, sizeof(offset), 1, outfp); - offset += filename.size() + 1; - bytes_for_filename_index += sizeof(offset); - bytes_for_filenames += filename.size() + 1; - } - - // Write the actual filenames. - for (const string &filename : files) { - fwrite(filename.c_str(), filename.size() + 1, 1, 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.get_pl_builder(hashtable[i].trgm).encoded; + fwrite(encoded.data(), encoded.size(), 1, outfp); } + // 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 = (bytes_for_trigrams + bytes_for_posting_lists + bytes_for_filename_index + bytes_for_filenames); + size_t total_bytes __attribute__((unused)) = (bytes_for_hashtable + bytes_for_posting_lists + bytes_for_filename_index + bytes_for_filenames); - dprintf("Trigrams: %'7.1f MB\n", bytes_for_trigrams / 1048576.0); + 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); @@ -241,8 +495,66 @@ void do_build(const char *infile, const char *outfile) 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" + " --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]); + static const struct option long_options[] = { + { "block-size", required_argument, 0, 'b' }, + { "help", no_argument, 0, 'h' }, + { "version", no_argument, 0, 'V' }, + { 0, 0, 0, 0 } + }; + + int block_size = 32; + + setlocale(LC_ALL, ""); + for (;;) { + int option_index = 0; + int c = getopt_long(argc, argv, "b:hV", long_options, &option_index); + if (c == -1) { + break; + } + switch (c) { + case 'b': + block_size = atoi(optarg); + break; + case 'h': + usage(); + exit(0); + case 'v': + version(); + exit(0); + default: + exit(1); + } + } + + if (argc - optind != 2) { + usage(); + exit(1); + } + + do_build(argv[optind], argv[optind + 1], block_size); exit(EXIT_SUCCESS); }