wardethan2000-eng turns document storage into matter-wide research

This fork makes the document collection a place to search across a matter and ask questions with page-level sources attached.

searchknowledge-management

The work combines conventional document search with local AI-generated meaning matching, so a query can surface both exact wording and relevant passages. It also keeps the text used for that matching on the organisation's own setup rather than sending it away for that step.

  • Matter-wide search can find passages across a document set, with page references.
  • Grounded answers consolidate results and cite the supporting documents and pages.
  • A search panel brings the capability into the documents view.
  • Image-only documents can still be found by filename, while Word, PowerPoint and text files have a safe fallback when no PDF is available.

The team has also batched the indexing work to avoid exhausting memory on larger collections.

So what Litigation and investigations teams should care if they want faster document triage without losing the ability to check where an answer came from.

View this fork on GitHub →

Spotted something wrong? Or know the PR text has fresher detail than the writeup above?

Commits in this thread

12 commits from wardethan2000-eng/mike, oldest first. Source extracted verbatim from the harvested git log.

SHA Subject Author Date
6f4d2edf feat: store document passages for search across a matter (stage 1) Ethan Ward 2026-08-18 ↗ GitHub
commit body
Groundwork for asking a question of a whole matter rather than of documents
handed over one at a time. Nothing user-facing yet.

Every document is now cut into passages when it is stored - after OCR for a
scan, since before that there is no text - and the passages are saved with the
page each came from, so a later search can cite by page. Storing happens after
the upload is answered and never fails it.

- lib/passages.ts    cuts page-labelled text into overlapping passages that
                     never span a page. Pure and unit-tested.
- lib/passageIndex.ts reads a stored version from whichever copy has the text
                     (the searchable PDF for scans, the file for a plain PDF,
                     the grid for a spreadsheet) and stores its passages,
                     replacing any held for that version so re-indexing is safe.
- migrations/20260818_01_document_passages.sql  the passages table, with
                     word-search and trigram indexes and user/project columns
                     so a search can be scoped to what the reader may see.
- scripts/backfillPassages.ts  indexes documents uploaded before this existed.

Word search over the stored passages is stage 3; the meaning fingerprint is a
later migration once the local model is chosen. Verified on the live server:
the existing scan indexed to page-numbered passages and word search found real
terms with correct pages.

New logic lives in new files; the edits to documents.ts and projects.ts only
add the call that stores passages after an upload.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PoCRDAPyEq4wwBVFFbDzck
dc4ed269 fix: index Word, PowerPoint and text files that never got a PDF rendition Ethan Ward 2026-08-18 ↗ GitHub
commit body
The passage indexer read every document through its PDF rendition, falling back
to the original file when there was none - but then still parsed those original
bytes as a PDF. A Word file whose LibreOffice conversion had failed at upload
therefore failed to index with "Invalid PDF structure". Found while indexing
existing documents: three real .docx letters failed this way.

versionText now reads the PDF rendition when one exists (it carries page
numbers), and otherwise reads the original with a reader that suits its type -
mammoth for Word, the slide reader for PowerPoint, the sheet reader for a
spreadsheet, plain text for a text file - and never parses non-PDF bytes as a
PDF. A Word file with no rendition indexes without page numbers, which is
correct: a Word document reflows and has no fixed pages.

Verified on the live server: the three failed letters now index (5, 3 and 5
passages of clean text), and the full backfill finished with 52 documents and
412 passages, 0 failures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PoCRDAPyEq4wwBVFFbDzck
b3146cf1 feat: search across a matter, with an assistant tool (stage 3) Ethan Ward 2026-08-18 ↗ GitHub
commit body
The assistant can now be asked a question of a whole matter - "which files
mention the summons", "where is the indemnity clause" - and find the answer
across every document, without being handed them by name.

- migrations/20260818_02_search_passages.sql - one database function that
  searches the passages. Full-text search understands word stems and phrases;
  a word-similarity fallback tolerates the character errors a scan carries, so
  an OCRd document is still found when whole-word search misses it. Only the
d92fffd7 feat: find a matter's documents by meaning, not just words (stage 4) Ethan Ward 2026-08-18 ↗ GitHub
commit body
Search now understands what a question means, not only the words in it.
Ask "who pays if this goes wrong" and it finds an indemnity clause that
never uses those words, alongside the exact-word and typo-tolerant
searches that were already there. The three run together and their results
are merged, so a passage found by more than one rises to the top; exact
words are weighted a little above the rest, so a party name or a section
number still lands first.

The meaning of each passage is read on our own machine, from a small model
(bge-small-en-v1.5) baked into the image, so no document text is sent
outside for this. Passages stored before now are filled in with a command
(backfillEmbeddings) that reads their text straight from the table, so no
document has to be read again.
1db72a9b feat: find image-only documents by their filename Ethan Ward 2026-08-18 ↗ GitHub
commit body
A photograph has no text to read, so an inspection photo named "Shattered
roof skylight leaving the interior exposed to weather elements" could not
be found by a search of the matter - recognition cannot help a picture
with no words in it. Now, when a document yields no text of its own, its
filename is stored as a single searchable passage, so the photo is found
by that description, by exact words or by meaning like anything else. A
result says plainly when the match was on the file's name rather than on
text inside the file, so the assistant does not quote a picture as if it
held words.
2e388a71 feat: a REST endpoint to search a matter's documents Ethan Ward 2026-08-18 ↗ GitHub
commit body
Adds GET /projects/:projectId/search - the same word-and-meaning search the
assistant runs, reachable directly so a search box in the interface can call
it without going through a conversation. It checks the caller may open the
project, searches only that project's documents (so a matter you are not on
never surfaces), and returns the best passages with their document, page, and
how each was found (words, meaning, a fuzzy match, or the file's own name).

This is the backbone for the search box; the box itself follows.
9618c5ea feat: one consolidated answer across a matter's documents Ethan Ward 2026-08-18 ↗ GitHub
commit body
Adds POST /projects/:projectId/search/answer - ask a whole matter a question
and get a single answer that cites the document and page for every point,
drawn only from the matter's own documents. It searches the passages (words
and meaning), hands the best ones to the model as its only source, and asks
for one grounded, cited answer; if the documents do not address the question
it says so rather than guessing. This is stage 7 of search across a matter,
reusing the same passages and page citations the assistant uses elsewhere.
642d4475 feat: a search box for a matter, in the documents view Ethan Ward 2026-08-18 ↗ GitHub
commit body
Adds the search box the earlier stages were building toward. In a project's
documents view you can now search the whole matter two ways: "Find passages"
lists the best-matching passages with their document, page, and how each was
found (exact words, meaning, an approximate match, or the file's own name);
"Ask the matter" returns one consolidated answer that cites the document and
page, drawn only from the matter's documents. Both call the endpoints added
earlier (GET /search and POST /search/answer); this is the interface for them.

Type-checked against the frontend; contains no build-tool-specific code.
dfa8adb7 fix: "Ask the matter" uses the model you have selected Ethan Ward 2026-08-18 ↗ GitHub
commit body
The consolidated-answer box was always asking the default model (Gemini),
so if that model was not set up it could never answer. It now uses whichever
model you have chosen in the app - the same one the assistant uses - so it
works with the provider you actually have configured.
3823eeb9 fix: matter-search routes 404 on a non-UUID project id Ethan Ward 2026-08-18 ↗ GitHub
commit body
The new /projects/:projectId/search and /search/answer routes matched
legacy paths like /projects/directory/search, since ":projectId" captured
"directory". A projectId that is not a UUID is never a real project, so both
routes now return 404 for one, which also restores the assertion that the old
project-directory search route is gone.
9c473ec2 fix: embed a document's passages in small batches Ethan Ward 2026-08-19 ↗ GitHub
commit body
Indexing a very long document (a few hundred pages) pushed all of its
passages through the embedding model in a single call, which exhausted
memory on a small machine and could stall or kill the process. Passages are
now embedded in small chunks, so a document of any length indexes within a
steady, small amount of memory. Order is unchanged.
35db5fd2 polish: render the matter answer as formatted text Ethan Ward 2026-08-19 ↗ GitHub
The "Ask the matter" answer showed its formatting marks (** for bold, - for
lists) as literal characters. It is now rendered properly - bold, bullet and
numbered lists, and headings - so a consolidated answer reads cleanly.

Capture this thread into my fork

Download a single Markdown prompt that tells Claude how to port every commit above into your working tree — adapting paths and structure to match your repo. Run it via claude -p < capture-thread-1253.md from inside the repo you want the changes in.

⬇ Download capture-thread-1253.md