A string dictionary is essentially a bidirectional mapping between strings and identifiers. Those identifiers are usually consecutive integer numbers that can be interpreted as the position of the string in the dictionary. By using string dictionaries, applications no longer need to store multiple references to large collections of strings. Replacing those strings, which can be long and have different lengths, with simple integer values simplifies the management of this kind of data.
Many applications need to make use of large string collections. The most immediate ones are text collections and full-text indexes, but several other applications, not specifically related to text processing, still require an efficient representation of string collections. Some relevant examples include those handling Web graphs, ontologies and RDF datasets, or biological sequences. Web graphs, for example, store a graph representing hyperlinks between Web pages, so the node identifiers are URLs. Most representations transform those strings into numeric identifiers (ids), and then store a graph referring to those ids. Compact Web graph representations can store the graphs within just a few bits per edge (webgraph; ktree). Since the average node arities are typically 15–30, storing the URL of the node becomes in practice a large fraction of the overall space. In RDF datasets, information is stored as a labeled graph where nodes are either blank, URIs, or literal values; labels are also URIs. The usual approach to store RDF data is also to use string dictionaries to obtain numeric identifiers for each element, in order to save space and speed up queries (rdfx). The classical technique of storing a string dictionary is extended in some proposals by keeping separate dictionaries for URIs and literal values (rdfdict).
In this paper we consider the problem of efficiently storing large static string dictionaries in compressed space in main memory, providing efficient support for two basic operations: lookup(s) receives a string and returns the string identifier, an integer value representing its position in the dictionary; access(i) receives a string identifier and returns the string in the dictionary corresponding to that identifier.
We focus on two types of dictionaries that are widely used in practical applications: URL dictionaries used in Web graphs, which are of special interest for many Web analysis and retrieval tasks; and URIs and literals dictionaries for RDF collections, which are a key component of the Web of Data and the Linked Data initiative and have experienced a sharp growth in recent years.
Our techniques achieve compression by exploiting repetitiveness among the strings, so they are especially well suited to URL and URI datasets where individual strings are relatively long and very similar to other strings close to them in lexicographical order. In particular, we build on Front-coding, which exploits long common prefixes between consecutive strings, and design a hierarchical version that enables binary searches without using any sampling. We enhance this binary search with techniques inherited from suffix array construction algorithms, which boost the computation of longest common prefixes along lexicographic ranges of strings. These main ideas are then composed with other compression techniques.
Experimental results on real-world datasets show that our data structures achieve better compression than the state-of-the-art alternatives, and we are much faster than the few alternatives that can reach similar compression. Even if faster solutions exist, our techniques are still competitive in query times and significantly smaller than them.
The remaining of this paper is organized as follows: in Section 2 we introduce some concepts and refer to previous work in string dictionary compression. Section 3 presents our proposal, describing the structure and query algorithms and explaining the main variants implemented. Section 4 contains the experimental evaluation of our structures. Finally, Section 5 summarizes the results and shows some lines for future work.
2. Related work
2.1. Previous concepts and basic compression techniques
In this section we introduce some preliminary concepts, presenting existing data structures and compression techniques that are used in the paper.
2.1.1. Bit sequences
A bit sequence or bitmap is a sequence of bits. Bit sequences are widely used in many compact data structures. Usually, bit sequences provide the following three basic operations: obtains the value of the bit at position , counts the number of bits set to up to position , and obtains the position in of the -th bit set to . All the operations can be answered in constant time using bits (cds, Ch. 4). Additionally, compressed bit sequence representations have been proposed to further reduce the space requirements (rrr). In this paper we use an implementation of the SDArray compressed bitmap (sdarray) provided by the Compact Data Structures Library libcds111https://github.com/fclaude/libcds. This solution can achieve compression when the sequence is sparse and still supports queries in constant time.
2.1.2. Integer compression techniques
In this paper we use Variable-byte (Vbyte) encoding (vbyte), a simple integer compression technique that essentially splits an integer in 7-bit chunks, and stores them in consecutive bytes, using the most significant bit of each byte to mark whether the number has more chunks or not. It is simple to implement and fast to decode.
A technique of special relevance is Directly Addressable Codes (DACs) (dacs). This technique aims at storing a sequence of integers in compressed space while providing direct access to any position. Given the Vbyte encoding of the integers, DACs store the first chunk of each integer consecutively, and use a bitmap to mark the entries with a second chunk. The process is repeated with the second chunks and its corresponding bitmap , and so on. DACs support decompressing entries accessing the first chunk directly and using operations on the s to locate the corresponding position of the next chunk.
DACs can work with Vbyte encoding but they are actually a general chunk-reordering technique. In this paper we make use of a variant that is designed to store a collection of variable-length integer sequences, instead of a sequence of integers. In this variant, that we call DAC-VLS, integers are not divided in chunks; instead, the first integer in each sequence is stored in the first level, and a bitmap is used to mark whether the current sequence has more elements. This technique does not reduce the space of the original integers, but provides direct access to any sequence in the collection.
2.1.3. String compression: Front-coding and Re-Pair
Front-coding is a folklore compression technique that is used as a building block in many well-known compression algorithms. Front-coding compresses a string relative to another by computing their longest common prefix (lcp) and removing the first characters from the encoded string. Hence, Front-coding represents as a tuple containing the and the substring after it . Despite its simplicity, it is a very useful technique for many applications, providing a simple way to compress collections of similar strings. URLs, for instance, tend to have relatively long common prefixes, so Front-coding compression is very effective on them, even if the string portions remaining after Front-coding, or string tails, are still relatively long.
Re-Pair (repair) is a grammar compression technique that achieves good compression in practice for different kinds of texts. Given a text , Re-Pair finds the most repeated pair of consecutive symbols and replaces each occurrence of by a new symbol , adding to the grammar a new rule . The process is repeated until no repeated pairs appear in the text. The output of Re-Pair is a list of rules and the resulting reduced text , represented as a sequence of integers in the range , where is the number of different symbols in the original text.
2.2. String dictionary compression
Simple techniques for storing collections of strings have been used in many applications. Hash tables and tries (knuth1998) are just some examples of classical representations that can be used in main memory for small dictionaries.
As the dictionary size increases, those classical data structures no longer fit in main memory, so a compressed representation has to be used or the dictionary must be stored in secondary memory. A simple approach to reduce space is to compress individual strings using general or domain-specific compression techniques, before adding them to the dictionary structure. Modern techniques for dictionary compression are based on specific compact data structures usually combined with custom compression techniques applied to the strings. Several theoretical solutions have been proposed for static dictionaries (bille), and solutions also exist for the dynamic dictionary problem (dpct; kanda1; kanda2). In this section we will focus on practical solutions for a static dictionary, outlining the most relevant existing implementations.
Martinez-Prieto et al. (Martinez-Prieto:2016) have proposed a collection of compressed string dictionary representations that provide a choice for different space/time tradeoffs. In their survey, they show advantages against proposals based on compressed tries and similar compression techniques. Their representations are based on well-known compression techniques that are combined to build space-efficient versions of data structures like tries and hash tables. The most relevant proposal in this survey is a collection of differentially encoded dictionaries. The authors sort the strings and split them into fixed-size buckets. Then, they store the first string of each bucket, or bucket header, in full, and the remaining strings of the bucket are compressed relative to the previous one using Front-coding. To answer lookup queries, a binary search in the bucket headers is used to locate the bucket containing the string, and a sequential search in the bucket is performed; access queries just traverse sequentially the bucket containing the query identifier. The authors propose several variants of this idea in the original paper that combine the previous idea with additional compression techniques like Huffman (huffman), Hu-Tucker (hutucker) or Re-Pair applied to the strings in each bucket or to the bucket headers to reduce the overall space usage.
In the previous work several other alternatives are proposed that share similarities with our proposal. Binary-searchable Re-Pair (RPDAC) compresses the strings with Re-Pair and uses DAC-VLS to provide direct access to each one, supporting lookup queries through binary search. An improvement on the same idea uses a hash table to provide direct access to the location of a string, instead of resorting to binary search, improving lookup queries significantly at the cost of additional space.
Grossi and Ottaviano propose a structure based on path decomposed tries (PDT) (pdtries). The authors create a path decomposition of the trie representing the dictionary strings, and build a compact representation of the tree generated by the path decomposition. They explore different techniques for the representation of the trie (lexicographical and centroid-based path decomposition). They also propose compressed variants in which the path labels are compressed using Re-Pair. Their solution has shown good results in different kinds of string dictionaries. Their compressed tries are competitive in space with previous techniques, but more importantly provide fast and very consistent query times.
Arz and Fischer (lzstringdict) have recently proposed a solution based on Lempel-Ziv-78 (LZ-78) compression on top of PDT. This technique has been shown to slightly improve the compression of PDT in some datasets, but improvement is small in most cases and the LZ-78-compressed structures have much higher query times, especially in lookup queries.
3. Our proposal
3.1. Data structure and algorithms
We propose a family of compression techniques for string collections that aim at providing good compression with efficient query times. Our techniques follow some of the ideas of differential compression described in Section 2.2 and aim at improving their weak points.
To build our representation, the strings are sorted in lexicographic order. This order is frequently used in most string dictionary representations, so that entries that are close to each other should also be similar to each other. For convenience, we also add two marker strings at the beginning and at the end of the collection: the former is the empty string, and the latter is a single-character string lexicographically larger than any string in the original collection.
Our goal is to use Front-coding to reduce the common prefix of common entries. However, instead of compressing each string relative to the previous one, we use a different scheme for comparisons that constitutes the basis of our proposal. Our technique is based on a binary decomposition of the list of strings, following similar ideas to the binary search algorithms over suffix arrays proposed by Manber and Myers (suffixArray).
Assume we have a collection of strings, including our initial and last string, and let be the string at position in the collection. Our structure is built as follows:
We initialize two markers and , set to the limits of the collection.
We select the middle point and compute and , the longest common prefixes between the string at position and the strings at both limits of the interval.
Let be the maximum between and . is compressed using Front-coding, by removing the initial bytes. In practice, Front-coding is applied relative to the most similar of the entries at each limit of the interval. We will refer to these as the “parents” of a given entry.
We recurse on both halves of the collection ( and ), repeating the previous steps to compare the middle element with the limits of the interval and apply Front-coding accordingly.
After this procedure, our conceptual representation consists of two integer sequences and , and the remaining of each string after Front-coding is applied to them. Let us call this . In practice we use different techniques to store the strings, but for simplicity we will write to refer to the string stored at position .
Figure 1 shows an example of our dictionary structure for a small set of strings. We use $ to denote a string terminator. Our marker strings are denoted as $ and ~$ respectively. The original strings at each position are displayed below the arrays, with the prefix that would be removed after Front-coding compression grayed out. Arrows identify the position of the left and right “parent” of each entry. For instance, is compared with positions 0 and 16 (our marker strings), and it is stored in full. (climate $) is compared with () and (), and after Front-coding is applied it becomes imate$, removing the longest common prefix. Note that the marker strings we use will never share a common prefix with any string in the collection, so both marker strings and the string in the middle position will always have and values of 0 and will be stored in full. The final representation needs to store the and arrays and the collection of string tails.
Our construction technique is expected to yield worst compression results than the usual Front-coding approach that would be applied sequentially to the collection of strings. We will describe later our implementation strategies to improve the space utilization. However, as we will see next, our binary decomposition allows us to provide an efficient method to answer queries without resorting to sampling or partitioning of the collection. Therefore, we avoid the need for bucket headers that arises in some of the solutions described in Section 2.
Next we outline the algorithms for lookup and access operations. In both cases we perform a binary-search-like traversal of the collection.
3.1.1. Lookup operation
To obtain the identifier of a string in the dictionary (lookup), a trivial algorithm would involve a binary search, checking the midpoint at each step and comparing the resulting string with the target. However, our scheme is able to improve the performance of lookup operations by avoiding some string comparisons.
The pseudo-code used for lookup searches is described in Algorithm 1. Let be the search string. The values and are the limits of our interval, initially and . The variables and store the longest common prefix of the left- and right-hand strings in the dictionary with the search string and are initially set to 0. Hence, a lookup() is translated into .
At any step of search, we first compare and . We will focus on the case (i.e., the string at is more similar to than the string at ) covered in lines 3-18 of the algorithm 222In practice, when = we have to check the values of and to choose the branch for traversal. Algorithm 1 shows the actual comparison., since the other case is symmetric. We obtain the midpoint and the value of and then compare it with :
If , entry has a longer prefix in common with than with . Hence, the result cannot be to the left of . We recurse on the right half of the range () without comparing strings.
If , we are on the symmetric case: entry is more similar to the pattern than to entry . We recurse on the left half of the interval, and we set the new lower bound , since our current string must have characters in common with the search string.
If , we need to compare entry with . Our comparison method in Algorithm 1 gives us the two relevant pieces of information: the comparison value , and the offset of the last equal character. If both strings are equal, we return immediately. Otherwise, we recurse on the appropriate half, setting the value of or to .
Following the example in Figure 1, assume we are searching for string clam. First we compare with clamp (due to our markers, in the first iteration a comparison is always performed). The query string is smaller than , and they share the first 4 characters. Therefore, we recurse on the left half , setting . In the next step (), and , so we do not need to compare strings: we just recurse on the right-side interval , and we set , since , meaning that it shares also a prefix of length 1 with . In the next step (), again , and , so we recurse on the interval . At the last step, , so we compare strings to find that both strings are equal.
3.1.2. Access operation
The second main operation, access(i) , is the opposite of the previous one, retrieving the string for a given identifier. It follows a bottom-up approach, starting at the position and traversing up to the parent position until we have recovered the full string. The procedure is described in Algorithm 2. The string is decoded from the end, prepending new characters at each new step until we reach the beginning of the string. Given an identifier , we read and and compute their maximum as . Then, we can extract all the characters from , that will correspond to the result string from position onwards. Since we have already decoded the result from position , in the next iterations we set a to mark that we only need to extract characters up to that position.
After extracting the required characters, we move to the appropriate parent333In practice, the parent positions are not computed bottom-up in our implementations. Instead, the list of search positions is obtained in a top-bottom fashion before the access algorithm starts. These details are omitted for simplicity in Algorithm 2., the one corresponding to the maximum lcp, and repeat the procedure. Whenever , we prepend the first characters of the current to the result. When we reach the result has been decoded and the procedure ends.
Note that in the worst case we may have to traverse up until we reach one of the positions that are always stored in full: , or , hence running string comparisons. However, in many instances we can reach earlier in the traversal. Additionally, in iterations where comparisons are skipped, therefore we do not even need to access the text. This will be relevant in some implementation variants that apply compression to the string tails, since in those solutions string comparisons are relatively expensive.
Following again the example in Figure 1, assume we want to obtain the string for identifier 9 (clean). At the first iteration, the maximum common prefix is . This means that is stored from position 4, so we can recover the characters from position 4 until the end of string (____n). We set for future iterations, and since the value was higher we move to the right-side parent, i.e. to position 10. Now , so and we can extract the first two characters of to fill positions 2-3 of the result string, getting __ean. In this step we could move to either side, assume we simply move left by convention. We reach position 8, and we get , so we copy the first two characters of to fill the remaining positions of our result and then return.
3.2. Implementation variants
Our conceptual representation stores two integer sequences and and a set of string tails . Several alternatives exist for the representation of both structures, hence originating a family of structures that provide a space/time tradeoff. In this section we introduce implementation details for the different variants of our proposal:
IBiS is the simplest proposal. In this approach we store and as sequences of fixed-length integers. This solution is simple and efficient, but in datasets where the maximum lcp value is high it is space-inefficient. The string tails are concatenated in a single sequence . A bitmap is added to indicate the position in where each string begins marking with 1 those positions and setting the remaining positions to 0. We store the bit array using an SDArray compressed bitmap representation, to provide select support. In this representation, is obtained by selecting the position of the -th 1 in , and extracting .
IBiS differs from the previous one on the representation of the strings. All the string tails are again concatenated in a single sequence , including the end-of-string markers, or string terminators. After this, a variant of Re-Pair compression is applied to the sequence, generating a grammar-compressed sequence where symbols never overlap two dictionary strings. This transforms the original byte string into a grammar and a sequence of integers. The sequence of integers is encoded using Vbyte. We also use a bitmap that marks with 1 the first byte of each dictionary string. can be obtained by extracting the sequence in the same way as before, and then decoding the corresponding Re-Pair sequence.
IBiS is similar to IBiS but it uses DACs to store the sequences and . This is expected to achieve much better space in many real-world collections, and especially in collections with long strings where the maximum lcp is much higher than the average.
IBiS is again similar to IBiS but uses the variant of DACs designed for variable-length integer sequences (DAC-VLS) to store the Re-Pair-compressed strings (i.e. the sequence of integers generated by Re-Pair), instead of compressing individual integers with Vbyte. Since the DAC-VLS structure provides direct access to any string, the bitmap is not necessary, and a string is just decoded by extracting symbols from the DAC-VLS structure and decompressing them using the Re-Pair grammar. Note that this combination of Re-Pair and DAC-VLS is the same underlying idea of RPDAC, described in Section 2.
IBiS combines the two previous ones: and are stored using DACs, and stored using DAC-VLS.
3.2.1. End-of-string symbols
All our implementations use a bitmap or a DAC-VLS structure to provide direct access to any string tail, so, unlike alternatives based on sequential search, our representation does not need to physically store end of string markers. The string terminators are used as markers when applying Re-Pair compression, so that no Re-Pair symbol overlaps two dictionary strings. However, after compression, we can remove these string terminators to save a byte per string in . Nevertheless, we still tested, as well, the version with string terminators since having them we can decode until we reach the terminator instead of performing a second operation on . Even though operations are constant-time, they are relatively costly and avoiding them we can speed up string decoding.
Notice that, when a string is compressed relative to a larger string in lexicographical order, a zero-length tail may appear (see for example the string at position 5 in Figure 1). We handle these empty strings as a special case, storing them as an end-of-string symbol even if our implementation would remove these symbols in any other case. This is necessary for select operations in to work, so that each is associated with a different offset in ; the DAC-VLS structure also requires this adjustment since it is not designed to support zero-length sequences. Note also that the DAC-VLS implementation, due to its construction, would not benefit from extra end-of-string symbols, so for those implementations we only use variants with no string terminators.
3.2.2. Single-lcp implementations
Our main proposal stores two integer sequences, and , to optimize lookup operations. Similar algorithms can be designed to work with only one array, saving half the space of these arrays at the cost of worst Front-coding compression.
Single-lcp implementations of any of our proposals can also be built in order to reduce the space utilization. The same idea of the general construction applies to these variants, but now we always compare with the left parent (llcp-only variants) or with the right parent (rlcp-only variants). Compression of the strings is expected to be worse since we are no longer using the maximum lcp, but these variants can still achieve better overall compression by removing one of the integer sequences.
Regarding query algorithms, lookup operations can still save some string comparisons using a similar algorithm to the one we proposed: essentially, llcp-only variants use lines 4-18 of the original algorithm, and rlcp-only variants lines 20-34. On access operations, the algorithm is also essentially the same, but we always move to the left (right) parent. When the lcp arrays are compressed, removing one access to them will have a positive effect on performance, since a single-lcp implementation only needs one DAC access per step.
4. Experimental evaluation
In this section we test the performance of our proposal in comparison with several alternatives in the state of the art. We perform tests with real-world datasets, focusing on two main application domains: representation of URLs, obtained from Web graph crawls, and representation of URIs and literal values extracted from RDF datasets. First we show an empirical evaluation of the implementation variants described in Section 3, in order to display their strengths. Then, we perform an experimental evaluation of our best implementation variants, comparing them with existing solutions for string dictionaries. Our comparison focuses on compression capabilities and query performance, and shows that our solutions obtain a better trade-off than state-of-the-art alternatives.
4.1. Experimental setup
We use in our tests a collection of datasets including URLs from real Web graphs and also URIs and literal values from an RDF dataset. Table 1 shows a summary of the datasets used. For each one, we display its size in plain, the number of strings it stores, the average string length and the alphabet size. Note that the average length displayed is computed as total size divided by number of strings, so it includes an extra character per string corresponding to the string terminator in the input.
UK and Arabic are datasets containing URLs of two different Web graph crawls. UK 444http://law..dsi.unimi.it/webdata/uk-2002 has been obtained from a 2002 crawl of .uk domains, whereas Arabic 555http://law.di.unimi.it/webdata/arabic-2005/ is a 2005 crawl that includes pages from countries whose content is potentially written in Arabic. Both datasets have been obtained from the Webgraph framework (webgraph). The UK dataset has been used in previous work as a baseline for URL compression (Martinez-Prieto:2016; pdtries; lzstringdict). The Arabic dataset is included for better confirmation of the performance of each solution in different Web graphs. Both datasets are similar in number of strings and average string length.
URIs contains all the different URIs in the English version of the DBpedia RDF dataset, in its 3.5.1 version666http://downloads.dbpedia.org/3.5.1/all_languages.tar.
Literals is a subset of the literals existing in the same DBpedia 3.5.1 dataset. Our input was generated from the original data by extracting all the literal values of the collection and obtaining the raw value from the RDF literal. To do this we remove language tags and type information, as well as the enclosing quotes of the original string. For instance, the RDF literal "100 AD"@en becomes 100 AD after removing the language tag, whereas the numeric value "57805"^^<http://www.w3.org/2001/XMLSchema#int> is converted to 57805. We sorted the values lexicographically, discarding duplicates and taking the entries in the first 2 GB. We limit the input size to 2 GB since it is the maximum supported by most of the state-of-the art alternatives that will be used for comparison. Notice that using raw literal values the strings are significantly shorter, but keeping the full strings would have little effect on our techniques: since only one language tag and a small number of different types are used, Re-Pair compression would be able to represent the extra characters at small cost. Our choice of raw values aims at highlighting the fundamental differences between Literals and the other datasets used, as Literals has much shorter strings on average, and much more different from each other.
The space shown for each structure is computed precisely from the size of the corresponding components. To measure query times, we build a set of 10,000 queries for each dataset by selecting random positions from the collection. The same positions are used for access and for the corresponding lookup queries. Query times are measured as the average over 100 iterations of the query set.
We implemented our proposals in C++777Our code is publicly available at https://gitlab.lbd.org.es/gdebernardo/improved-csd. We use an implementation of compressed bitmaps and Re-Pair based on the libcds library, the same used by Martinez-Prieto et al. (Martinez-Prieto:2016). All our implementations are compiled with g++ 4.8 with -O9 optimizations.
We compare our results with the following techniques:
PFC, RPFC and RPHTFC are some of the differential encoding techniques based on Front-coding (Martinez-Prieto:2016) described in Section 2. PFC is the plain solution, RPFC uses Re-Pair to compress buckets. RPHTFC is similar to the previous one, but it also applies Hu-Tucker compression to the bucket headers. We include PFC because it is the simplest solution, and RPFC and RPHTFC because they achieved the best results among their Front-coding-based solutions. We used bucket sizes 4, 8, 16 and 32.
RPDAC and HASHRPDAC are the binary searchable Re-Pair techniques also introduced in Section 2. The first one uses binary search, and the second one adds a hash table to speed up queries. Both of them are used with the default configuration parameters.
PDT is the the centroid-based compressed implementation of path-decomposed tries variants (pdtries), the best-performing alternative of this family.
All the alternatives are compiled with g++ with full optimizations enabled, using the default settings as provided by the authors apart from the parameters described above.
Note that we do not include a comparison with the implementation of LZ-78-compressed tries also described in Section 2, since their publicly-available code could not be compiled. Nevertheless, previous results (Martinez-Prieto:2016; lzstringdict) suggest that their proposals are dominated in most cases by PDT, and when they slightly improve compression they are much slower; they are also less efficient than HASHRPDAC and RPHTFC in most cases.
4.2. Comparison of our variants
Due to the relatively large number of variants proposed, we first outline some of the general characteristics of our implementation variants to display their relative strengths. After that, in the following sections we will only show experimental results corresponding to those of our techniques that provide the best tradeoff.
Figure 2 shows the space/time tradeoff provided by some of our proposals, considering both uncompressed (IBiS) and Re-Pair-compressed strings (IBiS, IBiS). For each approach we show the space/time tradeoff achieved in the dataset UK for the basic implementation (two lcp arrays) and both possible single-lcp implementations, labeled -L and -R respectively. For each of those, we show results for the basic techniques that keep string terminators and also for - implementations (labeled with -nt). The plot also shows a few of the differential encoding techniques described in Section 2, since they share similarities with our approach: PFC is similar to IBiS, whereas the rest of our variants are similar to RPFC or RPHTFC, improving compression through the use of Re-Pair and other techniques.
As shown in Figure 2, our plain implementations are not very competitive with PFC, since our techniques cannot compress each string with Front-coding as efficiently as the sequential encoding. Plain approaches will be omitted in the next sections, focusing on the more space-efficient alternatives. Figure 2 also shows some trends among our variants that are mostly the same in all the datasets used in our experiments:
Single-lcp implementations are, in general, a bit more space-efficient than double-lcp implementations in all the variants that use Re-Pair. A single-lcp variant may have significantly more characters in the string tails than a double-lcp variant. However, due to the efficiency of Re-Pair to compress the resulting strings, the actual increase in size of the compressed text is much smaller, and removing one of the lcp arrays easily compensates for this additional space. Plain single-lcp implementations, on the other hand, are much less efficient in space, since the extra bytes in the string tails are not compressed in any way. Regarding query times, single-lcp implementations are slower on lookup queries, due to the potentially larger cost of searches, but faster on access queries, thanks to the simpler bottom-up traversal that only needs to access a single lcp array. We will show experimental results for both single-lcp and double-lcp variants, since they can be useful in different scenarios depending on whether lookup or access queries are more relevant.
llcp-only and rlcp-only implementations achieve almost identical query times, as expected. However, llcp-only achieves slightly better compression in all cases, and it is also simpler, since in llcp-only variants we always perform Front-coding compression respective to a lexicographically smaller string, so we are guaranteed to have non-empty string tails in every position. In view of these results, we will omit rlcp-only variants from the remaining test results, noting that in all our experiments they were consistently slightly larger than their llcp-only counterparts and query times are similar.
- implementations achieve much better compression in most variants and in all datasets. This is expected since after Re-Pair compression is applied to the average length of a string tail is usually much shorter, so removing a byte per word yields a significant reduction in the overall space. As expected, - variants are also slightly slower, both in lookup and access queries, but we consider the effect on compression much more relevant. In the remaining test results we will focus mostly on - variants.
4.3. Comparison with the state of the art
Next we compare our implementations with the most significant state-of-the-art alternatives to the best of our knowledge. Note that, as stated earlier, we omit some of our implementation alternatives to provide clearer plots, and focus our comparison on the best-performing techniques from previous work.
Figures 3 and 4 show the space/time tradeoff on the Web graph datasets UK and Arabic. Both datasets are similar and the results obtained by the different techniques are also similar. Our proposals achieve the best compression among all the tested implementations. The llcp-only variant of IBiS obtains the best overall space results, but the equivalent IBiS is very close. We improve the space/time tradeoff RPFC and RPHTFC, since for smaller buckets they need much space to store bucket headers and for larger buckets their sequential traversal of the bucket becomes much slower. Regarding query times, the most efficient techniques are PDT and HASHRPDAC; RPDAC is similar to HASHRPDAC on access queries, but much less competitive on lookup queries, since it requires a binary search and must decode an entry of the DAC-VLS structure at each step. Our variants are similar on lookup queries, but the DAC-VLS solutions are slower on access queries. Note that our DAC-VLS solutions are much faster in lookup queries than RPDAC; both perform a binary search with accesses to a DAC-VLS structure, but we encode shorter entries thanks to Front-coding and we do not need to access the DAC-VLS at each step. The query times of our best solutions are roughly two times slower than the fastest solutions, but we are also significantly smaller than those, becoming the best alternative to optimize compression with competitive query times.
Figure 5 shows the results for the URIs dataset. Overall compression of all the tested representations is slightly worse when compared with the URL collections, but our compressed representations achieve again the best space results, around 15% compression. Again, the best compression is achieved by the llcp-only variants of IBiS and IBiS, and most of our proposals improve the tradeoff provided by RPFC and RPHTFC. Our best variants are also significantly smaller than HASHRPDAC, that achieves the best query times. PDT reaches compression close to ours, while achieving better query times on lookup queries. Nevertheless, on access queries our best structures are still competitive with PDT, achieving similar query times in less space. We consider this result on access queries more relevant than the result on lookup queries since, in practice, the former are usually more relevant than the latter, because they are more frequently used. In an RDF engine, for instance, a SPARQL query just requires a few lookup operations to encode the URIs/literals used in the query into numeric identifiers; then, after the query is executed, each result has to be translated back into the corresponding URIs/literals, which means a potentially very large number of access operations to answer a single query. Hence, even though good performance is required on both operations, performance on access queries may be more important in many applications.
Figure 6 shows the results obtained for the Literals dataset. In this dataset, the different nature of the strings leads to significantly different results: PDT, RPDAC and HASHRPDAC are much less efficient to compress the collection. Also, among our variants, the DAC-VLS techniques become much less efficient, since they are not well-suited to handle this kind of collection, with very short average string length but a few very long strings. Nevertheless, we still show in the plot the results for the best performing DAC-VLS variants, namely the IBiS approaches. Note also that, in this dataset, llcp-only implementations are not as efficient, and the smallest representation is the double-lcp IBiS. In spite of all these differences, our best solutions (both double-lcp and single-lcp) are much smaller than PDT and HASHRPDAC, while obtaining query times competitive with them. RPFC and RPHTFC, for larger bucket sizes, can achieve compression similar to us, but at the cost of much larger query times. Notice that, due to the characteristics of this dataset, the overall compression of all the solutions for this collection is much worse than in the previous ones, but still IBiS reaches 25% compression whereas PDT and HASHRPDAC are above 35%.
Taking into account the combined results from Figures 5 and 6, our techniques clearly obtain the best compression for both URIs and literal values, constituting a very efficient basis for string dictionary compression of RDF data. Our query times are competitive with those of existing data structures, especially on access queries, and the space-time tradeoff provided overcomes the tested alternatives.
5. Conclusions and future work
We have introduced a new family of compressed data structures for the efficient in-memory representation of string dictionaries. Our solutions can be regarded as an enhanced binary search that combines a hierarchical variant of Front-coding with suffix-array-based techniques to speed up longest-common-prefix computations. Those ideas are then composed with other techniques to derive a family of variants.
We perform a complete experimental evaluation of our proposals, comparing them with the best-performing state-of-the-art solutions and applying them to real-world datasets. We focus on two of the most active application domains for string dictionaries: Web graph and RDF data. Our results show that our representations achieve better compression than existing solutions, for similar query times, and are significantly smaller than any other alternative that is able to outperform our query times. Overall, our representation, in its several implementation variants, provides a relevant improvement in compression relative to previous proposals within very efficient query times.
We plan to explore the possibilities to extend our ideas to the dynamic scenario, where insertions and deletions are supported. A direct application of our techniques is not feasible in a dynamic environment, since we use a static decomposition of the collection and compression techniques that are also of static nature. However, we believe that simple adaptations based on the same compression techniques introduced here would still yield sufficiently compact dynamic dictionaries. Dynamic string dictionaries in compressed space are useful, for instance, for better handling large datasets in RDF engines in main memory.
Funded by EU H2020 MSCA RISE grant No 690941 (BIRDS). GN funded by the Millennium Institute for Foundational Research on Data (IMFD), and by Fondecyt Grant 1-170048, Conicyt, Chile. NB, ACP and GdB funded by Xunta de Galicia/FEDER-UE grants CSI: ED431G/01 and GRC:ED431C 2017/58; by MINECO-AEI/FEDER-UE grants TIN2016-77158-C4-3-R and TIN2016-78011-C4-1-R; and by MICINN grant RTC-2017-5908-7.