From 04acd3d6b9a95ff49d430eaf4f65aabc25282d88 Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Fri, 22 May 2026 10:56:29 +0300 Subject: Engine: Add setVersion() with strict validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Engine accepts only versions 7 and above (covering ES 7.x, 8.x, and OpenSearch 1.x/2.x/3.x via the typeless API). Anything below 7 raises with a clear message pointing at the bundled engine for ES 5 users. 🤖 Generated by [robots](https://vyos.io) --- ...SElasticModernFulltextStorageEngineTestCase.php | 38 ++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php (limited to 'src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php') diff --git a/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php b/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php new file mode 100644 index 0000000..7a41ca1 --- /dev/null +++ b/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php @@ -0,0 +1,38 @@ +setVersion($v); + } catch (Exception $e) { + $caught = $e; + } + $this->assertEqual( + null, + $caught, + pht('Expected no exception for version=%d.', $v)); + $this->assertEqual($v, $engine->getVersion()); + } + } + + public function testSetVersionRejectsBelowSeven() { + foreach (array(0, 1, 2, 5, 6) as $v) { + $engine = new VyOSElasticModernFulltextStorageEngine(); + $caught = null; + try { + $engine->setVersion($v); + } catch (Exception $e) { + $caught = $e; + } + $this->assertTrue( + $caught !== null, + pht('Expected an exception for version=%d.', $v)); + } + } + +} -- cgit v1.2.3 From ed0dee0333acf8a1c79845bba6455506c8210bad Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Sat, 23 May 2026 13:13:22 +0300 Subject: E1 fixup: initialize $version to null; getVersion() throws if unset; use TestDouble in tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated by [robots](https://vyos.io) --- src/__phutil_library_map__.php | 2 ++ ...SElasticModernFulltextStorageEngineTestCase.php | 35 ++++++++++++++++++++-- .../VyOSElasticModernFulltextStorageEngine.php | 6 +++- 3 files changed, 40 insertions(+), 3 deletions(-) (limited to 'src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php') diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index d365d58..291fb03 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -11,6 +11,7 @@ phutil_register_library_map(array( 'class' => array( 'VyOSElasticModernFulltextStorageEngine' => 'engine/VyOSElasticModernFulltextStorageEngine.php', 'VyOSElasticModernFulltextStorageEngineTestCase' => '__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php', + 'VyOSElasticModernFulltextStorageEngineTestDouble' => '__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php', 'VyOSElasticModernHost' => 'host/VyOSElasticModernHost.php', 'VyOSElasticModernHostTestCase' => '__tests__/VyOSElasticModernHostTestCase.php', ), @@ -18,6 +19,7 @@ phutil_register_library_map(array( 'xmap' => array( 'VyOSElasticModernFulltextStorageEngine' => 'PhabricatorFulltextStorageEngine', 'VyOSElasticModernFulltextStorageEngineTestCase' => 'PhutilTestCase', + 'VyOSElasticModernFulltextStorageEngineTestDouble' => 'VyOSElasticModernFulltextStorageEngine', 'VyOSElasticModernHost' => 'PhabricatorSearchHost', 'VyOSElasticModernHostTestCase' => 'PhutilTestCase', ), diff --git a/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php b/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php index 7a41ca1..7c0755c 100644 --- a/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php +++ b/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php @@ -1,11 +1,42 @@ newEngine(); $caught = null; try { $engine->setVersion($v); @@ -22,7 +53,7 @@ final class VyOSElasticModernFulltextStorageEngineTestCase public function testSetVersionRejectsBelowSeven() { foreach (array(0, 1, 2, 5, 6) as $v) { - $engine = new VyOSElasticModernFulltextStorageEngine(); + $engine = $this->newEngine(); $caught = null; try { $engine->setVersion($v); diff --git a/src/engine/VyOSElasticModernFulltextStorageEngine.php b/src/engine/VyOSElasticModernFulltextStorageEngine.php index 60683b1..2570311 100644 --- a/src/engine/VyOSElasticModernFulltextStorageEngine.php +++ b/src/engine/VyOSElasticModernFulltextStorageEngine.php @@ -7,7 +7,7 @@ abstract class VyOSElasticModernFulltextStorageEngine extends PhabricatorFulltextStorageEngine { - private $version; + private $version = null; public function setVersion($version) { $version = (int)$version; @@ -25,6 +25,10 @@ abstract class VyOSElasticModernFulltextStorageEngine } public function getVersion() { + if ($this->version === null) { + throw new Exception( + pht('Version not configured; call setVersion() or setService() first.')); + } return $this->version; } -- cgit v1.2.3 From 249feeb0a6d43b8f98f1e4d48e5cc3f6edc19998 Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Fri, 22 May 2026 10:58:20 +0300 Subject: Engine: Add getDocumentUri() helper for typeless document URLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated by [robots](https://vyos.io) --- .../VyOSElasticModernFulltextStorageEngineTestCase.php | 16 ++++++++++++++++ src/engine/VyOSElasticModernFulltextStorageEngine.php | 4 ++++ 2 files changed, 20 insertions(+) (limited to 'src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php') diff --git a/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php b/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php index 7c0755c..5ced477 100644 --- a/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php +++ b/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php @@ -66,4 +66,20 @@ final class VyOSElasticModernFulltextStorageEngineTestCase } } + public function testGetDocumentUri() { + $engine = id(new VyOSElasticModernFulltextStorageEngine()) + ->setVersion(7); + $uri = $engine->getDocumentUri('TASK', 'PHID-TASK-abc'); + $this->assertEqual('/_doc/PHID-TASK-abc', $uri); + } + + public function testGetDocumentUriIgnoresType() { + // The typeless API does not encode the doc type in the URL. + $engine = id(new VyOSElasticModernFulltextStorageEngine()) + ->setVersion(7); + $uri_a = $engine->getDocumentUri('TASK', 'PHID-TASK-abc'); + $uri_b = $engine->getDocumentUri('DREV', 'PHID-TASK-abc'); + $this->assertEqual($uri_a, $uri_b); + } + } diff --git a/src/engine/VyOSElasticModernFulltextStorageEngine.php b/src/engine/VyOSElasticModernFulltextStorageEngine.php index 2570311..cf9278b 100644 --- a/src/engine/VyOSElasticModernFulltextStorageEngine.php +++ b/src/engine/VyOSElasticModernFulltextStorageEngine.php @@ -32,6 +32,10 @@ abstract class VyOSElasticModernFulltextStorageEngine return $this->version; } + public function getDocumentUri($type, $phid) { + return '/_doc/'.$phid; + } + public function getEngineIdentifier() { return 'elasticsearch-modern'; } -- cgit v1.2.3 From daa340590037e7a3d87f123078cc450dcbcfbca9 Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Sat, 23 May 2026 13:18:06 +0300 Subject: E2 fixup: use newEngine() helper in testGetDocumentUri tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated by [robots](https://vyos.io) --- src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php') diff --git a/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php b/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php index 5ced477..3a96bac 100644 --- a/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php +++ b/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php @@ -67,16 +67,14 @@ final class VyOSElasticModernFulltextStorageEngineTestCase } public function testGetDocumentUri() { - $engine = id(new VyOSElasticModernFulltextStorageEngine()) - ->setVersion(7); + $engine = $this->newEngine()->setVersion(7); $uri = $engine->getDocumentUri('TASK', 'PHID-TASK-abc'); $this->assertEqual('/_doc/PHID-TASK-abc', $uri); } public function testGetDocumentUriIgnoresType() { // The typeless API does not encode the doc type in the URL. - $engine = id(new VyOSElasticModernFulltextStorageEngine()) - ->setVersion(7); + $engine = $this->newEngine()->setVersion(7); $uri_a = $engine->getDocumentUri('TASK', 'PHID-TASK-abc'); $uri_b = $engine->getDocumentUri('DREV', 'PHID-TASK-abc'); $this->assertEqual($uri_a, $uri_b); -- cgit v1.2.3 From 4a2e5781883f8963c36ee129cf76c43e887ac4dc Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Fri, 22 May 2026 11:00:04 +0300 Subject: Engine: Add getSearchUri() helper (typeless search endpoint) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated by [robots](https://vyos.io) --- .../VyOSElasticModernFulltextStorageEngineTestCase.php | 10 ++++++++++ src/engine/VyOSElasticModernFulltextStorageEngine.php | 4 ++++ 2 files changed, 14 insertions(+) (limited to 'src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php') diff --git a/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php b/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php index 3a96bac..e3cbef5 100644 --- a/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php +++ b/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php @@ -80,4 +80,14 @@ final class VyOSElasticModernFulltextStorageEngineTestCase $this->assertEqual($uri_a, $uri_b); } + public function testGetSearchUri() { + $engine = id(new VyOSElasticModernFulltextStorageEngine()) + ->setVersion(7); + $this->assertEqual( + '/_search', + $engine->getSearchUri(array('TASK', 'DREV'))); + $this->assertEqual('/_search', $engine->getSearchUri(array('USER'))); + $this->assertEqual('/_search', $engine->getSearchUri(array())); + } + } diff --git a/src/engine/VyOSElasticModernFulltextStorageEngine.php b/src/engine/VyOSElasticModernFulltextStorageEngine.php index cf9278b..e21021f 100644 --- a/src/engine/VyOSElasticModernFulltextStorageEngine.php +++ b/src/engine/VyOSElasticModernFulltextStorageEngine.php @@ -36,6 +36,10 @@ abstract class VyOSElasticModernFulltextStorageEngine return '/_doc/'.$phid; } + public function getSearchUri(array $types) { + return '/_search'; + } + public function getEngineIdentifier() { return 'elasticsearch-modern'; } -- cgit v1.2.3 From 13ca7a3fdb812467b57c6c4d33e13f69cf3cdcc3 Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Sat, 23 May 2026 13:21:09 +0300 Subject: E3 fixup: use newEngine() helper in testGetSearchUri MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated by [robots](https://vyos.io) --- src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php') diff --git a/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php b/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php index e3cbef5..1762d32 100644 --- a/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php +++ b/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php @@ -81,8 +81,7 @@ final class VyOSElasticModernFulltextStorageEngineTestCase } public function testGetSearchUri() { - $engine = id(new VyOSElasticModernFulltextStorageEngine()) - ->setVersion(7); + $engine = $this->newEngine()->setVersion(7); $this->assertEqual( '/_search', $engine->getSearchUri(array('TASK', 'DREV'))); -- cgit v1.2.3 From a529a40ec8569a7010df4965c4e7fff9e81eb125 Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Fri, 22 May 2026 11:38:59 +0300 Subject: Engine: Add buildTypeFilter() helper (body-level documentType filter) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated by [robots](https://vyos.io) --- ...SElasticModernFulltextStorageEngineTestCase.php | 25 ++++++++++++++++++++++ .../VyOSElasticModernFulltextStorageEngine.php | 8 +++++++ 2 files changed, 33 insertions(+) (limited to 'src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php') diff --git a/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php b/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php index 1762d32..95558a1 100644 --- a/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php +++ b/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php @@ -89,4 +89,29 @@ final class VyOSElasticModernFulltextStorageEngineTestCase $this->assertEqual('/_search', $engine->getSearchUri(array())); } + public function testBuildTypeFilter() { + $engine = id(new VyOSElasticModernFulltextStorageEngine()) + ->setVersion(7); + $filter = $engine->buildTypeFilter(array('TASK', 'DREV')); + $expected = array( + 'terms' => array( + 'documentType' => array('TASK', 'DREV'), + ), + ); + $this->assertEqual($expected, $filter); + } + + public function testBuildTypeFilterEmpty() { + // Empty list still produces a filter; the caller is responsible + // for normalizing "no types specified" to "all indexable types" + // before calling this method. This matches the bundled engine's + // pattern (executeSearch() normalizes before URL construction). + $engine = id(new VyOSElasticModernFulltextStorageEngine()) + ->setVersion(7); + $filter = $engine->buildTypeFilter(array()); + $this->assertEqual( + array('terms' => array('documentType' => array())), + $filter); + } + } diff --git a/src/engine/VyOSElasticModernFulltextStorageEngine.php b/src/engine/VyOSElasticModernFulltextStorageEngine.php index e21021f..316a02b 100644 --- a/src/engine/VyOSElasticModernFulltextStorageEngine.php +++ b/src/engine/VyOSElasticModernFulltextStorageEngine.php @@ -40,6 +40,14 @@ abstract class VyOSElasticModernFulltextStorageEngine return '/_search'; } + public function buildTypeFilter(array $types) { + return array( + 'terms' => array( + 'documentType' => array_values($types), + ), + ); + } + public function getEngineIdentifier() { return 'elasticsearch-modern'; } -- cgit v1.2.3 From 67d619a782eef8df6437525c45a63c9485c13a6a Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Sat, 23 May 2026 13:23:14 +0300 Subject: E4 fixup: use newEngine() helper in testBuildTypeFilter tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated by [robots](https://vyos.io) --- src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php') diff --git a/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php b/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php index 95558a1..ff4ade8 100644 --- a/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php +++ b/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php @@ -90,8 +90,7 @@ final class VyOSElasticModernFulltextStorageEngineTestCase } public function testBuildTypeFilter() { - $engine = id(new VyOSElasticModernFulltextStorageEngine()) - ->setVersion(7); + $engine = $this->newEngine()->setVersion(7); $filter = $engine->buildTypeFilter(array('TASK', 'DREV')); $expected = array( 'terms' => array( @@ -106,8 +105,7 @@ final class VyOSElasticModernFulltextStorageEngineTestCase // for normalizing "no types specified" to "all indexable types" // before calling this method. This matches the bundled engine's // pattern (executeSearch() normalizes before URL construction). - $engine = id(new VyOSElasticModernFulltextStorageEngine()) - ->setVersion(7); + $engine = $this->newEngine()->setVersion(7); $filter = $engine->buildTypeFilter(array()); $this->assertEqual( array('terms' => array('documentType' => array())), -- cgit v1.2.3 From fc0e71e28e49d9cdc84fadfda40692bdfc415a6c Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Fri, 22 May 2026 11:40:40 +0300 Subject: Engine: Add buildIndexMappings() helper (single typeless mapping) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single 'properties' block, documentType as a keyword field inside it, no include_in_all anywhere (the field was removed in ES 6). Relationships emit as keyword fields with doc_values: false, matching the bundled engine's ES-5-mode behavior. Field-data multi-analyzer shape (raw, keywords, stems) preserved. 🤖 Generated by [robots](https://vyos.io) --- ...SElasticModernFulltextStorageEngineTestCase.php | 54 ++++++++++++++++++++++ .../VyOSElasticModernFulltextStorageEngine.php | 47 +++++++++++++++++++ 2 files changed, 101 insertions(+) (limited to 'src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php') diff --git a/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php b/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php index ff4ade8..ab6f5c6 100644 --- a/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php +++ b/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php @@ -112,4 +112,58 @@ final class VyOSElasticModernFulltextStorageEngineTestCase $filter); } + public function testBuildIndexMappingsShape() { + $engine = id(new VyOSElasticModernFulltextStorageEngine()) + ->setVersion(7); + + $doc_types = array('TASK', 'DREV'); + $fields = array('title', 'body', 'comment'); + $relationships = array('authorPHID', 'projectPHID'); + $mappings = $engine->buildIndexMappings( + $doc_types, $fields, $relationships, 'text'); + + // Single typeless mapping with one 'properties' block. + $this->assertTrue(isset($mappings['properties'])); + $this->assertFalse(isset($mappings['TASK'])); + $this->assertFalse(isset($mappings['DREV'])); + + // Field properties exist with the multi-analyzer shape. + $this->assertTrue(isset($mappings['properties']['title'])); + $this->assertEqual('text', $mappings['properties']['title']['type']); + $this->assertTrue( + isset($mappings['properties']['title']['fields']['raw'])); + $this->assertTrue( + isset($mappings['properties']['title']['fields']['keywords'])); + $this->assertTrue( + isset($mappings['properties']['title']['fields']['stems'])); + + // Relationships emit as keyword fields with doc_values:false. + $this->assertEqual( + 'keyword', + $mappings['properties']['authorPHID']['type']); + $this->assertEqual( + false, + $mappings['properties']['authorPHID']['doc_values']); + $this->assertEqual( + 'date', + $mappings['properties']['authorPHID_ts']['type']); + + // No include_in_all anywhere. + $this->assertFalse( + isset($mappings['properties']['authorPHID']['include_in_all'])); + + // documentType is a keyword field inside properties. + $this->assertEqual( + 'keyword', + $mappings['properties']['documentType']['type']); + + // Standard date fields present. + $this->assertEqual( + 'date', + $mappings['properties']['dateCreated']['type']); + $this->assertEqual( + 'date', + $mappings['properties']['lastModified']['type']); + } + } diff --git a/src/engine/VyOSElasticModernFulltextStorageEngine.php b/src/engine/VyOSElasticModernFulltextStorageEngine.php index 316a02b..16c519c 100644 --- a/src/engine/VyOSElasticModernFulltextStorageEngine.php +++ b/src/engine/VyOSElasticModernFulltextStorageEngine.php @@ -48,6 +48,53 @@ abstract class VyOSElasticModernFulltextStorageEngine ); } + public function buildIndexMappings( + array $doc_types, array $fields, array $relationships, $text_type) { + + $properties = array(); + + foreach ($fields as $field) { + $properties[$field] = array( + 'type' => $text_type, + 'fields' => array( + 'raw' => array( + 'type' => $text_type, + 'analyzer' => 'english_exact', + 'search_analyzer' => 'english', + 'search_quote_analyzer' => 'english_exact', + ), + 'keywords' => array( + 'type' => $text_type, + 'analyzer' => 'letter_stop', + ), + 'stems' => array( + 'type' => $text_type, + 'analyzer' => 'english_stem', + ), + ), + ); + } + + foreach ($relationships as $rel) { + $properties[$rel] = array( + 'type' => 'keyword', + 'doc_values' => false, + ); + $properties[$rel.'_ts'] = array( + 'type' => 'date', + ); + } + + $properties['documentType'] = array('type' => 'keyword'); + $properties['dateCreated'] = array('type' => 'date'); + $properties['lastModified'] = array('type' => 'date'); + + // The $doc_types parameter is part of the signature for symmetry + // with the bundled engine's per-type loop, but the typeless API + // emits one mapping shared across all doc types. + return array('properties' => $properties); + } + public function getEngineIdentifier() { return 'elasticsearch-modern'; } -- cgit v1.2.3 From 9259fbf741f4a080285fbdd8d8586681558c0ab6 Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Sat, 23 May 2026 13:25:30 +0300 Subject: E5 fixup: expand testBuildIndexMappingsShape coverage to body/comment/projectPHID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated by [robots](https://vyos.io) --- ...SElasticModernFulltextStorageEngineTestCase.php | 64 +++++++++++++--------- 1 file changed, 39 insertions(+), 25 deletions(-) (limited to 'src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php') diff --git a/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php b/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php index ab6f5c6..979c419 100644 --- a/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php +++ b/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php @@ -113,8 +113,7 @@ final class VyOSElasticModernFulltextStorageEngineTestCase } public function testBuildIndexMappingsShape() { - $engine = id(new VyOSElasticModernFulltextStorageEngine()) - ->setVersion(7); + $engine = $this->newEngine()->setVersion(7); $doc_types = array('TASK', 'DREV'); $fields = array('title', 'body', 'comment'); @@ -127,30 +126,45 @@ final class VyOSElasticModernFulltextStorageEngineTestCase $this->assertFalse(isset($mappings['TASK'])); $this->assertFalse(isset($mappings['DREV'])); - // Field properties exist with the multi-analyzer shape. - $this->assertTrue(isset($mappings['properties']['title'])); - $this->assertEqual('text', $mappings['properties']['title']['type']); - $this->assertTrue( - isset($mappings['properties']['title']['fields']['raw'])); - $this->assertTrue( - isset($mappings['properties']['title']['fields']['keywords'])); - $this->assertTrue( - isset($mappings['properties']['title']['fields']['stems'])); - - // Relationships emit as keyword fields with doc_values:false. - $this->assertEqual( - 'keyword', - $mappings['properties']['authorPHID']['type']); - $this->assertEqual( - false, - $mappings['properties']['authorPHID']['doc_values']); - $this->assertEqual( - 'date', - $mappings['properties']['authorPHID_ts']['type']); + // All three text fields have the multi-analyzer shape. + foreach (array('title', 'body', 'comment') as $field) { + $this->assertTrue( + isset($mappings['properties'][$field]), + pht('Field "%s" missing from mappings.', $field)); + $this->assertEqual( + 'text', + $mappings['properties'][$field]['type'], + pht('Field "%s" should be type text.', $field)); + $this->assertTrue( + isset($mappings['properties'][$field]['fields']['raw']), + pht('Field "%s" missing raw sub-field.', $field)); + $this->assertTrue( + isset($mappings['properties'][$field]['fields']['keywords']), + pht('Field "%s" missing keywords sub-field.', $field)); + $this->assertTrue( + isset($mappings['properties'][$field]['fields']['stems']), + pht('Field "%s" missing stems sub-field.', $field)); + } - // No include_in_all anywhere. - $this->assertFalse( - isset($mappings['properties']['authorPHID']['include_in_all'])); + // Both relationships emit as keyword fields with doc_values:false. + foreach (array('authorPHID', 'projectPHID') as $rel) { + $this->assertEqual( + 'keyword', + $mappings['properties'][$rel]['type'], + pht('Relationship "%s" should be keyword type.', $rel)); + $this->assertEqual( + false, + $mappings['properties'][$rel]['doc_values'], + pht('Relationship "%s" should have doc_values:false.', $rel)); + $this->assertEqual( + 'date', + $mappings['properties'][$rel.'_ts']['type'], + pht('Relationship "%s" missing timestamp field.', $rel)); + // No include_in_all anywhere. + $this->assertFalse( + isset($mappings['properties'][$rel]['include_in_all']), + pht('Relationship "%s" should not have include_in_all.', $rel)); + } // documentType is a keyword field inside properties. $this->assertEqual( -- cgit v1.2.3 From de83584f8b782bde516d6b39f9f1bc4214585058 Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Sat, 23 May 2026 13:31:46 +0300 Subject: E5 fixup: detect intra-key duplicates in buildIndexMappings(); add collision tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated by [robots](https://vyos.io) --- ...SElasticModernFulltextStorageEngineTestCase.php | 56 ++++++++++++++++++++++ .../VyOSElasticModernFulltextStorageEngine.php | 21 ++++++-- 2 files changed, 73 insertions(+), 4 deletions(-) (limited to 'src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php') diff --git a/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php b/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php index 979c419..e3daab4 100644 --- a/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php +++ b/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php @@ -180,4 +180,60 @@ final class VyOSElasticModernFulltextStorageEngineTestCase $mappings['properties']['lastModified']['type']); } + public function testBuildIndexMappingsRejectsReservedFieldName() { + $engine = $this->newEngine()->setVersion(7); + $caught = null; + try { + $engine->buildIndexMappings( + array(), array('documentType'), array(), 'text'); + } catch (Exception $e) { + $caught = $e; + } + $this->assertTrue( + $caught !== null, + pht('Expected exception when field name collides with reserved key.')); + } + + public function testBuildIndexMappingsRejectsReservedRelationshipName() { + $engine = $this->newEngine()->setVersion(7); + $caught = null; + try { + $engine->buildIndexMappings( + array(), array(), array('lastModified'), 'text'); + } catch (Exception $e) { + $caught = $e; + } + $this->assertTrue( + $caught !== null, + pht('Expected exception when relationship name collides with reserved key.')); + } + + public function testBuildIndexMappingsRejectsFooTsClash() { + // A field named "foo_ts" would clash with the timestamp slot auto-generated + // for a relationship named "foo". + $engine = $this->newEngine()->setVersion(7); + $caught = null; + try { + $engine->buildIndexMappings( + array(), array('foo_ts'), array('foo'), 'text'); + } catch (Exception $e) { + $caught = $e; + } + $this->assertTrue( + $caught !== null, + pht('Expected exception for field/relationship timestamp-slot collision.')); + } + + public function testBuildIndexMappingsEmptyInputsYieldStandardFields() { + $engine = $this->newEngine()->setVersion(7); + $mappings = $engine->buildIndexMappings(array(), array(), array(), 'text'); + $this->assertTrue(isset($mappings['properties']['documentType'])); + $this->assertTrue(isset($mappings['properties']['dateCreated'])); + $this->assertTrue(isset($mappings['properties']['lastModified'])); + $this->assertEqual( + 'keyword', $mappings['properties']['documentType']['type']); + $this->assertEqual('date', $mappings['properties']['dateCreated']['type']); + $this->assertEqual('date', $mappings['properties']['lastModified']['type']); + } + } diff --git a/src/engine/VyOSElasticModernFulltextStorageEngine.php b/src/engine/VyOSElasticModernFulltextStorageEngine.php index f798e4b..fe88adc 100644 --- a/src/engine/VyOSElasticModernFulltextStorageEngine.php +++ b/src/engine/VyOSElasticModernFulltextStorageEngine.php @@ -55,10 +55,10 @@ abstract class VyOSElasticModernFulltextStorageEngine // Caller-supplied $fields or $relationships must not shadow them. static $reserved = array('documentType', 'dateCreated', 'lastModified'); - $all_caller_keys = array_merge( - $fields, - $relationships, - array_map(function($r) { return $r.'_ts'; }, $relationships)); + $rel_ts_keys = array_map(function($r) { return $r.'_ts'; }, $relationships); + $all_caller_keys = array_merge($fields, $relationships, $rel_ts_keys); + + // Check for caller-supplied names that shadow reserved fields. $collisions = array_intersect($all_caller_keys, $reserved); if ($collisions) { throw new Exception( @@ -68,6 +68,19 @@ abstract class VyOSElasticModernFulltextStorageEngine implode('", "', array_values($collisions)))); } + // Check for duplicates within caller-supplied keys themselves + // (e.g. a field named "foo_ts" that would clash with relationship "foo"'s + // implicit timestamp slot). + $counts = array_count_values($all_caller_keys); + $duplicates = array_keys(array_filter($counts, function($c) { return $c > 1; })); + if ($duplicates) { + throw new Exception( + pht( + 'buildIndexMappings(): caller-supplied key(s) "%s" appear more '. + 'than once (check for field/relationship/timestamp-slot collisions).', + implode('", "', $duplicates))); + } + $properties = array(); foreach ($fields as $field) { -- cgit v1.2.3 From f945e302f79086c6ea99bd45d414fe6bccefb9d4 Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Sat, 23 May 2026 13:44:01 +0300 Subject: E6 fixup: use trim() + empty-string guard in setService(); use newEngine() in tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - str_replace('/', '', \$index) → trim(\$index, '/') to preserve internal slashes in multi-segment index paths (e.g. 'phabricator/prod' must stay intact; only leading/trailing slashes are stripped). - Add empty-string guard: after trim, throw a clear exception if the result is '' so misconfigured paths fail loudly at setService() rather than silently at query time with a confusing endpoint URL. - Replace direct new VyOSElasticModernFulltextStorageEngine() in the four new test methods with \$this->newEngine() to avoid PHP fatal on abstract class instantiation. 🤖 Generated by [robots](https://vyos.io) --- ...SElasticModernFulltextStorageEngineTestCase.php | 21 ++++++++++++ .../VyOSElasticModernFulltextStorageEngine.php | 40 ++++++++++++++++++++++ 2 files changed, 61 insertions(+) (limited to 'src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php') diff --git a/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php b/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php index e3daab4..9f08f8c 100644 --- a/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php +++ b/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php @@ -236,4 +236,25 @@ final class VyOSElasticModernFulltextStorageEngineTestCase $this->assertEqual('date', $mappings['properties']['lastModified']['type']); } + public function testEngineIdentifier() { + $engine = $this->newEngine(); + $this->assertEqual('elasticsearch-modern', $engine->getEngineIdentifier()); + } + + public function testHostType() { + $engine = $this->newEngine(); + $host = $engine->getHostType(); + $this->assertTrue($host instanceof VyOSElasticModernHost); + } + + public function testGetTextFieldType() { + $engine = $this->newEngine()->setVersion(7); + $this->assertEqual('text', $engine->getTextFieldType()); + } + + public function testGetTimestampField() { + $engine = $this->newEngine()->setVersion(7); + $this->assertEqual('lastModified', $engine->getTimestampField()); + } + } diff --git a/src/engine/VyOSElasticModernFulltextStorageEngine.php b/src/engine/VyOSElasticModernFulltextStorageEngine.php index fe88adc..42e2fd6 100644 --- a/src/engine/VyOSElasticModernFulltextStorageEngine.php +++ b/src/engine/VyOSElasticModernFulltextStorageEngine.php @@ -8,6 +8,46 @@ abstract class VyOSElasticModernFulltextStorageEngine extends PhabricatorFulltextStorageEngine { private $version = null; + private $index; + + public function setService(PhabricatorSearchService $service) { + $this->service = $service; // inherited protected property + $config = $service->getConfig(); + $index = idx($config, 'path', '/phabricator'); + $normalized = trim($index, '/'); + if ($normalized === '') { + throw new Exception( + pht( + 'Invalid index path "%s" in cluster.search config: '. + 'path must contain at least one non-slash character.', + $index)); + } + $this->index = $normalized; + $this->setVersion(idx($config, 'version', 7)); + return $this; + } + + public function getTimestampField() { + return 'lastModified'; + } + + public function getTextFieldType() { + return 'text'; + } + + public function getHostForRead() { + return $this->getService()->getAnyHostForRole('read'); + } + + public function getHostForWrite() { + return $this->getService()->getAnyHostForRole('write'); + } + + public function getTypeConstants($class) { + $relationship_class = new ReflectionClass($class); + $typeconstants = $relationship_class->getConstants(); + return array_unique(array_values($typeconstants)); + } public function setVersion($version) { $version = (int)$version; -- cgit v1.2.3 From 4d836601cb2f602c0089134eda1604f605fb39da Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Sat, 23 May 2026 13:54:24 +0300 Subject: E8 fixup: four correctness fixes from Phase 0 review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. strlen(\$query_string) → null-safe check \$query->getParameter('query') may return null; strlen(null) is deprecated in PHP 8.1+. Switch to explicit !== null && !== '' check. 2. ids.values nesting fix in buildSpec() 'values' => array(\$exclude) wraps an already-array \$exclude in a nested array, producing invalid Elasticsearch ids syntax. Cast to (array) then re-index with array_values() so both scalar PHID and array-of-PHIDs produce a flat list. 3. initIndex() host consistency: pass write host to indexExists() initIndex() called indexExists() without a host argument, which fell back to the read host, then deleted via getHostForWrite(). A read/write split lag window could cause false-positive "index doesn't exist" results. Pass the already-resolved write host. 4. getIndexStats() unchecked array access \$res['indices'][\$this->index] was accessed without verifying the key exists. If the index was just deleted or the name drifted, this throws an undefined-offset PHP notice. Add an explicit isset() guard with a clear exception message naming the index. 🤖 Generated by [robots](https://vyos.io) --- ...yOSElasticModernFulltextStorageEngineTestCase.php | 20 ++++++++++++++++++++ .../VyOSElasticModernFulltextStorageEngine.php | 14 +++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) (limited to 'src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php') diff --git a/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php b/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php index 9f08f8c..8a8b29c 100644 --- a/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php +++ b/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php @@ -257,4 +257,24 @@ final class VyOSElasticModernFulltextStorageEngineTestCase $this->assertEqual('lastModified', $engine->getTimestampField()); } + public function testBuildSpecTreatsZeroQueryAsSearchTerm() { + // '0' is falsy in PHP but must be treated as a non-empty search term. + // strlen('0') == 1, so the query clause should be present and the + // default date-sorted path should NOT fire. + $engine = $this->newEngine()->setVersion(7); + $query = id(new PhabricatorSavedQuery()) + ->setParameter('query', '0'); + + $method = new ReflectionMethod($engine, 'buildSpec'); + $method->setAccessible(true); + $spec = $method->invoke($engine, $query, array('TASK')); + + $this->assertFalse(isset($spec['sort'])); + $this->assertEqual( + '0', + idxv( + $spec, + array('query', 'bool', 'must', 0, 'simple_query_string', 'query'))); + } + } diff --git a/src/engine/VyOSElasticModernFulltextStorageEngine.php b/src/engine/VyOSElasticModernFulltextStorageEngine.php index c49ff5e..70844f9 100644 --- a/src/engine/VyOSElasticModernFulltextStorageEngine.php +++ b/src/engine/VyOSElasticModernFulltextStorageEngine.php @@ -313,7 +313,7 @@ abstract class VyOSElasticModernFulltextStorageEngine private function buildSpec(PhabricatorSavedQuery $query, array $types) { $q = new PhabricatorElasticsearchQueryBuilder(); $query_string = $query->getParameter('query'); - if (strlen($query_string)) { + if ($query_string !== null && $query_string !== '') { $q->addMustClause(array( 'simple_query_string' => array( 'query' => $query_string, @@ -343,9 +343,11 @@ abstract class VyOSElasticModernFulltextStorageEngine $exclude = $query->getParameter('exclude'); if ($exclude) { // Correct from day one: bool.must_not, not the obsolete 'not' clause. + // Cast to array so a single PHID scalar and an already-array of PHIDs + // both produce a flat list for the Elasticsearch ids.values field. $q->addMustNotClause(array( 'ids' => array( - 'values' => array($exclude), + 'values' => array_values((array)$exclude), ), )); } @@ -460,7 +462,7 @@ abstract class VyOSElasticModernFulltextStorageEngine public function initIndex() { $host = $this->getHostForWrite(); - if ($this->indexExists()) { + if ($this->indexExists($host)) { $this->executeRequest($host, '/', array(), 'DELETE'); } $data = $this->getIndexConfiguration(); @@ -472,6 +474,12 @@ abstract class VyOSElasticModernFulltextStorageEngine $host = $this->getHostForRead(); } $res = $this->executeRequest($host, '/_stats/', array()); + if (!isset($res['indices'][$this->index])) { + throw new Exception( + pht( + 'Index "%s" not found in Elasticsearch _stats response.', + $this->index)); + } $stats = $res['indices'][$this->index]; return array( pht('Queries') => -- cgit v1.2.3 From 4d27ac74aff1ca47db53510bf07e9d083635175b Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Sat, 23 May 2026 15:14:02 +0300 Subject: fix: correct Phase-0 findings from promotion PR review (3 issues) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. TestDouble signatures: indexExists() and getIndexStats() in VyOSElasticModernFulltextStorageEngineTestDouble were missing the nullable host parameter added in E7/E8, causing a PHP fatal on 'arc unit --everything'. 2. buildSpec() zero-query sort bug: the falsy check (!$query->getParameter('query')) incorrectly treats the string '0' as empty (PHP falsy), enabling date-sorted path for a real search term. Changed to explicit null/empty-string checks to match the existing must-clause guard on line 316. All 22 unit tests now pass. 🤖 Generated by [robots](https://vyos.io) --- src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php | 4 ++-- src/engine/VyOSElasticModernFulltextStorageEngine.php | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) (limited to 'src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php') diff --git a/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php b/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php index 8a8b29c..0362f95 100644 --- a/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php +++ b/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php @@ -17,11 +17,11 @@ final class VyOSElasticModernFulltextStorageEngineTestDouble return array(); } - public function indexExists() { + public function indexExists(?VyOSElasticModernHost $host = null) { return false; } - public function getIndexStats() { + public function getIndexStats(?VyOSElasticModernHost $host = null) { return array(); } diff --git a/src/engine/VyOSElasticModernFulltextStorageEngine.php b/src/engine/VyOSElasticModernFulltextStorageEngine.php index 4c14ce0..7fc7fd7 100644 --- a/src/engine/VyOSElasticModernFulltextStorageEngine.php +++ b/src/engine/VyOSElasticModernFulltextStorageEngine.php @@ -410,7 +410,8 @@ abstract class VyOSElasticModernFulltextStorageEngine ), ); - if (!$query->getParameter('query')) { + $sort_query_string = $query->getParameter('query'); + if ($sort_query_string === null || $sort_query_string === '') { $spec['sort'] = array( array('dateCreated' => 'desc'), ); -- cgit v1.2.3 From 3e8f4190bf666133b59112e351071c8d92b77def Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Sat, 23 May 2026 17:23:31 +0300 Subject: Tests: refresh stale docblock after engine abstract→class revert MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The class docblock still described the engine as abstract, from before commit 011eec1 reverted 'abstract class' to plain 'class'. Updated wording to accurately describe the concrete engine and the test-double's role. 🤖 Generated by [robots](https://vyos.io) --- src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php') diff --git a/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php b/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php index 0362f95..7e20fdc 100644 --- a/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php +++ b/src/__tests__/VyOSElasticModernFulltextStorageEngineTestCase.php @@ -1,9 +1,9 @@