You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: kupua/exploration/docs/03 Ce n'est pas une pipe dream/media-api-work/phase-3-d3-searchafter-worklog.md
+19-1Lines changed: 19 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,7 +2,7 @@
2
2
3
3
Implement Phase 3 D3: `POST /images/search-after` endpoint (Leg A: Scala media-api) and kupua DAL wiring (Leg B: TypeScript). Full workplan in `phase-3-d3-searchafter-workplan.md`.
4
4
5
-
**BLOCKED**on PR [guardian/grid#4752](https://github.com/guardian/grid/pull/4752) ("Add chips and filtering to AI Search", author: ellenmuller). See Session Log for details. Do not start implementation until that PR is merged.
5
+
**UNBLOCKED 2026-06-10**— PR #4752 merged to `main` (commit `cdf30a5ca`) **and `main` is now merged into `mk-next-next-next`** (the merge is an ancestor of HEAD). Two developments shape the plan: (1) the merged PR already extracted our `buildFilteredQuery` as `QueryBuilder.buildFilterOpt`; (2) the Sort Hole companion mandates Option B (client sends resolved sort clause). The workplan (`phase-3-d3-searchafter-workplan.md`) has been revised throughout to match. **Ready to implement Leg A.** See Session Log 2026-06-10 entry.
6
6
7
7
## Session Log
8
8
@@ -104,3 +104,21 @@ Today's work added two things relevant to D3 implementation:
104
104
**`must_not: nested(usages.status:replaced)` default filter:** Added to `buildQuery` to mirror Kahuna's `thingsToHideByDefault`. This fires on ALL kupua queries (not just cursor pagination). When Leg B wires the new `POST /images/search-after` endpoint, verify the ES queries flowing to the server still include this `must_not` clause (it comes from `buildQuery(params)` which is called client-side in the ES adapter — it will be present in the query the server endpoint receives in its `q`/filter params). No action needed for Leg A (server never builds this rule). For Option A (future): this rule must be added to `buildFilteredQuery` on the server.
105
105
106
106
**`UsageFilterAggRequest` / nested aggs in `getAggregations`:** Not relevant to D3 itself, but affects the future C1/C2 aggregations endpoint — see phase-3 findings §7 observation 11.
Verified against the actual merge diff (`git diff cdf30a5ca^1 cdf30a5ca`). The merge is on `main` but **NOT yet in `mk-next-next-next`** (`git merge-base --is-ancestor cdf30a5ca HEAD` → false). Merge `main` in before implementing.
111
+
112
+
What 4752 actually did (8 files, +403/−139):
113
+
114
+
1.**`QueryBuilder.buildFilterOpt(params: SearchParams, searchFilters: SearchFilters, syndicationFilter: SyndicationFilter): Option[Query]`** — the ENTIRE ~80-line filter-assembly block (incl. the `dateAddedToCollectionFilter`/pathHierarchy special case) was lifted verbatim out of `search()` into this new public method on `QueryBuilder`. `search()` now just calls it: `val filterOpt = queryBuilder.buildFilterOpt(params, searchFilters, syndicationFilter)`. **Our `buildFilteredQuery` extraction task is now obsolete** — `searchAfter()` calls `buildFilterOpt` directly and combines exactly as `search()` does: `filterOpt.map(f => boolQuery() must query filter f).getOrElse(query)`. This removes our planned modification of Kahuna-serving `search()` — strictly safer.
115
+
2.**`SearchParams` gained `lazy val aiQueryParts: AiQueryParts`** (computed from `structuredQuery`). It's a body val, not a constructor param → `SearchParamsBody.fromJson` is unaffected; it auto-computes. No action.
116
+
3.**`MediaApi.scala` AI refactor** touched only `performAiSearchAndRespond`/`semanticSearchByText`/`semanticSearchByImage`/`parseAiSearchMode` + new `buildAiFilter`. **`imageSearch()` / `hitToImageEntity` untouched** → our lift-to-private-method plan still applies cleanly.
117
+
4. New `SimilarField`/`SimilarValue` querysyntax + `QueryBuilder``SimilarField → matchNoneQuery` outside AI mode. Not relevant to D3.
118
+
5.`knnSearch`/`hybridSearch`/`makeHybridSearchRequest`/`fetchMaxBm25Score` now take `filterOpt: Option[Query]`. Not relevant to D3 (cursor path is non-AI).
119
+
120
+
**Net plan changes:**
121
+
- DELETE "Extract `buildFilteredQuery`" task. Replace with: in `searchAfter()`, `val filterOpt = queryBuilder.buildFilterOpt(params, searchFilters, syndicationFilter)` (all three are `val`s on `ElasticSearch`).
122
+
- The pathHierarchy both-orders edit (workplan's "one allowed change to shared code") now targets `QueryBuilder.buildFilterOpt`. Still matches only `Some("dateAddedToCollection")` (DESC); adding the asc token remains behaviour-preserving for Kahuna. One-line edit. **Re-evaluate whether it's even needed under Option B** — orderBy is read server-side ONLY for this filter; if kupua sends `dateAddedToCollection` asc, the filter must fire for both tokens, so the edit stands.
123
+
- All Sort-Hole Option B work (`reverseSorts`, `jsonToSort`, `SearchAfterParams.sort: Seq[JsObject]`, length-parity validation) is unaffected by the merge and lands in the same commits.
124
+
-`SearchParamsBody.fromJson`, `hitToImageEntity` lift, `sorts.scala` additions, routes, tests — all unaffected.
**Status:**BLOCKED — waiting on PR [guardian/grid#4752](https://github.com/guardian/grid/pull/4752)to merge. That PR touches `ElasticSearch.scala` (moves filter construction into `QueryBuilder`), `ElasticSearchModel.scala` (adds `AiQueryParts`/`aiQueryParts`), and `MediaApi.scala`. Implement only after it merges — re-read those three files before starting, as the code shape may differ from what this workplan assumes. Full context in `phase-3-d3-searchafter-worklog.md`.
3
+
**Status:**READY TO IMPLEMENT. PR [guardian/grid#4752](https://github.com/guardian/grid/pull/4752)("Add chips and filtering to AI Search") merged to `main` (commit `cdf30a5ca`) and is now in `mk-next-next-next` (merged 2026-06-10). **That merge changed the landscape and this workplan has been revised accordingly:**
4
4
5
-
**Pre-block status:** Ready to implement. B1 and B2 refactors are complete.
5
+
- 4752 already extracted the ~80-line filter-assembly block out of `ElasticSearch.search()` into `QueryBuilder.buildFilterOpt(params, searchFilters, syndicationFilter): Option[Query]` (`QueryBuilder.scala:131`). The planned `buildFilteredQuery` extraction is therefore **obsolete** — `searchAfter()` calls the merged `buildFilterOpt` directly and **does not modify Kahuna's `search()`** at all (strictly safer than the original plan).
6
+
- 4752 added `SearchParams.aiQueryParts` — a `lazy val` derived from `structuredQuery`, **not** a constructor parameter — so `SearchParamsBody.fromJson` and `SearchParams` construction are unaffected.
7
+
- 4752 refactored only the AI-search methods (`performAiSearchAndRespond`/`semanticSearch*`/`parseAiSearchMode` + new `buildAiFilter`). **`imageSearch()` and the nested `hitToImageEntity` were NOT touched**, so the `hitToImageEntity` lift still applies cleanly.
8
+
- New caller of `buildFilterOpt`: AI `buildAiFilter`. This widens the blast radius of the one allowed shared-code change (matching both `dateAddedToCollection` tokens) — see the `buildFilterOpt` note in Implementation sketch §1.
9
+
10
+
Full diff analysis in the 2026-06-10 entry of `phase-3-d3-searchafter-worklog.md`.
11
+
12
+
**Status:** B1 and B2 refactors complete; 4752 merged. Ready to implement.
- The new endpoint may **read** the existing companion-filter logic (lift-and-reuse,
107
-
behaviour-preserving) but must not change its behaviour for `search()`.
108
-
- The two extractions below (`buildFilteredQuery`, `hitToImageEntity` lift) DO
109
-
modify Kahuna-serving code. They MUST be **strictly behaviour-preserving** and
110
-
the existing `search()` / `imageSearch()` tests MUST stay green unchanged.
111
-
112
-
### Refactoring extractions (same as old plan — still valid)
113
-
114
-
| Extraction | Justification |
113
+
- The new endpoint reuses the merged `QueryBuilder.buildFilterOpt` (already shared by
114
+
`search()` and AI search) — it must not change that method's behaviour for existing
115
+
callers. The one allowed change (matching both `dateAddedToCollection` tokens) is
116
+
behaviour-preserving for `search()` and AI search — see the `buildFilterOpt` note in
117
+
Implementation sketch §1.
118
+
- The remaining extraction (`hitToImageEntity` lift) DOES modify Kahuna-serving code.
119
+
It MUST be **strictly behaviour-preserving** and the existing `imageSearch()` tests
120
+
MUST stay green unchanged.
121
+
122
+
### Refactoring extractions (revised after 4752)
123
+
124
+
| Extraction | Status / Justification |
115
125
|---|---|
116
-
|`buildFilteredQuery(params: SearchParams): Query`|~80 lines inlined in `ElasticSearch.search()`— needed by 9+ endpoints|
117
-
| Lift `hitToImageEntity` to private method on `MediaApi`| Currently a closure inside `imageSearch()`; needed by this endpoint and Gap 12/8 |
118
-
|`SearchParamsBody.fromJson(body: JsValue, tier: Tier)`| All POST endpoints need body→SearchParams; write once |
126
+
|~~`buildFilteredQuery(params: SearchParams): Query`~~|**Done by 4752.** The ~80-line filter block was lifted out of `search()`into `QueryBuilder.buildFilterOpt(params, searchFilters, syndicationFilter): Option[Query]`. `searchAfter()` calls it directly — no new extraction, and `search()` is untouched.|
127
+
| Lift `hitToImageEntity` to private method on `MediaApi`| Currently a closure inside `imageSearch()`; needed by this endpoint and Gap 12/8. **Still required** — 4752 did not touch `imageSearch()`.|
128
+
|`SearchParamsBody.fromJson(body: JsValue, tier: Tier)`| All POST endpoints need body→SearchParams; write once. Unaffected by 4752.|
119
129
120
130
> **Note (Option B):** the old plan's `sorts.appendTiebreaker` extraction is **not**
121
131
> needed for this endpoint. Under Option B the client's `buildSortClause` already
@@ -128,37 +138,47 @@ additive** and MUST NOT alter anything Kahuna depends on:
128
138
|------|--------|
129
139
|`media-api/conf/routes`| Add `POST /images/search-after` before `GET /images/:id`|
130
140
|`media-api/app/lib/elasticsearch/ElasticSearchModel.scala`| New `SearchAfterParams`, `SearchAfterRawResults`, `SearchParamsBody.fromJson`|
131
-
|`media-api/app/lib/elasticsearch/ElasticSearch.scala`|Extract `buildFilteredQuery`; new `searchAfter()` method with null-zone detection |
141
+
|`media-api/app/lib/elasticsearch/ElasticSearch.scala`|New `searchAfter()` method (null-zone detection) calling the merged `queryBuilder.buildFilterOpt`; sort-value conversion helpers. **No `buildFilteredQuery` extraction — done by 4752.**|
132
142
|`media-api/app/lib/elasticsearch/sorts.scala`| Add `reverseSorts()` only (used by reverse pagination). **No `createSort` changes** — Option B applies the client clause verbatim. |
133
143
|`media-api/app/controllers/MediaApi.scala`| Lift `hitToImageEntity`; new `searchAfterImages()` handler |
0 commit comments