Michigan Appellate Case Search

About this tool

An independent, public finding aid for recent Michigan appellate decisions. It helps you locate documents; it does not write summaries, answer legal questions, or generate any text. It is not a product of, affiliated with, or endorsed by the Michigan courts, and nothing here is legal advice.

What is in the collection

SetCourtPeriodDocuments
ordersSupreme Court1987–202641,485
unpublished opinionsCourt of Appeals2016–202617,733
ordersCourt of Appeals20265,994
published opinionsCourt of Appeals2016–20262,002
opinionsSupreme Court2000–20261,077
other opinions (dissents, remand, reissued)Court of Appeals20268
Total68,299

Every document was retrieved from the Michigan Courts’ own case-search API at courts.michigan.gov — the Court of Appeals sets and Supreme Court opinions on August 25, 2026, the Supreme Court orders on August 26, 2026 — and every record links back to its official PDF. About 280 documents (almost all one-page orders, most of them from the 1980s and 1990s) were scanned images or had unreadable text layers and were converted with optical character recognition; those pages are flagged. Half of the Supreme Court orders carry no docket number in the court’s own index; for those, the case number was read from the face of the order itself.

What is not here. Court of Appeals opinions before 2016 and its orders before 2026, Supreme Court opinions before December 2000, Supreme Court orders before September 1987, and anything filed after August 26, 2026. The 2026 year is in progress. Coverage is uneven by design and the gaps are not symmetric: Supreme Court orders reach back to 1987 and Court of Appeals opinions to 2016, but Court of Appeals orders exist only for 2026. Do not treat a “no results” answer as proof that no such decision exists.

How the collection is built

How the collection is builtFive stages in sequence: collect the PDFs from the courts’ website; read the text, using optical character recognition where a PDF has no text layer; catalogue the docket numbers, parties, panel, county and disposition; divide long decisions into passages of about 1,600 characters, each carrying a header that names the case; and index those passages as both vectors and full text in Postgres.1Collect
PDFs from courts.michigan.gov
2Read
text extraction; OCR where needed
3Catalogue
docket, parties, panel, county, disposition
4Divide
~1,600-character passages + header
5Index
vectors + full text in Postgres
68,443 decisions · 479,604 passages · every stage re-runnable from the PDFs

Every stage re-runs from the PDFs, so the collection can be rebuilt from scratch and the result checked against what is live. A gate between the passage-building stage and the database refuses the build if measured defect rates rise — passages that end mid-sentence, passages shorter than their own header, a separate opinion attributed to the wrong author.

The courts publish some decisions twice: a slip opinion on the decision date, then the reporter’s advance-sheets version reset for the Michigan Appeals Reports, and a consolidated appeal is filed identically under each of its docket numbers. 1,488 such copies are collapsed so a case appears once, and the surviving record answers to every docket number its copies carried.

How the documents are described

Each PDF was converted to text and read for the fields you can filter on: court, document type, publication status, decision date, docket and lower-court numbers, the parties in the caption, the trial court and county, the panel and authoring judge, the disposition (affirmed, reversed, remanded, leave denied…), and the statutes (MCL) and court rules (MCR) cited. Nothing is guessed by a model — every value comes from a pattern on the page or from the court’s own index.

Area of law is decoded from the two-letter case-type code the State Court Administrative Office assigns to every lower-court case number (for example 22-107852-CZ is a general civil case; 2023-001994-NA is a child protective case; FC and FH are felony prosecutions). Where a document has no lower-court number, the area is inferred from the caption (People v — criminal; In re … Minor — child welfare).

Three ways of finding a case run at once, and the results are merged:

What happens when you searchYour search is optionally narrowed by filters and by an exact phrase, which together decide which decisions are eligible. Two searches then run in parallel over the eligible passages: a meaning search, which converts your words into a vector and finds passages whose vectors are closest, and a word search using Postgres full-text indexing, which carries docket numbers, party names and exact quotations. The two candidate lists are merged by reciprocal rank fusion, a cross-encoder re-reads the top forty candidates against your query and reorders them, and the passages are collapsed so each decision appears once.Your search
description, number, or name
Eligibility
filters + exact phrase decide which decisions can be returned
Meaning search
your words → a vector; nearest passages by cosine distance
Word search
Postgres full text; carries numbers, names, exact quotes
Merge
reciprocal rank fusion of the two lists
Re-read
cross-encoder reorders the top 40; one row per decision
Filters run inside the query, never after it — so the count beside a filter always matches what you get.Nothing on the page is written by a model: it converts text to vectors and reorders candidates, nothing more.

The description search combines two signals. Every passage of every document is stored as a vector — a numeric representation of its meaning, produced by an embedding model (Qwen3-Embedding-4B) — and your description is turned into a vector the same way; passages whose vectors sit closest to yours are candidates. In parallel, a conventional full-text index finds passages containing your distinctive terms. The two candidate lists are fused, and a cross-encoder (Jina) re-reads the top candidates against your description and reorders them. All filters are applied inside the database query, so counts and results always agree.

No text on this site is generated. The models here do two jobs only: turning text into vectors so passages can be compared, and reordering candidates. Every word you read — the case name, the excerpt, the summary line, the outcome — is either taken verbatim from the court's document or extracted from it by a rule you can inspect in the open-source pipeline.

Exact phrases and operators

The ordinary search box accepts the syntax you would expect, and the advanced search puts the exact phrase and the description in separate fields with an explanation of what each one does.

You typeWhat it means
“public building exception”those words, in that order, verbatim
-settlementexclude decisions containing the word
euthanized OR euthanasiaeither word. Capitalised — a lower-case “or” inside a sentence is read as an ordinary word, because legal prose is full of it
358358a docket or lower-court number goes straight to an exact lookup

Tools and stack

Everything is open and inspectable. The pipeline is Python, the site is a Next.js application, and the whole index lives in one Postgres database — there is no separate search service, which is why a filter and a ranking can be expressed in a single query.

LayerWhat is usedWhy
CollectionPython + requests, against the courts’ own case-search APIPublic documents, fetched with a record of where and when
Text extractionPyMuPDF; Tesseract for 293 scanned documentsThe PDFs are born-digital, so a layout model would add cost without accuracy
MetadataPython, pattern-basedNo model guesses a field; every value traces to a pattern on the page
Embeddingsqwen/qwen3-embedding-4b at 1,024 dimensions, via OpenRouterMeasured against alternatives; 1,024 dimensions cost nothing detectable against the full width
DatabasePostgres 17 with pgvector (HNSW over halfvec) and tsvector full textVectors, text and filters in one place, so filters run inside the query
Re-rankingjina-reranker-v2-base-multilingual over the top 40Reorders candidates; currently under evaluation and may be removed
SiteNext.js 15 on Vercel; Supabase-hosted Postgres—
Evaluation460-question benchmark, bootstrap confidence intervals, a build gateEvery quality claim is reproducible from the repository

What it does not do well

These are measured, not suspected, and each is tracked in the open. Knowing them is part of using the tool.

Reading the results responsibly

Privacy and logging

Every search is recorded: the words you typed, any filters you applied, how many documents came back, and how long it took. There are no accounts and no cookies that identify you; your IP address is not stored, only a truncated one-way hash of it, which lets us count distinct visitors without knowing who they are. The point of the log is narrow — finding searches that return nothing, or that are too broad to narrow anything down, so the search can be improved. Please do not type anything into the search box you would not want recorded.

Contact and source

Built by Brian E. Perron, PhD, University of Michigan School of Social Work, as a companion to the Michigan Court Rules search. Found an error? Email beperron@umich.edu.