Repository: RediSearch/RediSearch
Pull request #6159: [MOD-9734] Trie contains iterator
Author: login: LukeMathWalker
Labels: name: size:M
Created: 2025-05-19T12:58:53Z | Merged: 2025-05-21T17:45:24Z

## Describe the changes in the pull request

Stacked on top of #6155.

An iterator that returns all entries that contain a given fragment. The traversal is optimized to skip the check whenever we know that ancestors verified the predicate.
The performance is on par with the C version.

#### Mark if applicable

- [ ] This PR introduces API changes
- [ ] This PR introduces serialization changes



Diff:
diff --git a/src/redisearch_rs/trie_bencher/benches/iter.rs b/src/redisearch_rs/trie_bencher/benches/iter.rs
index 408ed12670e..9ab89759138 100644
--- a/src/redisearch_rs/trie_bencher/benches/iter.rs
+++ b/src/redisearch_rs/trie_bencher/benches/iter.rs
@@ -64,6 +64,13 @@ fn iter_benches_gutenberg(c: &mut Criterion) {
         },
     );
 
+    // It's a prefix of many titles, we will therefore be able to skip
+    // the check for all children of the prefix node.
+    bencher.contains_group(c, "An");
+
+    // Rarely a prefix, we have to scan ~all nodes.
+    bencher.contains_group(c, "of");
+
     bencher.into_values_group(c, "IntoValues iterator");
 }
 
diff --git a/src/redisearch_rs/trie_bencher/src/bencher.rs b/src/redisearch_rs/trie_bencher/src/bencher.rs
index 5cc63ebc4c1..39481499bcc 100644
--- a/src/redisearch_rs/trie_bencher/src/bencher.rs
+++ b/src/redisearch_rs/trie_bencher/src/bencher.rs
@@ -22,7 +22,7 @@ use std::{
     ptr::NonNull,
     time::Duration,
 };
-use trie_rs::iter::LendingIter;
+use trie_rs::iter::{ContainsLendingIter, LendingIter};
 use trie_rs::iter::{RangeFilter, RangeLendingIter};
 use wildcard::WildcardPattern;
 
@@ -207,6 +207,51 @@ impl OperationBencher {
         into_values_benchmark(&mut group, &self.rust_map);
         group.finish();
     }
+
+    /// Benchmark the `ContainsIter` iterator.
+    ///
+    /// The benchmark group will be marked with the given label.
+    pub fn contains_group(&self, c: &mut Criterion, target: &str) {
+        let label = format!("Contains [{target}]");
+        let mut group = self.benchmark_group_mutable(c, &label);
+        contains_rust_benchmark(&mut group, &self.rust_map, target);
+        contains_c_benchmark(&mut group, &self.keys, target);
+        group.finish();
+    }
+}
+
+fn contains_rust_benchmark<M: Measurement>(
+    c: &mut BenchmarkGroup<'_, M>,
+    map: &RustTrieMap,
+    target: &str,
+) {
+    c.bench_function("Rust", |b| {
+        b.iter(|| {
+            let mut iter: ContainsLendingIter<_> =
+                map.contains_iter(black_box(target.as_bytes())).into();
+            while let Some(entry) = LendingIterator::next(&mut iter) {
+                black_box(entry);
+            }
+        })
+    });
+}
+
+fn contains_c_benchmark<M: Measurement>(
+    c: &mut BenchmarkGroup<'_, M>,
+    terms: &[String],
+    target: &str,
+) {
+    let target = target.into_cstring();
+    let view = target.as_view();
+    let map = c_load_from_terms(terms);
+    c.bench_function("C", |b| {
+        b.iter(|| {
+            let mut iter = map.contains_iter(view);
+            while let Some(entry) = LendingIterator::next(&mut iter) {
+                black_box(entry);
+            }
+        })
+    });
 }
 
 fn into_values_benchmark<M: Measurement>(c: &mut BenchmarkGroup<'_, M>, map: &RustTrieMap) {
diff --git a/src/redisearch_rs/trie_bencher/src/c_map.rs b/src/redisearch_rs/trie_bencher/src/c_map.rs
index deaeaf25bc5..d88f76a6de5 100644
--- a/src/redisearch_rs/trie_bencher/src/c_map.rs
+++ b/src/redisearch_rs/trie_bencher/src/c_map.rs
@@ -72,6 +72,15 @@ impl CTrieMap {
         }
     }
 
+    pub fn contains_iter(&self, target: TrieTermView) -> ContainsCIter {
+        let iter = unsafe { crate::ffi::TrieMap_Iterate(self.0, target.ptr(), target.len()) };
+        (unsafe { *iter }).mode = crate::ffi::tm_iter_mode_TM_CONTAINS_MODE;
+        ContainsCIter {
+            iterator: iter,
+            finished: false,
+        }
+    }
+
     pub fn range_iter(
         &self,
         min: Option<TrieTermView>,
@@ -166,6 +175,46 @@ impl Drop for WildcardCIter {
     }
 }
 
+pub struct ContainsCIter {
+    iterator: *mut crate::ffi::TrieMapIterator,
+    finished: bool,
+}
+
+#[gat]
+// The 'tm lifetime parameter is not actually needless.
+#[allow(clippy::needless_lifetimes)]
+impl LendingIterator for ContainsCIter {
+    type Item<'next>
+    where
+        Self: 'next,
+    = (&'next [u8], *mut c_void);
+
+    fn next(&mut self) -> Option<Self::Item<'_>> {
+        if self.finished {
+            return None;
+        }
+
+        let mut ptr: *mut c_char = std::ptr::null_mut();
+        let mut len = 0;
+        let mut value: *mut c_void = std::ptr::null_mut();
+        let should_continue = unsafe {
+            crate::ffi::TrieMapIterator_NextContains(self.iterator, &mut ptr, &mut len, &mut value)
+        };
+
+        if should_continue != 1 {
+            self.finished = false;
+        }
+        let key: &[u8] = unsafe { std::slice::from_raw_parts(ptr as *const u8, len as usize) };
+        Some((key, value))
+    }
+}
+
+impl Drop for ContainsCIter {
+    fn drop(&mut self) {
+        unsafe { crate::ffi::TrieMapIterator_Free(self.iterator) };
+    }
+}
+
 unsafe extern "C" fn do_nothing(oldval: *mut c_void, _newval: *mut c_void) -> *mut c_void {
     // Just return the old value, since it's a null pointer and we don't want
     // the C map implementation to try to free it.
diff --git a/src/redisearch_rs/trie_bencher/src/ffi.rs b/src/redisearch_rs/trie_bencher/src/ffi.rs
index 602f42b22b1..197f793309e 100644
--- a/src/redisearch_rs/trie_bencher/src/ffi.rs
+++ b/src/redisearch_rs/trie_bencher/src/ffi.rs
@@ -30,8 +30,9 @@ mod bindings {
 pub(crate) use bindings::{
     NewTrieMap, TrieMap, TrieMap_Add, TrieMap_Delete, TrieMap_ExactMemUsage, TrieMap_Find,
     TrieMap_FindPrefixes, TrieMap_Free, TrieMap_Iterate, TrieMap_IterateRange, TrieMapIterator,
-    TrieMapIterator_Free, TrieMapIterator_NextWildcard, array_free, array_new_sz,
-    tm_iter_mode_TM_WILDCARD_FIXED_LEN_MODE, tm_iter_mode_TM_WILDCARD_MODE,
+    TrieMapIterator_Free, TrieMapIterator_NextContains, TrieMapIterator_NextWildcard, array_free,
+    array_new_sz, tm_iter_mode_TM_CONTAINS_MODE, tm_iter_mode_TM_WILDCARD_FIXED_LEN_MODE,
+    tm_iter_mode_TM_WILDCARD_MODE,
 };
 // used in outside binary crate (main.rs)
 pub use bindings::TRIEMAP_NOTFOUND;
diff --git a/src/redisearch_rs/trie_rs/src/iter/contains.rs b/src/redisearch_rs/trie_rs/src/iter/contains.rs
new file mode 100644
index 00000000000..a210668c893
--- /dev/null
+++ b/src/redisearch_rs/trie_rs/src/iter/contains.rs
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2006-Present, Redis Ltd.
+ * All rights reserved.
+ *
+ * Licensed under your choice of the Redis Source Available License 2.0
+ * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
+ * GNU Affero General Public License v3 (AGPLv3).
+*/
+
+use crate::node::Node;
+use memchr::memmem::Finder;
+
+/// Iterates over all the entries in a [`TrieMap`](crate::TrieMap) that contain the target fragment,
+/// in lexicographical order.
+///
+/// Invoke [`TrieMap::contains_iter`](crate::TrieMap::contains_iter) to create an instance of this iterator.
+pub struct ContainsIter<'tm, Data> {
+    /// Stack of nodes and whether they have been visited.
+    stack: Vec<StackItem<'tm, Data>>,
+    /// Concatenation of the labels of current node and its ancestors,
+    /// i.e. the key of the current node.
+    key: Vec<u8>,
+    /// The target fragment we are looking for.
+    finder: Finder<'tm>,
+}
+
+struct StackItem<'a, Data> {
+    node: &'a Node<Data>,
+    was_visited: bool,
+    /// Set to `true` if we can skip checking if the current key contains the target fragment.
+    ///
+    /// This happens when the concatenation of the parent nodes of [`Self::node`] have already
+    /// been verified to contain the target fragment, thus allowing us to avoid redundant work.
+    skip_check: bool,
+}
+
+impl<'tm, Data> ContainsIter<'tm, Data> {
+    /// Creates a new contains iterator over the entries of a [`TrieMap`](crate::TrieMap).
+    pub(crate) fn new(root: Option<&'tm Node<Data>>, target: &'tm [u8]) -> Self {
+        let finder = Finder::new(target);
+        Self {
+            stack: root
+                .into_iter()
+                .map(|node| StackItem {
+                    node,
+                    was_visited: false,
+                    skip_check: false,
+                })
+                .collect(),
+            key: vec![],
+            finder,
+        }
+    }
+}
+
+impl<'tm, Data> ContainsIter<'tm, Data> {
+    /// The current key, obtained by concatenating the labels of the nodes
+    /// between the root and the current node.
+    pub(crate) fn key(&self) -> &[u8] {
+        &self.key
+    }
+
+    /// Advance this iterator to the next node, and set the
+    /// key to the one matching that node's entry
+    pub(crate) fn advance(&mut self) -> Option<&'tm Data> {
+        loop {
+            let StackItem {
+                node,
+                was_visited,
+                skip_check,
+            } = self.stack.pop()?;
+
+            if was_visited {
+                // We have now visited this node and all its descendants.
+                // We restore the key to the value matching its parent.
+                self.key
+                    .truncate(self.key.len() - node.label_len() as usize);
+                continue;
+            }
+
+            // Push the current node into the stack to remember, once all
+            // its descendants have been visited, to remove its label
+            // from the key buffer.
+            self.stack.push(StackItem {
+                node,
+                was_visited: true,
+                skip_check,
+            });
+            self.key.extend(node.label());
+
+            let is_match = skip_check || self.finder.find(&self.key).is_some();
+
+            self.stack.reserve(node.children().len());
+            for child in node.children().iter().rev() {
+                self.stack.push(StackItem {
+                    node: child,
+                    was_visited: false,
+                    skip_check: is_match,
+                });
+            }
+
+            if is_match {
+                if let Some(data) = node.data() {
+                    return Some(data);
+                }
+            }
+        }
+    }
+}
+
+impl<'tm, Data> Iterator for ContainsIter<'tm, Data> {
+    type Item = (Vec<u8>, &'tm Data);
+
+    fn next(&mut self) -> Option<Self::Item> {
+        self.advance().map(|d| (self.key.clone(), d))
+    }
+}
diff --git a/src/redisearch_rs/trie_rs/src/iter/lending_contains.rs b/src/redisearch_rs/trie_rs/src/iter/lending_contains.rs
new file mode 100644
index 00000000000..3d5cfb86d6a
--- /dev/null
+++ b/src/redisearch_rs/trie_rs/src/iter/lending_contains.rs
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2006-Present, Redis Ltd.
+ * All rights reserved.
+ *
+ * Licensed under your choice of the Redis Source Available License 2.0
+ * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
+ * GNU Affero General Public License v3 (AGPLv3).
+*/
+
+use super::ContainsIter;
+use lending_iterator::prelude::*;
+
+/// Iterates over the entries of a [`TrieMap`](crate::TrieMap) that contain the target fragment,
+/// in lexicographical order.
+///
+/// Unlike [`ContainsIter`], this iterator lets you borrow the current key, rather than having to clone it.
+pub struct ContainsLendingIter<'tm, Data>(ContainsIter<'tm, Data>);
+
+impl<'tm, Data> From<ContainsIter<'tm, Data>> for ContainsLendingIter<'tm, Data> {
+    fn from(iter: ContainsIter<'tm, Data>) -> Self {
+        ContainsLendingIter(iter)
+    }
+}
+
+// The [`LendingIterator`] trait allows us to obtain a reference to
+// the key corresponding to the value.
+// The [`Iterator`] trait does not allow for its `Item` to be a reference
+// to the Iterator itself.
+//
+// Why do we need a crate? Well: <https://sabrinajewson.org/blog/the-better-alternative-to-lifetime-gats>
+#[gat]
+// The 'tm lifetime parameter is not actually needless.
+#[allow(clippy::needless_lifetimes)]
+impl<'tm, Data> LendingIterator for ContainsLendingIter<'tm, Data> {
+    type Item<'next>
+    where
+        Self: 'next,
+    = (&'next [u8], &'tm Data);
+
+    fn next(&mut self) -> Option<Self::Item<'_>> {
+        let item = self.0.advance()?;
+        Some((self.0.key(), item))
+    }
+}
diff --git a/src/redisearch_rs/trie_rs/src/iter/mod.rs b/src/redisearch_rs/trie_rs/src/iter/mod.rs
index 45ae6c91a19..65703a47567 100644
--- a/src/redisearch_rs/trie_rs/src/iter/mod.rs
+++ b/src/redisearch_rs/trie_rs/src/iter/mod.rs
@@ -8,19 +8,23 @@
 */
 
 //! Different iterators to traverse a [`TrieMap`](crate::TrieMap).
+mod contains;
 pub mod filter;
 mod into_values;
 mod iter_;
 mod lending;
+mod lending_contains;
 mod lending_range;
 mod prefixes;
 mod range;
 mod values;
 mod wildcard;
 
+pub use contains::ContainsIter;
 pub use into_values::IntoValues;
 pub use iter_::Iter;
 pub use lending::LendingIter;
+pub use lending_contains::ContainsLendingIter;
 pub use lending_range::RangeLendingIter;
 pub use prefixes::PrefixesIter;
 pub use range::{RangeBoundary, RangeFilter, RangeIter};
diff --git a/src/redisearch_rs/trie_rs/src/trie.rs b/src/redisearch_rs/trie_rs/src/trie.rs
index 091f281224d..b7351be8705 100644
--- a/src/redisearch_rs/trie_rs/src/trie.rs
+++ b/src/redisearch_rs/trie_rs/src/trie.rs
@@ -11,8 +11,8 @@ use wildcard::WildcardPattern;
 
 use crate::{
     iter::{
-        IntoValues, Iter, LendingIter, PrefixesIter, RangeFilter, RangeIter, Values, WildcardIter,
-        filter::VisitAll,
+        ContainsIter, IntoValues, Iter, LendingIter, PrefixesIter, RangeFilter, RangeIter, Values,
+        WildcardIter, filter::VisitAll,
     },
     node::Node,
     utils::strip_prefix,
@@ -190,6 +190,11 @@ impl<Data> TrieMap<Data> {
         RangeIter::new(self.root.as_ref(), filter)
     }
 
+    /// Iterate over the entries that contain the target fragment, in lexicographical key order.
+    pub fn contains_iter<'a>(&'a self, target: &'a [u8]) -> ContainsIter<'a, Data> {
+        ContainsIter::new(self.root.as_ref(), target)
+    }
+
     /// Iterate over the entries that start with the given prefix, borrowing the current key from the iterator,
     /// in lexicographical key order.
     pub fn prefixed_lending_iter(&self, prefix: &[u8]) -> LendingIter<'_, Data, VisitAll> {
diff --git a/src/redisearch_rs/trie_rs/tests/integration/iter/contains.rs b/src/redisearch_rs/trie_rs/tests/integration/iter/contains.rs
new file mode 100644
index 00000000000..28f601748ba
--- /dev/null
+++ b/src/redisearch_rs/trie_rs/tests/integration/iter/contains.rs
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2006-Present, Redis Ltd.
+ * All rights reserved.
+ *
+ * Licensed under your choice of the Redis Source Available License 2.0
+ * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
+ * GNU Affero General Public License v3 (AGPLv3).
+*/
+
+use lending_iterator::LendingIterator;
+use trie_rs::{TrieMap, iter::ContainsLendingIter};
+
+/// Return all the keys that contain the given target.
+fn contains<Data: Clone>(trie: &TrieMap<Data>, target: &[u8]) -> Vec<Vec<u8>> {
+    let lending_keys = {
+        let mut keys = Vec::new();
+        let mut iter: ContainsLendingIter<_> = trie.contains_iter(target).into();
+        while let Some((key, _)) = LendingIterator::next(&mut iter) {
+            keys.push(key.to_owned());
+        }
+        keys
+    };
+    let iter_keys = trie.contains_iter(target).map(|(k, _)| k).collect();
+    assert_eq!(
+        iter_keys, lending_keys,
+        "Lending and non-lending iterator don't agree on the result set"
+    );
+    iter_keys
+}
+
+#[test]
+fn empty_is_always_contained() {
+    let mut trie = TrieMap::new();
+    trie.insert(b"", b"".to_vec());
+    trie.insert(b"apple", b"apple".into());
+
+    assert_eq!(contains(&trie, b""), vec!["".as_bytes(), b"apple"]);
+}
+
+#[test]
+fn non_empty_contains() {
+    let mut trie = TrieMap::new();
+    trie.insert(b"apple", b"apple".to_vec());
+    trie.insert(b"ban", b"ban".into());
+    trie.insert(b"banana", b"banana".into());
+    trie.insert(b"apricot", b"apricot".into());
+
+    // No entry contains the target.
+    assert!(contains(&trie, b"coat").is_empty());
+
+    assert_eq!(contains(&trie, b"appl"), vec![b"apple"]);
+    assert_eq!(contains(&trie, b"ap"), vec!["apple".as_bytes(), b"apricot"]);
+    assert_eq!(contains(&trie, b"an"), vec!["ban".as_bytes(), b"banana"]);
+
+    // If the target is stored as a term in the trie,
+    // it is returned as it contains itself.
+    assert_eq!(contains(&trie, b"ban"), vec!["ban".as_bytes(), b"banana"]);
+}
+
+mod property_based {
+    #![cfg(not(miri))]
+
+    use std::collections::BTreeMap;
+    use trie_rs::TrieMap;
+
+    fn is_subslice(needle: &[u8], haystack: &[u8]) -> bool {
+        if needle.len() > haystack.len() {
+            return false;
+        }
+        if needle.len() == 0 {
+            return true;
+        }
+        haystack
+            .windows(needle.len())
+            .any(|window| window == needle)
+    }
+
+    proptest::proptest! {
+        #[test]
+        /// Test whether [`trie_rs::iter::ContainsIter`] yields the same entries as a filtered BTreeMap iterator.
+        /// In particular, entries are yielded in the same order.
+        fn test_contains_iter(entries: BTreeMap<Vec<u8>, i32>, target: Vec<u8>) {
+            let mut trie = TrieMap::new();
+            for (key, value) in entries.clone() {
+                trie.insert(key.as_slice(), value);
+            }
+            let trie_entries: Vec<(Vec<u8>, i32)> = trie.contains_iter(&target).map(|(k, v)| (k.clone(), *v)).collect();
+            let btree_entries: Vec<(Vec<u8>, i32)> = entries.iter().filter_map(|(k, v)| {
+                if is_subslice(&target, k) {
+                    Some((k.clone(), *v))
+                } else {
+                    None
+                }
+            }).collect();
+
+            assert_eq!(trie_entries, btree_entries);
+        }
+    }
+}
diff --git a/src/redisearch_rs/trie_rs/tests/integration/iter/mod.rs b/src/redisearch_rs/trie_rs/tests/integration/iter/mod.rs
index 6e997b4574e..0596c1c8d33 100644
--- a/src/redisearch_rs/trie_rs/tests/integration/iter/mod.rs
+++ b/src/redisearch_rs/trie_rs/tests/integration/iter/mod.rs
@@ -7,6 +7,7 @@
  * GNU Affero General Public License v3 (AGPLv3).
 */
 
+mod contains;
 mod filter;
 mod prefixed;
 mod prefixes;


Reviews:
user: login: alonre24
body: NIce!
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-20T16:37:56Z
user: login: LukeMathWalker
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T05:16:01Z
user: login: LukeMathWalker
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T05:16:50Z
user: login: LukeMathWalker
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T05:17:54Z
user: login: LukeMathWalker
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T05:18:59Z
user: login: LukeMathWalker
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T05:27:26Z
user: login: LukeMathWalker
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T05:27:51Z
user: login: alonre24
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T06:46:48Z
user: login: alonre24
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T06:57:00Z
user: login: LukeMathWalker
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T07:05:51Z
user: login: LukeMathWalker
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T07:17:04Z
user: login: alonre24
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-05-21T07:18:55Z
user: login: alonre24
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-21T16:10:02Z

Review comments:
diff_hunk: @@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2006-Present, Redis Ltd.
+ * All rights reserved.
+ *
+ * Licensed under your choice of the Redis Source Available License 2.0
+ * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
+ * GNU Affero General Public License v3 (AGPLv3).
+*/
+
+use crate::node::Node;
+use memchr::memmem::Finder;
+
+/// Iterates over all the entries in a [`TrieMap`](crate::TrieMap) that contain the target fragment,
+/// in lexicographical order.
+///
+/// Invoke [`TrieMap::contains_iter`](crate::TrieMap::contains_iter) to create an instance of this iterator.
+pub struct ContainsIter<'tm, Data> {
+    /// Stack of nodes and whether they have been visited.
+    stack: Vec<StackItem<'tm, Data>>,
+    /// Concatenation of the labels of current node and its ancestors,
+    /// i.e. the key of the current node.
+    key: Vec<u8>,
+    /// The target fragment we are looking for.
+    finder: Finder<'tm>,
+}
+
+struct StackItem<'a, Data> {
+    node: &'a Node<Data>,
+    was_visited: bool,
+    /// Set to `true` if we can skip checking if the current key contains the target fragment.
+    ///
+    /// This happens when the concatenation of the parent nodes of [`Self::node`] have already
+    /// been verified to contain the target fragment, thus allowing us to avoid redundant work.
+    skip_check: bool,
+}
+
+impl<'tm, Data> ContainsIter<'tm, Data> {
+    /// Creates a new contains iterator over the entries of a [`TrieMap`](crate::TrieMap).
+    pub(crate) fn new(root: Option<&'tm Node<Data>>, target: &'tm [u8]) -> Self {
+        let finder = Finder::new(target);
+        Self {
+            stack: root
+                .into_iter()
path: src/redisearch_rs/trie_rs/src/iter/contains.rs
user: login: alonre24
body: Why is `.into_iter()` required here? Aren't we talking the root node and creating a single `StackItem` out of it? I would love to better understand the syntax here
created_at: 2025-05-20T14:50:46Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2006-Present, Redis Ltd.
+ * All rights reserved.
+ *
+ * Licensed under your choice of the Redis Source Available License 2.0
+ * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
+ * GNU Affero General Public License v3 (AGPLv3).
+*/
+
+use lending_iterator::LendingIterator;
+use trie_rs::{TrieMap, iter::ContainsLendingIter};
+
+/// Return all the keys that contain the given target.
+fn contains<Data: Clone>(trie: &TrieMap<Data>, target: &[u8]) -> Vec<Vec<u8>> {
+    let lending_keys = {
+        let mut keys = Vec::new();
+        let mut iter: ContainsLendingIter<_> = trie.contains_iter(target).into();
+        while let Some((key, _)) = LendingIterator::next(&mut iter) {
+            keys.push(key.to_owned());
+        }
+        keys
+    };
+    let iter_keys = trie.contains_iter(target).map(|(k, _)| k).collect();
+    assert_eq!(
+        iter_keys, lending_keys,
+        "Lending and non-lending iterator don't agree on the result set"
+    );
+    iter_keys
+}
+
+#[test]
+fn empty_is_always_contained() {
+    let mut trie = TrieMap::new();
+    trie.insert(b"", b"".to_vec());
+    trie.insert(b"apple", b"apple".into());
+
+    assert_eq!(contains(&trie, b""), vec!["".as_bytes(), b"apple"]);
+}
+
+#[test]
+fn non_empty_contains() {
+    let mut trie = TrieMap::new();
+    trie.insert(b"apple", b"apple".to_vec());
+    trie.insert(b"ban", b"ban".into());
path: src/redisearch_rs/trie_rs/tests/integration/iter/contains.rs
user: login: alonre24
body: Why does "apple" using `.to_vec()` but other terms are using "`.into()`?
created_at: 2025-05-20T15:03:24Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2006-Present, Redis Ltd.
+ * All rights reserved.
+ *
+ * Licensed under your choice of the Redis Source Available License 2.0
+ * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
+ * GNU Affero General Public License v3 (AGPLv3).
+*/
+
+use super::ContainsIter;
+use lending_iterator::prelude::*;
+
+/// Iterates over the entries of a [`TrieMap`](crate::TrieMap) that contain the target fragment,
+/// in lexicographical order.
+///
+/// Unlike [`ContainsIter`], this iterator lets you borrow the current key, rather than having to clone it.
+pub struct ContainsLendingIter<'tm, Data>(ContainsIter<'tm, Data>);
path: src/redisearch_rs/trie_rs/src/iter/lending_contains.rs
user: login: alonre24
body: Does it exist only for testing? Dind't see it exposed anywhere 
created_at: 2025-05-20T15:43:12Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2006-Present, Redis Ltd.
+ * All rights reserved.
+ *
+ * Licensed under your choice of the Redis Source Available License 2.0
+ * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
+ * GNU Affero General Public License v3 (AGPLv3).
+*/
+
+use crate::node::Node;
+use memchr::memmem::Finder;
+
+/// Iterates over all the entries in a [`TrieMap`](crate::TrieMap) that contain the target fragment,
+/// in lexicographical order.
+///
+/// Invoke [`TrieMap::contains_iter`](crate::TrieMap::contains_iter) to create an instance of this iterator.
+pub struct ContainsIter<'tm, Data> {
+    /// Stack of nodes and whether they have been visited.
+    stack: Vec<StackItem<'tm, Data>>,
+    /// Concatenation of the labels of current node and its ancestors,
+    /// i.e. the key of the current node.
+    key: Vec<u8>,
+    /// The target fragment we are looking for.
+    finder: Finder<'tm>,
+}
+
+struct StackItem<'a, Data> {
+    node: &'a Node<Data>,
+    was_visited: bool,
+    /// Set to `true` if we can skip checking if the current key contains the target fragment.
+    ///
+    /// This happens when the concatenation of the parent nodes of [`Self::node`] have already
+    /// been verified to contain the target fragment, thus allowing us to avoid redundant work.
+    skip_check: bool,
+}
+
+impl<'tm, Data> ContainsIter<'tm, Data> {
+    /// Creates a new contains iterator over the entries of a [`TrieMap`](crate::TrieMap).
+    pub(crate) fn new(root: Option<&'tm Node<Data>>, target: &'tm [u8]) -> Self {
+        let finder = Finder::new(target);
+        Self {
+            stack: root
+                .into_iter()
+                .map(|node| StackItem {
+                    node,
+                    was_visited: false,
+                    skip_check: false,
+                })
+                .collect(),
+            key: vec![],
+            finder,
+        }
+    }
+}
+
+impl<'tm, Data> ContainsIter<'tm, Data> {
+    /// The current key, obtained by concatenating the labels of the nodes
+    /// between the root and the current node.
+    pub(crate) fn key(&self) -> &[u8] {
+        &self.key
+    }
+
+    /// Advance this iterator to the next node, and set the
+    /// key to the one matching that node's entry
+    pub(crate) fn advance(&mut self) -> Option<&'tm Data> {
+        loop {
+            let StackItem {
+                node,
+                was_visited,
+                skip_check,
+            } = self.stack.pop()?;
+
+            if was_visited {
+                self.key
+                    .truncate(self.key.len() - node.label_len() as usize);
+                continue;
+            }
+
+            self.stack.push(StackItem {
+                node,
+                was_visited: true,
+                skip_check,
+            });
+            self.key.extend(node.label());
+
+            let is_match = self.finder.find(&self.key).is_some();
+
+            self.stack.reserve(node.children().len());
+            for child in node.children().iter().rev() {
+                self.stack.push(StackItem {
+                    node: child,
+                    was_visited: false,
+                    skip_check: is_match,
path: src/redisearch_rs/trie_rs/src/iter/contains.rs
user: login: alonre24
body: Where are we testing the value of `skip_check`? Couldn't find a usage
created_at: 2025-05-20T16:05:30Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2006-Present, Redis Ltd.
+ * All rights reserved.
+ *
+ * Licensed under your choice of the Redis Source Available License 2.0
+ * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
+ * GNU Affero General Public License v3 (AGPLv3).
+*/
+
+use crate::node::Node;
+use memchr::memmem::Finder;
+
+/// Iterates over all the entries in a [`TrieMap`](crate::TrieMap) that contain the target fragment,
+/// in lexicographical order.
+///
+/// Invoke [`TrieMap::contains_iter`](crate::TrieMap::contains_iter) to create an instance of this iterator.
+pub struct ContainsIter<'tm, Data> {
+    /// Stack of nodes and whether they have been visited.
+    stack: Vec<StackItem<'tm, Data>>,
+    /// Concatenation of the labels of current node and its ancestors,
+    /// i.e. the key of the current node.
+    key: Vec<u8>,
+    /// The target fragment we are looking for.
+    finder: Finder<'tm>,
+}
+
+struct StackItem<'a, Data> {
+    node: &'a Node<Data>,
+    was_visited: bool,
+    /// Set to `true` if we can skip checking if the current key contains the target fragment.
+    ///
+    /// This happens when the concatenation of the parent nodes of [`Self::node`] have already
+    /// been verified to contain the target fragment, thus allowing us to avoid redundant work.
+    skip_check: bool,
+}
+
+impl<'tm, Data> ContainsIter<'tm, Data> {
+    /// Creates a new contains iterator over the entries of a [`TrieMap`](crate::TrieMap).
+    pub(crate) fn new(root: Option<&'tm Node<Data>>, target: &'tm [u8]) -> Self {
+        let finder = Finder::new(target);
+        Self {
+            stack: root
+                .into_iter()
+                .map(|node| StackItem {
+                    node,
+                    was_visited: false,
+                    skip_check: false,
+                })
+                .collect(),
+            key: vec![],
+            finder,
+        }
+    }
+}
+
+impl<'tm, Data> ContainsIter<'tm, Data> {
+    /// The current key, obtained by concatenating the labels of the nodes
+    /// between the root and the current node.
+    pub(crate) fn key(&self) -> &[u8] {
+        &self.key
+    }
+
+    /// Advance this iterator to the next node, and set the
+    /// key to the one matching that node's entry
+    pub(crate) fn advance(&mut self) -> Option<&'tm Data> {
+        loop {
+            let StackItem {
+                node,
+                was_visited,
+                skip_check,
+            } = self.stack.pop()?;
+
+            if was_visited {
+                self.key
+                    .truncate(self.key.len() - node.label_len() as usize);
+                continue;
+            }
+
+            self.stack.push(StackItem {
path: src/redisearch_rs/trie_rs/src/iter/contains.rs
user: login: alonre24
body: Why do we need to push again nodes that we have already visited? 
created_at: 2025-05-20T16:10:39Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2006-Present, Redis Ltd.
+ * All rights reserved.
+ *
+ * Licensed under your choice of the Redis Source Available License 2.0
+ * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
+ * GNU Affero General Public License v3 (AGPLv3).
+*/
+
+use lending_iterator::LendingIterator;
+use trie_rs::{TrieMap, iter::ContainsLendingIter};
+
+/// Return all the keys that contain the given target.
+fn contains<Data: Clone>(trie: &TrieMap<Data>, target: &[u8]) -> Vec<Vec<u8>> {
+    let lending_keys = {
+        let mut keys = Vec::new();
+        let mut iter: ContainsLendingIter<_> = trie.contains_iter(target).into();
+        while let Some((key, _)) = LendingIterator::next(&mut iter) {
+            keys.push(key.to_owned());
+        }
+        keys
+    };
+    let iter_keys = trie.contains_iter(target).map(|(k, _)| k).collect();
+    assert_eq!(
+        iter_keys, lending_keys,
+        "Lending and non-lending iterator don't agree on the result set"
+    );
+    iter_keys
+}
+
+#[test]
+fn empty_is_always_contained() {
+    let mut trie = TrieMap::new();
+    trie.insert(b"", b"".to_vec());
+    trie.insert(b"apple", b"apple".into());
+
+    assert_eq!(contains(&trie, b""), vec!["".as_bytes(), b"apple"]);
+}
+
+#[test]
+fn non_empty_contains() {
+    let mut trie = TrieMap::new();
+    trie.insert(b"apple", b"apple".to_vec());
+    trie.insert(b"ban", b"ban".into());
+    trie.insert(b"banana", b"banana".into());
+    trie.insert(b"apricot", b"apricot".into());
+
+    // No entry contains the target.
+    assert!(contains(&trie, b"coat").is_empty());
+
+    assert_eq!(contains(&trie, b"appl"), vec![b"apple"]);
+    assert_eq!(contains(&trie, b"ap"), vec!["apple".as_bytes(), b"apricot"]);
+    assert_eq!(contains(&trie, b"an"), vec!["ban".as_bytes(), b"banana"]);
+
+    // If the target is stored as a term in the trie,
+    // it is returned as it contains itself.
+    assert_eq!(contains(&trie, b"ban"), vec!["ban".as_bytes(), b"banana"]);
+}
path: src/redisearch_rs/trie_rs/tests/integration/iter/contains.rs
user: login: alonre24
body: Should we add some kind of fuzzy tests as we did for the basic trie map implementation? 
created_at: 2025-05-20T16:25:04Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2006-Present, Redis Ltd.
+ * All rights reserved.
+ *
+ * Licensed under your choice of the Redis Source Available License 2.0
+ * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
+ * GNU Affero General Public License v3 (AGPLv3).
+*/
+
+use crate::node::Node;
+use memchr::memmem::Finder;
+
+/// Iterates over all the entries in a [`TrieMap`](crate::TrieMap) that contain the target fragment,
+/// in lexicographical order.
+///
+/// Invoke [`TrieMap::contains_iter`](crate::TrieMap::contains_iter) to create an instance of this iterator.
+pub struct ContainsIter<'tm, Data> {
+    /// Stack of nodes and whether they have been visited.
+    stack: Vec<StackItem<'tm, Data>>,
+    /// Concatenation of the labels of current node and its ancestors,
+    /// i.e. the key of the current node.
+    key: Vec<u8>,
+    /// The target fragment we are looking for.
+    finder: Finder<'tm>,
+}
+
+struct StackItem<'a, Data> {
+    node: &'a Node<Data>,
+    was_visited: bool,
+    /// Set to `true` if we can skip checking if the current key contains the target fragment.
+    ///
+    /// This happens when the concatenation of the parent nodes of [`Self::node`] have already
+    /// been verified to contain the target fragment, thus allowing us to avoid redundant work.
+    skip_check: bool,
+}
+
+impl<'tm, Data> ContainsIter<'tm, Data> {
+    /// Creates a new contains iterator over the entries of a [`TrieMap`](crate::TrieMap).
+    pub(crate) fn new(root: Option<&'tm Node<Data>>, target: &'tm [u8]) -> Self {
+        let finder = Finder::new(target);
+        Self {
+            stack: root
+                .into_iter()
path: src/redisearch_rs/trie_rs/src/iter/contains.rs
user: login: LukeMathWalker
body: `root` is an `Option`—it will be set to `None` if the trie is empty.
`.into_iter()` converts an `Option` into an iterator with either one element or no elements, which we then convert into a vector by collecting it.
It'equivalent to:

```rust
match root {
    Some(root) => {
        vec![StackItem {
                    node,
                    was_visited: false,
                    skip_check: false,
               }],
    },
    None => vec![]
}
```
created_at: 2025-05-21T05:16:01Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2006-Present, Redis Ltd.
+ * All rights reserved.
+ *
+ * Licensed under your choice of the Redis Source Available License 2.0
+ * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
+ * GNU Affero General Public License v3 (AGPLv3).
+*/
+
+use lending_iterator::LendingIterator;
+use trie_rs::{TrieMap, iter::ContainsLendingIter};
+
+/// Return all the keys that contain the given target.
+fn contains<Data: Clone>(trie: &TrieMap<Data>, target: &[u8]) -> Vec<Vec<u8>> {
+    let lending_keys = {
+        let mut keys = Vec::new();
+        let mut iter: ContainsLendingIter<_> = trie.contains_iter(target).into();
+        while let Some((key, _)) = LendingIterator::next(&mut iter) {
+            keys.push(key.to_owned());
+        }
+        keys
+    };
+    let iter_keys = trie.contains_iter(target).map(|(k, _)| k).collect();
+    assert_eq!(
+        iter_keys, lending_keys,
+        "Lending and non-lending iterator don't agree on the result set"
+    );
+    iter_keys
+}
+
+#[test]
+fn empty_is_always_contained() {
+    let mut trie = TrieMap::new();
+    trie.insert(b"", b"".to_vec());
+    trie.insert(b"apple", b"apple".into());
+
+    assert_eq!(contains(&trie, b""), vec!["".as_bytes(), b"apple"]);
+}
+
+#[test]
+fn non_empty_contains() {
+    let mut trie = TrieMap::new();
+    trie.insert(b"apple", b"apple".to_vec());
+    trie.insert(b"ban", b"ban".into());
path: src/redisearch_rs/trie_rs/tests/integration/iter/contains.rs
user: login: LukeMathWalker
body: By calling `to_vec` on `apple` we are forcing the compiler to assume that all entries are `Vec<u8>`. 
After that, it's enough to call `.into()` and the compiler knows what the target type for the conversion is.
created_at: 2025-05-21T05:16:50Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2006-Present, Redis Ltd.
+ * All rights reserved.
+ *
+ * Licensed under your choice of the Redis Source Available License 2.0
+ * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
+ * GNU Affero General Public License v3 (AGPLv3).
+*/
+
+use crate::node::Node;
+use memchr::memmem::Finder;
+
+/// Iterates over all the entries in a [`TrieMap`](crate::TrieMap) that contain the target fragment,
+/// in lexicographical order.
+///
+/// Invoke [`TrieMap::contains_iter`](crate::TrieMap::contains_iter) to create an instance of this iterator.
+pub struct ContainsIter<'tm, Data> {
+    /// Stack of nodes and whether they have been visited.
+    stack: Vec<StackItem<'tm, Data>>,
+    /// Concatenation of the labels of current node and its ancestors,
+    /// i.e. the key of the current node.
+    key: Vec<u8>,
+    /// The target fragment we are looking for.
+    finder: Finder<'tm>,
+}
+
+struct StackItem<'a, Data> {
+    node: &'a Node<Data>,
+    was_visited: bool,
+    /// Set to `true` if we can skip checking if the current key contains the target fragment.
+    ///
+    /// This happens when the concatenation of the parent nodes of [`Self::node`] have already
+    /// been verified to contain the target fragment, thus allowing us to avoid redundant work.
+    skip_check: bool,
+}
+
+impl<'tm, Data> ContainsIter<'tm, Data> {
+    /// Creates a new contains iterator over the entries of a [`TrieMap`](crate::TrieMap).
+    pub(crate) fn new(root: Option<&'tm Node<Data>>, target: &'tm [u8]) -> Self {
+        let finder = Finder::new(target);
+        Self {
+            stack: root
+                .into_iter()
+                .map(|node| StackItem {
+                    node,
+                    was_visited: false,
+                    skip_check: false,
+                })
+                .collect(),
+            key: vec![],
+            finder,
+        }
+    }
+}
+
+impl<'tm, Data> ContainsIter<'tm, Data> {
+    /// The current key, obtained by concatenating the labels of the nodes
+    /// between the root and the current node.
+    pub(crate) fn key(&self) -> &[u8] {
+        &self.key
+    }
+
+    /// Advance this iterator to the next node, and set the
+    /// key to the one matching that node's entry
+    pub(crate) fn advance(&mut self) -> Option<&'tm Data> {
+        loop {
+            let StackItem {
+                node,
+                was_visited,
+                skip_check,
+            } = self.stack.pop()?;
+
+            if was_visited {
+                self.key
+                    .truncate(self.key.len() - node.label_len() as usize);
+                continue;
+            }
+
+            self.stack.push(StackItem {
path: src/redisearch_rs/trie_rs/src/iter/contains.rs
user: login: LukeMathWalker
body: We're only pushing them if they have not been visited before (see https://github.com/RediSearch/RediSearch/pull/6159/files/beef86ab7bc1954a082e80e4a99345c339a413a1#diff-f2f0d63463bbd76ed49ad3fa6a9ec2461a520e8679b5679d118ff4cbeb4b42daR76).
The reason is that we need to know when we have visited all their descendants and it's thus time to truncate the key buffer by removing their label.
created_at: 2025-05-21T05:17:54Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2006-Present, Redis Ltd.
+ * All rights reserved.
+ *
+ * Licensed under your choice of the Redis Source Available License 2.0
+ * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
+ * GNU Affero General Public License v3 (AGPLv3).
+*/
+
+use super::ContainsIter;
+use lending_iterator::prelude::*;
+
+/// Iterates over the entries of a [`TrieMap`](crate::TrieMap) that contain the target fragment,
+/// in lexicographical order.
+///
+/// Unlike [`ContainsIter`], this iterator lets you borrow the current key, rather than having to clone it.
+pub struct ContainsLendingIter<'tm, Data>(ContainsIter<'tm, Data>);
path: src/redisearch_rs/trie_rs/src/iter/lending_contains.rs
user: login: LukeMathWalker
body: It's exposed in https://github.com/RediSearch/RediSearch/pull/6159/files/beef86ab7bc1954a082e80e4a99345c339a413a1 and will be used by our FFI bindings (see https://github.com/RediSearch/RediSearch/pull/6160/files#diff-2b801982600b3b813d130cf4501908eba1db571386cea912c4073d861fb19ff0R22)
created_at: 2025-05-21T05:18:59Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2006-Present, Redis Ltd.
+ * All rights reserved.
+ *
+ * Licensed under your choice of the Redis Source Available License 2.0
+ * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
+ * GNU Affero General Public License v3 (AGPLv3).
+*/
+
+use lending_iterator::LendingIterator;
+use trie_rs::{TrieMap, iter::ContainsLendingIter};
+
+/// Return all the keys that contain the given target.
+fn contains<Data: Clone>(trie: &TrieMap<Data>, target: &[u8]) -> Vec<Vec<u8>> {
+    let lending_keys = {
+        let mut keys = Vec::new();
+        let mut iter: ContainsLendingIter<_> = trie.contains_iter(target).into();
+        while let Some((key, _)) = LendingIterator::next(&mut iter) {
+            keys.push(key.to_owned());
+        }
+        keys
+    };
+    let iter_keys = trie.contains_iter(target).map(|(k, _)| k).collect();
+    assert_eq!(
+        iter_keys, lending_keys,
+        "Lending and non-lending iterator don't agree on the result set"
+    );
+    iter_keys
+}
+
+#[test]
+fn empty_is_always_contained() {
+    let mut trie = TrieMap::new();
+    trie.insert(b"", b"".to_vec());
+    trie.insert(b"apple", b"apple".into());
+
+    assert_eq!(contains(&trie, b""), vec!["".as_bytes(), b"apple"]);
+}
+
+#[test]
+fn non_empty_contains() {
+    let mut trie = TrieMap::new();
+    trie.insert(b"apple", b"apple".to_vec());
+    trie.insert(b"ban", b"ban".into());
+    trie.insert(b"banana", b"banana".into());
+    trie.insert(b"apricot", b"apricot".into());
+
+    // No entry contains the target.
+    assert!(contains(&trie, b"coat").is_empty());
+
+    assert_eq!(contains(&trie, b"appl"), vec![b"apple"]);
+    assert_eq!(contains(&trie, b"ap"), vec!["apple".as_bytes(), b"apricot"]);
+    assert_eq!(contains(&trie, b"an"), vec!["ban".as_bytes(), b"banana"]);
+
+    // If the target is stored as a term in the trie,
+    // it is returned as it contains itself.
+    assert_eq!(contains(&trie, b"ban"), vec!["ban".as_bytes(), b"banana"]);
+}
path: src/redisearch_rs/trie_rs/tests/integration/iter/contains.rs
user: login: LukeMathWalker
body: Added!
created_at: 2025-05-21T05:27:26Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2006-Present, Redis Ltd.
+ * All rights reserved.
+ *
+ * Licensed under your choice of the Redis Source Available License 2.0
+ * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
+ * GNU Affero General Public License v3 (AGPLv3).
+*/
+
+use crate::node::Node;
+use memchr::memmem::Finder;
+
+/// Iterates over all the entries in a [`TrieMap`](crate::TrieMap) that contain the target fragment,
+/// in lexicographical order.
+///
+/// Invoke [`TrieMap::contains_iter`](crate::TrieMap::contains_iter) to create an instance of this iterator.
+pub struct ContainsIter<'tm, Data> {
+    /// Stack of nodes and whether they have been visited.
+    stack: Vec<StackItem<'tm, Data>>,
+    /// Concatenation of the labels of current node and its ancestors,
+    /// i.e. the key of the current node.
+    key: Vec<u8>,
+    /// The target fragment we are looking for.
+    finder: Finder<'tm>,
+}
+
+struct StackItem<'a, Data> {
+    node: &'a Node<Data>,
+    was_visited: bool,
+    /// Set to `true` if we can skip checking if the current key contains the target fragment.
+    ///
+    /// This happens when the concatenation of the parent nodes of [`Self::node`] have already
+    /// been verified to contain the target fragment, thus allowing us to avoid redundant work.
+    skip_check: bool,
+}
+
+impl<'tm, Data> ContainsIter<'tm, Data> {
+    /// Creates a new contains iterator over the entries of a [`TrieMap`](crate::TrieMap).
+    pub(crate) fn new(root: Option<&'tm Node<Data>>, target: &'tm [u8]) -> Self {
+        let finder = Finder::new(target);
+        Self {
+            stack: root
+                .into_iter()
+                .map(|node| StackItem {
+                    node,
+                    was_visited: false,
+                    skip_check: false,
+                })
+                .collect(),
+            key: vec![],
+            finder,
+        }
+    }
+}
+
+impl<'tm, Data> ContainsIter<'tm, Data> {
+    /// The current key, obtained by concatenating the labels of the nodes
+    /// between the root and the current node.
+    pub(crate) fn key(&self) -> &[u8] {
+        &self.key
+    }
+
+    /// Advance this iterator to the next node, and set the
+    /// key to the one matching that node's entry
+    pub(crate) fn advance(&mut self) -> Option<&'tm Data> {
+        loop {
+            let StackItem {
+                node,
+                was_visited,
+                skip_check,
+            } = self.stack.pop()?;
+
+            if was_visited {
+                self.key
+                    .truncate(self.key.len() - node.label_len() as usize);
+                continue;
+            }
+
+            self.stack.push(StackItem {
+                node,
+                was_visited: true,
+                skip_check,
+            });
+            self.key.extend(node.label());
+
+            let is_match = self.finder.find(&self.key).is_some();
+
+            self.stack.reserve(node.children().len());
+            for child in node.children().iter().rev() {
+                self.stack.push(StackItem {
+                    node: child,
+                    was_visited: false,
+                    skip_check: is_match,
path: src/redisearch_rs/trie_rs/src/iter/contains.rs
user: login: LukeMathWalker
body: 🤦🏻‍♂️ Fixed in https://github.com/RediSearch/RediSearch/pull/6159/files#diff-f2f0d63463bbd76ed49ad3fa6a9ec2461a520e8679b5679d118ff4cbeb4b42daR86
created_at: 2025-05-21T05:27:51Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2006-Present, Redis Ltd.
+ * All rights reserved.
+ *
+ * Licensed under your choice of the Redis Source Available License 2.0
+ * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
+ * GNU Affero General Public License v3 (AGPLv3).
+*/
+
+use crate::node::Node;
+use memchr::memmem::Finder;
+
+/// Iterates over all the entries in a [`TrieMap`](crate::TrieMap) that contain the target fragment,
+/// in lexicographical order.
+///
+/// Invoke [`TrieMap::contains_iter`](crate::TrieMap::contains_iter) to create an instance of this iterator.
+pub struct ContainsIter<'tm, Data> {
+    /// Stack of nodes and whether they have been visited.
+    stack: Vec<StackItem<'tm, Data>>,
+    /// Concatenation of the labels of current node and its ancestors,
+    /// i.e. the key of the current node.
+    key: Vec<u8>,
+    /// The target fragment we are looking for.
+    finder: Finder<'tm>,
+}
+
+struct StackItem<'a, Data> {
+    node: &'a Node<Data>,
+    was_visited: bool,
+    /// Set to `true` if we can skip checking if the current key contains the target fragment.
+    ///
+    /// This happens when the concatenation of the parent nodes of [`Self::node`] have already
+    /// been verified to contain the target fragment, thus allowing us to avoid redundant work.
+    skip_check: bool,
+}
+
+impl<'tm, Data> ContainsIter<'tm, Data> {
+    /// Creates a new contains iterator over the entries of a [`TrieMap`](crate::TrieMap).
+    pub(crate) fn new(root: Option<&'tm Node<Data>>, target: &'tm [u8]) -> Self {
+        let finder = Finder::new(target);
+        Self {
+            stack: root
+                .into_iter()
+                .map(|node| StackItem {
+                    node,
+                    was_visited: false,
+                    skip_check: false,
+                })
+                .collect(),
+            key: vec![],
+            finder,
+        }
+    }
+}
+
+impl<'tm, Data> ContainsIter<'tm, Data> {
+    /// The current key, obtained by concatenating the labels of the nodes
+    /// between the root and the current node.
+    pub(crate) fn key(&self) -> &[u8] {
+        &self.key
+    }
+
+    /// Advance this iterator to the next node, and set the
+    /// key to the one matching that node's entry
+    pub(crate) fn advance(&mut self) -> Option<&'tm Data> {
+        loop {
+            let StackItem {
+                node,
+                was_visited,
+                skip_check,
+            } = self.stack.pop()?;
+
+            if was_visited {
+                self.key
+                    .truncate(self.key.len() - node.label_len() as usize);
+                continue;
+            }
+
+            self.stack.push(StackItem {
path: src/redisearch_rs/trie_rs/src/iter/contains.rs
user: login: alonre24
body: OK, I think I understand... Could we comment on these assumptions? the traversing algorithm here is not that trivial 
created_at: 2025-05-21T06:46:48Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2006-Present, Redis Ltd.
+ * All rights reserved.
+ *
+ * Licensed under your choice of the Redis Source Available License 2.0
+ * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
+ * GNU Affero General Public License v3 (AGPLv3).
+*/
+
+use super::ContainsIter;
+use lending_iterator::prelude::*;
+
+/// Iterates over the entries of a [`TrieMap`](crate::TrieMap) that contain the target fragment,
+/// in lexicographical order.
+///
+/// Unlike [`ContainsIter`], this iterator lets you borrow the current key, rather than having to clone it.
+pub struct ContainsLendingIter<'tm, Data>(ContainsIter<'tm, Data>);
path: src/redisearch_rs/trie_rs/src/iter/lending_contains.rs
user: login: alonre24
body: I see, so actually *only* the legend is exposed for C code?
created_at: 2025-05-21T06:57:00Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2006-Present, Redis Ltd.
+ * All rights reserved.
+ *
+ * Licensed under your choice of the Redis Source Available License 2.0
+ * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
+ * GNU Affero General Public License v3 (AGPLv3).
+*/
+
+use super::ContainsIter;
+use lending_iterator::prelude::*;
+
+/// Iterates over the entries of a [`TrieMap`](crate::TrieMap) that contain the target fragment,
+/// in lexicographical order.
+///
+/// Unlike [`ContainsIter`], this iterator lets you borrow the current key, rather than having to clone it.
+pub struct ContainsLendingIter<'tm, Data>(ContainsIter<'tm, Data>);
path: src/redisearch_rs/trie_rs/src/iter/lending_contains.rs
user: login: LukeMathWalker
body: Yes, we only use the lending version for C code, to minimize the number of allocations and match the existing interface (i.e. the caller doesn't have to worry about freeing the key pointer it receives from next.
created_at: 2025-05-21T07:05:51Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2006-Present, Redis Ltd.
+ * All rights reserved.
+ *
+ * Licensed under your choice of the Redis Source Available License 2.0
+ * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
+ * GNU Affero General Public License v3 (AGPLv3).
+*/
+
+use crate::node::Node;
+use memchr::memmem::Finder;
+
+/// Iterates over all the entries in a [`TrieMap`](crate::TrieMap) that contain the target fragment,
+/// in lexicographical order.
+///
+/// Invoke [`TrieMap::contains_iter`](crate::TrieMap::contains_iter) to create an instance of this iterator.
+pub struct ContainsIter<'tm, Data> {
+    /// Stack of nodes and whether they have been visited.
+    stack: Vec<StackItem<'tm, Data>>,
+    /// Concatenation of the labels of current node and its ancestors,
+    /// i.e. the key of the current node.
+    key: Vec<u8>,
+    /// The target fragment we are looking for.
+    finder: Finder<'tm>,
+}
+
+struct StackItem<'a, Data> {
+    node: &'a Node<Data>,
+    was_visited: bool,
+    /// Set to `true` if we can skip checking if the current key contains the target fragment.
+    ///
+    /// This happens when the concatenation of the parent nodes of [`Self::node`] have already
+    /// been verified to contain the target fragment, thus allowing us to avoid redundant work.
+    skip_check: bool,
+}
+
+impl<'tm, Data> ContainsIter<'tm, Data> {
+    /// Creates a new contains iterator over the entries of a [`TrieMap`](crate::TrieMap).
+    pub(crate) fn new(root: Option<&'tm Node<Data>>, target: &'tm [u8]) -> Self {
+        let finder = Finder::new(target);
+        Self {
+            stack: root
+                .into_iter()
+                .map(|node| StackItem {
+                    node,
+                    was_visited: false,
+                    skip_check: false,
+                })
+                .collect(),
+            key: vec![],
+            finder,
+        }
+    }
+}
+
+impl<'tm, Data> ContainsIter<'tm, Data> {
+    /// The current key, obtained by concatenating the labels of the nodes
+    /// between the root and the current node.
+    pub(crate) fn key(&self) -> &[u8] {
+        &self.key
+    }
+
+    /// Advance this iterator to the next node, and set the
+    /// key to the one matching that node's entry
+    pub(crate) fn advance(&mut self) -> Option<&'tm Data> {
+        loop {
+            let StackItem {
+                node,
+                was_visited,
+                skip_check,
+            } = self.stack.pop()?;
+
+            if was_visited {
+                self.key
+                    .truncate(self.key.len() - node.label_len() as usize);
+                continue;
+            }
+
+            self.stack.push(StackItem {
path: src/redisearch_rs/trie_rs/src/iter/contains.rs
user: login: LukeMathWalker
body: Added!
https://github.com/RediSearch/RediSearch/pull/6159/files#diff-f2f0d63463bbd76ed49ad3fa6a9ec2461a520e8679b5679d118ff4cbeb4b42daR73-R83
created_at: 2025-05-21T07:17:03Z
author_association: COLLABORATOR

PR comments:
user: login: codecov[bot]
created_at: 2025-05-19T13:26:50Z
body: ## [Codecov](https://app.codecov.io/gh/RediSearch/RediSearch/pull/6159?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=RediSearch) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 88.26%. Comparing base [(`035a8b4`)](https://app.codecov.io/gh/RediSearch/RediSearch/commit/035a8b437e7e8bc35ed97dce098bc3470abdc000?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=RediSearch) to head [(`6df85d9`)](https://app.codecov.io/gh/RediSearch/RediSearch/commit/6df85d98051c4387fffa48ea93762823a0a41945?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=RediSearch).
> Report is 4 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff            @@
##           master    #6159    +/-   ##
========================================
  Coverage   88.26%   88.26%            
========================================
  Files         223      227     +4     
  Lines       39610    39884   +274     
  Branches     2318     2592   +274     
========================================
+ Hits        34963    35205   +242     
- Misses       4632     4664    +32     
  Partials       15       15            
```

| [Flag](https://app.codecov.io/gh/RediSearch/RediSearch/pull/6159/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=RediSearch) | Coverage Δ | |
|---|---|---|
| [flow](https://app.codecov.io/gh/RediSearch/RediSearch/pull/6159/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=RediSearch) | `83.40% <ø> (-0.16%)` | :arrow_down: |
| [unit](https://app.codecov.io/gh/RediSearch/RediSearch/pull/6159/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=RediSearch) | `45.47% <100.00%> (+0.32%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=RediSearch#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/RediSearch/RediSearch/pull/6159?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=RediSearch).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=RediSearch).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: LukeMathWalker
created_at: 2025-05-21T16:06:21Z
body: (Rebased on latest master)
author_association: COLLABORATOR