diff --git a/lexicon/tools/README.md b/lexicon/tools/README.md index 0d68c3b..efa6484 100644 --- a/lexicon/tools/README.md +++ b/lexicon/tools/README.md @@ -1,5 +1,5 @@ # Vyntr Lexicon -This is the code behind "word lookups" in Vyntr, powered by [English WordNet](https://en-word.net/) +This is the code behind "word lookups" and autocorrection in Vyntr, powered by [English WordNet](https://en-word.net/) To generate the needed `wn.json` in `vyntr/lexicon/wn.json`: 1. Go to https://en-word.net/ and click "Download as XML" diff --git a/lexicon/tools/pyproject.toml b/lexicon/tools/pyproject.toml index 372b289..47c9a9c 100644 --- a/lexicon/tools/pyproject.toml +++ b/lexicon/tools/pyproject.toml @@ -7,4 +7,5 @@ requires-python = ">=3.12" dependencies = [ "psycopg2>=2.9.10", "python-dotenv>=1.1.0", + "requests>=2.32.3", ] diff --git a/lexicon/tools/src/install_spelling_data.py b/lexicon/tools/src/install_spelling_data.py new file mode 100644 index 0000000..5d7f321 --- /dev/null +++ b/lexicon/tools/src/install_spelling_data.py @@ -0,0 +1,124 @@ +import requests +import zipfile +import os +import json +import psycopg2 +import psycopg2.extras +import os +import sys +import time +import math +from dotenv import load_dotenv + +SPELLING_DATA_BASE="https://raw.githubusercontent.com/first20hours/google-10000-english/refs/heads/master/google-10000-english.txt" +FILE="words_alpha.txt" + +script_dir = os.path.dirname(os.path.abspath(__file__)) +env_path = os.path.abspath(os.path.join(script_dir, '..', '..', '..', '.env')) +BATCH_SIZE = 100 +TABLE_NAME = "dictionary" + +if os.path.exists(env_path): + print(f"Loading environment variables from: {env_path}") + load_dotenv(dotenv_path=env_path) +else: + print(f"Warning: .env file not found at {env_path}.") + sys.exit(1) + +DB_URI = os.getenv("PRIVATE_DB_URL") + +if not os.getenv("PRIVATE_DB_URL"): + print("Warning: PRIVATE_DB_URL not found in environment or .env file.") + sys.exit(1) + +# spelling_correction + +def download_spelling_data(): + """ + Download the spelling data from the given URL and save it to the current directory. + """ + response = requests.get(SPELLING_DATA_BASE) + if response.status_code == 200: + with open(FILE, "wb") as f: + f.write(response.content) + print("Spelling data downloaded successfully.") + else: + print(f"Failed to download spelling data. Status code: {response.status_code}") + +def insert_data(conn, data): + """Inserts data into the wordnet table using execute_values.""" + insert_sql = f""" + INSERT INTO {TABLE_NAME} ( + word, weight + ) VALUES %s + ON CONFLICT DO NOTHING; + """ + values_to_insert = [] + total_inserted = 0 + start_time = time.time() + + print(f"Preparing and inserting data in batches of {BATCH_SIZE}...") + + for i, entry in enumerate(data): + values_to_insert.append(entry) + + if len(values_to_insert) >= BATCH_SIZE or (i + 1) == len(data): + if values_to_insert: + try: + with conn.cursor() as cur: + psycopg2.extras.execute_values( + cur, insert_sql, values_to_insert, page_size=BATCH_SIZE + ) + inserted_count = cur.rowcount + # use actual count if available and > 0, otherwise assume batch size for progress + actual_inserted_this_batch = inserted_count if inserted_count > 0 else len(values_to_insert) + total_inserted += actual_inserted_this_batch + conn.commit() + + print( + f" Processed batch ending at item {i + 1}. Total inserted/updated so far: ~{total_inserted}/{len(data)}") + values_to_insert = [] + except psycopg2.Error as e: + print(f"\nError inserting batch ending at item {i + 1}: {e}") + print("Attempted values (first item):", values_to_insert[0] if values_to_insert else "N/A") + conn.rollback() + values_to_insert = [] + + end_time = time.time() + print(f"\nData insertion finished in {end_time - start_time:.2f} seconds.") + print(f"Total entries processed/attempted: {len(data)}, Estimated inserted/updated: ~{total_inserted}") + + +def upload_spelling(): + with open(FILE, "r") as f: + lines = f.readlines() + data = [(line.strip(), -math.sqrt(i)) for i, line in enumerate(lines)] + + conn = None + try: + db_host_info = DB_URI.split('@')[-1] if '@' in DB_URI else DB_URI + print(f"Connecting to database: {db_host_info}...") + conn = psycopg2.connect(DB_URI) + print("Connection successful.") + + insert_data(conn, data) + + print("\nDatabase setup and data loading complete.") + + except psycopg2.OperationalError as e: + print(f"\nDatabase connection failed: {e}") + print("Please ensure the database server is running and accessible,") + print(f"and the connection URI (read from PRIVATE_DB_URL or default) is correct: {db_host_info}") + sys.exit(1) + except Exception as e: + print(f"\nAn unexpected error occurred: {e}") + sys.exit(1) + finally: + if conn: + conn.close() + print("Database connection closed.") + +if __name__ == "__main__": + download_spelling_data() + upload_spelling() + os.remove(FILE) \ No newline at end of file diff --git a/lexicon/tools/uv.lock b/lexicon/tools/uv.lock index 0b57bc9..ae4b65b 100644 --- a/lexicon/tools/uv.lock +++ b/lexicon/tools/uv.lock @@ -2,6 +2,59 @@ version = 1 revision = 1 requires-python = ">=3.12" +[[package]] +name = "certifi" +version = "2025.1.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, + { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, + { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, + { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, + { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, + { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, + { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, + { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, + { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, + { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, + { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, + { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, + { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, + { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, + { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, + { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, + { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, + { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, + { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, + { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, + { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, + { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, + { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, + { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, + { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, + { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + [[package]] name = "psycopg2" version = "2.9.10" @@ -22,6 +75,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, ] +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + [[package]] name = "tools" version = "0.1.0" @@ -29,10 +97,21 @@ source = { virtual = "." } dependencies = [ { name = "psycopg2" }, { name = "python-dotenv" }, + { name = "requests" }, ] [package.metadata] requires-dist = [ { name = "psycopg2", specifier = ">=2.9.10" }, { name = "python-dotenv", specifier = ">=1.1.0" }, + { name = "requests", specifier = ">=2.32.3" }, +] + +[[package]] +name = "urllib3" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, ] diff --git a/website/drizzle/0020_luxuriant_blacklash.sql b/website/drizzle/0020_luxuriant_blacklash.sql new file mode 100644 index 0000000..b86faad --- /dev/null +++ b/website/drizzle/0020_luxuriant_blacklash.sql @@ -0,0 +1,4 @@ +CREATE TABLE "dictionary" ( + "word" text NOT NULL, + CONSTRAINT "dictionary_word_unique" UNIQUE("word") +); diff --git a/website/drizzle/meta/0020_snapshot.json b/website/drizzle/meta/0020_snapshot.json new file mode 100644 index 0000000..188d14b --- /dev/null +++ b/website/drizzle/meta/0020_snapshot.json @@ -0,0 +1,1316 @@ +{ + "id": "f74ba7b6-cd51-4381-a319-fbea8ede6f83", + "prevId": "66637586-b4d0-4519-b818-a3120a25071a", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_summaries": { + "name": "ai_summaries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "query": { + "name": "query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_null": { + "name": "is_null", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_ai_summaries_query": { + "name": "idx_ai_summaries_query", + "columns": [ + { + "expression": "query", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ai_summaries_query_unique": { + "name": "ai_summaries_query_unique", + "nullsNotDistinct": false, + "columns": [ + "query" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_apikey_user": { + "name": "idx_apikey_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "apikey_user_id_user_id_fk": { + "name": "apikey_user_id_user_id_fk", + "tableFrom": "apikey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_usage": { + "name": "api_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_api_usage_user_date": { + "name": "idx_api_usage_user_date", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_usage_user_id_user_id_fk": { + "name": "api_usage_user_id_user_id_fk", + "tableFrom": "api_usage", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.daily_message_usage": { + "name": "daily_message_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_daily_message_usage_user_date": { + "name": "idx_daily_message_usage_user_date", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "daily_message_usage_user_id_user_id_fk": { + "name": "daily_message_usage_user_id_user_id_fk", + "tableFrom": "daily_message_usage", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "userDateUnique": { + "name": "userDateUnique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "date" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dictionary": { + "name": "dictionary", + "schema": "", + "columns": { + "word": { + "name": "word", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dictionary_word_unique": { + "name": "dictionary_word_unique", + "nullsNotDistinct": false, + "columns": [ + "word" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.search_queries": { + "name": "search_queries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "query": { + "name": "query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'vyntr'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_search_queries_query": { + "name": "idx_search_queries_query", + "columns": [ + { + "expression": "query", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "count", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_search_queries_count": { + "name": "idx_search_queries_count", + "columns": [ + { + "expression": "count", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "search_queries_query_unique": { + "name": "search_queries_query_unique", + "nullsNotDistinct": false, + "columns": [ + "query" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_banned": { + "name": "is_banned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "preferred_language": { + "name": "preferred_language", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'en'" + }, + "safe_search": { + "name": "safe_search", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "autocomplete": { + "name": "autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "instant_results": { + "name": "instant_results", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "ai_summarise": { + "name": "ai_summarise", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "anonymous_queries": { + "name": "anonymous_queries", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "analytics_enabled": { + "name": "analytics_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "ai_personalization": { + "name": "ai_personalization", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_user_preferences_user_id": { + "name": "idx_user_preferences_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_preferences_user_id_user_id_fk": { + "name": "user_preferences_user_id_user_id_fk", + "tableFrom": "user_preferences", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.website": { + "name": "website", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'::json" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "upvotes": { + "name": "upvotes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "downvotes": { + "name": "downvotes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "visits": { + "name": "visits", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_verified": { + "name": "is_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unlisted'" + }, + "is_featured": { + "name": "is_featured", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_website_status": { + "name": "idx_website_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_website_user_id": { + "name": "idx_website_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_website_domain_status": { + "name": "idx_website_domain_status", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "website_user_id_user_id_fk": { + "name": "website_user_id_user_id_fk", + "tableFrom": "website", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "website_domain_unique": { + "name": "website_domain_unique", + "nullsNotDistinct": false, + "columns": [ + "domain" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.website_votes": { + "name": "website_votes", + "schema": "", + "columns": { + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_website_votes_website": { + "name": "idx_website_votes_website", + "columns": [ + { + "expression": "website", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_website_votes_user": { + "name": "idx_website_votes_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "website_votes_website_website_domain_fk": { + "name": "website_votes_website_website_domain_fk", + "tableFrom": "website_votes", + "tableTo": "website", + "columnsFrom": [ + "website" + ], + "columnsTo": [ + "domain" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "website_votes_user_id_user_id_fk": { + "name": "website_votes_user_id_user_id_fk", + "tableFrom": "website_votes", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "website_votes_website_user_id_pk": { + "name": "website_votes_website_user_id_pk", + "columns": [ + "website", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/website/drizzle/meta/0021_snapshot.json b/website/drizzle/meta/0021_snapshot.json new file mode 100644 index 0000000..fe695f4 --- /dev/null +++ b/website/drizzle/meta/0021_snapshot.json @@ -0,0 +1,1323 @@ +{ + "id": "8787b29a-f2ed-4bfe-a0f3-a8616df47dae", + "prevId": "f74ba7b6-cd51-4381-a319-fbea8ede6f83", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_summaries": { + "name": "ai_summaries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "query": { + "name": "query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_null": { + "name": "is_null", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_ai_summaries_query": { + "name": "idx_ai_summaries_query", + "columns": [ + { + "expression": "query", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ai_summaries_query_unique": { + "name": "ai_summaries_query_unique", + "nullsNotDistinct": false, + "columns": [ + "query" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_apikey_user": { + "name": "idx_apikey_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "apikey_user_id_user_id_fk": { + "name": "apikey_user_id_user_id_fk", + "tableFrom": "apikey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_usage": { + "name": "api_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_api_usage_user_date": { + "name": "idx_api_usage_user_date", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_usage_user_id_user_id_fk": { + "name": "api_usage_user_id_user_id_fk", + "tableFrom": "api_usage", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.daily_message_usage": { + "name": "daily_message_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_daily_message_usage_user_date": { + "name": "idx_daily_message_usage_user_date", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "daily_message_usage_user_id_user_id_fk": { + "name": "daily_message_usage_user_id_user_id_fk", + "tableFrom": "daily_message_usage", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "userDateUnique": { + "name": "userDateUnique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "date" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dictionary": { + "name": "dictionary", + "schema": "", + "columns": { + "word": { + "name": "word", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "weight": { + "name": "weight", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dictionary_word_unique": { + "name": "dictionary_word_unique", + "nullsNotDistinct": false, + "columns": [ + "word" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.search_queries": { + "name": "search_queries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "query": { + "name": "query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'vyntr'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_search_queries_query": { + "name": "idx_search_queries_query", + "columns": [ + { + "expression": "query", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "count", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_search_queries_count": { + "name": "idx_search_queries_count", + "columns": [ + { + "expression": "count", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "search_queries_query_unique": { + "name": "search_queries_query_unique", + "nullsNotDistinct": false, + "columns": [ + "query" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_banned": { + "name": "is_banned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "preferred_language": { + "name": "preferred_language", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'en'" + }, + "safe_search": { + "name": "safe_search", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "autocomplete": { + "name": "autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "instant_results": { + "name": "instant_results", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "ai_summarise": { + "name": "ai_summarise", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "anonymous_queries": { + "name": "anonymous_queries", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "analytics_enabled": { + "name": "analytics_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "ai_personalization": { + "name": "ai_personalization", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_user_preferences_user_id": { + "name": "idx_user_preferences_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_preferences_user_id_user_id_fk": { + "name": "user_preferences_user_id_user_id_fk", + "tableFrom": "user_preferences", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.website": { + "name": "website", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'::json" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "upvotes": { + "name": "upvotes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "downvotes": { + "name": "downvotes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "visits": { + "name": "visits", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_verified": { + "name": "is_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unlisted'" + }, + "is_featured": { + "name": "is_featured", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_website_status": { + "name": "idx_website_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_website_user_id": { + "name": "idx_website_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_website_domain_status": { + "name": "idx_website_domain_status", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "website_user_id_user_id_fk": { + "name": "website_user_id_user_id_fk", + "tableFrom": "website", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "website_domain_unique": { + "name": "website_domain_unique", + "nullsNotDistinct": false, + "columns": [ + "domain" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.website_votes": { + "name": "website_votes", + "schema": "", + "columns": { + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_website_votes_website": { + "name": "idx_website_votes_website", + "columns": [ + { + "expression": "website", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_website_votes_user": { + "name": "idx_website_votes_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "website_votes_website_website_domain_fk": { + "name": "website_votes_website_website_domain_fk", + "tableFrom": "website_votes", + "tableTo": "website", + "columnsFrom": [ + "website" + ], + "columnsTo": [ + "domain" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "website_votes_user_id_user_id_fk": { + "name": "website_votes_user_id_user_id_fk", + "tableFrom": "website_votes", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "website_votes_website_user_id_pk": { + "name": "website_votes_website_user_id_pk", + "columns": [ + "website", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/website/drizzle/meta/_journal.json b/website/drizzle/meta/_journal.json index d439de2..302121d 100644 --- a/website/drizzle/meta/_journal.json +++ b/website/drizzle/meta/_journal.json @@ -141,6 +141,20 @@ "when": 1745169566227, "tag": "0019_cultured_ulik", "breakpoints": true + }, + { + "idx": 20, + "version": "7", + "when": 1745416936088, + "tag": "0020_luxuriant_blacklash", + "breakpoints": true + }, + { + "idx": 21, + "version": "7", + "when": 1745478839849, + "tag": "0021_wonderful_talon", + "breakpoints": true } ] } \ No newline at end of file diff --git a/website/package-lock.json b/website/package-lock.json index ee4d0ed..96f2c1c 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -28,6 +28,8 @@ "bits-ui": "^1.3.19", "clsx": "^2.1.1", "drizzle-kit": "^0.30.2", + "fastest-levenshtein": "^1.0.16", + "languagedetect": "^2.0.0", "lucide-svelte": "^0.477.0", "mode-watcher": "^0.5.1", "prettier": "^3.4.2", @@ -2558,6 +2560,16 @@ "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", "peer": true }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -2977,6 +2989,15 @@ "node": ">=14.0.0" } }, + "node_modules/languagedetect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/languagedetect/-/languagedetect-2.0.0.tgz", + "integrity": "sha512-AZb/liiQ+6ZoTj4f1J0aE6OkzhCo8fyH+tuSaPfSo8YHCWLFJrdSixhtO2TYdIkjcDQNaR4RmGaV2A5FJklDMQ==", + "dev": true, + "engines": { + "node": ">= 0.4.8" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", diff --git a/website/package.json b/website/package.json index 820a896..35eebe8 100644 --- a/website/package.json +++ b/website/package.json @@ -28,6 +28,7 @@ "bits-ui": "^1.3.19", "clsx": "^2.1.1", "drizzle-kit": "^0.30.2", + "fastest-levenshtein": "^1.0.16", "lucide-svelte": "^0.477.0", "mode-watcher": "^0.5.1", "prettier": "^3.4.2", diff --git a/website/src/lib/components/self/DetailsPanelSpellingMessage.svelte b/website/src/lib/components/self/DetailsPanelSpellingMessage.svelte new file mode 100644 index 0000000..ffa52ed --- /dev/null +++ b/website/src/lib/components/self/DetailsPanelSpellingMessage.svelte @@ -0,0 +1,20 @@ + + +
+
+
+ +
+
+
+ {detail} +
+
+
+
diff --git a/website/src/lib/server/heuristicsSpellingCorrectionFilter.ts b/website/src/lib/server/heuristicsSpellingCorrectionFilter.ts new file mode 100644 index 0000000..7f97a2e --- /dev/null +++ b/website/src/lib/server/heuristicsSpellingCorrectionFilter.ts @@ -0,0 +1,107 @@ +import { distance } from 'fastest-levenshtein'; + +// Key format: `${token}:${n} +const similarityCache: Record = {}; + +// Common first character typos for English +const commonFirstCharTypos: Record = { + a: ['q', 'w', 's', 'z'], + s: ['a', 'w', 'd', 'x', 'z'], + b: ['v', 'g', 'h', 'n'], + c: ['x', 'v', 'f', 'd'], + d: ['s', 'e', 'f', 'c', 'x'], + e: ['w', 'r', 'd', 's'], + f: ['d', 'r', 'g', 'v', 'c'], + g: ['f', 't', 'h', 'b', 'v'], + h: ['g', 'y', 'j', 'n', 'b'], + i: ['u', 'k', 'j', 'o'], + j: ['h', 'u', 'k', 'm', 'n'], + k: ['j', 'i', 'l', 'm'], + l: ['k', 'o', 'p'], + m: ['n', 'j', 'k'], + n: ['b', 'h', 'j', 'm'], + o: ['i', 'p', 'l'], + p: ['o', 'l'], + q: ['w', 'a'], + r: ['e', 'f', 't', 'd'], + t: ['r', 'g', 'y', 'f'], + u: ['y', 'j', 'i', 'h'], + v: ['c', 'f', 'g', 'b'], + w: ['q', 'a', 's', 'e'], + x: ['z', 's', 'd', 'c'], + y: ['t', 'h', 'u', 'g'], + z: ['a', 's', 'x'] +}; + +/** + * Returns the n most similar strings to the given token from a list of strings. + * Uses caching for performance and applies heuristics to improve search speed. + * + * @param stringList - A large array of strings to search through + * @param token - The target string to find similarities for + * @param n - Number of similar strings to return + * @param maxDistance - Optional maximum Levenshtein distance (default: 3) + * @returns Array of the n most similar strings + */ +export function findMostSimilar( + stringList: string[], + token: string, + n: number, + maxDistance: number = 3 +): string[] { + const cacheKey = `${token}:${n}`; + if (similarityCache[cacheKey]) { + return similarityCache[cacheKey]; + } + + if (!token || stringList.length === 0 || n <= 0) { + return []; + } + + const exactMatch = stringList.find((s) => s === token); + if (exactMatch) { + return [exactMatch]; + } + + const normalizedToken = token.toLowerCase().trim(); + const tokenLength = normalizedToken.length; + + // Heuristic 1: Filter by length similarity first (much faster than full distance calculation) + const lengthFilteredList = stringList.filter((s) => { + const lengthDiff = Math.abs(s.length - tokenLength); + return lengthDiff <= maxDistance; + }); + + // Heuristic 2: Filter by first character (optional, can improve performance for large lists) + // Only consider strings that start with the same letter or a common typo of the first letter + const firstChar = normalizedToken.charAt(0); + + let charactersToCheck = [firstChar]; + if (commonFirstCharTypos[firstChar]) { + charactersToCheck = [...charactersToCheck, ...commonFirstCharTypos[firstChar]]; + } + + const characterFilteredList = lengthFilteredList.filter((s) => + charactersToCheck.includes(s.charAt(0).toLowerCase()) + ); + + const listToProcess = characterFilteredList.length >= n ? characterFilteredList : lengthFilteredList; + + const withDistances = listToProcess.map((str) => ({ + string: str, + distance: distance(normalizedToken, str.toLowerCase()) + })); + + withDistances.sort((a, b) => a.distance - b.distance); + + // Get top N results within maxDistance + const results = withDistances + .filter((item) => item.distance <= maxDistance) + .slice(0, n) + .map((item) => item.string); + + // Cache the results + similarityCache[cacheKey] = results; + + return results; +} diff --git a/website/src/lib/server/schema.ts b/website/src/lib/server/schema.ts index 2fe2026..fb2f276 100644 --- a/website/src/lib/server/schema.ts +++ b/website/src/lib/server/schema.ts @@ -192,4 +192,9 @@ export const aiSummaries = pgTable('ai_summaries', { updatedAt: timestamp('updated_at').defaultNow().notNull(), }, (table) => ({ queryIdx: index("idx_ai_summaries_query").on(table.query) -})); \ No newline at end of file +})); + +export const dictionary = pgTable('dictionary', { + word: text('word').notNull().unique(), + weight: integer('weight').notNull().default(0), +}); diff --git a/website/src/lib/server/search.ts b/website/src/lib/server/search.ts index de6284f..b55afc8 100644 --- a/website/src/lib/server/search.ts +++ b/website/src/lib/server/search.ts @@ -10,6 +10,7 @@ import { eq, and, sql } from 'drizzle-orm'; import { SEARCH_ENDPOINT } from '$env/static/private'; import { getFavicon } from '$lib/utils'; import { db } from '$lib/server/db'; +import { tryCorrectSpelling } from '$lib/server/spellingCorrection'; export async function performSearch(query: string, userPrefs: any = null) { const language = userPrefs?.preferredLanguage || 'en'; @@ -19,6 +20,8 @@ export async function performSearch(query: string, userPrefs: any = null) { saveSearchQuery(query); } + const spellingCorrection = await tryCorrectSpelling(query); + // ==================== DATE ==================== const dateResult = parseDateQuery(query); const dateDetail = dateResult ? { @@ -59,16 +62,30 @@ export async function performSearch(query: string, userPrefs: any = null) { } // ==================== WEB SEARCH ==================== - const webResults = await fetchSearchResults(query, language); + let webQuery = query; + if (spellingCorrection) { + // Make use of the OR feature of tantivy + webQuery = "(" + query + ") OR (" + spellingCorrection.newQuery + ")"; + } + const webResults = await fetchSearchResults(webQuery, language); // ==================== BLIPTEXT SEARCH ==================== - const bliptextResults = await searchBliptext(query); + let bliptextResults = await searchBliptext(query); + // When no results are found, try the corrected spelling + if ((!bliptextResults.bestMatch) && spellingCorrection) { + bliptextResults = await searchBliptext(spellingCorrection.newQuery); + } const bliptextDetail = bliptextResults.bestMatch ? { type: 'bliptext', article: bliptextResults.bestMatch } : null; // ==================== WORD LOOKUP ==================== let wordDetail = null; try { - const matches = await searchWordnet(query); + let matches = await searchWordnet(query); + + if (matches.length === 0 && spellingCorrection) { + matches = await searchWordnet(spellingCorrection.newQuery); + } + if (matches.length > 0) { const bestMatch = matches[0]; wordDetail = bestMatch.entry; @@ -91,7 +108,8 @@ export async function performSearch(query: string, userPrefs: any = null) { word: wordDetail, currency: currencyDetail, unitConversion: unitConversionDetail, - ai_summary: aiSummary + ai_summary: aiSummary, + correction: spellingCorrection }; } diff --git a/website/src/lib/server/spellingCorrection.ts b/website/src/lib/server/spellingCorrection.ts new file mode 100644 index 0000000..1515b09 --- /dev/null +++ b/website/src/lib/server/spellingCorrection.ts @@ -0,0 +1,72 @@ +import { db } from '$lib/server/db'; +import { dictionary } from '$lib/server/schema'; +import { findMostSimilar } from '$lib/server/heuristicsSpellingCorrectionFilter'; + +const SPECIAL_MISSPELLINGS: Record = { + guthib: { shown: 'You spelled it wrong', replace: 'github' } +} as const; + +let dictionaryCache: { word: string; weight: number }[] | null = null; +let wordSet: string[] | null = null; +let wordWeights: Map | null = null; + +async function loadDictionary() { + dictionaryCache = await db.select().from(dictionary).execute(); + wordSet = dictionaryCache?.map(({ word }) => word); + wordWeights = new Map(); + dictionaryCache?.forEach(({ word, weight }) => { + wordWeights?.set(word, weight); + }); +} + +export type SpellingCorrection = { + newQuery: string; + shownMessage: string | null; +}; + +function characterSimilarity(a: string, b: string) { + let aChars = new Set(a.toLowerCase().split('')); + let bChars = new Set(b.toLowerCase().split('')); + let intersection = new Set([...aChars].filter((x) => bChars.has(x))); + return intersection.size; +} + +export async function tryCorrectSpelling(query: string): Promise { + await loadDictionary(); + const tokens = query.split(' '); + let message = null as string | null; + let anythingChanged = false; + const correctedTokens = tokens.map((token) => { + const specialCorrection = SPECIAL_MISSPELLINGS[token.toLowerCase()]; + if (specialCorrection) { + message = specialCorrection.shown; + anythingChanged = true; + return specialCorrection.replace; + } + + if (wordSet?.includes(token.toLowerCase())) return token; + + const correctionTry = findMostSimilar(wordSet!, token, 5, 1) + .map((word) => ({ + word, + weight: (wordWeights?.get(word) || 0) + characterSimilarity(token, word) * 10 + })) + .sort((a, b) => a.weight - b.weight) + .map(({ word }) => word) + .pop(); + + if (correctionTry && correctionTry !== token) { + anythingChanged = true; + return correctionTry; + } + + return token; + }); + + if (!anythingChanged) return null; + + return { + newQuery: correctedTokens.join(' '), + shownMessage: message + }; +} diff --git a/website/src/lib/types/searchDetails.ts b/website/src/lib/types/searchDetails.ts index 45f41bc..1aaf4ae 100644 --- a/website/src/lib/types/searchDetails.ts +++ b/website/src/lib/types/searchDetails.ts @@ -1,6 +1,7 @@ import type { ArticleData } from './article'; import type { TimeUnit } from '$lib/utils/date'; import type { UnitCategory } from '$lib/utils/units'; +import type { SpellingCorrection } from '$lib/server/spellingCorrection'; export type SearchDetailType = 'bliptext' | 'person' | 'calculator' | 'movie' | 'date' | 'word' | 'currency' | 'unitConversion'; @@ -119,4 +120,5 @@ export interface SearchResponse { currency: CurrencySearchDetail | null; unitConversion: UnitConversionSearchDetail | null; ai_summary: string | null; + correction: SpellingCorrection | null; } diff --git a/website/src/routes/search/+page.svelte b/website/src/routes/search/+page.svelte index 307fb1e..b9247c1 100644 --- a/website/src/routes/search/+page.svelte +++ b/website/src/routes/search/+page.svelte @@ -1,146 +1,166 @@ - {query} - Vyntr Search + {query} - Vyntr Search
-
- Vyntr Logo - -
- -
-
- -
- -
-
- {#if !isLoading} - {#if searchData.bliptext && !searchData.unitConversion && !searchData.currency && !searchData.word && !searchData.date && timerSeconds === null && !mathResult} -
- -
- {/if} - - {#if searchData.unitConversion || searchData.currency || searchData.word || searchData.date || timerSeconds !== null || mathResult || searchData.ai_summary} -
- {#if searchData.unitConversion} - - {:else if searchData.currency} - - {:else if searchData.word} - - {:else if searchData.date} - - {:else if timerSeconds !== null} - - {:else if mathResult} - - {:else if searchData.ai_summary} - - {/if} -
- - {#if searchData.bliptext} -
- -
- {/if} - {/if} - - - {:else} - -

Loading results

- {/if} -
- - {#if searchData.bliptext} - - {/if} -
-
+
+ Vyntr Logo + +
+ +
+
+ +
+ +
+
+ {#if !isLoading} + {#if searchData.correction} + + + {#if searchData.correction.shownMessage} +
+ +
+ {/if} + {/if} + + {#if searchData.bliptext && !searchData.unitConversion && !searchData.currency && !searchData.word && !searchData.date && timerSeconds === null && !mathResult} +
+ +
+ {/if} + + {#if searchData.unitConversion || searchData.currency || searchData.word || searchData.date || timerSeconds !== null || mathResult || searchData.ai_summary} +
+ {#if searchData.unitConversion} + + {:else if searchData.currency} + + {:else if searchData.word} + + {:else if searchData.date} + + {:else if timerSeconds !== null} + + {:else if mathResult} + + {:else if searchData.ai_summary} + + {/if} +
+ + {#if searchData.bliptext} +
+ +
+ {/if} + {/if} + + + {:else} + +

Loading results

+ {/if} +
+ + {#if searchData.bliptext} + + {/if} +
+