'Search in the local database for torrents matching a specific query. This method also assigns a relevance
score to each torrent, based on the name, files and file extensions.
The algorithm is based on BM25. The document length factor is regarded since our "documents" are very small
(often a few keywords).
See https://en.wikipedia.org/wiki/Okapi_BM25 for more information about BM25.'

def search_in_local_torrents_db(self, query, keys=None):

    search_results = []
    keys_str = ',    '.join(keys)
    keywords = split_into_keywords(query, to_filter_stopwords=True)
    infohash_index = keys.index('infohash')
    results = self._db.fetchall(("SELECT    DISTINCT    %s,    Matchinfo(FullTextIndex,    'pcnalx')    FROM    Torrent    T,    FullTextIndex    LEFT    OUTER    JOIN    _ChannelTorrents    C    ON    T.torrent_id    =    C.torrent_id    WHERE    t.name    IS    NOT    NULL    AND    t.torrent_id    =    FullTextIndex.rowid    AND    C.deleted_at    IS    NULL    AND    FullTextIndex    MATCH    ?" % keys_str), ('    OR    '.join(keywords),))
    for result in results:
        result = list(result)
        result[infohash_index] = str2bin(result[infohash_index])
        matchinfo = result[len(keys)]
        self.latest_matchinfo_torrent = (matchinfo, keywords)
        (num_phrases, num_cols, num_rows) = unpack_from('III', matchinfo)
        unpack_str = ('I' * ((3 * num_cols) * num_phrases))
        matchinfo = unpack_from((('I' * 9) + unpack_str), matchinfo)[9:]
        scores = []
        for col_ind in xrange(num_cols):
            score = 0
            for phrase_ind in xrange(num_phrases):
                base_term_offset = (3 * (col_ind + (phrase_ind * num_cols)))
                rows_with_term = matchinfo[(base_term_offset + 2)]
                term_freq = matchinfo[base_term_offset]
                inv_doc_freq = math.log((((num_rows - rows_with_term) + 0.5) / (rows_with_term + 0.5)), 2)
                right_side = ((term_freq * (1.2 + 1)) / (term_freq + 1.2))
                score += (inv_doc_freq * right_side)
            scores.append(score)
        extended_result = (result + [(((0.8 * scores[0]) + (0.1 * scores[1])) + (0.1 * scores[2]))])
        search_results.append(extended_result)
    return search_results
