197 lines
12 KiB
Markdown
197 lines
12 KiB
Markdown
# Shared research API
|
||
|
||
This work belongs to Thesisly 研迹 and uses Foundry Core rather than a separate
|
||
application backend. The corpus is public; account sessions and future private
|
||
collections remain project scoped.
|
||
|
||
## Current implementation boundary
|
||
|
||
`internal/research` implements the HTTP contract and a PostgreSQL repository
|
||
based on the fixed Paper Atlas `a3b677b` schema (TEXT/BIGINT columns, native
|
||
English title/abstract search index). `cmd/foundry` enables it only when
|
||
`FOUNDRY_RESEARCH_DATABASE_URL` is configured. The Paper Atlas migration is now published: 21 tables passed its independent
|
||
count/content-hash acceptance. A dedicated `foundry_research_reader` role has
|
||
SELECT on the ten tables consumed here, schema USAGE, and read-only defaults.
|
||
Its connection is stored only in the protected server file below.
|
||
The existing SQLite implementation was withdrawn. Real-corpus integration has
|
||
been checked through a loopback Core and the local frontend. Formal production
|
||
release still requires the user to merge and choose to deploy; this branch is
|
||
not deployed by these checks. Very broad queries remain subject to the budget
|
||
and performance limitation documented below.
|
||
Do not add `FOUNDRY_RESEARCH_DATABASE`.
|
||
|
||
The server-side deployment may load a protected
|
||
`/opt/endoleap-infra/foundry/research.env` containing
|
||
`FOUNDRY_RESEARCH_DATABASE_URL` and `FOUNDRY_RESEARCH_SCHEMA` (default `research`).
|
||
Keep the connection string out of Git, Actions variables, browser configuration
|
||
and command arguments. Use a dedicated role with schema USAGE and table SELECT
|
||
only. Core uses a separate two-connection pool, read-only connection defaults and
|
||
read-only transactions. Each research transaction disables JIT locally: the
|
||
measured count plan spent about 2.85 seconds compiling JIT functions, and its
|
||
end-to-end execution exceeded eight seconds. This does not alter shared
|
||
PostgreSQL settings or replace exact counts with estimates. Startup checks the published schema/SELECT permissions;
|
||
an explicitly enabled but invalid connection fails candidate readiness.
|
||
|
||
Deployment migration defaults now follow the shared `foundry-postgres`
|
||
container name. `FOUNDRY_POSTGRES_CONTAINER` or the existing `--container` option
|
||
can select a different container without changing database/table names.
|
||
|
||
## Routes
|
||
|
||
- `GET /projects/{project}/research/papers`
|
||
- `GET /projects/{project}/research/papers/{id}`
|
||
|
||
Only configured Foundry projects are accepted. Reading the public corpus does
|
||
not require a session; these routes do not expose private data or accept writes.
|
||
|
||
List parameters are `q` (200 Unicode characters, 20 words), `searchIn`
|
||
(`text` or `author`), `topic` (an exact arXiv category), `sort` (`recent` or
|
||
`title`), `cursor` (optional, opaque, at most 32768 characters), and
|
||
`pageSize` (1–50, default 8). Unknown, malformed
|
||
or repeated parameters return 400. `recent` means descending stable paper ID,
|
||
not publication date. Search terms must become bound query parameters, never
|
||
raw SQL or user-supplied full-text syntax.
|
||
|
||
Title/abstract search uses the migration's English vector with
|
||
`plainto_tsquery`; author search uses simple-language terms over the stored
|
||
author JSON. Both treat input as terms, never full-text operators. Initial full-corpus measurements showed eight-second timeouts for text,
|
||
author and topic counts; the unfiltered count took 7.25 seconds. Execution plans
|
||
confirmed sequential scans for authors and category joins.
|
||
`deploy/research/query-indexes.sql` supplies operator-run concurrent indexes for
|
||
those two missing access paths. It is separate from Core startup and auth
|
||
migrations; do not grant DDL to the runtime reader. Both indexes completed on the confirmed ECS (category 35.4 seconds, author
|
||
436.47 seconds); measured end-to-end checks are documented below.
|
||
|
||
List responses contain `items`, `nextCursor`, `previousCursor`, `topics`, and
|
||
`pageSize`. Empty cursor strings mean that direction is unavailable. There is
|
||
no `total`, page number, total page count or deep-page cap. The old `page`
|
||
parameter is rejected; coordinate the frontend/backend contract update.
|
||
Each paper preserves original title/abstract, authors, source dates, topics and
|
||
source links. Publication decisions need source evidence; absent decisions stay
|
||
unknown. No invented repositories, metrics or generated research conclusions.
|
||
|
||
Lists use a read-only repeatable-read snapshot without `count(*)` or `OFFSET`.
|
||
Keyset paging uses ID descending for recent order and `(title_key, id)` ascending
|
||
for title order, including a reverse query for previous pages. The title key is
|
||
kept in its stored codec and C collation. Cursors bind normalized query, search
|
||
scope, topic, sort and page size; changing filters requires a fresh cursor. They
|
||
are public row positions, not authorization tokens or guaranteed frozen snapshots
|
||
across requests. Concurrent corpus edits may alter later pages.
|
||
|
||
Only pageSize+1 positions are selected; the extra position indicates more data,
|
||
then full rows and batched metadata are loaded for at most pageSize papers.
|
||
For recent order with only a topic filter, page directly through the existing
|
||
`(tag_id,paper_id)` index and deduplicate evidence before LIMIT. For text/author
|
||
search in recent order, try an ID interval of 512 near the cursor, expanding
|
||
once to 4096 only if there are some matches but fewer than pageSize+1.
|
||
Only a full page plus one proves that interval contains the next page. If neither bounded attempt fills the page,
|
||
all local candidates are discarded and the complete indexed search is used;
|
||
rare or older matches are never silently truncated. The fallback materializes
|
||
matching IDs before sorting, avoiding an unbounded ordered heap scan for sparse
|
||
terms. Title-order filtered queries still use that complete search and can time
|
||
out for broad terms. No global database or proxy resource settings are changed.
|
||
The
|
||
HTTP layer permits two in-flight queries with an eight-second context budget.
|
||
Busy requests return 429 with `Retry-After: 1`; deadlines return 504; storage
|
||
errors return a generic 503 without database details. Missing papers return 404.
|
||
|
||
## Validation
|
||
|
||
`go test -race -count=1 ./internal/research` covers routing, query limits,
|
||
Unicode, anonymous reads, rejected mutations, project lookup, errors, saturation
|
||
and timeout slot release. Run compilation/tests only on the user-confirmed ECS
|
||
runner. These transport tests do not prove a database connection, full-corpus
|
||
query performance or production availability.
|
||
|
||
The PostgreSQL repository batches source/tag/identifier/submission queries in
|
||
the same read-only repeatable-read transaction. Author tuples are displayed as
|
||
given name, family name and suffix; an optional fourth affiliation field
|
||
is excluded from the display name (observed in real arXiv records); null abstracts/dates remain null. Publication
|
||
status comes from the latest observed decision, using decision date and ID as
|
||
tie-breakers, and stays unknown when no decision exists. Unit checks cover
|
||
author shapes, bound user values and schema qualification. The real Core detail
|
||
and homepage HTTP routes have returned 200.
|
||
The frontend has also rendered the actual 3,526,999-paper corpus through a
|
||
loopback SSH tunnel: eight rows, matching detail title/source, desktop and
|
||
mobile layout, no page exceptions or horizontal overflow in that check.
|
||
The earlier count-based API returned 146 author records in 0.336 seconds. Before the cursor optimization, after paging/author fixes, category `cs.AR` returned 9,457 records in 4.906
|
||
seconds, `CartoonGAN` returned three in 0.332 seconds, and the multi-term search
|
||
`attention is all you need` returned 8,653 in 6.325 seconds. These were sequential
|
||
manual HTTP checks, not a load test. Very broad text searches can still exceed
|
||
the eight-second budget; clients retain search controls so users can narrow
|
||
the query or retry. This known limit is not an estimated count or sample
|
||
fallback. This is not a
|
||
production deployment or an authentication acceptance result.
|
||
|
||
The existing `deploy/check-schema.sh` also loads the synthetic fixture in
|
||
`internal/research/testdata/corpus.sql` inside its disposable PostgreSQL
|
||
container (`--network none`, no host port). The research test process receives a
|
||
separate SELECT-only fixture role via `RESEARCH_TEST_DATABASE_URL`. These
|
||
integration checks execute real SQL for filtering, stable paging, nullable
|
||
content, source/tag/identifier joins, latest decision tie-breakers, permission
|
||
denial and SQL-backed HTTP routes. They never connect to the shared migration
|
||
instance and do not establish full-corpus performance or live-data correctness.
|
||
|
||
## Text storage compatibility (repair 4e62710)
|
||
|
||
The Paper Atlas repair uses `nul-soh-v1`: NUL is stored as SOH/SOH, and a
|
||
literal SOH as SOH/STX, preserving binary text ordering. Core reads the schema
|
||
comment `paper-atlas:text-codec=nul-soh-v1` through `pg_catalog` at startup,
|
||
restores SQL text before JSON parsing/API responses, and encodes bound search
|
||
terms. An absent/unrelated schema comment keeps legacy text unchanged; an
|
||
unsupported marked codec or malformed encoded text fails explicitly.
|
||
|
||
Schema comments are public metadata, so consumers need no privileges on the
|
||
private migration controller. The research role still needs only schema USAGE
|
||
and table SELECT. The codec adaptation is on the review branch, pushed with [skip ci] under the
|
||
user's no-CI instruction; the published schema marker has been verified with
|
||
the runtime reader.
|
||
All research tests passed manually against disposable PostgreSQL on the
|
||
confirmed ECS, including the encoded SQL fixture for schema detection, raw TEXT restoration,
|
||
JSON author/metadata decoding, search and the HTTP round trip. Passing isolated
|
||
fixtures does not establish full-corpus API performance.
|
||
|
||
|
||
## Identity handoff
|
||
|
||
The existing production Core received a configuration-only reload using its
|
||
unchanged release binary: the original projects were retained and thesisly was
|
||
registered. Both Google and Slack identity records were observed in this
|
||
project. The user independently confirmed each provider's real authorization,
|
||
reload/new-tab persistence and logout behavior. Browser control was unavailable,
|
||
so these browser results are attributed to user acceptance rather than an
|
||
automated real-account test. This does not deploy the research work branch;
|
||
mail delivery was not exercised.
|
||
|
||
|
||
## Cursor regression coverage
|
||
|
||
The disposable PostgreSQL fixture covers forward/backward traversal, identical
|
||
title keys with different IDs, published control-character encoding, sparse
|
||
matches separated by a large ID gap, and fallback from the bounded interval.
|
||
Invalid, oversized, cross-filter and wrong-version cursors are rejected before
|
||
repository execution. The runtime reader retains SELECT-only privileges; no DDL
|
||
or writes are added. Run tests manually on the confirmed ECS; skip CI for this
|
||
iteration and retain `[skip ci]` in commit and PR titles.
|
||
|
||
|
||
## Manual cursor measurements (2026-11-07)
|
||
|
||
On the confirmed ECS, after this task's compilation and disposable fixture had
|
||
finished, sequential direct requests to the temporary Core returned: homepage
|
||
65 ms, next/previous 39/21 ms, `graph` 345 ms, `CartoonGAN` 731 ms (3 papers),
|
||
`cs.AR` 145 ms, title order 180 ms, author `Buhrman` 769 ms, and
|
||
`attention is all you need` 1312 ms. Recent, topic and title cursor round trips
|
||
returned the same first page without overlap. These are individual manual
|
||
samples, not production latency guarantees; concurrent compilation earlier
|
||
produced multi-second cold measurements. No proxy, database memory, CPU or
|
||
production configuration was changed.
|
||
|
||
The final source compiled manually on the ECS; package unit checks and all ten
|
||
selected cursor/PostgreSQL integration tests passed, including interval expansion
|
||
and complete fallback. No CI or local build ran. The local authorized Next dev
|
||
uses the temporary API through the existing SSH tunnel. Independent Chrome also
|
||
verified real homepage/search navigation, a preserved document, one mobile
|
||
header/main, no page exceptions and no horizontal overflow. The production Core
|
||
was not replaced by these temporary preview updates.
|