+/**
+ * Manual validation script for BooleanQueryBuilder
+ * This tests the new fluent API functionality
+ */
+
+try {
+ // Get the SearchBuilder instance (this should work if the module is properly configured)
+ searchBuilder = new cbelasticsearch.models.SearchBuilder();
+
+ writeOutput("BooleanQueryBuilder Manual Validation
");
+
+ // Test 1: Basic fluent must() method
+ writeOutput("Test 1: Basic must().term() fluent API
");
+ testBuilder1 = new cbelasticsearch.models.SearchBuilder();
+ testBuilder1.new( "test_index", "test_type" );
+
+ // This should work: must().term()
+ testBuilder1.must().term( "status", "active" );
+ query1 = testBuilder1.getQuery();
+
+ writeOutput("Query Structure:
");
+ writeOutput("" & serializeJSON( query1, false, true ) & "
");
+
+ // Verify structure
+ hasCorrectStructure1 = structKeyExists( query1, "bool" )
+ && structKeyExists( query1.bool, "must" )
+ && isArray( query1.bool.must )
+ && arrayLen( query1.bool.must ) == 1
+ && structKeyExists( query1.bool.must[1], "term" )
+ && structKeyExists( query1.bool.must[1].term, "status" )
+ && query1.bool.must[1].term.status == "active";
+
+ writeOutput("Validation: " & (hasCorrectStructure1 ? "PASS" : "FAIL") & "
");
+
+ // Test 2: Nested fluent API - bool().filter().bool().must()
+ writeOutput("Test 2: Nested bool().filter().bool().must().wildcard() API
");
+ testBuilder2 = new cbelasticsearch.models.SearchBuilder();
+ testBuilder2.new( "test_index", "test_type" );
+
+ // This tests the original issue case
+ testBuilder2.bool().filter().bool().must().wildcard( "title", "*test*" );
+ query2 = testBuilder2.getQuery();
+
+ writeOutput("Query Structure:
");
+ writeOutput("" & serializeJSON( query2, false, true ) & "
");
+
+ // Verify nested structure
+ hasCorrectStructure2 = structKeyExists( query2, "bool" )
+ && structKeyExists( query2.bool, "filter" )
+ && structKeyExists( query2.bool.filter, "bool" )
+ && structKeyExists( query2.bool.filter.bool, "must" )
+ && isArray( query2.bool.filter.bool.must )
+ && arrayLen( query2.bool.filter.bool.must ) == 1
+ && structKeyExists( query2.bool.filter.bool.must[1], "wildcard" )
+ && structKeyExists( query2.bool.filter.bool.must[1].wildcard, "title" );
+
+ writeOutput("Validation: " & (hasCorrectStructure2 ? "PASS" : "FAIL") & "
");
+
+ // Test 3: Multiple chained operations
+ writeOutput("Test 3: Multiple chained operations
");
+ testBuilder3 = new cbelasticsearch.models.SearchBuilder();
+ testBuilder3.new( "test_index", "test_type" );
+
+ // Chain multiple operations
+ testBuilder3
+ .must().term( "status", "active" )
+ .should().match( "title", "elasticsearch" )
+ .filter().range( "price", gte = 10, lte = 100 );
+
+ query3 = testBuilder3.getQuery();
+
+ writeOutput("Query Structure:
");
+ writeOutput("" & serializeJSON( query3, false, true ) & "
");
+
+ // Verify multiple structures
+ hasCorrectStructure3 = structKeyExists( query3, "bool" )
+ && structKeyExists( query3.bool, "must" )
+ && structKeyExists( query3.bool, "should" )
+ && structKeyExists( query3.bool, "filter" )
+ && isArray( query3.bool.must )
+ && isArray( query3.bool.should )
+ && structKeyExists( query3.bool.filter, "range" );
+
+ writeOutput("Validation: " & (hasCorrectStructure3 ? "PASS" : "FAIL") & "
");
+
+ // Test 4: Comparison with original manual approach
+ writeOutput("Test 4: Comparison with Manual Approach
");
+
+ // Manual approach (what we're replacing)
+ manualBuilder = new cbelasticsearch.models.SearchBuilder();
+ manualBuilder.new( "test_index", "test_type" );
+ var q = manualBuilder.getQuery();
+ param q.bool = {};
+ param q.bool.filter = {};
+ param q.bool.filter.bool.must = [];
+ arrayAppend( q.bool.filter.bool.must, {
+ "wildcard" : {
+ "title" : {
+ "value" : "*test*"
+ }
+ }
+ } );
+
+ writeOutput("Manual Query Structure:
");
+ writeOutput("" & serializeJSON( manualBuilder.getQuery(), false, true ) & "
");
+
+ writeOutput("Fluent Query Structure (from Test 2):
");
+ writeOutput("" & serializeJSON( query2, false, true ) & "
");
+
+ // Compare structures
+ manualJSON = serializeJSON( manualBuilder.getQuery(), false, false );
+ fluentJSON = serializeJSON( query2, false, false );
+ structuresMatch = manualJSON == fluentJSON;
+
+ writeOutput("Structures Match: " & (structuresMatch ? "PASS" : "FAIL") & "
");
+
+ if (!structuresMatch) {
+ writeOutput("Manual JSON:
" & manualJSON & "
");
+ writeOutput("Fluent JSON:
" & fluentJSON & "
");
+ }
+
+ // Test 5: Backward compatibility
+ writeOutput("Test 5: Backward Compatibility
");
+ testBuilder4 = new cbelasticsearch.models.SearchBuilder();
+ testBuilder4.new( "test_index", "test_type" );
+
+ // Mix old and new APIs
+ testBuilder4.mustMatch( "title", "elasticsearch" ); // Old API
+ testBuilder4.must().term( "status", "active" ); // New API
+
+ query4 = testBuilder4.getQuery();
+ writeOutput("Mixed API Query Structure:
");
+ writeOutput("" & serializeJSON( query4, false, true ) & "
");
+
+ // Should have 2 items in must array
+ backwardCompatible = structKeyExists( query4, "bool" )
+ && structKeyExists( query4.bool, "must" )
+ && isArray( query4.bool.must )
+ && arrayLen( query4.bool.must ) == 2;
+
+ writeOutput("Backward Compatibility: " & (backwardCompatible ? "PASS" : "FAIL") & "
");
+
+ // Summary
+ writeOutput("Summary
");
+ allTestsPass = hasCorrectStructure1 && hasCorrectStructure2 && hasCorrectStructure3 && structuresMatch && backwardCompatible;
+ writeOutput("Overall Result: " & (allTestsPass ? "ALL TESTS PASS" : "SOME TESTS FAILED") & "
");
+
+ if (allTestsPass) {
+ writeOutput("✓ BooleanQueryBuilder implementation is working correctly!
");
+ writeOutput("The fluent API successfully replaces manual query structure manipulation.
");
+ }
+
+} catch (any e) {
+ writeOutput("Error in Manual Validation
");
+ writeOutput("Error Details:
");
+ writeOutput("" & serializeJSON( e, false, true ) & "
");
+
+ writeOutput("Troubleshooting:
");
+ writeOutput("");
+ writeOutput("- Check if BooleanQueryBuilder.cfc exists in models/ directory
");
+ writeOutput("- Verify SearchBuilder.cfc has the new fluent methods
");
+ writeOutput("- Ensure module mapping is correct
");
+ writeOutput("
");
+}
+
\ No newline at end of file
diff --git a/test-harness/tests/specs/unit/BooleanQueryBuilderTest.cfc b/test-harness/tests/specs/unit/BooleanQueryBuilderTest.cfc
new file mode 100644
index 0000000..77050ba
--- /dev/null
+++ b/test-harness/tests/specs/unit/BooleanQueryBuilderTest.cfc
@@ -0,0 +1,233 @@
+component extends="coldbox.system.testing.BaseTestCase" {
+
+ this.loadColdbox = true;
+
+ function beforeAll(){
+ super.beforeAll();
+
+ variables.model = getWirebox().getInstance( "BooleanQueryBuilder@cbElasticSearch" );
+ variables.searchBuilder = getWirebox().getInstance( "SearchBuilder@cbElasticSearch" );
+
+ variables.testIndexName = lCase( "booleanQueryBuilderTests" );
+ variables.searchBuilder.getClient().deleteIndex( variables.testIndexName );
+
+ // create our new index
+ getWirebox()
+ .getInstance( "IndexBuilder@cbelasticsearch" )
+ .new(
+ name = variables.testIndexName,
+ properties = {
+ "mappings" : {
+ "testdocs" : {
+ "_all" : { "enabled" : false },
+ "properties" : {
+ "title" : { "type" : "text" },
+ "createdTime" : { "type" : "date", "format" : "date_time_no_millis" },
+ "price" : { "type" : "float" },
+ "status" : { "type" : "keyword" }
+ }
+ }
+ }
+ }
+ )
+ .save();
+ }
+
+ function afterAll(){
+ variables.searchBuilder.getClient().deleteIndex( variables.testIndexName );
+ super.afterAll();
+ }
+
+ function run(){
+ describe( "Performs cbElasticsearch BooleanQueryBuilder fluent API tests", function(){
+
+ it( "Tests fluent bool().must().term() placement", function(){
+ var searchBuilder = variables.searchBuilder.new( variables.testIndexName, "testdocs" );
+
+ searchBuilder.bool().must().term( "status", "active" );
+
+ var query = searchBuilder.getQuery();
+ expect( query ).toBeStruct().toHaveKey( "bool" );
+ expect( query.bool ).toHaveKey( "must" );
+ expect( query.bool.must ).toBeArray().toHaveLength( 1 );
+ expect( query.bool.must[ 1 ] ).toHaveKey( "term" );
+ expect( query.bool.must[ 1 ].term ).toHaveKey( "status" );
+ expect( query.bool.must[ 1 ].term.status ).toBe( "active" );
+ } );
+
+ it( "Tests fluent bool().should().match() placement", function(){
+ var searchBuilder = variables.searchBuilder.new( variables.testIndexName, "testdocs" );
+
+ searchBuilder.bool().should().match( "title", "elasticsearch" );
+
+ var query = searchBuilder.getQuery();
+ expect( query ).toBeStruct().toHaveKey( "bool" );
+ expect( query.bool ).toHaveKey( "should" );
+ expect( query.bool.should ).toBeArray().toHaveLength( 1 );
+ expect( query.bool.should[ 1 ] ).toHaveKey( "match" );
+ expect( query.bool.should[ 1 ].match ).toHaveKey( "title" );
+ expect( query.bool.should[ 1 ].match.title ).toBe( "elasticsearch" );
+ } );
+
+ it( "Tests fluent bool().filter().bool().must().wildcard() nested placement", function(){
+ var searchBuilder = variables.searchBuilder.new( variables.testIndexName, "testdocs" );
+
+ searchBuilder.bool().filter().bool().must().wildcard( "title", "*test*" );
+
+ var query = searchBuilder.getQuery();
+ expect( query ).toBeStruct().toHaveKey( "bool" );
+ expect( query.bool ).toHaveKey( "filter" );
+ expect( query.bool.filter ).toHaveKey( "bool" );
+ expect( query.bool.filter.bool ).toHaveKey( "must" );
+ expect( query.bool.filter.bool.must ).toBeArray().toHaveLength( 1 );
+ expect( query.bool.filter.bool.must[ 1 ] ).toHaveKey( "wildcard" );
+ expect( query.bool.filter.bool.must[ 1 ].wildcard ).toHaveKey( "title" );
+ expect( query.bool.filter.bool.must[ 1 ].wildcard.title ).toHaveKey( "value" );
+ expect( query.bool.filter.bool.must[ 1 ].wildcard.title.value ).toBe( "*test*" );
+ } );
+
+ it( "Tests fluent must().term() direct placement", function(){
+ var searchBuilder = variables.searchBuilder.new( variables.testIndexName, "testdocs" );
+
+ searchBuilder.must().term( "status", "published" );
+
+ var query = searchBuilder.getQuery();
+ expect( query ).toBeStruct().toHaveKey( "bool" );
+ expect( query.bool ).toHaveKey( "must" );
+ expect( query.bool.must ).toBeArray().toHaveLength( 1 );
+ expect( query.bool.must[ 1 ] ).toHaveKey( "term" );
+ expect( query.bool.must[ 1 ].term ).toHaveKey( "status" );
+ expect( query.bool.must[ 1 ].term.status ).toBe( "published" );
+ } );
+
+ it( "Tests fluent should().terms() with array values", function(){
+ var searchBuilder = variables.searchBuilder.new( variables.testIndexName, "testdocs" );
+
+ searchBuilder.should().terms( "status", [ "active", "published" ] );
+
+ var query = searchBuilder.getQuery();
+ expect( query ).toBeStruct().toHaveKey( "bool" );
+ expect( query.bool ).toHaveKey( "should" );
+ expect( query.bool.should ).toBeArray().toHaveLength( 1 );
+ expect( query.bool.should[ 1 ] ).toHaveKey( "terms" );
+ expect( query.bool.should[ 1 ].terms ).toHaveKey( "status" );
+ expect( query.bool.should[ 1 ].terms.status ).toBeArray();
+ expect( arrayLen( query.bool.should[ 1 ].terms.status ) ).toBe( 2 );
+ } );
+
+ it( "Tests fluent mustNot().exists() placement", function(){
+ var searchBuilder = variables.searchBuilder.new( variables.testIndexName, "testdocs" );
+
+ searchBuilder.mustNot().exists( "deletedAt" );
+
+ var query = searchBuilder.getQuery();
+ expect( query ).toBeStruct().toHaveKey( "bool" );
+ expect( query.bool ).toHaveKey( "must_not" );
+ expect( query.bool.must_not ).toBeArray().toHaveLength( 1 );
+ expect( query.bool.must_not[ 1 ] ).toHaveKey( "exists" );
+ expect( query.bool.must_not[ 1 ].exists ).toHaveKey( "field" );
+ expect( query.bool.must_not[ 1 ].exists.field ).toBe( "deletedAt" );
+ } );
+
+ it( "Tests fluent filter().range() placement", function(){
+ var searchBuilder = variables.searchBuilder.new( variables.testIndexName, "testdocs" );
+
+ searchBuilder.filter().range( "price", gte = 10, lte = 100 );
+
+ var query = searchBuilder.getQuery();
+ expect( query ).toBeStruct().toHaveKey( "bool" );
+ expect( query.bool ).toHaveKey( "filter" );
+ expect( query.bool.filter ).toHaveKey( "range" );
+ expect( query.bool.filter.range ).toHaveKey( "price" );
+ expect( query.bool.filter.range.price ).toHaveKey( "gte" );
+ expect( query.bool.filter.range.price ).toHaveKey( "lte" );
+ expect( query.bool.filter.range.price.gte ).toBe( 10 );
+ expect( query.bool.filter.range.price.lte ).toBe( 100 );
+ } );
+
+ it( "Tests chaining multiple fluent operations", function(){
+ var searchBuilder = variables.searchBuilder.new( variables.testIndexName, "testdocs" );
+
+ searchBuilder
+ .must().term( "status", "active" )
+ .should().match( "title", "test" )
+ .filter().range( "price", gte = 0 );
+
+ var query = searchBuilder.getQuery();
+
+ // Verify must clause
+ expect( query ).toBeStruct().toHaveKey( "bool" );
+ expect( query.bool ).toHaveKey( "must" );
+ expect( query.bool.must ).toBeArray().toHaveLength( 1 );
+ expect( query.bool.must[ 1 ] ).toHaveKey( "term" );
+
+ // Verify should clause
+ expect( query.bool ).toHaveKey( "should" );
+ expect( query.bool.should ).toBeArray().toHaveLength( 1 );
+ expect( query.bool.should[ 1 ] ).toHaveKey( "match" );
+
+ // Verify filter clause
+ expect( query.bool ).toHaveKey( "filter" );
+ expect( query.bool.filter ).toHaveKey( "range" );
+ } );
+
+ it( "Tests fluent API preserves existing SearchBuilder functionality", function(){
+ var searchBuilder = variables.searchBuilder.new( variables.testIndexName, "testdocs" );
+
+ // Mix old and new API
+ searchBuilder.mustMatch( "title", "elasticsearch" );
+ searchBuilder.must().term( "status", "active" );
+
+ var query = searchBuilder.getQuery();
+ expect( query ).toBeStruct().toHaveKey( "bool" );
+ expect( query.bool ).toHaveKey( "must" );
+ expect( query.bool.must ).toBeArray().toHaveLength( 2 );
+
+ // First should be from mustMatch
+ expect( query.bool.must[ 1 ] ).toHaveKey( "match" );
+ expect( query.bool.must[ 1 ].match ).toHaveKey( "title" );
+
+ // Second should be from fluent API
+ expect( query.bool.must[ 2 ] ).toHaveKey( "term" );
+ expect( query.bool.must[ 2 ].term ).toHaveKey( "status" );
+ } );
+
+ it( "Tests term query with boost parameter", function(){
+ var searchBuilder = variables.searchBuilder.new( variables.testIndexName, "testdocs" );
+
+ searchBuilder.must().term( "status", "active", 2.0 );
+
+ var query = searchBuilder.getQuery();
+ expect( query.bool.must[ 1 ].term.status ).toBeStruct();
+ expect( query.bool.must[ 1 ].term.status ).toHaveKey( "value" );
+ expect( query.bool.must[ 1 ].term.status ).toHaveKey( "boost" );
+ expect( query.bool.must[ 1 ].term.status.value ).toBe( "active" );
+ expect( query.bool.must[ 1 ].term.status.boost ).toBe( 2.0 );
+ } );
+
+ it( "Tests match query with boost parameter", function(){
+ var searchBuilder = variables.searchBuilder.new( variables.testIndexName, "testdocs" );
+
+ searchBuilder.should().match( "title", "elasticsearch", 1.5 );
+
+ var query = searchBuilder.getQuery();
+ expect( query.bool.should[ 1 ].match.title ).toBeStruct();
+ expect( query.bool.should[ 1 ].match.title ).toHaveKey( "query" );
+ expect( query.bool.should[ 1 ].match.title ).toHaveKey( "boost" );
+ expect( query.bool.should[ 1 ].match.title.query ).toBe( "elasticsearch" );
+ expect( query.bool.should[ 1 ].match.title.boost ).toBe( 1.5 );
+ } );
+
+ it( "Tests that fluent methods can be executed and return valid results", function(){
+ var searchBuilder = variables.searchBuilder.new( variables.testIndexName, "testdocs" );
+
+ searchBuilder.must().term( "status", "nonexistent" );
+
+ var result = searchBuilder.execute();
+ expect( result ).toBeInstanceOf( "cbElasticsearch.models.SearchResult" );
+ } );
+
+ } );
+ }
+
+}
\ No newline at end of file