docs: ADR 0001, one search library across Typesense and Meilisearch - #1
docs: ADR 0001, one search library across Typesense and Meilisearch#1blarghmatey wants to merge 4 commits into
Conversation
…h first-class Fills in the cookiecutter stub with the decision this repo was created to record. Reworked from openedx/openedx-platform#39049, which proposed the same abstraction one layer too low: private to content/search, with Typesense as a second backend rather than a first-class target. The scope boundary is what keeps this from repeating django-haystack. Both engines are Algolia-shaped document stores, so the abstraction is narrow; Elasticsearch and OpenSearch stay out, as modular-learning#245 already concluded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ULXYpjr5Hq3s9K4imMvsWf
api.py holds 115 Meilisearch references on master, not 105, and views.py has two rather than none. The Elasticsearch licence passage is quoted in full: the truncated version dropped the AWS qualifier, which narrows what ADR 0001 actually claimed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ULXYpjr5Hq3s9K4imMvsWf
|
Thanks for the pull request, @blarghmatey! This repository is currently maintained by Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review. 🔘 Get product approvalIf you haven't already, check this list to see if your contribution needs to go through the product review process.
🔘 Provide contextTo help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:
🔘 Get a green buildIf one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green. DetailsWhere can I find more information?If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources: When can I expect my changes to be merged?Our goal is to get community contributions seen and reviewed as efficiently as possible. However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:
💡 As a result it may take up to several weeks or months to complete a review and merge your PR. |
Settles two of the open questions rather than leaving them for review. Algolia becomes a supported engine alongside Typesense and Meilisearch, not a possible future backend. Designing against three from the start is what makes the third cheap; discovering it later is what made haystack expensive. All four consumers adopt: content/search, edx-search's consumers, forum and edx-notes-api. Dropping Elasticsearch is now a migration with an end rather than a scope boundary, which means edx-notes-api cannot be left behind - it is the only consumer with no other path off the engine. Two costs this surfaces rather than hides. Algolia is SaaS-only with no official local emulator, so it cannot meet the same CI standard as the two self-hostable engines, and that asymmetry is a reason not to shape the interface around it. And edx-notes-api declares custom Elasticsearch analyzers with snowball stemming and an html_strip char filter, which no Algolia-shaped engine exposes: relevance will change, and the highlight markers edxapp consumes have to be preserved deliberately. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ULXYpjr5Hq3s9K4imMvsWf
Algolia drops out of the implementation and stays adoptable. Building it now would mean carrying a backend with no user, and being SaaS-only it cannot run in CI, which is how forum's Typesense support shipped broken. The commitment that survives is that adding it later is additive: an interface fitting two Algolia-shaped engines should fit a third, and the frontend adapter contract is Algolia's own client interface. The frontend abstracts at the InstantSearch client interface using the vendors' existing adapters instead of a seam of our own. This rests on a distinction the previous draft got wrong: InstantSearch's widgets cannot do multi-select hierarchical facets, which is why search-manager was hand-written, but its client interface has nothing to do with that. Both adapters implement searchForFacetValues, and instant-meilisearch passes facetQuery and facetName straight through to the method search-manager already calls. Decision 9 was too weak. Saying the API must not assume a task queue does not answer what happens on the engine that has one; picking the synchronous engine is not a design. Submitting a write is now separate from confirming it landed, and only a worker may wait. Also: edx-search stayed Elasticsearch-oriented after haystack was removed, and Elasticsearch memory use was a motivation independent of licensing (tutor#1141). 36868 is a closed issue rather than an open PR, and what shipped for it (38384) is prior art with a design point worth not relitigating. Studio courseware and library content are separate use cases. Retiring Elasticsearch needs a DEPR. content/search adopts first. Typesense holding its whole index in RAM is a real argument for Meilisearch as the default. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ULXYpjr5Hq3s9K4imMvsWf
|
Thanks, that was a useful review. Revised in fc8979d and replied on each thread. Everything is taken; four items reversed something I had written, so here is the short version of what moved. Algolia is out of the implementation. You are right that building a backend nobody has asked for is not worth the maintenance, and it also resolves the CI objection I had raised against my own proposal. What I kept is narrower: the backend interface and the frontend adapter contract are specified so adding Algolia is additive rather than a redesign. There is a Rejected Alternatives entry recording why it is not being built, so the next person to propose it finds the reasoning. The frontend uses your adapter suggestion, and that is the change I am most glad about, because it replaces something I was proposing to build and maintain. It also corrected a mistake of mine: I had written that the frontend must support multi-select hierarchical facets "which InstantSearch does not," conflating the widgets with the client interface. Only the widget limitation is real. I checked the part that decides whether the adapter route works, since the tag tree is what would fall through a shared abstraction: both adapters implement Your async point landed. Saying the API must not assume a task queue does not answer what happens on the engine that has one, and choosing Typesense is not a design. Submitting a write is now separate from confirming it landed, and the wait is callable only from a worker. I deliberately left the "how does the browser know" half to #38993 rather than presume it, but if you think the ADR should commit to a mechanism instead of reserving space for one, that is much cheaper to change now than after there are implementations. Typesense's memory profile is a better argument for Meilisearch as the default than anything I had against it, and I had not been weighing it. Verified: entire index in RAM, roughly 2-3x the searchable-field size, where Meilisearch can serve an index larger than memory. Recorded as an Open Question with your condition attached. It does not undercut the HA case, since replication under an open licence and indexing cost are separable, and the default favouring the cheaper-scaling engine is compatible with the other being fully supported. Two smaller things worth flagging rather than leaving in the diff:
Still open and genuinely undecided: the default engine, whether Studio courseware and learner courseware should share an index (your question, now explicit rather than answered by implication), and who picks up each consumer's migration. @ormsbee this is ready for your read whenever you have time. The one structural question is whether ADR 0001 should stay a short charter with this as 0002, since I filled in the stub rather than adding a file. Happy either way. |
|
Thanks @blarghmatey, sounds great. I'm not sure why, but GitHub isn't letting me mark all my comments as resolved now that you've addressed them. |
|
Resolved them all from my side, since as PR author I can. Every one had a reply first, so the reasoning is still there if anyone wants to reopen a thread rather than take my word that it was addressed. The two I would most want a second look at, since I made judgement calls rather than just applying a suggestion: whether Decision 9 should commit to a mechanism for how the browser learns the index caught up rather than reserving space for whatever #38993 concludes, and whether leaving the default engine open is right when your RAM argument is fairly decisive on its own. |
We generally want to keep ADRs short and more limited in scope, largely because (a) it keeps discussions from spiraling on forever; and (b) it makes it easier to find and change our minds on individual decisions in the future. This document lists ten decisions, which I would normally expect to come in at least 3-4 ADRs (e.g. backend engine support, frontend approach, platform adoption plan, etc.) That being said, much of what's written here is a summary and natural evolution of many, many discussions we've already had on this topic, and I think it's a very useful synthesis of where we are, how we got here, and the core architectural approach we want to adopt in order to address the shortcomings of all our existing systems. So unless we get really bogged down on one of the sub-decisions here, I think it's fine to leave as a big ADR 0001. The one thing I'll ask is that when we inevitably need to flesh things out later in the development cycle, we do it as new ADRs and update 0001 to make references to those new ADRs, instead of trying to update them in place here. For instance, at some point we may want an ADR to explicitly lay out how we expect clients of this library to test their registered use cases against real engines, and best practices for this. |
ormsbee
left a comment
There was a problem hiding this comment.
LGTM. I'm very interested in hearing @Ali-Salman29's thoughts on this.
| * **Notes** are served by ``edx-notes-api``, still on Elasticsearch 7 through | ||
| ``django-elasticsearch-dsl-drf``. It is the last consumer with no path off | ||
| Elasticsearch at all. |
There was a problem hiding this comment.
I just found out this isn't accurate: edx-notes-api does already support Meilisearch: https://github.com/openedx/edx-notes-api/blob/master/notesapi/v1/views/meilisearch.py
ADR 0001 for this repo: one search library for the platform's four search consumers, with Typesense and Meilisearch both first-class, Elasticsearch retired rather than merely excluded.
Reworked from openedx/openedx-platform#39049 per the conclusion there (@bradenmacdonald's preference was a new
openedx_searchrepo with the revised proposal as its ADR 0001), then revised again from his review here. #39049 is closed pointing at this.I filled in the existing
0001-purpose-of-this-repo.rststub rather than adding an0002, on the reading that "why this repo exists" and "what it decided" are the same document. Happy to split it into a short charter plus an0002if you would rather 0001 stay brief.What it decides
content/search(Meilisearch only),edx-search(four engines per its README),openedx/forum(es/meilisearch/typesense), andedx-notes-api(Elasticsearch 7 only).edx-searchas a prerequisite.What changed from the review
Four of these reversed something I had written, so they are worth calling out rather than burying in a diff.
Algolia is out of the implementation and only stays adoptable. Building it now means carrying a backend with no user, and being SaaS-only it cannot run in CI, which is precisely how forum's Typesense backend shipped non-functional. What survives is the useful half: the interface and adapter contract are specified so adding it is additive. On the frontend that is nearly free, because the adapter contract is Algolia's client interface.
The frontend uses the existing InstantSearch adapters (
typesense-instantsearch-adapter,instant-meilisearch) with React Query and Paragon, instead of the "engine-agnostic client seam" I had proposed building. This corrected a conflation in my own text: InstantSearch's widgets cannot do multi-select hierarchical facets, which is whysearch-managerwas hand-written, but its client interface has nothing to do with that, and that is all the adapters implement. I verified the part that decides it — both implementsearchForFacetValues, andinstant-meilisearchpassesfacetQuery/facetNamestraight through to the same Meilisearch methodsearch-managercalls today. So the tag tree keeps its bespoke UI and loses only its bespoke transport.Decision 9 was too weak. Saying the API must not assume a task queue does not answer what happens on the engine that has one, and picking the synchronous engine is not a design. Submitting a write is now separate from confirming it landed, and only a worker may wait. How the browser learns the index caught up is deliberately left to #38993.
Typesense holds its whole index in RAM (roughly 2-3x the searchable-field size), which is a real argument for Meilisearch as the default and one I had not been weighing. Recorded with @bradenmacdonald's condition attached. It does not undercut the HA argument: replication under an open licence and indexing cost are separable, and it is fine for the default to favour the cheaper-scaling engine while the other stays fully supported.
Also from review:
edx-searchstayed Elasticsearch-oriented even after haystack was removed, which is the missing explanation for why adding a second engine there is awkward; Elasticsearch's memory footprint was a motivation independent of licensing (tutor#1141); Studio courseware and library content are separate use cases, and splittingstudio_contentbecomes ordinary registration rather than a special case;content/searchadopts first, because a library that handles the hardest case handles the rest.One correction to a review request: #36868 is an issue rather than a PR, and closed as completed. What shipped for it is #38384, now cited as prior art, along with the design point it settled (Django migrations run once, so index schema changes cannot be migration files).
Still open in the ADR
Default engine; whether Studio courseware and learner courseware share an index; who picks up each consumer's migration.
Evidence
Claims are verified against live sources rather than memory, and where a claim is someone's account rather than something I checked, it says so. The facet feasibility result (29 checks against a real Typesense 30.2) and the same-host indexing measurements are carried over from #39049.
doc8 --max-line-length 120is clean, including its docutils validity check.🤖 Generated with Claude Code
https://claude.ai/code/session_01ULXYpjr5Hq3s9K4imMvsWf