【问题标题】:How to use SQLAlchemy to create a full text search index on SQLite and query it?如何使用 SQLAlchemy 在 SQLite 上创建全文搜索索引并进行查询?
【发布时间】:2021-01-15 16:38:36
【问题描述】:

我正在创建一个可以执行基本操作的简单应用程序。 SQLite 用作数据库。我想执行通配符搜索,但我知道它的性能很差。我想尝试全文搜索,但我无法完整示例 怎么做。我确认 SQLite 有full text search support。这是我的示例代码。

from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

class Person(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.Text, unique=True, nullable=False)
    thumb = db.Column(db.Text, nullable=False, default="")

    role = db.relationship("Role", backref="person", cascade="delete")


class Role(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    person_id = db.Column(db.Integer, db.ForeignKey(Person.id, ondelete="CASCADE"), nullable=False)
    role = db.Column(db.Text, nullable=False)

如何创建 FTS 索引并使用 SQLAlchemy 进行查询。例如,在 Person 中搜索姓名。

【问题讨论】:

  • @IljaEverilä 运行原始命令是不明智的。我正在使用 alembic 来维护数据库架构。
  • 没有什么能阻止您同时使用原始 SQL 和 alembic。
  • 经过大量的折腾,一种解决方案可能是创建一个external content FTS table 并将其映射为您的班级的non-primary mapper。它不漂亮,但它有效。试图四处寻找可以自动执行此操作的工具,但还没有找到。

标签: python sqlite sqlalchemy full-text-search flask-sqlalchemy


【解决方案1】:

FTS5 提供支持全文搜索的虚拟表。换句话说,您不能在现有表中的列上创建全文索引。相反,您可以创建一个 FTS5 虚拟表并从原始表中复制相关数据以进行索引。为了避免将相同的数据存储两次,您可以将其设为external content table,但您仍必须确保 FTS5 表保持同步,无论是手动还是使用触发器。

您可以创建一个通用的自定义 DDL 构造来处理创建一个镜像另一个表的 FTS5 虚拟表:

class CreateFtsTable(DDLElement):
    """Represents a CREATE VIRTUAL TABLE ... USING fts5 statement, for indexing
    a given table.

    """

    def __init__(self, table, version=5):
        self.table = table
        self.version = version


@compiles(CreateFtsTable)
def compile_create_fts_table(element, compiler, **kw):
    """
    """
    tbl = element.table
    version = element.version
    preparer = compiler.preparer
    sql_compiler = compiler.sql_compiler

    tbl_name = preparer.format_table(tbl)
    vtbl_name = preparer.quote(tbl.name + "_idx")

    text = "\nCREATE VIRTUAL TABLE "
    text += vtbl_name + " "
    text += "USING fts" + str(version) + "("

    separator = "\n"

    pk_column, = tbl.primary_key
    columns = [col for col in tbl.columns if col is not pk_column]

    for column in columns:
        text += separator
        separator = ", \n"
        text += "\t" + preparer.format_column(column)

        if not isinstance(column.type, String):
            text += " UNINDEXED"

    text += separator
    text += "\tcontent=" + sql_compiler.render_literal_value(
            tbl.name, String())

    text += separator
    text += "\tcontent_rowid=" + sql_compiler.render_literal_value(
            pk_column.name, String())

    text += "\n)\n\n"
    return text

给定的实现有点幼稚,默认情况下索引所有文本列。创建的虚拟表是通过在原始表名后添加_idx来隐式命名的。

但仅此还不够,如果您想自动保持表与触发器同步,并且由于您只为一个表添加索引,您可以选择在迁移脚本中使用文本 DDL 构造:

def upgrade():
    ddl = [
        """
        CREATE VIRTUAL TABLE person_idx USING fts5(
            name,
            thumb UNINDEXED,
            content='person',
            content_rowid='id'
        )
        """,
        """
        CREATE TRIGGER person_ai AFTER INSERT ON person BEGIN
            INSERT INTO person_idx (rowid, name, thumb)
            VALUES (new.id, new.name, new.thumb);
        END
        """,
        """
        CREATE TRIGGER person_ad AFTER DELETE ON person BEGIN
            INSERT INTO person_idx (person_idx, rowid, name, thumb)
            VALUES ('delete', old.id, old.name, old.thumb);
        END
        """,
        """
        CREATE TRIGGER person_au AFTER UPDATE ON person BEGIN
            INSERT INTO person_idx (person_idx, rowid, name, thumb)
            VALUES ('delete', old.id, old.name, old.thumb);
            INSERT INTO person_idx (rowid, name, thumb)
            VALUES (new.id, new.name, new.thumb);
        END
        """
    ]

    for stmt in ddl:
        op.execute(sa.DDL(stmt))

如果您的人员表包含现有数据,请记住将这些数据插入到创建的虚拟表中以进行索引。

为了实际使用创建的虚拟表,您可以为Person 创建一个non-primary mapper

person_idx = db.Table('person_idx', db.metadata,
                      db.Column('rowid', db.Integer(), primary_key=True),
                      db.Column('name', db.Text()),
                      db.Column('thumb', db.Text()))

PersonIdx = db.mapper(
    Person, person_idx, non_primary=True,
    properties={
        'id': person_idx.c.rowid
    }
)

并使用例如 MATCH 进行全文查询:

db.session.query(PersonIdx).\
    filter(PersonIdx.c.name.op("MATCH")("john")).\
    all()

请注意,结果是Person 对象的列表。 PersonIdx 只是一个Mapper


正如 Victor K 所指出的,不推荐使用非主映射器,新的替代方法是使用 aliased()。设置大致相同,但在使用Columnkey 参数创建person_idx Table 时需要进行rowidid 的映射:

person_idx = db.Table('person_idx', db.metadata,
                      db.Column('rowid', db.Integer(), key='id', primary_key=True),
                      db.Column('name', db.Text()),
                      db.Column('thumb', db.Text()))

而不是新的映射器创建别名:

PersonIdx = db.aliased(Person, person_idx, adapt_on_names=True)

别名的工作方式更像映射类,因为您不通过.c 访问映射属性,而是直接访问:

db.session.query(PersonIdx).\
    filter(PersonIdx.name.op("MATCH")("john")).\
    all()

【讨论】:

  • 鉴于根据this,从 SQLAlchemy 1.3 开始不推荐使用非主映射器,是否有一种现代方法可以实现同样的目标?
  • 也许使用aliased 的东西会起作用,必须检查。可能有问题的字段是 rowid(与您的 pk 相比),但也许可以在创建 FTS vtable 时对其进行控制。
  • @VictorK。是的,使用aliased() 有效,假设您在创建person_idx 时将key='id' 传递给rowid 列,并将adapt_on_names=True 传递给aliased()
  • 使用常用的布尔运算符。根据您的用例,您也可以将多个文本列连接到一个索引列中。
  • @kkyr 此外,您还可以通过将表名指定为列名来一次查询虚拟表中的所有列,例如column("person_idx").op("MATCH")("john")
猜你喜欢
  • 2017-07-12
  • 2012-03-09
  • 2011-03-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-07-08
  • 2011-11-21
  • 1970-01-01
相关资源
最近更新 更多