#pragma once #include "coding/bit_streams.hpp" #include "coding/byte_stream.hpp" #include "coding/huffman.hpp" #include "coding/reader.hpp" #include "coding/varint.hpp" #include "coding/writer.hpp" #include "base/buffer_vector.hpp" #include "base/checked_cast.hpp" #include "base/scope_guard.hpp" #include "base/string_utils.hpp" #include #include #include // Trie format: // -- Serialized Huffman encoding. // -- Topology of the trie built on Huffman-encoded input strings [2 bits per node, level-order representation]. // -- List of pairs (node id, offset). One pair per final node (i.e. a node where a string ends). // The lists of node ids and offsets are both non-decreasing and are delta-encoded with // varuints. // -- Values of final nodes in level-order. The values for final node |id| start at offset |offset| // if there is a pair (id, offset) in the list above. namespace trie { // Node is a temporary struct that is used to store the trie in memory // before it is converted to a succinct representation and put to disk. // It is rather verbose but hopefully is small enough to fit in memory. template struct Node { // The left child is reached by 0, the right by 1. Node *l, *r; // A node is final if a key string ends in it. // In particular, all non-final nodes' valueLists are empty // and it is expected that a final node's valueList is non-empty. bool m_isFinal; // m_valueLists stores all the additional information that is related // to the key string which leads to this node. TValueList m_valueList; Node() : l(nullptr), r(nullptr), m_isFinal(false) {} }; template TNode * AddToTrie(TNode * root, TReader & bitEncoding, uint32_t numBits) { ReaderSource src(bitEncoding); // String size. ReadVarUint>(src); BitReader> bitReader(src); auto cur = root; for (uint32_t i = 0; i < numBits; ++i) { auto nxt = bitReader.Read(1) == 0 ? &cur->l : &cur->r; if (!*nxt) *nxt = new TNode(); cur = *nxt; } return cur; } template void DeleteTrie(TNode * root) { if (!root) return; DeleteTrie(root->l); DeleteTrie(root->r); delete root; } template void WriteInLevelOrder(TNode * root, std::vector & levelOrder) { ASSERT(root, ()); levelOrder.push_back(root); size_t i = 0; while (i < levelOrder.size()) { TNode * cur = levelOrder[i++]; if (cur->l) levelOrder.push_back(cur->l); if (cur->r) levelOrder.push_back(cur->r); } } template void BuildSuccinctTrie(TWriter & writer, TIter const beg, TIter const end) { using TrieChar = uint32_t; using TTrieString = buffer_vector; using TNode = Node; using TEntry = typename TIter::value_type; TNode * root = new TNode(); SCOPE_GUARD(cleanup, std::bind(&DeleteTrie, root)); TTrieString prevKey; TEntry prevEntry; std::vector entries; std::vector entryStrings; for (TIter it = beg; it != end; ++it) { TEntry entry = *it; if (it != beg && entry == prevEntry) continue; TrieChar const * const keyData = entry.GetKeyData(); TTrieString key(keyData, keyData + entry.GetKeySize()); using namespace std::rel_ops; // ">=" for keys. CHECK_GREATER_OR_EQUAL(key, prevKey, (key, prevKey)); entries.push_back(entry); entryStrings.push_back(strings::UniString(keyData, keyData + entry.GetKeySize())); prevKey.swap(key); prevEntry.Swap(entry); } coding::HuffmanCoder huffman; huffman.Init(entryStrings); huffman.WriteEncoding(writer); for (size_t i = 0; i < entries.size(); ++i) { TEntry const & entry = entries[i]; auto const & key = entryStrings[i]; std::vector buf; MemWriter> memWriter(buf); uint32_t numBits = huffman.EncodeAndWrite(memWriter, key); MemReader bitEncoding(&buf[0], buf.size()); TNode * cur = AddToTrie(root, bitEncoding, numBits); cur->m_isFinal = true; cur->m_valueList.Append(entry.GetValue()); } std::vector levelOrder; WriteInLevelOrder(root, levelOrder); { // Trie topology. BitWriter bitWriter(writer); WriteVarUint(writer, levelOrder.size()); for (size_t i = 0; i < levelOrder.size(); ++i) { auto const & cur = levelOrder[i]; bitWriter.Write(cur->l ? 1 : 0, 1 /* numBits */); bitWriter.Write(cur->r ? 1 : 0, 1 /* numBits */); } } std::vector finalNodeIds; std::vector offsetTable; std::vector valueBuf; MemWriter> valueWriter(valueBuf); for (size_t i = 0; i < levelOrder.size(); ++i) { TNode const * node = levelOrder[i]; if (!node->m_isFinal) continue; finalNodeIds.push_back(static_cast(i)); offsetTable.push_back(base::asserted_cast(valueWriter.Pos())); node->m_valueList.Dump(valueWriter); } WriteVarUint(writer, finalNodeIds.size()); for (size_t i = 0; i < finalNodeIds.size(); ++i) { if (i == 0) { WriteVarUint(writer, finalNodeIds[i]); WriteVarUint(writer, offsetTable[i]); } else { WriteVarUint(writer, finalNodeIds[i] - finalNodeIds[i - 1]); WriteVarUint(writer, offsetTable[i] - offsetTable[i - 1]); } } writer.Write(valueBuf.data(), valueBuf.size()); // todo(@pimenov): // 1. Investigate the possibility of path compression (short edges + lcp table). // 2. It is highly probable that valueList will be only a list of feature ids in our // future implementations of the search. // We can then dispose of offsetTable and store finalNodeIds as another bit vector. // Alternatively (and probably better), we can append a special symbol to all those // strings that have non-empty value lists. We can then get the leaf number // of a final node by doing rank0. } } // namespace trie