-
Notifications
You must be signed in to change notification settings - Fork 7
Feat/acronym search 04 detection and retrieval #7
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
Open
borkarsaish65
wants to merge
49
commits into
ELEVATE-Project:release-2.1.0
Choose a base branch
from
borkarsaish65:feat/acronym-search-04-detection-and-retrieval
base: release-2.1.0
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
49 commits
Select commit
Hold shift + click to select a range
8640a51
feat: bootstrap Alembic and add acronym_mapping table
borkarsaish65 f160cd7
feat: add acronym cache-aside lookup with startup warm-up
borkarsaish65 f670964
fix: fall back to Postgres on acronym cache errors, not just misses
borkarsaish65 0e2cf5c
feat: add internal bulk upload endpoint for acronym mappings
borkarsaish65 97e7d90
chore: add placeholder value for INTERNAL_API_TOKEN in .env.sample
borkarsaish65 9bd63ee
fix: harden bulk upload against cache outages and BOM-prefixed CSVs
borkarsaish65 8f06323
fix: validate acronym length per-row, keep bulk upload off the event …
borkarsaish65 897a269
feat: wire acronym detection into dense+sparse retrieval (PR4+5)
borkarsaish65 fdb84f0
fix: acronym DB lookup failure no longer takes down all search
borkarsaish65 0d7e0a7
fix: acronym expansions containing a backslash no longer crash search
borkarsaish65 1626174
fix: cache negative acronym lookups, not just positive hits
borkarsaish65 55b719c
fix: acronym row with empty expansions no longer crashes search
borkarsaish65 29ee62e
fix: stop writing fake boolean/phrase syntax the BM25 encoder ignores
borkarsaish65 796efbc
fix: skip the redundant dense query variant for case-only expansions
borkarsaish65 16158b3
fix: curate seed acronym dictionary (remove false positives, dead wei…
borkarsaish65 585fb44
fix: remove seed acronyms that can never be detected due to punctuati…
borkarsaish65 87b3549
fix: remove more false-positive/non-acronym entries from seed dictionary
borkarsaish65 fa0f571
fix: Redis client hangs indefinitely on an unresponsive cache host
borkarsaish65 c0bc6cb
fix: use constant-time comparison for the internal API token
borkarsaish65 5fb48f0
fix: non-ASCII internal token crashes with 500 instead of clean 401
borkarsaish65 e6d6433
fix: acronym substitution garbles text when one expansion contains an…
borkarsaish65 f0cef33
fix: every search grinds to a halt while Postgres is unreachable
borkarsaish65 38109b9
fix: acronym-detected search lets real keyword matches lose to AI-gue…
borkarsaish65 6062f61
fix: acronym reweight could penalize a document for having real evidence
borkarsaish65 6ea52ac
fix: address PR #4 review comments on acronym_mapping migration
borkarsaish65 e401258
feat: add acronym cache-aside lookup with startup warm-up
borkarsaish65 6fdd669
fix: fall back to Postgres on acronym cache errors, not just misses
borkarsaish65 667cf44
fix: address PR #5 review comments on acronym redis cache
borkarsaish65 97065a8
fix: document REDIS_DB in .env.sample
borkarsaish65 815b851
feat: add internal bulk upload endpoint for acronym mappings
borkarsaish65 01e2a77
chore: add placeholder value for INTERNAL_API_TOKEN in .env.sample
borkarsaish65 6c316e8
fix: harden bulk upload against cache outages and BOM-prefixed CSVs
borkarsaish65 3215236
fix: validate acronym length per-row, keep bulk upload off the event …
borkarsaish65 e3650e4
chore: drop bulk-upload test files from tracking
borkarsaish65 2d5fef1
fix: address PR #6 review comments on bulk upload endpoint
borkarsaish65 b675481
fix: rename ambiguous row variable in get_expansion
borkarsaish65 74da593
Merge branch 'feat/acronym-search-02-redis-cache' into feat/acronym-s…
borkarsaish65 95b8b84
Merge branch 'feat/acronym-search-03-bulk-upload-endpoint' into feat/…
borkarsaish65 3340e9a
perf: batch acronym lookups instead of one Redis/DB round-trip per token
borkarsaish65 1649fc1
chore: stop tracking tests/ in this branch
borkarsaish65 0ad1a23
fix: restore tracking for pre-existing baseline test files
borkarsaish65 33ca9d2
perf: batch title/summary expansion checks into one Qdrant call
borkarsaish65 6de6425
perf: filter English stopwords out of multi-word acronym candidates
borkarsaish65 18cb784
refactor: merge acronym multi-embedding fan-out into the single searc…
borkarsaish65 eb282f8
fix: address PR #7 review comments (cache batching, validation, comme…
borkarsaish65 f6b045c
feat: surface detected acronym + expansions in search response
borkarsaish65 ef2a52e
feat: support enable/disable via the acronym bulk upload CSV
borkarsaish65 5ef26e7
docs: add release-2.1.0 deployment doc for acronym-aware search
borkarsaish65 a120f1f
Merge remote-tracking branch 'origin/release-2.1.0' into feat/acronym…
borkarsaish65 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
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,150 @@ | ||
| # A generic, single database configuration. | ||
|
|
||
| [alembic] | ||
| # path to migration scripts. | ||
| # this is typically a path given in POSIX (e.g. forward slashes) | ||
| # format, relative to the token %(here)s which refers to the location of this | ||
| # ini file | ||
| script_location = %(here)s/migrations | ||
|
|
||
| # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s | ||
| # Uncomment the line below if you want the files to be prepended with date and time | ||
| # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file | ||
| # for all available tokens | ||
| # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s | ||
| # Or organize into date-based subdirectories (requires recursive_version_locations = true) | ||
| # file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s | ||
|
|
||
| # sys.path path, will be prepended to sys.path if present. | ||
| # defaults to the current working directory. for multiple paths, the path separator | ||
| # is defined by "path_separator" below. | ||
| prepend_sys_path = . | ||
|
|
||
|
|
||
| # timezone to use when rendering the date within the migration file | ||
| # as well as the filename. | ||
| # If specified, requires the tzdata library which can be installed by adding | ||
| # `alembic[tz]` to the pip requirements. | ||
| # string value is passed to ZoneInfo() | ||
| # leave blank for localtime | ||
| # timezone = | ||
|
|
||
| # max length of characters to apply to the "slug" field | ||
| # truncate_slug_length = 40 | ||
|
|
||
| # set to 'true' to run the environment during | ||
| # the 'revision' command, regardless of autogenerate | ||
| # revision_environment = false | ||
|
|
||
| # set to 'true' to allow .pyc and .pyo files without | ||
| # a source .py file to be detected as revisions in the | ||
| # versions/ directory | ||
| # sourceless = false | ||
|
|
||
| # version location specification; This defaults | ||
| # to <script_location>/versions. When using multiple version | ||
| # directories, initial revisions must be specified with --version-path. | ||
| # The path separator used here should be the separator specified by "path_separator" | ||
| # below. | ||
| # version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions | ||
|
|
||
| # path_separator; This indicates what character is used to split lists of file | ||
| # paths, including version_locations and prepend_sys_path within configparser | ||
| # files such as alembic.ini. | ||
| # The default rendered in new alembic.ini files is "os", which uses os.pathsep | ||
| # to provide os-dependent path splitting. | ||
| # | ||
| # Note that in order to support legacy alembic.ini files, this default does NOT | ||
| # take place if path_separator is not present in alembic.ini. If this | ||
| # option is omitted entirely, fallback logic is as follows: | ||
| # | ||
| # 1. Parsing of the version_locations option falls back to using the legacy | ||
| # "version_path_separator" key, which if absent then falls back to the legacy | ||
| # behavior of splitting on spaces and/or commas. | ||
| # 2. Parsing of the prepend_sys_path option falls back to the legacy | ||
| # behavior of splitting on spaces, commas, or colons. | ||
| # | ||
| # Valid values for path_separator are: | ||
| # | ||
| # path_separator = : | ||
| # path_separator = ; | ||
| # path_separator = space | ||
| # path_separator = newline | ||
| # | ||
| # Use os.pathsep. Default configuration used for new projects. | ||
| path_separator = os | ||
|
|
||
| # set to 'true' to search source files recursively | ||
| # in each "version_locations" directory | ||
| # new in Alembic version 1.10 | ||
| # recursive_version_locations = false | ||
|
|
||
| # the output encoding used when revision files | ||
| # are written from script.py.mako | ||
| # output_encoding = utf-8 | ||
|
|
||
| # database URL. This is consumed by the user-maintained env.py script only. | ||
| # other means of configuring database URLs may be customized within the env.py | ||
| # file. | ||
| # Overridden at runtime by migrations/env.py from settings.DATABASE_URL (env-driven). | ||
| # sqlalchemy.url = driver://user:pass@localhost/dbname | ||
|
|
||
|
|
||
| [post_write_hooks] | ||
| # post_write_hooks defines scripts or Python functions that are run | ||
| # on newly generated revision scripts. See the documentation for further | ||
| # detail and examples | ||
|
|
||
| # format using "black" - use the console_scripts runner, against the "black" entrypoint | ||
| # hooks = black | ||
| # black.type = console_scripts | ||
| # black.entrypoint = black | ||
| # black.options = -l 79 REVISION_SCRIPT_FILENAME | ||
|
|
||
| # lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module | ||
| # hooks = ruff | ||
| # ruff.type = module | ||
| # ruff.module = ruff | ||
| # ruff.options = check --fix REVISION_SCRIPT_FILENAME | ||
|
|
||
| # Alternatively, use the exec runner to execute a binary found on your PATH | ||
| # hooks = ruff | ||
| # ruff.type = exec | ||
| # ruff.executable = ruff | ||
| # ruff.options = check --fix REVISION_SCRIPT_FILENAME | ||
|
|
||
| # Logging configuration. This is also consumed by the user-maintained | ||
| # env.py script only. | ||
| [loggers] | ||
| keys = root,sqlalchemy,alembic | ||
|
|
||
| [handlers] | ||
| keys = console | ||
|
|
||
| [formatters] | ||
| keys = generic | ||
|
|
||
| [logger_root] | ||
| level = WARNING | ||
| handlers = console | ||
| qualname = | ||
|
|
||
| [logger_sqlalchemy] | ||
| level = WARNING | ||
| handlers = | ||
| qualname = sqlalchemy.engine | ||
|
|
||
| [logger_alembic] | ||
| level = INFO | ||
| handlers = | ||
| qualname = alembic | ||
|
|
||
| [handler_console] | ||
| class = StreamHandler | ||
| args = (sys.stderr,) | ||
| level = NOTSET | ||
| formatter = generic | ||
|
|
||
| [formatter_generic] | ||
| format = %(levelname)-5.5s [%(name)s] %(message)s | ||
| datefmt = %H:%M:%S | ||
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,25 @@ | ||
| import secrets | ||
| from typing import Optional | ||
|
|
||
| from fastapi import Header, HTTPException | ||
|
|
||
| from app.config import settings | ||
|
|
||
|
|
||
| def verify_internal_token(x_internal_token: Optional[str] = Header(None)) -> None: | ||
| """Gate internal-only endpoints behind a shared secret (X-Internal-Token header). | ||
|
|
||
| Header is optional so a missing one fails with the same 401 as a wrong one, | ||
| not a 422 that leaks the auth mechanism. compare_digest on utf-8/ | ||
| surrogateescape-encoded bytes gives constant-time comparison (no timing | ||
| side-channel) without crashing on non-ASCII header bytes. | ||
| """ | ||
| if ( | ||
| not settings.INTERNAL_API_TOKEN | ||
| or not x_internal_token | ||
| or not secrets.compare_digest( | ||
| x_internal_token.encode("utf-8", "surrogateescape"), | ||
| settings.INTERNAL_API_TOKEN.encode("utf-8", "surrogateescape"), | ||
| ) | ||
| ): | ||
| raise HTTPException(status_code=401, detail="Invalid or missing internal token") |
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 |
|---|---|---|
| @@ -1,8 +1,9 @@ | ||
| from fastapi import APIRouter | ||
| from app.api.v1.endpoints import documents, query, cache | ||
| from app.api.v1.endpoints import documents, query, cache, acronyms | ||
|
|
||
| api_router = APIRouter() | ||
|
|
||
| api_router.include_router(documents.router, prefix="", tags=["documents"]) | ||
| api_router.include_router(query.router, prefix="/query", tags=["query"]) | ||
| api_router.include_router(cache.router, prefix="/cache", tags=["cache"]) | ||
| api_router.include_router(acronyms.router, prefix="/acronyms", tags=["acronyms"]) |
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,85 @@ | ||
| import csv | ||
| import io | ||
| import logging | ||
|
|
||
| from fastapi import APIRouter, Depends, File, HTTPException, UploadFile | ||
| from starlette.concurrency import run_in_threadpool | ||
|
|
||
| from app.api.deps import verify_internal_token | ||
| from app.config import settings | ||
| from app.constants import ACRONYM_CSV_COLUMN_ACRONYM, ACRONYM_CSV_COLUMN_EXPANSIONS | ||
| from app.models.api_models import AcronymBulkUploadResponse | ||
| from app.services.acronym_service import bulk_upsert, invalidate_cache, refresh_cache | ||
|
|
||
| router = APIRouter() | ||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| @router.post("/bulk", response_model=AcronymBulkUploadResponse, dependencies=[Depends(verify_internal_token)]) | ||
| async def bulk_upload_acronyms(file: UploadFile = File(...)): | ||
| """Internal-only: upsert acronym -> expansions rows from a CSV upload (spec | ||
| §7). Columns: acronym, expansions (pipe-separated), description (optional), | ||
| is_active (optional, "true"/"false" — defaults to active if omitted). | ||
| A bad row is reported in `errors`, not a batch failure — the rest commits. | ||
| """ | ||
| raw = await file.read() | ||
| max_bytes = settings.ACRONYM_BULK_UPLOAD_MAX_SIZE_MB * 1024 * 1024 | ||
| if len(raw) > max_bytes: | ||
| raise HTTPException( | ||
| status_code=413, | ||
| detail=f"File exceeds max size of {settings.ACRONYM_BULK_UPLOAD_MAX_SIZE_MB}MB", | ||
| ) | ||
|
|
||
| try: | ||
| # utf-8-sig strips a leading BOM if present (common in CSVs exported | ||
| # from Excel/Sheets) and is otherwise identical to plain utf-8 — a | ||
| # BOM left in place would silently become part of the first header | ||
| # name ("acronym"), making every row fail acronym validation. | ||
| text = raw.decode("utf-8-sig") | ||
| except UnicodeDecodeError: | ||
| raise HTTPException(status_code=400, detail="File must be UTF-8 encoded") | ||
|
|
||
| reader = csv.DictReader(io.StringIO(text)) | ||
| required_columns = {ACRONYM_CSV_COLUMN_ACRONYM, ACRONYM_CSV_COLUMN_EXPANSIONS} | ||
| if not reader.fieldnames or not required_columns.issubset(reader.fieldnames): | ||
| raise HTTPException( | ||
| status_code=400, | ||
| detail=f"CSV must have columns {sorted(required_columns)}, found {reader.fieldnames}", | ||
| ) | ||
|
|
||
| rows = list(reader) | ||
| if not rows: | ||
| raise HTTPException(status_code=400, detail="CSV file has no data rows") | ||
|
|
||
| # bulk_upsert/refresh_cache/invalidate_cache are all synchronous, blocking | ||
| # Postgres/Redis I/O — run_in_threadpool keeps them off the event loop. | ||
| created, updated, deactivated, errors = await run_in_threadpool(bulk_upsert, rows) | ||
|
|
||
| # Spec §7: commit first (bulk_upsert already did), then refresh the cache. | ||
| # deactivated rows go straight to invalidate_cache (no DB round-trip needed, | ||
| # bulk_upsert already knows their status) — refresh_cache's own query filters | ||
| # to is_active=true, so it would silently skip them and leave their old | ||
| # cached expansion in place. If anything in this block fails, invalidate the | ||
| # whole batch instead so the next lookup reloads from Postgres rather than | ||
| # serving stale data. | ||
| if created or updated: | ||
| active_batch = [a for a in (created + updated) if a not in deactivated] | ||
| try: | ||
| if active_batch: | ||
| await run_in_threadpool(refresh_cache, active_batch) | ||
| if deactivated: | ||
| await run_in_threadpool(invalidate_cache, deactivated) | ||
| except Exception as e: | ||
| logger.warning(f"Cache refresh failed after bulk upload, invalidating instead: {e}") | ||
| await run_in_threadpool(invalidate_cache, created + updated) | ||
|
|
||
| logger.info( | ||
| f"Acronym bulk upload: {len(created)} created, {len(updated)} updated, " | ||
| f"{len(errors)} error(s)" | ||
| ) | ||
| return AcronymBulkUploadResponse( | ||
| received=len(rows), | ||
| created=len(created), | ||
| updated=len(updated), | ||
| errors=errors, | ||
| ) |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| ACRONYM_CACHE_KEY_PREFIX = "acronym" | ||
|
|
||
| # Bulk-upload CSV column names (spec §7) | ||
| ACRONYM_CSV_COLUMN_ACRONYM = "acronym" | ||
| ACRONYM_CSV_COLUMN_EXPANSIONS = "expansions" | ||
| ACRONYM_CSV_COLUMN_DESCRIPTION = "description" | ||
| ACRONYM_CSV_COLUMN_IS_ACTIVE = "is_active" |
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.