Laravel: fulltext selection and ordering

Standard

Yesterday I was looking for a way to do a FULLTEXT select using Laravel. It was not so easy. In this article I’m going to explain how to a FULLTEXT select and to order by this selection.

The migration

If you want to do a FULLTEXT search, you will need a FULLTEXT index on at least one column of your table. Warning: if you are using InnoDB as your table’s engine, you will need MySQL >= 5.6. If you are using MyISAM as your table’s engine, you are good to go for the index but you can’t use foreign keys.

I’m using InnoDB with MySQL 5.6, here is my code for the migration of the table.

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;

class CreateQuotesTable extends Migration {

    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::dropIfExists('quotes');

        Schema::create('quotes', function(Blueprint $table) {
            $table->engine = "InnoDB";
            $table->increments('id');
            $table->string('content', 500);
            $table->integer('user_id')->unsigned()->index();
            $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
            $table->tinyInteger('approved')->default(0);
            $table->timestamps();
        });

        // Here we create the FULLTEXT index
        DB::statement('ALTER TABLE quotes ADD FULLTEXT search(content)');
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        // Drop the index before dropping the table
        Schema::table('quotes', function($table) {
            $table->dropIndex('search');
        });
        Schema::drop('quotes');
    }

}

Nothing uncommon, note that you will have to use a DB::statement('ALTER TABLE quotes ADD FULLTEXT search(content)') to create the index.

Searching using the FULLTEXT index

Here it comes the fun part. Now that we have your index, let’s begin to use it. I want to get quotes based on a search on their content. I want pertinent results so I’ll advantage of the index.

My code is the following:

/**
 * @brief Function used to search for quotes using the FULLTEXT index on content
 *
 * @param  string $search Our search query
 * @return Collection Collection of Quote
 */
public static function searchQuotes($search)
{
    return Quote::
    select('id', 'content', 'user_id', 'approved', 'created_at', 'updated_at', DB::raw("MATCH(content) AGAINST(?) AS `rank`"))
    // $search will NOT be bind here
    // it will be bind when calling setBindings
    ->whereRaw("MATCH(content) AGAINST(?)", array($search))
    // I want to keep only published quotes
    ->where('approved', '=', 1)
    // Order by the rank column we got with our FULLTEXT index
    ->orderBy('rank', 'DESC')
    // Bind variables here
    // We really need to bind ALL variables here
    // question marks will be replaced in the query
    ->setBindings([$search, $search, 1])
    ->get();
}

I haven’t found a convenient way to select all columns from my table plus an additional one: the rank given by the FULLTEXT search. The tricky part here is really the binding. You need to bind all variables at the end of your query to make it work.

I’m not using the FULLTEXT search in BOOLEAN MODE here. If you need to do so, take a look at the official documentation: http://dev.mysql.com/doc/refman/5.0/en/fulltext-boolean.html. You will only need to add two strings to make it work.

Paginate posts correctly when they are random ordered

Standard

The problem

This is a common problem: you have entities in a category, you want to display them by pages because you have a lot of entities and you don’t want to have entities from page 1 in your page 2.

If you are using the ORDER BY RAND() function from MySQL, you will have a problem. MySQL splits up the data into pages of X posts each (paginates) and fails to include a new set of X posts on page 2 and so forth. In other words, because it is listing things in a random order, it just goes out and gets another X random posts. As a result, you will have some repeated posts instead of a new set of X random posts on page 2, etc.

The solution

Fortunately, there is a solution for this problem. You will be able to “remember” which random 10 posts were included on page 1, and then have a new set of 10 posts to put on pages 2, 3, etc. until all posts are displayed.

The MySQL RAND() function accepts a seed as an optional argument. Using a seed, it will return the same randomized result set each time. For example, if you want your posts to be random ordered, paginated with no repetition, you can write a query like this: SELECT * FROM posts ORDER BY RAND(42) to select posts.

If you do not want to have the same results for every user viewing the list, do not give an arbitrary value to the RAND function: generate a random number, store it in session and pass it to the MySQL RAND function when selecting posts.

You don’t write code for machines

Standard
double m[]= {7709179928849219.0, 771};
int main()
{
    m[1]--?m[0]*=2,main():printf((char*)m);    
}

You know what these lines print? They print C++Sucks.

Yes, really, you can give it a try if you want. If you want the explanation you can check this question on StackOverflow.

My point is that you don’t write code for machines. If you are happy when your code compiles or when it runs and prints what you expected, you are a fool. Of course it’s a success when your code does what you wanted to do, but this is the most basic thing you can expect from it.

Programming is difficult. Reading others people’s code is even more difficult. And yet you are going to do it everyday. So the next time you are going to write some code, or contribute to some code, keep in mind that your ultimate goal is not to make it work, but to write it in a way that should be understandable by other smart folks.