Sqlite с реальным «Полнотекстовым поиском» и орфографическими ошибками (FTS + spellfix вместе) - PullRequest
0 голосов
/ 14 октября 2018

Допустим, у нас есть 1 миллион строк, подобных этой:

import sqlite3
db = sqlite3.connect(':memory:')
c = db.cursor()
c.execute('CREATE TABLE mytable (id integer, description text)')
c.execute('INSERT INTO mytable VALUES (1, "Riemann")')
c.execute('INSERT INTO mytable VALUES (2, "All the Carmichael numbers")')

Фон:

Я знаю, как это сделать с помощью Sqlite:

  • Найти строку с запросом из одного слова , до нескольких орфографических ошибок с модулем spellfix и расстоянием Левенштейна (я разместил подробный ответ здесь о том, как его скомпилировать, как его использовать, ...):

    db.enable_load_extension(True)
    db.load_extension('./spellfix')
    c.execute('SELECT * FROM mytable WHERE editdist3(description, "Riehmand") < 300'); print c.fetchall()
    
    #Query: 'Riehmand'
    #Answer: [(1, u'Riemann')]
    

    С 1М строками это будет очень медленно!Как подробно описано здесь , postgresql может иметь оптимизацию с использованием trigrams.Быстрое решение, доступное с Sqlite, заключается в использовании VIRTUAL TABLE USING spellfix:

    c.execute('CREATE VIRTUAL TABLE mytable3 USING spellfix1')
    c.execute('INSERT INTO mytable3(word) VALUES ("Riemann")')
    c.execute('SELECT * FROM mytable3 WHERE word MATCH "Riehmand"'); print c.fetchall()
    
    #Query: 'Riehmand'
    #Answer: [(u'Riemann', 1, 76, 0, 107, 7)], working!
    
  • Найти выражение с запросом, совпадающим с одним или несколькими словами с FTS («Полнотекстовый поиск»):

    c.execute('CREATE VIRTUAL TABLE mytable2 USING fts4(id integer, description text)')
    c.execute('INSERT INTO mytable2 VALUES (2, "All the Carmichael numbers")')
    c.execute('SELECT * FROM mytable2 WHERE description MATCH "NUMBERS carmichael"'); print c.fetchall()
    
    #Query: 'NUMBERS carmichael'
    #Answer: [(2, u'All the Carmichael numbers')]
    

    Он не учитывает регистр, и вы даже можете использовать запрос с двумя словами в неправильном порядке и т. Д .: FTS действительно очень мощный,Но недостатком является то, что каждое ключевое слово запроса должно быть правильно написано , т. Е. Только FTS не допускает орфографические ошибки.

Вопрос:

Как выполнить полнотекстовый поиск (FTS) с Sqlite , а также разрешить орфографические ошибки ? т.е. "FTS + spellfix" вместе

Пример:

  • строка в БД: "All the Carmichael numbers"
  • запрос: "NUMMBER carmickaeel" должен соответствовать этому!

Как это сделать с Sqlite?

Это возможно с Sqlite, поскольку эта страница сообщает:

Или его [spellfix] можно использовать с FTS4 для полнотекстового поискаиспользование слов с ошибками.

Связанный вопрос: Сходство строк с Python + Sqlite (расстояние Левенштейна / расстояние редактирования)

Ответы [ 2 ]

0 голосов
/ 23 октября 2018

Принятый ответ хорош (полная ему заслуга), но есть небольшое отклонение, которое, хотя и не так полно, как принятый для сложных случаев, полезно для понимания идеи:

import sqlite3
db = sqlite3.connect(':memory:')
db.enable_load_extension(True)
db.load_extension('./spellfix')
c = db.cursor()
c.execute("CREATE VIRTUAL TABLE mytable2 USING fts4(description text)")
c.execute("CREATE VIRTUAL TABLE mytable2_terms USING fts4aux(mytable2)")
c.execute("CREATE VIRTUAL TABLE mytable3 USING spellfix1")
c.execute("INSERT INTO mytable2 VALUES ('All the Carmichael numbers')")   # populate the table
c.execute("INSERT INTO mytable2 VALUES ('They are great')")
c.execute("INSERT INTO mytable2 VALUES ('Here some other numbers')")
c.execute("INSERT INTO mytable3(word) SELECT term FROM mytable2_terms WHERE col='*'")

def search(query):
    # Correcting each query term with spellfix table
    correctedquery = []
    for t in query.split():
        spellfix_query = "SELECT word FROM mytable3 WHERE word MATCH ? and top=1"
        c.execute(spellfix_query, (t,))
        r = c.fetchone()
        correctedquery.append(r[0] if r is not None else t)  # correct the word if any match in the spellfix table; if no match, keep the word spelled as it is (then the search will give no result!)

    correctedquery = ' '.join(correctedquery)

    # Now do the FTS
    fts_query = 'SELECT * FROM mytable2 WHERE description MATCH ?'
    c.execute(fts_query, (correctedquery,))
    return {'result': c.fetchall(), 'correctedquery': correctedquery, 'query': query}

print(search('NUMBBERS carmickaeel'))
print(search('some HERE'))
print(search('some qsdhiuhsd'))

Вот результат:

{'query': 'NUMBBERS carmickaeel', 'correctedquery': u'numbers carmichael ',' result ': [(u' Все числа Кармайкла ',)]}
{'query': 'some ЗДЕСЬ', 'correctedquery': u'some here ',' result ': [(u'Here some other numbers',)]}
{'query': 'некоторые qsdhiuhsd ',' correctedquery ': u'some qsdhiuhsd', 'result': []}

Примечание: Можно отметить, что "Исправление каждого термина запроса с таблицей заклинаний" часть выполняется с одним запросом SQL на срок.Производительность этого по сравнению с одним SQL-запросом UNION изучается здесь .

0 голосов
/ 17 октября 2018

Документация spellfix1 фактически говорит вам, как это сделать.Из раздела Обзор :

Если вы намереваетесь использовать эту виртуальную таблицу вместе с таблицей FTS4 (для исправления орфографии поисковых терминов), то выможет извлечь словарь, используя таблицу fts4aux :

INSERT INTO demo(word) SELECT term FROM search_aux WHERE col='*';

Оператор SELECT term from search_aux WHERE col='*' извлекает все индексированные токены .

Соединяя это с вашими примерами, где mytable2 - это ваша виртуальная таблица fts4, вы можете создать таблицу fts4aux и вставить эти токены в таблицу mytable3 spellfix1 с помощью:

CREATE VIRTUAL TABLE mytable2_terms USING fts4aux(mytable2);
INSERT INTO mytable3(word) SELECT term FROM mytable2_terms WHERE col='*';

Возможно, вы захотитедалее уточните этот запрос, чтобы пропустить любые термины, уже вставленные в spellfix1, в противном случае вы получите двойные записи:

INSERT INTO mytable3(word)
    SELECT term FROM mytable2_terms
    WHERE col='*' AND 
        term not in (SELECT word from mytable3_vocab);

Теперь вы можете использовать mytable3, чтобы отобразить слова с ошибками в исправленные токены, а затем использовать эти исправленные токены.в запросе MATCH againsts mytable2.

В зависимости от ваших потребностей это может означать, что вам нужно выполнить собственную обработку токена и построение запроса;нет синтаксического синтаксического анализатора запроса fts4.Таким образом, ваша строка поиска с двумя токенами должна быть разделена, каждый токен проходит через таблицу spellfix1 для сопоставления с существующими токенами, а затем эти токены передаются в запрос fts4.

Игнорирование синтаксиса SQL для обработки этогоИспользование Python для разделения достаточно просто:

def spellcheck_terms(conn, terms):
    cursor = conn.cursor()
    base_spellfix = """
        SELECT :term{0} as term, word FROM spellfix1data
        WHERE word MATCH :term{0} and top=1
    """
    terms = terms.split()
    params = {"term{}".format(i): t for i, t in enumerate(terms, 1)}
    query = " UNION ".join([
        base_spellfix.format(i + 1) for i in range(len(params))])
    cursor.execute(query, params)
    correction_map = dict(cursor)
    return " ".join([correction_map.get(t, t) for t in terms])

def spellchecked_search(conn, terms):
    corrected_terms = spellcheck_terms(conn, terms)
    cursor = conn.cursor()
    fts_query = 'SELECT * FROM mytable2 WHERE mytable2 MATCH ?'
    cursor.execute(fts_query, (corrected_terms,))
    return cursor.fetchall()

Затем возвращается [('All the Carmichael numbers',)] для spellchecked_search(db, "NUMMBER carmickaeel").

Сохранение обработки проверки орфографии в Python позволяет затем поддерживать более сложныеЗапросы FTS по мере необходимости;вам, возможно, придется переопределить синтаксический анализатор выражений , чтобы сделать это, но, по крайней мере, Python предоставляет вам инструменты для этого.

Полный пример, упаковывающий вышеупомянутый подход в классе,которые просто извлекают термины в виде буквенно-цифровых символьных последовательностей (чего достаточно, читая спецификации синтаксиса выражений):

import re
import sqlite3
import sys

class FTS4SpellfixSearch(object):
    def __init__(self, conn, spellfix1_path):
        self.conn = conn
        self.conn.enable_load_extension(True)
        self.conn.load_extension(spellfix1_path)

    def create_schema(self):
        self.conn.executescript(
            """
            CREATE VIRTUAL TABLE IF NOT EXISTS fts4data
                USING fts4(description text);
            CREATE VIRTUAL TABLE IF NOT EXISTS fts4data_terms
                USING fts4aux(fts4data);
            CREATE VIRTUAL TABLE IF NOT EXISTS spellfix1data
                USING spellfix1;
            """
        )

    def index_text(self, *text):
        cursor = self.conn.cursor()
        with self.conn:
            params = ((t,) for t in text)
            cursor.executemany("INSERT INTO fts4data VALUES (?)", params)
            cursor.execute(
                """
                INSERT INTO spellfix1data(word)
                SELECT term FROM fts4data_terms
                WHERE col='*' AND
                    term not in (SELECT word from spellfix1data_vocab)
                """
            )

    # fts3 / 4 search expression tokenizer
    # no attempt is made to validate the expression, only
    # to identify valid search terms and extract them.
    # the fts3/4 tokenizer considers any alphanumeric ASCII character
    # and character in the range U+0080 and over to be terms.
    if sys.maxunicode == 0xFFFF:
        # UCS2 build, keep it simple, match any UTF-16 codepoint 0080 and over
        _fts4_expr_terms = re.compile(u"[a-zA-Z0-9\u0080-\uffff]+")
    else:
        # UCS4
        _fts4_expr_terms = re.compile(u"[a-zA-Z0-9\u0080-\U0010FFFF]+")

    def _terms_from_query(self, search_query):
        """Extract search terms from a fts3/4 query

        Returns a list of terms and a template such that
        template.format(*terms) reconstructs the original query.

        terms using partial* syntax are ignored, as you can't distinguish
        between a misspelled prefix search that happens to match existing
        tokens and a valid spelling that happens to have 'near' tokens in
        the spellfix1 database that would not otherwise be matched by fts4

        """
        template, terms, lastpos = [], [], 0
        for match in self._fts4_expr_terms.finditer(search_query):
            token, (start, end) = match.group(), match.span()
            # skip columnname: and partial* terms by checking next character
            ismeta = search_query[end:end + 1] in {":", "*"}
            # skip digits if preceded by "NEAR/"
            ismeta = ismeta or (
                token.isdigit() and template and template[-1] == "NEAR"
                and "/" in search_query[lastpos:start])
            if token not in {"AND", "OR", "NOT", "NEAR"} and not ismeta:
                # full search term, not a keyword, column name or partial*
                terms.append(token)
                token = "{}"
            template += search_query[lastpos:start], token
            lastpos = end
        template.append(search_query[lastpos:])
        return terms, "".join(template)

    def spellcheck_terms(self, search_query):
        cursor = self.conn.cursor()
        base_spellfix = """
            SELECT :term{0} as term, word FROM spellfix1data
            WHERE word MATCH :term{0} and top=1
        """
        terms, template = self._terms_from_query(search_query)
        params = {"term{}".format(i): t for i, t in enumerate(terms, 1)}
        query = " UNION ".join(
            [base_spellfix.format(i + 1) for i in range(len(params))]
        )
        cursor.execute(query, params)
        correction_map = dict(cursor)
        return template.format(*(correction_map.get(t, t) for t in terms))

    def search(self, search_query):
        corrected_query = self.spellcheck_terms(search_query)
        cursor = self.conn.cursor()
        fts_query = "SELECT * FROM fts4data WHERE fts4data MATCH ?"
        cursor.execute(fts_query, (corrected_query,))
        return {
            "terms": search_query,
            "corrected": corrected_query,
            "results": cursor.fetchall(),
        }

и интерактивную демонстрацию с использованием класса:

>>> db = sqlite3.connect(":memory:")
>>> fts = FTS4SpellfixSearch(db, './spellfix')
>>> fts.create_schema()
>>> fts.index_text("All the Carmichael numbers")  # your example
>>> from pprint import pprint
>>> pprint(fts.search('NUMMBER carmickaeel'))
{'corrected': 'numbers carmichael',
 'results': [('All the Carmichael numbers',)],
 'terms': 'NUMMBER carmickaeel'}
>>> fts.index_text(
...     "They are great",
...     "Here some other numbers",
... )
>>> pprint(fts.search('here some'))  # edgecase, multiple spellfix matches
{'corrected': 'here some',
 'results': [('Here some other numbers',)],
 'terms': 'here some'}
>>> pprint(fts.search('NUMMBER NOT carmickaeel'))  # using fts4 query syntax 
{'corrected': 'numbers NOT carmichael',
 'results': [('Here some other numbers',)],
 'terms': 'NUMMBER NOT carmickaeel'}
...