• Full-text searching using MATCH() ... AGAINST in xPDO#

  • goldsky Reply #1, 1 year, 3 months ago

    Reply
    I dug the xPDO docs and codes, and I couldn't find any reference that related to this searching type.
    I'm using a custom table.

    For now on, I use some 'deprecated' methods like this:
    <?php
            $queryString = "
                SELECT *, MATCH(title, url, author, category, level, version, tag, language, summary) AGAINST ('{$str}' IN BOOLEAN MODE) as score
                FROM {$this->modx->config['table_prefix']}linx
                WHERE MATCH(title, url, author, category, level, version, tag, language, summary) AGAINST ('{$str}' IN BOOLEAN MODE)
                ORDER BY score DESC
                ";
            $result = $this->modx->db->query($queryString);
    
            if (!$result) {
                echo __LINE__ . ' : #' . mysql_errno() . ' ' . mysql_error() . '<br />' . $queryString . '<br />';
                return FALSE;
            }
    
            $rowArray = $this->modx->db->makeArray($result);
    
    


    How to do it with xPDO?
    TIA.


  • opengeek Reply #2, 1 year, 3 months ago

    Reply
    NOTE: DO NOT use any mysql_ functions when working even with the deprecated DBAPI methods in Revolution—the DBAPI in Revolution uses xPDO itself already.

    You can pass any SQL-specific functions or statements, like MySQL fulltext query syntax, into any criteria, or you can simply do what you did using the PDO API, which again, is built into xPDO, and thus MODx. That said, there are still a lot of different ways to accomplish this with xPDO. Let's start with the one closest to how you are used to doing it:
    <?php
    $queryString = "
        SELECT *, MATCH(title, url, author, category, level, version, tag, language, summary) AGAINST (:str1 IN BOOLEAN MODE) as score
        FROM {$this->modx->getOption('table_prefix')}linx
        WHERE MATCH(title, url, author, category, level, version, tag, language, summary) AGAINST (:str2 IN BOOLEAN MODE)
        ORDER BY score DESC
        ";
    $criteria = new xPDOCriteria($this->modx, $queryString, array(':str1' => $str, ':str2' => $str));
    if (!$criteria->prepare() || !$criteria->stmt->execute()) {
        echo __LINE__ . ' : ' . print_r($this->modx->errorInfo(), true) . '<br />' . $queryString . '<br />';
        return FALSE;
    }
    $rowArray = $criteria->stmt->fetchAll(PDO::FETCH_ASSOC);
    


    However, if you created an xPDO model for your linx table, you could create the criteria programmatically via a gsLinx class, e.g.
    <?php
    $criteria = $this->modx->newQuery('gsLinx');
    $criteria->select($this->modx->getSelectColumns('gsLinx'));
    $criteria->select("MATCH(title, url, author, category, level, version, tag, language, summary) AGAINST (:str1 IN BOOLEAN MODE) AS score");
    $criteria->where("MATCH(title, url, author, category, level, version, tag, language, summary) AGAINST (:str2 IN BOOLEAN MODE)");
    $criteria->sortby('score', 'DESC');
    $stmt = $criteria->prepare();
    $stmt->bindParam(':str1', $str);
    $stmt->bindParam(':str2', $str);
    

    At this point, you have a PDOStatement (the $stmt variable) wrapped by an xPDOCriteria object (the $criteria variable); note that $stmt is a reference to $criteria->stmt which is automatically assigned when you call prepare. You can make use of either one at this point in any way you want. For instance, if you just wanted an array result set like you had before:
    <?php
    if ($criteria->stmt->execute()) {
        $rowArray = $criteria->stmt->fetchAll(PDO::FETCH_ASSOC);
    } else {
        var_dump($criteria->stmt->errorInfo());
    }
    

    Or if you want to work with your rows as objects:
    <?php
    $collection = $this->modx->getCollection('gsLinx', $criteria);
    $output = array();
    foreach ($collection as $object) {
        $output[]= $object->toArray();
    }
    return implode("<br />", $output);
    


  • goldsky Reply #3, 1 year, 3 months ago

    Reply
    TYVM for your answer, Jason.
    I agree 101% that I should keep away from mysql_ functions, but again, I was showing the dirty query to explain what I was trying to do.
    Your answer sheds me a light.
    Anyhow,
    I tried the first option, it works.
    While I want to use the second option to keep the consistency of all passing properties among methods, it throws this:
    [2011-02-18 12:47:23] (ERROR @ D:\localhost\[mywebsite.com]\core\xpdo\om\xpdoobject.class.php : 235) PHP warning: PDOStatement::execute() [<a href='pdostatement.execute'>pdostatement.execute</a>]: SQLSTATE[HY093]: Invalid parameter number: no parameters were bound
    [2011-02-18 12:47:23] (ERROR @ /[mywebsite.com]/www/index.php) Error HY093 executing statement:
    Array
    (
    [0] => HY093
    )
    What am I missing here?

    PS:
    There was a similar problem here: http://modxcms.com/forums/index.php?topic=60005.0


  • opengeek Reply #4, 1 year, 3 months ago

    Reply
    I updated the example to bind the parameters after the prepare()—that should resolve it if your model is properly created and included.


  • goldsky Reply #5, 1 year, 3 months ago

    Reply
    it doesn't work.
    strangely, this works:
    <?php
            if (!empty($str))
                $this->searchString = $str;
    
            ############################################################################
            # Modified of Jason's:
            # http://forums.modx.com/thread/31992/full-text-searching-using-match-against-in-xpdo#dis-post-172945
    
            ############################################################################
            $c = $this->modx->newQuery('Link');
            $c->select($this->modx->getSelectColumns('Link'));
            $fields = 'title, url, author, category, level, version, tag, language, summary';
            $c->select("MATCH ($fields) AGAINST ('$this->searchString' IN BOOLEAN MODE) AS score");
            $c->where("MATCH ($fields) AGAINST ('$this->searchString' IN BOOLEAN MODE)");
            $c->where("approved='1'");
    
            $this->_links['count'] = $this->modx->getCount('Link', $c);
            
            $c->sortby("score", "DESC");
    
            if (!empty($limit)) {
                $this->_links['limit'] = intval($limit);
            } else {
                $this->_links['limit'] = $this->config['limit'];
            }
    
            $c->limit($this->_links['limit'], $this->_links['limit'] * $this->config['p']);
    
            $links = $this->modx->getCollection('Link', $c);
    
            foreach ($links as $link) {
                // ... filling the placeholders here
    
            }
    


    Notice the single quotes that wrap $this->searchString PS:
    1. The getCount() was retrieved BEFORE the limit and sortby (You only mentioned about limit on the Doc).
    2. This $this->searchString property will be used again for the pagination method... as below
           
    <?php
            if (empty($this->searchString))
                return FALSE;
            $pages = array();
    
            if (!isset($this->_links['count'])) {
                $c = $this->modx->newQuery('Link');
                $c->select('COUNT(DISTINCT id)');
                $c->select("MATCH(title, url, author, category, level, version, tag, language, summary) AGAINST (:str1 IN BOOLEAN MODE) AS score");
                $c->where("MATCH(title, url, author, category, level, version, tag, language, summary) AGAINST (:str2 IN BOOLEAN MODE)");
                $c->where("approved='1'");
                $c->sortby("score", "DESC");
    
                $stmt = $c->prepare(array(':str1' => $this->searchString, ':str2' => $this->searchString));
    
                $this->_links['count'] = 0;
                if ($c->stmt->execute()) {
                    $rows = $c->stmt->fetchAll(PDO::FETCH_COLUMN);
                    $this->_links['count'] = (integer) reset($rows);
                }
            }
    
            if ($this->_links['count'] <= $this->_links['limit'])
                return FALSE;
    

    This pagination uses array, not object.


  • opengeek Reply #6, 1 year, 3 months ago

    Reply
    These things I exemplified (these were obviously not working, tested examples, since I don't have access to your model or know the code involved until you reveal it) work fine in various implementations in my custom xPDO models, including MODX. I'm not sure what you are claiming doesn't work, but ok...


  • Lucas Reply #7, 4 months ago

    Reply
    Just wanted to say that I found this post really useful - saved me plenty of headaches
    A quick question with regards to this:
    $criteria = $this->modx->newQuery('gsLinx');
    $criteria->select($this->modx->getSelectColumns('gsLinx'));
    $criteria->select("MATCH(title, etc...) AGAINST (:str1 IN BOOLEAN MODE) AS score");
    $criteria->where("MATCH(title, etc...) AGAINST (:str2)");
    $criteria->sortby('score', 'DESC');
    $stmt = $criteria->prepare();
    $stmt->bindParam(':str1', $str);
    $stmt->bindParam(':str2', $str);
    
    $collection = $this->modx->getCollection('gsLinx', $criteria);
    


    I couldn't get it to work using $stmt->bindParam(); - I didn't understand how $stmt fits in with the getCollection call, so ended up doing this instead (simplified to get the idea across):
    $c = $modx->newQuery('myPackage');
    $c->select($modx->getSelectColumns('myPackage'));
    $c->where("MATCH (col_1, col_2) AGAINST ('$query')");
    
    $collection = $modx->getCollection('myPackage', $c);
    

    Do I miss out on any caching or any advantages by not using bindParam?