Skip to content

Commit bc75925

Browse files
SteNicholasclaude
andcommitted
perf(lucene): load Jieba dictionaries once per process
Every Lucene global index reader and writer constructed its own cppjieba::Jieba, reloading about 11.6MB of dictionaries each time. A table-level full-text search creates one reader per shard, so every query paid this cost once per shard. After the table-level full-text search tests were added in #461, paimon-global-index-test on tsan-x86_64 grew from about 1400s to about 3500s and pushed the job past its 120-minute timeout. Add LuceneUtils::GetJieba(), which loads each dictionary directory once per process and shares a const segmenter with all readers and writers. Segmentation only calls const cppjieba methods, which keep no mutable state and are safe to call concurrently. The cache is keyed by the canonical absolute path, so a relative PAIMON_JIEBA_DICT_DIR resolves against the current working directory instead of reusing dictionaries loaded from another one, and a directory that cannot be resolved returns an IOError instead of aborting in cppjieba. Document how a relative directory is resolved and that dictionary changes take effect only after the process restarts. On tsan-x86_64, this cuts paimon-global-index-test from about 3500s to about 160s, and the job finishes in about an hour instead of timing out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 03d291f commit bc75925

10 files changed

Lines changed: 185 additions & 16 deletions

‎docs/source/user_guide/global_index.rst‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ a column name containing only such whitespace selects the indexed field.
144144

145145
- **Environment Variable**: ``PAIMON_JIEBA_DICT_DIR``
146146

147-
- **Description**: Specifies the directory containing Jieba dictionary files for Chinese text tokenization. At runtime, the system first checks this environment variable; if not set, it falls back to the compile-time ``JIEBA_TEST_DICT_DIR`` macro (only available in test builds). If neither is available, will fail with an error.
147+
- **Description**: Specifies the directory containing Jieba dictionary files for Chinese text tokenization. At runtime, the system first checks this environment variable; if not set, it falls back to the compile-time ``JIEBA_TEST_DICT_DIR`` macro (only available in test builds). If neither is available, will fail with an error. A relative directory is resolved against the current working directory. Each dictionary directory is loaded once per process and shared by all Lucene index readers and writers, so changes to its dictionary files take effect only after the process restarts.
148148

149149
Full-Text Index (Experimental)
150150
------------------------------

‎src/paimon/global_index/lucene/CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ if(PAIMON_ENABLE_LUCENE)
5858
lucene_directory_test.cpp
5959
lucene_global_index_test.cpp
6060
lucene_filter_test.cpp
61+
lucene_utils_test.cpp
6162
EXTRA_INCLUDES
6263
${LUCENE_INCLUDE_DIR}
6364
STATIC_LINK_LIBS

‎src/paimon/global_index/lucene/jieba_analyzer.cpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
namespace paimon::lucene {
2626
JiebaTokenizerContext::JiebaTokenizerContext(const std::string& _tokenize_mode, bool _with_position,
27-
const std::shared_ptr<cppjieba::Jieba>& _jieba,
27+
const std::shared_ptr<const cppjieba::Jieba>& _jieba,
2828
const std::shared_ptr<MemoryPool>& _pool,
2929
int32_t _buffer_size)
3030
: pool(_pool),

‎src/paimon/global_index/lucene/jieba_analyzer.h‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,15 @@
2626
namespace paimon::lucene {
2727
struct JiebaTokenizerContext {
2828
JiebaTokenizerContext(const std::string& _tokenize_mode, bool _with_position,
29-
const std::shared_ptr<cppjieba::Jieba>& _jieba,
29+
const std::shared_ptr<const cppjieba::Jieba>& _jieba,
3030
const std::shared_ptr<MemoryPool>& _pool,
3131
int32_t _buffer_size = kReadBufferSize);
3232

3333
std::shared_ptr<MemoryPool> pool;
3434
std::string tokenize_mode;
3535
bool with_position;
3636
int32_t buffer_size;
37-
std::shared_ptr<cppjieba::Jieba> jieba;
37+
std::shared_ptr<const cppjieba::Jieba> jieba;
3838

3939
static inline const int32_t kReadBufferSize = 5 * 1024 * 1024;
4040
static inline const int32_t kMaxWordLen = 1024;

‎src/paimon/global_index/lucene/lucene_global_index_reader.cpp‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -530,11 +530,8 @@ Result<std::shared_ptr<LuceneGlobalIndexReader>> LuceneGlobalIndexReader::Create
530530
Lucene::IndexReaderPtr reader = Lucene::IndexReader::open(lucene_dir, /*read_only=*/true);
531531
Lucene::IndexSearcherPtr searcher = Lucene::newLucene<Lucene::IndexSearcher>(reader);
532532

533-
PAIMON_ASSIGN_OR_RAISE(std::string dictionary_dir, LuceneUtils::GetJiebaDictionaryDir());
534-
auto jieba = std::make_shared<cppjieba::Jieba>(
535-
dictionary_dir + "/jieba.dict.utf8", dictionary_dir + "/hmm_model.utf8",
536-
dictionary_dir + "/user.dict.utf8", dictionary_dir + "/idf.utf8",
537-
dictionary_dir + "/stop_words.utf8");
533+
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<const cppjieba::Jieba> jieba,
534+
LuceneUtils::GetJieba());
538535

539536
// priority: read options > write options > kDefaultJiebaTokenizeMode
540537
PAIMON_ASSIGN_OR_RAISE(

‎src/paimon/global_index/lucene/lucene_global_index_reader.h‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ class LuceneGlobalIndexReader : public GlobalIndexReader {
121121
LuceneGlobalIndexReader(const std::wstring& wfield_name,
122122
const Lucene::IndexSearcherPtr& searcher,
123123
const std::string& tokenize_mode,
124-
const std::shared_ptr<cppjieba::Jieba>& jieba)
124+
const std::shared_ptr<const cppjieba::Jieba>& jieba)
125125
: wfield_name_(wfield_name),
126126
searcher_(searcher),
127127
tokenize_mode_(tokenize_mode),
@@ -142,6 +142,6 @@ class LuceneGlobalIndexReader : public GlobalIndexReader {
142142
std::wstring wfield_name_;
143143
Lucene::IndexSearcherPtr searcher_;
144144
std::string tokenize_mode_;
145-
std::shared_ptr<cppjieba::Jieba> jieba_;
145+
std::shared_ptr<const cppjieba::Jieba> jieba_;
146146
};
147147
} // namespace paimon::lucene

‎src/paimon/global_index/lucene/lucene_global_index_writer.cpp‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,8 @@ Result<std::shared_ptr<LuceneGlobalIndexWriter>> LuceneGlobalIndexWriter::Create
7676
std::string tokenize_mode,
7777
OptionsUtils::GetValueFromMap(options, kJiebaTokenizeMode,
7878
std::string(kDefaultJiebaTokenizeMode)));
79-
PAIMON_ASSIGN_OR_RAISE(std::string dictionary_dir, LuceneUtils::GetJiebaDictionaryDir());
80-
auto jieba = std::make_shared<cppjieba::Jieba>(
81-
dictionary_dir + "/jieba.dict.utf8", dictionary_dir + "/hmm_model.utf8",
82-
dictionary_dir + "/user.dict.utf8", dictionary_dir + "/idf.utf8",
83-
dictionary_dir + "/stop_words.utf8");
79+
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<const cppjieba::Jieba> jieba,
80+
LuceneUtils::GetJieba());
8481
JiebaTokenizerContext jieba_context(tokenize_mode,
8582
/*with_position=*/!omit_term_freq_and_positions, jieba,
8683
pool);

‎src/paimon/global_index/lucene/lucene_utils.cpp‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@
1616
* limitations under the License.
1717
*/
1818
#include "paimon/global_index/lucene/lucene_utils.h"
19+
20+
#include <filesystem>
21+
#include <map>
22+
#include <mutex>
23+
#include <system_error>
24+
25+
#include "cppjieba/Jieba.hpp"
1926
namespace paimon::lucene {
2027

2128
Result<std::string> LuceneUtils::GetJiebaDictionaryDir() {
@@ -30,4 +37,27 @@ Result<std::string> LuceneUtils::GetJiebaDictionaryDir() {
3037
fmt::format("cannot get dictionary dir for jieba, must set env {}", kJiebaDictDirEnv));
3138
}
3239

40+
Result<std::shared_ptr<const cppjieba::Jieba>> LuceneUtils::GetJieba() {
41+
PAIMON_ASSIGN_OR_RAISE(std::string configured_dir, GetJiebaDictionaryDir());
42+
std::error_code ec;
43+
std::string dictionary_dir = std::filesystem::canonical(configured_dir, ec).string();
44+
if (ec) {
45+
return Status::IOError(fmt::format("cannot resolve dictionary dir {} for jieba: {}",
46+
configured_dir, ec.message()));
47+
}
48+
static std::mutex mutex;
49+
static std::map<std::string, std::shared_ptr<const cppjieba::Jieba>> dir_to_jieba;
50+
// The lock is held while loading, so concurrent callers wait for one load of a directory
51+
// instead of each loading it.
52+
std::lock_guard<std::mutex> lock(mutex);
53+
std::shared_ptr<const cppjieba::Jieba>& jieba = dir_to_jieba[dictionary_dir];
54+
if (!jieba) {
55+
jieba = std::make_shared<cppjieba::Jieba>(
56+
dictionary_dir + "/jieba.dict.utf8", dictionary_dir + "/hmm_model.utf8",
57+
dictionary_dir + "/user.dict.utf8", dictionary_dir + "/idf.utf8",
58+
dictionary_dir + "/stop_words.utf8");
59+
}
60+
return jieba;
61+
}
62+
3363
} // namespace paimon::lucene

‎src/paimon/global_index/lucene/lucene_utils.h‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,16 @@
1616
* limitations under the License.
1717
*/
1818
#pragma once
19+
#include <memory>
20+
#include <string>
21+
1922
#include "fmt/format.h"
2023
#include "lucene++/StringUtils.h"
2124
#include "paimon/global_index/lucene/lucene_defs.h"
2225
#include "paimon/result.h"
26+
namespace cppjieba {
27+
class Jieba;
28+
} // namespace cppjieba
2329
namespace paimon::lucene {
2430
class LuceneUtils {
2531
public:
@@ -37,5 +43,11 @@ class LuceneUtils {
3743
}
3844

3945
static Result<std::string> GetJiebaDictionaryDir();
46+
47+
/// Returns the Jieba segmenter of the dictionaries in `GetJiebaDictionaryDir()`, which is
48+
/// resolved to a canonical absolute path. Loading the dictionaries is slow, so each directory
49+
/// is loaded on first use, cached until the process exits and shared by all Lucene index
50+
/// readers and writers; its const methods are safe to call from many threads.
51+
static Result<std::shared_ptr<const cppjieba::Jieba>> GetJieba();
4052
};
4153
} // namespace paimon::lucene
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
#include "paimon/global_index/lucene/lucene_utils.h"
19+
20+
#include <cstdlib>
21+
#include <filesystem>
22+
#include <memory>
23+
#include <optional>
24+
#include <string>
25+
#include <system_error>
26+
#include <thread>
27+
#include <vector>
28+
29+
#include "cppjieba/Jieba.hpp"
30+
#include "fmt/format.h"
31+
#include "gtest/gtest.h"
32+
#include "paimon/common/utils/path_util.h"
33+
#include "paimon/fs/file_system.h"
34+
#include "paimon/testing/utils/testharness.h"
35+
namespace paimon::lucene::test {
36+
class LuceneUtilsTest : public ::testing::Test {
37+
public:
38+
void SetUp() override {
39+
const char* dict_dir_env = std::getenv(kJiebaDictDirEnv);
40+
if (dict_dir_env != nullptr) {
41+
original_dict_dir_env_ = dict_dir_env;
42+
}
43+
ASSERT_OK_AND_ASSIGN(working_dir_, PathUtil::GetWorkingDirectory());
44+
ASSERT_OK_AND_ASSIGN(std::string dict_dir, LuceneUtils::GetJiebaDictionaryDir());
45+
default_dict_dir_ = std::filesystem::canonical(dict_dir);
46+
test_dir_ = paimon::test::UniqueTestDirectory::Create();
47+
ASSERT_TRUE(test_dir_);
48+
}
49+
50+
void TearDown() override {
51+
std::error_code ec;
52+
std::filesystem::current_path(working_dir_, ec);
53+
if (original_dict_dir_env_) {
54+
setenv(kJiebaDictDirEnv, original_dict_dir_env_->c_str(), /*overwrite=*/1);
55+
} else {
56+
unsetenv(kJiebaDictDirEnv);
57+
}
58+
}
59+
60+
std::string CopyDictionary(const std::string& relative_dir) const {
61+
std::string dir = PathUtil::JoinPath(test_dir_->Str(), relative_dir);
62+
EXPECT_TRUE(paimon::test::TestUtil::CopyDirectory(default_dict_dir_, dir));
63+
return dir;
64+
}
65+
66+
protected:
67+
std::optional<std::string> original_dict_dir_env_;
68+
std::string working_dir_;
69+
std::filesystem::path default_dict_dir_;
70+
std::unique_ptr<paimon::test::UniqueTestDirectory> test_dir_;
71+
};
72+
73+
TEST_F(LuceneUtilsTest, TestGetJiebaSharesOneSegmenter) {
74+
std::string dict_dir = CopyDictionary("dict");
75+
setenv(kJiebaDictDirEnv, dict_dir.c_str(), /*overwrite=*/1);
76+
77+
constexpr size_t kThreadNum = 4;
78+
std::vector<std::shared_ptr<const cppjieba::Jieba>> jiebas(kThreadNum);
79+
std::vector<std::thread> threads;
80+
threads.reserve(kThreadNum);
81+
for (size_t i = 0; i < kThreadNum; ++i) {
82+
threads.emplace_back([&jiebas, i]() {
83+
ASSERT_OK_AND_ASSIGN(jiebas[i], LuceneUtils::GetJieba());
84+
std::vector<std::string> words;
85+
jiebas[i]->CutForSearch("我爱机器学习", words);
86+
std::vector<std::string> expected = {"我", "爱", "机器", "学习"};
87+
ASSERT_EQ(words, expected);
88+
});
89+
}
90+
for (auto& thread : threads) {
91+
thread.join();
92+
}
93+
ASSERT_TRUE(jiebas[0]);
94+
for (const auto& jieba : jiebas) {
95+
ASSERT_EQ(jieba, jiebas[0]);
96+
}
97+
98+
std::weak_ptr<const cppjieba::Jieba> cached_jieba = jiebas[0];
99+
jiebas.clear();
100+
ASSERT_FALSE(cached_jieba.expired());
101+
ASSERT_OK_AND_ASSIGN(std::shared_ptr<const cppjieba::Jieba> jieba, LuceneUtils::GetJieba());
102+
ASSERT_EQ(jieba, cached_jieba.lock());
103+
}
104+
105+
TEST_F(LuceneUtilsTest, TestGetJiebaResolvesRelativeDictionaryDir) {
106+
std::string relative_dir = default_dict_dir_.filename().string();
107+
setenv(kJiebaDictDirEnv, relative_dir.c_str(), /*overwrite=*/1);
108+
std::filesystem::current_path(test_dir_->Str());
109+
ASSERT_NOK_WITH_MSG(LuceneUtils::GetJieba(),
110+
fmt::format("cannot resolve dictionary dir {} for jieba", relative_dir));
111+
112+
std::string user_dict = PathUtil::JoinPath(CopyDictionary(relative_dir), "user.dict.utf8");
113+
ASSERT_OK(test_dir_->GetFileSystem()->WriteFile(user_dict, "机器学习\n", /*overwrite=*/true));
114+
ASSERT_OK_AND_ASSIGN(std::shared_ptr<const cppjieba::Jieba> jieba_with_user_word,
115+
LuceneUtils::GetJieba());
116+
std::filesystem::current_path(default_dict_dir_.parent_path());
117+
ASSERT_OK_AND_ASSIGN(std::shared_ptr<const cppjieba::Jieba> default_jieba,
118+
LuceneUtils::GetJieba());
119+
ASSERT_NE(jieba_with_user_word, default_jieba);
120+
121+
std::vector<std::string> words;
122+
jieba_with_user_word->Cut("机器学习", words);
123+
ASSERT_EQ(words, std::vector<std::string>({"机器学习"}));
124+
default_jieba->Cut("机器学习", words);
125+
ASSERT_EQ(words, std::vector<std::string>({"机器", "学习"}));
126+
127+
setenv(kJiebaDictDirEnv, default_dict_dir_.c_str(), /*overwrite=*/1);
128+
ASSERT_OK_AND_ASSIGN(std::shared_ptr<const cppjieba::Jieba> jieba, LuceneUtils::GetJieba());
129+
ASSERT_EQ(jieba, default_jieba);
130+
}
131+
132+
} // namespace paimon::lucene::test

0 commit comments

Comments
 (0)