This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new 4c70d287 feat(fulltext): read global indexes via paimon-ftindex-core 
(#571)
4c70d287 is described below

commit 4c70d2871e0a738c1a75554211b0243173804aeb
Author: shyjsarah <[email protected]>
AuthorDate: Thu Jul 23 14:06:41 2026 +0800

    feat(fulltext): read global indexes via paimon-ftindex-core (#571)
---
 Cargo.lock                                         | 340 ++-------
 DEPENDENCIES.rust.tsv                              |  22 -
 bindings/python/DEPENDENCIES.rust.tsv              |  23 -
 crates/integrations/datafusion/Cargo.toml          |   2 +
 .../integrations/datafusion/DEPENDENCIES.rust.tsv  |  22 -
 .../integrations/datafusion/tests/read_tables.rs   | 158 +++-
 crates/paimon/Cargo.toml                           |   7 +-
 crates/paimon/DEPENDENCIES.rust.tsv                |  24 -
 .../{tantivy/full_text_search.rs => full_text.rs}  |  87 ++-
 crates/paimon/src/lib.rs                           |   4 +-
 crates/paimon/src/table/data_file_reader.rs        |   2 +-
 crates/paimon/src/table/full_text_index_adapter.rs | 411 +++++++++++
 .../paimon/src/table/full_text_search_builder.rs   | 806 +++++++++++++++++++--
 crates/paimon/src/table/mod.rs                     |   2 +
 crates/paimon/src/tantivy/directory.rs             | 399 ----------
 crates/paimon/src/tantivy/mod.rs                   |  23 -
 crates/paimon/src/tantivy/reader.rs                | 198 -----
 crates/paimon/src/tantivy/writer.rs                | 242 -------
 18 files changed, 1399 insertions(+), 1373 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 126dfeb1..ffcb3c7a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -512,7 +512,7 @@ dependencies = [
  "futures-lite",
  "parking",
  "polling",
- "rustix 1.1.4",
+ "rustix",
  "slab",
  "windows-sys 0.61.2",
 ]
@@ -554,7 +554,7 @@ dependencies = [
  "cfg-if",
  "event-listener",
  "futures-lite",
- "rustix 1.1.4",
+ "rustix",
 ]
 
 [[package]]
@@ -569,7 +569,7 @@ dependencies = [
  "cfg-if",
  "futures-core",
  "futures-io",
- "rustix 1.1.4",
+ "rustix",
  "signal-hook-registry",
  "slab",
  "windows-sys 0.61.2",
@@ -770,7 +770,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "regex",
- "rustc-hash 2.1.3",
+ "rustc-hash",
  "shlex 1.3.0",
  "syn 2.0.119",
 ]
@@ -2346,12 +2346,6 @@ dependencies = [
  "windows-sys 0.60.2",
 ]
 
-[[package]]
-name = "downcast-rs"
-version = "1.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
-
 [[package]]
 name = "downcast-rs"
 version = "2.0.2"
@@ -2604,23 +2598,13 @@ dependencies = [
  "percent-encoding",
 ]
 
-[[package]]
-name = "fs4"
-version = "0.8.4"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "f7e180ac76c23b45e767bd7ae9579bc0bb458618c4bc71835926e098e61d15f8"
-dependencies = [
- "rustix 0.38.44",
- "windows-sys 0.52.0",
-]
-
 [[package]]
 name = "fs4"
 version = "0.13.1"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4"
 dependencies = [
- "rustix 1.1.4",
+ "rustix",
  "windows-sys 0.59.0",
 ]
 
@@ -2636,7 +2620,7 @@ version = "0.5.11"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "9b13ac798afc0d9194eb4efefef8b9332efbd80b43f302a968cb8cb23b9d5360"
 dependencies = [
- "rustc-hash 2.1.3",
+ "rustc-hash",
 ]
 
 [[package]]
@@ -2882,8 +2866,6 @@ version = "0.15.5"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
 dependencies = [
- "allocator-api2",
- "equivalent",
  "foldhash 0.1.5",
 ]
 
@@ -3338,18 +3320,6 @@ dependencies = [
  "generic-array",
 ]
 
-[[package]]
-name = "instant"
-version = "0.1.13"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
-dependencies = [
- "cfg-if",
- "js-sys",
- "wasm-bindgen",
- "web-sys",
-]
-
 [[package]]
 name = "integer-encoding"
 version = "3.0.4"
@@ -3377,15 +3347,6 @@ version = "1.70.2"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
 
-[[package]]
-name = "itertools"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
-dependencies = [
- "either",
-]
-
 [[package]]
 name = "itertools"
 version = "0.13.0"
@@ -3431,7 +3392,7 @@ dependencies = [
  "jieba-macros",
  "phf 0.13.1",
  "regex",
- "rustc-hash 2.1.3",
+ "rustc-hash",
 ]
 
 [[package]]
@@ -3770,12 +3731,6 @@ dependencies = [
  "redox_syscall 0.9.0",
 ]
 
-[[package]]
-name = "linux-raw-sys"
-version = "0.4.15"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
-
 [[package]]
 name = "linux-raw-sys"
 version = "0.12.1"
@@ -3803,15 +3758,6 @@ version = "0.4.33"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
 
-[[package]]
-name = "lru"
-version = "0.12.5"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
-dependencies = [
- "hashbrown 0.15.5",
-]
-
 [[package]]
 name = "lru"
 version = "0.16.4"
@@ -3916,16 +3862,6 @@ dependencies = [
  "slab",
 ]
 
-[[package]]
-name = "measure_time"
-version = "0.8.3"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "dbefd235b0aadd181626f281e1d684e116972988c14c264e42069d5e8a5775cc"
-dependencies = [
- "instant",
- "log",
-]
-
 [[package]]
 name = "measure_time"
 version = "0.9.0"
@@ -4195,16 +4131,6 @@ dependencies = [
  "libm",
 ]
 
-[[package]]
-name = "num_cpus"
-version = "1.17.0"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
-dependencies = [
- "hermit-abi",
- "libc",
-]
-
 [[package]]
 name = "num_enum"
 version = "0.7.6"
@@ -4578,15 +4504,6 @@ dependencies = [
  "hashbrown 0.14.5",
 ]
 
-[[package]]
-name = "ownedbytes"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "c3a059efb063b8f425b948e042e6b9bd85edfe60e913630ed727b23e2dfcc558"
-dependencies = [
- "stable_deref_trait",
-]
-
 [[package]]
 name = "ownedbytes"
 version = "0.9.0"
@@ -4655,7 +4572,6 @@ dependencies = [
  "sha2 0.10.9",
  "snafu 0.9.1",
  "snap",
- "tantivy 0.22.1",
  "tempfile",
  "tokio",
  "tokio-util",
@@ -4690,6 +4606,7 @@ dependencies = [
  "arrow-schema",
  "arrow-select",
  "async-trait",
+ "bytes",
  "chrono",
  "constant_time_eq",
  "datafusion",
@@ -4698,6 +4615,7 @@ dependencies = [
  "lexical-write-float",
  "log",
  "paimon",
+ "paimon-ftindex-core",
  "parquet",
  "serde",
  "serde_json",
@@ -4717,8 +4635,8 @@ dependencies = [
  "roaring",
  "serde",
  "serde_json",
- "tantivy 0.26.1",
- "tantivy-common 0.11.0",
+ "tantivy",
+ "tantivy-common",
  "tantivy-fst",
  "tantivy-jieba",
  "tempfile",
@@ -5080,7 +4998,7 @@ dependencies = [
  "concurrent-queue",
  "hermit-abi",
  "pin-project-lite",
- "rustix 1.1.4",
+ "rustix",
  "windows-sys 0.61.2",
 ]
 
@@ -5356,7 +5274,7 @@ dependencies = [
  "pin-project-lite",
  "quinn-proto",
  "quinn-udp",
- "rustc-hash 2.1.3",
+ "rustc-hash",
  "rustls",
  "socket2",
  "thiserror 2.0.19",
@@ -5378,7 +5296,7 @@ dependencies = [
  "rand 0.10.2",
  "rand_pcg",
  "ring",
- "rustc-hash 2.1.3",
+ "rustc-hash",
  "rustls",
  "rustls-pki-types",
  "slab",
@@ -5505,16 +5423,6 @@ version = "0.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
 
-[[package]]
-name = "rand_distr"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31"
-dependencies = [
- "num-traits",
- "rand 0.8.7",
-]
-
 [[package]]
 name = "rand_pcg"
 version = "0.10.2"
@@ -5952,12 +5860,6 @@ dependencies = [
  "serde_derive",
 ]
 
-[[package]]
-name = "rustc-hash"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
-
 [[package]]
 name = "rustc-hash"
 version = "2.1.3"
@@ -5973,19 +5875,6 @@ dependencies = [
  "semver",
 ]
 
-[[package]]
-name = "rustix"
-version = "0.38.44"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
-dependencies = [
- "bitflags 2.13.1",
- "errno",
- "libc",
- "linux-raw-sys 0.4.15",
- "windows-sys 0.59.0",
-]
-
 [[package]]
 name = "rustix"
 version = "1.1.4"
@@ -5995,7 +5884,7 @@ dependencies = [
  "bitflags 2.13.1",
  "errno",
  "libc",
- "linux-raw-sys 0.12.1",
+ "linux-raw-sys",
  "windows-sys 0.61.2",
 ]
 
@@ -6440,15 +6329,6 @@ version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
 
-[[package]]
-name = "sketches-ddsketch"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "85636c14b73d81f541e525f585c0a2109e6744e1565b5c1668e31c70c10ed65c"
-dependencies = [
- "serde",
-]
-
 [[package]]
 name = "sketches-ddsketch"
 version = "0.4.0"
@@ -6758,57 +6638,6 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
 
-[[package]]
-name = "tantivy"
-version = "0.22.1"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "96599ea6fccd844fc833fed21d2eecac2e6a7c1afd9e044057391d78b1feb141"
-dependencies = [
- "aho-corasick",
- "arc-swap",
- "base64",
- "bitpacking",
- "byteorder",
- "census",
- "crc32fast",
- "crossbeam-channel",
- "downcast-rs 1.2.1",
- "fastdivide",
- "fnv",
- "fs4 0.8.4",
- "htmlescape",
- "itertools 0.12.1",
- "levenshtein_automata",
- "log",
- "lru 0.12.5",
- "lz4_flex 0.11.6",
- "measure_time 0.8.3",
- "memmap2",
- "num_cpus",
- "once_cell",
- "oneshot 0.1.13",
- "rayon",
- "regex",
- "rust-stemmers",
- "rustc-hash 1.1.0",
- "serde",
- "serde_json",
- "sketches-ddsketch 0.2.2",
- "smallvec",
- "tantivy-bitpacker 0.6.0",
- "tantivy-columnar 0.3.0",
- "tantivy-common 0.7.0",
- "tantivy-fst",
- "tantivy-query-grammar 0.22.0",
- "tantivy-stacker 0.3.0",
- "tantivy-tokenizer-api 0.3.0",
- "tempfile",
- "thiserror 1.0.69",
- "time",
- "uuid",
- "winapi",
-]
-
 [[package]]
 name = "tantivy"
 version = "0.26.1"
@@ -6825,35 +6654,35 @@ dependencies = [
  "crc32fast",
  "crossbeam-channel",
  "datasketches",
- "downcast-rs 2.0.2",
+ "downcast-rs",
  "fastdivide",
  "fnv",
- "fs4 0.13.1",
+ "fs4",
  "htmlescape",
  "itertools 0.14.0",
  "levenshtein_automata",
  "log",
- "lru 0.16.4",
+ "lru",
  "lz4_flex 0.13.1",
- "measure_time 0.9.0",
+ "measure_time",
  "memmap2",
  "once_cell",
  "oneshot 0.1.13",
  "rayon",
  "regex",
  "rust-stemmers",
- "rustc-hash 2.1.3",
+ "rustc-hash",
  "serde",
  "serde_json",
- "sketches-ddsketch 0.4.0",
+ "sketches-ddsketch",
  "smallvec",
- "tantivy-bitpacker 0.10.0",
- "tantivy-columnar 0.7.0",
- "tantivy-common 0.11.0",
+ "tantivy-bitpacker",
+ "tantivy-columnar",
+ "tantivy-common",
  "tantivy-fst",
- "tantivy-query-grammar 0.26.0",
- "tantivy-stacker 0.7.0",
- "tantivy-tokenizer-api 0.7.0",
+ "tantivy-query-grammar",
+ "tantivy-stacker",
+ "tantivy-tokenizer-api",
  "tempfile",
  "thiserror 2.0.19",
  "time",
@@ -6862,15 +6691,6 @@ dependencies = [
  "winapi",
 ]
 
-[[package]]
-name = "tantivy-bitpacker"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "284899c2325d6832203ac6ff5891b297fc5239c3dc754c5bc1977855b23c10df"
-dependencies = [
- "bitpacking",
-]
-
 [[package]]
 name = "tantivy-bitpacker"
 version = "0.10.0"
@@ -6880,49 +6700,20 @@ dependencies = [
  "bitpacking",
 ]
 
-[[package]]
-name = "tantivy-columnar"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "12722224ffbe346c7fec3275c699e508fd0d4710e629e933d5736ec524a1f44e"
-dependencies = [
- "downcast-rs 1.2.1",
- "fastdivide",
- "itertools 0.12.1",
- "serde",
- "tantivy-bitpacker 0.6.0",
- "tantivy-common 0.7.0",
- "tantivy-sstable 0.3.0",
- "tantivy-stacker 0.3.0",
-]
-
 [[package]]
 name = "tantivy-columnar"
 version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "c57166f5bcfd478f370ab8445afb4678dce44801fa5ce5c451aaf8595583c5dc"
 dependencies = [
- "downcast-rs 2.0.2",
+ "downcast-rs",
  "fastdivide",
  "itertools 0.14.0",
  "serde",
- "tantivy-bitpacker 0.10.0",
- "tantivy-common 0.11.0",
- "tantivy-sstable 0.7.0",
- "tantivy-stacker 0.7.0",
-]
-
-[[package]]
-name = "tantivy-common"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "8019e3cabcfd20a1380b491e13ff42f57bb38bf97c3d5fa5c07e50816e0621f4"
-dependencies = [
- "async-trait",
- "byteorder",
- "ownedbytes 0.7.0",
- "serde",
- "time",
+ "tantivy-bitpacker",
+ "tantivy-common",
+ "tantivy-sstable",
+ "tantivy-stacker",
 ]
 
 [[package]]
@@ -6933,7 +6724,7 @@ checksum = 
"bbf10915aa75da3c3b0d58b58853d2e889efbaf32d4982a4c3715dde6bba23e5"
 dependencies = [
  "async-trait",
  "byteorder",
- "ownedbytes 0.9.0",
+ "ownedbytes",
  "serde",
  "time",
 ]
@@ -6957,16 +6748,7 @@ checksum = 
"3392170e86f1c387170aba7d171a466ffdc98a8b55b006e19ac64b123a7b690a"
 dependencies = [
  "jieba-rs",
  "lazy_static",
- "tantivy-tokenizer-api 0.7.0",
-]
-
-[[package]]
-name = "tantivy-query-grammar"
-version = "0.22.0"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "847434d4af57b32e309f4ab1b4f1707a6c566656264caa427ff4285c4d9d0b82"
-dependencies = [
- "nom",
+ "tantivy-tokenizer-api",
 ]
 
 [[package]]
@@ -6982,18 +6764,6 @@ dependencies = [
  "serde_json",
 ]
 
-[[package]]
-name = "tantivy-sstable"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "c69578242e8e9fc989119f522ba5b49a38ac20f576fc778035b96cc94f41f98e"
-dependencies = [
- "tantivy-bitpacker 0.6.0",
- "tantivy-common 0.7.0",
- "tantivy-fst",
- "zstd",
-]
-
 [[package]]
 name = "tantivy-sstable"
 version = "0.7.0"
@@ -7002,23 +6772,12 @@ checksum = 
"8a2cfc3ac5164cbadc28965ffb145a8f47582a60ae5897859ad8d4316596c606"
 dependencies = [
  "futures-util",
  "itertools 0.14.0",
- "tantivy-bitpacker 0.10.0",
- "tantivy-common 0.11.0",
+ "tantivy-bitpacker",
+ "tantivy-common",
  "tantivy-fst",
  "zstd",
 ]
 
-[[package]]
-name = "tantivy-stacker"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "c56d6ff5591fc332739b3ce7035b57995a3ce29a93ffd6012660e0949c956ea8"
-dependencies = [
- "murmurhash32",
- "rand_distr",
- "tantivy-common 0.7.0",
-]
-
 [[package]]
 name = "tantivy-stacker"
 version = "0.7.0"
@@ -7026,16 +6785,7 @@ source = 
"registry+https://github.com/rust-lang/crates.io-index";
 checksum = "6cbb051742da9d53ca9e8fff43a9b10e319338b24e2c0e15d0372df19ffeb951"
 dependencies = [
  "murmurhash32",
- "tantivy-common 0.11.0",
-]
-
-[[package]]
-name = "tantivy-tokenizer-api"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "2a0dcade25819a89cfe6f17d932c9cedff11989936bf6dd4f336d50392053b04"
-dependencies = [
- "serde",
+ "tantivy-common",
 ]
 
 [[package]]
@@ -7079,7 +6829,7 @@ dependencies = [
  "fastrand",
  "getrandom 0.4.3",
  "once_cell",
- "rustix 1.1.4",
+ "rustix",
  "windows-sys 0.61.2",
 ]
 
@@ -7581,7 +7331,7 @@ dependencies = [
  "itertools 0.14.0",
  "num-traits",
  "prost 0.14.4",
- "rustc-hash 2.1.3",
+ "rustc-hash",
  "vortex-array",
  "vortex-buffer",
  "vortex-error",
@@ -7626,7 +7376,7 @@ dependencies = [
  "pin-project-lite",
  "prost 0.14.4",
  "rand 0.10.2",
- "rustc-hash 2.1.3",
+ "rustc-hash",
  "simdutf8",
  "smallvec",
  "static_assertions",
@@ -7720,7 +7470,7 @@ dependencies = [
  "num-traits",
  "parking_lot",
  "rand 0.10.2",
- "rustc-hash 2.1.3",
+ "rustc-hash",
  "tracing",
  "vortex-array",
  "vortex-buffer",
@@ -7930,8 +7680,8 @@ dependencies = [
  "paste",
  "pin-project-lite",
  "prost 0.14.4",
- "rustc-hash 2.1.3",
- "sketches-ddsketch 0.4.0",
+ "rustc-hash",
+ "sketches-ddsketch",
  "termtree",
  "tokio",
  "tracing",
@@ -7968,7 +7718,7 @@ source = 
"registry+https://github.com/rust-lang/crates.io-index";
 checksum = "6e079c9fe032af05b2e38bf91e63d2bdf36bd01e3920bc4a352648b2b21e9f70"
 dependencies = [
  "parking_lot",
- "sketches-ddsketch 0.4.0",
+ "sketches-ddsketch",
 ]
 
 [[package]]
@@ -8578,7 +8328,7 @@ source = 
"registry+https://github.com/rust-lang/crates.io-index";
 checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
 dependencies = [
  "libc",
- "rustix 1.1.4",
+ "rustix",
 ]
 
 [[package]]
diff --git a/DEPENDENCIES.rust.tsv b/DEPENDENCIES.rust.tsv
index 166d7ce6..cb427ff2 100644
--- a/DEPENDENCIES.rust.tsv
+++ b/DEPENDENCIES.rust.tsv
@@ -183,7 +183,6 @@ [email protected]               X                               
                                                X
 [email protected]               X                                               
                                X                                               
        
 [email protected]         X                                                       
                        X                                                       
 [email protected]               X                                               
                                X                                               
        
[email protected]              X                                               
                                X                                               
        
 [email protected]              X                                               
                                X                                               
        
 [email protected]           X                                               
                                                                                
        
 [email protected]            X                                       X               
                                X                                               
@@ -214,7 +213,6 @@ [email protected]         X                               
                                                X
 [email protected]             X                                       
                                        X                                       
                
 [email protected]          X                                               
                                X                                               
        
 [email protected]             X                                                       
                        X                                                       
[email protected]              X                                                       
                        X                                                       
 [email protected]                                                                 
                        X                                                       
 [email protected]         X                                                       
                                                                                
 [email protected]                                                                    
                        X                                                       
@@ -278,12 +276,10 @@ [email protected]               X
 [email protected]           X                                       
                                                                                
                
 [email protected]                X                                               
                                X                                               
        
 [email protected]            X                                                       
                        X                                                       
[email protected]                                 X                               
                                                                                
 [email protected]                                                         
                                X                                               
        
 [email protected]               X                                               
                                X                                               
        
 [email protected]           X                                                       
                        X                                                       
 [email protected]            X                                       
                                        X                                       
                
[email protected]               X                                               
                                X                                               
        
 [email protected]               X                                               
                                X                                               
        
 [email protected]               X                                               
                                X                                               
        
 [email protected]            X                                                       
                        X                                                       
@@ -322,11 +318,9 @@ [email protected]          X                               
                                                X
 [email protected]                                                                    
                        X                                                       
 [email protected]                                                                
                                X                                               
        
 [email protected]           X       X                                       
                                X                                               
        
[email protected]           X       X                                       
                                X                                               
        
 [email protected]                                                                  
                                                X                               
 [email protected]                X                                               
                                X                                               
        
 [email protected]             X                                                       
                        X                                                       
[email protected]                                                                     
                        X                                                       
 [email protected]                                                                     
                        X                                                       
 [email protected]                                                                
                                X                                               
        
 [email protected]                                                                
                                X                                               
        
@@ -338,7 +332,6 @@ [email protected]               X                       
                                                        X
 [email protected]            X                                                       
                        X                                                       
 [email protected]            X                                                       
                        X                                                       
 [email protected]              X                                                       
                                                                                
[email protected]                                                             
                                X                                               
        
 [email protected]                                                             
                                X                                               
        
 [email protected]                                                                   
                        X                               X                       
 [email protected]         X                                                       
                        X                                                       
@@ -365,7 +358,6 @@ [email protected]          X                               
                                                X
 [email protected]                X                                               
                                X                                               
        
 [email protected]             X                                               
                                X                                               
        
 [email protected]              X                                               
                                X                                               
        
[email protected]                X                                               
                                X                                               
        
 [email protected]         X                       X                               
                        X                                                       
 [email protected]          X                       X                       
                                X                                               
        
 [email protected]          X                                                       
                        X                                                       
@@ -393,7 +385,6 @@ [email protected]              X
 [email protected]                                                           
                                X                                               
        
 [email protected]                                                            
                                X                                               
        
 [email protected]                                                         
                                X                                               
        
[email protected]                                                               
                                X                                               
        
 [email protected]                                                               
                                X                                               
        
 [email protected]           X                                                       
                                                                                
 [email protected]         X                                                       
                                                                                
@@ -469,7 +460,6 @@ [email protected]           X                               
                                                X
 [email protected]               X                                               
                                X                                               
        
 [email protected]                X                                               
                                X                                               
        
 [email protected]                X                                               
                                X                                               
        
[email protected]               X                                               
                                X                                               
        
 [email protected]             X                                               
                                X                                               
        
 [email protected]               X                                               
                                X                                               
        
 [email protected]           X                                                       
                        X                                                       
@@ -498,10 +488,8 @@ [email protected]           X                               
                                                X
 [email protected]             X                                                       
                        X                                                       
 [email protected]                                                                
                                X                                               
        
 [email protected]                                    X                       
                                X                                               
        
[email protected]               X                                               
                                X                                               
        
 [email protected]               X                                               
                                X                                               
        
 [email protected]            X                                               
                                X                                               
        
[email protected]         X       X                                               
                        X                                                       
 [email protected]           X       X                                               
                        X                                                       
 [email protected]         X                                                       
        X               X                                                       
 [email protected]              X                                       
                        X               X                                       
                
@@ -545,7 +533,6 @@ [email protected]                                         
                                                X
 [email protected]               X                                               
                                X                                               
        
 [email protected]         X                                                       
                        X                                                       
 [email protected]                X                                               
                                X                                               
        
[email protected]                X                                       
                                                                                
                
 [email protected]                X                                       
                                                                                
                
 [email protected]                                                                    
                        X                                                       
 [email protected]                X                                               
                                X                                               
        
@@ -579,23 +566,15 @@ [email protected]                                       
                                                        X
 [email protected]             X                                       
                                        X                                       
                
 [email protected]         X                                       
                                        X                                       
                
 [email protected]           X                                                       
                        X                                                       
[email protected]                                                                 
                        X                                                       
 [email protected]                                                                 
                        X                                                       
 [email protected]                                                       
                                        X                                       
                
[email protected]                                                        
                                        X                                       
                
[email protected]                                                         
                                X                                               
        
 [email protected]                                                         
                                X                                               
        
 [email protected]                                                          
                                X                                               
        
[email protected]                                                           
                                X                                               
        
 [email protected]                                                              
                                X                               X               
        
 [email protected]                                                           
                                X                                               
        
[email protected]                                                   
                                        X                                       
                
 [email protected]                                                   
                                        X                                       
                
[email protected]                                                          
                                X                                               
        
 [email protected]                                                          
                                X                                               
        
[email protected]                                                          
                                X                                               
        
 [email protected]                                                          
                                X                                               
        
[email protected]                                                    
                                        X                                       
                
 [email protected]                                                    
                                        X                                       
                
 [email protected]                                                                      
                        X                                                       
 [email protected]                  X                                       
                                                                                
        
@@ -703,7 +682,6 @@ [email protected]          X                               
                                                X
 [email protected]         X                                               
                                X                                               
        
 [email protected]           X                                               
                                X                                               
        
 [email protected]          X                                               
                                X                                               
        
[email protected]             X                                               
                                X                                               
        
 [email protected]             X                                               
                                X                                               
        
 [email protected]             X                                               
                                X                                               
        
 [email protected]             X                                               
                                X                                               
        
diff --git a/bindings/python/DEPENDENCIES.rust.tsv 
b/bindings/python/DEPENDENCIES.rust.tsv
index 041777ad..f2749c96 100644
--- a/bindings/python/DEPENDENCIES.rust.tsv
+++ b/bindings/python/DEPENDENCIES.rust.tsv
@@ -148,7 +148,6 @@ [email protected]               X                               
                                                X
 [email protected]               X                                               
                                X                                               
        
 [email protected]         X                                                       
                        X                                                       
 [email protected]               X                                               
                                X                                               
        
[email protected]              X                                               
                                X                                               
        
 [email protected]              X                                               
                                X                                               
        
 [email protected]            X                                       X               
                                X                                               
 [email protected]          X                                                       
                        X                                                       
@@ -170,7 +169,6 @@ [email protected]         X                               
                                                X
 [email protected]             X                                       
                                        X                                       
                
 [email protected]          X                                               
                                X                                               
        
 [email protected]             X                                                       
                        X                                                       
[email protected]              X                                                       
                        X                                                       
 [email protected]                                                                 
                        X                                                       
 [email protected]         X                                                       
                        X                                                       
 [email protected]         X                                               
                                X                                               
        
@@ -197,7 +195,6 @@ [email protected]            X                               
                                                X
 [email protected]               X                                               
                                X                                               
        
 [email protected]             X                                               
                                                                                
        
 [email protected]             X                                                       
                        X                                                       
[email protected]               X                                               
                                X                                               
        
 [email protected]              X                                                       
                        X                                                       
 [email protected]            X                                                       
                        X                                                       
 [email protected]            X                                                       
                        X                                                       
@@ -230,11 +227,9 @@ [email protected]                X
 [email protected]           X                                       
                                                                                
                
 [email protected]                X                                               
                                X                                               
        
 [email protected]            X                                                       
                        X                                                       
[email protected]                                 X                               
                                                                                
 [email protected]                                                         
                                X                                               
        
 [email protected]               X                                               
                                X                                               
        
 [email protected]           X                                                       
                        X                                                       
[email protected]               X                                               
                                X                                               
        
 [email protected]               X                                               
                                X                                               
        
 [email protected]            X                                                       
                        X                                                       
 [email protected]                                                            
                                X                                               
        
@@ -267,11 +262,9 @@ [email protected]          X                               
                                                X
 [email protected]                                                                    
                        X                                                       
 [email protected]                                                                
                                X                                               
        
 [email protected]           X       X                                       
                                X                                               
        
[email protected]           X       X                                       
                                X                                               
        
 [email protected]                                                                  
                                                X                               
 [email protected]                X                                               
                                X                                               
        
 [email protected]             X                                                       
                        X                                                       
[email protected]                                                                     
                        X                                                       
 [email protected]                                                                     
                        X                                                       
 [email protected]                                                                
                                X                                               
        
 [email protected]                                                                
                                X                                               
        
@@ -280,7 +273,6 @@ [email protected]               X                       
                                                        X
 [email protected]            X                                                       
                        X                                                       
 [email protected]            X                                                       
                        X                                                       
 [email protected]              X                                                       
                                                                                
[email protected]                                                             
                                X                                               
        
 [email protected]                                                             
                                X                                               
        
 [email protected]                                                                   
                        X                               X                       
 [email protected]         X                                                       
                        X                                                       
@@ -303,7 +295,6 @@ [email protected]          X                               
                                                X
 [email protected]                X                                               
                                X                                               
        
 [email protected]             X                                               
                                X                                               
        
 [email protected]              X                                               
                                X                                               
        
[email protected]                X                                               
                                X                                               
        
 [email protected]          X                                                       
                        X                                                       
 [email protected]            X                                               
                                X                                               
        
 [email protected]               X                                               
                                X                                               
        
@@ -327,7 +318,6 @@ [email protected]              X
 [email protected]                                                           
                                X                                               
        
 [email protected]                                                            
                                X                                               
        
 [email protected]                                                         
                                X                                               
        
[email protected]                                                               
                                X                                               
        
 [email protected]                                                               
                                X                                               
        
 [email protected]           X                                                       
                                                                                
 [email protected]                X                                       
                                                                                
                
@@ -391,7 +381,6 @@ [email protected]           X                               
                                                X
 [email protected]              X                                               
                                X                                               
        
 [email protected]                X                                               
                                X                                               
        
 [email protected]                X                                               
                                X                                               
        
[email protected]               X                                               
                                X                                               
        
 [email protected]               X                                               
                                X                                               
        
 [email protected]           X                                                       
                        X                                                       
 [email protected]              X                                               
                                X                                               
        
@@ -419,10 +408,8 @@ [email protected]           X                               
                                                X
 [email protected]             X                                                       
                        X                                                       
 [email protected]                                                                
                                X                                               
        
 [email protected]                                    X                       
                                X                                               
        
[email protected]               X                                               
                                X                                               
        
 [email protected]               X                                               
                                X                                               
        
 [email protected]            X                                               
                                X                                               
        
[email protected]         X       X                                               
                        X                                                       
 [email protected]           X       X                                               
                        X                                                       
 [email protected]         X                                                       
        X               X                                                       
 [email protected]              X                                       
                        X               X                                       
                
@@ -463,7 +450,6 @@ [email protected]                                         
                                                X
 [email protected]               X                                               
                                X                                               
        
 [email protected]         X                                                       
                        X                                                       
 [email protected]                X                                               
                                X                                               
        
[email protected]                X                                       
                                                                                
                
 [email protected]                X                                       
                                                                                
                
 [email protected]                                                                    
                        X                                                       
 [email protected]                X                                               
                                X                                               
        
@@ -493,23 +479,15 @@ [email protected]                X
 [email protected]                                                            
                                X                                               
        
 [email protected]             X                                       
                                        X                                       
                
 [email protected]         X                                       
                                        X                                       
                
[email protected]                                                                 
                        X                                                       
 [email protected]                                                                 
                        X                                                       
 [email protected]                                                       
                                        X                                       
                
[email protected]                                                        
                                        X                                       
                
[email protected]                                                         
                                X                                               
        
 [email protected]                                                         
                                X                                               
        
 [email protected]                                                          
                                X                                               
        
[email protected]                                                           
                                X                                               
        
 [email protected]                                                              
                                X                               X               
        
 [email protected]                                                           
                                X                                               
        
[email protected]                                                   
                                        X                                       
                
 [email protected]                                                   
                                        X                                       
                
[email protected]                                                          
                                X                                               
        
 [email protected]                                                          
                                X                                               
        
[email protected]                                                          
                                X                                               
        
 [email protected]                                                          
                                X                                               
        
[email protected]                                                    
                                        X                                       
                
 [email protected]                                                    
                                        X                                       
                
 [email protected]                  X                                       
                                                                                
        
 [email protected]                X                                               
                                X                                               
        
@@ -584,7 +562,6 @@ [email protected]          X                               
                                                X
 [email protected]         X                                               
                                X                                               
        
 [email protected]           X                                               
                                X                                               
        
 [email protected]          X                                               
                                X                                               
        
[email protected]             X                                               
                                X                                               
        
 [email protected]             X                                               
                                X                                               
        
 [email protected]             X                                               
                                X                                               
        
 [email protected]             X                                               
                                X                                               
        
diff --git a/crates/integrations/datafusion/Cargo.toml 
b/crates/integrations/datafusion/Cargo.toml
index 37fb1947..17acaea0 100644
--- a/crates/integrations/datafusion/Cargo.toml
+++ b/crates/integrations/datafusion/Cargo.toml
@@ -50,7 +50,9 @@ uuid = { version = "1", features = ["v4"] }
 [dev-dependencies]
 arrow-array = { workspace = true }
 arrow-schema = { workspace = true }
+bytes = "1.7.1"
 flate2 = "1"
+paimon-ftindex-core = "0.1.0"
 parquet = { workspace = true }
 tar = "0.4"
 tempfile = "3"
diff --git a/crates/integrations/datafusion/DEPENDENCIES.rust.tsv 
b/crates/integrations/datafusion/DEPENDENCIES.rust.tsv
index f230597d..9724882f 100644
--- a/crates/integrations/datafusion/DEPENDENCIES.rust.tsv
+++ b/crates/integrations/datafusion/DEPENDENCIES.rust.tsv
@@ -152,7 +152,6 @@ [email protected]               X                               
                                        X
 [email protected]          X                                                       
                X                                                       
 [email protected]               X                                               
                        X                                                       
 [email protected]         X                                                       
                X                                                       
[email protected]              X                                               
                        X                                                       
 [email protected]              X                                               
                        X                                                       
 [email protected]           X                                               
                                                                                
 [email protected]            X                                       X               
                        X                                               
@@ -183,7 +182,6 @@ [email protected]         X                               
                                        X
 [email protected]             X                                       
                                X                                               
        
 [email protected]          X                                               
                        X                                                       
 [email protected]             X                                                       
                X                                                       
[email protected]              X                                                       
                X                                                       
 [email protected]                                                                 
                X                                                       
 [email protected]         X                                                       
                                                                        
 [email protected]                                                                    
                X                                                       
@@ -242,11 +240,9 @@ [email protected]                X
 [email protected]            X                                       
                                                                                
        
 [email protected]           X                                       
                                                                                
        
 [email protected]                X                                               
                        X                                                       
[email protected]                                 X                               
                                                                        
 [email protected]                                                         
                        X                                                       
 [email protected]               X                                               
                        X                                                       
 [email protected]           X                                                       
                X                                                       
[email protected]               X                                               
                        X                                                       
 [email protected]               X                                               
                        X                                                       
 [email protected]               X                                               
                        X                                                       
 [email protected]            X                                                       
                X                                                       
@@ -284,11 +280,9 @@ [email protected]              X                               
                                        X
 [email protected]              X                                               
                        X                                                       
 [email protected]                                                                    
                X                                                       
 [email protected]           X       X                                       
                        X                                                       
[email protected]           X       X                                       
                        X                                                       
 [email protected]                                                                  
                                        X                               
 [email protected]                X                                               
                        X                                                       
 [email protected]             X                                                       
                X                                                       
[email protected]                                                                     
                X                                                       
 [email protected]                                                                     
                X                                                       
 [email protected]                                                                
                        X                                                       
 [email protected]                                                                
                        X                                                       
@@ -299,7 +293,6 @@ [email protected]               X                       
                                                X
 [email protected]            X                                                       
                X                                                       
 [email protected]            X                                                       
                X                                                       
 [email protected]              X                                                       
                                                                        
[email protected]                                                             
                        X                                                       
 [email protected]                                                             
                        X                                                       
 [email protected]                                                                   
                X                               X                       
 [email protected]         X                                                       
                X                                                       
@@ -325,7 +318,6 @@ [email protected]          X                               
                                        X
 [email protected]                X                                               
                        X                                                       
 [email protected]             X                                               
                        X                                                       
 [email protected]              X                                               
                        X                                                       
[email protected]                X                                               
                        X                                                       
 [email protected]         X                       X                               
                X                                                       
 [email protected]          X                       X                       
                        X                                                       
 [email protected]          X                                                       
                X                                                       
@@ -345,7 +337,6 @@ [email protected]              X
 [email protected]                                                           
                        X                                                       
 [email protected]                                                            
                        X                                                       
 [email protected]                                                         
                        X                                                       
[email protected]                                                               
                        X                                                       
 [email protected]                                                               
                        X                                                       
 [email protected]           X                                                       
                                                                        
 [email protected]                X                                       
                                                                                
        
@@ -403,7 +394,6 @@ [email protected]           X                               
                                        X
 [email protected]               X                                               
                        X                                                       
 [email protected]                X                                               
                        X                                                       
 [email protected]                X                                               
                        X                                                       
[email protected]               X                                               
                        X                                                       
 [email protected]             X                                               
                        X                                                       
 [email protected]               X                                               
                        X                                                       
 [email protected]           X                                                       
                X                                                       
@@ -424,10 +414,8 @@ [email protected]              X                       
                                                X
 [email protected]         X                                                       
                X                                                       
 [email protected]                                                                
                        X                                                       
 [email protected]                                    X                       
                        X                                                       
[email protected]               X                                               
                        X                                                       
 [email protected]               X                                               
                        X                                                       
 [email protected]            X                                               
                        X                                                       
[email protected]         X       X                                               
                X                                                       
 [email protected]           X       X                                               
                X                                                       
 [email protected]         X                                                       
X               X                                                       
 [email protected]              X                                       
                X               X                                               
        
@@ -466,7 +454,6 @@ [email protected]                                         
                                        X
 [email protected]               X                                               
                        X                                                       
 [email protected]         X                                                       
                X                                                       
 [email protected]                X                                               
                        X                                                       
[email protected]                X                                       
                                                                                
        
 [email protected]                X                                       
                                                                                
        
 [email protected]                                                                    
                X                                                       
 [email protected]                X                                               
                        X                                                       
@@ -494,23 +481,15 @@ [email protected]                                       
                                                X
 [email protected]             X                                       
                                X                                               
        
 [email protected]         X                                       
                                X                                               
        
 [email protected]           X                                                       
                X                                                       
[email protected]                                                                 
                X                                                       
 [email protected]                                                                 
                X                                                       
 [email protected]                                                       
                                X                                               
        
[email protected]                                                        
                                X                                               
        
[email protected]                                                         
                        X                                                       
 [email protected]                                                         
                        X                                                       
 [email protected]                                                          
                        X                                                       
[email protected]                                                           
                        X                                                       
 [email protected]                                                              
                        X                               X                       
 [email protected]                                                           
                        X                                                       
[email protected]                                                   
                                X                                               
        
 [email protected]                                                   
                                X                                               
        
[email protected]                                                          
                        X                                                       
 [email protected]                                                          
                        X                                                       
[email protected]                                                          
                        X                                                       
 [email protected]                                                          
                        X                                                       
[email protected]                                                    
                                X                                               
        
 [email protected]                                                    
                                X                                               
        
 [email protected]                                                                      
                X                                                       
 [email protected]                X                                               
                        X                                                       
@@ -611,7 +590,6 @@ [email protected]          X                               
                                        X
 [email protected]         X                                               
                        X                                                       
 [email protected]           X                                               
                        X                                                       
 [email protected]          X                                               
                        X                                                       
[email protected]             X                                               
                        X                                                       
 [email protected]             X                                               
                        X                                                       
 [email protected]             X                                               
                        X                                                       
 [email protected]         X                                               
                        X                                                       
diff --git a/crates/integrations/datafusion/tests/read_tables.rs 
b/crates/integrations/datafusion/tests/read_tables.rs
index 87655c25..b4637b5b 100644
--- a/crates/integrations/datafusion/tests/read_tables.rs
+++ b/crates/integrations/datafusion/tests/read_tables.rs
@@ -1461,34 +1461,21 @@ async fn 
test_case_insensitive_column_not_supported_via_sql() {
 mod fulltext_tests {
     use std::sync::Arc;
 
+    use bytes::Bytes;
     use datafusion::arrow::array::Int32Array;
     use paimon::catalog::Identifier;
+    use paimon::spec::{GlobalIndexMeta, IndexFileMeta};
     use paimon::table::BranchManager;
-    use paimon::{Catalog, CatalogOptions, FileSystemCatalog, Options};
+    use paimon::{Catalog, CatalogOptions, CommitMessage, FileSystemCatalog, 
Options, TableCommit};
     use paimon_datafusion::{register_full_text_search, SQLContext};
 
     use super::common::string_value;
+    use paimon_ftindex_core::io::PosWriter;
+    use paimon_ftindex_core::{FullTextIndexConfig, FullTextIndexWriter};
 
-    /// Extract the bundled tar.gz into a temp dir and return (tempdir, 
warehouse_path).
-    fn extract_test_warehouse() -> (tempfile::TempDir, String) {
-        let archive_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
-            .join("testdata/test_tantivy_fulltext.tar.gz");
-        let file = std::fs::File::open(&archive_path)
-            .unwrap_or_else(|e| panic!("Failed to open {}: {e}", 
archive_path.display()));
-        let decoder = flate2::read::GzDecoder::new(file);
-        let mut archive = tar::Archive::new(decoder);
-
+    async fn create_fulltext_context() -> (SQLContext, Arc<FileSystemCatalog>, 
tempfile::TempDir) {
         let tmp = tempfile::tempdir().expect("Failed to create temp dir");
-        let db_dir = tmp.path().join("default.db");
-        std::fs::create_dir_all(&db_dir).unwrap();
-        archive.unpack(&db_dir).unwrap();
-
         let warehouse = format!("file://{}", tmp.path().display());
-        (tmp, warehouse)
-    }
-
-    async fn create_fulltext_context() -> (SQLContext, Arc<FileSystemCatalog>, 
tempfile::TempDir) {
-        let (tmp, warehouse) = extract_test_warehouse();
         let mut options = Options::new();
         options.set(CatalogOptions::WAREHOUSE, warehouse);
         let catalog = Arc::new(FileSystemCatalog::new(options).expect("Failed 
to create catalog"));
@@ -1498,9 +1485,113 @@ mod fulltext_tests {
             .await
             .expect("Failed to register catalog");
         register_full_text_search(ctx.ctx(), catalog.clone(), "default");
+        run_fulltext_sql(
+            &ctx,
+            "CREATE TABLE paimon.default.test_fulltext (
+                id INT,
+                content STRING
+            ) WITH (
+                'row-tracking.enabled' = 'true',
+                'global-index.search-mode' = 'full'
+            )",
+        )
+        .await;
+        for insert in [
+            "INSERT INTO paimon.default.test_fulltext VALUES (0, 'alphaft 
lakehouse storage')",
+            "INSERT INTO paimon.default.test_fulltext VALUES (1, 'betafft 
gammaft full-text search')",
+            "INSERT INTO paimon.default.test_fulltext VALUES (2, 'alphaft 
supports indexed queries')",
+            "INSERT INTO paimon.default.test_fulltext VALUES (3, 'gammaft over 
raw fallback rows')",
+            "INSERT INTO paimon.default.test_fulltext VALUES (4, 'alphaft 
tables can be queried')",
+        ] {
+            run_fulltext_sql(&ctx, insert).await;
+        }
+        build_fulltext_index(catalog.as_ref()).await;
         (ctx, catalog, tmp)
     }
 
+    async fn build_fulltext_index(catalog: &dyn Catalog) {
+        let table = catalog
+            .get_table(&Identifier::new("default", "test_fulltext"))
+            .await
+            .expect("table should exist");
+        let content_field_id = table
+            .schema()
+            .fields()
+            .iter()
+            .find(|field| field.name() == "content")
+            .expect("content field")
+            .id();
+
+        let mut writer =
+            
FullTextIndexWriter::new(FullTextIndexConfig::new().with_text_fields(["content"]))
+                .expect("full-text writer");
+        for (row_id, content) in [
+            (0, "alphaft lakehouse storage"),
+            (1, "betafft gammaft full-text search"),
+            (2, "alphaft supports indexed queries"),
+            (3, "gammaft over raw fallback rows"),
+            (4, "alphaft tables can be queried"),
+        ] {
+            writer
+                .add_document_fields(row_id, [("content", content)])
+                .expect("add full-text document");
+        }
+        let mut index_bytes = Vec::new();
+        writer
+            .write(&mut PosWriter::new(&mut index_bytes))
+            .expect("write full-text index");
+
+        let index_file_name = "ft-datafusion-fulltext.index";
+        table
+            .file_io()
+            .mkdirs(&format!(
+                "{}/index/",
+                table.location().trim_end_matches('/')
+            ))
+            .await
+            .expect("create index dir");
+        table
+            .file_io()
+            .new_output(&format!(
+                "{}/index/{index_file_name}",
+                table.location().trim_end_matches('/')
+            ))
+            .expect("index output")
+            .write(Bytes::from(index_bytes.clone()))
+            .await
+            .expect("write index file");
+
+        let mut message = CommitMessage::new(Vec::new(), 0, Vec::new());
+        message.new_index_files = vec![IndexFileMeta {
+            index_type: "full-text".to_string(),
+            file_name: index_file_name.to_string(),
+            file_size: i64::try_from(index_bytes.len()).unwrap(),
+            row_count: 5,
+            deletion_vectors_ranges: None,
+            global_index_meta: Some(GlobalIndexMeta {
+                row_range_start: 0,
+                row_range_end: 4,
+                index_field_id: content_field_id,
+                extra_field_ids: None,
+                index_meta: None,
+                source_meta: None,
+            }),
+        }];
+        TableCommit::new(table, "test-user".to_string())
+            .commit(vec![message])
+            .await
+            .expect("commit full-text index manifest");
+    }
+
+    async fn run_fulltext_sql(ctx: &SQLContext, sql: &str) {
+        ctx.sql(sql)
+            .await
+            .unwrap_or_else(|e| panic!("Failed to plan `{sql}`: {e}"))
+            .collect()
+            .await
+            .unwrap_or_else(|e| panic!("Failed to execute `{sql}`: {e}"));
+    }
+
     fn extract_id_content_rows(
         batches: &[datafusion::arrow::record_batch::RecordBatch],
     ) -> Vec<(i32, String)> {
@@ -1524,12 +1615,12 @@ mod fulltext_tests {
         rows
     }
 
-    /// Search for 'paimon' — rows 0, 2, 4 mention "paimon".
+    /// Search for 'alphaft' — rows 0, 2, 4 contain the token.
     #[tokio::test]
     async fn test_full_text_search_paimon() {
         let (ctx, _catalog, _tmp) = create_fulltext_context().await;
         let batches = ctx
-            .sql("SELECT id, content FROM 
full_text_search('paimon.default.test_tantivy_fulltext', 'content', 'paimon', 
10)")
+            .sql("SELECT id, content FROM 
full_text_search('paimon.default.test_fulltext', 'content', 'alphaft', 10)")
             .await
             .expect("SQL should parse")
             .collect()
@@ -1541,14 +1632,14 @@ mod fulltext_tests {
         assert_eq!(
             ids,
             vec![0, 2, 4],
-            "Searching 'paimon' should match rows 0, 2, 4"
+            "Searching 'alphaft' should match rows 0, 2, 4"
         );
     }
 
     #[tokio::test]
     async fn test_full_text_search_branch() {
         let (ctx, catalog, _tmp) = create_fulltext_context().await;
-        let identifier = Identifier::new("default", "test_tantivy_fulltext");
+        let identifier = Identifier::new("default", "test_fulltext");
         let table = catalog.get_table(&identifier).await.expect("load table");
         let snapshot_manager = table.snapshot_manager();
         let snapshot = snapshot_manager
@@ -1572,7 +1663,7 @@ mod fulltext_tests {
             .expect("delete main snapshots");
 
         let batches = ctx
-            .sql("SELECT id, content FROM 
full_text_search('paimon.default.test_tantivy_fulltext$branch_b1', 'content', 
'paimon', 10)")
+            .sql("SELECT id, content FROM 
full_text_search('paimon.default.test_fulltext$branch_b1', 'content', 
'alphaft', 10)")
             .await
             .expect("SQL should parse")
             .collect()
@@ -1584,12 +1675,12 @@ mod fulltext_tests {
         assert_eq!(ids, vec![0, 2, 4]);
     }
 
-    /// Search for 'tantivy' — only row 1.
+    /// Search for 'betafft' — only row 1.
     #[tokio::test]
-    async fn test_full_text_search_tantivy() {
+    async fn test_full_text_search_betafft() {
         let (ctx, _catalog, _tmp) = create_fulltext_context().await;
         let batches = ctx
-            .sql("SELECT id, content FROM 
full_text_search('paimon.default.test_tantivy_fulltext', 'content', 'tantivy', 
10)")
+            .sql("SELECT id, content FROM 
full_text_search('paimon.default.test_fulltext', 'content', 'betafft', 10)")
             .await
             .expect("SQL should parse")
             .collect()
@@ -1598,15 +1689,15 @@ mod fulltext_tests {
 
         let rows = extract_id_content_rows(&batches);
         let ids: Vec<i32> = rows.iter().map(|(id, _)| *id).collect();
-        assert_eq!(ids, vec![1], "Searching 'tantivy' should match row 1");
+        assert_eq!(ids, vec![1], "Searching 'betafft' should match row 1");
     }
 
-    /// Search for 'search' — rows 1, 3 mention "full-text search".
+    /// Search for 'gammaft' — rows 1, 3 contain the token.
     #[tokio::test]
     async fn test_full_text_search_search() {
         let (ctx, _catalog, _tmp) = create_fulltext_context().await;
         let batches = ctx
-            .sql("SELECT id, content FROM 
full_text_search('paimon.default.test_tantivy_fulltext', 'content', 'search', 
10)")
+            .sql("SELECT id, content FROM 
full_text_search('paimon.default.test_fulltext', 'content', 'gammaft', 10)")
             .await
             .expect("SQL should parse")
             .collect()
@@ -1615,8 +1706,11 @@ mod fulltext_tests {
 
         let rows = extract_id_content_rows(&batches);
         let ids: Vec<i32> = rows.iter().map(|(id, _)| *id).collect();
-        assert!(ids.contains(&1), "Searching 'search' should match row 1");
-        assert!(ids.contains(&3), "Searching 'search' should match row 3");
+        assert_eq!(
+            ids,
+            vec![1, 3],
+            "Searching 'gammaft' should match rows 1, 3"
+        );
     }
 }
 
diff --git a/crates/paimon/Cargo.toml b/crates/paimon/Cargo.toml
index a2042c5a..043f95f8 100644
--- a/crates/paimon/Cargo.toml
+++ b/crates/paimon/Cargo.toml
@@ -42,7 +42,7 @@ storage-all = [
     "storage-gcs",
     "storage-hdfs",
 ]
-fulltext = ["tantivy", "tempfile", "dep:paimon-ftindex-core"]
+fulltext = ["dep:paimon-ftindex-core", "dep:tempfile"]
 vortex = ["dep:vortex"]
 
 storage-memory = ["opendal/services-memory"]
@@ -112,10 +112,9 @@ md-5 = "0.10"
 regex = "1"
 uuid = { version = "1", features = ["v4"] }
 urlencoding = "2.1"
-tantivy = { version = "0.22", optional = true }
-tempfile = { version = "3", optional = true }
-paimon-ftindex-core = { version = "0.1.0", optional = true }
 paimon-mosaic-core = "0.2.0"
+paimon-ftindex-core = { version = "0.1.0", optional = true }
+tempfile = { version = "3", optional = true }
 paimon-vindex-core = "0.2.0"
 vortex = { version = "0.75.0", features = ["tokio"], optional = true }
 libloading = "0.9"
diff --git a/crates/paimon/DEPENDENCIES.rust.tsv 
b/crates/paimon/DEPENDENCIES.rust.tsv
index b032f87b..d78da445 100644
--- a/crates/paimon/DEPENDENCIES.rust.tsv
+++ b/crates/paimon/DEPENDENCIES.rust.tsv
@@ -124,7 +124,6 @@ [email protected]               X                               
                                        X
 [email protected]               X                                               
                        X                                               
 [email protected]         X                                                       
                X                                               
 [email protected]               X                                               
                        X                                               
[email protected]              X                                               
                        X                                               
 [email protected]              X                                               
                        X                                               
 [email protected]           X                                               
                                                                        
 [email protected]            X                                       X               
                        X                                       
@@ -148,13 +147,11 @@ [email protected]             X                       
                                                X
 [email protected]           X                                               
                                                                        
 [email protected]           X                                                       
                X                                               
 [email protected]              X                                                       
                X                                               
[email protected]                                                                 
                                                        X       
 [email protected]                                                                 
                                                        X       
 [email protected]            X                                               
                        X                                               
 [email protected]             X                                       
                                X                                               
 [email protected]          X                                               
                        X                                               
 [email protected]             X                                                       
                X                                               
[email protected]              X                                                       
                X                                               
 [email protected]                                                                 
                X                                               
 [email protected]         X                                                       
                                                                
 [email protected]                                                                    
                X                                               
@@ -179,7 +176,6 @@ [email protected]          X                                       
                                X
 [email protected]                                                                      
                X                                               
 [email protected]             X                                                       
                X                                               
 [email protected]               X                                               
                        X                                               
[email protected]               X                                               
                        X                                               
 [email protected]               X                                               
                        X                                               
 [email protected]               X                                               
                        X                                               
 [email protected]             X                                               
                                                                        
@@ -217,11 +213,9 @@ [email protected]                X
 [email protected]           X                                       
                                                                                
 [email protected]                X                                               
                        X                                               
 [email protected]            X                                                       
                X                                               
[email protected]                                 X                               
                                                                
 [email protected]                                                         
                        X                                               
 [email protected]               X                                               
                        X                                               
 [email protected]           X                                                       
                X                                               
[email protected]               X                                               
                        X                                               
 [email protected]               X                                               
                        X                                               
 [email protected]               X                                               
                        X                                               
 [email protected]            X                                                       
                X                                               
@@ -257,11 +251,9 @@ [email protected]                                           
                        X
 [email protected]                                                                    
                X                                               
 [email protected]                                                                
                        X                                               
 [email protected]           X       X                                       
                        X                                               
[email protected]           X       X                                       
                        X                                               
 [email protected]                                                                  
                                        X                       
 [email protected]                X                                               
                        X                                               
 [email protected]             X                                                       
                X                                               
[email protected]                                                                     
                X                                               
 [email protected]                                                                     
                X                                               
 [email protected]                                                                
                        X                                               
 [email protected]                                                                
                        X                                               
@@ -272,7 +264,6 @@ [email protected]               X                       
                                                X
 [email protected]            X                                                       
                X                                               
 [email protected]            X                                                       
                X                                               
 [email protected]              X                                                       
                                                                
[email protected]                                                             
                        X                                               
 [email protected]                                                             
                        X                                               
 [email protected]                                                                   
                X                               X               
 [email protected]         X                                                       
                X                                               
@@ -299,7 +290,6 @@ [email protected]          X                               
                                        X
 [email protected]                X                                               
                        X                                               
 [email protected]             X                                               
                        X                                               
 [email protected]              X                                               
                        X                                               
[email protected]                X                                               
                        X                                               
 [email protected]         X                       X                               
                X                                               
 [email protected]          X                       X                       
                        X                                               
 [email protected]               X                                               
                        X                                               
@@ -324,7 +314,6 @@ [email protected]              X
 [email protected]                                                           
                        X                                               
 [email protected]                                                            
                        X                                               
 [email protected]                                                         
                        X                                               
[email protected]                                                               
                        X                                               
 [email protected]                                                               
                        X                                               
 [email protected]           X                                                       
                                                                
 [email protected]              X                                       
                                                                                
@@ -384,7 +373,6 @@ [email protected]           X                               
                                        X
 [email protected]               X                                               
                        X                                               
 [email protected]                X                                               
                        X                                               
 [email protected]                X                                               
                        X                                               
[email protected]               X                                               
                        X                                               
 [email protected]             X                                               
                        X                                               
 [email protected]               X                                               
                        X                                               
 [email protected]           X                                                       
                X                                               
@@ -411,10 +399,8 @@ [email protected]           X                               
                                        X
 [email protected]             X                                                       
                X                                               
 [email protected]                                                                
                        X                                               
 [email protected]                                    X                       
                        X                                               
[email protected]               X                                               
                        X                                               
 [email protected]               X                                               
                        X                                               
 [email protected]            X                                               
                        X                                               
[email protected]         X       X                                               
                X                                               
 [email protected]           X       X                                               
                X                                               
 [email protected]         X                                                       
X               X                                               
 [email protected]              X                                       
                X               X                                               
@@ -456,7 +442,6 @@ [email protected]                                         
                                        X
 [email protected]               X                                               
                        X                                               
 [email protected]         X                                                       
                X                                               
 [email protected]                X                                               
                        X                                               
[email protected]                X                                       
                                                                                
 [email protected]                X                                       
                                                                                
 [email protected]                                                                    
                X                                               
 [email protected]                X                                               
                        X                                               
@@ -484,23 +469,15 @@ [email protected]                                       
                                                X
 [email protected]             X                                       
                                X                                               
 [email protected]         X                                       
                                X                                               
 [email protected]           X                                                       
                X                                               
[email protected]                                                                 
                X                                               
 [email protected]                                                                 
                X                                               
 [email protected]                                                       
                                X                                               
[email protected]                                                        
                                X                                               
[email protected]                                                         
                        X                                               
 [email protected]                                                         
                        X                                               
 [email protected]                                                          
                        X                                               
[email protected]                                                           
                        X                                               
 [email protected]                                                              
                        X                               X               
 [email protected]                                                           
                        X                                               
[email protected]                                                   
                                X                                               
 [email protected]                                                   
                                X                                               
[email protected]                                                          
                        X                                               
 [email protected]                                                          
                        X                                               
[email protected]                                                          
                        X                                               
 [email protected]                                                          
                        X                                               
[email protected]                                                    
                                X                                               
 [email protected]                                                    
                                X                                               
 [email protected]                                                                      
                X                                               
 [email protected]                X                                               
                        X                                               
@@ -602,7 +579,6 @@ [email protected]          X                               
                                        X
 [email protected]         X                                               
                        X                                               
 [email protected]           X                                               
                        X                                               
 [email protected]          X                                               
                        X                                               
[email protected]             X                                               
                        X                                               
 [email protected]             X                                               
                        X                                               
 [email protected]             X                                               
                        X                                               
 [email protected]             X                                               
                        X                                               
diff --git a/crates/paimon/src/tantivy/full_text_search.rs 
b/crates/paimon/src/full_text.rs
similarity index 69%
rename from crates/paimon/src/tantivy/full_text_search.rs
rename to crates/paimon/src/full_text.rs
index 2f57ef8a..0c0b5f71 100644
--- a/crates/paimon/src/tantivy/full_text_search.rs
+++ b/crates/paimon/src/full_text.rs
@@ -15,18 +15,13 @@
 // specific language governing permissions and limitations
 // under the License.
 
-//! Full-text search types for global index.
-//!
-//! Reference: 
[org.apache.paimon.predicate.FullTextSearch](https://github.com/apache/paimon/blob/master/paimon-common/src/main/java/org/apache/paimon/predicate/FullTextSearch.java)
-
 /// Full-text search predicate.
-///
-/// Reference: `org.apache.paimon.predicate.FullTextSearch`
 #[derive(Debug, Clone)]
 pub struct FullTextSearch {
     pub query_text: String,
     pub field_name: String,
     pub limit: usize,
+    pub include_row_ids: Option<roaring::RoaringTreemap>,
 }
 
 impl FullTextSearch {
@@ -50,8 +45,14 @@ impl FullTextSearch {
             query_text,
             field_name,
             limit,
+            include_row_ids: None,
         })
     }
+
+    pub fn with_include_row_ids(mut self, include_row_ids: 
roaring::RoaringTreemap) -> Self {
+        self.include_row_ids = Some(include_row_ids);
+        self
+    }
 }
 
 /// Search result containing parallel arrays of row IDs and scores.
@@ -108,8 +109,18 @@ impl SearchResult {
     pub fn or(&self, other: &SearchResult) -> Self {
         let mut row_ids = self.row_ids.clone();
         let mut scores = self.scores.clone();
-        row_ids.extend_from_slice(&other.row_ids);
-        scores.extend_from_slice(&other.scores);
+        row_ids.reserve(other.row_ids.len());
+        scores.reserve(other.scores.len());
+        let mut seen = row_ids
+            .iter()
+            .copied()
+            .collect::<std::collections::HashSet<_>>();
+        for (&row_id, &score) in other.row_ids.iter().zip(&other.scores) {
+            if seen.insert(row_id) {
+                row_ids.push(row_id);
+                scores.push(score);
+            }
+        }
         Self { row_ids, scores }
     }
 
@@ -155,6 +166,31 @@ impl SearchResult {
         Ok(Self { row_ids, scores })
     }
 
+    pub(crate) fn without_row_ranges(
+        &self,
+        ranges: &[crate::table::RowRange],
+    ) -> crate::Result<Self> {
+        if ranges.is_empty() {
+            return Ok(self.clone());
+        }
+        let index = 
crate::table::global_index_scanner::RowRangeIndex::create(ranges.to_vec());
+        let mut row_ids = Vec::with_capacity(self.row_ids.len());
+        let mut scores = Vec::with_capacity(self.scores.len());
+        for (&row_id, &score) in self.row_ids.iter().zip(&self.scores) {
+            let row_id_i64 = i64::try_from(row_id).map_err(|_| 
crate::Error::DataInvalid {
+                message: format!(
+                    "Full-text search row id {row_id} exceeds i64::MAX and 
cannot be checked against row ranges"
+                ),
+                source: None,
+            })?;
+            if !index.intersects(row_id_i64, row_id_i64) {
+                row_ids.push(row_id);
+                scores.push(score);
+            }
+        }
+        Ok(Self { row_ids, scores })
+    }
+
     /// Convert to sorted, merged row ranges.
     pub fn to_row_ranges(&self) -> Vec<crate::table::RowRange> {
         if self.row_ids.is_empty() {
@@ -191,6 +227,18 @@ mod tests {
         assert_eq!(fts.query_text, "hello");
         assert_eq!(fts.limit, 10);
         assert_eq!(fts.field_name, "text");
+        assert!(fts.include_row_ids.is_none());
+    }
+
+    #[test]
+    fn test_full_text_search_clone_preserves_include_row_ids() {
+        let mut include_row_ids = roaring::RoaringTreemap::new();
+        include_row_ids.insert(42);
+        let search = FullTextSearch::new("hello".into(), 10, "text".into())
+            .unwrap()
+            .with_include_row_ids(include_row_ids.clone());
+
+        assert_eq!(search.clone().include_row_ids, Some(include_row_ids));
     }
 
     #[test]
@@ -213,4 +261,27 @@ mod tests {
         assert_eq!(filtered.row_ids, vec![1, 4]);
         assert_eq!(filtered.scores, vec![0.1, 0.2]);
     }
+
+    #[test]
+    fn test_search_result_or_preserves_left_score_and_order() {
+        let left = SearchResult::new(vec![1, 2], vec![0.4, 0.5]);
+        let right = SearchResult::new(vec![2, 3], vec![0.8, 0.6]);
+
+        let merged = left.or(&right);
+
+        assert_eq!(merged.row_ids, vec![1, 2, 3]);
+        assert_eq!(merged.scores, vec![0.4, 0.5, 0.6]);
+    }
+
+    #[test]
+    fn test_search_result_without_row_ranges() {
+        let result = SearchResult::new(vec![1, 2, 3, 4], vec![0.1, 0.9, 0.8, 
0.2]);
+
+        let filtered = result
+            .without_row_ranges(&[crate::table::RowRange::new(2, 3)])
+            .unwrap();
+
+        assert_eq!(filtered.row_ids, vec![1, 4]);
+        assert_eq!(filtered.scores, vec![0.1, 0.2]);
+    }
 }
diff --git a/crates/paimon/src/lib.rs b/crates/paimon/src/lib.rs
index 85ad6b51..e372a40b 100644
--- a/crates/paimon/src/lib.rs
+++ b/crates/paimon/src/lib.rs
@@ -32,13 +32,13 @@ mod deletion_vector;
 pub mod file_index;
 #[cfg(feature = "fulltext")]
 pub mod ftindex;
+#[cfg(feature = "fulltext")]
+pub mod full_text;
 pub mod io;
 pub mod lumina;
 mod predicate_stats;
 pub mod spec;
 pub mod table;
-#[cfg(feature = "fulltext")]
-pub mod tantivy;
 pub mod variant;
 pub mod vector_search;
 pub mod vindex;
diff --git a/crates/paimon/src/table/data_file_reader.rs 
b/crates/paimon/src/table/data_file_reader.rs
index 7c4861f7..b94016d1 100644
--- a/crates/paimon/src/table/data_file_reader.rs
+++ b/crates/paimon/src/table/data_file_reader.rs
@@ -170,7 +170,7 @@ impl DataFileReader {
                         file_meta,
                         data_fields,
                         dv,
-                        None,
+                        split.row_ranges().map(|ranges| ranges.to_vec()),
                     )?;
                     while let Some(batch) = stream.next().await {
                         yield batch?;
diff --git a/crates/paimon/src/table/full_text_index_adapter.rs 
b/crates/paimon/src/table/full_text_index_adapter.rs
new file mode 100644
index 00000000..680d1c3d
--- /dev/null
+++ b/crates/paimon/src/table/full_text_index_adapter.rs
@@ -0,0 +1,411 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use crate::ftindex::reader::FullTextArchiveReader;
+use crate::io::FileRead;
+use bytes::Bytes;
+use futures::StreamExt;
+use paimon_ftindex_core::io::{ReadRequest, SeekRead};
+use paimon_ftindex_core::FullTextSearchResult;
+use roaring::RoaringTreemap;
+use std::fs::File;
+use std::io::{self, Read, Seek, SeekFrom};
+use std::sync::{mpsc, Arc, Mutex};
+
+const FULL_TEXT_RANGE_READ_CONCURRENCY: usize = 8;
+
+pub(crate) async fn search_full_text_index(
+    reader: Box<dyn FileRead>,
+    file_name: String,
+    query: String,
+    limit: usize,
+    include_row_ids: Option<RoaringTreemap>,
+) -> crate::Result<FullTextSearchResult> {
+    let handle =
+        tokio::runtime::Handle::try_current().map_err(|error| 
crate::Error::UnexpectedError {
+            message: "Full-text index reader requires a Tokio 
runtime".to_string(),
+            source: Some(Box::new(error)),
+        })?;
+    let task_file_name = file_name.clone();
+    tokio::task::spawn_blocking(move || {
+        search_native_index(
+            AsyncFileSeekRead::new(reader, handle),
+            &task_file_name,
+            &query,
+            limit,
+            include_row_ids,
+        )
+    })
+    .await
+    .map_err(|error| crate::Error::UnexpectedError {
+        message: format!("Full-text index task for '{file_name}' failed: 
{error}"),
+        source: None,
+    })?
+}
+
+pub(crate) async fn search_full_text_file(
+    file: File,
+    file_name: String,
+    query: String,
+    limit: usize,
+    include_row_ids: Option<RoaringTreemap>,
+) -> crate::Result<FullTextSearchResult> {
+    let task_file_name = file_name.clone();
+    tokio::task::spawn_blocking(move || {
+        search_native_index(
+            StdFileSeekRead::new(file),
+            &task_file_name,
+            &query,
+            limit,
+            include_row_ids,
+        )
+    })
+    .await
+    .map_err(|error| crate::Error::UnexpectedError {
+        message: format!("Full-text index task for '{file_name}' failed: 
{error}"),
+        source: None,
+    })?
+}
+
+fn search_native_index<R: SeekRead + 'static>(
+    input: R,
+    file_name: &str,
+    query: &str,
+    limit: usize,
+    include_row_ids: Option<RoaringTreemap>,
+) -> crate::Result<FullTextSearchResult> {
+    let reader = FullTextArchiveReader::from_seek_read(input)
+        .map_err(|error| native_error(file_name, error))?;
+    let hits = match include_row_ids {
+        Some(include) => reader.search_with_include(query, limit, &include),
+        None => reader.search(query, limit),
+    }
+    .map_err(|error| native_error(file_name, error))?;
+    Ok(FullTextSearchResult {
+        row_ids: hits.row_ids,
+        scores: hits.scores,
+    })
+}
+
+fn native_error(file_name: &str, error: impl std::fmt::Display) -> 
crate::Error {
+    crate::Error::UnexpectedError {
+        message: format!("Failed to read full-text index '{file_name}': 
{error}"),
+        source: None,
+    }
+}
+
+struct AsyncFileSeekRead {
+    reader: Arc<dyn FileRead>,
+    handle: tokio::runtime::Handle,
+    permits: Arc<tokio::sync::Semaphore>,
+}
+
+impl AsyncFileSeekRead {
+    fn new(reader: Box<dyn FileRead>, handle: tokio::runtime::Handle) -> Self {
+        Self {
+            reader: Arc::from(reader),
+            handle,
+            permits: Arc::new(tokio::sync::Semaphore::new(
+                FULL_TEXT_RANGE_READ_CONCURRENCY,
+            )),
+        }
+    }
+}
+
+impl SeekRead for AsyncFileSeekRead {
+    fn pread(&self, requests: &mut [ReadRequest<'_>]) -> io::Result<()> {
+        if requests.is_empty() {
+            return Ok(());
+        }
+        let ranges = requests
+            .iter()
+            .map(|request| {
+                let length = u64::try_from(request.buf.len()).map_err(|_| {
+                    io::Error::new(
+                        io::ErrorKind::InvalidInput,
+                        "full-text read length overflow",
+                    )
+                })?;
+                let end = request.pos.checked_add(length).ok_or_else(|| {
+                    io::Error::new(io::ErrorKind::InvalidInput, "full-text 
read range overflow")
+                })?;
+                Ok((request.pos, end))
+            })
+            .collect::<io::Result<Vec<_>>>()?;
+
+        let reader = Arc::clone(&self.reader);
+        let permits = Arc::clone(&self.permits);
+        let (tx, rx) = mpsc::sync_channel(1);
+        self.handle.spawn(async move {
+            let results = 
futures::stream::iter(ranges.into_iter().map(|(start, end)| {
+                let reader = Arc::clone(&reader);
+                let permits = Arc::clone(&permits);
+                async move {
+                    if start == end {
+                        return Ok(Bytes::new());
+                    }
+                    let _permit = permits
+                        .acquire_owned()
+                        .await
+                        .map_err(|_| io::Error::other("full-text range reader 
closed"))?;
+                    reader
+                        .read(start..end)
+                        .await
+                        .map_err(|error| io::Error::other(error.to_string()))
+                }
+            }))
+            .buffered(FULL_TEXT_RANGE_READ_CONCURRENCY)
+            .collect::<Vec<_>>()
+            .await;
+            let _ = tx.send(results);
+        });
+
+        let results = rx
+            .recv()
+            .map_err(|_| io::Error::other("full-text async read task was 
cancelled"))?;
+        for (request, result) in requests.iter_mut().zip(results) {
+            let bytes = result?;
+            if bytes.len() != request.buf.len() {
+                return Err(io::Error::new(
+                    io::ErrorKind::UnexpectedEof,
+                    format!(
+                        "full-text read expected {} bytes, got {}",
+                        request.buf.len(),
+                        bytes.len()
+                    ),
+                ));
+            }
+            request.buf.copy_from_slice(&bytes);
+        }
+        Ok(())
+    }
+}
+
+struct StdFileSeekRead {
+    file: Mutex<File>,
+}
+
+impl StdFileSeekRead {
+    fn new(file: File) -> Self {
+        Self {
+            file: Mutex::new(file),
+        }
+    }
+}
+
+impl SeekRead for StdFileSeekRead {
+    fn pread(&self, requests: &mut [ReadRequest<'_>]) -> io::Result<()> {
+        let mut file = self
+            .file
+            .lock()
+            .map_err(|_| io::Error::other("full-text temporary file lock 
poisoned"))?;
+        for request in requests {
+            file.seek(SeekFrom::Start(request.pos))?;
+            file.read_exact(request.buf)?;
+        }
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use paimon_ftindex_core::io::PosWriter;
+    use paimon_ftindex_core::{FullTextIndexConfig, FullTextIndexWriter};
+    use std::ops::Range;
+    use std::sync::atomic::{AtomicUsize, Ordering};
+
+    struct BytesFileRead {
+        data: Bytes,
+        ranges: Arc<Mutex<Vec<Range<u64>>>>,
+    }
+
+    #[async_trait::async_trait]
+    impl FileRead for BytesFileRead {
+        async fn read(&self, range: Range<u64>) -> crate::Result<Bytes> {
+            self.ranges.lock().unwrap().push(range.clone());
+            tokio::task::yield_now().await;
+            let start = usize::try_from(range.start).unwrap();
+            let end = usize::try_from(range.end).unwrap();
+            Ok(self.data.slice(start..end))
+        }
+    }
+
+    fn test_index_bytes() -> Vec<u8> {
+        let mut writer = 
FullTextIndexWriter::new(FullTextIndexConfig::new()).unwrap();
+        writer.add_document(7, "apache paimon").unwrap();
+        writer.add_document(9, "unrelated").unwrap();
+        let mut bytes = Vec::new();
+        writer.write(&mut PosWriter::new(&mut bytes)).unwrap();
+        bytes
+    }
+
+    #[test]
+    fn test_range_backed_search_works_on_current_thread_runtime() {
+        let runtime = tokio::runtime::Builder::new_current_thread()
+            .enable_all()
+            .build()
+            .unwrap();
+        runtime.block_on(async {
+            let data = Bytes::from(test_index_bytes());
+            let ranges = Arc::new(Mutex::new(Vec::new()));
+            let reader = BytesFileRead {
+                data,
+                ranges: Arc::clone(&ranges),
+            };
+
+            let result = search_full_text_index(
+                Box::new(reader),
+                "test.index".to_string(),
+                r#"{"match":{"query":"paimon"}}"#.to_string(),
+                10,
+                None,
+            )
+            .await
+            .unwrap();
+
+            assert_eq!(result.row_ids, vec![7]);
+            assert!(!ranges.lock().unwrap().is_empty());
+        });
+    }
+
+    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+    async fn test_range_backed_search_works_on_multi_thread_runtime() {
+        let data = Bytes::from(test_index_bytes());
+        let ranges = Arc::new(Mutex::new(Vec::new()));
+        let result = search_full_text_index(
+            Box::new(BytesFileRead {
+                data,
+                ranges: Arc::clone(&ranges),
+            }),
+            "test.index".to_string(),
+            r#"{"match":{"query":"paimon"}}"#.to_string(),
+            10,
+            None,
+        )
+        .await
+        .unwrap();
+
+        assert_eq!(result.row_ids, vec![7]);
+        assert!(!ranges.lock().unwrap().is_empty());
+    }
+
+    #[test]
+    fn test_async_seek_reader_preserves_multi_range_order() {
+        let runtime = tokio::runtime::Builder::new_current_thread()
+            .enable_all()
+            .build()
+            .unwrap();
+        runtime.block_on(async {
+            let data = Bytes::from_static(b"0123456789");
+            let reader = AsyncFileSeekRead::new(
+                Box::new(BytesFileRead {
+                    data,
+                    ranges: Arc::new(Mutex::new(Vec::new())),
+                }),
+                tokio::runtime::Handle::current(),
+            );
+            let result = tokio::task::spawn_blocking(move || {
+                let mut first = [0u8; 3];
+                let mut second = [0u8; 2];
+                reader.pread(&mut [
+                    ReadRequest::new(4, &mut first),
+                    ReadRequest::new(1, &mut second),
+                ])?;
+                Ok::<_, io::Error>((first, second))
+            })
+            .await
+            .unwrap()
+            .unwrap();
+
+            assert_eq!(&result.0, b"456");
+            assert_eq!(&result.1, b"12");
+        });
+    }
+
+    struct ShortFileRead;
+
+    #[async_trait::async_trait]
+    impl FileRead for ShortFileRead {
+        async fn read(&self, range: Range<u64>) -> crate::Result<Bytes> {
+            let requested = usize::try_from(range.end - range.start).unwrap();
+            Ok(Bytes::from(vec![0; requested.saturating_sub(1)]))
+        }
+    }
+
+    #[tokio::test]
+    async fn test_async_seek_reader_rejects_short_reads() {
+        let reader =
+            AsyncFileSeekRead::new(Box::new(ShortFileRead), 
tokio::runtime::Handle::current());
+        let error = tokio::task::spawn_blocking(move || {
+            let mut buffer = [0u8; 4];
+            reader.pread(&mut [ReadRequest::new(0, &mut buffer)])
+        })
+        .await
+        .unwrap()
+        .unwrap_err();
+
+        assert_eq!(error.kind(), io::ErrorKind::UnexpectedEof);
+    }
+
+    struct ConcurrentFileRead {
+        active: Arc<AtomicUsize>,
+        maximum: Arc<AtomicUsize>,
+    }
+
+    #[async_trait::async_trait]
+    impl FileRead for ConcurrentFileRead {
+        async fn read(&self, range: Range<u64>) -> crate::Result<Bytes> {
+            let active = self.active.fetch_add(1, Ordering::SeqCst) + 1;
+            self.maximum.fetch_max(active, Ordering::SeqCst);
+            tokio::task::yield_now().await;
+            self.active.fetch_sub(1, Ordering::SeqCst);
+            Ok(Bytes::from(vec![
+                0;
+                usize::try_from(range.end - range.start)
+                    .unwrap()
+            ]))
+        }
+    }
+
+    #[tokio::test]
+    async fn test_async_seek_reader_bounds_range_concurrency() {
+        let active = Arc::new(AtomicUsize::new(0));
+        let maximum = Arc::new(AtomicUsize::new(0));
+        let reader = AsyncFileSeekRead::new(
+            Box::new(ConcurrentFileRead {
+                active: Arc::clone(&active),
+                maximum: Arc::clone(&maximum),
+            }),
+            tokio::runtime::Handle::current(),
+        );
+        tokio::task::spawn_blocking(move || {
+            let mut buffers = [[0u8; 1]; FULL_TEXT_RANGE_READ_CONCURRENCY * 2];
+            let mut requests = buffers
+                .iter_mut()
+                .enumerate()
+                .map(|(position, buffer)| ReadRequest::new(position as u64, 
buffer))
+                .collect::<Vec<_>>();
+            reader.pread(&mut requests)
+        })
+        .await
+        .unwrap()
+        .unwrap();
+
+        assert!(maximum.load(Ordering::SeqCst) <= 
FULL_TEXT_RANGE_READ_CONCURRENCY);
+    }
+}
diff --git a/crates/paimon/src/table/full_text_search_builder.rs 
b/crates/paimon/src/table/full_text_search_builder.rs
index 30e1d358..005d65e6 100644
--- a/crates/paimon/src/table/full_text_search_builder.rs
+++ b/crates/paimon/src/table/full_text_search_builder.rs
@@ -19,26 +19,29 @@
 //!
 //! Reference: 
[FullTextSearchBuilderImpl.java](https://github.com/apache/paimon/blob/master/paimon-core/src/main/java/org/apache/paimon/table/source/FullTextSearchBuilderImpl.java)
 
-use crate::io::{FileIO, FileIOBuilder};
+use crate::full_text::{FullTextSearch, SearchResult};
+use crate::io::FileIO;
 use crate::spec::{
     CoreOptions, DataField, FileKind, GlobalIndexSearchMode, IndexFileMeta, 
IndexManifest,
     IndexManifestEntry, ROW_ID_FIELD_NAME,
 };
+use crate::table::full_text_index_adapter::{search_full_text_file, 
search_full_text_index};
 use crate::table::global_index_scanner::{
     deleted_row_ranges_for_data_evolution_dvs, search_limit_with_deleted_rows,
     unindexed_ranges_for_global_index_entries, RowRangeIndex,
 };
 use crate::table::{find_field_id_by_name, merge_row_ranges, RowRange, Table};
-use crate::tantivy::full_text_search::{FullTextSearch, SearchResult};
-use crate::tantivy::reader::TantivyFullTextReader;
-use crate::tantivy::writer::TantivyFullTextWriter;
 use arrow_array::{Array, Int64Array, LargeStringArray, RecordBatch, 
StringArray};
-use futures::TryStreamExt;
+use futures::{StreamExt, TryStreamExt};
+use paimon_ftindex_core::io::PosWriter;
+use paimon_ftindex_core::{FullTextIndexConfig, FullTextIndexWriter};
+use roaring::RoaringTreemap;
+use serde_json::json;
 use std::collections::{HashMap, HashSet};
-use uuid::Uuid;
 
 const INDEX_DIR: &str = "index";
-const TANTIVY_FULLTEXT_INDEX_TYPE: &str = "tantivy-fulltext";
+const FULL_TEXT_INDEX_TYPE: &str = "full-text";
+const FULL_TEXT_INDEX_SEARCH_CONCURRENCY: usize = 8;
 
 /// Builder for executing full-text search on a Paimon table.
 ///
@@ -58,6 +61,7 @@ pub struct FullTextSearchBuilder<'a> {
     text_column: Option<String>,
     query_text: Option<String>,
     limit: Option<usize>,
+    include_row_ids: Option<RoaringTreemap>,
 }
 
 impl<'a> FullTextSearchBuilder<'a> {
@@ -67,6 +71,7 @@ impl<'a> FullTextSearchBuilder<'a> {
             text_column: None,
             query_text: None,
             limit: None,
+            include_row_ids: None,
         }
     }
 
@@ -82,6 +87,12 @@ impl<'a> FullTextSearchBuilder<'a> {
         self
     }
 
+    /// Set candidate row IDs to include in the search.
+    pub fn with_include_row_ids(&mut self, include_row_ids: RoaringTreemap) -> 
&mut Self {
+        self.include_row_ids = Some(include_row_ids);
+        self
+    }
+
     /// Set the top-k limit for results.
     pub fn with_limit(&mut self, limit: usize) -> &mut Self {
         self.limit = Some(limit);
@@ -91,7 +102,7 @@ impl<'a> FullTextSearchBuilder<'a> {
     /// Execute the full-text search and return row ranges.
     ///
     /// This reads the latest snapshot, loads the index manifest, and evaluates
-    /// the search against Tantivy indexes.
+    /// the search against full-text indexes.
     ///
     /// Reference: `FullTextSearchBuilder.executeLocal()`
     pub async fn execute(&self) -> crate::Result<Vec<RowRange>> {
@@ -117,7 +128,14 @@ impl<'a> FullTextSearchBuilder<'a> {
             message: "Limit must be set via with_limit()".to_string(),
         })?;
 
-        let search = FullTextSearch::new(query_text.to_string(), limit, 
text_column.to_string())?;
+        let mut search = FullTextSearch::new(
+            normalize_query_text(query_text, text_column)?,
+            limit,
+            text_column.to_string(),
+        )?;
+        if let Some(include_row_ids) = &self.include_row_ids {
+            search = search.with_include_row_ids(include_row_ids.clone());
+        }
 
         let snapshot_manager = self.table.snapshot_manager();
 
@@ -150,7 +168,7 @@ impl<'a> FullTextSearchBuilder<'a> {
     }
 }
 
-/// Evaluate a full-text search query against Tantivy indexes found in the 
index manifest.
+/// Evaluate a full-text search query against full-text indexes found in the 
index manifest.
 struct FullTextSearchEvaluation<'a> {
     table: Option<&'a Table>,
     file_io: &'a FileIO,
@@ -174,20 +192,21 @@ async fn evaluate_full_text_search(
         None => return Ok(SearchResult::empty()),
     };
 
-    // Collect tantivy fulltext entries for the target field.
+    // Collect full-text entries for the target field.
     let fulltext_entries: Vec<_> = index_entries
         .iter()
         .filter(|e| {
             e.kind == FileKind::Add
-                && e.index_file.index_type == TANTIVY_FULLTEXT_INDEX_TYPE
+                && e.index_file.index_type == FULL_TEXT_INDEX_TYPE
                 && e.index_file
                     .global_index_meta
                     .as_ref()
                     .is_some_and(|m| m.index_field_id == field_id)
         })
         .collect();
+    let has_fulltext_entries = !fulltext_entries.is_empty();
 
-    if fulltext_entries.is_empty() && search_mode == 
GlobalIndexSearchMode::Fast {
+    if !has_fulltext_entries && search_mode == GlobalIndexSearchMode::Fast {
         return Ok(SearchResult::empty());
     }
 
@@ -206,13 +225,16 @@ async fn evaluate_full_text_search(
 
     let mut merged = SearchResult::empty();
     if !fulltext_entries.is_empty() {
-        let futures: Vec<_> = fulltext_entries
-            .into_iter()
-            .map(|entry| {
+        let plans =
+            plan_full_text_index_searches(fulltext_entries, 
search.include_row_ids.as_ref())?;
+        let results = futures::stream::iter(plans)
+            .map(|plan| {
+                let entry = plan.entry;
                 let global_meta = 
entry.index_file.global_index_meta.as_ref().unwrap();
                 let path = format!("{table_path}/{INDEX_DIR}/{}", 
entry.index_file.file_name);
                 let file_name = entry.index_file.file_name.clone();
                 let query_text = search.query_text.clone();
+                let local_filter = plan.local_filter;
                 let row_range_start = global_meta.row_range_start;
                 let row_range_end = global_meta.row_range_end;
                 let limit = search_limit_with_deleted_rows(
@@ -221,31 +243,24 @@ async fn evaluate_full_text_search(
                     row_range_end,
                     deleted_row_index.as_ref(),
                 );
-                let input = evaluation.file_io.new_input(&path);
                 async move {
-                    let input = input?;
-                    let reader = TantivyFullTextReader::from_input_file(&input)
-                        .await
-                        .map_err(|e| crate::Error::UnexpectedError {
-                            message: format!(
-                                "Failed to open Tantivy full-text index '{}': 
{}",
-                                file_name, e
-                            ),
-                            source: None,
-                        })?;
-                    let result = reader.search(&query_text, limit)?;
-                    Ok::<_, crate::Error>(result.offset(row_range_start))
+                    let input = evaluation.file_io.new_input(&path)?;
+                    let reader = Box::new(input.reader().await?);
+                    let result =
+                        search_full_text_index(reader, file_name, query_text, 
limit, local_filter)
+                            .await?;
+                    Ok::<_, 
crate::Error>(search_result_from_core(result)?.offset(row_range_start))
                 }
             })
-            .collect();
-
-        let results = futures::future::try_join_all(futures).await?;
+            .buffered(FULL_TEXT_INDEX_SEARCH_CONCURRENCY)
+            .try_collect::<Vec<_>>()
+            .await?;
         for r in &results {
             merged = merged.or(r);
         }
     }
 
-    if search_mode != GlobalIndexSearchMode::Fast {
+    if search_mode != GlobalIndexSearchMode::Fast && has_fulltext_entries {
         let detail_ranges = if search_mode == GlobalIndexSearchMode::Detail {
             let table = evaluation.table.ok_or_else(|| 
crate::Error::DataInvalid {
                 message: "Full-text raw search in detail mode requires table 
context".to_string(),
@@ -262,14 +277,17 @@ async fn evaluate_full_text_search(
             search_mode,
             evaluation.next_row_id,
             &detail_ranges,
-            is_tantivy_fulltext_index_file,
+            is_full_text_index_file,
         );
         if !raw_ranges.is_empty() {
             let table = evaluation.table.ok_or_else(|| 
crate::Error::DataInvalid {
                 message: "Full-text raw search requires table 
context".to_string(),
                 source: None,
             })?;
-            let raw_result = read_raw_full_text_search(table, search, 
&raw_ranges).await?;
+            let raw_result =
+                read_raw_full_text_search(table, search, &raw_ranges, 
evaluation.table_options)
+                    .await?;
+            merged = merged.without_row_ranges(&raw_ranges)?;
             merged = merged.or(&raw_result);
         }
     }
@@ -279,8 +297,88 @@ async fn evaluate_full_text_search(
         .top_k(search.limit))
 }
 
-fn is_tantivy_fulltext_index_file(index_file: &IndexFileMeta) -> bool {
-    index_file.index_type == TANTIVY_FULLTEXT_INDEX_TYPE
+struct FullTextIndexSearchPlan {
+    entry: IndexManifestEntry,
+    local_filter: Option<RoaringTreemap>,
+}
+
+fn plan_full_text_index_searches(
+    entries: Vec<&IndexManifestEntry>,
+    include_row_ids: Option<&RoaringTreemap>,
+) -> crate::Result<Vec<FullTextIndexSearchPlan>> {
+    let mut ranges = Vec::with_capacity(entries.len());
+    for (index, entry) in entries.iter().enumerate() {
+        let meta = entry.index_file.global_index_meta.as_ref().ok_or_else(|| {
+            crate::Error::DataInvalid {
+                message: format!(
+                    "Full-text index '{}' is missing global index metadata",
+                    entry.index_file.file_name
+                ),
+                source: None,
+            }
+        })?;
+        if meta.row_range_start < 0 || meta.row_range_end < 
meta.row_range_start {
+            return Err(crate::Error::DataInvalid {
+                message: format!(
+                    "Invalid full-text row range [{}, {}] for '{}'",
+                    meta.row_range_start, meta.row_range_end, 
entry.index_file.file_name
+                ),
+                source: None,
+            });
+        }
+        ranges.push((
+            meta.row_range_start as u64,
+            meta.row_range_end as u64,
+            index,
+        ));
+    }
+    ranges.sort_unstable_by_key(|(start, _, _)| *start);
+
+    let Some(include_row_ids) = include_row_ids else {
+        return Ok(entries
+            .into_iter()
+            .map(|entry| FullTextIndexSearchPlan {
+                entry: entry.clone(),
+                local_filter: None,
+            })
+            .collect());
+    };
+
+    let mut local_filters = (0..entries.len())
+        .map(|_| RoaringTreemap::new())
+        .collect::<Vec<_>>();
+    let mut active = Vec::<usize>::new();
+    let mut next_range = 0usize;
+    for row_id in include_row_ids.iter() {
+        while next_range < ranges.len() && ranges[next_range].0 <= row_id {
+            active.push(next_range);
+            next_range += 1;
+        }
+        active.retain(|range_index| ranges[*range_index].1 >= row_id);
+        for range_index in &active {
+            let (start, _, original_index) = ranges[*range_index];
+            local_filters[original_index].insert(row_id - start);
+        }
+        if next_range == ranges.len() && active.is_empty() {
+            break;
+        }
+    }
+
+    let mut plans = Vec::with_capacity(entries.len());
+    for (entry, local_filter) in entries.into_iter().zip(local_filters) {
+        if local_filter.is_empty() {
+            continue;
+        }
+        plans.push(FullTextIndexSearchPlan {
+            entry: entry.clone(),
+            local_filter: Some(local_filter),
+        });
+    }
+    Ok(plans)
+}
+
+fn is_full_text_index_file(index_file: &IndexFileMeta) -> bool {
+    index_file.index_type == FULL_TEXT_INDEX_TYPE
 }
 
 async fn detail_data_ranges_for_table(table: &Table) -> 
crate::Result<Vec<RowRange>> {
@@ -305,15 +403,22 @@ async fn read_raw_full_text_search(
     table: &Table,
     search: &FullTextSearch,
     raw_ranges: &[RowRange],
+    table_options: &HashMap<String, String>,
 ) -> crate::Result<SearchResult> {
     if raw_ranges.is_empty() {
         return Ok(SearchResult::empty());
     }
+    let raw_ranges = merge_row_ranges(raw_ranges.to_vec());
+    let row_range_start = raw_ranges.first().map(RowRange::from).unwrap_or(0);
+    let row_range_end = raw_ranges
+        .last()
+        .map(RowRange::to)
+        .unwrap_or(row_range_start);
 
     let mut read_builder = table.new_read_builder();
     read_builder
         .with_projection(&[search.field_name.as_str(), ROW_ID_FIELD_NAME])?
-        .with_row_ranges(raw_ranges.to_vec());
+        .with_row_ranges(raw_ranges);
     let plan = read_builder.new_scan().plan().await?;
     if plan.splits().is_empty() {
         return Ok(SearchResult::empty());
@@ -321,31 +426,85 @@ async fn read_raw_full_text_search(
     let read = read_builder.new_read()?;
     let mut stream = read.to_arrow(plan.splits())?;
 
-    let mut writer = TantivyFullTextWriter::new()?;
+    let config = raw_full_text_config(table_options)?;
+    let mut writer = tokio::task::spawn_blocking(move || {
+        FullTextIndexWriter::new(config).map_err(full_text_error)
+    })
+    .await
+    .map_err(raw_index_task_error)??;
+    let mut document_count = 0usize;
     while let Some(batch) = stream.try_next().await? {
-        add_raw_full_text_batch(&batch, search, &mut writer)?;
+        let documents = raw_full_text_batch_documents(&batch, search, 
row_range_start)?;
+        document_count += documents.len();
+        writer = tokio::task::spawn_blocking(move || {
+            for (row_id, text) in documents {
+                writer.add_document(row_id, text).map_err(full_text_error)?;
+            }
+            Ok::<_, crate::Error>(writer)
+        })
+        .await
+        .map_err(raw_index_task_error)??;
     }
 
-    let memory_io = FileIOBuilder::new("memory").build()?;
-    let output = memory_io.new_output(&format!("/raw-fulltext-{}.archive", 
Uuid::new_v4()))?;
-    if !writer.finish(&output).await? {
+    if document_count == 0 {
         return Ok(SearchResult::empty());
     }
-    let input = output.to_input_file();
-    let reader = TantivyFullTextReader::from_input_file(&input)
-        .await
-        .map_err(|e| crate::Error::UnexpectedError {
-            message: format!("Failed to open raw Tantivy full-text index: 
{e}"),
+    let temporary_index = tokio::task::spawn_blocking(move || {
+        let mut temporary_index =
+            tempfile::NamedTempFile::new().map_err(|error| 
crate::Error::UnexpectedError {
+                message: format!("Failed to create temporary full-text index: 
{error}"),
+                source: None,
+            })?;
+        writer
+            .write(&mut PosWriter::new(temporary_index.as_file_mut()))
+            .map_err(full_text_error)?;
+        temporary_index.as_file_mut().sync_all().map_err(|error| {
+            crate::Error::UnexpectedError {
+                message: format!("Failed to flush temporary full-text index: 
{error}"),
+                source: None,
+            }
+        })?;
+        Ok::<_, crate::Error>(temporary_index)
+    })
+    .await
+    .map_err(raw_index_task_error)??;
+    let index_file = temporary_index
+        .reopen()
+        .map_err(|error| crate::Error::UnexpectedError {
+            message: format!("Failed to reopen temporary full-text index: 
{error}"),
             source: None,
         })?;
-    reader.search(&search.query_text, search.limit)
+    let local_filter = match search.include_row_ids.as_ref() {
+        Some(include_row_ids) => Some(local_filter(
+            include_row_ids,
+            row_range_start,
+            row_range_end,
+        )?),
+        None => None,
+    };
+    let result = search_full_text_file(
+        index_file,
+        "raw-full-text.index".to_string(),
+        search.query_text.clone(),
+        search.limit,
+        local_filter,
+    )
+    .await?;
+    search_result_from_core(result).map(|result| 
result.offset(row_range_start))
+}
+
+fn raw_index_task_error(error: tokio::task::JoinError) -> crate::Error {
+    crate::Error::UnexpectedError {
+        message: format!("Full-text raw index task failed: {error}"),
+        source: None,
+    }
 }
 
-fn add_raw_full_text_batch(
+fn raw_full_text_batch_documents(
     batch: &RecordBatch,
     search: &FullTextSearch,
-    writer: &mut TantivyFullTextWriter,
-) -> crate::Result<()> {
+    row_range_start: i64,
+) -> crate::Result<Vec<(i64, String)>> {
     let text_index =
         batch
             .schema()
@@ -376,17 +535,33 @@ fn add_raw_full_text_batch(
     let column = batch.column(text_index);
 
     if let Some(strings) = column.as_any().downcast_ref::<StringArray>() {
+        let mut documents = Vec::with_capacity(batch.num_rows());
         for row in 0..batch.num_rows() {
-            add_raw_full_text_row(row_ids, row, get_string_value(strings, 
row), writer)?;
+            if let Some(document) = raw_full_text_document(
+                row_ids,
+                row,
+                get_string_value(strings, row),
+                row_range_start,
+            )? {
+                documents.push(document);
+            }
         }
-        return Ok(());
+        return Ok(documents);
     }
 
     if let Some(strings) = column.as_any().downcast_ref::<LargeStringArray>() {
+        let mut documents = Vec::with_capacity(batch.num_rows());
         for row in 0..batch.num_rows() {
-            add_raw_full_text_row(row_ids, row, 
get_large_string_value(strings, row), writer)?;
+            if let Some(document) = raw_full_text_document(
+                row_ids,
+                row,
+                get_large_string_value(strings, row),
+                row_range_start,
+            )? {
+                documents.push(document);
+            }
         }
-        return Ok(());
+        return Ok(documents);
     }
 
     Err(crate::Error::DataInvalid {
@@ -411,41 +586,153 @@ fn get_large_string_value(strings: &LargeStringArray, 
row: usize) -> Option<&str
     }
 }
 
-fn add_raw_full_text_row(
+fn raw_full_text_document(
     row_ids: &Int64Array,
     row: usize,
     text: Option<&str>,
-    writer: &mut TantivyFullTextWriter,
-) -> crate::Result<()> {
+    row_range_start: i64,
+) -> crate::Result<Option<(i64, String)>> {
     if row_ids.is_null(row) {
         return Err(crate::Error::DataInvalid {
             message: "Full-text raw search found null _ROW_ID".to_string(),
             source: None,
         });
     }
-    let row_id = u64::try_from(row_ids.value(row)).map_err(|_| 
crate::Error::DataInvalid {
-        message: format!(
-            "Negative _ROW_ID {} cannot be used for global index search",
-            row_ids.value(row)
-        ),
+    let row_id = row_ids.value(row);
+    if row_id < 0 {
+        return Err(crate::Error::DataInvalid {
+            message: format!("Negative _ROW_ID {row_id} cannot be used for 
global index search"),
+            source: None,
+        });
+    }
+    match text {
+        Some(text) => {
+            let local_row_id =
+                row_id
+                    .checked_sub(row_range_start)
+                    .ok_or_else(|| crate::Error::DataInvalid {
+                        message: format!(
+                        "Raw full-text row id {row_id} is before row range 
start {row_range_start}"
+                    ),
+                        source: None,
+                    })?;
+            Ok(Some((local_row_id, text.to_string())))
+        }
+        None => Ok(None),
+    }
+}
+
+fn raw_full_text_config(
+    table_options: &HashMap<String, String>,
+) -> crate::Result<FullTextIndexConfig> {
+    let native_options: HashMap<String, String> = table_options
+        .iter()
+        .filter_map(|(key, value)| {
+            key.strip_prefix("full-text.")
+                .map(|native_key| (native_key.to_string(), value.clone()))
+        })
+        .collect();
+    FullTextIndexConfig::from_options(&native_options).map_err(full_text_error)
+}
+
+fn local_filter(
+    global: &RoaringTreemap,
+    row_range_start: i64,
+    row_range_end: i64,
+) -> crate::Result<RoaringTreemap> {
+    if row_range_start < 0 || row_range_end < row_range_start {
+        return Err(crate::Error::DataInvalid {
+            message: format!("Invalid full-text row range [{row_range_start}, 
{row_range_end}]"),
+            source: None,
+        });
+    }
+
+    let start = u64::try_from(row_range_start).map_err(|_| 
crate::Error::DataInvalid {
+        message: format!("Invalid negative row range start {row_range_start}"),
+        source: None,
+    })?;
+    let end = u64::try_from(row_range_end).map_err(|_| 
crate::Error::DataInvalid {
+        message: format!("Invalid negative row range end {row_range_end}"),
         source: None,
     })?;
-    writer.add_document(row_id, text)
+    let mut local = RoaringTreemap::new();
+    for row_id in global
+        .iter()
+        .filter(|row_id| *row_id >= start && *row_id <= end)
+    {
+        local.insert(row_id - start);
+    }
+    Ok(local)
+}
+
+fn normalize_query_text(query_text: &str, _field_name: &str) -> 
crate::Result<String> {
+    if is_full_text_query_dsl(query_text) {
+        return Ok(query_text.to_string());
+    }
+    serde_json::to_string(&json!({
+        "match": {
+            "query": query_text,
+        }
+    }))
+    .map_err(|e| crate::Error::ConfigInvalid {
+        message: format!("Failed to build full-text query JSON: {e}"),
+    })
+}
+
+fn is_full_text_query_dsl(query_text: &str) -> bool {
+    let Ok(serde_json::Value::Object(object)) =
+        serde_json::from_str::<serde_json::Value>(query_text)
+    else {
+        return false;
+    };
+    object.keys().any(|key| {
+        matches!(
+            key.as_str(),
+            "match" | "multi_match" | "match_phrase" | "phrase" | "boolean" | 
"boost"
+        )
+    })
+}
+
+fn search_result_from_core(
+    result: paimon_ftindex_core::FullTextSearchResult,
+) -> crate::Result<SearchResult> {
+    let mut row_ids = Vec::with_capacity(result.row_ids.len());
+    for row_id in result.row_ids {
+        row_ids.push(
+            u64::try_from(row_id).map_err(|_| crate::Error::DataInvalid {
+                message: format!("Full-text index returned negative row id 
{row_id}"),
+                source: None,
+            })?,
+        );
+    }
+    Ok(SearchResult::new(row_ids, result.scores))
+}
+
+fn full_text_error(error: impl std::fmt::Display) -> crate::Error {
+    crate::Error::UnexpectedError {
+        message: format!("Full-text index error: {error}"),
+        source: None,
+    }
 }
 
 #[cfg(test)]
 mod tests {
     use super::*;
     use crate::catalog::Identifier;
-    use crate::spec::{DataType, IntType, Schema, TableSchema, VarCharType};
+    use crate::io::FileIOBuilder;
+    use crate::spec::{DataType, GlobalIndexMeta, IntType, Schema, TableSchema, 
VarCharType};
     use crate::table::table_write::TableWrite;
     use crate::table::TableCommit;
     use arrow_array::StringArray;
     use arrow_schema::{DataType as ArrowDataType, Field as ArrowField, Schema 
as ArrowSchema};
+    use bytes::Bytes;
+    use paimon_ftindex_core::io::SliceReader;
+    use paimon_ftindex_core::FullTextIndexReader;
+    use roaring::RoaringTreemap;
     use std::sync::Arc;
 
     #[tokio::test]
-    async fn test_evaluate_full_mode_without_fulltext_entries_uses_raw_path() {
+    async fn test_evaluate_full_mode_without_fulltext_entries_returns_empty() {
         let file_io = FileIOBuilder::new("memory").build().unwrap();
         let fields = vec![DataField::new(
             1,
@@ -455,7 +742,7 @@ mod tests {
         let search = FullTextSearch::new("hello".to_string(), 10, 
"body".to_string()).unwrap();
         let options = HashMap::from([("global-index.search-mode".to_string(), 
"full".to_string())]);
 
-        let err = evaluate_full_text_search(
+        let result = evaluate_full_text_search(
             FullTextSearchEvaluation {
                 table: None,
                 file_io: &file_io,
@@ -468,16 +755,12 @@ mod tests {
             &search,
         )
         .await
-        .unwrap_err();
-        assert!(
-            err.to_string()
-                .contains("Full-text raw search requires table context"),
-            "unexpected error: {err}"
-        );
+        .unwrap();
+        assert!(result.is_empty());
     }
 
     #[tokio::test]
-    async fn test_execute_full_mode_without_index_manifest_searches_raw_rows() 
{
+    async fn test_execute_full_mode_without_index_manifest_returns_empty() {
         let file_io = FileIOBuilder::new("memory").build().unwrap();
         let table_path = "memory:/full_text_raw_no_manifest";
         setup_dirs(&file_io, table_path).await;
@@ -485,7 +768,13 @@ mod tests {
 
         let mut table_write = TableWrite::new(&table, 
"test-user".to_string()).unwrap();
         table_write
-            .write_arrow_batch(&text_batch(vec!["hello world", "goodbye"]))
+            .write_arrow_batch(&text_batch(vec![
+                "alphaft lakehouse",
+                "betafft gammaft",
+                "alphaft indexed",
+                "gammaft fallback",
+                "alphaft queried",
+            ]))
             .await
             .unwrap();
         let messages = table_write.prepare_commit().await.unwrap();
@@ -497,11 +786,372 @@ mod tests {
         let mut builder = table.new_full_text_search_builder();
         builder
             .with_text_column("body")
-            .with_query_text("hello")
+            .with_query_text("alphaft")
             .with_limit(10);
         let row_ranges = builder.execute().await.unwrap();
 
-        assert_eq!(row_ranges, vec![RowRange::new(0, 0)]);
+        assert!(row_ranges.is_empty());
+    }
+
+    #[tokio::test]
+    async fn test_raw_full_text_search_applies_include_row_ids() {
+        let file_io = FileIOBuilder::new("memory").build().unwrap();
+        let table_path = "memory:/full_text_raw_with_filter";
+        setup_dirs(&file_io, table_path).await;
+        let table = full_text_raw_table(&file_io, table_path);
+
+        let mut table_write = TableWrite::new(&table, 
"test-user".to_string()).unwrap();
+        table_write
+            .write_arrow_batch(&text_batch(vec!["hello world", "hello 
filtered"]))
+            .await
+            .unwrap();
+        let messages = table_write.prepare_commit().await.unwrap();
+        TableCommit::new(table.clone(), "test-user".to_string())
+            .commit(messages)
+            .await
+            .unwrap();
+
+        let mut filter = RoaringTreemap::new();
+        filter.insert(1);
+
+        let search = FullTextSearch::new(
+            r#"{"match":{"column":"text","query":"hello"}}"#.to_string(),
+            10,
+            "body".to_string(),
+        )
+        .unwrap()
+        .with_include_row_ids(filter);
+        let row_ranges = read_raw_full_text_search(
+            &table,
+            &search,
+            &[RowRange::new(0, 1)],
+            table.schema().options(),
+        )
+        .await
+        .unwrap()
+        .to_row_ranges();
+
+        assert_eq!(row_ranges, vec![RowRange::new(1, 1)]);
+    }
+
+    #[tokio::test]
+    async fn test_evaluate_reads_new_full_text_index_file() {
+        let file_io = FileIOBuilder::new("memory").build().unwrap();
+        let table_path = "memory:/full_text_indexed";
+        setup_dirs(&file_io, table_path).await;
+        file_io
+            .mkdirs(&format!("{table_path}/index/"))
+            .await
+            .unwrap();
+
+        let mut writer =
+            
FullTextIndexWriter::new(FullTextIndexConfig::new().with_text_fields(["body"]))
+                .unwrap();
+        writer
+            .add_document_fields(0, [("body", "hello world")])
+            .unwrap();
+        writer
+            .add_document_fields(1, [("body", "goodbye")])
+            .unwrap();
+        let mut index_bytes = Vec::new();
+        writer.write(&mut PosWriter::new(&mut index_bytes)).unwrap();
+        file_io
+            .new_output(&format!("{table_path}/index/ft-0"))
+            .unwrap()
+            .write(Bytes::from(index_bytes.clone()))
+            .await
+            .unwrap();
+
+        let fields = vec![DataField::new(
+            1,
+            "body".to_string(),
+            DataType::VarChar(VarCharType::string_type()),
+        )];
+        let search = FullTextSearch::new(
+            normalize_query_text("hello", "body").unwrap(),
+            10,
+            "body".to_string(),
+        )
+        .unwrap();
+        let entry = IndexManifestEntry {
+            kind: FileKind::Add,
+            partition: Vec::new(),
+            bucket: 0,
+            index_file: IndexFileMeta {
+                index_type: FULL_TEXT_INDEX_TYPE.to_string(),
+                file_name: "ft-0".to_string(),
+                file_size: i64::try_from(index_bytes.len()).unwrap(),
+                row_count: 2,
+                deletion_vectors_ranges: None,
+                global_index_meta: Some(GlobalIndexMeta {
+                    row_range_start: 100,
+                    row_range_end: 101,
+                    index_field_id: 1,
+                    extra_field_ids: None,
+                    index_meta: None,
+                    source_meta: None,
+                }),
+            },
+            version: 1,
+        };
+        let result = evaluate_full_text_search(
+            FullTextSearchEvaluation {
+                table: None,
+                file_io: &file_io,
+                table_path,
+                table_options: &HashMap::new(),
+                schema_fields: &fields,
+                next_row_id: Some(102),
+            },
+            &[entry],
+            &search,
+        )
+        .await
+        .unwrap();
+
+        assert_eq!(result.row_ids, vec![100]);
+    }
+
+    #[tokio::test]
+    async fn test_indexed_full_text_search_offsets_include_row_ids() {
+        let file_io = FileIOBuilder::new("memory").build().unwrap();
+        let table_path = "memory:/full_text_indexed_filter";
+        setup_dirs(&file_io, table_path).await;
+        file_io
+            .mkdirs(&format!("{table_path}/index/"))
+            .await
+            .unwrap();
+
+        let mut writer =
+            
FullTextIndexWriter::new(FullTextIndexConfig::new().with_text_fields(["body"]))
+                .unwrap();
+        writer
+            .add_document_fields(0, [("body", "hello world")])
+            .unwrap();
+        writer
+            .add_document_fields(1, [("body", "hello world")])
+            .unwrap();
+        let mut index_bytes = Vec::new();
+        writer.write(&mut PosWriter::new(&mut index_bytes)).unwrap();
+        file_io
+            .new_output(&format!("{table_path}/index/ft-filter"))
+            .unwrap()
+            .write(Bytes::from(index_bytes.clone()))
+            .await
+            .unwrap();
+
+        let fields = vec![DataField::new(
+            1,
+            "body".to_string(),
+            DataType::VarChar(VarCharType::string_type()),
+        )];
+        let mut filter = RoaringTreemap::new();
+        filter.insert(101);
+        let search = FullTextSearch::new(
+            normalize_query_text("hello", "body").unwrap(),
+            10,
+            "body".to_string(),
+        )
+        .unwrap()
+        .with_include_row_ids(filter);
+        let entry = IndexManifestEntry {
+            kind: FileKind::Add,
+            partition: Vec::new(),
+            bucket: 0,
+            index_file: IndexFileMeta {
+                index_type: FULL_TEXT_INDEX_TYPE.to_string(),
+                file_name: "ft-filter".to_string(),
+                file_size: i64::try_from(index_bytes.len()).unwrap(),
+                row_count: 2,
+                deletion_vectors_ranges: None,
+                global_index_meta: Some(GlobalIndexMeta {
+                    row_range_start: 100,
+                    row_range_end: 101,
+                    index_field_id: 1,
+                    extra_field_ids: None,
+                    index_meta: None,
+                    source_meta: None,
+                }),
+            },
+            version: 1,
+        };
+        let missing_empty_filter_entry = 
full_text_index_entry("missing-filtered-shard", 200, 201);
+
+        let result = evaluate_full_text_search(
+            FullTextSearchEvaluation {
+                table: None,
+                file_io: &file_io,
+                table_path,
+                table_options: &HashMap::new(),
+                schema_fields: &fields,
+                next_row_id: Some(102),
+            },
+            &[entry, missing_empty_filter_entry],
+            &search,
+        )
+        .await
+        .unwrap();
+
+        assert_eq!(result.row_ids, vec![101]);
+    }
+
+    #[test]
+    fn test_normalize_query_text_only_passes_query_dsl_objects() {
+        let normalized = normalize_query_text("123", "body").unwrap();
+        assert_eq!(
+            serde_json::from_str::<serde_json::Value>(&normalized).unwrap(),
+            serde_json::json!({"match": {"query": "123"}})
+        );
+
+        let normalized = normalize_query_text("\"hello\"", "body").unwrap();
+        assert_eq!(
+            serde_json::from_str::<serde_json::Value>(&normalized).unwrap(),
+            serde_json::json!({"match": {"query": "\"hello\""}})
+        );
+
+        let dsl = r#"{"match":{"query":"hello"}}"#;
+        assert_eq!(normalize_query_text(dsl, "body").unwrap(), dsl);
+    }
+
+    #[test]
+    fn test_raw_full_text_config_preserves_native_text_fields() {
+        let options = HashMap::from([(
+            "full-text.text-fields".to_string(),
+            "native_text,secondary".to_string(),
+        )]);
+
+        let config = raw_full_text_config(&options).unwrap();
+
+        assert_eq!(config.text_fields, vec!["native_text", "secondary"]);
+        assert_eq!(config.default_text_field(), "native_text");
+    }
+
+    #[test]
+    fn test_raw_batches_keep_one_bm25_corpus_and_configured_field() {
+        let options = HashMap::from([(
+            "full-text.text-field".to_string(),
+            "native_text".to_string(),
+        )]);
+        let config = raw_full_text_config(&options).unwrap();
+        let search = FullTextSearch::new(
+            r#"{"match":{"column":"native_text","query":"rare 
common"}}"#.to_string(),
+            10,
+            "body".to_string(),
+        )
+        .unwrap();
+        let batches = [
+            raw_text_batch(&[10, 11], &["rare common", "common common common 
common"]),
+            raw_text_batch(&[20, 21], &["rare", "unrelated"]),
+        ];
+
+        let mut batched_writer = 
FullTextIndexWriter::new(config.clone()).unwrap();
+        let mut all_documents = Vec::new();
+        for batch in &batches {
+            let documents = raw_full_text_batch_documents(batch, &search, 
10).unwrap();
+            for (row_id, text) in &documents {
+                batched_writer.add_document(*row_id, text).unwrap();
+            }
+            all_documents.extend(documents);
+        }
+        let mut reference_writer = FullTextIndexWriter::new(config).unwrap();
+        for (row_id, text) in all_documents {
+            reference_writer.add_document(row_id, text).unwrap();
+        }
+
+        let query = search.query_text.as_str();
+        let batched_result = write_and_search_test_index(&mut batched_writer, 
query);
+        let reference_result = write_and_search_test_index(&mut 
reference_writer, query);
+
+        assert!(!batched_result.row_ids.is_empty());
+        assert_eq!(batched_result.row_ids, reference_result.row_ids);
+        assert_eq!(batched_result.scores, reference_result.scores);
+    }
+
+    fn raw_text_batch(row_ids: &[i64], texts: &[&str]) -> RecordBatch {
+        let schema = Arc::new(ArrowSchema::new(vec![
+            ArrowField::new("body", ArrowDataType::Utf8, true),
+            ArrowField::new(ROW_ID_FIELD_NAME, ArrowDataType::Int64, false),
+        ]));
+        RecordBatch::try_new(
+            schema,
+            vec![
+                Arc::new(StringArray::from(texts.to_vec())),
+                Arc::new(Int64Array::from(row_ids.to_vec())),
+            ],
+        )
+        .unwrap()
+    }
+
+    fn write_and_search_test_index(
+        writer: &mut FullTextIndexWriter,
+        query: &str,
+    ) -> paimon_ftindex_core::FullTextSearchResult {
+        let mut bytes = Vec::new();
+        writer.write(&mut PosWriter::new(&mut bytes)).unwrap();
+        FullTextIndexReader::open(SliceReader::new(bytes))
+            .unwrap()
+            .search(query, 10)
+            .unwrap()
+    }
+
+    #[test]
+    fn 
test_plan_full_text_index_searches_partitions_once_and_preserves_order() {
+        let entries = [
+            full_text_index_entry("first", 100, 109),
+            full_text_index_entry("second", 0, 9),
+            full_text_index_entry("overlap", 104, 106),
+            full_text_index_entry("empty", 200, 209),
+        ];
+        let mut include_row_ids = RoaringTreemap::new();
+        include_row_ids.insert(1);
+        include_row_ids.insert(105);
+        include_row_ids.insert(999);
+
+        let plans = plan_full_text_index_searches(entries.iter().collect(), 
Some(&include_row_ids))
+            .unwrap();
+
+        assert_eq!(
+            plans
+                .iter()
+                .map(|plan| plan.entry.index_file.file_name.as_str())
+                .collect::<Vec<_>>(),
+            vec!["first", "second", "overlap"]
+        );
+        let decoded = plans
+            .iter()
+            .map(|plan| {
+                plan.local_filter
+                    .as_ref()
+                    .unwrap()
+                    .iter()
+                    .collect::<Vec<_>>()
+            })
+            .collect::<Vec<_>>();
+        assert_eq!(decoded, vec![vec![5], vec![1], vec![1]]);
+    }
+
+    fn full_text_index_entry(name: &str, start: i64, end: i64) -> 
IndexManifestEntry {
+        IndexManifestEntry {
+            kind: FileKind::Add,
+            partition: Vec::new(),
+            bucket: 0,
+            index_file: IndexFileMeta {
+                index_type: FULL_TEXT_INDEX_TYPE.to_string(),
+                file_name: name.to_string(),
+                file_size: 0,
+                row_count: i32::try_from(end - start + 1).unwrap(),
+                deletion_vectors_ranges: None,
+                global_index_meta: Some(GlobalIndexMeta {
+                    row_range_start: start,
+                    row_range_end: end,
+                    index_field_id: 1,
+                    extra_field_ids: None,
+                    index_meta: None,
+                    source_meta: None,
+                }),
+            },
+            version: 1,
+        }
     }
 
     async fn setup_dirs(file_io: &FileIO, table_path: &str) {
diff --git a/crates/paimon/src/table/mod.rs b/crates/paimon/src/table/mod.rs
index 5c34918d..b7d48a63 100644
--- a/crates/paimon/src/table/mod.rs
+++ b/crates/paimon/src/table/mod.rs
@@ -43,6 +43,8 @@ mod format_table_read;
 mod format_table_scan;
 mod format_write_builder;
 #[cfg(feature = "fulltext")]
+mod full_text_index_adapter;
+#[cfg(feature = "fulltext")]
 mod full_text_search_builder;
 pub(crate) mod global_index_build_common;
 mod global_index_drop_builder;
diff --git a/crates/paimon/src/tantivy/directory.rs 
b/crates/paimon/src/tantivy/directory.rs
deleted file mode 100644
index 22440a60..00000000
--- a/crates/paimon/src/tantivy/directory.rs
+++ /dev/null
@@ -1,399 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied.  See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-//! Archive-backed Tantivy Directory implementation.
-//!
-//! Reads a Tantivy index packed into a single archive file. The archive format
-//! (Big-Endian, compatible with Java Paimon):
-//!
-//! ```text
-//! [fileCount: 4 bytes BE]
-//! for each file:
-//!   [nameLen: 4 bytes BE]
-//!   [name: nameLen bytes UTF-8]
-//!   [dataLen: 8 bytes BE]
-//!   [data: dataLen bytes]
-//! ```
-//!
-//! Reference: 
`org.apache.paimon.tantivy.index.TantivyFullTextGlobalIndexWriter.packIndex()`
-//! Reference: 
`org.apache.paimon.tantivy.index.TantivyFullTextGlobalIndexReader.parseArchiveHeader()`
-
-use bytes::Bytes;
-use std::collections::HashMap;
-use std::fmt;
-use std::io;
-use std::ops::Range;
-use std::path::{Path, PathBuf};
-use std::sync::{Arc, Mutex};
-use tantivy::directory::error::{DeleteError, LockError, OpenReadError, 
OpenWriteError};
-use tantivy::directory::Directory;
-use tantivy::directory::{
-    AntiCallToken, DirectoryLock, FileHandle, Lock, OwnedBytes, 
TerminatingWrite, WatchCallback,
-    WatchHandle, WritePtr,
-};
-use tantivy::HasLen;
-
-use crate::io::FileRead;
-
-/// Metadata for a single file within the archive.
-#[derive(Clone, Debug)]
-struct FileMeta {
-    /// Absolute byte offset of the file data within the archive.
-    offset: u64,
-    length: usize,
-}
-
-/// A read-only Tantivy `Directory` backed by an archive file.
-///
-/// Only the archive header (file names, offsets, lengths) is parsed eagerly.
-/// Actual file data is read on demand via `FileRead`.
-#[derive(Clone)]
-pub struct ArchiveDirectory {
-    files: Arc<HashMap<PathBuf, FileMeta>>,
-    reader: Arc<dyn FileRead>,
-    /// In-memory storage for atomic_write (used by Tantivy for meta.json).
-    atomic_data: Arc<Mutex<HashMap<PathBuf, Vec<u8>>>>,
-}
-
-impl ArchiveDirectory {
-    /// Create an `ArchiveDirectory` from an async `FileRead`.
-    ///
-    /// Only the archive header (file names, offsets, lengths) is read eagerly.
-    /// Actual file data is read on demand when Tantivy requests it.
-    pub async fn from_reader(reader: impl FileRead, file_size: u64) -> 
io::Result<Self> {
-        let reader: Arc<dyn FileRead> = Arc::new(reader);
-
-        if file_size < 4 {
-            return Err(io::Error::new(
-                io::ErrorKind::InvalidData,
-                "Archive too small to contain file count",
-            ));
-        }
-
-        // Read file count (4 bytes).
-        let buf = reader
-            .read(0..4)
-            .await
-            .map_err(|e| io::Error::other(e.to_string()))?;
-        let file_count = i32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
-        if file_count < 0 {
-            return Err(io::Error::new(
-                io::ErrorKind::InvalidData,
-                format!("Negative file count in archive: {file_count}"),
-            ));
-        }
-        let file_count = file_count as usize;
-
-        let mut pos: u64 = 4;
-        let mut files = HashMap::with_capacity(file_count);
-
-        for _ in 0..file_count {
-            // Read name_len (4 bytes).
-            let buf = reader
-                .read(pos..pos + 4)
-                .await
-                .map_err(|e| io::Error::other(e.to_string()))?;
-            let name_len = i32::from_be_bytes([buf[0], buf[1], buf[2], 
buf[3]]);
-            if name_len < 0 {
-                return Err(io::Error::new(
-                    io::ErrorKind::InvalidData,
-                    format!("Negative name length in archive: {name_len}"),
-                ));
-            }
-            let name_len = name_len as u64;
-            pos += 4;
-
-            // Read name + data_len together in a single IO call.
-            let meta_buf = reader
-                .read(pos..pos + name_len + 8)
-                .await
-                .map_err(|e| io::Error::other(e.to_string()))?;
-
-            let name = String::from_utf8(meta_buf[..name_len as 
usize].to_vec()).map_err(|e| {
-                io::Error::new(
-                    io::ErrorKind::InvalidData,
-                    format!("Invalid UTF-8 in file name: {}", e),
-                )
-            })?;
-
-            let dl = name_len as usize;
-            let data_len = i64::from_be_bytes([
-                meta_buf[dl],
-                meta_buf[dl + 1],
-                meta_buf[dl + 2],
-                meta_buf[dl + 3],
-                meta_buf[dl + 4],
-                meta_buf[dl + 5],
-                meta_buf[dl + 6],
-                meta_buf[dl + 7],
-            ]);
-            if data_len < 0 {
-                return Err(io::Error::new(
-                    io::ErrorKind::InvalidData,
-                    format!("Negative data length in archive: {data_len}"),
-                ));
-            }
-            let data_len = data_len as u64;
-            pos += name_len + 8;
-
-            let data_offset = pos;
-            files.insert(
-                PathBuf::from(&name),
-                FileMeta {
-                    offset: data_offset,
-                    length: data_len as usize,
-                },
-            );
-
-            // Skip past file data — do NOT read it.
-            pos += data_len;
-        }
-
-        Ok(Self {
-            files: Arc::new(files),
-            reader,
-            atomic_data: Arc::new(Mutex::new(HashMap::new())),
-        })
-    }
-}
-
-/// Bridge sync Tantivy `FileHandle::read_bytes` to async `FileRead::read`.
-///
-/// Uses `block_in_place` on multi-threaded tokio runtimes (no thread spawn
-/// overhead). Falls back to a scoped thread for current-thread runtimes.
-fn block_on_read(reader: &Arc<dyn FileRead>, range: Range<u64>) -> 
io::Result<Bytes> {
-    let handle = tokio::runtime::Handle::current();
-    let do_read = || {
-        handle
-            .block_on(reader.read(range.clone()))
-            .map_err(|e| io::Error::other(e.to_string()))
-    };
-
-    match handle.runtime_flavor() {
-        tokio::runtime::RuntimeFlavor::MultiThread => 
tokio::task::block_in_place(do_read),
-        _ => {
-            // Current-thread runtime: block_in_place is not available,
-            // fall back to a scoped thread.
-            let reader = Arc::clone(reader);
-            std::thread::scope(|s| {
-                s.spawn(move || {
-                    handle
-                        .block_on(reader.read(range))
-                        .map_err(|e| io::Error::other(e.to_string()))
-                })
-                .join()
-                .map_err(|_| io::Error::other("reader thread panicked"))?
-            })
-        }
-    }
-}
-
-impl fmt::Debug for ArchiveDirectory {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        f.debug_struct("ArchiveDirectory")
-            .field("files", &self.files.keys().collect::<Vec<_>>())
-            .finish()
-    }
-}
-
-/// A `FileHandle` for a single file within the archive.
-#[derive(Clone)]
-struct ArchiveFileHandle {
-    reader: Arc<dyn FileRead>,
-    file_offset: u64,
-    file_length: usize,
-}
-
-impl fmt::Debug for ArchiveFileHandle {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        f.debug_struct("ArchiveFileHandle")
-            .field("offset", &self.file_offset)
-            .field("length", &self.file_length)
-            .finish()
-    }
-}
-
-impl HasLen for ArchiveFileHandle {
-    fn len(&self) -> usize {
-        self.file_length
-    }
-}
-
-impl FileHandle for ArchiveFileHandle {
-    fn read_bytes(&self, range: Range<usize>) -> io::Result<OwnedBytes> {
-        if range.end > self.file_length {
-            return Err(io::Error::new(
-                io::ErrorKind::InvalidInput,
-                format!(
-                    "Read range {:?} exceeds file length {}",
-                    range, self.file_length
-                ),
-            ));
-        }
-
-        let abs_start = self.file_offset + range.start as u64;
-        let abs_end = self.file_offset + range.end as u64;
-        let data = block_on_read(&self.reader, abs_start..abs_end)?;
-        Ok(OwnedBytes::new(data.to_vec()))
-    }
-}
-
-impl Directory for ArchiveDirectory {
-    fn get_file_handle(&self, path: &Path) -> Result<Arc<dyn FileHandle>, 
OpenReadError> {
-        let meta = self
-            .files
-            .get(path)
-            .ok_or_else(|| 
OpenReadError::FileDoesNotExist(path.to_path_buf()))?;
-
-        Ok(Arc::new(ArchiveFileHandle {
-            reader: self.reader.clone(),
-            file_offset: meta.offset,
-            file_length: meta.length,
-        }))
-    }
-
-    fn exists(&self, path: &Path) -> Result<bool, OpenReadError> {
-        Ok(self.files.contains_key(path) || 
self.atomic_data.lock().unwrap().contains_key(path))
-    }
-
-    fn atomic_read(&self, path: &Path) -> Result<Vec<u8>, OpenReadError> {
-        if let Some(data) = self.atomic_data.lock().unwrap().get(path) {
-            return Ok(data.clone());
-        }
-        let meta = self
-            .files
-            .get(path)
-            .ok_or_else(|| 
OpenReadError::FileDoesNotExist(path.to_path_buf()))?;
-
-        let data = block_on_read(&self.reader, meta.offset..meta.offset + 
meta.length as u64)
-            .map_err(|e| OpenReadError::wrap_io_error(e, path.to_path_buf()))?;
-        Ok(data.to_vec())
-    }
-
-    fn atomic_write(&self, path: &Path, data: &[u8]) -> io::Result<()> {
-        self.atomic_data
-            .lock()
-            .unwrap()
-            .insert(path.to_path_buf(), data.to_vec());
-        Ok(())
-    }
-
-    fn delete(&self, _path: &Path) -> Result<(), DeleteError> {
-        Ok(())
-    }
-
-    fn open_write(&self, _path: &Path) -> Result<WritePtr, OpenWriteError> {
-        Ok(io::BufWriter::new(Box::new(
-            VecTerminatingWrite(Vec::new()),
-        )))
-    }
-
-    fn sync_directory(&self) -> io::Result<()> {
-        Ok(())
-    }
-
-    fn acquire_lock(&self, _lock: &Lock) -> Result<DirectoryLock, LockError> {
-        Ok(DirectoryLock::from(Box::new(())))
-    }
-
-    fn watch(&self, _watch_callback: WatchCallback) -> 
tantivy::Result<WatchHandle> {
-        Ok(WatchHandle::empty())
-    }
-}
-
-/// Dummy writer for lock file support.
-struct VecTerminatingWrite(Vec<u8>);
-
-impl io::Write for VecTerminatingWrite {
-    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
-        self.0.extend_from_slice(buf);
-        Ok(buf.len())
-    }
-
-    fn flush(&mut self) -> io::Result<()> {
-        Ok(())
-    }
-}
-
-impl TerminatingWrite for VecTerminatingWrite {
-    fn terminate_ref(&mut self, _token: AntiCallToken) -> io::Result<()> {
-        Ok(())
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::io::FileIOBuilder;
-    use crate::tantivy::writer::TantivyFullTextWriter;
-
-    async fn make_test_dir() -> ArchiveDirectory {
-        let file_io = FileIOBuilder::new("memory").build().unwrap();
-
-        let mut writer = TantivyFullTextWriter::new().unwrap();
-        writer.add_document(0, Some("hello")).unwrap();
-        writer.add_document(1, Some("world")).unwrap();
-        let output = file_io.new_output("/test_archive.bin").unwrap();
-        writer.finish(&output).await.unwrap();
-
-        let input = output.to_input_file();
-        let metadata = input.metadata().await.unwrap();
-        let reader = input.reader().await.unwrap();
-        ArchiveDirectory::from_reader(reader, metadata.size)
-            .await
-            .unwrap()
-    }
-
-    #[tokio::test]
-    async fn test_parse_archive() {
-        let dir = make_test_dir().await;
-        // Tantivy index files should be present.
-        assert!(!dir.files.is_empty());
-    }
-
-    #[tokio::test]
-    async fn test_read_file_from_archive() {
-        let dir = make_test_dir().await;
-        // Find a non-empty file (some tantivy index files can be 0 bytes).
-        let non_empty_path = dir
-            .files
-            .iter()
-            .find(|(_, meta)| meta.length > 0)
-            .map(|(p, _)| p.clone())
-            .expect("archive should contain at least one non-empty file");
-        let handle = dir.get_file_handle(&non_empty_path).unwrap();
-        assert!(handle.len() > 0);
-        let data = handle.read_bytes(0..handle.len()).unwrap();
-        assert_eq!(data.len(), handle.len());
-    }
-
-    #[tokio::test]
-    async fn test_atomic_read_write() {
-        let dir = make_test_dir().await;
-
-        // atomic_write + atomic_read
-        dir.atomic_write(Path::new("meta.json"), b"{}").unwrap();
-        let data = dir.atomic_read(Path::new("meta.json")).unwrap();
-        assert_eq!(&data, b"{}");
-    }
-
-    #[tokio::test]
-    async fn test_file_not_found() {
-        let dir = make_test_dir().await;
-        assert!(dir.get_file_handle(Path::new("missing.txt")).is_err());
-    }
-}
diff --git a/crates/paimon/src/tantivy/mod.rs b/crates/paimon/src/tantivy/mod.rs
deleted file mode 100644
index 09229374..00000000
--- a/crates/paimon/src/tantivy/mod.rs
+++ /dev/null
@@ -1,23 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied.  See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-//! Tantivy-based full-text search for global index.
-
-pub(crate) mod directory;
-pub mod full_text_search;
-pub mod reader;
-pub mod writer;
diff --git a/crates/paimon/src/tantivy/reader.rs 
b/crates/paimon/src/tantivy/reader.rs
deleted file mode 100644
index 17488506..00000000
--- a/crates/paimon/src/tantivy/reader.rs
+++ /dev/null
@@ -1,198 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied.  See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-//! Tantivy full-text index reader.
-//!
-//! Reads a Tantivy index from an archive (packed by `TantivyFullTextWriter`)
-//! and performs full-text search queries.
-//!
-//! Reference: 
`org.apache.paimon.tantivy.index.TantivyFullTextGlobalIndexReader`
-
-use crate::io::{FileRead, InputFile};
-use crate::tantivy::directory::ArchiveDirectory;
-use crate::tantivy::full_text_search::SearchResult;
-use tantivy::collector::TopDocs;
-use tantivy::query::QueryParser;
-use tantivy::{Index, IndexReader, ReloadPolicy};
-
-/// Reader for a Tantivy full-text index stored in archive format.
-pub struct TantivyFullTextReader {
-    reader: IndexReader,
-    index: Index,
-}
-
-impl TantivyFullTextReader {
-    /// Open a reader from an `InputFile` (on-demand reading, no full load).
-    pub async fn from_input_file(input: &InputFile) -> crate::Result<Self> {
-        let metadata = input.metadata().await?;
-        let reader = input.reader().await?;
-        Self::from_reader(reader, metadata.size).await
-    }
-
-    /// Open a reader from an async `FileRead` and file size.
-    pub async fn from_reader(reader: impl FileRead, file_size: u64) -> 
crate::Result<Self> {
-        let directory = ArchiveDirectory::from_reader(reader, file_size)
-            .await
-            .map_err(|e| crate::Error::UnexpectedError {
-                message: format!("Failed to parse Tantivy archive: {}", e),
-                source: None,
-            })?;
-
-        let index = Index::open(directory).map_err(|e| 
crate::Error::UnexpectedError {
-            message: format!("Failed to open Tantivy index from archive: {}", 
e),
-            source: None,
-        })?;
-
-        let reader = index
-            .reader_builder()
-            .reload_policy(ReloadPolicy::Manual)
-            .try_into()
-            .map_err(|e| crate::Error::UnexpectedError {
-                message: format!("Failed to create Tantivy reader: {}", e),
-                source: None,
-            })?;
-
-        Ok(Self { reader, index })
-    }
-
-    /// Search the index and return top-N results ranked by score.
-    pub fn search(&self, query_text: &str, limit: usize) -> 
crate::Result<SearchResult> {
-        let schema = self.index.schema();
-        let text_field = schema
-            .get_field("text")
-            .map_err(|_| crate::Error::UnexpectedError {
-                message: "Tantivy schema missing 'text' field".to_string(),
-                source: None,
-            })?;
-
-        let searcher = self.reader.searcher();
-        let query_parser = QueryParser::for_index(&self.index, 
vec![text_field]);
-        let query =
-            query_parser
-                .parse_query(query_text)
-                .map_err(|e| crate::Error::UnexpectedError {
-                    message: format!("Failed to parse query '{}': {}", 
query_text, e),
-                    source: None,
-                })?;
-
-        let top_docs = searcher
-            .search(&query, &TopDocs::with_limit(limit))
-            .map_err(|e| crate::Error::UnexpectedError {
-                message: format!("Tantivy search failed: {}", e),
-                source: None,
-            })?;
-
-        let mut row_ids = Vec::with_capacity(top_docs.len());
-        let mut scores = Vec::with_capacity(top_docs.len());
-
-        for (score, doc_address) in &top_docs {
-            let segment_reader = 
searcher.segment_reader(doc_address.segment_ord);
-            let fast_fields = 
segment_reader.fast_fields().u64("row_id").map_err(|e| {
-                crate::Error::UnexpectedError {
-                    message: format!("Failed to get row_id fast field: {}", e),
-                    source: None,
-                }
-            })?;
-            let row_id = fast_fields.first(doc_address.doc_id).ok_or_else(|| {
-                crate::Error::UnexpectedError {
-                    message: format!(
-                        "Missing row_id for doc_id {} in segment {}",
-                        doc_address.doc_id, doc_address.segment_ord
-                    ),
-                    source: None,
-                }
-            })?;
-            row_ids.push(row_id);
-            scores.push(*score);
-        }
-
-        Ok(SearchResult::new(row_ids, scores))
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::io::FileIOBuilder;
-    use crate::tantivy::writer::TantivyFullTextWriter;
-
-    async fn create_test_reader() -> TantivyFullTextReader {
-        let file_io = FileIOBuilder::new("memory").build().unwrap();
-        let mut writer = TantivyFullTextWriter::new().unwrap();
-        writer
-            .add_document(0, Some("the quick brown fox jumps over the lazy 
dog"))
-            .unwrap();
-        writer
-            .add_document(1, Some("rust programming language"))
-            .unwrap();
-        writer
-            .add_document(2, Some("apache paimon data lake"))
-            .unwrap();
-        writer
-            .add_document(3, Some("full text search with tantivy"))
-            .unwrap();
-        writer
-            .add_document(4, Some("the fox is quick and brown"))
-            .unwrap();
-        let output = file_io.new_output("/test_reader_index.archive").unwrap();
-        writer.finish(&output).await.unwrap();
-        TantivyFullTextReader::from_input_file(&output.to_input_file())
-            .await
-            .unwrap()
-    }
-
-    #[tokio::test]
-    async fn test_search_basic() {
-        let reader = create_test_reader().await;
-        let result = reader.search("fox", 10).unwrap();
-        assert_eq!(result.len(), 2);
-        assert!(result.row_ids.contains(&0));
-        assert!(result.row_ids.contains(&4));
-    }
-
-    #[tokio::test]
-    async fn test_search_limit() {
-        let reader = create_test_reader().await;
-        let result = reader.search("fox", 1).unwrap();
-        assert_eq!(result.len(), 1);
-    }
-
-    #[tokio::test]
-    async fn test_search_no_match() {
-        let reader = create_test_reader().await;
-        let result = reader.search("nonexistent", 10).unwrap();
-        assert!(result.is_empty());
-    }
-
-    #[tokio::test]
-    async fn test_search_scored() {
-        let reader = create_test_reader().await;
-        let result = reader.search("tantivy", 10).unwrap();
-        assert_eq!(result.len(), 1);
-        assert!(result.row_ids.contains(&3));
-        assert!(result.scores[0] > 0.0);
-    }
-
-    #[tokio::test]
-    async fn test_search_with_offset() {
-        let reader = create_test_reader().await;
-        let result = reader.search("fox", 10).unwrap();
-        let offset_result = result.offset(1000);
-        assert!(offset_result.row_ids.contains(&1000));
-        assert!(offset_result.row_ids.contains(&1004));
-    }
-}
diff --git a/crates/paimon/src/tantivy/writer.rs 
b/crates/paimon/src/tantivy/writer.rs
deleted file mode 100644
index a12ed300..00000000
--- a/crates/paimon/src/tantivy/writer.rs
+++ /dev/null
@@ -1,242 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied.  See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-//! Tantivy full-text index writer.
-//!
-//! Writes documents (rowId + text) to a local Tantivy index, then packs the
-//! index directory into a single archive file (Big-Endian, Java-compatible)
-//! and writes it to an `OutputFile`.
-//!
-//! Reference: 
`org.apache.paimon.tantivy.index.TantivyFullTextGlobalIndexWriter`
-
-use bytes::Bytes;
-use std::io::Read;
-
-use crate::io::OutputFile;
-use tantivy::schema::{Field, NumericOptions, Schema, TEXT};
-use tantivy::{Index, IndexWriter, TantivyDocument};
-
-/// Builds the fixed schema: `row_id` (u64 fast+stored+indexed) + `text` 
(full-text).
-fn build_schema() -> (Schema, Field, Field) {
-    let mut builder = Schema::builder();
-    let row_id_field = builder.add_u64_field(
-        "row_id",
-        NumericOptions::default()
-            .set_stored()
-            .set_indexed()
-            .set_fast(),
-    );
-    let text_field = builder.add_text_field("text", TEXT);
-    (builder.build(), row_id_field, text_field)
-}
-
-/// Writer for creating a Tantivy full-text index and packing it into an 
archive.
-pub struct TantivyFullTextWriter {
-    writer: IndexWriter,
-    row_id_field: Field,
-    text_field: Field,
-    temp_dir: tempfile::TempDir,
-    row_count: u64,
-}
-
-impl TantivyFullTextWriter {
-    /// Create a new writer. The index is built in a temporary directory.
-    pub fn new() -> crate::Result<Self> {
-        let temp_dir = tempfile::tempdir().map_err(|e| 
crate::Error::UnexpectedError {
-            message: format!("Failed to create temp directory for Tantivy 
index: {}", e),
-            source: None,
-        })?;
-
-        let (schema, row_id_field, text_field) = build_schema();
-        let index = Index::create_in_dir(temp_dir.path(), schema).map_err(|e| {
-            crate::Error::UnexpectedError {
-                message: format!("Failed to create Tantivy index: {}", e),
-                source: None,
-            }
-        })?;
-        let writer = index
-            .writer(50_000_000)
-            .map_err(|e| crate::Error::UnexpectedError {
-                message: format!("Failed to create Tantivy writer: {}", e),
-                source: None,
-            })?;
-
-        Ok(Self {
-            writer,
-            row_id_field,
-            text_field,
-            temp_dir,
-            row_count: 0,
-        })
-    }
-
-    /// Add a document with the given row ID and text content.
-    /// If text is None, the row ID is still incremented (null value).
-    pub fn add_document(&mut self, row_id: u64, text: Option<&str>) -> 
crate::Result<()> {
-        if let Some(text) = text {
-            let mut doc = TantivyDocument::new();
-            doc.add_u64(self.row_id_field, row_id);
-            doc.add_text(self.text_field, text);
-            self.writer
-                .add_document(doc)
-                .map_err(|e| crate::Error::UnexpectedError {
-                    message: format!("Failed to add document: {}", e),
-                    source: None,
-                })?;
-        }
-        self.row_count += 1;
-        Ok(())
-    }
-
-    /// Commit, pack the index into an archive, and write it to the given 
`OutputFile`.
-    ///
-    /// Returns `false` if no documents were written (nothing is written to 
the file).
-    ///
-    /// Reference: `TantivyFullTextGlobalIndexWriter.packIndex()`
-    pub async fn finish(mut self, output: &OutputFile) -> crate::Result<bool> {
-        if self.row_count == 0 {
-            return Ok(false);
-        }
-
-        self.writer
-            .commit()
-            .map_err(|e| crate::Error::UnexpectedError {
-                message: format!("Failed to commit Tantivy index: {}", e),
-                source: None,
-            })?;
-
-        // Drop the writer to release file locks before packing.
-        drop(self.writer);
-
-        // Stream the archive directly to the OutputFile.
-        let mut file_writer = output.writer().await?;
-
-        // Collect file entries from the temp directory.
-        let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
-        for entry in
-            std::fs::read_dir(self.temp_dir.path()).map_err(|e| 
crate::Error::UnexpectedError {
-                message: format!("Failed to read Tantivy index directory: {}", 
e),
-                source: None,
-            })?
-        {
-            let entry = entry.map_err(|e| crate::Error::UnexpectedError {
-                message: format!("Failed to read directory entry: {}", e),
-                source: None,
-            })?;
-            if entry.file_type().map(|t| t.is_file()).unwrap_or(false) {
-                let name = entry.file_name().to_string_lossy().to_string();
-                entries.push((name, entry.path()));
-            }
-        }
-
-        // Write file count (4 bytes BE).
-        file_writer
-            .write(Bytes::from((entries.len() as i32).to_be_bytes().to_vec()))
-            .await?;
-
-        // Write each file: name_len + name + data_len + data (chunked).
-        const CHUNK_SIZE: usize = 4 * 1024 * 1024; // 4 MiB
-        for (name, path) in &entries {
-            let name_bytes = name.as_bytes();
-            file_writer
-                .write(Bytes::from(
-                    (name_bytes.len() as i32).to_be_bytes().to_vec(),
-                ))
-                .await?;
-            file_writer.write(Bytes::from(name_bytes.to_vec())).await?;
-
-            let file_len = std::fs::metadata(path)
-                .map_err(|e| crate::Error::UnexpectedError {
-                    message: format!("Failed to stat index file '{}': {}", 
name, e),
-                    source: None,
-                })?
-                .len();
-            file_writer
-                .write(Bytes::from((file_len as i64).to_be_bytes().to_vec()))
-                .await?;
-
-            let mut file =
-                std::fs::File::open(path).map_err(|e| 
crate::Error::UnexpectedError {
-                    message: format!("Failed to open index file '{}': {}", 
name, e),
-                    source: None,
-                })?;
-            let mut buf = vec![0u8; CHUNK_SIZE];
-            loop {
-                let n = file
-                    .read(&mut buf)
-                    .map_err(|e| crate::Error::UnexpectedError {
-                        message: format!("Failed to read index file '{}': {}", 
name, e),
-                        source: None,
-                    })?;
-                if n == 0 {
-                    break;
-                }
-                file_writer.write(Bytes::copy_from_slice(&buf[..n])).await?;
-            }
-        }
-
-        file_writer.close().await?;
-        Ok(true)
-    }
-
-    /// Number of rows processed (including nulls).
-    pub fn row_count(&self) -> u64 {
-        self.row_count
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::io::FileIOBuilder;
-    use crate::tantivy::reader::TantivyFullTextReader;
-
-    #[tokio::test]
-    async fn test_write_and_read_roundtrip() {
-        let file_io = FileIOBuilder::new("memory").build().unwrap();
-
-        let mut writer = TantivyFullTextWriter::new().unwrap();
-        writer.add_document(0, Some("hello world")).unwrap();
-        writer.add_document(1, Some("foo bar baz")).unwrap();
-        writer.add_document(2, None).unwrap();
-        writer.add_document(3, Some("hello again")).unwrap();
-
-        let output = file_io.new_output("/test_index.archive").unwrap();
-        let written = writer.finish(&output).await.unwrap();
-        assert!(written);
-
-        let input = output.to_input_file();
-        let reader = TantivyFullTextReader::from_input_file(&input)
-            .await
-            .unwrap();
-        let result = reader.search("hello", 10).unwrap();
-        assert_eq!(result.len(), 2);
-        assert!(result.row_ids.contains(&0));
-        assert!(result.row_ids.contains(&3));
-    }
-
-    #[tokio::test]
-    async fn test_empty_writer() {
-        let file_io = FileIOBuilder::new("memory").build().unwrap();
-        let output = file_io.new_output("/empty_index.archive").unwrap();
-
-        let writer = TantivyFullTextWriter::new().unwrap();
-        let written = writer.finish(&output).await.unwrap();
-        assert!(!written);
-        assert!(!output.exists().await.unwrap());
-    }
-}


Reply via email to