SteNicholas commented on code in PR #424:
URL: https://github.com/apache/paimon-cpp/pull/424#discussion_r4249559455


##########
src/paimon/global_index/lucene/lucene_global_index_reader.cpp:
##########
@@ -17,23 +17,444 @@
  */
 #include "paimon/global_index/lucene/lucene_global_index_reader.h"
 
+#include <algorithm>
+#include <cmath>
+#include <functional>
+#include <initializer_list>
+#include <limits>
+#include <utility>
+
 #include "arrow/c/bridge.h"
-#include "lucene++/DisjunctionMaxQuery.h"
 #include "lucene++/FileUtils.h"
 #include "paimon/common/utils/options_utils.h"
 #include "paimon/common/utils/path_util.h"
 #include "paimon/common/utils/rapidjson_util.h"
-#include "paimon/global_index/bitmap_global_index_result.h"
+#include "paimon/common/utils/string_utils.h"
 #include "paimon/global_index/bitmap_scored_global_index_result.h"
 #include "paimon/global_index/lucene/jieba_analyzer.h"
-#include "paimon/global_index/lucene/lucene_collector.h"
 #include "paimon/global_index/lucene/lucene_defs.h"
 #include "paimon/global_index/lucene/lucene_directory.h"
 #include "paimon/global_index/lucene/lucene_filter.h"
 #include "paimon/global_index/lucene/lucene_utils.h"
 #include "paimon/io/data_input_stream.h"
+#include "rapidjson/document.h"
+#include "rapidjson/error/en.h"
 
 namespace paimon::lucene {
+namespace {
+
+std::string JsonString(const rapidjson::Value& value) {
+    return std::string(value.GetString(), value.GetStringLength());
+}
+
+/// Trims Unicode White_Space as Rust `str::trim` does in the native DSL.
+std::wstring TrimDslWhitespace(const std::wstring& value) {
+    constexpr wchar_t kWhitespace[] =
+        L"\t\n\v\f\r \u0085\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005"
+        L"\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000";
+    size_t first = value.find_first_not_of(kWhitespace);
+    if (first == std::wstring::npos) {
+        return L"";
+    }
+    size_t last = value.find_last_not_of(kWhitespace);
+    return value.substr(first, last - first + 1);
+}
+
+bool TrimmedIsOneOf(const std::string& value, std::initializer_list<const 
wchar_t*> candidates) {
+    std::wstring trimmed = 
TrimDslWhitespace(LuceneUtils::StringToWstring(value));
+    for (const wchar_t* candidate : candidates) {
+        if (trimmed == candidate) {
+            return true;
+        }
+    }
+    return false;
+}
+
+/// Translates the JSON DSL query of `FullTextSearch` into a Lucene query on 
the indexed field.
+///
+/// The DSL is defined by apache/paimon-full-text `core/src/query.rs`. The 
supported queries use
+/// its field names, aliases and defaults. Duplicate known fields are 
rejected, and unknown fields
+/// are ignored as in the native engine.
+/// `match`, `multi_match`, `match_phrase` and `boolean` queries are 
translated into term, boolean
+/// and phrase queries; `boost` queries and fuzzy matching are rejected.
+class DslQueryTranslator {
+ public:
+    /// Splits text into index terms with the analyzer used at indexing time.
+    using Analyzer = std::function<std::vector<std::wstring>(const 
std::string&)>;
+
+    DslQueryTranslator(const std::wstring& wfield_name, Analyzer analyzer)
+        : wfield_name_(wfield_name), analyzer_(std::move(analyzer)) {}
+
+    Result<Lucene::QueryPtr> Translate(const std::string& query) const 
noexcept(false) {
+        // RapidJSON treats a NUL byte as the end of input even when a length 
is provided.
+        if (query.find('\0') != std::string::npos) {
+            return Status::Invalid("lucene full-text query must not contain 
NUL characters");
+        }
+        rapidjson::Document document;
+        document.Parse(query.data(), query.size());

Review Comment:
   Thanks, addressed in 90d936df:
   
   - `Translate` now parses with `rapidjson::kParseIterativeFlag`, so parsing 
no longer recurses, and the document is freed by its memory pool without 
recursion.
   - `DslQueryTranslator` tracks the nesting depth of the JSON arrays and 
objects it translates and returns an `Invalid` status ("full-text query exceeds 
the maximum nesting depth of 127") before recursing past 127 levels. This 
matches serde_json's default recursion limit in paimon-full-text, so both 
backends accept the same queries. As in serde_json, unknown fields are skipped 
without counting toward the limit.
   - `TestNestedQuery` in `lucene_global_index_test.cpp` covers 41 levels of 
`{"boolean":{"must":[...]}}` (accepted), 42 levels and mixed `must`/`queries` 
nesting just past the limit (rejected), 100000 levels (rejected with a status), 
and an unknown field nested 100000 levels (ignored).
   



##########
src/paimon/global_index/full_text/full_text_global_index_test.cpp:
##########
@@ -395,6 +391,10 @@ TEST_F(FullTextGlobalIndexTest, TestWriteAndSearch) {
     ASSERT_OK_AND_ASSIGN(auto result,
                          
reader->VisitFullTextSearch(MakeSearch(MatchQuery("THE LAKE"))));
     CheckRowIds(result, {0, 2});
+    ASSERT_OK_AND_ASSIGN(
+        result, reader->VisitFullTextSearch(
+                    MakeSearch(R"({"multi_match":{"query":"paimon 
lake","columns":["text"]}})")));

Review Comment:
   Thanks, added in 90d936df:
   
   - `full-text`: `TestNestedBooleanAndBoost` in 
`full_text_global_index_test.cpp` checks `paimon AND (streaming OR batch)` → 
rows {0, 1}, and `paimon AND NOT (streaming OR batch)` in the `queries` form → 
row {2}. It also runs `{"match":{"query":"lake","boost":1.0}}` and the same 
query with `"boost":2.0`, asserting that both return rows {0, 1, 2, 3} and that 
each boosted score is 2× the unboosted one.
   - `lucene-fts`: `TestNestedQuery` in `lucene_global_index_test.cpp` runs the 
same two nested `boolean` queries on the same rows with the same expected row 
ids.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to