diff --git a/build.rs b/build.rs
index d170572..d5bd33a 100644
--- a/build.rs
+++ b/build.rs
@@ -3,14 +3,10 @@
feature = "default-medium",
feature = "default-large"
))]
-use lz4_flex::block::compress_prepend_size;
-
-#[cfg(any(
- feature = "default-small",
- feature = "default-medium",
- feature = "default-large"
-))]
-use std::{collections::HashMap, env, fs, path::PathBuf};
+use {
+ lz4_flex::block::compress_prepend_size,
+ std::{collections::HashMap, env, fs, path::PathBuf},
+};
fn main() {
#[cfg(feature = "default-small")]
diff --git a/src/constants.rs b/src/constants.rs
new file mode 100644
index 0000000..ad60ea7
--- /dev/null
+++ b/src/constants.rs
@@ -0,0 +1,11 @@
+/// The character used to denote word breaks in the tokenized output.
+pub(crate) const WORD_BREAK_CHAR: &str = "▁";
+
+/// The token used to mark the start of a sentence.
+pub(crate) const SENTENCE_START_TOKEN: &str = "";
+
+/// The token used to mark the end of a sentence.
+pub(crate) const SENTENCE_END_TOKEN: &str = "";
+
+/// The token used to represent unknown words or subwords.
+pub(crate) const UNKNOWN_TOKEN: &str = "";
diff --git a/src/default_vocabs.rs b/src/default_vocabs.rs
new file mode 100644
index 0000000..3fe92a9
--- /dev/null
+++ b/src/default_vocabs.rs
@@ -0,0 +1,84 @@
+use crate::{BytePairEncoder, BytePairEncoderError};
+
+#[cfg(any(
+ feature = "default-small",
+ feature = "default-medium",
+ feature = "default-large"
+))]
+use {lz4_flex::decompress_size_prepended, std::collections::HashMap};
+
+#[allow(dead_code)]
+#[derive(Debug)]
+pub(crate) enum DefaultVocab {
+ Small,
+ Medium,
+ Large,
+}
+
+#[cfg(feature = "default-small")]
+const DEFAULT_SMALL_DATA: &[u8] = include_bytes!(concat!(
+ env!("OUT_DIR"),
+ "/",
+ "multi.wiki.bpe.vs100000.vocab.hashmap.bincode.lz4"
+));
+
+#[allow(dead_code)]
+#[cfg(not(feature = "default-small"))]
+const DEFAULT_SMALL_DATA: &[u8] = &[];
+
+#[cfg(feature = "default-medium")]
+const DEFAULT_MEDIUM_DATA: &[u8] = include_bytes!(concat!(
+ env!("OUT_DIR"),
+ "/",
+ "multi.wiki.bpe.vs320000.vocab.hashmap.bincode.lz4"
+));
+
+#[allow(dead_code)]
+#[cfg(not(feature = "default-medium"))]
+const DEFAULT_MEDIUM_DATA: &[u8] = &[];
+
+#[cfg(feature = "default-large")]
+const DEFAULT_LARGE_DATA: &[u8] = include_bytes!(concat!(
+ env!("OUT_DIR"),
+ "/",
+ "multi.wiki.bpe.vs1000000.vocab.hashmap.bincode.lz4"
+));
+
+#[allow(dead_code)]
+#[cfg(not(feature = "default-large"))]
+const DEFAULT_LARGE_DATA: &[u8] = &[];
+
+// The helper function to initialize a BytePairEncoder from a compressed vocabulary dataset.
+// This doesn't need to be public, so we use `pub(crate)` or leave it private entirely.
+#[cfg(any(
+ feature = "default-small",
+ feature = "default-medium",
+ feature = "default-large"
+))]
+pub(crate) fn new_default(vocab: DefaultVocab) -> Result {
+ let data = match vocab {
+ DefaultVocab::Small => DEFAULT_SMALL_DATA,
+ DefaultVocab::Medium => DEFAULT_MEDIUM_DATA,
+ DefaultVocab::Large => DEFAULT_LARGE_DATA,
+ };
+
+ // Decompress the LZ4 binary data.
+ let uncompressed = decompress_size_prepended(data)
+ .map_err(|e| BytePairEncoderError::DecompressionError(e.to_string()))?;
+
+ // Deserialize the uncompressed data into a HashMap.
+ let tokens: HashMap = bincode::deserialize(&uncompressed)
+ .map_err(|e| BytePairEncoderError::DeserializationError(e.to_string()))?;
+
+ // Return the BytePairEncoder.
+ Ok(BytePairEncoder { tokens })
+}
+
+#[cfg(not(any(
+ feature = "default-small",
+ feature = "default-medium",
+ feature = "default-large"
+)))]
+pub(crate) fn new_default(_vocab: DefaultVocab) -> Result {
+ Err(BytePairEncoderError::NoDefaultVocabFeature)
+}
diff --git a/src/errors.rs b/src/errors.rs
new file mode 100644
index 0000000..2ef0006
--- /dev/null
+++ b/src/errors.rs
@@ -0,0 +1,25 @@
+use thiserror::Error;
+
+/// Represents errors that can occur during BPE tokenization operations.
+#[derive(Error, Debug, Clone, PartialEq, Eq)]
+pub enum BytePairEncoderError {
+ /// Indicates an error occurred while reading the vocabulary file.
+ #[error("Error reading file: {0}")]
+ InvalidFile(String),
+
+ /// Indicates that the vocabulary input was invalid or could not be parsed correctly.
+ #[error("Invalid vocabulary input: Could not parse vocabulary file.")]
+ InvalidVocabularyInput,
+
+ /// Indicates an error occurred during decompression of the vocabulary data.
+ #[error("Error decompressing vocabulary data: {0}")]
+ DecompressionError(String),
+
+ /// Indicates an error occurred during deserialization of the vocabulary data.
+ #[error("Error deserializing vocabulary data: {0}")]
+ DeserializationError(String),
+
+ // Indicates attempt to use a default vocabulary without enabling its Cargo feature.
+ #[error("Error, must enable defualt-small, default-medium, and/or default-large feature(s) to use default vocabulary.")]
+ NoDefaultVocabFeature,
+}
diff --git a/src/lib.rs b/src/lib.rs
index cb99352..e51d11e 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -129,1157 +129,15 @@
//! [`BytePairEncoder::new_default_large`]) become available for constructing a `BytePairEncoder`.
//! Only enable the features that you need to ensure minimized memory and binary size.
-use std::{collections::HashMap, fs, iter};
-
-use thiserror::Error;
-use unicode_segmentation::UnicodeSegmentation;
-
-#[cfg(any(
- feature = "default-small",
- feature = "default-medium",
- feature = "default-large"
-))]
-use lz4_flex::decompress_size_prepended;
-
-/// Represents errors that can occur during BPE tokenization operations.
-#[derive(Error, Debug, Clone, PartialEq, Eq)]
-pub enum BytePairEncoderError {
- /// Indicates an error occurred while reading the vocabulary file.
- #[error("Error reading file: {0}")]
- InvalidFile(String),
-
- /// Indicates that the vocabulary input was invalid or could not be parsed correctly.
- #[error("Invalid vocabulary input: Could not parse vocabulary file.")]
- InvalidVocabularyInput,
-
- /// Indicates an error occurred during decompression of the vocabulary data.
- #[error("Error decompressing vocabulary data: {0}")]
- DecompressionError(String),
-
- /// Indicates an error occurred during deserialization of the vocabulary data.
- #[error("Error deserializing vocabulary data: {0}")]
- DeserializationError(String),
-}
-
-/// The character used to denote word breaks in the tokenized output.
-const WORD_BREAK_CHAR: &str = "▁";
-
-/// The token used to mark the start of a sentence.
-const SENTENCE_START_TOKEN: &str = "";
-
-/// The token used to mark the end of a sentence.
-const SENTENCE_END_TOKEN: &str = "";
-
-/// The token used to represent unknown words or subwords.
-const UNKNOWN_TOKEN: &str = "";
-
-/// Token vocabulary data for the default small model.
-#[cfg(feature = "default-small")]
-const DEFAULT_SMALL_DATA: &[u8] = include_bytes!(concat!(
- env!("OUT_DIR"),
- "/",
- "multi.wiki.bpe.vs100000.vocab.hashmap.bincode.lz4"
-));
-
-/// Token vocabulary data for the default small model.
-#[cfg(feature = "default-medium")]
-const DEFAULT_MEDIUM_DATA: &[u8] = include_bytes!(concat!(
- env!("OUT_DIR"),
- "/",
- "multi.wiki.bpe.vs320000.vocab.hashmap.bincode.lz4"
-));
-
-/// Token vocabulary data for the default small model.
-#[cfg(feature = "default-large")]
-const DEFAULT_LARGE_DATA: &[u8] = include_bytes!(concat!(
- env!("OUT_DIR"),
- "/",
- "multi.wiki.bpe.vs1000000.vocab.hashmap.bincode.lz4"
-));
-
-/// # Represents a Byte Pair Encoding (BPE) vocabulary used for tokenization.
-///
-/// This struct holds the mapping of tokens to their respective scores (or IDs)
-/// and provides methods for tokenizing text using the BPE algorithm.
-///
-/// The vocabulary is typically loaded from a file or string where each line
-/// contains a token and its score, separated by a tab character.
-///
-/// ## Example
-///
-/// ```
-/// use bpe_tokenizer::BytePairEncoder;
-///
-/// let vocab = BytePairEncoder::new_from_str("hello\t1\nworld\t2").unwrap();
-/// let tokenized = vocab.tokenize("Hello, world!");
-/// ```
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct BytePairEncoder {
- /// # A mapping of tokens to their respective scores (or IDs).
- ///
- /// In BPE, tokens with lower scores (or IDs) are typically more common
- /// and are preferred during the tokenization process.
- tokens: HashMap,
-}
-
-impl BytePairEncoder {
- /// # Creates a new `BytePairEncoder` from a file containing token-score pairs.
- ///
- /// This function reads the contents of the file specified by `file_path` and constructs
- /// a `BytePairEncoder` from it. The file should contain token-score pairs, with each pair
- /// on a separate line and the token and score separated by a tab character (`\t`).
- ///
- /// ## Input Format
- ///
- /// The file is expected to follow this format:
- ///
- /// ```text
- /// \t\n
- /// ```
- ///
- /// Each line should consist of:
- /// * A token (a string) followed by a tab character (`\t`)
- /// * A score (an integer) as either a positive or negative value.
- ///
- /// Example lines from the file:
- ///
- /// ```text
- /// 0
- /// ▁t -0
- /// ▁the -4
- /// ```
- ///
- /// ## Arguments
- ///
- /// * `file_path` - A string slice that holds the path to the file containing token-score pairs.
- ///
- /// ## Returns
- ///
- /// * `Result` - A Result containing the created `BytePairEncoder` if successful,
- /// or a `BytePairEncoderError` if there was an error reading the file or parsing its contents.
- ///
- /// ## Errors
- ///
- /// This function will return an error if:
- /// * The file cannot be read (returns `BytePairEncoderError::InvalidFile`)
- /// * The file contents are not in the expected format (returns `BytePairEncoderError::InvalidVocabularyInput`)
- ///
- /// ## Example
- ///
- /// ```
- /// use bpe_tokenizer::BytePairEncoder;
- ///
- /// let vocab = BytePairEncoder::new_from_file("path/to/vocabulary/file.txt");
- /// ```
- pub fn new_from_file(file_path: &str) -> Result {
- Self::new_from_str(
- fs::read_to_string(file_path)
- .map_err(|_| BytePairEncoderError::InvalidFile(file_path.to_string()))?
- .as_ref(),
- )
- }
-
- /// # Creates a new `BytePairEncoder` from a string containing token-score pairs.
- ///
- /// This function parses the input string to construct a `BytePairEncoder`. The input should
- /// contain token-score pairs, with each pair on a separate line and the token and score
- /// separated by a tab character (`\t`).
- ///
- /// ## Input Format
- ///
- /// The string must follow this format:
- ///
- /// ```text
- /// \t\n
- /// ```
- ///
- /// Each line in the string should consist of:
- /// * A token (a string) followed by a tab character (`\t`)
- /// * A score (an integer) as either a positive or negative value.
- ///
- /// For example:
- ///
- /// ```text
- /// hello 1
- /// world 2
- /// ▁the -4
- /// ```
- ///
- /// ## Arguments
- ///
- /// * `input` - A string slice that holds the token-score pairs.
- ///
- /// ## Returns
- ///
- /// * `Result` - A Result containing the created `BytePairEncoder` if successful,
- /// or a `BytePairEncoderError` if there was an error parsing the input.
- ///
- /// ## Errors
- ///
- /// This function will return `BytePairEncoderError::InvalidVocabularyInput` if:
- /// * A line doesn't contain a tab character to separate token and score.
- /// * The score cannot be parsed as an `isize`.
- ///
- /// ## Example
- ///
- /// ```
- /// use bpe_tokenizer::BytePairEncoder;
- ///
- /// let input = "hello\t1\nworld\t2";
- /// let vocab = BytePairEncoder::new_from_str(input).unwrap();
- /// ```
- pub fn new_from_str(input: &str) -> Result {
- let mut tokens = HashMap::new();
-
- for line in input.lines() {
- let (token, score_str) = match line.split_once('\t') {
- Some(pair) => pair,
- None => return Err(BytePairEncoderError::InvalidVocabularyInput),
- };
- let score = match score_str.parse::() {
- Ok(score) => score,
- Err(_) => return Err(BytePairEncoderError::InvalidVocabularyInput),
- };
- tokens.insert(token.to_string(), score);
- }
-
- Ok(BytePairEncoder { tokens })
- }
-
- /// # Initializes a new `BytePairEncoder` from given compressed vocabulary data.
- ///
- /// This private function decompresses and deserializes a pre-trained multilingual vocabulary that supports
- /// 275 languages. The compressed vocabulary is expected to be in LZ4 format. It is then decompressed and
- /// deserialized into a `HashMap`, which is used to tokenize text.
- ///
- /// The function handles any decompression or deserialization failures by returning a `BytePairEncoderError`.
- ///
- /// ## Arguments
- ///
- /// * `data` - A reference to a binary slice holding compressed LZ4 vocabulary data.
- ///
- /// ## Returns
- ///
- /// A `Result` where on success, a `BytePairEncoder` instance is returned, and
- /// on failure, a `BytePairEncoderError` indicates what went wrong.
- ///
- /// ## Errors
- ///
- /// * `DecompressionError`: Returned if decompression of the LZ4-based vocabulary data fails.
- /// * `DeserializationError`: Returned if deserialization of the decompressed data fails.
- #[cfg(any(
- feature = "default-small",
- feature = "default-medium",
- feature = "default-large"
- ))]
- fn new_default(data: &'static [u8]) -> Result {
- // Decompress the binary data
- let uncompressed = decompress_size_prepended(data)
- .map_err(|e| BytePairEncoderError::DecompressionError(e.to_string()))?;
-
- // Deserialize the uncompressed data into a HashMap
- let tokens: HashMap = bincode::deserialize(&uncompressed)
- .map_err(|e| BytePairEncoderError::DeserializationError(e.to_string()))?;
-
- // Successfully create a BytePairEncoder
- Ok(Self { tokens })
- }
-
- /// # Creates a new `BytePairEncoder` with a default small vocabulary size (100,000 tokens).
- ///
- /// This function constructs a `BytePairEncoder` using a pre-trained multilingual vocabulary
- /// that supports 275 languages. The vocabulary is sourced from the
- /// [BPEmb](https://github.com/bheinzerling/bpemb) project, licensed under MIT. The small-sized
- /// vocabulary file consists of 100,000 tokens, allowing for highly compressed tokenization
- /// suitable for tasks with limited memory constraints.
- ///
- /// ## Returns
- ///
- /// A `Result`, constructing the `BytePairEncoder` on successful
- /// vocabulary loading, or a corresponding error if initialization fails.
- ///
- /// ## Example
- ///
- /// ```
- /// use bpe_tokenizer::BytePairEncoder;
- ///
- /// let encoder = BytePairEncoder::new_default_small().unwrap();
- /// ```
- ///
- /// ## Note
- ///
- /// This is only enabled when the `default-small` feature is enabled in Cargo.toml.
- ///
- /// ```toml
- /// [dependencies]
- /// bpe-tokenizer = { version = " Result {
- Self::new_default(DEFAULT_SMALL_DATA)
- }
-
- /// # Creates a new `BytePairEncoder` with a default medium vocabulary size (320,000 tokens).
- ///
- /// This function constructs a `BytePairEncoder` using a pre-trained multilingual vocabulary
- /// that supports 275 languages. The vocabulary is sourced from the
- /// [BPEmb](https://github.com/bheinzerling/bpemb) project, licensed under MIT. The
- /// medium-sized vocabulary file consists of 320,000 tokens, offering a balance between token
- /// coverage and memory efficiency, making it suitable for a wide variety of NLP tasks.
- ///
- /// ## Returns
- ///
- /// A `Result`, constructing the `BytePairEncoder` on successful
- /// vocabulary loading, or a corresponding error if initialization fails.
- ///
- /// ## Example
- ///
- /// ```
- /// use bpe_tokenizer::BytePairEncoder;
- ///
- /// let encoder = BytePairEncoder::new_default_medium().unwrap();
- /// ```
- ///
- /// ## Note
- ///
- /// This is only enabled when the `default-medium` feature is enabled in Cargo.toml.
- ///
- /// ```toml
- /// [dependencies]
- /// bpe-tokenizer = { version = " Result {
- Self::new_default(DEFAULT_MEDIUM_DATA)
- }
-
- /// # Creates a new `BytePairEncoder` with a default large vocabulary size (1,000,000 tokens).
- ///
- /// This function constructs a `BytePairEncoder` using a pre-trained multilingual vocabulary
- /// that supports 275 languages. The vocabulary is sourced from the
- /// [BPEmb](https://github.com/bheinzerling/bpemb) project, licensed under MIT. The large-sized
- /// vocabulary consists of 1,000,000 tokens, providing maximum coverage for detailed language
- /// representation, especially useful in applications requiring high granularity.
- ///
- /// ## Returns
- ///
- /// A `Result`, constructing the `BytePairEncoder` on successful
- /// vocabulary loading, or a corresponding error if initialization fails.
- ///
- /// ## Example
- ///
- /// ```
- /// use bpe_tokenizer::BytePairEncoder;
- ///
- /// let encoder = BytePairEncoder::new_default_large().unwrap();
- /// ```
- ///
- /// ## Note
- ///
- /// This is only enabled when the `default-large` feature is enabled in Cargo.toml.
- ///
- /// ```toml
- /// [dependencies]
- /// bpe-tokenizer = { version = " Result {
- Self::new_default(DEFAULT_LARGE_DATA)
- }
-
- /// # Tokenizes a text into sentences, then words, and finally into BPE tokens.
- ///
- /// This function takes a string of text and returns an iterator that yields
- /// vectors of tokens, where each vector represents a tokenized sentence.
- ///
- /// ## Arguments
- ///
- /// * `text` - A string slice containing the text to be tokenized.
- ///
- /// ## Returns
- ///
- /// An iterator that yields `Vec`, where each `Vec` represents
- /// a tokenized sentence.
- ///
- /// ## Example
- ///
- /// ```
- /// use bpe_tokenizer::BytePairEncoder;
- ///
- /// let vocab = BytePairEncoder::new_from_str("hello\t1\nworld\t2").unwrap();
- /// let text = "Hello, world! How are you?";
- /// let tokenized: Vec> = vocab
- /// .tokenize_sentences_iter(text)
- /// .map(|sentence_iter| sentence_iter.collect()) // Collect each inner iterator into a Vec
- /// .collect(); // Then collect everything into Vec>
- /// ```
- ///
- /// ## Notes
- ///
- /// - This function uses Unicode-aware sentence and word segmentation.
- /// - Each sentence is wrapped with sentence start (``) and end (``) tokens.
- /// - Words are prefixed with the word break character (`▁`).
- /// - Unknown tokens are replaced with the `` token.
- pub fn tokenize_sentences_iter<'a>(
- &'a self,
- text: &'a str,
- ) -> impl Iterator- + 'a> + 'a {
- UnicodeSegmentation::unicode_sentences(text)
- .map(move |sentence| self.tokenize_with_sentence_markers_iter(sentence))
- }
-
- /// # Tokenizes a text into a flat sequence of BPE tokens.
- ///
- /// This function takes a string of text and returns an iterator that yields
- /// individual tokens. It first tokenizes the text into sentences, then words,
- /// and finally into BPE tokens, flattening the result into a single sequence.
- ///
- /// ## Arguments
- ///
- /// * `text` - A string slice containing the text to be tokenized.
- ///
- /// ## Returns
- ///
- /// An iterator that yields `String`, where each `String` represents a token.
- ///
- /// ## Example
- ///
- /// ```
- /// use bpe_tokenizer::BytePairEncoder;
- ///
- /// let vocab = BytePairEncoder::new_from_str("hello\t1\nworld\t2").unwrap();
- /// let text = "Hello, world! How are you?";
- /// let tokenized: Vec = vocab.tokenize_iter(text).collect();
- /// ```
- ///
- /// ## Notes
- ///
- /// - This function uses Unicode-aware sentence and word segmentation.
- /// - Each sentence is wrapped with sentence start (`
`) and end (``) tokens.
- /// - Words are prefixed with the word break character (`▁`).
- /// - Unknown tokens are replaced with the `` token.
- pub fn tokenize_iter<'a>(&'a self, text: &'a str) -> impl Iterator- + 'a {
- self.tokenize_sentences_iter(text).flatten()
- }
-
- /// # Tokenizes a text into sentences, then words, and finally into BPE tokens.
- ///
- /// This function takes a string of text and returns a vector of tokenized sentences,
- /// where each sentence is represented as a vector of tokens.
- ///
- /// ## Arguments
- ///
- /// * `text` - A string slice containing the text to be tokenized.
- ///
- /// ## Returns
- ///
- /// A `Vec>`, where each inner `Vec` represents a tokenized sentence.
- ///
- /// ## Example
- ///
- /// ```
- /// use bpe_tokenizer::BytePairEncoder;
- ///
- /// let vocab = BytePairEncoder::new_from_str("hello\t1\nworld\t2").unwrap();
- /// let text = "Hello, world! How are you?";
- /// let tokenized = vocab.tokenize_sentences(text);
- /// ```
- ///
- /// ## Notes
- ///
- /// - This function uses Unicode-aware sentence and word segmentation.
- /// - Each sentence is wrapped with sentence start (`
`) and end (``) tokens.
- /// - Words are prefixed with the word break character (`▁`).
- /// - Unknown tokens are replaced with the `` token.
- pub fn tokenize_sentences(&self, text: &str) -> Vec> {
- self.tokenize_sentences_iter(text)
- .map(|sentence_iter| sentence_iter.collect())
- .collect()
- }
-
- /// # Tokenizes a text into a flat sequence of BPE tokens.
- ///
- /// This function takes a string of text and returns a vector of tokens.
- /// It first tokenizes the text into sentences, then words, and finally into BPE tokens,
- /// flattening the result into a single sequence.
- ///
- /// ## Arguments
- ///
- /// * `text` - A string slice containing the text to be tokenized.
- ///
- /// ## Returns
- ///
- /// A `Vec`, where each `String` represents a token.
- ///
- /// ## Example
- ///
- /// ```
- /// use bpe_tokenizer::BytePairEncoder;
- ///
- /// let vocab = BytePairEncoder::new_from_str("hello\t1\nworld\t2").unwrap();
- /// let text = "Hello, world! How are you?";
- /// let tokenized = vocab.tokenize(text);
- /// ```
- ///
- /// ## Notes
- ///
- /// - This function uses Unicode-aware sentence and word segmentation.
- /// - Each sentence is wrapped with sentence start (``) and end (``) tokens.
- /// - Words are prefixed with the word break character (`▁`).
- /// - Unknown tokens are replaced with the `` token.
- pub fn tokenize(&self, text: &str) -> Vec {
- self.tokenize_iter(text).collect()
- }
-
- /// # Tokenizes a single sentence, adding sentence start and end markers.
- ///
- /// This function breaks down the tokenization process for a single sentence:
- /// 1. Adds a sentence start token.
- /// 2. Splits the sentence into words using Unicode-aware word segmentation.
- /// 3. Prepends each word with the word break character.
- /// 4. Tokenizes each word using the BPE vocabulary.
- /// 5. Adds a sentence end token.
- ///
- /// ## Arguments
- ///
- /// * `sentence` - A string slice containing a single sentence to be tokenized.
- ///
- /// ## Returns
- ///
- /// An iterator that yields `String`s representing the tokenized sentence,
- /// including start and end markers.
- ///
- /// ## Implementation Notes
- ///
- /// - Uses `unicode_words` for word segmentation to handle various Unicode scripts correctly.
- /// - Converts words to lowercase before tokenization to match the vocabulary.
- /// - Returns an iterator instead of a fully collected `Vec` to allow for
- /// more efficient tokenization and processing.
- fn tokenize_with_sentence_markers_iter<'a>(
- &'a self,
- sentence: &'a str,
- ) -> impl Iterator- + 'a {
- iter::once(SENTENCE_START_TOKEN.to_string())
- .chain(sentence.unicode_words().flat_map(move |word| {
- self.tokenize_word(&format!("{}{}", WORD_BREAK_CHAR, word.to_lowercase()))
- }))
- .chain(iter::once(SENTENCE_END_TOKEN.to_string()))
- }
-
- /// # Tokenizes a single word using the Byte Pair Encoding (BPE) algorithm.
- ///
- /// This function implements the core BPE tokenization logic:
- /// 1. If the word is empty, return an empty vector.
- /// 2. Convert the word to a vector of Unicode characters.
- /// 3. Iterate through possible substrings of the word, from longest to shortest.
- /// 4. For each substring length, find all matching tokens in the vocabulary.
- /// 5. Choose the matching token with the highest score in the vocabulary.
- /// 6. Split the word at the chosen token and recursively tokenize the parts before and after.
- /// 7. If no match is found, return the unknown token.
- ///
- /// ## Arguments
- ///
- /// * `text` - A string slice containing a single word to be tokenized.
- ///
- /// ## Returns
- ///
- /// A `Vec` containing the BPE tokens for the input word.
- ///
- /// ## Implementation Notes
- ///
- /// - The algorithm prioritizes longer matches over shorter ones.
- /// - In case of multiple matches of the same length, it chooses the one with the highest score.
- /// - The function is recursive, handling subwords created by splitting at a matched token.
- /// - If no match is found in the vocabulary, it returns the unknown token.
- fn tokenize_word(&self, text: &str) -> Vec {
- // Base case: If the input is empty, return an empty vector
- if text.is_empty() {
- return vec![];
- }
-
- // Convert the `text` to a Vec of `char`s to index by character rather than byte
- let word: Vec = text.chars().collect();
-
- // Look for the longest matching token in the vocabulary
- for len in (1..=word.len()).rev() {
- let mut matches = vec![];
- // Iterate over each possible start position for substrings of length `len`
- for start in 0..=(word.len() - len) {
- let end = start + len;
-
- // Extract candidate substring (convert chars[start..end] back to a &str)
- let candidate = &word[start..end].iter().collect::();
-
- // If we have an exact match, just store it for now
- if self.tokens.contains_key(candidate) {
- matches.push((candidate.to_string(), start, end));
- }
- }
-
- // If we got matches, choose the one with the highest score
- if !matches.is_empty() {
- let (candidate, start, end) = matches
- .into_iter()
- .max_by_key(|(candidate, _, _)| {
- self.tokens.get(candidate).copied().unwrap_or(isize::MIN)
- })
- .unwrap();
-
- // Recursively process the left part (before the match)
- let left: String = word[..start].iter().collect();
- let left_tokens = self.tokenize_word(&left);
-
- // The middle part is the matched token
- let middle = vec![candidate];
-
- // Recursively process the right part (after the match)
- let right: String = word[end..].iter().collect();
- let right_tokens = self.tokenize_word(&right);
-
- // Concatenate the result of left, middle, and right
- return [left_tokens, middle, right_tokens].concat();
- }
- }
-
- // If no match is found, return for the whole text
- vec![UNKNOWN_TOKEN.to_string()]
- }
-}
+mod constants;
+mod default_vocabs;
+mod errors;
+mod tokenizer;
+// tests
#[cfg(test)]
-mod tests {
- use super::*;
- use std::fs::File;
- use std::io::Write;
+mod tests;
- #[test]
- fn test_new_valid_file() {
- // Create a temporary file with valid content
- let file_path = "test_vocab.txt";
- let mut file = File::create(file_path).unwrap();
- file.write_all(b"hello\t1\nworld\t2").unwrap();
-
- // Test the new function
- let result = BytePairEncoder::new_from_file(file_path);
- assert!(result.is_ok());
-
- let vocab = result.unwrap();
- assert_eq!(vocab.tokens.len(), 2);
- assert_eq!(vocab.tokens.get("hello"), Some(&1));
- assert_eq!(vocab.tokens.get("world"), Some(&2));
-
- // Clean up the temporary file
- std::fs::remove_file(file_path).unwrap();
- }
-
- #[test]
- fn test_new_invalid_file() {
- // Test with a non-existent file
- let result = BytePairEncoder::new_from_file("non_existent_file.txt");
- assert!(result.is_err());
- assert!(matches!(
- result.unwrap_err(),
- BytePairEncoderError::InvalidFile(_)
- ));
- }
-
- #[test]
- fn test_new_from_str_valid_input() {
- let input = "hello\t1\nworld\t2\ntest\t3";
- let result = BytePairEncoder::new_from_str(input);
-
- assert!(result.is_ok());
- let vocab = result.unwrap();
-
- assert_eq!(vocab.tokens.len(), 3);
- assert_eq!(vocab.tokens.get("hello"), Some(&1));
- assert_eq!(vocab.tokens.get("world"), Some(&2));
- assert_eq!(vocab.tokens.get("test"), Some(&3));
- }
-
- #[test]
- fn test_new_from_str_empty_input() {
- let input = "";
- let result = BytePairEncoder::new_from_str(input);
-
- assert!(result.is_ok());
- let vocab = result.unwrap();
-
- assert_eq!(vocab.tokens.len(), 0);
- }
-
- #[test]
- fn test_new_from_str_invalid_format() {
- let input = "hello 1\nworld\t2";
- let result = BytePairEncoder::new_from_str(input);
-
- assert!(result.is_err());
- assert_eq!(
- result.unwrap_err(),
- BytePairEncoderError::InvalidVocabularyInput
- );
- }
-
- #[test]
- fn test_new_from_str_invalid_score() {
- let input = "hello\t1\nworld\tabc";
- let result = BytePairEncoder::new_from_str(input);
-
- assert!(result.is_err());
- assert_eq!(
- result.unwrap_err(),
- BytePairEncoderError::InvalidVocabularyInput
- );
- }
-
- #[test]
- #[cfg(feature = "default-small")]
- fn test_new_default_small_with_tokenization() {
- // Initialize the BytePairEncoder with the default small vocabulary
- let result = BytePairEncoder::new_default_small();
- assert!(result.is_ok());
-
- let vocab = result.unwrap();
- assert!(!vocab.tokens.is_empty());
-
- // Test tokenizing a phrase
- let text = "This is a test sentence.";
- let tokenized = vocab.tokenize(text);
-
- // Ensure we get the correct tokens. Since the vocabulary is pre-trained, ensure it returns sensible results.
- let expected_tokens = vec![
- "
".to_string(), // Sentence start
- "▁this".to_string(), // Word break for 'This'
- "▁is".to_string(), // Word break for 'This'
- "▁a".to_string(), // Word break for 'This'
- "▁test".to_string(), // Word break for 'This'
- "▁sent".to_string(), // Word break for 'This'
- "ence".to_string(), // Word break for 'This'
- "".to_string(), // Sentence end
- ];
-
- assert_eq!(tokenized, expected_tokens);
- }
-
- #[test]
- #[cfg(feature = "default-medium")]
- fn test_new_default_medium_with_tokenization() {
- // Initialize the BytePairEncoder with the default medium vocabulary
- let result = BytePairEncoder::new_default_medium();
- assert!(result.is_ok());
-
- let vocab = result.unwrap();
- assert!(!vocab.tokens.is_empty());
-
- // Test tokenizing a phrase
- let text = "This is a test sentence.";
- let tokenized = vocab.tokenize(text);
-
- // Ensure we get the correct tokens. Since the vocabulary is pre-trained, ensure it returns sensible results.
- let expected_tokens = vec![
- "".to_string(), // Sentence start
- "▁this".to_string(), // Word break for 'This'
- "▁is".to_string(), // Word break for 'This'
- "▁a".to_string(), // Word break for 'This'
- "▁test".to_string(), // Word break for 'This'
- "▁sentence".to_string(), // Word break for 'This'
- "".to_string(), // Sentence end
- ];
-
- assert_eq!(tokenized, expected_tokens);
- }
-
- #[test]
- #[cfg(feature = "default-large")]
- fn test_new_default_large_with_tokenization() {
- // Initialize the BytePairEncoder with the default large vocabulary
- let result = BytePairEncoder::new_default_large();
- assert!(result.is_ok());
-
- let vocab = result.unwrap();
- assert!(!vocab.tokens.is_empty());
-
- // Test tokenizing a phrase
- let text = "This is a test sentence.";
- let tokenized = vocab.tokenize(text);
-
- // Ensure we get the correct tokens. Since the vocabulary is pre-trained, ensure it returns sensible results.
- let expected_tokens = vec![
- "".to_string(), // Sentence start
- "▁this".to_string(), // Word break for 'This'
- "▁is".to_string(), // Word break for 'This'
- "▁a".to_string(), // Word break for 'This'
- "▁test".to_string(), // Word break for 'This'
- "▁sentence".to_string(), // Word break for 'This'
- "".to_string(), // Sentence end
- ];
-
- assert_eq!(tokenized, expected_tokens);
- }
-
- #[test]
- fn test_tokenize_sentences_iter() {
- let vocab_str = "hello\t1\nworld\t2\n▁\t3";
- let vocab = BytePairEncoder::new_from_str(vocab_str).unwrap();
-
- let text = "Hello, world! How are you?";
- let tokenized: Vec> = vocab
- .tokenize_sentences_iter(text)
- .map(|sentence_iter| sentence_iter.collect())
- .collect();
-
- assert_eq!(tokenized.len(), 2);
-
- assert_eq!(
- tokenized[0],
- vec![
- "".to_string(),
- "▁".to_string(),
- "hello".to_string(),
- "▁".to_string(),
- "world".to_string(),
- "".to_string(),
- ]
- );
-
- assert_eq!(
- tokenized[1],
- vec![
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "".to_string(),
- ]
- );
- }
-
- #[test]
- fn test_tokenize_sentences_iter_empty_input() {
- let vocab = BytePairEncoder::new_from_str("test\t1").unwrap();
- let text = "";
- let tokenized: Vec> = vocab
- .tokenize_sentences_iter(text)
- .map(|sentence_iter| sentence_iter.collect())
- .collect();
-
- assert_eq!(tokenized.len(), 0);
- }
-
- #[test]
- fn test_tokenize_sentences_iter_unicode() {
- let vocab_str = "こんにちは\t1\n世界\t2\n▁\t3";
- let vocab = BytePairEncoder::new_from_str(vocab_str).unwrap();
-
- let text = "こんにちは、世界!お元気ですか?";
- let tokenized: Vec> = vocab
- .tokenize_sentences_iter(text)
- .map(|sentence_iter| sentence_iter.collect())
- .collect();
-
- assert_eq!(tokenized.len(), 2);
-
- assert_eq!(
- tokenized[0],
- vec![
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "".to_string(),
- ]
- );
-
- assert_eq!(
- tokenized[1],
- vec![
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "".to_string(),
- ]
- );
- }
-
- #[test]
- fn test_tokenize_iter() {
- let vocab_str = "hello\t1\nworld\t2\n▁\t3";
- let vocab = BytePairEncoder::new_from_str(vocab_str).unwrap();
-
- let text = "Hello, world! How are you?";
- let tokenized: Vec = vocab.tokenize_iter(text).collect();
-
- assert_eq!(
- tokenized,
- vec![
- "".to_string(),
- "▁".to_string(),
- "hello".to_string(),
- "▁".to_string(),
- "world".to_string(),
- "".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "".to_string(),
- ]
- );
- }
-
- #[test]
- fn test_tokenize_iter_empty_input() {
- let vocab = BytePairEncoder::new_from_str("test\t1").unwrap();
- let text = "";
- let tokenized: Vec = vocab.tokenize_iter(text).collect();
-
- assert_eq!(tokenized.len(), 0);
- }
-
- #[test]
- fn test_tokenize_iter_unicode() {
- let vocab_str = "こんにちは\t1\n世界\t2\n▁\t3";
- let vocab = BytePairEncoder::new_from_str(vocab_str).unwrap();
-
- let text = "こんにちは、世界!お元気ですか?";
- let tokenized: Vec = vocab.tokenize_iter(text).collect();
-
- assert_eq!(
- tokenized,
- vec![
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "".to_string(),
- ]
- );
- }
-
- #[test]
- fn test_tokenize_sentences() {
- let vocab_str = "hello\t1\nworld\t2\n▁\t3";
- let vocab = BytePairEncoder::new_from_str(vocab_str).unwrap();
-
- let text = "Hello, world! How are you?";
- let tokenized = vocab.tokenize_sentences(text);
-
- assert_eq!(tokenized.len(), 2);
- assert_eq!(
- tokenized[0],
- vec![
- "".to_string(),
- "▁".to_string(),
- "hello".to_string(),
- "▁".to_string(),
- "world".to_string(),
- "".to_string(),
- ]
- );
- assert_eq!(
- tokenized[1],
- vec![
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "".to_string(),
- ]
- );
- }
-
- #[test]
- fn test_tokenize() {
- let vocab_str = "hello\t1\nworld\t2\n▁\t3";
- let vocab = BytePairEncoder::new_from_str(vocab_str).unwrap();
-
- let text = "Hello, world! How are you?";
- let tokenized = vocab.tokenize(text);
-
- assert_eq!(
- tokenized,
- vec![
- "".to_string(),
- "▁".to_string(),
- "hello".to_string(),
- "▁".to_string(),
- "world".to_string(),
- "".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "".to_string(),
- ]
- );
- }
-
- #[test]
- fn test_tokenize_empty_input() {
- let vocab = BytePairEncoder::new_from_str("test\t1").unwrap();
- let text = "";
-
- assert_eq!(vocab.tokenize_sentences(text), Vec::>::new());
- assert_eq!(vocab.tokenize(text), Vec::::new());
- }
-
- #[test]
- fn test_tokenize_with_sentence_markers() {
- let vocab_str = "hello\t1\nworld\t2\n▁\t3";
- let vocab = BytePairEncoder::new_from_str(vocab_str).unwrap();
-
- let sentence = "Hello, World!";
- let tokenized: Vec = vocab
- .tokenize_with_sentence_markers_iter(sentence)
- .collect();
-
- assert_eq!(
- tokenized,
- vec![
- "".to_string(),
- "▁".to_string(),
- "hello".to_string(),
- "▁".to_string(),
- "world".to_string(),
- "".to_string(),
- ]
- );
- }
-
- #[test]
- fn test_tokenize_with_sentence_markers_unicode() {
- let vocab_str = "こんにちは\t1\n世界\t2\n▁\t3";
- let vocab = BytePairEncoder::new_from_str(vocab_str).unwrap();
-
- let sentence = "こんにちは、世界!";
- let tokenized: Vec = vocab
- .tokenize_with_sentence_markers_iter(sentence)
- .collect();
-
- assert_eq!(
- tokenized,
- vec![
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "▁".to_string(),
- "".to_string(),
- "".to_string(),
- ]
- );
- }
-
- #[test]
- fn test_tokenize_word() {
- let vocab_str = "hell\t1\no\t2\nwo\t3\nrld\t4\n▁\t5";
- let vocab = BytePairEncoder::new_from_str(vocab_str).unwrap();
-
- assert_eq!(
- vocab.tokenize_word("▁hello"),
- vec!["▁".to_string(), "hell".to_string(), "o".to_string()]
- );
- assert_eq!(
- vocab.tokenize_word("▁world"),
- vec!["▁".to_string(), "wo".to_string(), "rld".to_string()]
- );
- assert_eq!(
- vocab.tokenize_word("▁unknown"),
- vec![
- "▁".to_string(),
- "".to_string(),
- "o".to_string(),
- "".to_string()
- ]
- );
- }
-
- #[test]
- fn test_tokenize_word_empty() {
- let vocab = BytePairEncoder::new_from_str("test\t1").unwrap();
- assert_eq!(vocab.tokenize_word(""), Vec::::new());
- }
-
- #[test]
- fn test_tokenize_word_partial_match() {
- let vocab_str = "partial\t1\npar\t2\ntial\t3\n▁\t4";
- let vocab = BytePairEncoder::new_from_str(vocab_str).unwrap();
-
- assert_eq!(
- vocab.tokenize_word("▁partial"),
- vec!["▁".to_string(), "partial".to_string()]
- );
- assert_eq!(
- vocab.tokenize_word("▁partially"),
- vec!["▁".to_string(), "partial".to_string(), "".to_string()]
- );
- }
-}
+// re-exports
+pub use errors::BytePairEncoderError;
+pub use tokenizer::BytePairEncoder;
diff --git a/src/tests.rs b/src/tests.rs
new file mode 100644
index 0000000..6eb962f
--- /dev/null
+++ b/src/tests.rs
@@ -0,0 +1,533 @@
+use std::{fs::File, io::Write};
+
+use crate::{BytePairEncoder, BytePairEncoderError};
+
+#[test]
+fn test_new_valid_file() {
+ // Create a temporary file with valid content
+ let file_path = "test_vocab.txt";
+ let mut file = File::create(file_path).unwrap();
+ file.write_all(b"hello\t1\nworld\t2").unwrap();
+
+ // Test the new function
+ let result = BytePairEncoder::new_from_file(file_path);
+ assert!(result.is_ok());
+
+ let vocab = result.unwrap();
+ assert_eq!(vocab.tokens.len(), 2);
+ assert_eq!(vocab.tokens.get("hello"), Some(&1));
+ assert_eq!(vocab.tokens.get("world"), Some(&2));
+
+ // Clean up the temporary file
+ std::fs::remove_file(file_path).unwrap();
+}
+
+#[test]
+fn test_new_invalid_file() {
+ // Test with a non-existent file
+ let result = BytePairEncoder::new_from_file("non_existent_file.txt");
+ assert!(result.is_err());
+ assert!(matches!(
+ result.unwrap_err(),
+ BytePairEncoderError::InvalidFile(_)
+ ));
+}
+
+#[test]
+fn test_new_from_str_valid_input() {
+ let input = "hello\t1\nworld\t2\ntest\t3";
+ let result = BytePairEncoder::new_from_str(input);
+
+ assert!(result.is_ok());
+ let vocab = result.unwrap();
+
+ assert_eq!(vocab.tokens.len(), 3);
+ assert_eq!(vocab.tokens.get("hello"), Some(&1));
+ assert_eq!(vocab.tokens.get("world"), Some(&2));
+ assert_eq!(vocab.tokens.get("test"), Some(&3));
+}
+
+#[test]
+fn test_new_from_str_empty_input() {
+ let input = "";
+ let result = BytePairEncoder::new_from_str(input);
+
+ assert!(result.is_ok());
+ let vocab = result.unwrap();
+
+ assert_eq!(vocab.tokens.len(), 0);
+}
+
+#[test]
+fn test_new_from_str_invalid_format() {
+ let input = "hello 1\nworld\t2";
+ let result = BytePairEncoder::new_from_str(input);
+
+ assert!(result.is_err());
+ assert_eq!(
+ result.unwrap_err(),
+ BytePairEncoderError::InvalidVocabularyInput
+ );
+}
+
+#[test]
+fn test_new_from_str_invalid_score() {
+ let input = "hello\t1\nworld\tabc";
+ let result = BytePairEncoder::new_from_str(input);
+
+ assert!(result.is_err());
+ assert_eq!(
+ result.unwrap_err(),
+ BytePairEncoderError::InvalidVocabularyInput
+ );
+}
+
+#[test]
+#[cfg(feature = "default-small")]
+fn test_new_default_small_with_tokenization() {
+ // Initialize the BytePairEncoder with the default small vocabulary
+ let result = BytePairEncoder::new_default_small();
+ assert!(result.is_ok());
+
+ let vocab = result.unwrap();
+ assert!(!vocab.tokens.is_empty());
+
+ // Test tokenizing a phrase
+ let text = "This is a test sentence.";
+ let tokenized = vocab.tokenize(text);
+
+ // Ensure we get the correct tokens. Since the vocabulary is pre-trained, ensure it returns sensible results.
+ let expected_tokens = vec![
+ "".to_string(), // Sentence start
+ "▁this".to_string(), // Word break for 'This'
+ "▁is".to_string(), // Word break for 'This'
+ "▁a".to_string(), // Word break for 'This'
+ "▁test".to_string(), // Word break for 'This'
+ "▁sent".to_string(), // Word break for 'This'
+ "ence".to_string(), // Word break for 'This'
+ "".to_string(), // Sentence end
+ ];
+
+ assert_eq!(tokenized, expected_tokens);
+}
+
+#[test]
+#[cfg(feature = "default-medium")]
+fn test_new_default_medium_with_tokenization() {
+ // Initialize the BytePairEncoder with the default medium vocabulary
+ let result = BytePairEncoder::new_default_medium();
+ assert!(result.is_ok());
+
+ let vocab = result.unwrap();
+ assert!(!vocab.tokens.is_empty());
+
+ // Test tokenizing a phrase
+ let text = "This is a test sentence.";
+ let tokenized = vocab.tokenize(text);
+
+ // Ensure we get the correct tokens. Since the vocabulary is pre-trained, ensure it returns sensible results.
+ let expected_tokens = vec![
+ "".to_string(), // Sentence start
+ "▁this".to_string(), // Word break for 'This'
+ "▁is".to_string(), // Word break for 'This'
+ "▁a".to_string(), // Word break for 'This'
+ "▁test".to_string(), // Word break for 'This'
+ "▁sentence".to_string(), // Word break for 'This'
+ "".to_string(), // Sentence end
+ ];
+
+ assert_eq!(tokenized, expected_tokens);
+}
+
+#[test]
+#[cfg(feature = "default-large")]
+fn test_new_default_large_with_tokenization() {
+ // Initialize the BytePairEncoder with the default large vocabulary
+ let result = BytePairEncoder::new_default_large();
+ assert!(result.is_ok());
+
+ let vocab = result.unwrap();
+ assert!(!vocab.tokens.is_empty());
+
+ // Test tokenizing a phrase
+ let text = "This is a test sentence.";
+ let tokenized = vocab.tokenize(text);
+
+ // Ensure we get the correct tokens. Since the vocabulary is pre-trained, ensure it returns sensible results.
+ let expected_tokens = vec![
+ "".to_string(), // Sentence start
+ "▁this".to_string(), // Word break for 'This'
+ "▁is".to_string(), // Word break for 'This'
+ "▁a".to_string(), // Word break for 'This'
+ "▁test".to_string(), // Word break for 'This'
+ "▁sentence".to_string(), // Word break for 'This'
+ "".to_string(), // Sentence end
+ ];
+
+ assert_eq!(tokenized, expected_tokens);
+}
+
+#[test]
+fn test_tokenize_sentences_iter() {
+ let vocab_str = "hello\t1\nworld\t2\n▁\t3";
+ let vocab = BytePairEncoder::new_from_str(vocab_str).unwrap();
+
+ let text = "Hello, world! How are you?";
+ let tokenized: Vec> = vocab
+ .tokenize_sentences_iter(text)
+ .map(|sentence_iter| sentence_iter.collect())
+ .collect();
+
+ assert_eq!(tokenized.len(), 2);
+
+ assert_eq!(
+ tokenized[0],
+ vec![
+ "".to_string(),
+ "▁".to_string(),
+ "hello".to_string(),
+ "▁".to_string(),
+ "world".to_string(),
+ "".to_string(),
+ ]
+ );
+
+ assert_eq!(
+ tokenized[1],
+ vec![
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "".to_string(),
+ ]
+ );
+}
+
+#[test]
+fn test_tokenize_sentences_iter_empty_input() {
+ let vocab = BytePairEncoder::new_from_str("test\t1").unwrap();
+ let text = "";
+ let tokenized: Vec> = vocab
+ .tokenize_sentences_iter(text)
+ .map(|sentence_iter| sentence_iter.collect())
+ .collect();
+
+ assert_eq!(tokenized.len(), 0);
+}
+
+#[test]
+fn test_tokenize_sentences_iter_unicode() {
+ let vocab_str = "こんにちは\t1\n世界\t2\n▁\t3";
+ let vocab = BytePairEncoder::new_from_str(vocab_str).unwrap();
+
+ let text = "こんにちは、世界!お元気ですか?";
+ let tokenized: Vec> = vocab
+ .tokenize_sentences_iter(text)
+ .map(|sentence_iter| sentence_iter.collect())
+ .collect();
+
+ assert_eq!(tokenized.len(), 2);
+
+ assert_eq!(
+ tokenized[0],
+ vec![
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "".to_string(),
+ ]
+ );
+
+ assert_eq!(
+ tokenized[1],
+ vec![
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "".to_string(),
+ ]
+ );
+}
+
+#[test]
+fn test_tokenize_iter() {
+ let vocab_str = "hello\t1\nworld\t2\n▁\t3";
+ let vocab = BytePairEncoder::new_from_str(vocab_str).unwrap();
+
+ let text = "Hello, world! How are you?";
+ let tokenized: Vec = vocab.tokenize_iter(text).collect();
+
+ assert_eq!(
+ tokenized,
+ vec![
+ "".to_string(),
+ "▁".to_string(),
+ "hello".to_string(),
+ "▁".to_string(),
+ "world".to_string(),
+ "".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "".to_string(),
+ ]
+ );
+}
+
+#[test]
+fn test_tokenize_iter_empty_input() {
+ let vocab = BytePairEncoder::new_from_str("test\t1").unwrap();
+ let text = "";
+ let tokenized: Vec = vocab.tokenize_iter(text).collect();
+
+ assert_eq!(tokenized.len(), 0);
+}
+
+#[test]
+fn test_tokenize_iter_unicode() {
+ let vocab_str = "こんにちは\t1\n世界\t2\n▁\t3";
+ let vocab = BytePairEncoder::new_from_str(vocab_str).unwrap();
+
+ let text = "こんにちは、世界!お元気ですか?";
+ let tokenized: Vec = vocab.tokenize_iter(text).collect();
+
+ assert_eq!(
+ tokenized,
+ vec![
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "".to_string(),
+ ]
+ );
+}
+
+#[test]
+fn test_tokenize_sentences() {
+ let vocab_str = "hello\t1\nworld\t2\n▁\t3";
+ let vocab = BytePairEncoder::new_from_str(vocab_str).unwrap();
+
+ let text = "Hello, world! How are you?";
+ let tokenized = vocab.tokenize_sentences(text);
+
+ assert_eq!(tokenized.len(), 2);
+ assert_eq!(
+ tokenized[0],
+ vec![
+ "".to_string(),
+ "▁".to_string(),
+ "hello".to_string(),
+ "▁".to_string(),
+ "world".to_string(),
+ "".to_string(),
+ ]
+ );
+ assert_eq!(
+ tokenized[1],
+ vec![
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "".to_string(),
+ ]
+ );
+}
+
+#[test]
+fn test_tokenize() {
+ let vocab_str = "hello\t1\nworld\t2\n▁\t3";
+ let vocab = BytePairEncoder::new_from_str(vocab_str).unwrap();
+
+ let text = "Hello, world! How are you?";
+ let tokenized = vocab.tokenize(text);
+
+ assert_eq!(
+ tokenized,
+ vec![
+ "".to_string(),
+ "▁".to_string(),
+ "hello".to_string(),
+ "▁".to_string(),
+ "world".to_string(),
+ "".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "".to_string(),
+ ]
+ );
+}
+
+#[test]
+fn test_tokenize_empty_input() {
+ let vocab = BytePairEncoder::new_from_str("test\t1").unwrap();
+ let text = "";
+
+ assert_eq!(vocab.tokenize_sentences(text), Vec::>::new());
+ assert_eq!(vocab.tokenize(text), Vec::::new());
+}
+
+#[test]
+fn test_tokenize_with_sentence_markers() {
+ let vocab_str = "hello\t1\nworld\t2\n▁\t3";
+ let vocab = BytePairEncoder::new_from_str(vocab_str).unwrap();
+
+ let sentence = "Hello, World!";
+ let tokenized: Vec = vocab
+ .tokenize_with_sentence_markers_iter(sentence)
+ .collect();
+
+ assert_eq!(
+ tokenized,
+ vec![
+ "".to_string(),
+ "▁".to_string(),
+ "hello".to_string(),
+ "▁".to_string(),
+ "world".to_string(),
+ "".to_string(),
+ ]
+ );
+}
+
+#[test]
+fn test_tokenize_with_sentence_markers_unicode() {
+ let vocab_str = "こんにちは\t1\n世界\t2\n▁\t3";
+ let vocab = BytePairEncoder::new_from_str(vocab_str).unwrap();
+
+ let sentence = "こんにちは、世界!";
+ let tokenized: Vec = vocab
+ .tokenize_with_sentence_markers_iter(sentence)
+ .collect();
+
+ assert_eq!(
+ tokenized,
+ vec![
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "▁".to_string(),
+ "".to_string(),
+ "".to_string(),
+ ]
+ );
+}
+
+#[test]
+fn test_tokenize_word() {
+ let vocab_str = "hell\t1\no\t2\nwo\t3\nrld\t4\n▁\t5";
+ let vocab = BytePairEncoder::new_from_str(vocab_str).unwrap();
+
+ assert_eq!(
+ vocab.tokenize_word("▁hello"),
+ vec!["▁".to_string(), "hell".to_string(), "o".to_string()]
+ );
+ assert_eq!(
+ vocab.tokenize_word("▁world"),
+ vec!["▁".to_string(), "wo".to_string(), "rld".to_string()]
+ );
+ assert_eq!(
+ vocab.tokenize_word("▁unknown"),
+ vec![
+ "▁".to_string(),
+ "".to_string(),
+ "o".to_string(),
+ "".to_string()
+ ]
+ );
+}
+
+#[test]
+fn test_tokenize_word_empty() {
+ let vocab = BytePairEncoder::new_from_str("test\t1").unwrap();
+ assert_eq!(vocab.tokenize_word(""), Vec::::new());
+}
+
+#[test]
+fn test_tokenize_word_partial_match() {
+ let vocab_str = "partial\t1\npar\t2\ntial\t3\n▁\t4";
+ let vocab = BytePairEncoder::new_from_str(vocab_str).unwrap();
+
+ assert_eq!(
+ vocab.tokenize_word("▁partial"),
+ vec!["▁".to_string(), "partial".to_string()]
+ );
+ assert_eq!(
+ vocab.tokenize_word("▁partially"),
+ vec!["▁".to_string(), "partial".to_string(), "".to_string()]
+ );
+}
diff --git a/src/tokenizer.rs b/src/tokenizer.rs
new file mode 100644
index 0000000..135f307
--- /dev/null
+++ b/src/tokenizer.rs
@@ -0,0 +1,522 @@
+use std::{collections::HashMap, fs, iter};
+
+use unicode_segmentation::UnicodeSegmentation;
+
+use crate::{
+ constants::*,
+ default_vocabs::{new_default, DefaultVocab},
+ BytePairEncoderError,
+};
+
+/// # Represents a Byte Pair Encoding (BPE) vocabulary used for tokenization.
+///
+/// This struct holds the mapping of tokens to their respective scores and provides methods for
+/// tokenizing text using the BPE algorithm.
+///
+/// The vocabulary is typically loaded from a file or string where each line
+/// contains a token and its score, separated by a tab character.
+///
+/// ## Example
+///
+/// ```
+/// use bpe_tokenizer::BytePairEncoder;
+///
+/// let vocab = BytePairEncoder::new_from_str("hello\t1\nworld\t2").unwrap();
+/// let tokenized = vocab.tokenize("Hello, world!");
+/// ```
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct BytePairEncoder {
+ /// # A mapping of tokens to their respective scores.
+ ///
+ /// In BPE, tokens with higher scores are typically more common and are preferred during the
+ /// tokenization process.
+ pub(crate) tokens: HashMap,
+}
+
+impl BytePairEncoder {
+ /// # Creates a new `BytePairEncoder` from a file containing token-score pairs.
+ ///
+ /// This function reads the contents of the file specified by `file_path` and constructs
+ /// a `BytePairEncoder` from it. The file should contain token-score pairs, with each pair
+ /// on a separate line and the token and score separated by a tab character (`\t`).
+ ///
+ /// ## Input Format
+ ///
+ /// The file is expected to follow this format:
+ ///
+ /// ```text
+ /// \t\n
+ /// ```
+ ///
+ /// Each line should consist of:
+ /// * A token (a string) followed by a tab character (`\t`)
+ /// * A score (an integer) as either a positive or negative value.
+ ///
+ /// Example lines from the file:
+ ///
+ /// ```text
+ /// 0
+ /// ▁t -0
+ /// ▁the -4
+ /// ```
+ ///
+ /// ## Arguments
+ ///
+ /// * `file_path` - A string slice that holds the path to the file containing token-score pairs.
+ ///
+ /// ## Returns
+ ///
+ /// * `Result` - A Result containing the created `BytePairEncoder` if successful,
+ /// or a `BytePairEncoderError` if there was an error reading the file or parsing its contents.
+ ///
+ /// ## Errors
+ ///
+ /// This function will return an error if:
+ /// * The file cannot be read (returns `BytePairEncoderError::InvalidFile`)
+ /// * The file contents are not in the expected format (returns `BytePairEncoderError::InvalidVocabularyInput`)
+ ///
+ /// ## Example
+ ///
+ /// ```
+ /// use bpe_tokenizer::BytePairEncoder;
+ ///
+ /// let vocab = BytePairEncoder::new_from_file("path/to/vocabulary/file.txt");
+ /// ```
+ pub fn new_from_file(file_path: &str) -> Result {
+ Self::new_from_str(
+ fs::read_to_string(file_path)
+ .map_err(|_| BytePairEncoderError::InvalidFile(file_path.to_string()))?
+ .as_ref(),
+ )
+ }
+
+ /// # Creates a new `BytePairEncoder` from a string containing token-score pairs.
+ ///
+ /// This function parses the input string to construct a `BytePairEncoder`. The input should
+ /// contain token-score pairs, with each pair on a separate line and the token and score
+ /// separated by a tab character (`\t`).
+ ///
+ /// ## Input Format
+ ///
+ /// The string must follow this format:
+ ///
+ /// ```text
+ /// \t\n
+ /// ```
+ ///
+ /// Each line in the string should consist of:
+ /// * A token (a string) followed by a tab character (`\t`)
+ /// * A score (an integer) as either a positive or negative value.
+ ///
+ /// For example:
+ ///
+ /// ```text
+ /// hello 1
+ /// world 2
+ /// ▁the -4
+ /// ```
+ ///
+ /// ## Arguments
+ ///
+ /// * `input` - A string slice that holds the token-score pairs.
+ ///
+ /// ## Returns
+ ///
+ /// * `Result` - A Result containing the created `BytePairEncoder` if successful,
+ /// or a `BytePairEncoderError` if there was an error parsing the input.
+ ///
+ /// ## Errors
+ ///
+ /// This function will return `BytePairEncoderError::InvalidVocabularyInput` if:
+ /// * A line doesn't contain a tab character to separate token and score.
+ /// * The score cannot be parsed as an `isize`.
+ ///
+ /// ## Example
+ ///
+ /// ```
+ /// use bpe_tokenizer::BytePairEncoder;
+ ///
+ /// let input = "hello\t1\nworld\t2";
+ /// let vocab = BytePairEncoder::new_from_str(input).unwrap();
+ /// ```
+ pub fn new_from_str(input: &str) -> Result {
+ let mut tokens = HashMap::new();
+
+ for line in input.lines() {
+ let (token, score_str) = match line.split_once('\t') {
+ Some(pair) => pair,
+ None => return Err(BytePairEncoderError::InvalidVocabularyInput),
+ };
+ let score = match score_str.parse::() {
+ Ok(score) => score,
+ Err(_) => return Err(BytePairEncoderError::InvalidVocabularyInput),
+ };
+ tokens.insert(token.to_string(), score);
+ }
+
+ Ok(BytePairEncoder { tokens })
+ }
+
+ /// # Creates a new `BytePairEncoder` with a default small vocabulary size (100,000 tokens).
+ ///
+ /// This function constructs a `BytePairEncoder` using a pre-trained multilingual vocabulary
+ /// that supports 275 languages. The vocabulary is sourced from the
+ /// [BPEmb](https://github.com/bheinzerling/bpemb) project, licensed under MIT. The small-sized
+ /// vocabulary file consists of 100,000 tokens, allowing for highly compressed tokenization
+ /// suitable for tasks with limited memory constraints.
+ ///
+ /// ## Returns
+ ///
+ /// A `Result`, constructing the `BytePairEncoder` on successful
+ /// vocabulary loading, or a corresponding error if initialization fails.
+ ///
+ /// ## Example
+ ///
+ /// ```
+ /// # #[cfg(feature = "default-small")] {
+ /// use bpe_tokenizer::BytePairEncoder;
+ ///
+ /// let encoder = BytePairEncoder::new_default_small().unwrap();
+ /// # }
+ /// ```
+ ///
+ /// ## Note
+ ///
+ /// This is only enabled when the `default-small` feature is enabled in Cargo.toml.
+ ///
+ /// ```toml
+ /// [dependencies]
+ /// bpe-tokenizer = { version = " Result {
+ new_default(DefaultVocab::Small)
+ }
+
+ /// # Creates a new `BytePairEncoder` with a default medium vocabulary size (320,000 tokens).
+ ///
+ /// This function constructs a `BytePairEncoder` using a pre-trained multilingual vocabulary
+ /// that supports 275 languages. The vocabulary is sourced from the
+ /// [BPEmb](https://github.com/bheinzerling/bpemb) project, licensed under MIT. The
+ /// medium-sized vocabulary file consists of 320,000 tokens, offering a balance between token
+ /// coverage and memory efficiency, making it suitable for a wide variety of NLP tasks.
+ ///
+ /// ## Returns
+ ///
+ /// A `Result`, constructing the `BytePairEncoder` on successful
+ /// vocabulary loading, or a corresponding error if initialization fails.
+ ///
+ /// ## Example
+ ///
+ /// ```
+ /// # #[cfg(feature = "default-medium")] {
+ /// use bpe_tokenizer::BytePairEncoder;
+ ///
+ /// let encoder = BytePairEncoder::new_default_medium().unwrap();
+ /// # }
+ /// ```
+ ///
+ /// ## Note
+ ///
+ /// This is only enabled when the `default-medium` feature is enabled in Cargo.toml.
+ ///
+ /// ```toml
+ /// [dependencies]
+ /// bpe-tokenizer = { version = " Result {
+ new_default(DefaultVocab::Medium)
+ }
+
+ /// # Creates a new `BytePairEncoder` with a default large vocabulary size (1,000,000 tokens).
+ ///
+ /// This function constructs a `BytePairEncoder` using a pre-trained multilingual vocabulary
+ /// that supports 275 languages. The vocabulary is sourced from the
+ /// [BPEmb](https://github.com/bheinzerling/bpemb) project, licensed under MIT. The large-sized
+ /// vocabulary consists of 1,000,000 tokens, providing maximum coverage for detailed language
+ /// representation, especially useful in applications requiring high granularity.
+ ///
+ /// ## Returns
+ ///
+ /// A `Result`, constructing the `BytePairEncoder` on successful
+ /// vocabulary loading, or a corresponding error if initialization fails.
+ ///
+ /// ## Example
+ ///
+ /// ```
+ /// # #[cfg(feature = "default-large")] {
+ /// use bpe_tokenizer::BytePairEncoder;
+ ///
+ /// let encoder = BytePairEncoder::new_default_large().unwrap();
+ /// # }
+ /// ```
+ ///
+ /// ## Note
+ ///
+ /// This is only enabled when the `default-large` feature is enabled in Cargo.toml.
+ ///
+ /// ```toml
+ /// [dependencies]
+ /// bpe-tokenizer = { version = " Result {
+ new_default(DefaultVocab::Large)
+ }
+
+ /// # Tokenizes a text into sentences, then words, and finally into BPE tokens.
+ ///
+ /// This function takes a string of text and returns an iterator that yields
+ /// vectors of tokens, where each vector represents a tokenized sentence.
+ ///
+ /// ## Arguments
+ ///
+ /// * `text` - A string slice containing the text to be tokenized.
+ ///
+ /// ## Returns
+ ///
+ /// An iterator that yields `Vec`, where each `Vec` represents
+ /// a tokenized sentence.
+ ///
+ /// ## Example
+ ///
+ /// ```
+ /// use bpe_tokenizer::BytePairEncoder;
+ ///
+ /// let vocab = BytePairEncoder::new_from_str("hello\t1\nworld\t2").unwrap();
+ /// let text = "Hello, world! How are you?";
+ /// let tokenized: Vec> = vocab
+ /// .tokenize_sentences_iter(text)
+ /// .map(|sentence_iter| sentence_iter.collect()) // Collect each inner iterator into a Vec
+ /// .collect(); // Then collect everything into Vec>
+ /// ```
+ ///
+ /// ## Notes
+ ///
+ /// - This function uses Unicode-aware sentence and word segmentation.
+ /// - Each sentence is wrapped with sentence start (``) and end (``) tokens.
+ /// - Words are prefixed with the word break character (`▁`).
+ /// - Unknown tokens are replaced with the `` token.
+ pub fn tokenize_sentences_iter<'a>(
+ &'a self,
+ text: &'a str,
+ ) -> impl Iterator- + 'a> + 'a {
+ UnicodeSegmentation::unicode_sentences(text)
+ .map(move |sentence| self.tokenize_with_sentence_markers_iter(sentence))
+ }
+
+ /// # Tokenizes a text into a flat sequence of BPE tokens.
+ ///
+ /// This function takes a string of text and returns an iterator that yields
+ /// individual tokens. It first tokenizes the text into sentences, then words,
+ /// and finally into BPE tokens, flattening the result into a single sequence.
+ ///
+ /// ## Arguments
+ ///
+ /// * `text` - A string slice containing the text to be tokenized.
+ ///
+ /// ## Returns
+ ///
+ /// An iterator that yields `String`, where each `String` represents a token.
+ ///
+ /// ## Example
+ ///
+ /// ```
+ /// use bpe_tokenizer::BytePairEncoder;
+ ///
+ /// let vocab = BytePairEncoder::new_from_str("hello\t1\nworld\t2").unwrap();
+ /// let text = "Hello, world! How are you?";
+ /// let tokenized: Vec = vocab.tokenize_iter(text).collect();
+ /// ```
+ ///
+ /// ## Notes
+ ///
+ /// - This function uses Unicode-aware sentence and word segmentation.
+ /// - Each sentence is wrapped with sentence start (`
`) and end (``) tokens.
+ /// - Words are prefixed with the word break character (`▁`).
+ /// - Unknown tokens are replaced with the `` token.
+ pub fn tokenize_iter<'a>(&'a self, text: &'a str) -> impl Iterator- + 'a {
+ self.tokenize_sentences_iter(text).flatten()
+ }
+
+ /// # Tokenizes a text into sentences, then words, and finally into BPE tokens.
+ ///
+ /// This function takes a string of text and returns a vector of tokenized sentences,
+ /// where each sentence is represented as a vector of tokens.
+ ///
+ /// ## Arguments
+ ///
+ /// * `text` - A string slice containing the text to be tokenized.
+ ///
+ /// ## Returns
+ ///
+ /// A `Vec>`, where each inner `Vec` represents a tokenized sentence.
+ ///
+ /// ## Example
+ ///
+ /// ```
+ /// use bpe_tokenizer::BytePairEncoder;
+ ///
+ /// let vocab = BytePairEncoder::new_from_str("hello\t1\nworld\t2").unwrap();
+ /// let text = "Hello, world! How are you?";
+ /// let tokenized = vocab.tokenize_sentences(text);
+ /// ```
+ ///
+ /// ## Notes
+ ///
+ /// - This function uses Unicode-aware sentence and word segmentation.
+ /// - Each sentence is wrapped with sentence start (`
`) and end (``) tokens.
+ /// - Words are prefixed with the word break character (`▁`).
+ /// - Unknown tokens are replaced with the `` token.
+ pub fn tokenize_sentences(&self, text: &str) -> Vec> {
+ self.tokenize_sentences_iter(text)
+ .map(|sentence_iter| sentence_iter.collect())
+ .collect()
+ }
+
+ /// # Tokenizes a text into a flat sequence of BPE tokens.
+ ///
+ /// This function takes a string of text and returns a vector of tokens.
+ /// It first tokenizes the text into sentences, then words, and finally into BPE tokens,
+ /// flattening the result into a single sequence.
+ ///
+ /// ## Arguments
+ ///
+ /// * `text` - A string slice containing the text to be tokenized.
+ ///
+ /// ## Returns
+ ///
+ /// A `Vec`, where each `String` represents a token.
+ ///
+ /// ## Example
+ ///
+ /// ```
+ /// use bpe_tokenizer::BytePairEncoder;
+ ///
+ /// let vocab = BytePairEncoder::new_from_str("hello\t1\nworld\t2").unwrap();
+ /// let text = "Hello, world! How are you?";
+ /// let tokenized = vocab.tokenize(text);
+ /// ```
+ ///
+ /// ## Notes
+ ///
+ /// - This function uses Unicode-aware sentence and word segmentation.
+ /// - Each sentence is wrapped with sentence start (``) and end (``) tokens.
+ /// - Words are prefixed with the word break character (`▁`).
+ /// - Unknown tokens are replaced with the `` token.
+ pub fn tokenize(&self, text: &str) -> Vec {
+ self.tokenize_iter(text).collect()
+ }
+
+ /// # Tokenizes a single sentence, adding sentence start and end markers.
+ ///
+ /// This function breaks down the tokenization process for a single sentence:
+ /// 1. Adds a sentence start token.
+ /// 2. Splits the sentence into words using Unicode-aware word segmentation.
+ /// 3. Prepends each word with the word break character.
+ /// 4. Tokenizes each word using the BPE vocabulary.
+ /// 5. Adds a sentence end token.
+ ///
+ /// ## Arguments
+ ///
+ /// * `sentence` - A string slice containing a single sentence to be tokenized.
+ ///
+ /// ## Returns
+ ///
+ /// An iterator that yields `String`s representing the tokenized sentence,
+ /// including start and end markers.
+ ///
+ /// ## Implementation Notes
+ ///
+ /// - Uses `unicode_words` for word segmentation to handle various Unicode scripts correctly.
+ /// - Converts words to lowercase before tokenization to match the vocabulary.
+ /// - Returns an iterator instead of a fully collected `Vec` to allow for
+ /// more efficient tokenization and processing.
+ pub(crate) fn tokenize_with_sentence_markers_iter<'a>(
+ &'a self,
+ sentence: &'a str,
+ ) -> impl Iterator- + 'a {
+ iter::once(SENTENCE_START_TOKEN.to_string())
+ .chain(sentence.unicode_words().flat_map(move |word| {
+ self.tokenize_word(&format!("{}{}", WORD_BREAK_CHAR, word.to_lowercase()))
+ }))
+ .chain(iter::once(SENTENCE_END_TOKEN.to_string()))
+ }
+
+ /// # Tokenizes a single word using the Byte Pair Encoding (BPE) algorithm.
+ ///
+ /// This function implements the core BPE tokenization logic:
+ /// 1. If the word is empty, return an empty vector.
+ /// 2. Convert the word to a vector of Unicode characters.
+ /// 3. Iterate through possible substrings of the word, from longest to shortest.
+ /// 4. For each substring length, find all matching tokens in the vocabulary.
+ /// 5. Choose the matching token with the highest score in the vocabulary.
+ /// 6. Split the word at the chosen token and recursively tokenize the parts before and after.
+ /// 7. If no match is found, return the unknown token.
+ ///
+ /// ## Arguments
+ ///
+ /// * `text` - A string slice containing a single word to be tokenized.
+ ///
+ /// ## Returns
+ ///
+ /// A `Vec` containing the BPE tokens for the input word.
+ ///
+ /// ## Implementation Notes
+ ///
+ /// - The algorithm prioritizes longer matches over shorter ones.
+ /// - In case of multiple matches of the same length, it chooses the one with the highest score.
+ /// - The function is recursive, handling subwords created by splitting at a matched token.
+ /// - If no match is found in the vocabulary, it returns the unknown token.
+ pub(crate) fn tokenize_word(&self, text: &str) -> Vec {
+ // Base case: If the input is empty, return an empty vector
+ if text.is_empty() {
+ return vec![];
+ }
+
+ // Convert the `text` to a Vec of `char`s to index by character rather than byte
+ let word: Vec = text.chars().collect();
+
+ // Look for the longest matching token in the vocabulary
+ for len in (1..=word.len()).rev() {
+ let mut matches = vec![];
+ // Iterate over each possible start position for substrings of length `len`
+ for start in 0..=(word.len() - len) {
+ let end = start + len;
+
+ // Extract candidate substring (convert chars[start..end] back to a &str)
+ let candidate = &word[start..end].iter().collect::();
+
+ // If we have an exact match, just store it for now
+ if self.tokens.contains_key(candidate) {
+ matches.push((candidate.to_string(), start, end));
+ }
+ }
+
+ // If we got matches, choose the one with the highest score
+ if !matches.is_empty() {
+ let (candidate, start, end) = matches
+ .into_iter()
+ .max_by_key(|(candidate, _, _)| {
+ self.tokens.get(candidate).copied().unwrap_or(isize::MIN)
+ })
+ .unwrap();
+
+ // Recursively process the left part (before the match)
+ let left: String = word[..start].iter().collect();
+ let left_tokens = self.tokenize_word(&left);
+
+ // The middle part is the matched token
+ let middle = vec![candidate];
+
+ // Recursively process the right part (after the match)
+ let right: String = word[end..].iter().collect();
+ let right_tokens = self.tokenize_word(&right);
+
+ // Concatenate the result of left, middle, and right
+ return [left_tokens, middle, right_tokens].concat();
+ }
+ }
+
+ // If no match is found, return for the whole text
+ vec![UNKNOWN_TOKEN.to_string()]
+ }
+}