blob: 31861cf2906cc7749cea16f5ea57acf7b0d4663d [file] [view]
<!--
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.
-->
# Apache Paimon Full Text Index
Standalone Tantivy-based full-text index library for Apache Paimon-style data
lake storage. The project follows the same shape as `paimon-vector-index`:
- `core`: Rust implementation and v1 storage format.
- `ffi`: C ABI over the Rust core.
- `jni`: Java JNI bridge over the Rust core.
- `java`: public Java API.
- `python`: Python ctypes API over the C ABI.
The index file is self-describing. Readers only need positional `pread` I/O
and do not depend on Paimon manifest metadata.
## Current Status
Implemented:
- Rust writer, reader, v1 envelope, and search.
- Single-field, multi-field, repeated-field string-array, and dotted-path text
fields.
- Match, fuzzy match (`fuzziness`, `auto`, `max_expansions`,
`prefix_length`), phrase, boolean, multi-match, and boost-demotion queries.
- C FFI writer/reader/search with query JSON strings, including serialized
64-bit Roaring row-id filters.
- Java API and JNI bridge.
- Python ctypes package.
- Cross-boundary round-trip tests for Rust core, FFI, Java/JNI, and Python.
Supported tokenizers in this first implementation:
- `default`
- `simple`
- `whitespace`
- `raw`
- `ngram`
- `jieba`
Default tokenizer behavior uses English full-text defaults: lower-case,
stemming, stop-word removal, ASCII folding, max token length 40, and positions.
Set `with-position=false` only when phrase search is not needed.
Readers expose archived Tantivy files through a seek-on-demand directory, so
opening an index reads the envelope and Tantivy metadata without loading all
segment files into memory.
## Build
```bash
cargo test -p paimon-ftindex-core
cargo test -p paimon-ftindex-ffi
cargo build -p paimon-ftindex-ffi
cargo build -p paimon-ftindex-jni
mvn -q -f java/pom.xml test
PYTHONPATH=python python3 -m pytest -q python/tests
```
## Rust Example
```rust
use paimon_ftindex_core::io::{PosWriter, SliceReader};
use paimon_ftindex_core::{FullTextIndexConfig, FullTextIndexReader, FullTextIndexWriter};
let mut writer = FullTextIndexWriter::new(FullTextIndexConfig::new())?;
writer.add_document(1, "Apache Paimon full text search")?;
let mut bytes = Vec::new();
writer.write(&mut PosWriter::new(&mut bytes))?;
let reader = FullTextIndexReader::open(SliceReader::new(bytes))?;
reader.prewarm()?;
let result = reader.search(r#"{"match":{"query":"paimon","column":"text"}}"#, 10)?;
```
Multi-field indexes can be configured with named fields:
```rust
let config = FullTextIndexConfig::new().with_text_fields(["title", "body"]);
let mut writer = FullTextIndexWriter::new(config)?;
writer.add_document_fields(
1,
[
("title".to_string(), "Apache Paimon".to_string()),
("body".to_string(), "lake storage".to_string()),
],
)?;
```
When a `match` query omits `column`, the reader searches all indexed text
fields. This lets a Paimon adapter populate extra fields internally without
requiring callers to build a `multi_match` query.
To restrict search to an upstream candidate set, pass a serialized
`RoaringTreemap` of allowed row ids:
```rust
let filtered = reader.search_with_roaring_filter(
r#"{"match":{"query":"paimon","column":"text"}}"#,
10,
roaring_filter_bytes,
)?;
```
## Python Example
```python
from io import BytesIO
from paimon_ftindex import FullTextIndexReader, FullTextIndexWriter
out = BytesIO()
with FullTextIndexWriter({"text-fields": "title,body"}) as writer:
writer.add_document_fields(1, {"title": "Apache Paimon", "body": "lake storage"})
writer.write(out)
class Input:
def __init__(self, data):
self.data = data
def pread(self, pos, length):
return self.data[pos:pos + length]
with FullTextIndexReader(Input(out.getvalue())) as reader:
reader.prewarm()
ids, scores = reader.search('{"match":{"query":"paimon"}}', limit=10)
filtered_ids, filtered_scores = reader.search(
'{"match":{"query":"paimno","column":"title","fuzziness":1}}',
limit=10,
filter_bytes=roaring_filter_bytes,
)
metrics = reader.read_metrics()
```
Search APIs accept the query DSL as a JSON string.
`prewarm()` eagerly initializes the underlying search reader and archive cache
before a query burst. `read_metrics()` reports positional read calls/bytes and
archive cache hit/miss counters for tuning reader reuse and object-store access
patterns.