diff --git a/docs/cli.rst b/docs/cli.rst index 305051a..9440699 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -154,6 +154,21 @@ After running the above ``dogs.json`` example, try running this:: This will replace the record for id=2 (Pancakes) with a new record with an updated age. +Configuring full-text search +============================ + +You can enable SQLite full-text search on a table and a set of columns like this:: + + $ sqlite-utils enable-fts mydb.db documents title summary + +This will use SQLite's FTS5 module by default. Use ``--fts4`` if you want to use FTS4:: + + $ sqlite-utils enable-fts mydb.db documents title summary --fts4 + +The ``enable-fts`` command will populate the new index with all existing documents. If you later add more documents you will need to use ``populate-fts`` to cause them to be indexed as well:: + + $ sqlite-utils populatesfts mydb.db documents title summary + Vacuum ====== diff --git a/sqlite_utils/cli.py b/sqlite_utils/cli.py index 3e42480..97dac60 100644 --- a/sqlite_utils/cli.py +++ b/sqlite_utils/cli.py @@ -62,6 +62,45 @@ def optimize(path, no_vacuum): db.vacuum() +@cli.command(name="enable-fts") +@click.argument( + "path", + type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False), + required=True, +) +@click.argument("table") +@click.argument("column", nargs=-1, required=True) +@click.option( + "--fts4", help="Just show FTS4 enabled tables", default=False, is_flag=True +) +@click.option( + "--fts5", help="Just show FTS5 enabled tables", default=False, is_flag=True +) +def enable_fts(path, table, column, fts4, fts5): + fts_version = "FTS5" + if fts4 and fts5: + click.echo("Can only use one of --fts4 or --fts5", err=True) + return + elif fts4: + fts_version = "FTS4" + + db = sqlite_utils.Database(path) + db[table].enable_fts(column, fts_version=fts_version) + + +@cli.command(name="populate-fts") +@click.argument( + "path", + type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False), + required=True, +) +@click.argument("table") +@click.argument("column", nargs=-1, required=True) +def populate_fts(path, table, column): + db = sqlite_utils.Database(path) + db[table].populate_fts(column) + + def insert_upsert_options(fn): for decorator in reversed( ( diff --git a/tests/test_cli.py b/tests/test_cli.py index 3b66871..76e1d9b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -37,6 +37,41 @@ def test_tables_fts5(db_path): assert "Gosh_fts" == result.output.strip() +def test_enable_fts(db_path): + assert None == Database(db_path)["Gosh"].detect_fts() + result = CliRunner().invoke( + cli.cli, ["enable-fts", db_path, "Gosh", "c1", "--fts4"] + ) + assert 0 == result.exit_code + assert "Gosh_fts" == Database(db_path)["Gosh"].detect_fts() + + +def test_populate_fts(db_path): + Database(db_path)["Gosh"].insert_all([{"c1": "baz"}]) + exit_code = ( + CliRunner() + .invoke(cli.cli, ["enable-fts", db_path, "Gosh", "c1", "--fts4"]) + .exit_code + ) + assert 0 == exit_code + + def search(q): + return ( + Database(db_path) + .conn.execute("select c1 from Gosh_fts where c1 match ?", [q]) + .fetchall() + ) + + assert [("baz",)] == search("baz") + Database(db_path)["Gosh"].insert_all([{"c1": "martha"}]) + assert [] == search("martha") + exit_code = ( + CliRunner().invoke(cli.cli, ["populate-fts", db_path, "Gosh", "c1"]).exit_code + ) + assert 0 == exit_code + assert [("martha",)] == search("martha") + + def test_vacuum(db_path): result = CliRunner().invoke(cli.cli, ["vacuum", db_path]) assert 0 == result.exit_code