mirror of
https://github.com/run-llama/LlamaIndexTS.git
synced 2026-07-16 07:14:29 -04:00
fix: sentence splitter must not trim whitespaces (#2046)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@llamaindex/core": patch
|
||||
---
|
||||
|
||||
Fix: split sentences must not trim whitespaces
|
||||
@@ -17,7 +17,7 @@ export class SentenceWindowNodeParser extends NodeParser<TextNode[]> {
|
||||
windowSize: number;
|
||||
windowMetadataKey: string;
|
||||
originalTextMetadataKey: string;
|
||||
sentenceSplitter: TextSplitterFn = splitBySentenceTokenizer();
|
||||
sentenceSplitter: TextSplitterFn = splitBySentenceTokenizer([], true);
|
||||
idGenerator: () => string = () => randomUUID();
|
||||
|
||||
constructor(params?: z.input<typeof sentenceWindowNodeParserSchema>) {
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
declare class SentenceTokenizer {
|
||||
constructor(abbreviations?: string[]);
|
||||
constructor(abbreviations?: string[], trimSentences?: boolean);
|
||||
tokenize(text: string): string[];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,24 @@
|
||||
/*
|
||||
Copyright (c) 2024, Hugo W.L. ter Doest
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __commonJS = (cb, mod) =>
|
||||
function __require() {
|
||||
@@ -30,32 +51,47 @@ var require_tokenizer = __commonJS({
|
||||
// lib/natural/tokenizers/sentence_tokenizer.js
|
||||
var require_sentence_tokenizer = __commonJS({
|
||||
"lib/natural/tokenizers/sentence_tokenizer.js"(exports, module) {
|
||||
var Tokenizer = require_tokenizer();
|
||||
var NUM = "NUMBER";
|
||||
var DELIM = "DELIM";
|
||||
var URI = "URI";
|
||||
var ABBREV = "ABBREV";
|
||||
var DEBUG = false;
|
||||
const Tokenizer = require_tokenizer();
|
||||
|
||||
// Strings that will be used to create placeholders
|
||||
const NUM = "NUMBER";
|
||||
const DELIM = "DELIM";
|
||||
const URI = "URI";
|
||||
const ABBREV = "ABBREV";
|
||||
|
||||
const DEBUG = false;
|
||||
|
||||
function generateUniqueCode(base, index) {
|
||||
// Surround the placeholder with {{}} to prevent shorter numbers to be recognized
|
||||
// in larger numbers
|
||||
return `{{${base}_${index}}}`;
|
||||
}
|
||||
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
var SentenceTokenizer = class extends Tokenizer {
|
||||
constructor(abbreviations) {
|
||||
|
||||
class SentenceTokenizer extends Tokenizer {
|
||||
constructor(abbreviations, trimSentences) {
|
||||
super();
|
||||
if (abbreviations) {
|
||||
this.abbreviations = abbreviations;
|
||||
} else {
|
||||
this.abbreviations = [];
|
||||
}
|
||||
if (trimSentences === undefined) {
|
||||
this.trimSentences = true;
|
||||
} else {
|
||||
this.trimSentences = trimSentences;
|
||||
}
|
||||
this.replacementMap = null;
|
||||
this.replacementCounter = 0;
|
||||
}
|
||||
|
||||
replaceUrisWithPlaceholders(text) {
|
||||
const urlPattern =
|
||||
/(https?:\/\/\S+|www\.\S+|ftp:\/\/\S+|(mailto:)?[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}|file:\/\/\S+)/gi;
|
||||
|
||||
const modifiedText = text.replace(urlPattern, (match) => {
|
||||
const placeholder = generateUniqueCode(
|
||||
URI,
|
||||
@@ -64,8 +100,10 @@ var require_sentence_tokenizer = __commonJS({
|
||||
this.replacementMap.set(placeholder, match);
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
return modifiedText;
|
||||
}
|
||||
|
||||
replaceAbbreviations(text) {
|
||||
if (this.abbreviations.length === 0) {
|
||||
return text;
|
||||
@@ -79,9 +117,14 @@ var require_sentence_tokenizer = __commonJS({
|
||||
this.replacementMap.set(code, match);
|
||||
return code;
|
||||
});
|
||||
|
||||
return replacedText;
|
||||
}
|
||||
|
||||
replaceDelimitersWithPlaceholders(text) {
|
||||
// Regular expression for sentence delimiters optionally followed by a bracket or quote
|
||||
// Multiple delimiters with spaces in between are allowed
|
||||
// The expression makes sure that the sentence delimiter group ends with a sentence delimiter
|
||||
const delimiterPattern = /([.?!… ]*)([.?!…])(["'”’)}\]]?)/g;
|
||||
const modifiedText = text.replace(
|
||||
delimiterPattern,
|
||||
@@ -94,32 +137,42 @@ var require_sentence_tokenizer = __commonJS({
|
||||
return placeholder;
|
||||
},
|
||||
);
|
||||
|
||||
return modifiedText;
|
||||
}
|
||||
|
||||
splitOnPlaceholders(text, placeholders) {
|
||||
if (this.delimiterMap.size === 0) {
|
||||
return [text];
|
||||
}
|
||||
|
||||
const keys = Array.from(this.delimiterMap.keys());
|
||||
const pattern = new RegExp(`(${keys.map(escapeRegExp).join("|")})`);
|
||||
const parts = text.split(pattern);
|
||||
|
||||
const sentences = [];
|
||||
for (let i = 0; i < parts.length; i += 2) {
|
||||
const sentence = parts[i];
|
||||
const placeholder = parts[i + 1] || "";
|
||||
sentences.push(sentence + placeholder);
|
||||
}
|
||||
|
||||
return sentences;
|
||||
}
|
||||
|
||||
replaceNumbersWithCode(text) {
|
||||
// Regular expression to match numbers, including decimal points and commas
|
||||
const numberPattern = /\b\d{1,3}(?:,\d{3})*(?:\.\d+)?\b/g;
|
||||
|
||||
const replacedText = text.replace(numberPattern, (match) => {
|
||||
const code = generateUniqueCode(NUM, this.replacementCounter++);
|
||||
this.replacementMap.set(code, match);
|
||||
return code;
|
||||
});
|
||||
|
||||
return replacedText;
|
||||
}
|
||||
|
||||
revertReplacements(text) {
|
||||
let originalText = text;
|
||||
for (const [
|
||||
@@ -129,16 +182,20 @@ var require_sentence_tokenizer = __commonJS({
|
||||
const pattern = new RegExp(escapeRegExp(placeholder), "g");
|
||||
originalText = originalText.replace(pattern, replacement);
|
||||
}
|
||||
|
||||
return originalText;
|
||||
}
|
||||
|
||||
revertDelimiters(text) {
|
||||
let originalText = text;
|
||||
for (const [placeholder, replacement] of this.delimiterMap.entries()) {
|
||||
const pattern = new RegExp(escapeRegExp(placeholder), "g");
|
||||
originalText = originalText.replace(pattern, replacement);
|
||||
}
|
||||
|
||||
return originalText;
|
||||
}
|
||||
|
||||
tokenize(text) {
|
||||
this.replacementCounter = 0;
|
||||
this.replacementMap = /* @__PURE__ */ new Map();
|
||||
@@ -148,32 +205,43 @@ var require_sentence_tokenizer = __commonJS({
|
||||
"---Start of sentence tokenization-----------------------",
|
||||
);
|
||||
DEBUG && console.log("Original input: >>>" + text + "<<<");
|
||||
// Replace abbreviations
|
||||
const result1 = this.replaceAbbreviations(text);
|
||||
DEBUG &&
|
||||
console.log(
|
||||
"Phase 1: replacing abbreviations: " + JSON.stringify(result1),
|
||||
);
|
||||
|
||||
// Replace URIs
|
||||
const result2 = this.replaceUrisWithPlaceholders(result1);
|
||||
DEBUG &&
|
||||
console.log("Phase 2: replacing URIs: " + JSON.stringify(result2));
|
||||
|
||||
// Replace delimiters followed by optional quotes, brackets, and braces
|
||||
const result3 = this.replaceNumbersWithCode(result2);
|
||||
DEBUG &&
|
||||
console.log(
|
||||
"Phase 3: replacing numbers with placeholders: " +
|
||||
JSON.stringify(result3),
|
||||
);
|
||||
|
||||
// Replace delimiters followed by optional quotes, brackets, and braces
|
||||
const result4 = this.replaceDelimitersWithPlaceholders(result3);
|
||||
DEBUG &&
|
||||
console.log(
|
||||
"Phase 4: replacing delimiters with placeholders: " +
|
||||
JSON.stringify(result4),
|
||||
);
|
||||
|
||||
// Split on placeholders for sentence delimiters
|
||||
const sentences = this.splitOnPlaceholders(result4);
|
||||
DEBUG &&
|
||||
console.log(
|
||||
"Phase 5: splitting into sentences on placeholders: " +
|
||||
JSON.stringify(sentences),
|
||||
);
|
||||
|
||||
// Replace back all abbreviations, URIs, and delimiters
|
||||
const newSentences = sentences.map((s) => {
|
||||
const s1 = this.revertReplacements(s);
|
||||
return this.revertDelimiters(s1);
|
||||
@@ -183,13 +251,17 @@ var require_sentence_tokenizer = __commonJS({
|
||||
"Phase 6: replacing back abbreviations, URIs, numbers and delimiters: " +
|
||||
JSON.stringify(newSentences),
|
||||
);
|
||||
|
||||
const trimmedSentences = this.trim(newSentences);
|
||||
DEBUG &&
|
||||
console.log(
|
||||
"Phase 7: trimming array of empty sentences: " +
|
||||
JSON.stringify(trimmedSentences),
|
||||
);
|
||||
const trimmedSentences2 = trimmedSentences.map((sent) => sent.trim());
|
||||
|
||||
const trimmedSentences2 = trimmedSentences.map((sent) =>
|
||||
this.trimSentences ? sent.trim() : sent,
|
||||
);
|
||||
DEBUG &&
|
||||
console.log(
|
||||
"Phase 8: trimming sentences from surrounding whitespace: " +
|
||||
@@ -213,9 +285,10 @@ var require_sentence_tokenizer = __commonJS({
|
||||
console.log(
|
||||
"---------------------------------------------------------",
|
||||
);
|
||||
|
||||
return trimmedSentences2;
|
||||
}
|
||||
};
|
||||
}
|
||||
module.exports = SentenceTokenizer;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -37,13 +37,17 @@ export const splitByChar = (): TextSplitterFn => {
|
||||
|
||||
export const splitBySentenceTokenizer = (
|
||||
extraAbbreviations: string[] | undefined = [],
|
||||
trimSentences: boolean = false,
|
||||
): TextSplitterFn => {
|
||||
const tokenizer = new SentenceTokenizer([
|
||||
...abbreviations.english,
|
||||
...abbreviations.spanish,
|
||||
// Add the extra abbreviations provided by the user, e.g. for business-specific context
|
||||
...extraAbbreviations,
|
||||
]);
|
||||
const tokenizer = new SentenceTokenizer(
|
||||
[
|
||||
...abbreviations.english,
|
||||
...abbreviations.spanish,
|
||||
// Add the extra abbreviations provided by the user, e.g. for business-specific context
|
||||
...extraAbbreviations,
|
||||
],
|
||||
trimSentences,
|
||||
);
|
||||
return (text: string) => {
|
||||
try {
|
||||
return tokenizer.tokenize(text);
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
splitBySentenceTokenizer,
|
||||
} from "@llamaindex/core/node-parser";
|
||||
import { Document } from "@llamaindex/core/schema";
|
||||
import { Tokenizers, tokenizers } from "@llamaindex/env/tokenizers";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
describe("sentence splitter", () => {
|
||||
@@ -75,20 +76,26 @@ describe("sentence splitter", () => {
|
||||
expect(splits).toEqual(["This is a sentence. This is another sentence."]);
|
||||
});
|
||||
|
||||
test("overall split long text", () => {
|
||||
test("keep the space, if there's no split", () => {
|
||||
const text = "The short sentence. The long long long long sentence.";
|
||||
const sentenceSplitter = new SentenceSplitter({
|
||||
chunkSize: 10,
|
||||
chunkSize: 1024,
|
||||
chunkOverlap: 0,
|
||||
});
|
||||
const splits = sentenceSplitter.splitText(
|
||||
"The first short sentence. The first long long long sentence. The second short sentence. The second long long long sentence.",
|
||||
);
|
||||
expect(splits).toEqual([
|
||||
"The first short sentence.",
|
||||
"The first long long long sentence.",
|
||||
"The second short sentence.",
|
||||
"The second long long long sentence.",
|
||||
]);
|
||||
const splits = sentenceSplitter.splitText(text);
|
||||
expect(splits).toEqual([text]);
|
||||
});
|
||||
|
||||
test("split at tokenizer limit", () => {
|
||||
const tokenizer = tokenizers.tokenizer(Tokenizers.CL100K_BASE);
|
||||
const text = "The short sentence. The long long long long sentence.";
|
||||
const sentenceSplitter = new SentenceSplitter({
|
||||
tokenizer,
|
||||
chunkSize: tokenizer.encode(text).length,
|
||||
chunkOverlap: 0,
|
||||
});
|
||||
const splits = sentenceSplitter.splitText(text + " " + text);
|
||||
expect(splits).toEqual([text, text]);
|
||||
});
|
||||
|
||||
test("doesn't split decimals", () => {
|
||||
|
||||
Reference in New Issue
Block a user