daachorse

repository·main·Indexed 42 days ago

https://github.com/daac-tools/daachorse

A high-performance Rust implementation of the Aho-Corasick algorithm using a compact double-array data structure. Version 5.0.0 provides fast multiple pattern matching with minimal memory overhead, supporting overlapping and non-overlapping searches, custom pattern values, and multibyte character matching via CharwiseDoubleArrayAhoCorasick. It supports no_std environments and includes features for serialization, deserialization, and Profile-Guided Optimization (PGO). Requires Rust 1.77 or higher.

Tokens
15.4K
Snippets
53
Records
60
Agent score
54%

What's inside daachorse

  1. Install and use daachorse

    main

    Daachorse is a fast implementation of the Aho-Corasick algorithm using a compact double-array data structure. It is optimized for time and memory efficiency, representing each state using only 12 bytes. It requires Rust 1.77 or higher.

    To use it in your Rust project, add it to your Cargo.toml dependencies. It supports no_std environments provided a global allocator is available via the alloc crate.

    # Add to Cargo.toml
    daachorse = "5.0.0"
  2. What is CharwiseDoubleArrayAhoCorasick and when to use it

    main

    A CharwiseDoubleArrayAhoCorasick is a fast multiple pattern match automaton that uses Unicode code point values instead of raw UTF-8 bytes for transitions.

    When to use it

    Use this version when matching against multibyte characters (such as CJK characters) to achieve faster matching speeds.

    Trade-offs compared to standard DoubleArrayAhoCorasick

    • Matching Speed: Generally faster for multibyte characters.
    • Construction Time: May be slower.
    • Memory Usage:
      • For large scales, memory efficiency is competitive.
      • For small scales, the double array can be sparse and memory-inefficient.
  3. How the SOG prefilter works

    main

    The Prefilter uses the SOG (Shift-Or with q-Grams) algorithm to optimize pattern matching. It scans a haystack using a bit-parallel shift-or automaton over overlapping 2-grams.

    Key behaviors:

    • Candidate Reporting: It identifies positions where an occurrence of a pattern might start. These are 'candidates'.
    • False Positives: Candidates may be false positives. The Aho-Corasick automaton must still verify them. However, the filter guarantees that no actual occurrence exists in the sections between reported candidates, allowing the automaton to skip those sections entirely.
    • Window Length: The filter uses a window based on the shortest pattern, capped at Prefilter::MAX_WINDOW_LEN (9 bytes).
    • Complexity: It is designed to be faster than plain automaton scanning by skipping large portions of the haystack when the pattern density is low.
    /* Concept: SOG (Shift-Or with 2-Grams) prefilter */
  4. Use PrefilterGate to optimize runtime performance

    main

    A PrefilterGate acts as a runtime safeguard. It monitors the efficiency of the prefilter during actual scanning. If the prefilter fails to skip enough bytes (i.e., it's not providing a significant speedup), the gate automatically disables prefiltering for the current iterator to fall back to plain automaton scanning.

    How it works:

    • It tracks performance in windows of GATE_WINDOW_CALLS (64 calls).
    • If the total bytes skipped (gain_in_window) in a window is less than GATE_MIN_WINDOW_GAIN (512 bytes), the gate sets enabled to false.
    • Once disabled, it stays disabled for the remainder of that iterator's lifecycle.
  5. How serialization and deserialization work in daachorse

    main

    Serialization in daachorse is designed to be efficient and safe.

    1. Primitives: All integer primitives are serialized using little-endian byte order.
    2. Options: Option<T> is serialized by writing a u8 flag (0 for None, 1 for Some) followed by the serialized value of T if it exists. Option<NonZeroU32> is optimized to use only 4 bytes (the u32 value itself, where 0 represents None).
    3. Vectors: Vec<S> is serialized by writing the length as a u32 followed by the elements. During deserialization, the library validates that the requested number of elements can actually fit within the provided byte slice to prevent malicious memory exhaustion attacks.
    4. Empty Types: The Empty type is a zero-cost abstraction that occupies 0 bytes when serialized and consumes no data during deserialization.
  6. Build a Double-Array Aho-Corasick automaton

    main

    Use DoubleArrayAhoCorasickBuilder to construct an efficient Aho-Corasick automaton. You can build it using a simple list of patterns (where the index is automatically used as the value) or by providing explicit pattern-value pairs.

    Basic usage with patterns

    When passing a list of patterns, the builder automatically associates each pattern with its index i in the list. The type V must be able to be created from a usize via TryFrom.

    use daachorse::DoubleArrayAhoCorasickBuilder;
    
    let patterns = vec!["bcd", "ab", "a"];
    let pma = DoubleArrayAhoCorasickBuilder::new()
        .build(patterns)
        .unwrap();

    Usage with explicit values

    If you need to associate specific data (like IDs or custom structs) with your patterns, use build_with_values. This allows you to map each pattern to a custom type V.

    use daachorse::DoubleArrayAhoCorasickBuilder;
    
    let patvals = vec![("bcd", 0), ("ab", 1), ("a", 2), ("e", 1)];
    let pma = DoubleArrayAhoCorasickBuilder::new()
        .build_with_values(patvals)
        .unwrap();
    use daachorse::DoubleArrayAhoCorasickBuilder;
    
    let patterns = vec!["bcd", "ab", "a"];
    let pma = DoubleArrayAhoCorasickBuilder::new()
        .build(patterns)
        .unwrap();
    
    let mut it = pma.find_iter("abcd");
    
    let m = it.next().unwrap();
    assert_eq!((m.start(), m.end(), m.value()), (0, 1, 2));
    
    let m = it.next().unwrap();
    assert_eq!((m.start(), m.end(), m.value()), (1, 4, 0));
    
    assert_eq!(None, it.next());
  7. Build a Prefilter using PrefilterBuilder

    main

    To create a Prefilter, use the PrefilterBuilder. This is an incremental builder that allows you to register patterns without keeping the entire pattern set in memory.

    Steps:

    1. Initialize a new builder with PrefilterBuilder::new().
    2. Add patterns using .add(&[u8]).
    3. Call .build() to attempt to create the filter.

    Note on .build() failure: The builder returns None if:

    • No patterns were added.
    • Any pattern is shorter than Prefilter::MIN_WINDOW_LEN (2 bytes).
    • The resulting filter table is too dense (the estimated candidate rate exceeds MAX_EXPECTED_CANDIDATE_RATE), meaning prefiltering would likely be slower than standard scanning.
    let mut builder = PrefilterBuilder::new();
    builder.add(b"pattern1");
    builder.add(b"pattern2");
    
    if let Some(prefilter) = builder.build() {
        // Use the prefilter
    }
  8. Build a character-based Aho-Corasick automaton with CharwiseDoubleArrayAhoCorasickBuilder

    main

    Use CharwiseDoubleArrayAhoCorasickBuilder to construct an automaton for searching patterns of characters (e.g., Unicode characters). You can build the automaton using either a simple list of patterns or a list of pattern-value pairs where you associate arbitrary data with each pattern.

    Basic Usage (Patterns only)

    When using .build(patterns), the index of each pattern in the input list is automatically associated as its value.

    Advanced Usage (Pattern-Value pairs)

    Use .build_with_values(patvals) to associate custom values (that implement Copy) with each pattern. This is useful for mapping patterns to IDs, database keys, or other metadata.

    Configuration Options

    • .match_kind(kind): Specifies the matching strategy (e.g., MatchKind::Standard, MatchKind::LeftmostLongest, or MatchKind::LeftmostFirst).
    • .use_prefilter(bool): Enables or disables a match-candidate prefilter. Enabled by default. It accelerates slice-based searches at the cost of ~64KiB of additional memory.
    • .corpus(haystacks): Provides sample documents for Profile-Guided Optimization (PGO). This reorders the automaton's memory layout so that states frequently accessed in the provided corpus are packed densely, improving cache efficiency during searches on similar text. Note that this increases construction time.
    use daachorse::CharwiseDoubleArrayAhoCorasickBuilder;
    
    // Example 1: Simple pattern list (values are indices)
    let patterns = vec!["全世界", "世界", "に"];
    let pma = CharwiseDoubleArrayAhoCorasickBuilder::new()
        .build(patterns)
        .unwrap();
    
    // Example 2: Custom pattern-value pairs
    let patvals = vec![("全世界", 0), ("世界", 10), ("に", 100)];
    let pma = CharwiseDoubleArrayAhoCorasickBuilder::new()
        .build_with_values(patvals)
        .unwrap();
    
    // Example 3: Using a corpus for layout optimization
    let pma = CharwiseDoubleArrayAhoCorasickBuilder::new()
        .corpus(["全世界中に"])
        .build(patterns)
        .unwrap();
  9. Use CharwiseDoubleArrayAhoCorasick for multibyte characters

    main

    The standard DoubleArrayAhoCorasick treats strings as UTF-8 byte sequences. For better performance and efficiency when matching multibyte characters (like Unicode), use CharwiseDoubleArrayAhoCorasick. This version uses Unicode code point values for transitions, which reduces the number of transitions and enables faster matching on non-ASCII text.

    use daachorse::CharwiseDoubleArrayAhoCorasick;
    
    let patterns = vec!["全世界", "世界", "に"];
    let pma = CharwiseDoubleArrayAhoCorasick::new(patterns).unwrap();
    
    let mut it = pma.find_iter("全世界中に");
    
    let m = it.next().unwrap();
    assert_eq!((m.start(), m.end(), m.value()), (0, 9, 0));
    
    let m = it.next().unwrap();
    assert_eq!((m.start(), m.end(), m.value()), (12, 15, 2));
    
    assert_eq!(None, it.next());
  10. Find non-overlapping occurrences with leftmost-first matching

    main

    To search for the earliest registered pattern among those starting from the search position, configure the automaton with MatchKind::LeftmostFirst using the DoubleArrayAhoCorasickBuilder and use leftmost_find_iter(). This is useful when multiple patterns start at the same position and you want the one that was added to the automaton first.

    use daachorse::{DoubleArrayAhoCorasickBuilder, MatchKind};
    
    let patterns = vec!["ab", "a", "abcd"];
    let pma = DoubleArrayAhoCorasickBuilder::new()
        .match_kind(MatchKind::LeftmostFirst)
        .build(&patterns)
        .unwrap();
    
    let mut it = pma.leftmost_find_iter("abcd");
    
    let m = it.next().unwrap();
    assert_eq!((m.start(), m.end(), m.value()), (0, 2, 0));
    
    assert_eq!(None, it.next());
  11. Serialize and deserialize automata

    main

    Daachorse allows you to serialize an automaton into a byte sequence for storage or transmission.

    Warning: Major version upgrades may break compatibility with existing serialized formats.

    • Use serialize() to convert the automaton to bytes.
    • Use deserialize() for safe deserialization when the source is untrusted.
    • Use deserialize_unchecked() (unsafe) if you are certain the serialized data has not been modified (e.g., it was embedded at build time via build.rs).
    use daachorse::DoubleArrayAhoCorasick;
    
    let patterns = vec!["ab", "a", "abcd"];
    let pma = DoubleArrayAhoCorasick::<u32>::new(patterns).unwrap();
    
    let serialized = pma.serialize();
    
    // Safe version
    let (pma_deserialized, _) = DoubleArrayAhoCorasick::<u32>::deserialize(&serialized).unwrap();
    
    // Unsafe version (use only if source is guaranteed safe)
    let (pma_deserialized, _) = unsafe {
        DoubleArrayAhoCorasick::<u32>::deserialize_unchecked(&serialized)
    };
  12. Find non-overlapping occurrences with longest matching

    main

    To search for the longest pattern without positional overlap in each iteration, configure the automaton with MatchKind::LeftmostLongest using the DoubleArrayAhoCorasickBuilder and use leftmost_find_iter().

    use daachorse::{DoubleArrayAhoCorasickBuilder, MatchKind};
    
    let patterns = vec!["ab", "a", "abcd"];
    let pma = DoubleArrayAhoCorasickBuilder::new()
        .match_kind(MatchKind::LeftmostLongest)
        .build(&patterns)
        .unwrap();
    
    let mut it = pma.leftmost_find_iter("abcd");
    
    let m = it.next().unwrap();
    assert_eq!((m.start(), m.end(), m.value()), (0, 4, 2));
    
    assert_eq!(None, it.next());