0
\$\begingroup\$

My problem is, that I want to take a search string, and split it into keywords.

I have a function

function search(array $keywords) 

that generates an sql along these lines (for the record it handles sql injection, this is just an example)

select * from table where 
col1 like '%{$keywords[0]}%' or col1 like '%{$keywords[1]}%' 
or col2 like '%{$keywords[0]}%' or col2 like '%{$keywords[1]}%'

That part works fine

The code that I would like reviewed, is how I split the search string into keywords.

This code is only run once per page, so optimisation is not a priority, the aim was simple and easily understandable, as it won't get touched often.

As a side request, if you can point me down the right track on how to match on accented chars if the user only types plain chars that would be helpful. Eg I would like keyword "Maori" to match on text "Māori"

Note: The backend database is sqlanywhere and we do not have fulltext search available.

This is the code I came up with

public static function splitSearchPatterns($pattern) {

            $keywords = [];

            // extract any quoted search terms
            if (preg_match_all('/"(.+?)"/', $pattern, $matches)) {

                // for each match, extract the replace the match in the pattern, so we are left without quoted text
                $num_matches = count($matches);
                for ($i=1; $i<$num_matches; $i++) {
                    foreach ($matches[$i] as $match) {
                        $keywords[] = $match;
                        $pattern = str_replace("\"{$match}\"", '', $pattern);
                    }
                }
            }

            // replace multiple whitespace with single space
            $pattern = preg_replace('/\s+/', ' ', $pattern);

            // split on whitespaces
            $keywords = array_merge($keywords, explode(' ', $pattern));

            // remove all empty values
            $keywords = array_filter($keywords);

            // remove all single char values
            $keywords = array_filter($keywords, function($val) {
                return mb_strlen($val) > 1;
            });

            // no point matching on the same thing twice
            $keywords = array_unique($keywords);

            // reset indexes after array merge
            return array_values($keywords);
        }

These are some unit tests I wrote to go with it

class TextUtilsTest extends TestCase {

    public function testSplitSearchPatternsNull() {
        $original = null;
        $expected = [];
        $actual = TextUtils::splitSearchPatterns($original);

        $this->assertEquals($expected, $actual);
    }

    public function testSplitSearchPatternsBlank() {
        $original = '';
        $expected = [];
        $actual = TextUtils::splitSearchPatterns($original);

        $this->assertEquals($expected, $actual);
    }

    public function testSplitSearchPatternsMultipleSpaces() {
        $original = '     ';
        $expected = [];
        $actual = TextUtils::splitSearchPatterns($original);

        $this->assertEquals($expected, $actual);
    }

    public function testSplitSearchPatternsSingleWord() {
        $original = 'search';
        $expected = ['search'];
        $actual = TextUtils::splitSearchPatterns($original);

        $this->assertEquals($expected, $actual);
    }

    public function testSplitSearchPatternsMultipleWords() {
        $original = 'search text';
        $expected = ['search', 'text'];
        $actual = TextUtils::splitSearchPatterns($original);

        $this->assertEquals($expected, $actual);
    }

    public function testSplitSearchPatternsMultipleRepeatingWords() {
        $original = 'search text search text';
        $expected = ['search', 'text'];
        $actual = TextUtils::splitSearchPatterns($original);

        $this->assertEquals($expected, $actual);
    }

    public function testSplitSearchPatternsQuotedText() {
        $original = 'search "text search" text';
        $expected = ['text search', 'search', 'text'];
        $actual = TextUtils::splitSearchPatterns($original);

        $this->assertEquals($expected, $actual);
    }

    public function testSplitSearchPatternsQuotedTextExtraQuote() {
        $original = 'search "text search" text"';
        $expected = ['text search', 'search', 'text"'];
        $actual = TextUtils::splitSearchPatterns($original);

        $this->assertEquals($expected, $actual);
    }

    public function testSplitSearchPatternsMultipleQuotedText() {
        $original = 'search "text search" text "quoted text"';
        $expected = ['text search', 'quoted text', 'search', 'text'];
        $actual = TextUtils::splitSearchPatterns($original);

        $this->assertEquals($expected, $actual);
    }

    public function testSplitSearchPatternsMultipleSingleCharText() {
        $original = 'this is a short pattern';
        $expected = ['this', 'is', 'short', 'pattern'];
        $actual = TextUtils::splitSearchPatterns($original);

        $this->assertEquals($expected, $actual);
    }

}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.