1 /* Copyright (C) 2000-2004  Thomas Bopp, Thorsten Hampel, Ludger Merkens
     3  *  This program is free software; you can redistribute it and/or modify
     4  *  it under the terms of the GNU General Public License as published by
     5  *  the Free Software Foundation; either version 2 of the License, or
     6  *  (at your option) any later version.
     8  *  This program is distributed in the hope that it will be useful,
     9  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  *  GNU General Public License for more details.
    13  *  You should have received a copy of the GNU General Public License
    14  *  along with this program; if not, write to the Free Software
    15  *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
    17  * $Id: searching.pike,v 1.9 2010/02/01 23:35:23 nicke Exp $
    19 inherit "/kernel/db_searching";
    22 #include <attributes.h>
    25 class searching : public db_searching{
    30 string get_identifier() { return "searching"; }
    35 #define LOG_QUERY(a, b...) werror(a,b);
    37 #define LOG_QUERY(a, b...)
    51   void create(string store, string k, mixed v, string ao) {
    55       value = "%" + v->get_object_id();
    69 string compose_value_expression(SearchToken st)
    72   if ( st->storage == STORE_ATTRIB ) 
    73     query = " (ob_ident = 'attrib' and ob_attr='"+st->key+"' and ob_data "+
    74       st->value[0]+" '"+st->value[1]+"') ";
    75   else if ( st->storage == "doc_data" ) 
    76     query = " (match(doc_data) against (\""+st->value[1]+"\") ";
    78     query = " (ob_data "+ st->value[0]+" '" + st->value[1]+"') ";
    82 string compose_value_expressions(array tokens)
    86   SearchToken last = tokens[0];
    87   string exp = compose_value_expression(last);
    89   for ( int i = 1; i < sizeof(tokens); i++ ) {
    90     SearchToken token = tokens[i];
    91     query += exp + " or ";
    92     exp = compose_value_expression(token);
    99 string get_table_name() { return "ob_data"; }
   106   void create(function func, mixed a) { f = func; args = a; }
   108   void asyncResult(mixed id, mixed result) {
   113  mapping results = ([ ]);
   118   array eq(string|int value) {
   120          intp(value) ? (string)value : fDb()->quote(value)
   123   array gt(string|int value) {
   124     return ({ ">", intp(value) ? (string)value : fDb()->quote(value)});
   126   array lt(string|int value) {
   127     return ({ "<", intp(value) ? (string)value : fDb()->quote(value)});
   129   array like(string value) {
   130     return ({ "like", intp(value) ? (string)value : fDb()->quote(value)});
   132   array lte(string|int value) {
   133     return ({ "<=", intp(value) ? (string)value : fDb()->quote(value)});
   135   array gte(string|int value) {
   136     return ({ ">=", intp(value) ? (string)value : fDb()->quote(value)});
   138   array btw(string|int low, string|int up) {
   139     return and(gte(low), lte(up));
   141   array or(array a, array b) {
   142     return ({ "or", ({a, b}) });
   144   array and(array a, array b) {
   145     return ({ "and", ({a, b}) });
   148   array extends = ({ });  
   149   array limits = ({ });
   150   array fulltext = ({ });
   154   void create(int sid, array cl) {
   157     service = get_module("ServiceManager");
   160   void search_attribute(string key, string value) {
   161     extends += ({ search(STORE_ATTRIB, key, like(value), "or") });
   164   void first_query(string store, string key, mixed value, mixed filter) {
   165     extends += ({ search(store, key, value, "or") });
   168   void limit(string store, string key, mixed value) {
   169     limits += ({ search(store, key, value, "and") });
   171   void extend(string store, string key, mixed value) {
   172     extends += ({ search(store, key, value, "or") });
   175   void extend_ft(string pattern) {
   176     fulltext += ({ SearchToken("doc_ft", "doc_data", pattern, "or") });
   179   SearchToken search(string store, string key, mixed value, string andor) {
   180     return SearchToken(store, key, value, andor);
   182   mapping serialize_token(SearchToken s) {
   185   void execute() { run(); }
   187     if ( !service->is_service("search") ) {
   188       FATAL("Unable to locate Search Service - running locally !");
   189       string _query = "select distinct ob_id from ( ob_data ";
   191       if ( arrayp(classes) && sizeof(classes) > 0 ) {
   192    _query += "INNER JOIN ob_class on ob_class.ob_id=ob_data.ob_id and ("+
   193      ( "ob_class = "+classes*" or ob_class=")+")";
   198       _query += compose_value_expressions(extends);
   199       LOG_QUERY("Query is: %s", _query);
   200       array res = query(_query);
   201       array sresult = ({ });
   202       foreach(res, mixed r) 
   204      sresult += ({ r->ob_id });
   206       handle_service(search_id, sresult);
   210      service->call_service_async("search", 
   211                  "search_id": search_id,
   213                  "extends": map(extends, serialize_token),
   214                  "limits": map(limits, serialize_token),
   215                  "fulltext": map(fulltext, serialize_token), ]));
   218    result->vars = ([ "id": search_id, ]);
   219    result->processFunc = handle_result;
   220    result->resultFunc = results[search_id]->asyncResult;
   224       if ( !service->is_service("search") )
   225      steam_error("Unable to locate search service !");
   227    service->call_service_async("search", 
   228                  "search_id": search_id,
   230                  "extends": map(extends, serialize_token),
   231                  "limits": map(limits, serialize_token),
   232                  "fulltext": map(fulltext, serialize_token), ]));
   236 array handle_result(array res)
   238   int size = sizeof(res);
   239   array result = ({ });
   241   for (int i =0; i<size; i++) {
   242     object o = find_object((int)res[i]);
   243     if ( objectp(o) && o->status() >= 0 )
   249 void handle_service(int id, array result) 
   251   result = handle_result(result);
   252   Result r = results[id];
   253   r->f(r->args, result);
   256 Search searchQuery(function result_cb, mixed result_args, mixed ... params)
   258   results[++searches] = Result(result_cb, result_args);
   259   return Search(searches, @params);
   262 object searchAsyncAttribute(string key, mixed val, mixed ... params) 
   264   Async.Return r = Async.Return();
   265   results[++searches] = Result(r->asyncResult, 0);
   266   Search s = Search(searches, @params);
   267   s->search_attribute(key, val);
   272 object searchAsync(array extends, array limits, array fulltext, void|int cBits)
   274   array classlist = ({ });
   276   while ( (cBits > 0) && (i < (1<<31)) )
   278     if ( (cBits & i) == cBits)
   279       classlist += ({ _Database->get_class_string(i) });
   282   object aResult = get_module("ServiceManager")->call_service_async("search", 
   283                                    "search_id": searches, 
   284                                    "classes": classlist,
   287                                    "fulltext": fulltext, ]));
   288   aResult->processFunc = handle_result;
   292 array search_simple(string searchTerm, void|int classBit)
   294   object handle = _Database->get_db_handle();
   295   string query = "SELECT distinct ob_id from ob_class WHERE obkeywords";
   296   if (search(searchTerm, "%") >= 0)
   297     query += " like '"+ handle->quote(searchTerm) + "'";
   299     query += "='"+handle->quote(searchTerm) +"'";
   302     query += " AND ob_class='"+_Database->get_class_string(classBit)+"'";
   303   object result = handle->big_query(query);
   304   array resultArr = allocate(result->num_rows());
   305   for (int i=0; i < result->num_rows(); i++) {
   306     mixed row = result->fetch_row();
   307     resultArr[i] = find_object((int)row[0]);
   312 string object_to_dc(object obj) 
   314   string rdf = "<rdf:RDF\n"+
   315   "xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"\n"+
   316   "xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n";
   318   object creator = obj->get_creator() || USER("root");
   320   rdf += sprintf("<rdf:Description rdf:about=\"%s\">\n"+
   321             "<dc:creator>%s</dc:creator>\n"+
   322             "<dc:title>%s</dc:title>\n"+
   323             "<dc:description>%s</dc:description>\n"+
   324             "<dc:subject>%s</dc:subject>\n"+
   325             "<dc:source>%s</dc:source>\n"+
   326             "<dc:date>%s</dc:date>\n"+
   327             "<dc:type>%s</dc:type>\n"+
   328             "<dc:identifier>%s</dc:identifier>\n"+
   329             "</rdf:Description>\n",
   330             get_module("filepath:tree")->object_to_filename(obj),
   332             obj->get_identifier(),
   333             obj->query_attribute(OBJ_DESC),
   336             (string)obj->query_attribute(OBJ_CREATION_TIME),
   337             obj->query_attribute(DOC_MIME_TYPE) || "",
   338             (string)obj->get_object_id()
   343 object searchKeyword(string keyword) 
   345   object aResult = get_module("ServiceManager")->call_service_async(
   347                                        "search_id": searches, 
   349                                        "extends": (["keyword": keyword]),
   351                                        "fulltext": ({ }), ]));
   352   aResult->processFunc = handle_result;
   359  * Filters an array of objects and returns those objects matching specified
   360  * filter rules, sorted in a specified order and with optional pagination.
   362  * The filter entries are applied in the order in which they are given. Each
   363  * filter must be an array like this:
   364  *   ({ +/-, "class", class-bitmask })
   365  *   ({ +/-, "attribute", attribute-name, condition, value/values })
   366  *   ({ +/-, "function", function-name, condition, value/values, [params] })
   367  *   ({ +/-, "access", access-bitmask })
   368  * If an object doesn't match any filter rule, it will be excluded by
   369  * default, so if you would like to include any objects that didn't match any
   370  * filter, append ({ "+", "class", CLASS_ALL }) to the end of your filter list.
   371  * If the second parameter of a filter is prefixed by an exclamation mark, then
   372  * the filter rule will match if the condition is not met. E.g.:
   373  *   ({ -, "!access", access-bitmask })
   375  * * +/- must be either "+" (include) or "-" (exclude) as a string.
   376  * * class-bitmask must be either a CLASS_* constant or a combination (binary
   377  *   OR) of CLASS_* constants (e.g. CLASS_CONTAINER|CLASS_DOCUMENT).
   378  * * attribute-name must be the name (key) of an attribute, e.g. "OBJ_NAME".
   379  * * condition must be one of the following strings: "==", "!=", "<",
   380  *   "<=", ">", ">=", "prefix", "suffix". Note that some conditions will only
   381  *   match for certain attribute types, e.g. int, float or string.
   382  * * value/values must be either a simple value like int, float, string,
   383  *   object, or an array of simple values, in which case the condition will
   384  *   try to match at least one of these values.
   385  * * params is optional and must be an array of parameters to pass to the
   386  *   function if specified.
   387  * * access-bitmask must be either a SANCTION_* constant or a combination
   388  *   (binary OR) of SANCTION_* constants (e.g. SANCTION_READ|SANCTION_ANNOTATE).
   390  * The sort entries are applied in the order in which they are given in case
   391  * some entries are considered equal regarding the previous sort rule. Each
   392  * sort entry must be an array like this:
   393  * ({ >/<, "class", class-order })
   394  * ({ >/<, "attribute", attribute-name })
   396  * * >/< must be "<" (ascending) or ">" (descending)
   397  * * class-order is optional and can be an array of CLASS_* constants. The
   398  *   result will be sorted in the specified order by the objects that match
   399  *   the specified classes. All classes that were not specified will be
   400  *   considered equal for this sort entry.
   401  * * attribute-name must be the name (key) of an attribute, e.g. "OBJ_NAME".
   404  * Return all documents and containers (no users) that the user can read,
   405  * sorted by type and then
   407  * filter_objects_array(
   409  *     ({ "-", "!access", SANCTION_READ }),
   410  *     ({ "-", "class", CLASS_USER }),
   411  *     ({ "+", "class", CLASS_DOCUMENT|CLASS_CONTAINER })
   414  *     ({ "<", "class", ({ CLASS_CONTAINER, CLASS_DOCUMENT }) }),
   415  *     ({ "<", "attribute", "OBJ_NAME" })
   419  * Return all documents with keywords "urgent" or "important" that the user
   420  * has read access to, that are no wikis and that have been changed in the
   421  * last 24 hours, sort them by modification date (newest first) and return
   422  * only the first 10 results:
   423  * filter_objects_array(
   425  *     ({ "-", "!access", SANCTION_READ }),
   426  *     ({ "-", "attribute", "OBJ_TYPE", "prefix", "container_wiki" }),
   427  *     ({ "-", "attribute", "DOC_LAST_MODIFIED", "<", time()-86400 }),
   428  *     ({ "-", "attribute", "OBJ_KEYWORDS", "!=", ({ "urgent", "important" }) }),
   429  *     ({ "+", "class", CLASS_DOCUMENT })
   432  *     ({ ">", "attribute", "DOC_LAST_MODIFIED" })
   435  * @param objects the array of which to retrieve a filtered selection
   436  * @param filters (optional) an array of filters (each an array as described
   437  *   above) that specify which objects to return
   438  * @param sort (optional) an array of sort entries (each an array as described
   439  *   above) that specify the order of the items (before pagination)
   440  * @param offset (optional) only return the objects starting at (and including)
   442  * @param length (optional) only return a maximum of this many objects
   443  * @return a mapping ([ "objects":({...}), "total":nr, "length":nr,
   444  *   "start":nr, "page":nr ]), where the "objects" value is an array of
   445  *   objects that match the specified filters, sort order and pagination.
   446  *   The other indices contain pagination information ("total" is the total
   447  *   number of objects after filtering but before applying "length", "length"
   448  *   is the requested number of items to return (as in the parameter list),
   449  *   "start" is the start index of the result in the total number of objects,
   450  *   and "page" is the page number (starting with 1) of pages with "length"
   451  *   objects each, or 0 if invalid).
   453 mapping paginate_object_array ( array objects, array|void filters, array|void sort, int|void offset, int|void length )
   456   if ( !arrayp(filters) || sizeof(filters) == 0 )
   461     foreach ( objects, object obj ) {
   462       if ( !objectp(obj) ) continue;
   463       foreach ( filters, mixed filter ) {
   464         if ( !arrayp(filter) || sizeof(filter) < 2 )
   465           THROW( sprintf( "Invalid filter: %O", filter ), E_ERROR );
   468         string filter_type = filter[1];
   469         if ( has_prefix( filter_type, "!" ) ) {
   471           filter_type = filter_type[1..];
   473         switch ( filter_type ) {
   476             if ( sizeof(filter) < 3 )
   477               THROW( sprintf( "Invalid filter: %O", filter ), E_ERROR );
   478             int obj_class = obj->get_object_class();
   479             if ( invert != ((filter[2] & obj_class) != 0) ) {  // matches class
   480               if ( filter[0] == "+" ) {  // include class
   484               else done = 1;  // exclude class
   489             if ( sizeof(filter) < 3 )
   490               THROW( sprintf( "Invalid filter: %O", filter ), E_ERROR );
   491             if ( invert != (get_module( "security" )->check_user_access( obj,
   492                                  this_user(), filter[2], 0, false ) != 0) ) {
   493               if ( filter[0] == "+" ) { // include
   497               else done = 1;  // exclude
   503             if ( sizeof(filter) < 5 )
   504               THROW( sprintf( "Invalid filter: %O", filter ), E_ERROR );
   506             if ( filter_type == "function" ) {
   507               mixed func_name = filter[2];
   508               if ( sizeof(filter) > 5 ) {
   509                 mixed func_params = filter[5];
   510                 if ( !arrayp(func_params) ) func_params = ({ func_params });
   511                 catch( value = obj->find_function(func_name)( @func_params ) );
   514                 catch( value = obj->find_function(func_name)() );
   518               value = obj->query_attribute( filter[2] );
   519             if ( mappingp(value) )
   520               break; // ignore mappings
   521             mixed target_value = filter[4];
   522             switch ( filter[3] ) {  // condition:
   524                 if ( !arrayp(value) ) {
   525                   if ( invert != (value == target_value) ) done = 1;
   528                 foreach ( value, mixed subvalue ) {
   529                   if ( subvalue == target_value ) {
   534                 if ( invert ) done = !done;
   537                 if ( !arrayp(value) ) {
   538                   if ( invert != (value != target_value) ) done = 1;
   542                 foreach ( value, mixed subvalue ) {
   543                   if ( subvalue == target_value ) {
   549                 if ( invert ) done = !done;
   552                 if ( !arrayp(value) ) {
   553                   if ( invert != (value <= target_value) ) done = 1;
   556                 foreach ( value, mixed subvalue ) {
   557                   if ( subvalue <= target_value ) {
   562                 if ( invert ) done = !done;
   565                 if ( !arrayp(value) ) {
   566                   if ( invert != (value >= target_value) ) done = 1;
   569                 foreach ( value, mixed subvalue ) {
   570                   if ( subvalue >= target_value ) {
   575                 if ( invert ) done = !done;
   578                 if ( !arrayp(value) ) {
   579                   if ( invert != (value < target_value) ) done = 1;
   582                 foreach ( value, mixed subvalue ) {
   583                   if ( subvalue < target_value ) {
   588                 if ( invert ) done = !done;
   591                 if ( !arrayp(value) ) {
   592                   if ( invert != (value > target_value) ) done = 1;
   595                 foreach ( value, mixed subvalue ) {
   596                   if ( subvalue > target_value ) {
   601                 if ( invert ) done = !done;
   604                 if ( stringp(value) ) {
   605                   if ( invert != has_prefix( value, target_value ) ) done = 1;
   608                 if ( !arrayp(value) ) {
   609                   if ( invert ) done = 1;
   612                 foreach ( value, mixed subvalue ) {
   613                   if ( !stringp(subvalue) ) continue;
   614                   if ( invert != has_prefix( subvalue, target_value ) ) {
   619                 if ( invert ) done = !done;
   622                 if ( stringp(value) ) {
   623                   if ( invert != has_suffix( value, target_value ) ) done = 1;
   626                 if ( !arrayp(value) ) {
   627                   if ( invert ) done = 1;
   630                 foreach ( value, mixed subvalue ) {
   631                   if ( !stringp(subvalue) ) continue;
   632                   if ( has_suffix( subvalue, target_value ) ) {
   637                 if ( invert ) done = !done;
   640             if ( done && filter[0] == "+" ) {  // condition matches
   642             }  // otherwise "done" is set and the object will be excluded
   652   if ( arrayp(sort) && sizeof(sort) > 0 ) {
   653     result = Array.sort_array( result, sort_objects_filter, sort );
   656   mapping info = ([ "total":sizeof(result), "length":length, "start":offset,
   657                     "page":0, "objects":({ }) ]);
   658   if ( offset >= sizeof(result) ) return info;
   659   if ( offset != 0 || ( length > 0 && length < sizeof(result) ) ) {
   660     if ( length < 1 || (offset + length >= sizeof(result)) )
   661       length = sizeof(result) - offset;
   662     result = result[ offset .. (offset+length-1) ];
   664   if ( result == objects )
   665     result = copy_value( objects );
   666   info["objects"] = result;
   667   info["page"] = (int)ceil( (float)length / (float)info["total"] );
   672  * Filters an object array according to filter rules, sorting, offset and
   673  * length. This returns the same as the "objects" index in the result of
   674  * paginate_object_array() and is here for compatibility reasons and ease of
   675  * use (if you don't need pagination information).
   677  * @see paginate_object_array
   679 array filter_object_array ( array objects, array|void filters, array|void sort, int|void offset, int|void length )
   681   return paginate_object_array( objects, filters, sort, offset, length )["objects"];
   684 object paginate_search_async ( array|void filters, array|void sort, int|void offset, int|void length )
   686   array limits = ({ });
   687   array extends = ({ });
   688   array fulltext = ({ });
   691   array remaining_filters = ({ });
   693   foreach ( filters, mixed filter ) {
   694     if ( !arrayp(filter) || sizeof(filter) < 2 )
   695       THROW( sprintf( "Invalid filter: %O", filter ), E_ERROR );
   697     string filter_type = filter[1];
   698     if ( has_prefix( filter_type, "!" ) ) {
   699       // inverted filters are currently not handled in the search itself,
   700       // they will be applied after the search.
   701       remaining_filters += ({ filter });
   705     switch ( filter_type ) {
   708       if ( sizeof(filter) < 3 )
   709         THROW( sprintf( "Invalid filter: %O", filter ), E_ERROR );
   710       if ( filter[0] == "+" ) classes |= filter[2];
   711       else classes_not |= filter[2];
   715       if ( sizeof(filter) < 3 )
   716         THROW( sprintf( "Invalid filter: %O", filter ), E_ERROR );
   717       // access filters are currently applied after the search, they are not
   718       // handled by the search itself
   719       remaining_filters += ({ filter });
   724       if ( sizeof(filter) < 5 )
   725         THROW( sprintf( "Invalid filter: %O", filter ), E_ERROR );
   726       // functions are currently not handled by the search itself, these
   727       // filters will be applied after the search
   728       remaining_filters += ({ filter });
   733       if ( sizeof(filter) < 5 )
   734         THROW( sprintf( "Invalid filter: %O", filter ), E_ERROR );
   735       string attrib = filter[2];
   736       string condition = filter[3];
   737       mixed target_value = filter[4];
   738       switch ( condition ) {
   748           // condition is okay for search
   753           // like doesn't check strictly enough, so use it for a first
   754           // selection and then run the filter after the search
   755           remaining_filters += ({ filter });
   758           THROW( "Invalid condition for search: " + condition, E_ERROR );
   760       if ( !arrayp( target_value ) ) target_value = ({ target_value });
   761       foreach ( target_value, mixed value ) {
   762         if ( filter[0] == "+" ) extends += ({ ([ "storage":"attrib",
   763                 "key":attrib, "value":({ condition, value }) ]) });
   764         else limits += ({ ([ "storage":"attrib",
   765                 "key":attrib, "value":({ condition, value }) ]) });
   770         if ( sizeof(filter) < 3 )
   771           THROW( sprintf( "Invalid filter: %O", filter ), E_ERROR );
   772         // limiting by content is not supported at the moment:
   773         if ( filter[0] != "+" ) break;
   774         fulltext += ({ ([ "storage":"doc_ft", "value":filter[2] ]) });
   779   if ( classes == 0 ) classes = CLASS_ALL & (~classes_not);
   780   array classlist = ({ });
   781   if ( classes != CLASS_ALL ) {
   784       mixed class_string = _Database->get_class_string(cBits);
   785       if ( class_string != "/classes/Object" )
   786         classlist += ({ class_string });
   791   object async_result = get_module("ServiceManager")->call_service_async(
   792                    "extends":extends, "limits":limits, "fulltext":fulltext ])
   794   async_result->processFunc = handle_paginate_search_result;
   795   async_result->userData = ([ "filters":remaining_filters, "sort":sort,
   796                               "offset":offset, "length":length ]);
   801  mapping handle_paginate_search_result ( array res, mapping data )
   803   array objects = ({ });
   804   foreach ( res, mixed obj ) {
   805     if ( stringp(obj) ) obj = (int)obj;
   806     if ( intp(obj) ) obj = find_object( obj );
   807     if ( objectp(obj) ) objects += ({ obj });
   809   return paginate_object_array( objects, data->filters, data->sort,
   810                                 data->offset, data->length );
   815 object filter_search_async ( array|void filters, array|void sort, int|void offset, int|void length )
   817   object async_result = paginate_search_async( filters, sort, offset, length );
   818   async_result->processFunc = handle_filter_search_result;
   823  array handle_filter_search_result ( array res, mapping data )
   825   array objects = ({ });
   826   foreach ( res, mixed obj ) {
   827     if ( stringp(obj) ) obj = (int)obj;
   828     if ( intp(obj) ) obj = find_object( obj );
   829     if ( objectp(obj) ) objects += ({ obj });
   831   return filter_object_array( objects, data->filters, data->sort,
   832                               data->offset, data->length );
   838  int sort_objects_filter ( object obj1, object obj2, array rules )
   840   foreach ( rules, array rule ) {
   842     if ( rule[0] == ">" ) reverse = 1;
   845         int obj_class1 = obj1->get_object_class();
   846         int obj_class2 = obj2->get_object_class();
   847         if ( sizeof(rule) < 3 ) {
   848           if ( obj_class1 > obj_class2 ) return 1 ^ reverse;
   849           else if ( obj_class1 < obj_class2 ) return 0 ^ reverse;
   855         foreach ( rule[2], int obj_class ) {
   856           if ( obj_class & obj_class1 ) index1 = index;
   857           if ( obj_class & obj_class2 ) index2 = index;
   858           if ( index1 >= 0 && index2 >= 0 ) break;
   861         if ( index1 > index2 ) return 1 ^ reverse;
   862         else if ( index1 < index2 ) return 0 ^ reverse;
   868         if ( arrayp( rule[2] ) ) {
   869           foreach ( rule[2], mixed key ) {
   870             if ( !value1 ) value1 = obj1->query_attribute( key );
   871             if ( !value2 ) value2 = obj2->query_attribute( key );
   875           value1 = obj1->query_attribute( rule[2] );
   876           value2 = obj2->query_attribute( rule[2] );
   878         if ( value1 == 0 && value2 == 0 ) continue;
   879         if ( stringp(value1) && value2 == 0 ) return 1 ^ reverse;
   880         else if ( value1 == 0 && stringp(value2) ) return 0 ^ reverse;
   881         else if ( stringp(value1) && stringp(value2) ) {
   882           if ( value1 > value2 ) return 1 ^ reverse;
   883           else if ( value1 < value2 ) return 0 ^ reverse;
   886         else if ( (intp(value1) || floatp(value1)) &&
   887                   (intp(value2) || floatp(value2)) ) {
   888           if ( value1 > value2 ) return 1 ^ reverse;
   889           else if ( value1 < value2 ) return 0 ^ reverse;
   902 private  array test_objects = ({ });
   905   // first create some objects to search
   906   object obj = get_factory(CLASS_DOCUMENT)->execute((["name": "document.doc", ]));
   907   obj->set_attribute(OBJ_KEYWORDS, ({ "Mistel", "approach" }));
   908   test_objects += ({ obj });
   909   // test external search service
   910   Test.add_test_function(test_search, 20, 1, ([ ]));
   915   if ( arrayp(test_objects) ) {
   916     foreach ( test_objects, object obj )
   917       catch ( obj->delete() );
   921 void search_test_finished(object result, array results)
   923   if ( sizeof(results) == 0 ) {
   924     Test.failed(result->userData->name,
   925            "Search %s finished with %d results in %d ms", 
   926            result->userData->name,
   928            get_time_millis()-result->userData->time);
   931     Test.succeeded(result->userData->name,
   932               "Search %s finished with %d results in %d ms", 
   933               result->userData->name,
   935               get_time_millis()-result->userData->time);
   937   result->userData->tests[result->userData->name] = 1;
   941  void test_search(int nr_tries, mapping tests)
   943   object serviceManager = get_module("ServiceManager");
   945   if ( !Test.test("Service Manager", 
   946              objectp(serviceManager), "Failed to find ServiceManager!") )
   949   if ( !serviceManager->is_service("search") ) {
   951       Test.failed("search service", 
   952              "failed to locate search services after %d tries",
   955       Test.add_test_function(test_search, 10, nr_tries+1, tests);
   958   if ( sizeof(tests) != 0 ) {
   959     foreach(values(tests), int i) {
   961    werror("Waiting for tests to finish!");
   962    Test.add_test_function(test_search, 10, nr_tries+1, tests);
   968   // now search for common queries
   969   object result, query;
   972   tests["simple1"] = 0;
   973   query = searchQuery(search_test_finished, ([ ]),  ({ }));
   974   query->extend(STORE_ATTRIB, OBJ_NAME, query->like("steam"));
   975   result = query->run_async();
   976   result->resultFunc = search_test_finished;
   977   result->userData = ([ "name": "simple1", 
   979                    "time":get_time_millis(),]);
   981   tests["simple2"] = 0;
   982   query = searchQuery(search_test_finished, ([ ]),  ({ }));
   983   query->extend(STORE_ATTRIB, OBJ_NAME, query->like("steam"));
   984   query->extend(STORE_ATTRIB, OBJ_DESC, query->like("steam"));
   985   query->extend(STORE_ATTRIB, OBJ_KEYWORDS, query->like("steam"));
   986   result = query->run_async();
   987   result->resultFunc = search_test_finished;
   988   result->userData = ([ "name": "simple2", 
   990                    "time":get_time_millis(),]);
   993   tests["keywords"] = 0;
   994   query = searchQuery(search_test_finished, ([ ]),  ({ }));
   995   query->extend(STORE_ATTRIB, OBJ_KEYWORDS, query->like("Mistel"));
   996   result = query->run_async();
   997   result->resultFunc = search_test_finished;
   998   result->userData = ([ "name": "keywords", 
  1000                    "time":get_time_millis(),]);
  1002   tests["target room"] = 0;
  1003   query = searchQuery(search_test_finished, ([ ]),  ({ "\"/classes/Room\"" }));
  1004   query->extend(STORE_ATTRIB, OBJ_NAME, query->like("coder%"));
  1005   result = query->run_async();
  1006   result->resultFunc = search_test_finished;
  1007   result->userData = ([ "name": "target room", 
  1009                    "time":get_time_millis(),]);
  1011   tests["user 1"] = 0;
  1012   query = searchQuery(search_test_finished, ([ ]),  ({ "\"/classes/User\"" }));
  1013   query->extend(STORE_ATTRIB, OBJ_NAME, query->like("service"));
  1014   result = query->run_async();
  1015   result->resultFunc = search_test_finished;
  1016   result->userData = ([ "name": "user 1", 
  1018                    "time":get_time_millis(),]);
  1020   Test.add_test_function(test_search, 10, 0, tests);