-
Notifications
You must be signed in to change notification settings - Fork 268
feat(core): support unsegmented-script full-text search #1373
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
37122c6
feat(core): support unsegmented-script full-text search
phernandez 917552b
fix(core): preserve absent runtime search indexes
phernandez 0e40744
fix(core): address multilingual FTS review findings
phernandez c9cf725
fix(core): preserve structured FTS semantics
phernandez 8d568e8
fix(core): preserve word-query compatibility forms
phernandez dc91a73
fix(core): preserve word-only search ranking
phernandez eda1885
fix(core): cover script continuations and ranking
phernandez 22f752f
fix(core): handle mixed stopword script queries
phernandez 39de387
fix(core): preserve SQLite script query semantics
phernandez 5c8ab1e
fix(core): index accepted-note script grams
phernandez b99ba4c
fix(core): bound accepted-note script indexing
phernandez d9f0647
fix(core): align boolean whitespace handling
phernandez 2eb08e7
fix(core): preserve filtered script candidates
phernandez dc8edfb
fix(core): preserve boundary boolean operators
phernandez 831a0c5
docs(core): correct script reindex command
phernandez 154783d
fix(core): preserve adjoining mixed-script tokens
phernandez 3d14332
fix(core): isolate relaxed script search channels
phernandez 8556e4c
fix(core): preserve mixed search replacement invariants
phernandez 9bf2ed8
fix(core): separate mixed-script query channels
phernandez 4e8e6c3
fix(core): preserve mixed-token compatibility text
phernandez ab1aeda
fix(core): index mixed-token word fragments
phernandez e880d19
fix(core): bound mixed-token search terms
phernandez 20dba6b
fix(core): cover script join controls and extended kana
phernandez 1e6d50f
fix(core): preserve mixed-token search semantics
phernandez ad91d8a
fix(core): bound mixed-token ordering terms
phernandez 3d9b3e1
fix(core): bind mixed-token blocks to script roles
phernandez 62b1e94
fix(core): keep mixed-script tokens on word search path
phernandez File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
123 changes: 123 additions & 0 deletions
123
src/basic_memory/alembic/versions/d2e3f4a5b6c7_add_script_ngrams_to_full_text_search.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| """Add portable script n-grams to full-text search. | ||
|
|
||
| Revision ID: d2e3f4a5b6c7 | ||
| Revises: bcdbd5a942ca | ||
| Create Date: 2026-08-29 00:00:00.000000 | ||
|
|
||
| """ | ||
|
|
||
| from typing import Sequence, Union | ||
|
|
||
| from alembic import op | ||
| from sqlalchemy import text | ||
|
|
||
|
|
||
| revision: str = "d2e3f4a5b6c7" | ||
| down_revision: Union[str, None] = "bcdbd5a942ca" | ||
| branch_labels: Union[str, Sequence[str], None] = None | ||
| depends_on: Union[str, Sequence[str], None] = None | ||
|
|
||
|
|
||
| SQLITE_COLUMNS = """ | ||
| id, title, content_stems, content_snippet, {script_column} permalink, | ||
| file_path, type, project_id, from_id, to_id, relation_type, entity_id, | ||
| category, metadata, created_at, updated_at | ||
| """ | ||
|
|
||
|
|
||
| def rebuild_sqlite_search_index(*, include_script_ngrams: bool) -> None: | ||
| """Copy the FTS5 table while changing its indexed-column contract.""" | ||
| bind = op.get_bind() | ||
| search_index_sql: str | None = bind.execute( | ||
| text("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'search_index'") | ||
| ).scalar_one_or_none() | ||
|
|
||
| # The runtime owns creation of the derived FTS table, so migrations only transform an | ||
| # existing FTS5 index. This also leaves unrelated physical tables with the same name alone. | ||
| if search_index_sql is None or "using fts5" not in search_index_sql.casefold(): | ||
| return | ||
|
|
||
| script_definition = "script_ngrams," if include_script_ngrams else "" | ||
| op.execute(f""" | ||
| CREATE VIRTUAL TABLE search_index_rebuilt USING fts5( | ||
| id UNINDEXED, | ||
| title, | ||
| content_stems, | ||
| content_snippet, | ||
| {script_definition} | ||
| permalink, | ||
| file_path UNINDEXED, | ||
| type UNINDEXED, | ||
| project_id UNINDEXED, | ||
| from_id UNINDEXED, | ||
| to_id UNINDEXED, | ||
| relation_type UNINDEXED, | ||
| entity_id UNINDEXED, | ||
| category UNINDEXED, | ||
| metadata UNINDEXED, | ||
| created_at UNINDEXED, | ||
| updated_at UNINDEXED, | ||
| tokenize='unicode61 tokenchars 0x2F', | ||
| prefix='1,2,3,4' | ||
| ) | ||
| """) | ||
|
|
||
| source_columns = SQLITE_COLUMNS.format(script_column="") | ||
| target_columns = SQLITE_COLUMNS.format( | ||
| script_column="script_ngrams," if include_script_ngrams else "" | ||
| ) | ||
| selected_columns = SQLITE_COLUMNS.format(script_column="'' AS script_ngrams,") | ||
| if not include_script_ngrams: | ||
| selected_columns = source_columns | ||
| op.execute( | ||
| f"INSERT INTO search_index_rebuilt ({target_columns}) " | ||
| f"SELECT {selected_columns} FROM search_index" | ||
| ) | ||
| op.execute("DROP TABLE search_index") | ||
| op.execute("ALTER TABLE search_index_rebuilt RENAME TO search_index") | ||
|
|
||
|
|
||
| def upgrade() -> None: | ||
| """Add the derived script channel; a reindex populates existing rows.""" | ||
| if op.get_bind().dialect.name == "sqlite": | ||
| rebuild_sqlite_search_index(include_script_ngrams=True) | ||
| return | ||
|
|
||
| op.execute("ALTER TABLE search_index ADD COLUMN script_ngrams TEXT NOT NULL DEFAULT ''") | ||
| op.execute(""" | ||
| ALTER TABLE search_index | ||
| ADD COLUMN script_ngrams_index_col tsvector GENERATED ALWAYS AS ( | ||
| to_tsvector('simple', script_ngrams) | ||
| ) STORED | ||
| """) | ||
| op.execute(""" | ||
| CREATE INDEX idx_search_index_script_ngrams_fts | ||
| ON search_index USING gin(script_ngrams_index_col) | ||
| """) | ||
| op.execute( | ||
| "ALTER TABLE search_index_fts_chunks ADD COLUMN script_ngrams TEXT NOT NULL DEFAULT ''" | ||
| ) | ||
| op.execute(""" | ||
| ALTER TABLE search_index_fts_chunks | ||
| ADD COLUMN script_ngrams_index_col tsvector GENERATED ALWAYS AS ( | ||
| to_tsvector('simple', script_ngrams) | ||
| ) STORED | ||
| """) | ||
| op.execute(""" | ||
| CREATE INDEX idx_search_index_fts_chunks_script_ngrams_fts | ||
| ON search_index_fts_chunks USING gin(script_ngrams_index_col) | ||
| """) | ||
|
|
||
|
|
||
| def downgrade() -> None: | ||
| """Remove the script channel while preserving the existing word index.""" | ||
| if op.get_bind().dialect.name == "sqlite": | ||
| rebuild_sqlite_search_index(include_script_ngrams=False) | ||
| return | ||
|
|
||
| op.execute("DROP INDEX IF EXISTS idx_search_index_fts_chunks_script_ngrams_fts") | ||
| op.execute("ALTER TABLE search_index_fts_chunks DROP COLUMN IF EXISTS script_ngrams_index_col") | ||
| op.execute("ALTER TABLE search_index_fts_chunks DROP COLUMN IF EXISTS script_ngrams") | ||
| op.execute("DROP INDEX IF EXISTS idx_search_index_script_ngrams_fts") | ||
| op.execute("ALTER TABLE search_index DROP COLUMN IF EXISTS script_ngrams_index_col") | ||
| op.execute("ALTER TABLE search_index DROP COLUMN IF EXISTS script_ngrams") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.