Skip to content

Commit a7157c2

Browse files
committed
feat: add Jieba tokenizer for full-text search
1 parent b3da562 commit a7157c2

19 files changed

Lines changed: 1459 additions & 584 deletions

CMakeLists.txt

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,22 @@ if(PAIMON_ENABLE_LUMINA)
290290
DESTINATION ${CMAKE_INSTALL_LIBDIR})
291291
endif()
292292
293+
if(PAIMON_ENABLE_LUCENE)
294+
set(PAIMON_DICT_DEST "share/paimon/dict")
295+
296+
install(DIRECTORY ${JIEBA_DICT_DIR}/
297+
DESTINATION ${PAIMON_DICT_DEST}
298+
FILES_MATCHING
299+
PATTERN "jieba.dict.utf8"
300+
PATTERN "hmm_model.utf8"
301+
PATTERN "idf.utf8"
302+
PATTERN "stop_words.utf8"
303+
PATTERN "user.dict.utf8"
304+
PATTERN "pos_dict"
305+
PATTERN ".git*" EXCLUDE
306+
PATTERN "*.md" EXCLUDE)
307+
endif()
308+
293309
install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/
294310
DESTINATION "include"
295311
FILES_MATCHING
@@ -389,7 +405,6 @@ if(PAIMON_BUILD_TESTS)
389405
list(APPEND TEST_STATIC_LINK_LIBS paimon_lucene_index_shared)
390406
list(APPEND TEST_STATIC_LINK_LIBS "-Wl,--as-needed")
391407
endif()
392-
393408
endif()
394409
395410
include(CMakePackageConfigHelpers)

cmake_modules/ThirdpartyToolchain.cmake

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -322,8 +322,7 @@ macro(build_lucene)
322322
set(LUCENE_INCLUDE_DIR "${LUCENE_PREFIX}/include")
323323
# The include directory must exist before it is referenced by a target.
324324
file(MAKE_DIRECTORY "${LUCENE_INCLUDE_DIR}")
325-
include_directories(SYSTEM ${LUCENE_INCLUDE_DIR} ${BOOST_INCLUDE_DIR}
326-
${BOOST_EXTRA_INCLUDE_DIR})
325+
include_directories(SYSTEM ${LUCENE_INCLUDE_DIR} ${BOOST_INCLUDE_DIR})
327326
add_library(lucene INTERFACE IMPORTED)
328327
target_include_directories(lucene SYSTEM INTERFACE "${LUCENE_INCLUDE_DIR}")
329328
target_compile_options(lucene INTERFACE -pthread)
@@ -343,6 +342,44 @@ macro(build_lucene)
343342
add_dependencies(lucene lucene_ep)
344343
endmacro()
345344

345+
macro(build_jieba)
346+
message(STATUS "Building jieba from source")
347+
set(JIEBA_PREFIX "${CMAKE_CURRENT_BINARY_DIR}/jieba_ep-prefix")
348+
set(JIEBA_INSTALL "${CMAKE_CURRENT_BINARY_DIR}/jieba_ep-install")
349+
set(JIEBA_INCLUDE_DIR "${JIEBA_INSTALL}/include")
350+
set(JIEBA_DICT_DIR "${JIEBA_INSTALL}/dict")
351+
file(MAKE_DIRECTORY ${JIEBA_INCLUDE_DIR})
352+
file(MAKE_DIRECTORY ${JIEBA_DICT_DIR})
353+
354+
set(JIEBA_CMAKE_ARGS
355+
${EP_COMMON_CMAKE_ARGS}
356+
"-DENABLE_TEST=OFF"
357+
"-DCPPJIEBA_TOP_LEVEL_PROJECT=OFF"
358+
"-DCMAKE_INSTALL_PREFIX=${JIEBA_INSTALL}")
359+
360+
set(PATCH_FILE "${CMAKE_CURRENT_LIST_DIR}/jieba.diff")
361+
externalproject_add(jieba_ep
362+
${EP_COMMON_OPTIONS}
363+
GIT_REPOSITORY https://github.com/yanyiwu/cppjieba.git
364+
GIT_TAG ${PAIMON_JIEBA_BUILD_VERSION}
365+
GIT_SHALLOW FALSE
366+
GIT_PROGRESS TRUE
367+
GIT_SUBMODULES_RECURSE TRUE
368+
CMAKE_ARGS ${JIEBA_CMAKE_ARGS}
369+
LOG_PATCH ON
370+
PATCH_COMMAND ${CMAKE_COMMAND} -E chdir <SOURCE_DIR> bash -c
371+
"[ -f .patched ] && echo '<SOURCE_DIR> patch already applied, ignore...' || patch -s -N -p1 -i '${PATCH_FILE}' && touch .patched"
372+
INSTALL_COMMAND bash -c
373+
"cp -r ${JIEBA_PREFIX}/src/jieba_ep/include/* ${JIEBA_INSTALL}/include/ && cp -r ${JIEBA_PREFIX}/src/jieba_ep/dict/* ${JIEBA_INSTALL}/dict/ && cp -r ${JIEBA_PREFIX}/src/jieba_ep/deps/limonp/include/* ${JIEBA_INSTALL}/include/"
374+
)
375+
376+
# The include directory must exist before it is referenced by a target.
377+
include_directories(SYSTEM ${JIEBA_INCLUDE_DIR} ${JIEBA_DICT_DIR})
378+
add_library(jieba INTERFACE IMPORTED)
379+
target_include_directories(jieba SYSTEM INTERFACE "${JIEBA_INCLUDE_DIR} ${JIEBA_DICT_DIR}")
380+
add_dependencies(jieba jieba_ep)
381+
endmacro()
382+
346383
macro(build_rapidjson)
347384
message(STATUS "Building RapidJSON from source")
348385
set(RAPIDJSON_PREFIX "${CMAKE_CURRENT_BINARY_DIR}/rapidjson_ep-install")
@@ -1272,4 +1309,5 @@ endif()
12721309
if(PAIMON_ENABLE_LUCENE)
12731310
build_boost()
12741311
build_lucene()
1312+
build_jieba()
12751313
endif()

cmake_modules/jieba.diff

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
diff --git a/include/cppjieba/KeywordExtractor.hpp b/include/cppjieba/KeywordExtractor.hpp
2+
index 24b2c40..c7c6a94 100644
3+
--- a/include/cppjieba/KeywordExtractor.hpp
4+
+++ b/include/cppjieba/KeywordExtractor.hpp
5+
@@ -89,6 +89,11 @@ class KeywordExtractor {
6+
std::partial_sort(keywords.begin(), keywords.begin() + topN, keywords.end(), Compare);
7+
keywords.resize(topN);
8+
}
9+
+
10+
+ const std::unordered_set<std::string>& GetStopWords() const {
11+
+ return stopWords_;
12+
+ }
13+
+
14+
private:
15+
void LoadIdfDict(const std::string& idfPath) {
16+
std::ifstream ifs(idfPath.c_str());

src/paimon/global_index/lucene/CMakeLists.txt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,23 @@
1313
# limitations under the License.
1414

1515
if(PAIMON_ENABLE_LUCENE)
16-
set(PAIMON_LUCENE lucene_global_index.cpp lucene_directory.cpp
16+
set(PAIMON_LUCENE lucene_global_index.cpp
17+
lucene_directory.cpp
18+
jieba_analyzer.cpp
19+
lucene_global_index_writer.cpp
20+
lucene_global_index_reader.cpp
1721
lucene_global_index_factory.cpp)
1822

1923
add_paimon_lib(paimon_lucene_index
2024
SOURCES
2125
${PAIMON_LUCENE}
2226
EXTRA_INCLUDES
2327
${LUCENE_INCLUDE_DIR}
28+
${JIEBA_INCLUDE_DIR}
2429
DEPENDENCIES
2530
paimon_shared
2631
lucene
32+
jieba
2733
STATIC_LINK_LIBS
2834
lucene
2935
arrow
@@ -38,7 +44,9 @@ if(PAIMON_ENABLE_LUCENE)
3844
if(PAIMON_BUILD_TESTS)
3945
add_paimon_test(lucene_index_test
4046
SOURCES
47+
jieba_analyzer_test.cpp
4148
lucene_api_test.cpp
49+
jieba_api_test.cpp
4250
lucene_directory_test.cpp
4351
lucene_global_index_test.cpp
4452
lucene_filter_test.cpp
@@ -52,5 +60,8 @@ if(PAIMON_ENABLE_LUCENE)
5260
paimon_lucene_index_static
5361
"-Wl,--no-whole-archive"
5462
${GTEST_LINK_TOOLCHAIN})
63+
64+
target_compile_definitions(paimon-lucene-index-test PRIVATE
65+
JIEBA_TEST_DICT_DIR="${JIEBA_DICT_DIR}")
5566
endif()
5667
endif()
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/*
2+
* Copyright 2026-present Alibaba Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
#include "paimon/global_index/lucene/jieba_analyzer.h"
17+
18+
#include "paimon/global_index/lucene/lucene_utils.h"
19+
20+
namespace paimon::lucene {
21+
JiebaTokenizerContext::JiebaTokenizerContext(const std::string& _tokenize_mode, bool _with_position,
22+
const std::shared_ptr<cppjieba::Jieba> _jieba,
23+
const std::shared_ptr<MemoryPool>& _pool,
24+
int32_t _buffer_size)
25+
: pool(_pool),
26+
tokenize_mode(_tokenize_mode),
27+
with_position(_with_position),
28+
buffer_size(_buffer_size),
29+
jieba(_jieba) {}
30+
31+
JiebaTokenizer::JiebaTokenizer(const JiebaTokenizerContext& context, const Lucene::ReaderPtr& input)
32+
: Lucene::Tokenizer(), context_(context) {
33+
this->input = input;
34+
term_att_ = addAttribute<Lucene::TermAttribute>();
35+
pos_att_ = addAttribute<Lucene::PositionIncrementAttribute>();
36+
buffer_ = static_cast<wchar_t*>(
37+
context_.pool->Malloc(context_.buffer_size * sizeof(wchar_t), /*aligment=*/8));
38+
}
39+
40+
JiebaTokenizer::~JiebaTokenizer() {
41+
if (buffer_) {
42+
context_.pool->Free(reinterpret_cast<void*>(buffer_),
43+
context_.buffer_size * sizeof(wchar_t),
44+
/*aligment=*/8);
45+
buffer_ = nullptr;
46+
}
47+
}
48+
49+
bool JiebaTokenizer::incrementToken() {
50+
if (term_index_ >= normalized_terms_.size()) {
51+
return false;
52+
}
53+
54+
const auto& term = normalized_terms_[term_index_++];
55+
clearAttributes();
56+
57+
term_att_->setTermBuffer(LuceneUtils::StringToWstring(term));
58+
59+
if (context_.with_position) {
60+
pos_att_->setPositionIncrement(1);
61+
} else {
62+
pos_att_->setPositionIncrement(0);
63+
}
64+
return true;
65+
}
66+
67+
void JiebaTokenizer::CutWithMode(const std::string& tokenize_mode, const cppjieba::Jieba* jieba,
68+
const std::string& str, std::vector<std::string>* terms_ptr) {
69+
auto& terms = *terms_ptr;
70+
if (tokenize_mode == "mp") {
71+
jieba->CutSmall(str, terms, /*max_word_len=*/JiebaTokenizerContext::kMaxWordLen);
72+
} else if (tokenize_mode == "hmm") {
73+
jieba->CutHMM(str, terms);
74+
} else if (tokenize_mode == "mix") {
75+
jieba->Cut(str, terms, /*hmm=*/true);
76+
} else if (tokenize_mode == "full") {
77+
jieba->CutAll(str, terms);
78+
} else if (tokenize_mode == "query") {
79+
jieba->CutForSearch(str, terms, /*hmm=*/true);
80+
} else {
81+
throw Lucene::IllegalArgumentException(
82+
L"only support mp/hmm/mix/full/query in jieba tokenizer");
83+
}
84+
}
85+
86+
void JiebaTokenizer::Normalize(const std::unordered_set<std::string>& stop_words,
87+
std::vector<std::string>* input_ptr,
88+
std::vector<std::string_view>* output_ptr) {
89+
auto& input = *input_ptr;
90+
auto& output = *output_ptr;
91+
output.clear();
92+
output.reserve(input.size());
93+
for (auto& term : input) {
94+
// remove stop words
95+
if (stop_words.find(term) != stop_words.end()) {
96+
continue;
97+
}
98+
99+
// to lower case
100+
bool is_alphanumeric = true;
101+
for (const unsigned char& c : term) {
102+
if (!std::isalnum(c)) {
103+
is_alphanumeric = false;
104+
break;
105+
}
106+
}
107+
if (is_alphanumeric && !term.empty()) {
108+
std::transform(term.begin(), term.end(), term.begin(), ::tolower);
109+
}
110+
output.emplace_back(term.data(), term.length());
111+
}
112+
}
113+
114+
void JiebaTokenizer::reset() {
115+
Lucene::Tokenizer::reset();
116+
terms_.clear();
117+
normalized_terms_.clear();
118+
term_index_ = 0;
119+
120+
// read wchar from input
121+
Lucene::String wstr;
122+
wstr.reserve(context_.buffer_size);
123+
while (true) {
124+
int32_t length = input->read(buffer_, /*offset=*/0, context_.buffer_size);
125+
if (length <= 0) {
126+
break;
127+
}
128+
wstr.append(buffer_, length);
129+
}
130+
131+
// jieba tokenize
132+
std::string doc_str = LuceneUtils::WstringToString(wstr);
133+
// TODO(xinyu.lxy): support porter2 stemmer
134+
CutWithMode(context_.tokenize_mode, context_.jieba.get(), doc_str, &terms_);
135+
Normalize(context_.jieba->extractor.GetStopWords(), &terms_, &normalized_terms_);
136+
}
137+
138+
} // namespace paimon::lucene
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2026-present Alibaba Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
#pragma once
17+
#include "cppjieba/Jieba.hpp"
18+
#include "lucene++/LuceneHeaders.h"
19+
#include "lucene++/MiscUtils.h"
20+
#include "lucene++/PositionIncrementAttribute.h"
21+
#include "lucene++/TermAttribute.h"
22+
#include "paimon/global_index/lucene/lucene_utils.h"
23+
#include "paimon/memory/memory_pool.h"
24+
namespace paimon::lucene {
25+
struct JiebaTokenizerContext {
26+
JiebaTokenizerContext(const std::string& _tokenize_mode, bool _with_position,
27+
const std::shared_ptr<cppjieba::Jieba> _jieba,
28+
const std::shared_ptr<MemoryPool>& _pool,
29+
int32_t _buffer_size = kReadBufferSize);
30+
31+
std::shared_ptr<MemoryPool> pool;
32+
std::string tokenize_mode;
33+
bool with_position;
34+
int32_t buffer_size;
35+
std::shared_ptr<cppjieba::Jieba> jieba;
36+
37+
static inline const int32_t kReadBufferSize = 5 * 1024 * 1024;
38+
static inline const int32_t kMaxWordLen = 1024;
39+
};
40+
41+
class JiebaTokenizer : public Lucene::Tokenizer {
42+
public:
43+
JiebaTokenizer(const JiebaTokenizerContext& context, const Lucene::ReaderPtr& input);
44+
45+
~JiebaTokenizer() override;
46+
47+
bool incrementToken() override;
48+
49+
void reset() override;
50+
51+
static void CutWithMode(const std::string& tokenize_mode, const cppjieba::Jieba* jieba,
52+
const std::string& str, std::vector<std::string>* terms_ptr);
53+
54+
// inplace to lower to avoid data copy
55+
static void Normalize(const std::unordered_set<std::string>& stop_words,
56+
std::vector<std::string>* input, std::vector<std::string_view>* output);
57+
58+
private:
59+
JiebaTokenizerContext context_;
60+
size_t term_index_ = 0;
61+
std::vector<std::string> terms_;
62+
std::vector<std::string_view> normalized_terms_;
63+
wchar_t* buffer_;
64+
Lucene::TermAttributePtr term_att_;
65+
Lucene::PositionIncrementAttributePtr pos_att_;
66+
};
67+
68+
class JiebaAnalyzer : public Lucene::Analyzer {
69+
public:
70+
JiebaAnalyzer(const JiebaTokenizerContext& context) : context_(context) {}
71+
72+
~JiebaAnalyzer() override = default;
73+
74+
Lucene::TokenStreamPtr tokenStream(const Lucene::String& field_name,
75+
const Lucene::ReaderPtr& reader) override {
76+
return Lucene::newLucene<JiebaTokenizer>(context_, reader);
77+
}
78+
79+
private:
80+
JiebaTokenizerContext context_;
81+
};
82+
} // namespace paimon::lucene

0 commit comments

Comments
 (0)