Functional custom search by attribute

Permalink 5 users found helpful
A custom search by attributes block/form with user selected inputs (text, select boxes, radio/check buttons) will be greatly appreciated. The best I could do was a workaround a bug in the database_indexed_search and search by a single attribute variable. How to search by attributes with more variables (user inputs)?

If anyone can help me with this one, I'm happy to post the whole code for others to reuse. Maybe someone could make a proper installable block out of it.

linuxoid
 
synlag replied on at Permalink Reply
synlag
linuxoid replied on at Permalink Reply
linuxoid
I appreciate some need to be paid for their efforts. But I wish I had more help from the community. If I was working at a web design company or making a business out of my website, I wouldn't hesitate to purchase any add-ons especially if it's such a small investment you can later reuse a thousand times. But at this stage I can't afford US$20 for something I don't even know is going to do what I need.

I think Drupal and Joomla have such a huge community because there's a lot of free add-ons for them. If C5 had more free stuff I'm sure they'd build a big community too. And the bigger the community the bigger an opportunity to get paid for the service.

If only could they offered something like this:http://extensions.joomla.org/extensions/search-a-indexing/tags-a-cl...
Tony replied on at Permalink Reply
Tony
if you don't want to spend $20 on that search tools block (which does look really nice) you can code something up with the help of the /concrete/models/page_list class, which has a bunch of built in methods for filtering search results. check out the pagelist block's controller ( /concrete/blocks/pagelist/controller.php ) for a usage example.
linuxoid replied on at Permalink Reply
linuxoid
That's exactly what I was trying to do but looks like the page_list class doesn't do what it's supposed to do or it's got a bug (http://www.concrete5.org/community/forums/customizing_c5/how-to-change-search-block-to-filterbyattribute#51682/).
Tony replied on at Permalink Reply
Tony
yeah, searching by attribute isn't as easy as it could be.

here's a code snippet:

$pagelist = new PageList();
$ak = CollectionAttributeKey::get( $akID );
$type = $ak->getAttributeType();
$cnt = $type->getController();
$cnt->setAttributeKey($ak);
$req = $pagelist->getSearchRequest();
$cnt->setRequestArray($req);
$cnt->searchForm($pagelist);
$results = $pagelist->get( $pageSize, $result_offset );
linuxoid replied on at Permalink Reply
linuxoid
Tony,

What does this code do? For example, the PageList class doesn't have a function getSearchRequest(). Or it's in the DatabaseItemsList? How can I find all those classes? Do I need to load the other classes or it's done automatically?

Isn't this inventing the wheel? I mean, if the Search block's controller do_search() function already can filterByAttribute (as I showed here:http://www.concrete5.org/community/forums/customizing_c5/how-to-cha... can't it be somehow modified to do search to match all selected attributes?

Or you're saying there's no other way but to do this from scratch as you've showed?

Thank you for your help.
Tony replied on at Permalink Reply
Tony
the pagelist's parent class, DatabaseItemList ( /concrete/libraries/item_list.php ), does have a method called getSearchRequest(), which the pagelist class inherits.

If you look at the search block's controller, it uses IndexedPageList, which has the comment "@DEPRECATED - Just use PageList with filterByKeywords instead. We'll keep this around so people know what to expect", and it extends the PageList class anyway, so the example I'm showing would work probably work fine within the search block do_search. just replace the $pagelist from my example with $ipl.
Tony replied on at Permalink Best Answer Reply
Tony
like...

Loader::library('database_indexed_search');
$ipl = new IndexedPageList();
$ipl->filterByKeywords($q);
if( is_array($_REQUEST['search_paths']) ){
    foreach($_REQUEST['search_paths'] as $path) {
        $ipl->addSearchPath($path);
    }
}
//make sure you set a value for $akID! 
$ak = CollectionAttributeKey::get( $akID );
$type = $ak->getAttributeType();
$cnt = $type->getController();
$cnt->setAttributeKey($ak);
$req = $ipl->getSearchRequest();
$cnt->setRequestArray($req);


note, that this is expecting the search form to display data in the same way as an attribute's 'form' is displayed.

$attrKey->render('form', false, true);
linuxoid replied on at Permalink Reply
linuxoid
"make sure you set a value for $akID" - you mean an attribute name/handle in each page?

Thanks a lot for the example.

It's nearly impossible for those new to C5 to get their way around due to lack or incompleteness of documentation. I'm not blaming anyone, just stating the fact that it's so hard to understand the C5 way of doing things. This is why I'm asking so many dumb questions.

The best documentation IMO is for Qt toolkit. If only was C5's done in a similar way, it would have been priceless.
synlag replied on at Permalink Reply
synlag
you can also grab the attribute key ($ak) by handle. As an example to display past and current events (event page type comes within the calendar package) in a page list, i copied just the page list controller to <web-root>/blocks/page_list to override it there and filter for past and current projects. This code is located before the $pl is set:

function getPages($query = null) {
         ...
         Loader::model('attribute/categories/collection');
         $cak_end_date = CollectionAttributeKey::getByHandle('calendar_event_end_date_time');
         if (is_object($cak_end_date)) {
            $localDateTime = Loader::Helper('date')->getLocalDatetime();
            $cah = 'calendar_event_end_date_time';
            if ( $c->getCollectionTypeHandle() == 'event_listing') { 
               $pl->filterByAttribute( $cah, $localDateTime, '>=');
            } else if ( $c->getCollectionTypeHandle() == 'event_archiv') {
               $pl->filterByAttribute( $cah, $localDateTime, '<');
            }
         }
         ...
      }


Just for explanation: If a page has the attribute 'calendar_event_date_time' set, we assume it's an event. However if we got an event_listing or an event_archiv page type handle active or past projects are outputted in the page list.
linuxoid replied on at Permalink Reply
linuxoid
Thank you, I'll try these.

Aren't page attributes stored in the database? Wouldn't it be easier just to query the DB and get only those pages which match the whole query?
synlag replied on at Permalink Reply
synlag
Attributes are stored in the db and are used for several table relation ships.

Page list inherits a lot stuff from DatabaseItemList and ItemList, which forces a very generic use of various filter methods for database items.

I don't think it's easier to setup the query by hand, but try it out :)
linuxoid replied on at Permalink Reply
linuxoid
Well, I tried everything but got nowhere, I'm still missing something, most of those functions gave me errors like "Fatal error: Call to a member function getCollectionTypeHandle() on a non-object in /srv/www/htdocs/c5/blocks/search/controller.php", looks like I'm missing some class initiations but I don't know which classes I have to instansiate.

I've looked in /models/attribute/categories/collection.php, 'getCollectionTypeHandle()' doesn't exist. I've checked in key.php, it doesn't exist there either. What's the $c object by the way?

Well, the example above has
getCollectionTypeHandle()
filterByAttribute()

and none of these exist in neither of those classes. Please tell me how someone new to C5 can figure out what's going on?
linuxoid replied on at Permalink Reply
linuxoid
I think I'm getting somewhere, I've got one more wall to climb over.

I've finally figured out how to get pages' attributes and can display any or all of them. My question is, if I have a page list, how can I get page attributes of individual pages in that list? What's the function?

Here's a part of my controller which gets a 'Meta Title' attribute string of the current page which I display in the view.php as a $temp:
Loader::model('page_list');
      $pl = new PageList();
      if( is_array($_REQUEST['search_paths']) ){ 
         foreach($_REQUEST['search_paths'] as $path) {
            //if(!strlen($path)) continue;
            $pl->addSearchPath($path);
         }
      }
      $res = $pl->getPage(); 
      foreach ($this->getCustomFields() as $att) {
         $val = $this->getValue($att->akHandle);
         if ($att->akName == 'Meta Title'){
            $temp =  $att->akName . ': ' . $val;
         }
      }

As I understand it, the $res is a list of pages. How do I get each page's attribute from this list?

And how should I modify this
foreach($res as $r) { 
         $results[] = new IndexedSearchResult($r['cID'], $r['cName'], $r['cDescription'], $r['score'], $r['cPath'], $r['content']);
      }

to display the results if IndexedSearchResult is not part of PageList? It says: "Fatal error: Cannot use object of type Page as array in /srv/www/htdocs/c5/blocks/search/controller.php"

Thank you.
Tony replied on at Permalink Reply
Tony
"how do i get each page's attribute from this list?"

foreach($res as $page){
echo $page->getAttribute('my_attribute_handle');
}

I don't really understand what you're trying to do with a lot of that code, but the pagelist block should return fully instantiated Page object, not an array.
linuxoid replied on at Permalink Reply
linuxoid
I'm trying to do the same thing - to filter a list of pages by a page type first, and then to filter the filtered list by page attributes.

Yes, the PageList returns a page of pages, and I need to show only those pages which match all selected attributes. If I don't do that, it shows every single page of the website. If I filterByAttribute, it only filters by one page attribute, I need to filter by at least 5. That's why I need to get a list of pages first, check one if all its attributes match the ones selected, if yes - show it, if not move to the next page and so on.

I guess I'll have to change the code to:
foreach($res as $page){
       if ($page->getAttribute('attribute_handle_1') == 'attribute_value_1' && $page->getAttribute('attribute_handle_2') == 'attribute_value_2' )
       //show the page as in my above code
}

where the 'attribute_value_1' and 'attribute_value_2' are the selected values in the form.

Or you have a better idea how this can be done?
linuxoid replied on at Permalink Reply
linuxoid
No, it didn't work. Here's my do_search function:
function do_search() {
      $q = $_REQUEST['query'];
      $length = $_REQUEST['length'];
      $continent = $_REQUEST['continent'];
      $adventure = $_REQUEST['adventure'];
      $interest = $_REQUEST['interest'];
      $attributeKeyHandle = 'continent';
      Loader::library('database_indexed_search');
      //$pl = new IndexedPageList();
      //$ipl->filterByKeywords($q);
      //Loader::library('indexed_search');
      Loader::model('page_list');
      $pl = new PageList();
      $pl->filterByCollectionTypeHandle('Press Release');
      if( is_array($_REQUEST['search_paths']) ){


If I use

$temp = $this->page->getAttribute('meta_title');

it shows the current (Search) page attribute. If I use

$temp = $r->page->getAttribute('meta_title');

is says "Fatal error: Call to a member function getAttribute() on a non-object".
linuxoid replied on at Permalink Reply
linuxoid
OK, I finally got it!!! I can filter the list of all articles by one attribute. This has been a freaking ride of my life for the past few week. Now all I need to do is to add 3-4 more && == to the if() and hope it's all gonna work as I hope.

Here's what did it in the controller:
foreach($res as $r) { 
         if($r['attribute'] == $continent){
            $results[] = new IndexedSearchResult($r['cID'], $r['cName'], $r['cDescription'], $r['score'], $r['cPath'], $r['content'], $r['attribute']);
         }
      }
...
   public function getSearchPage($p) {
      $results = array();
      foreach($p as $c) {
         $results[] = array('cID' => $c->getCollectionID(), 'cName' => $c->getCollectionName(), 'cDescription' => $c->getCollectionDescription(), 'score' => $c->getPageIndexScore(), 'cPath' => $c->getCollectionPath(), 'content' => $c->getPageIndexContent(), 'attribute' => $c->getAttribute('continent'));
      }
      return $results;
   }

in the view:
<b><?php echo $r->getAttribute();?></b>

and in the overriden database_indexed_search:
class IndexedSearchResult {
   public function __construct($id, $name, $description, $score, $cPath, $content, $attribute) {
      $this->cID = $id;
      $this->cName = $name;
      $this->cDescription = $description;      
      $this->score = $score;
      $this->cPath = $cPath;
      $this->content = $content;
      $this->attribute = $attribute;
   }
   public function getID() {return $this->cID;}
   public function getName() {return $this->cName;}
   public function getDescription() {return $this->cDescription;}
   public function getScore() {return $this->score;}
   public function getCollectionPath() {return $this->cPath;}


Then when I polish everything, I'll submit the whole custom_search module to the Marketplace, hope they can help me make a free block out of it.

If you have better ideas how to do this, please feel free to suggest. I don't really like that idea of building that long array in the database_indexed_search class adding all possible attributes to it. I'm sure there's gotta be a simple solution. If only could I figure out how to get attributes directly from the $pl page list???...

Geez... gotta go and have a big glass of wine :)
fastcrash replied on at Permalink Reply
fastcrash
my footprint come here.
this guy took 3 days to figure it out, how to get search result by useratribut.

thank you, i will try your code linux, i too want a search result by a real_name i define in user attribut.
mkly replied on at Permalink Reply
mkly
lol no. That guy is crazy. You can just do a user list
Loader::model('user_list');
$ul = new UserList();
$ul->filterByAttribute('real_name');
$users = $ul->get();
if(!empty($users)) {
  foreach($users as $user) {
    //do some stuff
  }
}
midlight replied on at Permalink Reply
midlight
thanks for saving my day :D