2 min read

DuckDB で日本語全文検索

DuckDB-VSSPLaMo-Embedding-1B を利用することで、ベクトル検索を実現できますが、DuckDB-FTS (Full-Text Search) と形態素解析ライブラリである Lindera を組み合わせて日本語全文検索を実現できます。

DuckDB-FTS + Lindera

DuckDB の全文検索拡張は日本語には対応していないないのですが、スペース区切りでトークン化することで、日本語の全文検索を利用する事が出来ます。トークン化には Meilisearch にも利用されている信頼と安心の Lindera を利用することにしました。


今回この参考コードを Lindera の作者であり検索の専門家でもある Minoru OSUKAにレビューいただきました。本当にありがとうございます。

以下は参考コードです。

[project]
name = "duckdb-fts-lindera"
version = "0.1.0"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "duckdb>=1.2.2",
    "lindera-py>=0.41.0",
]
# SPDX-License-Identifier: Apache-2.0
import duckdb
from lindera_py import Segmenter, Tokenizer, load_dictionary

dictionary = load_dictionary("ipadic")
segmenter = Segmenter("normal", dictionary)
tokenizer = Tokenizer(segmenter)


def ja_tokens(text: str) -> str:
    return " ".join(t.text for t in tokenizer.tokenize(text))


def main():
    conn = duckdb.connect()
    conn.install_extension("fts")
    conn.load_extension("fts")

    conn.sql("CREATE SEQUENCE IF NOT EXISTS id_sequence START 1;")
    conn.sql("""
    CREATE TABLE sora_doc (
        id INTEGER DEFAULT nextval('id_sequence') PRIMARY KEY,
        content VARCHAR,  
        content_t VARCHAR
    );
    """)

    # https://sora-doc.shiguredo.jp/ より引用
    docs = [
        "例えば 3 ノードのクラスターがある場合、 すでに接続しているクライアントがいるノードとは異なるノードにクライアントが接続した場合、Sora はその異なるノードにすでに接続しているクライアントの音声や映像、データをリレーします。",
        "StartRecording API やセッションウェブフックの戻り値で指定できる録画メタデータについてはセンシティブなデータとして扱っていません。これは録画ファイル出力時の録画メタデータファイルに含まれ、映像合成時に利用する事を想定しているためです。",
        "WebSocket は TCP ベースのため Head of Line Blocking が存在し、不安定な回線などでパケットが詰まってしまうことがあります。 DataChannel は WebSocket とは異なり、パケットを並列でやりとりできるため、不安定な回線などでもパケットが詰まることが少なくなります。 シグナリングを WebSocket 経由から DataChannel 経由へ切り替える機能を提供することでより安定した接続が維持できます。",
    ]

    for doc in docs:
        conn.execute(
            "INSERT INTO sora_doc (content, content_t) VALUES (?, ?)",
            [
                doc,
                ja_tokens(doc),
            ],
        )

    query = "センシティブデータについて教えてください"
    print("query:", query)

    conn.sql("""
    PRAGMA create_fts_index(
        'sora_doc',
        'id',
        'content_t',

        stemmer = 'none',
        stopwords = 'none',
        ignore = '',
        lower = false,
        strip_accents = false
    );
    """)

    q_tokens = ja_tokens(query)

    rows = conn.sql(f"""
        SELECT id, fts_main_sora_doc.match_bm25(id, '{q_tokens}') AS score, content
        FROM sora_doc
        WHERE score IS NOT NULL
        ORDER BY score DESC
    """).fetchall()

    for row in rows:
        print(f"ID: {row[0]}, Score: {row[1]}, Content: {row[2]}")


if __name__ == "__main__":
    main()
    # query: センシティブデータについて教えてください
    # ID: 2, Score: 4.536910447182791, Content: StartRecording API やセッションウェブフックの戻り値で指定できる録画メタデータについてはセンシティブなデータとして扱っていません。これは録画ファイル出力時の録画メタデータファイルに含まれ、映像合成時に利用する事を想定しているためです。
    # ID: 1, Score: 1.754047940301512, Content: 例えば 3 ノードのクラスターがある場合、 すでに接続しているクライアントがいるノードとは異なるノードにクライアントが接続した場合、Sora はその異なるノードにすでに接続しているクライアントの音声や映像、データをリレーします。
    # ID: 3, Score: 0.8606840213455622, Content: WebSocket は TCP ベースのため Head of Line Blocking が存在し、不安定な回線などでパケットが詰まってしまうことがあります。 DataChannel は WebSocket とは異なり、パケットを並列でやりとりできるため、不安定な回線などでもパケットが詰まることが少なくなります。 シグナリングを WebSocket 経由から DataChannel 経由へ切り替える機能を提供することでより安定した接続が維持できます。

DuckDB-FTS と Lindera を組み合わせることで簡単に日本語全文検索を利用する事ができます、是非試してみてください。