句子窗口节点解析器:
node_parser = SentenceWindowNodeParser.from_defaults(
window_size=3,
window_metadata_key="window",
original_text_metadata_key="original_text",
)
源码查看如何切割的
默认是 split_by_sentence_tokenizer 方法
class SentenceWindowNodeParser(NodeParser):
"""Sentence window node parser.
Splits a document into Nodes, with each node being a sentence.
Each node contains a window from the surrounding sentences in the metadata.
Args:
sentence_splitter (Optional[Callable]): splits text into sentences
include_metadata (bool): whether to include metadata in nodes
include_prev_next_rel (bool): whether to include prev/next relationships
"""
sentence_splitter: Callable[[str], List[str]] = Field(
default_factory=split_by_sentence_tokenizer,
description="The text splitter to use when splitting documents.",
exclude=True,
)
window_size: int = Field(
default=DEFAULT_WINDOW_SIZE,
description="The number of sentences on each side of a sentence to capture.",
gt=0,
)
window_metadata_key: str = Field(
default=DEFAULT_WINDOW_METADATA_KEY,
description="The metadata key to store the sentence window under.",
)
original_text_metadata_key: str = Field(
default=DEFAULT_OG_TEXT_METADATA_KEY,
description="The metadata key to store the original sentence in.",
)
@classmethod
def class_name(cls) -> str:
return "SentenceWindowNodeParser"
@classmethod
def from_defaults(
cls,
sentence_splitter: Optional[Callable[[str], List[str]]] = None,
window_size: int = DEFAULT_WINDOW_SIZE,
window_metadata_key: str = DEFAULT_WINDOW_METADATA_KEY,
original_text_metadata_key: str = DEFAULT_OG_TEXT_METADATA_KEY,
include_metadata: bool = True,
include_prev_next_rel: bool = True,
callback_manager: Optional[CallbackManager] = None,
id_func: Optional[Callable[[int, Document], str]] = None,
) -> "SentenceWindowNodeParser":
callback_manager = callback_manager or CallbackManager([])
sentence_splitter = sentence_splitter or split_by_sentence_tokenizer()
id_func = id_func or default_id_func
return cls(
sentence_splitter=sentence_splitter,
window_size=window_size,
window_metadata_key=window_metadata_key,
original_text_metadata_key=original_text_metadata_key,
include_metadata=include_metadata,
include_prev_next_rel=include_prev_next_rel,
callback_manager=callback_manager,
id_func=id_func,
)
def _parse_nodes(
self,
nodes: Sequence[BaseNode],
show_progress: bool = False,
**kwargs: Any,
) -> List[BaseNode]:
"""Parse document into nodes."""
all_nodes: List[BaseNode] = []
nodes_with_progress = get_tqdm_iterable(nodes, show_progress, "Parsing nodes")
for node in nodes_with_progress:
nodes = self.build_window_nodes_from_documents([node])
all_nodes.extend(nodes)
return all_nodes
def build_window_nodes_from_documents(
self, documents: Sequence[Document]
) -> List[BaseNode]:
"""Build window nodes from documents."""
all_nodes: List[BaseNode] = []
for doc in documents:
text = doc.text
text_splits = self.sentence_splitter(text)
nodes = build_nodes_from_splits(
text_splits,
doc,
id_func=self.id_func,
)
# add window to each node
for i, node in enumerate(nodes):
window_nodes = nodes[
max(0, i - self.window_size) : min(
i + self.window_size + 1, len(nodes)
)
]
node.metadata[self.window_metadata_key] = " ".join(
[n.text for n in window_nodes]
)
node.metadata[self.original_text_metadata_key] = node.text
# exclude window metadata from embed and llm
node.excluded_embed_metadata_keys.extend(
[self.window_metadata_key, self.original_text_metadata_key]
)
node.excluded_llm_metadata_keys.extend(
[self.window_metadata_key, self.original_text_metadata_key]
)
all_nodes.extend(nodes)
return all_nodes
utils中的split_by_sentence_tokenizer
用nltk中的PunktSentenceTokenizer的span_tokenize 方法
def split_by_sentence_tokenizer() -> Callable[[str], List[str]]:
import nltk
tokenizer = nltk.tokenize.PunktSentenceTokenizer()
return lambda text: split_by_sentence_tokenizer_internal(text, tokenizer)
def split_by_sentence_tokenizer_internal(text: str, tokenizer) -> List[str]:
"""Get the spans and then return the sentences.
Using the start index of each span
Instead of using end, use the start of the next span if available
"""
spans = list(tokenizer.span_tokenize(text))
sentences = []
for i, span in enumerate(spans):
start = span[0]
if i < len(spans) - 1:
end = spans[i + 1][0]
else:
end = len(text)
sentences.append(text[start:end])
return sentences
nltk中的PunktSentenceTokenizer的span_tokenize
这段代码定义了一个名为 span_tokenize
的方法,用于将输入的文本分割成句子,并生成每个句子的起始和结束位置(即 span)。以下是对代码的详细解释:
方法签名
def span_tokenize(
self, text: str, realign_boundaries: bool = True
) -> Iterator[Tuple[int, int]]:
- self: 这是一个实例方法,
self
表示调用该方法的实例对象。 - text: 输入的文本字符串,类型为
str
。 - realign_boundaries: 一个布尔值,默认为
True
。如果设置为True
,则会对分割边界进行重新对齐。 - 返回值: 返回一个生成器,生成每个句子的起始和结束位置的元组
(start, end)
。
方法实现
"""
Given a text, generates (start, end) spans of sentences
in the text.
"""
slices = self._slices_from_text(text)
if realign_boundaries:
slices = self._realign_boundaries(text, slices)
for sentence in slices:
yield (sentence.start, sentence.stop)
-
获取初始分割切片:
slices = self._slices_from_text(text)
调用
self._slices_from_text(text)
方法,该方法根据某种逻辑(可能是基于标点符号或其他分隔符)将文本分割成多个切片(即潜在的句子)。每个切片可能是一个slice
对象,包含了句子的起始和结束位置。 -
重新对齐边界(可选):
if realign_boundaries: slices = self._realign_boundaries(text, slices)
如果
realign_boundaries
为True
,则调用self._realign_boundaries(text, slices)
方法,该方法会对分割边界进行重新对齐,以确保分割的准确性。例如,可能会处理一些特殊情况,如标点符号紧跟在单词后面等。 -
生成句子 span:
for sentence in slices: yield (sentence.start, sentence.stop)
遍历调整后的切片列表
slices
,对于每个切片(即句子),生成一个包含起始和结束位置的元组(sentence.start, sentence.stop)
。这些元组通过yield
关键字逐个返回,形成一个生成器。
总结
这个 span_tokenize
方法的主要功能是将输入的文本分割成句子,并生成每个句子的起始和结束位置。通过可选的边界重新对齐步骤,可以提高分割的准确性。最终,该方法返回一个生成器,逐个生成每个句子的 span。
_slices_from_text
这段代码定义了一个名为 _slices_from_text
的私有方法,用于将输入的文本分割成潜在的句子切片。以下是对代码的详细解释:
方法签名
def _slices_from_text(self, text: str) -> Iterator[slice]:
- self: 这是一个实例方法,
self
表示调用该方法的实例对象。 - text: 输入的文本字符串,类型为
str
。 - 返回值: 返回一个生成器,生成每个潜在句子的
slice
对象。
方法实现
last_break = 0
for match, context in self._match_potential_end_contexts(text):
if self.text_contains_sentbreak(context):
yield slice(last_break, match.end())
if match.group("next_tok"):
# next sentence starts after whitespace
last_break = match.start("next_tok")
else:
# next sentence starts at following punctuation
last_break = match.end()
# The last sentence should not contain trailing whitespace.
yield slice(last_break, len(text.rstrip()))
-
初始化变量:
last_break = 0
last_break
变量用于记录上一个句子的结束位置。初始值为 0,表示文本的起始位置。 -
遍历潜在的句子结束位置:
for match, context in self._match_potential_end_contexts(text):
调用
self._match_potential_end_contexts(text)
方法,该方法返回文本中潜在的句子结束位置的匹配对象和上下文。match
是一个匹配对象,context
是匹配的上下文。 -
检查是否包含句子分隔符:
if self.text_contains_sentbreak(context):
调用
self.text_contains_sentbreak(context)
方法,检查上下文是否包含句子分隔符(如句号、问号、感叹号等)。 -
生成句子切片:
yield slice(last_break, match.end())
如果上下文包含句子分隔符,则生成一个
slice
对象,表示从last_break
到match.end()
的切片,即当前句子的起始和结束位置。 -
更新
last_break
:if match.group("next_tok"): # next sentence starts after whitespace last_break = match.start("next_tok") else: # next sentence starts at following punctuation last_break = match.end()
根据匹配结果更新
last_break
:- 如果匹配对象包含
"next_tok"
组(表示下一个句子的起始位置在空白字符之后),则将last_break
更新为match.start("next_tok")
。 - 否则,将
last_break
更新为match.end()
,即当前匹配的结束位置。
- 如果匹配对象包含
-
生成最后一个句子切片:
yield slice(last_break, len(text.rstrip()))
最后生成一个切片,表示从
last_break
到文本的最后一个非空白字符的位置。这样可以确保最后一个句子不包含尾随的空白字符。
总结
这个 _slices_from_text
方法的主要功能是将输入的文本分割成潜在的句子切片。通过遍历潜在的句子结束位置,并检查上下文是否包含句子分隔符,生成每个句子的 slice
对象。最终,该方法返回一个生成器,逐个生成每个句子的切片。
_match_potential_end_contexts
这段代码定义了一个名为 _match_potential_end_contexts
的私有方法,用于在输入文本中找到潜在的句子结束位置的匹配对象,并返回这些匹配对象及其上下文。以下是对代码的详细解释:
方法签名
def _match_potential_end_contexts(self, text: str) -> Iterator[Tuple[Match, str]]:
- self: 这是一个实例方法,
self
表示调用该方法的实例对象。 - text: 输入的文本字符串,类型为
str
。 - 返回值: 返回一个生成器,生成每个潜在句子结束位置的匹配对象及其上下文的元组
(Match, str)
。
方法实现
previous_slice = slice(0, 0)
previous_match = None
for match in self._lang_vars.period_context_re().finditer(text):
# Get the slice of the previous word
before_text = text[previous_slice.stop : match.start()]
index_after_last_space = self._get_last_whitespace_index(before_text)
if index_after_last_space:
# + 1 to exclude the space itself
index_after_last_space += previous_slice.stop + 1
else:
index_after_last_space = previous_slice.start
prev_word_slice = slice(index_after_last_space, match.start())
# If the previous slice does not overlap with this slice, then
# we can yield the previous match and slice. If there is an overlap,
# then we do not yield the previous match and slice.
if previous_match and previous_slice.stop <= prev_word_slice.start:
yield (
previous_match,
text[previous_slice]
+ previous_match.group()
+ previous_match.group("after_tok"),
)
previous_match = match
previous_slice = prev_word_slice
# Yield the last match and context, if it exists
if previous_match:
yield (
previous_match,
text[previous_slice]
+ previous_match.group()
+ previous_match.group("after_tok"),
)
-
初始化变量:
previous_slice = slice(0, 0) previous_match = None
previous_slice
用于记录上一个匹配的切片,初始值为slice(0, 0)
,表示空切片。previous_match
用于记录上一个匹配对象,初始值为None
。 -
遍历潜在的句子结束位置:
for match in self._lang_vars.period_context_re().finditer(text):
调用
self._lang_vars.period_context_re().finditer(text)
方法,该方法返回文本中潜在的句子结束位置的匹配对象。 -
获取上一个单词的切片:
before_text = text[previous_slice.stop : match.start()] index_after_last_space = self._get_last_whitespace_index(before_text) if index_after_last_space: # + 1 to exclude the space itself index_after_last_space += previous_slice.stop + 1 else: index_after_last_space = previous_slice.start prev_word_slice = slice(index_after_last_space, match.start())
- 从
previous_slice.stop
到match.start()
获取before_text
。 - 调用
self._get_last_whitespace_index(before_text)
方法,找到before_text
中最后一个空白字符的索引。 - 根据
index_after_last_space
计算prev_word_slice
,表示上一个单词的切片。
- 从
-
检查切片是否重叠:
if previous_match and previous_slice.stop <= prev_word_slice.start: yield ( previous_match, text[previous_slice] + previous_match.group() + previous_match.group("after_tok"), )
- 如果
previous_match
存在且previous_slice.stop
小于等于prev_word_slice.start
,则表示上一个匹配和当前匹配不重叠,可以生成上一个匹配及其上下文。 - 生成一个元组,包含上一个匹配对象和其上下文。
- 如果
-
更新
previous_match
和previous_slice
:previous_match = match previous_slice = prev_word_slice
更新
previous_match
和previous_slice
为当前匹配对象和切片。 -
生成最后一个匹配及其上下文:
if previous_match: yield ( previous_match, text[previous_slice] + previous_match.group() + previous_match.group("after_tok"), )
如果
previous_match
存在,生成最后一个匹配对象及其上下文。
总结
这个 _match_potential_end_contexts
方法的主要功能是在输入文本中找到潜在的句子结束位置的匹配对象,并返回这些匹配对象及其上下文。通过遍历潜在的句子结束位置,获取上一个单词的切片,并检查切片是否重叠,生成每个匹配对象及其上下文的元组。最终,该方法返回一个生成器,逐个生成每个匹配对象及其上下文。
这段代码定义了一个名为 text_contains_sentbreak
的方法,用于判断给定的文本是否包含句子分隔符。以下是对代码的详细解释:
text_contains_sentbreak
方法签名
def text_contains_sentbreak(self, text: str) -> bool:
- self: 这是一个实例方法,
self
表示调用该方法的实例对象。 - text: 输入的文本字符串,类型为
str
。 - 返回值: 返回一个布尔值,表示文本是否包含句子分隔符。
方法实现
"""
Returns True if the given text includes a sentence break.
"""
found = False # used to ignore last token
for tok in self._annotate_tokens(self._tokenize_words(text)):
if found:
return True
if tok.sentbreak:
found = True
return False
-
初始化变量:
found = False # used to ignore last token
found
变量用于标记是否找到了句子分隔符。初始值为False
。 -
遍历标记:
for tok in self._annotate_tokens(self._tokenize_words(text)):
调用
self._tokenize_words(text)
方法将文本分割成单词,然后调用self._annotate_tokens
方法对这些单词进行注释,返回一个包含注释信息的标记列表。 -
检查句子分隔符:
if found: return True if tok.sentbreak: found = True
- 如果
found
为True
,表示已经找到了句子分隔符,直接返回True
。 - 如果当前标记
tok
包含句子分隔符(即tok.sentbreak
为True
),则将found
设置为True
。
- 如果
-
返回结果:
return False
如果遍历完所有标记后仍未找到句子分隔符,则返回
False
。
总结
这个 text_contains_sentbreak
方法的主要功能是判断给定的文本是否包含句子分隔符。通过遍历文本中的标记,检查每个标记是否包含句子分隔符,如果找到则返回 True
,否则返回 False
。