Current section

Files

Jump to
langchain lib text_splitter recursive_character_text_splitter.ex
Raw

lib/text_splitter/recursive_character_text_splitter.ex

defmodule LangChain.TextSplitter.RecursiveCharacterTextSplitter do
@moduledoc """
The `RecursiveCharacterTextSplitter` is the recommended spliltter for generic text.
It splits the text based on a list of characters.
It uses each of these characters sequentially, until the text is split
into small enough chunks. The default list is `["\n\n", "\n", " ", ""]`.
The purpose is to prepare text for processing
by large language models with limited context windows,
or where a shorter context window is desired.
The main characterstinc of this splitter is that tries to keep
paragraphs, sentences or code functions together as long as possible.
`LangChain.TextSplitter.LanguageSeparators` provide separator lists for some programming and markup languages.
To use these Separators, it's recommended to set the `is_separator_regex` option to `true`.
How it works:
- It splits the text at the first specified `separator` characters
from the given `separators` list.
It uses `LangChain.TextSplitter.CharacterTextSplitter` to do so.
- For each of the above splits, it calls itself recursively
using the tail of the `separators` list.
A `RecursiveCharacterTextSplitter` is defined using a schema.
* `separators` - List of string that split a given text.
The default list is `["\n\n", "\n", " ", ""]`.
* `chunk_size` - Integer number of characters that a chunk should have.
* `chunk_overlap` - Integer number of characters that two consecutive chunks should share.
* `keep_separator` - Either `:discard_separator`, `:start` or `:end`. If `nil`, the separator is discarded from the output chunks. `:start` and `:end` keep the separator at the start or end of the output chunks. Defaults to `start`.
* `is_separator_regex` - Boolean defaulting to `false`. If `true`, the `separator` string is not escaped. Defaults to `false`
"""
use Ecto.Schema
import Ecto.Changeset
alias LangChain.LangChainError
alias LangChain.TextSplitter
alias LangChain.TextSplitter.CharacterTextSplitter
alias __MODULE__
@primary_key false
embedded_schema do
field :separators, {:array, :string}, default: ["\n\n", "\n", " ", ""]
field :chunk_size, :integer
field :chunk_overlap, :integer
field :keep_separator, Ecto.Enum,
values: [:discard_separator, :start, :end],
default: :start
field :is_separator_regex, :boolean, default: false
end
@type t :: %RecursiveCharacterTextSplitter{}
@update_fields [
:separators,
:chunk_size,
:chunk_overlap,
:keep_separator,
:is_separator_regex
]
@create_fields @update_fields
@doc """
Build a new RecursiveCharcterTextSplitter and return an `:ok`/`:error` tuple with the result.
"""
def new(attrs \\ %{}) do
%TextSplitter.RecursiveCharacterTextSplitter{}
|> cast(attrs, @create_fields, empty_values: [nil])
|> apply_action(:insert)
end
@doc """
Build a new RecursiveCharacterTextSplitter and return it or raise an error if invalid.
"""
def new!(attrs \\ %{}) do
case new(attrs) do
{:ok, character_text_spliiter} ->
character_text_spliiter
{:error, changeset} ->
raise LangChainError, changeset
end
end
@doc """
Splits text recursively based on a list of characters.
By default, the `separators` characters are kept at the start
iex> split_tags = [",", "."]
iex> base_params = %{chunk_size: 10, chunk_overlap: 0, separators: split_tags}
iex> query = "Apple,banana,orange and tomato."
iex> splitter = RecursiveCharacterTextSplitter.new!(base_params)
iex> splitter |> RecursiveCharacterTextSplitter.split_text(query)
["Apple", ",banana", ",orange and tomato", "."]
We can keep the separator at the end of a chunk, providing the
`keep_separator: :end` option:
iex> split_tags = [",", "."]
iex> base_params = %{chunk_size: 10, chunk_overlap: 0, separators: split_tags, keep_separator: :end}
iex> query = "Apple,banana,orange and tomato."
iex> splitter = RecursiveCharacterTextSplitter.new!(base_params)
iex> splitter |> RecursiveCharacterTextSplitter.split_text(query)
["Apple,", "banana,", "orange and tomato."]
See `LangChain.TextSplitter.CharacterTextSplitter` for the usage of the different options.
`LanguageSeparators` provides `separators` for multiple
programming and markdown languages.
To use these Separators, it's recommended to set the `is_separator_regex` option to `true`.
To split Python code:
iex> python_code = "
...>def hello_world():
...> print('Hello, World')
...>
...>
...># Call the function
...>hello_world()"
iex> splitter =
...> RecursiveCharacterTextSplitter.new!(%{
...> separators: LanguageSeparators.python(),
...> keep_separator: :start,
...> is_separator_regex: :true,
...> chunk_size: 16,
...> chunk_overlap: 0})
iex> splitter |> RecursiveCharacterTextSplitter.split_text(python_code)
["def", "hello_world():", "print('Hello,", "World')", "# Call the", "function", "hello_world()"]
"""
def split_text(%RecursiveCharacterTextSplitter{} = text_splitter, text) do
new_separators =
text_splitter.separators
|> Enum.map(fn s ->
if text_splitter.is_separator_regex,
do: s,
else: s |> Regex.escape()
end)
|> Enum.drop_while(fn s -> not (s |> Regex.compile!() |> Regex.match?(text)) end)
separator =
if Enum.count(new_separators) > 0,
do: List.first(new_separators),
else:
text_splitter.separators
|> List.last()
|> Regex.escape()
character_text_splitter =
CharacterTextSplitter.new!(
text_splitter
|> Map.from_struct()
|> Map.delete(:separators)
|> Map.put(:is_separator_regex, true)
|> Map.put_new(:separator, separator)
)
acc = %{good_splits: [], final_chunks: []}
splits =
text
|> CharacterTextSplitter.split_text_with_regex(character_text_splitter)
merge_separator =
if not (text_splitter.keep_separator == :discard_separator),
do: "",
else: character_text_splitter.separator
recursive_splits =
splits
|> Enum.reduce(acc, fn split, acc ->
if String.length(split) < text_splitter.chunk_size do
%{acc | good_splits: acc.good_splits ++ [split]}
else
acc =
if Enum.count(acc.good_splits) > 0 do
merged_text =
TextSplitter.merge_splits(
acc.good_splits,
character_text_splitter,
merge_separator
)
%{good_splits: [], final_chunks: acc.final_chunks ++ merged_text}
else
acc
end
if Enum.count(new_separators) <= 1 do
%{acc | final_chunks: acc.final_chunks ++ [split]}
else
new_recursive_splitter =
%{
(text_splitter
|> Map.from_struct())
| separators: new_separators |> Enum.drop(1),
is_separator_regex: true
}
|> RecursiveCharacterTextSplitter.new!()
other_info =
new_recursive_splitter
|> RecursiveCharacterTextSplitter.split_text(split)
%{acc | final_chunks: acc.final_chunks ++ other_info}
end
end
end)
if Enum.count(recursive_splits.good_splits) > 0 do
merged_text =
recursive_splits.good_splits
|> TextSplitter.merge_splits(character_text_splitter, merge_separator)
recursive_splits.final_chunks ++ merged_text
else
recursive_splits.final_chunks
end
|> Enum.filter(fn x -> not (x |> is_nil()) end)
end
end