docs: function (q) {
      q = dataUtils.isJSON(q) ? q : {};
  
      var _builder = this;
  
      /**
       * The last {@link SearchQueryBuilder} generated from the
       * last {@link QueryBuilder#search}() call.
       * @type {SearchQueryBuilder}
       * @private
       */
      var _lastSearchQueryBuilder;
  
      /**
       * The last {@link FieldQueryBuilder} generated from the
       * last {@link QueryBuilder#field}() call.
       * @type {FieldQueryBuilder}
       * @private
       */
      var _lastFieldQueryBuilder;
  
      /**
       * The last {@link OrQueryBuilder} generated from the
       * last {@link QueryBuilder#field}() call.
       * @type {OrQueryBuilder}
       * @private
       */
      var _lastOrQueryBuilder;
  
      /**
       * Creates an $or query. If an $or exists already, both
       * the existing and this are moved to $and as entries.
       * @param {Array.<{}>} queries
       * @private
       */
      this._or = function (queries) {
          if (!queries || !queries.length) return;
  
          // single element $or is as good as $and
          if (queries.length == 1) {
              return _builder._and(queries);
          }
          // $and with existing $or, if any
          else if (q.$or) {
              q.$and = q.$and || [];
              q.$and.push({$or: q.$or}, {$or: queries});
              delete q.$or;
          }
          else {
              q.$or = queries;
          }
      };
  
      /**
       * Creates an $and query. Tries to merge query directly
       * into the main query, or as entries to an $and operator.
       * @param {Array.<{}>} queries
       * @private
       */
      this._and = function (queries) {
          // merge query, its existing $and, and the given values
          var i;
          var toBeAnded = [];
          if (Object.keys(q).length) {
              toBeAnded.push(q);
          }
          if (q.$and) {
              var aq;
              for (i = 0; i < q.$and.length; ++i) {
                  aq = q.$and[i];
                  if (!dataUtils.arrayContainsValue(toBeAnded, aq)) {
                      toBeAnded.push(aq);
                  }
              }
              delete q.$and;
          }
          var query;
          for (i = 0; i < queries.length; ++i) {
              query = queries[i];
              if (!dataUtils.arrayContainsValue(toBeAnded, query)) {
                  toBeAnded.push(query);
              }
          }
          // use the merged value as the new q
          var mergedResult = _mergeManyQueryJSONs(toBeAnded);
          q = mergedResult.shift();
          // and any residues as the new $and
          if (mergedResult.length) {
              q.$and = mergedResult;
          }
      };
  
      /**
       * Ensures that the field matches all the values.
       * @param {string} field A field in the target document.
       * @param {Array.<{}>} values Array of values to be matched.
       * @returns {QueryBuilder} this builder for further chaining.
       * @private
       */
      this._matchesAll = function (field, values) {
          if (!dataUtils.isValidStr(field)) throw "Invalid field, should be a string: " + dataUtils.JSONstringify(field);
  
          var queries = [];
          var vq;
          for (var i = 0; i < values.length; ++i) {
              vq = {};
              vq[field] = values[i];
              if (!dataUtils.arrayContainsValue(queries, vq)) {
                  queries.push(vq);
              }
          }
  
          _builder._and(queries);
          return _builder;
      };
  
      /**
       * Ensures that the field matches at least one of the values.
       * @param {string} field A field in the target document.
       * @param {Array.<{}>} values Array of values to be matched.
       * @param {boolean} [addToExistingOr=false] If true, this will
       * be added to the existing OR list ($in), if any.
       * @returns {QueryBuilder} this builder for further chaining.
       * @private
       */
      this._matchesAny = function (field, values, addToExistingOr) {
          if (!dataUtils.isValidStr(field)) throw "Invalid field, should be a string: " + dataUtils.JSONstringify(field);
  
          if (values.length == 1 && !addToExistingOr) {
              return _builder._matchesAll(field, values);
          }
  
          // collect any existing 'or' values
          var existingFieldValues;
          if (addToExistingOr && q[field] && q[field].$in) {
              existingFieldValues = q[field].$in;
              // and delete them. We'll collate and add the $in again.
              if (Object.keys(q[field]).length == 1) { // field: { $in: [] }
                  delete q[field];
              } else { // field: { $in: [], prop1: something, ... }
                  delete q[field].$in;
              }
  
          } else {
              existingFieldValues = [];
          }
          // add 'values' to the 'or' list
          var v;
          for (var i = 0; i < values.length; ++i) {
              v = values[i];
              if (!dataUtils.arrayContainsValue(existingFieldValues, v)) {
                  existingFieldValues.push(v);
              }
          }
          // add the 'or' list to the actual query
          if (existingFieldValues.length) {
              _builder._compare(field, "$in", existingFieldValues);
          }
  
          return _builder;
      };
  
      /**
       * Ensures the comparison of the field with value.
       * @param {string} field A field in the target document.
       * @param {string} comparator e.g. "$gt", "$gte", etc.
       * @param {*} value
       * @private
       */
      this._compare = function (field, comparator, value) {
          if (!dataUtils.isValidStr(field)) throw "Invalid field, should be a string: " + dataUtils.JSONstringify(field);
          if (!dataUtils.isValidStr(comparator)) throw "Invalid comparator, should be a string: " + dataUtils.JSONstringify(comparator);
  
          var cq = {};
          cq[comparator] = value;
          return _builder._matchesAll(field, [cq]);
      };
  
      /**
       * All other functions are by default ANDed. So
       * this is just a helper function to improve
       * readability of complex queries.
       *
       * @returns {QueryBuilder} this query builder.
       */
      this.and = function () {
          return _builder;
      };
  
      /**
       * Creates a new {@link SearchQueryBuilder} to search fields using
       * string queries.
       *
       * @param {string} queryStr Input string containing one or more
       * space separated tokens.
       * @param {boolean} [matchWithinWords=false] If true, matching is done anywhere.
       * By default, matching is done only at word beginnings.
       * @returns {SearchQueryBuilder} a new {@link SearchQueryBuilder}
       */
      this.search = function (queryStr, matchWithinWords) {
          return _lastSearchQueryBuilder = new SearchQueryBuilder(_builder, queryStr, matchWithinWords);
      };
  
      /**
       * Continue more query chaining with the last {@link SearchQueryBuilder}
       * from the last {@link QueryBuilder#search}() call.
       *
       * @returns {SearchQueryBuilder} the last {@link SearchQueryBuilder}
       * from the last {@link QueryBuilder#search}() call.
       */
      this.andSearch = function () {
          if (!_lastSearchQueryBuilder) throw "Illegal andSearch() call: Should be called only after search() was called!";
          return _lastSearchQueryBuilder;
      };
  
      /**
       * Creates a new {@link FieldQueryBuilder} to create queries
       * for document fields.
       *
       * @param {string} field A field in the target document.
       * @returns {FieldQueryBuilder} a new {@link FieldQueryBuilder}.
       */
      this.field = function (field) {
          return _lastFieldQueryBuilder = new FieldQueryBuilder(_builder, field);
      };
  
      /**
       * Continue more query chaining with the last {@link FieldQueryBuilder}
       * generated from the last {@link QueryBuilder#field}() call.
       * @returns {FieldQueryBuilder} the last {@link FieldQueryBuilder}
       * generated from the last {@link QueryBuilder#field}() call.
       */
      this.andField = function () {
          if (!_lastFieldQueryBuilder) throw "Illegal andField() call: Should be called only after field() was called!";
          return _lastFieldQueryBuilder;
      };
  
      /**
       * Starts an OR query builder.
       * @returns {ChildQueryBuilder} A new {@link ChildQueryBuilder} child in
       * this OR group.
       */
      this.either = function () {
          if (_lastOrQueryBuilder) _builder.build();
          _lastOrQueryBuilder = new OrQueryBuilder(_builder);
          return _lastOrQueryBuilder.or();
      };
  
      /**
       * Returns the final query object built.
       * @returns {{}} the final query object built.
       */
      this.build = function () {
          if (_lastOrQueryBuilder) {
              _builder._or(_lastOrQueryBuilder.flush());
          }
          return q;
      };
  }