Magento – Include category in search result

catalogsearchmagento-1.9

When we search any text in default magento it shows only products in the search result but I want to show the categories also when someone searches for a query string.
Like if query string is like catalogsearch/result/?q=brother
Then the result should contain category and products.
The result should also be formatted like first we show the category then it's products that match the query string then second category and it's products and so on. If category match not found show only products and if product match is not found then show categories which match the query string.

What I want is the list view which contains both categories and products just like http://www.ldproducts.com/ldpsearch/result/?q=officejet+pro+8000

Goto this URL and scroll till the bottom and you'll see what I exactly want.

Googled a lot but was unable to find the desired answer. I am not supposed to use any extension or make any changes on the phtml file(until it's damn mandatory).

My magento version is 1.9.0.1

Best Answer

Interesting question, hence upvoting it. Below is the code for achieving the categories and the corresponding products as results. I have planned to reuse the FILTERS (Layered Navigation) concept to be able to display the categories and products as per the screenshots here:

enter image description here

enter image description here

enter image description here

These screenshots are currently not displaying the correct price, name etc. Hence this might need to be checked in the product collection that is present in the code written below. Hope you can figure out how to bring that and display it.

Step 1:

Create the below mentioned files.

  1. /app/code/local/Namespace/Modulename/Block/Catalog/Product/List.php
  2. /app/code/local/Namespace/Modulename/Block/CatalogSearch/Layer/Filter/Category.php
  3. /app/code/local/Namespace/Modulename/Block/CatalogSearch/Layer/State.php
  4. /app/code/local/Namespace/Modulename/Block/CatalogSearch/Layer.php
  5. /app/code/local/Namespace/Modulename/etc/config.xml
  6. /app/etc/modules/Namespace_Modulename.xml
  7. /app/design/frontend/rwd/default/layout/catalogsearch.xml
  8. /app/design/frontend/rwd/default/template/catalogsearch/ /app/design/frontend/rwd/default/template/catalogsearch/product/list.phtml /app/design/frontend/rwd/default/template/catalogsearch/layer/filter.phtml /app/design/frontend/rwd/default/template/catalogsearch/layer/state.phtml /app/design/frontend/rwd/default/template/catalogsearch/layer/view.phtml

See each file content as below:

1. /app/code/local/Namespace/Modulename/Block/Catalog/Product/List.php

<?php
class Namespace_Modulename_Block_Catalog_Product_List extends Mage_Catalog_Block_Product_List
{
    /**
     * return the search results with the category filters added
     * 
     * @return Mage_Catalog_Model_Resource_Product_Collection|false
     */
    public function getCategoryProductSearchResults()
    {
        $productCollection = $this->getData('product_collection');
        return $productCollection;
    }
}

2. /app/code/local/Namespace/Modulename/Block/CatalogSearch/Layer/Filter/Category.php

<?php
/**
 * reuse existing magento default category filters
 * in order to display the categories and the corresponding products
 */
class Namespace_Modulename_Block_CatalogSearch_Layer_Filter_Category extends Mage_Catalog_Block_Layer_Filter_Category
{
    public $_filter;

    /**
     * Initialize filter template
     *
     */
    public function __construct()
    {
        parent::__construct();
        $this->setTemplate('catalogsearch/layer/filter.phtml');
    }

    /**
     * set product collection based on the search term
     * add to the block and render the block HTML
     * 
     * @param Mage_Catalog_Model_Category $_item
     * @param string $count
     * 
     * @return HTML
     */
    public function getCategoryProductSearchResultHtml($_item, $count)
    {
        $block = '';

        //form the search query once again based on the keyword to get the complete product collection
        $term = Mage::helper('catalogsearch')->getQueryText();
        $query = Mage::getModel('catalogsearch/query')->setQueryText($term)->prepare();

        Mage::getResourceModel('catalogsearch/fulltext')->prepareResult(
            Mage::getModel('catalogsearch/fulltext'),
            $term,
            $query
        );

        //get complete product collection based on the search term
        $collection = Mage::getResourceModel('catalog/product_collection');
        $collection->getSelect()->joinInner(
            array('search_result' => $collection->getTable('catalogsearch/result')),
            $collection->getConnection()->quoteInto(
                'search_result.product_id=e.entity_id AND search_result.query_id=?',
                $query->getId()
            ),
            array('relevance' => 'relevance')
        );

        //add category filter for each category
        $categoryId = $this->getCategoryId($_item);        
        $collection->joinField('category_id','catalog/category_product','category_id','product_id=entity_id',null,'left')
                    ->addAttributeToFilter('category_id', array('in' => $categoryId));


        Mage::getSingleton('catalog/product_status')->addVisibleFilterToCollection($collection);
        Mage::getSingleton('catalog/product_visibility')->addVisibleInSearchFilterToCollection($collection);       

        //$productIds = array();
        //$productIds = $collection->getAllIds();
        //Zend_Debug::dump($productIds);
        //$this->_productCollection = $collection;
        //$collection->printLogQuery(true);

        if(count($collection))
        {
            $block = $this->getLayout()->createBlock('namespace_modulename/catalog_product_list')  
                ->setProductCollection($collection)
                ->setTemplate('catalogsearch/product/list.phtml');

            return $block->toHtml(); 
        }
    }

    /**
     * get the category id based on the name/label of the category filter
     * 
     * @param Mage_Catalog_Model_Category $_item
     * @return string
     */
    public function getCategoryId($_item)
    {
        $categoryCollection = Mage::getModel('catalog/category')->getCollection()
                ->addAttributeToSelect('*')
                ->addAttributeToFilter('name', trim($_item->getLabel()))
                ;

        foreach ($categoryCollection as $data)
        {
            return $data->getId();
        }
    }
}

3. /app/code/local/Namespace/Modulename/Block/CatalogSearch/Layer/State.php

<?php
/**
 * reuse the concept of state_renderers from default magento
 * change the template for search results page only
 */
class Namespace_Modulename_Block_CatalogSearch_Layer_State extends Mage_Catalog_Block_Layer_State
{
    /**
     * Initialize Layer State template
     *
     */
    public function __construct()
    {
        parent::__construct();
        $this->setTemplate('catalogsearch/layer/state.phtml');
    }
}

4. /app/code/local/Namespace/Modulename/Block/CatalogSearch/Layer.php

<?php
/**
 * reuse the default magento layer concept
 * add a new block if its a search page with new template
 * load the existing block to display the HTML
 */
class Namespace_Modulename_Block_CatalogSearch_Layer extends Mage_CatalogSearch_Block_Layer
{
    protected $_searchStateBlockName;

    protected $_searchFilterHtmlBlockName;

    /**
     * Initialize blocks names
     */
    protected function _initBlocks()
    {
        parent::_initBlocks();

        $route = Mage::app()->getRequest()->getRouteName();
        if ( $route == 'catalogsearch' )
        {
            $this->_searchStateBlockName = 'namespace_modulename/catalogsearch_layer_state';
            $this->_searchFilterHtmlBlockName = 'namespace_modulename/catalogsearch_layer_filter_category';
        }
    }

    /**
     * Get search categories layered navigation state html
     *
     * @return string
     */
    public function getSearchStateHtml()
    {                
        if ( $this->_searchStateBlockName != NULL )
        {
            $searchStateBlock = $this->getLayout()->createBlock($this->_searchStateBlockName)->setLayer($this->getLayer());
            $this->setChild('search_layer_state', $searchStateBlock);
        }

        return $this->getChildHtml('search_layer_state');
    }

    /**
     * return the manipulated filter html from the PHTML for search
     * 
     * @param Mage_Catalog_Model_Layer_Filter_Abstract|false $_filter
     * @return HTML
     */
    public function getSearchFilterHtml($_filter)
    {
        if ( $this->_searchStateBlockName != NULL )
        {
            $block = $this->getLayout()->createBlock($this->_searchFilterHtmlBlockName)
                    ->setLayer($this->getLayer())
                    ;
            $block->_filter = $_filter;

            return $block->toHtml();
        }
    }
}

5. /app/code/local/Namespace/Modulename/etc/config.xml

<?xml version="1.0" encoding="UTF-8"?>
<config>
    <modules>
        <Namespace_Modulename>
            <version>1.0.0</version>
        </Namespace_Modulename>
    </modules>
    <global>
        <blocks>
            <namespace_modulename>
                <class>Namespace_Modulename_Block</class>
            </namespace_modulename>
            <catalog>
                <rewrite>
                    <product_list>Namespace_Modulename_Block_Catalog_Product_List</product_list>
                </rewrite>
            </catalog>
            <catalogsearch>
                <rewrite>
                    <layer>Namespace_Modulename_Block_CatalogSearch_Layer</layer>
                </rewrite>
            </catalogsearch>
        </blocks>
        <helpers>
            <namespace_modulename>
                <class>Namespace_Modulename_Helper</class>
            </namespace_modulename>
        </helpers>
    </global>
</config>

6. /app/etc/modules/Namespace_Modulename.xml

I am sure you can create this :-)

7. /app/design/frontend/rwd/default/layout/catalogsearch.xml

...
    <catalogsearch_result_index translate="label">
        <label>Quick Search Form</label>
        <reference name="root">
            <action method="setTemplate"><template>page/3columns.phtml</template></action>
        </reference>
        <reference name="left_first">
            <block type="catalogsearch/layer" name="catalogsearch.leftnav" after="currency" template="catalog/layer/view.phtml">
                <block type="core/text_list" name="catalog.leftnav.state.renderers" as="state_renderers" />
            </block>
        </reference>
        <reference name="content">
            <block type="catalogsearch/result" name="search.result" template="catalogsearch/result.phtml">
                <!--block type="catalog/product_list" name="search_result_list" template="catalog/product/list.phtml"-->
                <block type="namespace_modulename/catalog_product_list" name="search_result_list" template="catalogsearch/product/list.phtml">
                    <block type="namespace_modulename/catalogsearch_layer" name="modulename.catalogsearch.leftnav" after="currency" template="catalogsearch/layer/view.phtml">
                        <block type="core/text_list" name="catalog.leftnav.state.renderers" as="state_renderers" />
                    </block>
                </block>
            </block>
        </reference>
    </catalogsearch_result_index>
...

/app/design/frontend/rwd/default/template/catalogsearch/product/list.phtml

<?php echo $this->getChildHtml('modulename.catalogsearch.leftnav'); ?>
<?php
    $_productCollection=$this->getCategoryProductSearchResults();

    if (count($_productCollection))
    {
        $_helper = $this->helper('catalog/output');
?>
    <div class="category-products">

        <?php // Grid Mode ?>
        <?php $_collectionSize = $_productCollection->count() ?>
        <?php $_columnCount = $this->getColumnCount(); ?>
        <ul class="products-grid products-grid--max-<?php echo $_columnCount; ?>-col">
            <?php $i=0; foreach ($_productCollection as $_product): ?>
                <?php /*if ($i++%$_columnCount==0): ?>
                <?php endif*/ ?>
                <li class="item<?php if(($i-1)%$_columnCount==0): ?> first<?php elseif($i%$_columnCount==0): ?> last<?php endif; ?>">
                    <a href="<?php echo $_product->getProductUrl() ?>" title="<?php echo $this->stripTags($this->getImageLabel($_product, 'small_image'), null, true) ?>" class="product-image">
                        <?php $_imgSize = 210; ?>
                        <img id="product-collection-image-<?php echo $_product->getId(); ?>"
                             src="<?php echo $this->helper('catalog/image')->init($_product, 'small_image')->resize($_imgSize); ?>"
                             alt="<?php echo $this->stripTags($this->getImageLabel($_product, 'small_image'), null, true) ?>" />
                    </a>
                    <div class="product-info">
                        <h2 class="product-name"><a href="<?php echo $_product->getProductUrl() ?>" title="<?php echo $this->stripTags($_product->getName(), null, true) ?>"><?php echo $_helper->productAttribute($_product, $_product->getName(), 'name') ?></a></h2>
                        <?php
                        // Provides extra blocks on which to hang some features for products in the list
                        // Features providing UI elements targeting this block will display directly below the product name
                        if ($this->getChild('name.after')) {
                            $_nameAfterChildren = $this->getChild('name.after')->getSortedChildren();
                            foreach ($_nameAfterChildren as $_nameAfterChildName) {
                                $_nameAfterChild = $this->getChild('name.after')->getChild($_nameAfterChildName);
                                $_nameAfterChild->setProduct($_product);
                                echo $_nameAfterChild->toHtml();
                            }
                        }
                        ?>
                        <?php echo $this->getPriceHtml($_product, true) ?>
                        <?php if($_product->getRatingSummary()): ?>
                        <?php echo $this->getReviewsSummaryHtml($_product, 'short') ?>
                        <?php endif; ?>
                        <div class="actions">
                            <?php if(!$_product->canConfigure() && $_product->isSaleable()): ?>
                                <button type="button" title="<?php echo $this->quoteEscape($this->__('Add to Cart')) ?>" class="button btn-cart" onclick="setLocation('<?php echo $this->getAddToCartUrl($_product) ?>')"><span><span><?php echo $this->__('Add to Cart') ?></span></span></button>
                            <?php elseif($_product->getStockItem() && $_product->getStockItem()->getIsInStock()): ?>
                                <a title="<?php echo $this->quoteEscape($this->__('View Details')) ?>" class="button" href="<?php echo $_product->getProductUrl() ?>"><?php echo $this->__('View Details') ?></a>
                            <?php else: ?>
                                <p class="availability out-of-stock"><span><?php echo $this->__('Out of stock') ?></span></p>
                            <?php endif; ?>
                            <ul class="add-to-links">
                                <?php if ($this->helper('wishlist')->isAllow()) : ?>
                                    <li><a href="<?php echo $this->helper('wishlist')->getAddUrl($_product) ?>" class="link-wishlist"><?php echo $this->__('Add to Wishlist') ?></a></li>
                                <?php endif; ?>
                                <?php if($_compareUrl=$this->getAddToCompareUrl($_product)): ?>
                                    <li><span class="separator">|</span> <a href="<?php echo $_compareUrl ?>" class="link-compare"><?php echo $this->__('Add to Compare') ?></a></li>
                                <?php endif; ?>
                            </ul>
                        </div>

                        <?php
                        // Provides extra blocks on which to hang some features for products in the list
                        // Features providing UI elements targeting this block will display directly below the product name
                        if ($this->getChild('price.cart.after')) {
                            $_priceCartAfterChildren = $this->getChild('price.cart.after')->getSortedChildren();
                            foreach ($_priceCartAfterChildren as $_priceCartAfterChildName) {
                                $_priceCartAfterChild = $this->getChild('price.cart.after')->getChild($_priceCartAfterChildName);
                                $_priceCartAfterChild->setProduct($_product);
                                echo $_priceCartAfterChild->toHtml();
                            }
                        }
                        ?>
                    </div>
                </li>
                <?php /*if ($i%$_columnCount==0 || $i==$_collectionSize): ?>
                <?php endif*/ ?>
            <?php endforeach ?>
        </ul>
        <script type="text/javascript">decorateGeneric($$('ul.products-grid'), ['odd','even','first','last'])</script>
    </div>
    <?php
    // Provides a block where additional page components may be attached, primarily good for in-page JavaScript
    if ($this->getChild('after')) {
        $_afterChildren = $this->getChild('after')->getSortedChildren();
        foreach ($_afterChildren as $_afterChildName) {
            $_afterChild = $this->getChild('after')->getChild($_afterChildName);
            //set product collection on after blocks
            $_afterChild->setProductCollection($_productCollection);
            echo $_afterChild->toHtml();
        }
    }
}
?>

/app/design/frontend/rwd/default/template/catalogsearch/layer/filter.phtml

<?php
/**
 * Magento
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Academic Free License (AFL 3.0)
 * that is bundled with this package in the file LICENSE_AFL.txt.
 * It is also available through the world-wide-web at this URL:
 * http://opensource.org/licenses/afl-3.0.php
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@magento.com so we can send you a copy immediately.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade Magento to newer
 * versions in the future. If you wish to customize Magento for your
 * needs please refer to http://www.magento.com for more information.
 *
 * @category    design
 * @package     rwd_default
 * @copyright   Copyright (c) 2006-2015 X.commerce, Inc. (http://www.magento.com)
 * @license     http://opensource.org/licenses/afl-3.0.php  Academic Free License (AFL 3.0)
 */
?>
<?php
/**
 * Template for filter items block
 *
 * @see Mage_Catalog_Block_Layer_Filter
 */
?>

<ol>
     <?php $count = 1; ?>
<?php foreach ($this->getItems() as $_item): ?>
    <li>
        <?php if ($_item->getCount() > 0): ?>
            <?php /*<a href="<?php echo $this->urlEscape($_item->getUrl()) ?>">*/ ?>
                <?php echo $_item->getLabel() ?>
                <?php if ($this->shouldDisplayProductCount()): ?>
                <span class="count">(<?php echo $_item->getCount() ?>)</span>
                <?php endif; ?>
                <?php echo $this->getCategoryProductSearchResultHtml($_item, $count); ?>
            <?php /*</a>*/ ?>
        <?php else: ?>
            <span>
                <?php echo $_item->getLabel(); ?>
                <?php if ($this->shouldDisplayProductCount()): ?>
                    <span class="count">(<?php echo $_item->getCount() ?>)</span>
                <?php endif; ?>
            </span>
        <?php endif; ?>
    </li>
<?php endforeach ?>
</ol>

/app/design/frontend/rwd/default/template/catalogsearch/layer/state.phtml

This file MAY NOT be needed. I have kept it to just make sure the reusing of filters works correctly or not.

<?php
/**
 * Magento
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Academic Free License (AFL 3.0)
 * that is bundled with this package in the file LICENSE_AFL.txt.
 * It is also available through the world-wide-web at this URL:
 * http://opensource.org/licenses/afl-3.0.php
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@magento.com so we can send you a copy immediately.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade Magento to newer
 * versions in the future. If you wish to customize Magento for your
 * needs please refer to http://www.magento.com for more information.
 *
 * @category    design
 * @package     rwd_default
 * @copyright   Copyright (c) 2006-2015 X.commerce, Inc. (http://www.magento.com)
 * @license     http://opensource.org/licenses/afl-3.0.php  Academic Free License (AFL 3.0)
 */
?>
<?php
/**
 * Category layered navigation state
 *
 * @see Mage_Catalog_Block_Layer_State
 */
?>
<?php
$_filters = $this->getActiveFilters();
$_renderers = $this->getParentBlock()->getChild('state_renderers')->getSortedChildren();
?>
<?php if(!empty($_filters)): ?>
<div class="currently">
    <ol>
    <?php foreach ($_filters as $_filter): ?>
        <?php
        $_rendered = false;
        foreach ($_renderers as $_rendererName):
            $_renderer = $this->getParentBlock()->getChild('state_renderers')->getChild($_rendererName);
            if (method_exists($_renderer, 'shouldRender') && $_renderer->shouldRender($_filter)):
                $_renderer->setFilter($_filter);
                echo $_renderer->toHtml();
                $_rendered = true;
                break;
            endif;
        endforeach;

        if (!$_rendered):
        ?>
        <li>
            <?php
                $clearLinkUrl = $_filter->getClearLinkUrl();
                if ($clearLinkUrl):
            ?>
                <a  class="btn-previous" href="<?php echo $_filter->getRemoveUrl() ?>" title="<?php echo Mage::helper('core')->quoteEscape($this->__('Previous')) ?>"><?php echo $this->__('Previous') ?></a>
                <a  class="btn-remove" title="<?php echo $this->escapeHtml($_filter->getFilter()->getClearLinkText()) ?>" href="<?php echo $clearLinkUrl ?>"><?php echo $this->escapeHtml($_filter->getFilter()->getClearLinkText()) ?></a>
            <?php else: ?>
                <a  class="btn-remove" href="<?php echo $_filter->getRemoveUrl() ?>" title="<?php echo Mage::helper('core')->quoteEscape($this->__('Remove This Item')) ?>"><?php echo $this->__('Remove This Item') ?></a>
            <?php endif; ?>
            <span class="label"><?php echo $this->__($_filter->getName()) ?>:</span> <span class="value"><?php echo $this->stripTags($_filter->getLabel()) ?></span>
        </li>
        <?php endif; ?>
    <?php endforeach; ?>
    </ol>
</div>
<?php endif; ?>

/app/design/frontend/rwd/default/template/catalogsearch/layer/view.phtml

<?php
/**
 * Magento
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Academic Free License (AFL 3.0)
 * that is bundled with this package in the file LICENSE_AFL.txt.
 * It is also available through the world-wide-web at this URL:
 * http://opensource.org/licenses/afl-3.0.php
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@magento.com so we can send you a copy immediately.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade Magento to newer
 * versions in the future. If you wish to customize Magento for your
 * needs please refer to http://www.magento.com for more information.
 *
 * @category    design
 * @package     rwd_default
 * @copyright   Copyright (c) 2006-2015 X.commerce, Inc. (http://www.magento.com)
 * @license     http://opensource.org/licenses/afl-3.0.php  Academic Free License (AFL 3.0)
 */
?>
<?php
/**
 * Category layered navigation
 *
 * @see Mage_Catalog_Block_Layer_View
 */
?>
<?php if($this->canShowBlock()): ?>
<div class="block block-layered-nav<?php if (!$this->getLayer()->getState()->getFilters()): ?> block-layered-nav--no-filters<?php endif; ?>">
    <?php /*<div class="block-title">
        <strong><span><?php echo $this->__('Shop By') ?></span></strong>
    </div>*/ ?>
    <!--div class="block-content toggle-content"-->
        <?php echo $this->getSearchStateHtml() ?>
        <?php /*if ($this->getLayer()->getState()->getFilters()): ?>
            <div class="actions"><a href="<?php echo $this->getClearUrl() ?>"><?php echo $this->__('Clear All') ?></a></div>
        <?php endif;*/ ?>
        <?php if($this->canShowOptions()): ?>
            <!--p class="block-subtitle block-subtitle--filter"><?php //echo $this->__('Filter') ?></p>
            <dl id="narrow-by-list"-->
                <?php $_filters = $this->getFilters() ?>
                <?php foreach ($_filters as $_filter): ?>
                <?php if($_filter->getItemsCount()): ?>
                    <dd><?php echo $this->getSearchFilterHtml($_filter); ?></dd>
                <?php endif; ?>
                <?php endforeach; ?>
            <!--/dl-->
            <script type="text/javascript">decorateDataList('narrow-by-list')</script>
        <?php endif; ?>
    <!--/div-->
</div>
<?php endif; ?>

DISCLAIMER: I was able to work on this only to produce relevant results how the question was described above. To be able to display categories and products based on the search term. I am not sure if this will work to fetch only categories or only products. This needs to be checked based on our discussion in the chat.

As a note: currently the left navigation on the search page is not working as expected based on the code I have done. That also might need to be checked.

Hope this helps you and someone.

Happy Coding...