diff --git a/src/queries/core/complexquery.js b/src/queries/core/complexquery.js
index 72bff72cf1f05c616f02f3c92a111e3a61363d81..9015739a58e1c3ff6c9ab42299ef900aab3314b4 100644
--- a/src/queries/core/complexquery.js
+++ b/src/queries/core/complexquery.js
@@ -48,8 +48,12 @@ inherits(ComplexQuery, Query);
 /**
  * #crossLink "Query/match:method"
  */
-ComplexQuery.prototype.match = function (item, wildcard_character) {
-  return this[this.operator](item, wildcard_character);
+ComplexQuery.prototype.match = function (item) {
+  var operator = this.operator;
+  if (!(/^(?:AND|OR|NOT)$/i.test(operator))) {
+    operator = "AND";
+  }
+  return this[operator.toUpperCase()](item);
 };
 
 /**
@@ -79,6 +83,7 @@ ComplexQuery.prototype.serialized = function () {
   });
   return s;
 };
+ComplexQuery.prototype.toJSON = ComplexQuery.prototype.serialized;
 
 /**
  * Comparison operator, test if all sub queries match the
@@ -86,13 +91,12 @@ ComplexQuery.prototype.serialized = function () {
  *
  * @method AND
  * @param  {Object} item The item to match
- * @param  {String} wildcard_character The wildcard character
  * @return {Boolean} true if all match, false otherwise
  */
-ComplexQuery.prototype.AND = function (item, wildcard_character) {
+ComplexQuery.prototype.AND = function (item) {
   var j, promises = [];
   for (j = 0; j < this.query_list.length; j += 1) {
-    promises.push(this.query_list[j].match(item, wildcard_character));
+    promises.push(this.query_list[j].match(item));
   }
 
   function cancel() {
@@ -133,13 +137,12 @@ ComplexQuery.prototype.AND = function (item, wildcard_character) {
  *
  * @method OR
  * @param  {Object} item The item to match
- * @param  {String} wildcard_character The wildcard character
  * @return {Boolean} true if one match, false otherwise
  */
-ComplexQuery.prototype.OR =  function (item, wildcard_character) {
+ComplexQuery.prototype.OR =  function (item) {
   var j, promises = [];
   for (j = 0; j < this.query_list.length; j += 1) {
-    promises.push(this.query_list[j].match(item, wildcard_character));
+    promises.push(this.query_list[j].match(item));
   }
 
   function cancel() {
@@ -180,12 +183,11 @@ ComplexQuery.prototype.OR =  function (item, wildcard_character) {
  *
  * @method NOT
  * @param  {Object} item The item to match
- * @param  {String} wildcard_character The wildcard character
  * @return {Boolean} true if one match, false otherwise
  */
-ComplexQuery.prototype.NOT = function (item, wildcard_character) {
+ComplexQuery.prototype.NOT = function (item) {
   return sequence([function () {
-    return this.query_list[0].match(item, wildcard_character);
+    return this.query_list[0].match(item);
   }, function (answer) {
     return !answer;
   }]);
diff --git a/src/queries/core/parser.par b/src/queries/core/parser.par
index c235cff8e1e918e0ee1b43728065130da4144180..242bf8ed0e283f3436c86187320cb1a03f429c9e 100644
--- a/src/queries/core/parser.par
+++ b/src/queries/core/parser.par
@@ -62,7 +62,11 @@ var arrayExtend = function () {
   return newlist;
 
 }, mkSimpleQuery = function (key, value, operator) {
-  return {"type": "simple", "operator": "=", "key": key, "value": value};
+  var object = {"type": "simple", "key": key, "value": value};
+  if (operator !== undefined) {
+    object.operator = operator;
+  }
+  return object;
 
 }, mkNotQuery = function (query) {
   if (query.operator === "NOT") {
diff --git a/src/queries/core/query.js b/src/queries/core/query.js
index fb6eebc4862a8215b44981562bfe3584f81b8766..dee09838c826937f98be5d10f580c7626b1fc73e 100644
--- a/src/queries/core/query.js
+++ b/src/queries/core/query.js
@@ -56,7 +56,6 @@ function Query() {
  * @method exec
  * @param  {Array} item_list The list of object
  * @param  {Object} [option] Some operation option
- * @param  {String} [option.wildcard_character="%"] The wildcard character
  * @param  {Array} [option.select_list] A object keys to retrieve
  * @param  {Array} [option.sort_on] Couples of object keys and "ascending"
  *                 or "descending"
@@ -75,14 +74,11 @@ Query.prototype.exec = function (item_list, option) {
     throw new TypeError("Query().exec(): " +
                         "Optional argument 2 is not of type 'object'");
   }
-  if (option.wildcard_character === undefined) {
-    option.wildcard_character = '%';
-  }
   for (i = 0; i < item_list.length; i += 1) {
     if (!item_list[i]) {
       promises.push(RSVP.resolve(false));
     } else {
-      promises.push(this.match(item_list[i], option.wildcard_character));
+      promises.push(this.match(item_list[i]));
     }
   }
   return sequence([function () {
@@ -113,7 +109,6 @@ Query.prototype.exec = function (item_list, option) {
  *
  * @method match
  * @param  {Object} item The object to test
- * @param  {String} wildcard_character The wildcard character to use
  * @return {Boolean} true if match, false otherwise
  */
 Query.prototype.match = function () {
diff --git a/src/queries/core/serializer.js b/src/queries/core/serializer.js
index c4d62967b55dd9337b7ec2a6401b855b7fa14931..56fa53d691633ba47acea0e3dd9ffdcc370268ab 100644
--- a/src/queries/core/serializer.js
+++ b/src/queries/core/serializer.js
@@ -1,5 +1,5 @@
 /*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
-/*global _export: true, to_export: true */
+/*global _export: true */
 
 function objectToSearchText(query) {
   var str_list = [];
@@ -15,7 +15,7 @@ function objectToSearchText(query) {
   }
   if (query.type === "simple") {
     return (query.key ? query.key + ": " : "") +
-      (query.operator || "=") + ' "' + query.value + '"';
+      (query.operator || "") + ' "' + query.value + '"';
   }
   throw new TypeError("This object is not a query");
 }
diff --git a/src/queries/core/simplequery.js b/src/queries/core/simplequery.js
index f354d9e143202e6f84fa6be56ee69e8f570fb9ee..e3283d300c34db07ecf9b45d41c195493c50c203 100644
--- a/src/queries/core/simplequery.js
+++ b/src/queries/core/simplequery.js
@@ -1,6 +1,6 @@
 /*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
 /*global Query: true, inherits: true, query_class_dict: true, _export: true,
-  convertStringToRegExp, RSVP */
+  searchTextToRegExp, RSVP */
 
 var checkKeySchema = function (key_schema) {
   var prop;
@@ -54,10 +54,9 @@ function SimpleQuery(spec, key_schema) {
    *
    * @attribute operator
    * @type String
-   * @default "="
    * @optional
    */
-  this.operator = spec.operator || "=";
+  this.operator = spec.operator;
 
   /**
    * Key of the object which refers to the value to compare
@@ -105,15 +104,28 @@ var checkKey = function (key) {
 /**
  * #crossLink "Query/match:method"
  */
-SimpleQuery.prototype.match = function (item, wildcard_character) {
+SimpleQuery.prototype.match = function (item) {
   var object_value = null,
     equal_match = null,
     cast_to = null,
     matchMethod = null,
+    operator = this.operator,
     value = null,
     key = this.key;
 
-  matchMethod = this[this.operator];
+  /*jslint regexp: true */
+  if (!(/^(?:!?=|<=?|>=?)$/i.test(operator))) {
+    // `operator` is not correct, we have to change it to "like" or "="
+    if (/%/.test(this.value)) {
+      // `value` contains a non escaped `%`
+      operator = "like";
+    } else {
+      // `value` does not contain non escaped `%`
+      operator = "=";
+    }
+  }
+
+  matchMethod = this[operator];
 
   if (this._key_schema.key_set && this._key_schema.key_set[key] !== undefined) {
     key = this._key_schema.key_set[key];
@@ -133,7 +145,8 @@ SimpleQuery.prototype.match = function (item, wildcard_character) {
 
     // equal_match overrides the default '=' operator
     if (equal_match !== undefined) {
-      matchMethod = (this.operator === '=') ? equal_match : matchMethod;
+      matchMethod = (operator === "=" || operator === "like" ?
+                     equal_match : matchMethod);
     }
 
     value = this.value;
@@ -155,28 +168,32 @@ SimpleQuery.prototype.match = function (item, wildcard_character) {
   if (object_value === undefined || value === undefined) {
     return RSVP.resolve(false);
   }
-  return matchMethod(object_value, value, wildcard_character);
+  return matchMethod(object_value, value);
 };
 
 /**
  * #crossLink "Query/toString:method"
  */
 SimpleQuery.prototype.toString = function () {
-  return (this.key ? this.key + ": " : "") + (this.operator || "=") + ' "' +
-    this.value + '"';
+  return (this.key ? this.key + ":" : "") +
+    (this.operator ? " " + this.operator : "") + ' "' + this.value + '"';
 };
 
 /**
  * #crossLink "Query/serialized:method"
  */
 SimpleQuery.prototype.serialized = function () {
-  return {
+  var object = {
     "type": "simple",
-    "operator": this.operator,
     "key": this.key,
     "value": this.value
   };
+  if (this.operator !== undefined) {
+    object.operator = this.operator;
+  }
+  return object;
 };
+SimpleQuery.prototype.toJSON = SimpleQuery.prototype.serialized;
 
 /**
  * Comparison operator, test if this query value matches the item value
@@ -184,11 +201,40 @@ SimpleQuery.prototype.serialized = function () {
  * @method =
  * @param  {String} object_value The value to compare
  * @param  {String} comparison_value The comparison value
- * @param  {String} wildcard_character The wildcard_character
  * @return {Boolean} true if match, false otherwise
  */
-SimpleQuery.prototype["="] = function (object_value, comparison_value,
-                                       wildcard_character) {
+SimpleQuery.prototype["="] = function (object_value, comparison_value) {
+  var value, i;
+  if (!Array.isArray(object_value)) {
+    object_value = [object_value];
+  }
+  for (i = 0; i < object_value.length; i += 1) {
+    value = object_value[i];
+    if (typeof value === 'object' && value.hasOwnProperty('content')) {
+      value = value.content;
+    }
+    if (typeof value.cmp === "function") {
+      return RSVP.resolve(value.cmp(comparison_value) === 0);
+    }
+    if (
+      searchTextToRegExp(comparison_value.toString(), false).
+        test(value.toString())
+    ) {
+      return RSVP.resolve(true);
+    }
+  }
+  return RSVP.resolve(false);
+};
+
+/**
+ * Comparison operator, test if this query value matches the item value
+ *
+ * @method like
+ * @param  {String} object_value The value to compare
+ * @param  {String} comparison_value The comparison value
+ * @return {Boolean} true if match, false otherwise
+ */
+SimpleQuery.prototype.like = function (object_value, comparison_value) {
   var value, i;
   if (!Array.isArray(object_value)) {
     object_value = [object_value];
@@ -198,15 +244,11 @@ SimpleQuery.prototype["="] = function (object_value, comparison_value,
     if (typeof value === 'object' && value.hasOwnProperty('content')) {
       value = value.content;
     }
-    if (value.cmp !== undefined) {
-      return RSVP.resolve(value.cmp(comparison_value,
-                                    wildcard_character) === 0);
+    if (typeof value.cmp === "function") {
+      return RSVP.resolve(value.cmp(comparison_value) === 0);
     }
     if (
-      convertStringToRegExp(
-        comparison_value.toString(),
-        wildcard_character
-      ).test(value.toString())
+      searchTextToRegExp(comparison_value.toString()).test(value.toString())
     ) {
       return RSVP.resolve(true);
     }
@@ -220,11 +262,9 @@ SimpleQuery.prototype["="] = function (object_value, comparison_value,
  * @method !=
  * @param  {String} object_value The value to compare
  * @param  {String} comparison_value The comparison value
- * @param  {String} wildcard_character The wildcard_character
  * @return {Boolean} true if not match, false otherwise
  */
-SimpleQuery.prototype["!="] = function (object_value, comparison_value,
-                                        wildcard_character) {
+SimpleQuery.prototype["!="] = function (object_value, comparison_value) {
   var value, i;
   if (!Array.isArray(object_value)) {
     object_value = [object_value];
@@ -234,15 +274,12 @@ SimpleQuery.prototype["!="] = function (object_value, comparison_value,
     if (typeof value === 'object' && value.hasOwnProperty('content')) {
       value = value.content;
     }
-    if (value.cmp !== undefined) {
-      return RSVP.resolve(value.cmp(comparison_value,
-                                    wildcard_character) !== 0);
+    if (typeof value.cmp === "function") {
+      return RSVP.resolve(value.cmp(comparison_value) !== 0);
     }
     if (
-      convertStringToRegExp(
-        comparison_value.toString(),
-        wildcard_character
-      ).test(value.toString())
+      searchTextToRegExp(comparison_value.toString(), false).
+        test(value.toString())
     ) {
       return RSVP.resolve(false);
     }
@@ -267,7 +304,7 @@ SimpleQuery.prototype["<"] = function (object_value, comparison_value) {
   if (typeof value === 'object' && value.hasOwnProperty('content')) {
     value = value.content;
   }
-  if (value.cmp !== undefined) {
+  if (typeof value.cmp === "function") {
     return RSVP.resolve(value.cmp(comparison_value) < 0);
   }
   return RSVP.resolve(value < comparison_value);
@@ -291,7 +328,7 @@ SimpleQuery.prototype["<="] = function (object_value, comparison_value) {
   if (typeof value === 'object' && value.hasOwnProperty('content')) {
     value = value.content;
   }
-  if (value.cmp !== undefined) {
+  if (typeof value.cmp === "function") {
     return RSVP.resolve(value.cmp(comparison_value) <= 0);
   }
   return RSVP.resolve(value <= comparison_value);
@@ -315,7 +352,7 @@ SimpleQuery.prototype[">"] = function (object_value, comparison_value) {
   if (typeof value === 'object' && value.hasOwnProperty('content')) {
     value = value.content;
   }
-  if (value.cmp !== undefined) {
+  if (typeof value.cmp === "function") {
     return RSVP.resolve(value.cmp(comparison_value) > 0);
   }
   return RSVP.resolve(value > comparison_value);
@@ -339,7 +376,7 @@ SimpleQuery.prototype[">="] = function (object_value, comparison_value) {
   if (typeof value === 'object' && value.hasOwnProperty('content')) {
     value = value.content;
   }
-  if (value.cmp !== undefined) {
+  if (typeof value.cmp === "function") {
     return RSVP.resolve(value.cmp(comparison_value) >= 0);
   }
   return RSVP.resolve(value >= comparison_value);
diff --git a/src/queries/core/tool.js b/src/queries/core/tool.js
index 06b5f191c0c309ec09f625b993491c0900b068fa..c0985589ad3e9daf49074c3a0f4dad0318dfca6f 100644
--- a/src/queries/core/tool.js
+++ b/src/queries/core/tool.js
@@ -264,29 +264,27 @@ _export('limit', limit);
  * Convert a search text to a regexp.
  *
  * @param  {String} string The string to convert
- * @param  {String} [wildcard_character=undefined] The wildcard chararter
+ * @param  {Boolean} [use_wildcard_character=true] Use wildcard "%" and "_"
  * @return {RegExp} The search text regexp
  */
-function convertStringToRegExp(string, wildcard_character) {
+function searchTextToRegExp(string, use_wildcard_characters) {
   if (typeof string !== 'string') {
-    throw new TypeError("complex_queries.convertStringToRegExp(): " +
+    throw new TypeError("complex_queries.searchTextToRegExp(): " +
                         "Argument 1 is not of type 'string'");
   }
-  if (wildcard_character === undefined ||
-      wildcard_character === null || wildcard_character === '') {
+  if (use_wildcard_characters === false) {
     return new RegExp("^" + stringEscapeRegexpCharacters(string) + "$");
   }
-  if (typeof wildcard_character !== 'string' || wildcard_character.length > 1) {
-    throw new TypeError("complex_queries.convertStringToRegExp(): " +
-                        "Optional argument 2 must be a string of length <= 1");
-  }
   return new RegExp("^" + stringEscapeRegexpCharacters(string).replace(
-    new RegExp(stringEscapeRegexpCharacters(wildcard_character), 'g'),
-    '.*'
+    /%/g,
+    ".*"
+  ).replace(
+    /_/g,
+    "."
   ) + "$");
 }
 
-_export('convertStringToRegExp', convertStringToRegExp);
+_export("searchTextToRegExp", searchTextToRegExp);
 
 /**
  * sequence(thens): Promise
diff --git a/test/queries/key.tests.js b/test/queries/key.tests.js
index 9c9c9b65473cc3472c5ed1fc09b43a2d9753f61e..bf4bb57fa276d1c4cd9ebee0f9786041f7eda1c7 100644
--- a/test/queries/key.tests.js
+++ b/test/queries/key.tests.js
@@ -22,7 +22,6 @@
   };
 
   test('Simple Key with read_from', function () {
-    /*jslint unparam: true*/
     var docList = function () {
       return [
         {'identifier': 'a'},
@@ -35,12 +34,11 @@
       },
       case_insensitive_identifier: {
         read_from: 'identifier',
-        equal_match: function (object_value, value, wildcard_character) {
+        equal_match: function (object_value, value) {
           return (object_value.toLowerCase() === value.toLowerCase());
         }
       }
     }, promise = [];
-    /*jslint unparam: false*/
 
     stop();
 
diff --git a/test/queries/tests.js b/test/queries/tests.js
index 556a99347d8098e9cab59827f99bf0f07a6523e2..fc991aeab5b83410e9985d64973bcc8a4d790e11 100644
--- a/test/queries/tests.js
+++ b/test/queries/tests.js
@@ -105,44 +105,66 @@
     var doc_list = [
       {"identifier": "a"},
       {"identifier": "a%"},
+      {"identifier": "a\\%"},
       {"identifier": ["ab", "b"]}
     ];
     stop();
-    complex_queries.QueryFactory.create('identifier: "a%"').exec(doc_list, {
-      // "wildcard_character": "%" // default
-    }).then(function (doc_list) {
+    complex_queries.QueryFactory.create('identifier: "a%"').exec(
+      doc_list
+    ).then(function (doc_list) {
       deepEqual(doc_list, [
         {"identifier": "a"},
         {"identifier": "a%"},
+        {"identifier": "a\\%"},
         {"identifier": ["ab", "b"]}
       ], 'All documents should be kept');
 
       doc_list = [
         {"identifier": "a"},
         {"identifier": "a%"},
+        {"identifier": "a\\%"},
         {"identifier": ["ab", "b"]}
       ];
 
-      return complex_queries.QueryFactory.create('identifier: "a%"').
-        exec(doc_list, {"wildcard_character": null});
+      return complex_queries.QueryFactory.create('identifier: "a\\%"').
+        exec(doc_list);
     }).then(function (doc_list) {
       deepEqual(doc_list, [
-        {"identifier": "a%"}
-      ], 'Document "a%" should be kept');
+        {"identifier": "a\\%"}
+      ], 'Only third document should be kept');
+      // yes.. it's weird but ERP5 acts like that.
+      // `\` (or "\\") is taken literaly (= /\\/)
 
       doc_list = [
         {"identifier": "a"},
         {"identifier": "a%"},
+        {"identifier": "a\\%"},
         {"identifier": ["ab", "b"]}
       ];
-      return complex_queries.QueryFactory.create('identifier: "b"').
-        exec(doc_list, {"wildcard_character": "b"});
+
+      return complex_queries.QueryFactory.create('identifier: "__"').
+        exec(doc_list);
     }).then(function (doc_list) {
       deepEqual(doc_list, [
+      ], 'Should keep nothing');
+
+      doc_list = [
         {"identifier": "a"},
         {"identifier": "a%"},
+        {"identifier": "a\\%"},
         {"identifier": ["ab", "b"]}
-      ], 'All documents should be kept');
+      ];
+
+      return complex_queries.QueryFactory.create('identifier: "__%"').
+        exec(doc_list);
+    }).then(function (doc_list) {
+      deepEqual(doc_list, [
+        {"identifier": "a%"},
+        {"identifier": "a\\%"},
+        {"identifier": ["ab", "b"]}
+      ], 'First should not be kept');
+      // yes.. it's weird but ERP5 acts like that.
+      // `_` is not considered as wildcard (= /./)
     }).always(start);
   });
 
@@ -164,4 +186,46 @@
     }).always(start);
   });
 
+  test("JSON query", function () {
+    var jsoned = complex_queries.QueryFactory.create(
+      "NOT(a:=b OR c:% AND d:<2)"
+    ).toJSON();
+    deepEqual(
+      jsoned,
+      {
+        "type": "complex",
+        "operator": "NOT",
+        "query_list": [{
+          "type": "complex",
+          "operator": "OR",
+          "query_list": [{
+            "key": "a",
+            "operator": "=",
+            "type": "simple",
+            "value": "b"
+          }, {
+            "type": "complex",
+            "operator": "AND",
+            "query_list": [{
+              "key": "c",
+              "type": "simple",
+              "value": "%"
+            }, {
+              "key": "d",
+              "operator": "<",
+              "type": "simple",
+              "value": "2"
+            }]
+          }]
+        }]
+      },
+      "\"NOT(a:=b OR c:% AND d:<2)\".toJSON()"
+    );
+    deepEqual(
+      complex_queries.parseStringToObject("NOT(a:=b OR c:% AND d:<2)"),
+      jsoned,
+      "parseStringToObject(\"NOT(a:=b OR c:% AND d:<2)\");"
+    );
+  });
+
 }));