mirror of
https://github.com/lancedb/lancedb.git
synced 2026-10-12 14:52:26 +00:00
Merge latest main into gatekeeper/fix-4392-1
Integrate the upstream tokenizer default fix after validating the combined remote error behavior and Python binding.
This commit is contained in:
@@ -321,8 +321,8 @@ def tokenize(
|
||||
language: str = "English",
|
||||
max_token_length: Optional[int] = 40,
|
||||
lower_case: bool = True,
|
||||
stem: bool = True,
|
||||
remove_stop_words: bool = True,
|
||||
stem: Optional[bool] = None,
|
||||
remove_stop_words: Optional[bool] = None,
|
||||
custom_stop_words: Optional[List[str]] = None,
|
||||
ascii_folding: bool = True,
|
||||
ngram_min_length: int = 3,
|
||||
@@ -333,6 +333,8 @@ def tokenize(
|
||||
|
||||
This does not require an FTS index. The tokenizer options match
|
||||
:class:`lancedb.index.FTS`. ``custom_stop_words`` accepts a list of strings.
|
||||
When ``stem`` or ``remove_stop_words`` is None, the base tokenizer's default
|
||||
applies: False for ``code`` and ``ngram``, True otherwise.
|
||||
"""
|
||||
|
||||
return _tokenize(
|
||||
|
||||
@@ -58,8 +58,8 @@ def tokenize(
|
||||
language: str = "English",
|
||||
max_token_length: Optional[int] = 40,
|
||||
lower_case: bool = True,
|
||||
stem: bool = True,
|
||||
remove_stop_words: bool = True,
|
||||
stem: Optional[bool] = None,
|
||||
remove_stop_words: Optional[bool] = None,
|
||||
custom_stop_words: Optional[List[str]] = None,
|
||||
ascii_folding: bool = True,
|
||||
ngram_min_length: int = 3,
|
||||
|
||||
@@ -138,6 +138,7 @@ class FTS:
|
||||
- "simple": Splits text by whitespace and punctuation.
|
||||
- "whitespace": Split text by whitespace, but not punctuation.
|
||||
- "raw": No tokenization. The entire text is treated as a single token.
|
||||
- "code": Tokenizes source code and identifiers.
|
||||
- "ngram": N-gram tokenizer for substring-style matching.
|
||||
- "icu": ICU dictionary-based word segmentation.
|
||||
- "icu/split": ICU segmentation with simple-style delimiter splitting.
|
||||
@@ -151,12 +152,16 @@ class FTS:
|
||||
ignored.
|
||||
lower_case : bool, default True
|
||||
Whether to convert the token to lower case. This makes queries case-insensitive.
|
||||
stem : bool, default True
|
||||
stem : bool, optional
|
||||
Whether to stem the token. Stemming reduces words to their root form.
|
||||
For example, in English "running" and "runs" would both be reduced to "run".
|
||||
remove_stop_words : bool, default True
|
||||
``None`` uses the base tokenizer's default: False for ``code`` and
|
||||
``ngram``, True otherwise.
|
||||
remove_stop_words : bool, optional
|
||||
Whether to remove stop words. Stop words are common words that are often
|
||||
removed from text before indexing. For example, in English "the" and "and".
|
||||
``None`` uses the base tokenizer's default: False for ``code`` and
|
||||
``ngram``, True otherwise.
|
||||
custom_stop_words : list of str, optional
|
||||
Custom words replace the built-in language stop words
|
||||
and only take effect when ``remove_stop_words`` is True. ``None`` uses
|
||||
@@ -197,8 +202,8 @@ class FTS:
|
||||
language: str = "English"
|
||||
max_token_length: Optional[int] = 40
|
||||
lower_case: bool = True
|
||||
stem: bool = True
|
||||
remove_stop_words: bool = True
|
||||
stem: Optional[bool] = None
|
||||
remove_stop_words: Optional[bool] = None
|
||||
ascii_folding: bool = True
|
||||
ngram_min_length: int = 3
|
||||
ngram_max_length: int = 3
|
||||
|
||||
@@ -380,8 +380,8 @@ class RemoteTable(Table):
|
||||
language: str = "English",
|
||||
max_token_length: Optional[int] = 40,
|
||||
lower_case: bool = True,
|
||||
stem: bool = True,
|
||||
remove_stop_words: bool = True,
|
||||
stem: Optional[bool] = None,
|
||||
remove_stop_words: Optional[bool] = None,
|
||||
custom_stop_words: Optional[List[str]] = None,
|
||||
ascii_folding: bool = True,
|
||||
ngram_min_length: int = 3,
|
||||
|
||||
@@ -1612,8 +1612,8 @@ class Table(ABC):
|
||||
language: str = "English",
|
||||
max_token_length: Optional[int] = 40,
|
||||
lower_case: bool = True,
|
||||
stem: bool = True,
|
||||
remove_stop_words: bool = True,
|
||||
stem: Optional[bool] = None,
|
||||
remove_stop_words: Optional[bool] = None,
|
||||
custom_stop_words: Optional[List[str]] = None,
|
||||
ascii_folding: bool = True,
|
||||
ngram_min_length: int = 3,
|
||||
@@ -1663,6 +1663,7 @@ class Table(ABC):
|
||||
- "simple": Splits text by whitespace and punctuation.
|
||||
- "whitespace": Split text by whitespace, but not punctuation.
|
||||
- "raw": No tokenization. The entire text is treated as a single token.
|
||||
- "code": Tokenizes source code and identifiers.
|
||||
- "ngram": N-Gram tokenizer.
|
||||
- "icu": ICU dictionary-based word segmentation.
|
||||
- "icu/split": ICU segmentation with simple-style delimiter splitting.
|
||||
@@ -1677,12 +1678,16 @@ class Table(ABC):
|
||||
lower_case : bool, default True
|
||||
Whether to convert the token to lower case. This makes queries
|
||||
case-insensitive.
|
||||
stem : bool, default True
|
||||
stem : bool, optional
|
||||
Whether to stem the token. Stemming reduces words to their root form.
|
||||
For example, in English "running" and "runs" would both be reduced to "run".
|
||||
remove_stop_words : bool, default True
|
||||
``None`` uses the base tokenizer's default: False for ``code`` and
|
||||
``ngram``, True otherwise.
|
||||
remove_stop_words : bool, optional
|
||||
Whether to remove stop words. Stop words are common words that are often
|
||||
removed from text before indexing. For example, in English "the" and "and".
|
||||
``None`` uses the base tokenizer's default: False for ``code`` and
|
||||
``ngram``, True otherwise.
|
||||
custom_stop_words : list of str, optional
|
||||
Custom words that replace the built-in language stop words. ``None``
|
||||
uses the built-in list; an empty list explicitly uses no stop words.
|
||||
@@ -3820,8 +3825,8 @@ class LanceTable(Table):
|
||||
language: str = "English",
|
||||
max_token_length: Optional[int] = 40,
|
||||
lower_case: bool = True,
|
||||
stem: bool = True,
|
||||
remove_stop_words: bool = True,
|
||||
stem: Optional[bool] = None,
|
||||
remove_stop_words: Optional[bool] = None,
|
||||
custom_stop_words: Optional[List[str]] = None,
|
||||
ascii_folding: bool = True,
|
||||
ngram_min_length: int = 3,
|
||||
|
||||
@@ -895,6 +895,78 @@ def test_tokenize_uses_simple_index_tokenizer(mem_db: DBConnection):
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("legacy", [False, True])
|
||||
@pytest.mark.parametrize(
|
||||
("base_tokenizer", "options", "expected_filters", "expected_tokens"),
|
||||
[
|
||||
(
|
||||
"code",
|
||||
{},
|
||||
(False, False),
|
||||
[("running", 0), ("in", 1), ("cafe", 2)],
|
||||
),
|
||||
(
|
||||
"code",
|
||||
{"stem": None, "remove_stop_words": None},
|
||||
(False, False),
|
||||
[("running", 0), ("in", 1), ("cafe", 2)],
|
||||
),
|
||||
(
|
||||
"code",
|
||||
{"stem": True},
|
||||
(True, False),
|
||||
[("run", 0), ("in", 1), ("cafe", 2)],
|
||||
),
|
||||
(
|
||||
"code",
|
||||
{"remove_stop_words": True},
|
||||
(False, True),
|
||||
[("running", 0), ("cafe", 2)],
|
||||
),
|
||||
(
|
||||
"code",
|
||||
{"stem": True, "remove_stop_words": True},
|
||||
(True, True),
|
||||
[("run", 0), ("cafe", 2)],
|
||||
),
|
||||
(
|
||||
"simple",
|
||||
{},
|
||||
(True, True),
|
||||
[("run", 0), ("cafe", 2)],
|
||||
),
|
||||
(
|
||||
"simple",
|
||||
{"stem": False, "remove_stop_words": False},
|
||||
(False, False),
|
||||
[("running", 0), ("in", 1), ("cafe", 2)],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_fts_tokenizer_filter_defaults(
|
||||
mem_db, legacy, base_tokenizer, options, expected_filters, expected_tokens
|
||||
):
|
||||
text = "Running in café"
|
||||
table = mem_db.create_table("test_filter_defaults", data=[{"text": text}])
|
||||
if legacy:
|
||||
with pytest.warns(DeprecationWarning, match="create_fts_index"):
|
||||
table.create_fts_index("text", base_tokenizer=base_tokenizer, **options)
|
||||
else:
|
||||
table.create_index("text", config=FTS(base_tokenizer=base_tokenizer, **options))
|
||||
|
||||
details = table.list_indices()[0].index_details
|
||||
assert (details["stem"], details["remove_stop_words"]) == expected_filters
|
||||
assert [
|
||||
(token.text, token.position) for token in table.tokenize(text, column="text")
|
||||
] == expected_tokens
|
||||
assert [
|
||||
(token.text, token.position)
|
||||
for token in ldb.tokenize(text, base_tokenizer=base_tokenizer, **options)
|
||||
] == expected_tokens
|
||||
if base_tokenizer == "code" and not expected_filters[1]:
|
||||
assert table.search("in", query_type="fts").to_list()[0]["text"] == text
|
||||
|
||||
|
||||
def test_tokenize_uses_icu_index_tokenizer_by_name(mem_db: DBConnection):
|
||||
data = pa.table({"text": ["Hello, こんにちは世界!"]})
|
||||
table = mem_db.create_table("test_tokenize_icu", data=data)
|
||||
|
||||
@@ -1128,6 +1128,77 @@ def test_table_create_indices():
|
||||
table.drop_index("custom_fts_idx")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("legacy", [False, True])
|
||||
@pytest.mark.parametrize(
|
||||
("base_tokenizer", "options", "expected_filters"),
|
||||
[
|
||||
("code", {}, (False, False)),
|
||||
("code", {"stem": None, "remove_stop_words": None}, (False, False)),
|
||||
("code", {"stem": True}, (True, False)),
|
||||
("code", {"remove_stop_words": True}, (False, True)),
|
||||
("code", {"stem": True, "remove_stop_words": True}, (True, True)),
|
||||
("simple", {}, (True, True)),
|
||||
("simple", {"stem": False, "remove_stop_words": False}, (False, False)),
|
||||
],
|
||||
)
|
||||
def test_remote_fts_tokenizer_filter_defaults(
|
||||
legacy, base_tokenizer, options, expected_filters
|
||||
):
|
||||
from lancedb.index import FTS
|
||||
|
||||
received_requests = []
|
||||
|
||||
def handler(request):
|
||||
if request.path == "/v1/table/test/create_index/":
|
||||
content_len = int(request.headers["Content-Length"])
|
||||
received_requests.append(json.loads(request.rfile.read(content_len)))
|
||||
request.send_response(200)
|
||||
request.end_headers()
|
||||
elif request.path == "/v1/table/test/create/?mode=create":
|
||||
request.send_response(200)
|
||||
request.send_header("Content-Type", "application/json")
|
||||
request.end_headers()
|
||||
request.wfile.write(b"{}")
|
||||
elif request.path == "/v1/table/test/describe/":
|
||||
request.send_response(200)
|
||||
request.send_header("Content-Type", "application/json")
|
||||
request.end_headers()
|
||||
request.wfile.write(
|
||||
json.dumps(
|
||||
{
|
||||
"version": 1,
|
||||
"schema": {
|
||||
"fields": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": {"type": "string"},
|
||||
"nullable": False,
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
).encode()
|
||||
)
|
||||
else:
|
||||
request.send_response(404)
|
||||
request.end_headers()
|
||||
|
||||
with mock_lancedb_connection(handler) as db:
|
||||
table = db.create_table("test", [{"text": "Running in cafes"}])
|
||||
if legacy:
|
||||
with pytest.warns(DeprecationWarning, match="create_fts_index"):
|
||||
table.create_fts_index("text", base_tokenizer=base_tokenizer, **options)
|
||||
else:
|
||||
table.create_index(
|
||||
"text", config=FTS(base_tokenizer=base_tokenizer, **options)
|
||||
)
|
||||
|
||||
assert len(received_requests) == 1
|
||||
params = received_requests[0]
|
||||
assert params["base_tokenizer"] == base_tokenizer
|
||||
assert (params["stem"], params["remove_stop_words"]) == expected_filters
|
||||
|
||||
|
||||
def test_remote_create_index_async_returns_job():
|
||||
from lancedb.index import BTree
|
||||
|
||||
|
||||
+8
-4
@@ -54,8 +54,6 @@ pub fn extract_index_params(source: &Option<Bound<'_, PyAny>>) -> PyResult<Lance
|
||||
.with_position(params.with_position)
|
||||
.lower_case(params.lower_case)
|
||||
.max_token_length(params.max_token_length)
|
||||
.remove_stop_words(params.remove_stop_words)
|
||||
.stem(params.stem)
|
||||
.ascii_folding(params.ascii_folding)
|
||||
.ngram_min_length(params.ngram_min_length)
|
||||
.ngram_max_length(params.ngram_max_length)
|
||||
@@ -65,6 +63,12 @@ pub fn extract_index_params(source: &Option<Bound<'_, PyAny>>) -> PyResult<Lance
|
||||
DocumentGranularity::try_from(params.document_granularity.as_str())
|
||||
.map_err(|err| PyValueError::new_err(err.to_string()))?,
|
||||
);
|
||||
if let Some(stem) = params.stem {
|
||||
inner_opts = inner_opts.stem(stem);
|
||||
}
|
||||
if let Some(remove_stop_words) = params.remove_stop_words {
|
||||
inner_opts = inner_opts.remove_stop_words(remove_stop_words);
|
||||
}
|
||||
if let Some(memory_limit) = params.memory_limit {
|
||||
inner_opts = inner_opts.memory_limit_mb(memory_limit);
|
||||
}
|
||||
@@ -215,8 +219,8 @@ struct FtsParams {
|
||||
language: String,
|
||||
max_token_length: Option<usize>,
|
||||
lower_case: bool,
|
||||
stem: bool,
|
||||
remove_stop_words: bool,
|
||||
stem: Option<bool>,
|
||||
remove_stop_words: Option<bool>,
|
||||
custom_stop_words: Option<Vec<String>>,
|
||||
ascii_folding: bool,
|
||||
ngram_min_length: u32,
|
||||
|
||||
+11
-7
@@ -823,8 +823,8 @@ impl From<LanceDbFtsToken> for FtsToken {
|
||||
language = "English".to_string(),
|
||||
max_token_length = Some(40),
|
||||
lower_case = true,
|
||||
stem = true,
|
||||
remove_stop_words = true,
|
||||
stem = None,
|
||||
remove_stop_words = None,
|
||||
custom_stop_words = None,
|
||||
ascii_folding = true,
|
||||
ngram_min_length = 3,
|
||||
@@ -838,15 +838,15 @@ pub fn tokenize(
|
||||
language: String,
|
||||
max_token_length: Option<u32>,
|
||||
lower_case: bool,
|
||||
stem: bool,
|
||||
remove_stop_words: bool,
|
||||
stem: Option<bool>,
|
||||
remove_stop_words: Option<bool>,
|
||||
custom_stop_words: Option<Vec<String>>,
|
||||
ascii_folding: bool,
|
||||
ngram_min_length: u32,
|
||||
ngram_max_length: u32,
|
||||
prefix_only: bool,
|
||||
) -> PyResult<Vec<FtsToken>> {
|
||||
let params = FtsIndexBuilder::default()
|
||||
let mut params = FtsIndexBuilder::default()
|
||||
.base_tokenizer(base_tokenizer)
|
||||
.language(&language)
|
||||
.map_err(|_| {
|
||||
@@ -857,13 +857,17 @@ pub fn tokenize(
|
||||
})?
|
||||
.max_token_length(max_token_length.map(|value| value as usize))
|
||||
.lower_case(lower_case)
|
||||
.stem(stem)
|
||||
.remove_stop_words(remove_stop_words)
|
||||
.ascii_folding(ascii_folding)
|
||||
.ngram_min_length(ngram_min_length)
|
||||
.ngram_max_length(ngram_max_length)
|
||||
.ngram_prefix_only(prefix_only)
|
||||
.custom_stop_words(custom_stop_words);
|
||||
if let Some(stem) = stem {
|
||||
params = params.stem(stem);
|
||||
}
|
||||
if let Some(remove_stop_words) = remove_stop_words {
|
||||
params = params.remove_stop_words(remove_stop_words);
|
||||
}
|
||||
let tokens = lancedb_tokenize(&query, ¶ms).infer_error()?;
|
||||
Ok(tokens.into_iter().map(FtsToken::from).collect())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user