[gold] Make new search API opt-in and fix a few bugs.

If we ingested a second data point for a trace at the same commit
we had observed previously, we were inconsistent about updating
TraceValues and ValuesAtHead. This was noticed because on the infra
repo, we currently have build_system as an optional key. A follow-on
CL will change that to be a normal key.

The new API wasn't filling out the deprecated field
tileSize, making the entire dots-sk not draw. This removes that field
from the frontend and backend.

This adds one new index to help make loading trace history faster.

Bug: skia:10582
Change-Id: I7c983c3914d3ab331b4f272d7effeb30b1d077ed
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/403538
Reviewed-by: Kevin Lubick <kjlubick@google.com>
Commit-Queue: Kevin Lubick <kjlubick@google.com>
diff --git a/golden/cmd/gold_frontend/gold_frontend.go b/golden/cmd/gold_frontend/gold_frontend.go
index 6c22b50..cee2333 100644
--- a/golden/cmd/gold_frontend/gold_frontend.go
+++ b/golden/cmd/gold_frontend/gold_frontend.go
@@ -838,6 +838,10 @@
 	add("/json/v1/paramset", handlers.ParamsHandler, "GET")
 	add("/json/search", handlers.SearchHandler, "GET")
 	add("/json/v1/search", handlers.SearchHandler, "GET")
+	if !fsc.IsPublicView {
+		// Search2 API doesn't support public view yet
+		add("/json/v2/search", handlers.SearchHandler2, "GET")
+	}
 	add("/json/triage", handlers.TriageHandler, "POST")
 	add("/json/v1/triage", handlers.TriageHandler, "POST")
 	add("/json/triagelog", handlers.TriageLogHandler, "GET")
diff --git a/golden/go/ingestion_processors/primarysql.go b/golden/go/ingestion_processors/primarysql.go
index 9752d1e..44b2928 100644
--- a/golden/go/ingestion_processors/primarysql.go
+++ b/golden/go/ingestion_processors/primarysql.go
@@ -709,12 +709,12 @@
 				row.OptionsID, row.GroupingID, row.Keys)
 		}
 		// If the row already exists, we'll update these three fields if and only if the
-		// commit_id comes after the stored commit_id.
+		// commit_id comes after or at the stored commit_id.
 		statement += `
 ON CONFLICT (trace_id)
 DO UPDATE SET (most_recent_commit_id, digest, options_id) =
     (excluded.most_recent_commit_id, excluded.digest, excluded.options_id)
-WHERE excluded.most_recent_commit_id > ValuesAtHead.most_recent_commit_id`
+WHERE excluded.most_recent_commit_id >= ValuesAtHead.most_recent_commit_id`
 		err := crdbpgx.ExecuteTx(ctx, s.db, pgx.TxOptions{}, func(tx pgx.Tx) error {
 			_, err := tx.Exec(ctx, statement, arguments...)
 			return err // Don't wrap - crdbpgx might retry
diff --git a/golden/go/ingestion_processors/primarysql_test.go b/golden/go/ingestion_processors/primarysql_test.go
index bbc0b62..4f6238d 100644
--- a/golden/go/ingestion_processors/primarysql_test.go
+++ b/golden/go/ingestion_processors/primarysql_test.go
@@ -1146,6 +1146,67 @@
 	})
 }
 
+func TestPrimarySQL_Process_DataFromSameTraceWithDifferentOptions_ValuesAtHeadUpdated(t *testing.T) {
+	unittest.LargeTest(t)
+	ctx := context.Background()
+	db := sqltest.NewCockroachDBForTestsWithProductionSchema(ctx, t)
+	require.NoError(t, sqltest.BulkInsertDataTables(ctx, db, dks.Build()))
+
+	const circleTraceKeys = `{"color mode":"RGB","device":"taimen","name":"circle","os":"Android","source_type":"round"}`
+	const newOptions = `{"build_system":"bazel","ext":"png"}`
+	const expectedCommitID = "0000000110"
+	src := fakeGCSSourceFromFile(t, "trace_with_new_option.json")
+	s := PrimaryBranchSQL(src, map[string]string{sqlTileWidthConfig: "5"}, db)
+	require.NoError(t, s.Process(ctx, "trace_with_new_option.json"))
+
+	// Spot check the created or updated data due to the ingested file.
+	actualValuesAtHead := sqltest.GetAllRows(ctx, t, db, "ValuesAtHead", &schema.ValueAtHeadRow{}).([]schema.ValueAtHeadRow)
+	assert.Contains(t, actualValuesAtHead, schema.ValueAtHeadRow{
+		TraceID:            h(circleTraceKeys),
+		MostRecentCommitID: expectedCommitID,
+		Digest:             d(dks.DigestC01Pos), // This was updated
+		OptionsID:          h(newOptions),       // This was updated
+		GroupingID:         h(circleGrouping),
+		Corpus:             dks.RoundCorpus,
+		Keys: paramtools.Params{
+			dks.ColorModeKey:      dks.RGBColorMode,
+			dks.DeviceKey:         dks.TaimenDevice,
+			dks.OSKey:             dks.AndroidOS,
+			types.PrimaryKeyField: dks.CircleTest,
+			types.CorpusField:     dks.RoundCorpus,
+		},
+		MatchesAnyIgnoreRule: schema.NBTrue,
+	})
+
+	actualTraceValues := sqltest.GetAllRows(ctx, t, db, "TraceValues", &schema.TraceValueRow{}).([]schema.TraceValueRow)
+	assert.Contains(t, actualTraceValues, schema.TraceValueRow{
+		Shard:        0,
+		TraceID:      h(circleTraceKeys),
+		CommitID:     expectedCommitID,
+		Digest:       d(dks.DigestC01Pos), // This was updated
+		GroupingID:   h(circleGrouping),   // This was updated
+		OptionsID:    h(newOptions),
+		SourceFileID: h("trace_with_new_option.json"),
+	})
+
+	actualTiledTraces := sqltest.GetAllRows(ctx, t, db, "TiledTraceDigests", &schema.TiledTraceDigestRow{}).([]schema.TiledTraceDigestRow)
+	assert.Contains(t, actualTiledTraces, schema.TiledTraceDigestRow{
+		TraceID: h(circleTraceKeys), Digest: d(dks.DigestC01Pos), TileID: 1, GroupingID: h(circleGrouping)})
+	// previous value should still be there
+	assert.Contains(t, actualTiledTraces, schema.TiledTraceDigestRow{
+		TraceID: h(circleTraceKeys), Digest: d(dks.DigestC05Unt), TileID: 1, GroupingID: h(circleGrouping)})
+
+	actualOptions := sqltest.GetAllRows(ctx, t, db, "Options", &schema.OptionsRow{}).([]schema.OptionsRow)
+	assert.Contains(t, actualOptions, schema.OptionsRow{
+		OptionsID: h(newOptions),
+		Keys: paramtools.Params{
+			"build_system": "bazel",
+			"ext":          "png",
+		},
+	})
+
+}
+
 func TestPrimarySQL_Process_OlderData_SomeValuesAtHeadUpdated(t *testing.T) {
 	unittest.LargeTest(t)
 	ctx := context.Background()
diff --git a/golden/go/ingestion_processors/testdata/trace_with_new_option.json b/golden/go/ingestion_processors/testdata/trace_with_new_option.json
new file mode 100644
index 0000000..ceb8c75
--- /dev/null
+++ b/golden/go/ingestion_processors/testdata/trace_with_new_option.json
@@ -0,0 +1,22 @@
+{
+  "description": "This file has data from one trace on commit id 0000000110. It has different options than datakitchensink.",
+  "gitHash": "f4412901bfb130a8774c0c719450d1450845f471",
+  "key": {
+    "os": "Android",
+    "device": "taimen"
+  },
+  "results": [
+    {
+      "key": {
+        "color mode": "RGB",
+        "name": "circle",
+        "source_type": "round"
+      },
+      "md5": "c01c01c01c01c01c01c01c01c01c01c0",
+      "options": {
+        "ext": "png",
+        "build_system": "bazel"
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/golden/go/search/frontend/types.go b/golden/go/search/frontend/types.go
index b040fd2..80642ab 100644
--- a/golden/go/search/frontend/types.go
+++ b/golden/go/search/frontend/types.go
@@ -175,9 +175,6 @@
 // TraceGroup is info about a group of traces. The concrete use of TraceGroup is to represent all
 // traces that draw a given digest (known below as the "primary digest") for a given test.
 type TraceGroup struct {
-	// TileSize is how many digests appear in Traces.
-	// TODO(kjlubick) this is no longer needed, now that Traces are dense and not skipping commits.
-	TileSize int `json:"tileSize"`
 	// Traces represents all traces in the TraceGroup. All of these traces have the primary digest.
 	Traces []Trace `json:"traces"`
 	// Digests represents the triage status of the primary digest and the first N-1 digests that
diff --git a/golden/go/search/search.go b/golden/go/search/search.go
index 2a96c5f..1123529 100644
--- a/golden/go/search/search.go
+++ b/golden/go/search/search.go
@@ -942,7 +942,6 @@
 		// No longer need the RawTrace data, now that it has been turned into the frontend version.
 		oneTrace.RawTrace = nil
 		traceGroup.Traces[idx] = oneTrace
-		traceGroup.TileSize = traceLen // TileSize will go away soon.
 	}
 	traceGroup.TotalDigests = totalDigests
 }
diff --git a/golden/go/search/search_test.go b/golden/go/search/search_test.go
index c58af96..960f5c9 100644
--- a/golden/go/search/search_test.go
+++ b/golden/go/search/search_test.go
@@ -104,7 +104,6 @@
 					"ext":                 {data.PNGExtension},
 				},
 				TraceGroup: frontend.TraceGroup{
-					TileSize:     3, // 3 commits in tile
 					TotalDigests: 2,
 					Traces: []frontend.Trace{
 						{
@@ -178,7 +177,6 @@
 					"ext":                 {data.PNGExtension},
 				},
 				TraceGroup: frontend.TraceGroup{
-					TileSize:     3,
 					TotalDigests: 1,
 					Traces: []frontend.Trace{
 						{
@@ -777,7 +775,6 @@
 							},
 						},
 					},
-					TileSize: 4,
 					Digests: []frontend.DigestStatus{
 						{
 							Digest: BetaBrandNewDigest,
@@ -973,7 +970,6 @@
 				"ext":                 {data.PNGExtension},
 			},
 			TraceGroup: frontend.TraceGroup{
-				TileSize:     3, // 3 commits in tile
 				TotalDigests: 2,
 				Traces: []frontend.Trace{ // the digest we care about appears in two traces
 					{
diff --git a/golden/go/search2/mocks/API.go b/golden/go/search2/mocks/API.go
index 4961884..5ddf2cd 100644
--- a/golden/go/search2/mocks/API.go
+++ b/golden/go/search2/mocks/API.go
@@ -6,6 +6,10 @@
 	context "context"
 
 	mock "github.com/stretchr/testify/mock"
+	frontend "go.skia.org/infra/golden/go/search/frontend"
+
+	query "go.skia.org/infra/golden/go/search/query"
+
 	search2 "go.skia.org/infra/golden/go/search2"
 
 	time "time"
@@ -57,3 +61,26 @@
 
 	return r0, r1
 }
+
+// Search provides a mock function with given fields: _a0, _a1
+func (_m *API) Search(_a0 context.Context, _a1 *query.Search) (*frontend.SearchResponse, error) {
+	ret := _m.Called(_a0, _a1)
+
+	var r0 *frontend.SearchResponse
+	if rf, ok := ret.Get(0).(func(context.Context, *query.Search) *frontend.SearchResponse); ok {
+		r0 = rf(_a0, _a1)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*frontend.SearchResponse)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *query.Search) error); ok {
+		r1 = rf(_a0, _a1)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
diff --git a/golden/go/search2/search2.go b/golden/go/search2/search2.go
index f63fef4..8d873f4 100644
--- a/golden/go/search2/search2.go
+++ b/golden/go/search2/search2.go
@@ -43,6 +43,10 @@
 	// ChangelistLastUpdated returns the timestamp that the given CL was updated. It returns an
 	// error if the CL does not exist.
 	ChangelistLastUpdated(ctx context.Context, qCLID string) (time.Time, error)
+
+	// Search queries the current tile based on the parameters specified in
+	// the instance of the *query.Search.
+	Search(context.Context, *query.Search) (*frontend.SearchResponse, error)
 }
 
 // NewAndUntriagedSummary is a summary of the results associated with a given CL. It focuses on
diff --git a/golden/go/sql/schema/sql.go b/golden/go/sql/schema/sql.go
index 28108e0..fac2526 100644
--- a/golden/go/sql/schema/sql.go
+++ b/golden/go/sql/schema/sql.go
@@ -152,7 +152,8 @@
   grouping_id BYTES NOT NULL,
   options_id BYTES NOT NULL,
   source_file_id BYTES NOT NULL,
-  PRIMARY KEY (shard, commit_id, trace_id)
+  PRIMARY KEY (shard, commit_id, trace_id),
+  INDEX trace_commit_idx (trace_id, commit_id) STORING (digest, options_id)
 );
 CREATE TABLE IF NOT EXISTS Traces (
   trace_id BYTES PRIMARY KEY,
diff --git a/golden/go/sql/schema/tables.go b/golden/go/sql/schema/tables.go
index 5b32ea7..a5d44ec 100644
--- a/golden/go/sql/schema/tables.go
+++ b/golden/go/sql/schema/tables.go
@@ -165,6 +165,8 @@
 	// to data from the same trace, but in different commits w/o overloading a single range (if
 	// commit_id were first) and w/o spreading our data too thin (if trace_id were first).
 	primaryKey struct{} `sql:"PRIMARY KEY (shard, commit_id, trace_id)"`
+
+	traceCommitIndex struct{} `sql:"INDEX trace_commit_idx (trace_id, commit_id) STORING (digest, options_id)"`
 }
 
 // ToSQLRow implements the sqltest.SQLExporter interface.
diff --git a/golden/go/web/web.go b/golden/go/web/web.go
index 7d54a90..0c8aef6 100644
--- a/golden/go/web/web.go
+++ b/golden/go/web/web.go
@@ -579,7 +579,7 @@
 	}
 	ctx, cancel := context.WithTimeout(r.Context(), 3*time.Minute)
 	defer cancel()
-	ctx, span := trace.StartSpan(ctx, "SearchHandler_sql")
+	ctx, span := trace.StartSpan(ctx, "SearchHandler")
 	defer span.End()
 
 	searchResponse, err := wh.SearchAPI.Search(ctx, q)
@@ -590,6 +590,32 @@
 	sendJSONResponse(w, searchResponse)
 }
 
+// SearchHandler2 searches the data in the new SQL backend. It times out after 3 minutes, to prevent
+// outstanding requests from growing unbounded.
+func (wh *Handlers) SearchHandler2(w http.ResponseWriter, r *http.Request) {
+	defer metrics2.FuncTimer().Stop()
+	if err := wh.limitForAnonUsers(r); err != nil {
+		httputils.ReportError(w, err, "Try again later", http.StatusInternalServerError)
+		return
+	}
+
+	q, ok := parseSearchQuery(w, r)
+	if !ok {
+		return
+	}
+	ctx, cancel := context.WithTimeout(r.Context(), 3*time.Minute)
+	defer cancel()
+	ctx, span := trace.StartSpan(ctx, "web_SearchHandler2", trace.WithSampler(trace.AlwaysSample()))
+	defer span.End()
+
+	searchResponse, err := wh.Search2API.Search(ctx, q)
+	if err != nil {
+		httputils.ReportError(w, err, "Search for digests failed in the SQL backend.", http.StatusInternalServerError)
+		return
+	}
+	sendJSONResponse(w, searchResponse)
+}
+
 // parseSearchQuery extracts the search query from request.
 func parseSearchQuery(w http.ResponseWriter, r *http.Request) (*query.Search, bool) {
 	q := query.Search{Limit: 50}
diff --git a/golden/go/web/web_test.go b/golden/go/web/web_test.go
index 7d03712..f46cec2 100644
--- a/golden/go/web/web_test.go
+++ b/golden/go/web/web_test.go
@@ -2658,7 +2658,7 @@
 	w := httptest.NewRecorder()
 	r := httptest.NewRequest(http.MethodGet, "/json/v1/diff?test=alpha&left=11111111111111111111111111111111&right=22222222222222222222222222222222", nil)
 	wh.DiffHandler(w, r)
-	const expectedResponse = `{"left":{"digest":"11111111111111111111111111111111","test":"alpha","status":"untriaged","triage_history":null,"paramset":{"device":["bullhead"],"ext":["png"],"name":["test_alpha"],"source_type":["gm"]},"traces":{"tileSize":0,"traces":null,"digests":null,"total_digests":0},"refDiffs":null,"closestRef":""},"right":{"numDiffPixels":13,"combinedMetric":4.2,"pixelDiffPercent":0.5,"maxRGBADiffs":[8,9,10,11],"dimDiffer":true,"digest":"22222222222222222222222222222222","status":"positive","paramset":{"device":["angler","crosshatch"],"ext":["png"],"name":["test_alpha"],"source_type":["gm"]}}}`
+	const expectedResponse = `{"left":{"digest":"11111111111111111111111111111111","test":"alpha","status":"untriaged","triage_history":null,"paramset":{"device":["bullhead"],"ext":["png"],"name":["test_alpha"],"source_type":["gm"]},"traces":{"traces":null,"digests":null,"total_digests":0},"refDiffs":null,"closestRef":""},"right":{"numDiffPixels":13,"combinedMetric":4.2,"pixelDiffPercent":0.5,"maxRGBADiffs":[8,9,10,11],"dimDiffer":true,"digest":"22222222222222222222222222222222","status":"positive","paramset":{"device":["angler","crosshatch"],"ext":["png"],"name":["test_alpha"],"source_type":["gm"]}}}`
 	assertJSONResponseWas(t, http.StatusOK, expectedResponse, w)
 }
 
diff --git a/golden/modules/dots-sk/demo_data.ts b/golden/modules/dots-sk/demo_data.ts
index 9601f01..f131610 100644
--- a/golden/modules/dots-sk/demo_data.ts
+++ b/golden/modules/dots-sk/demo_data.ts
@@ -17,7 +17,6 @@
  * -1 means "missing digest".
  */
 export const traces: TraceGroup = {
-  tileSize: 20,
   traces: [{
     // Note: the backend tops out at 8, but we should handle values greater than
     // MAX_UNIQUE_DIGESTS with grace and poise.
diff --git a/golden/modules/dots-sk/dots-sk.ts b/golden/modules/dots-sk/dots-sk.ts
index 0bcc952..5a1e53b 100644
--- a/golden/modules/dots-sk/dots-sk.ts
+++ b/golden/modules/dots-sk/dots-sk.ts
@@ -48,6 +48,17 @@
   }
 }, 40);
 
+/**
+ * Used to index into the dot color arrays (DOT_STROKE_COLORS, etc.). Returns
+ * the last color in the array if the given unique digest index exceeds
+ * MAX_UNIQUE_DIGESTS.
+ *
+ * This assumes that the color array is of length MAX_UNIQUE_DIGESTS + 1.
+ */
+const getColorSafe = (colorArray: string[], uniqueDigestIndex: number): string => {
+  return colorArray[Math.min(colorArray.length - 1, uniqueDigestIndex)];
+}
+
 export class DotsSk extends ElementSk {
   private static template = () => html`<canvas></canvas>`;
 
@@ -55,7 +66,7 @@
   private ctx: CanvasRenderingContext2D | null = null;
 
   private _commits: Commit[] = [];
-  private _value: TraceGroup = {tileSize: 0, traces: [], digests: [], total_digests: 0};
+  private _value: TraceGroup = {traces: [], digests: [], total_digests: 0};
 
   // The index of the trace that should be highlighted.
   private hoverIndex = -1;
@@ -114,7 +125,7 @@
   get value(): TraceGroup { return this._value; }
 
   set value(value: TraceGroup) {
-    if (!value || (value.tileSize === 0)) {
+    if (!value || (!value.traces?.length)) {
       return;
     }
     this._value = value;
@@ -142,8 +153,11 @@
 
   /** Draws the entire canvas. */
   private draw() {
-    const w = (this._value.tileSize - 1) * DOT_SCALE_X + 2 * DOT_OFFSET_X;
-    const h = (this._value.traces!.length - 1) * DOT_SCALE_Y + 2 * DOT_OFFSET_Y;
+    if (!this._value.traces || !this._value.traces.length) {
+      return;
+    }
+    const w = (this._value.traces[0].data!.length - 1) * DOT_SCALE_X + 2 * DOT_OFFSET_X;
+    const h = (this._value.traces.length - 1) * DOT_SCALE_Y + 2 * DOT_OFFSET_Y;
     this.canvas!.setAttribute('width', `${w}px`);
     this.canvas!.setAttribute('height', `${h}px`);
 
@@ -191,10 +205,10 @@
         return;
       }
       this.ctx!.beginPath();
-      this.ctx!.strokeStyle = this.getColorSafe(DOT_STROKE_COLORS, c);
+      this.ctx!.strokeStyle = getColorSafe(DOT_STROKE_COLORS, c);
       this.ctx!.fillStyle = (this.hoverIndex === y)
-        ? this.getColorSafe(DOT_FILL_COLORS_HIGHLIGHTED, c)
-        : this.getColorSafe(DOT_FILL_COLORS, c);
+        ? getColorSafe(DOT_FILL_COLORS_HIGHLIGHTED, c)
+        : getColorSafe(DOT_FILL_COLORS, c);
       this.ctx!.arc(
         dotToCanvasX(x), dotToCanvasY(y), DOT_RADIUS, 0, Math.PI * 2,
       );
@@ -203,17 +217,6 @@
     });
   }
 
-  /**
-   * Used to index into the dot color arrays (DOT_STROKE_COLORS, etc.). Returns
-   * the last color in the array if the given unique digest index exceeds
-   * MAX_UNIQUE_DIGESTS.
-   *
-   * This assumes that the color array is of length MAX_UNIQUE_DIGESTS + 1.
-   */
-  private getColorSafe(colorArray: string[], uniqueDigestIndex: number): string {
-    return colorArray[Math.min(colorArray.length - 1, uniqueDigestIndex)];
-  }
-
   /** Redraws just the circles for a single trace. */
   private redrawTraceDots(traceIndex: number) {
     const trace = this._value.traces![traceIndex];
diff --git a/golden/modules/dots-sk/dots-sk_test.ts b/golden/modules/dots-sk/dots-sk_test.ts
index a730d7d..4348145 100644
--- a/golden/modules/dots-sk/dots-sk_test.ts
+++ b/golden/modules/dots-sk/dots-sk_test.ts
@@ -153,7 +153,7 @@
   const ascii = [];
   for (let y = 0; y < traces.traces!.length; y++) {
     const trace = [];
-    for (let x = 0; x < traces.tileSize; x++) {
+    for (let x = 0; x < traces.traces![0].data!.length; x++) {
       trace.push(dotToAscii(dotsSkCanvasCtx, x, y));
     }
     ascii.push(trace.join(''));
diff --git a/golden/modules/rpc_types.ts b/golden/modules/rpc_types.ts
index 2727cc5..a29da04 100644
--- a/golden/modules/rpc_types.ts
+++ b/golden/modules/rpc_types.ts
@@ -52,7 +52,6 @@
 }
 
 export interface TraceGroup {
-	tileSize: number;
 	traces: Trace[] | null;
 	digests: DigestStatus[] | null;
 	total_digests: number;
diff --git a/golden/modules/search-page-sk/demo_data.ts b/golden/modules/search-page-sk/demo_data.ts
index 614835f..45eb016 100644
--- a/golden/modules/search-page-sk/demo_data.ts
+++ b/golden/modules/search-page-sk/demo_data.ts
@@ -227,7 +227,6 @@
           "source_type": ["infra"]
       },
       "traces": {
-          "tileSize": 200,
           "traces": [{
               "label": ",name=gold_search-controls-sk_right-hand-trace-filter-editor,source_type=infra,",
               "data": [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
@@ -288,7 +287,6 @@
           "source_type": ["infra"]
       },
       "traces": {
-          "tileSize": 200,
           "traces": [{
               "label": ",name=perf_alert-config-sk,source_type=infra,",
               "data": [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 8, 4, 4, 4, 4, 4, 4, 4, 8, 8, 7, 7, 7, 7, 7, 8, 0, 0, 8, 0, 0, 0, 0, 0, 8, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 6, 6, 6, 6, 6, 6, 3, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
@@ -361,7 +359,6 @@
           "source_type": ["infra"]
       },
       "traces": {
-          "tileSize": 200,
           "traces": [{
               "label": ",name=perf_alert-config-sk,source_type=infra,",
               "data": [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 8, 5, 5, 5, 5, 5, 5, 5, 8, 8, 8, 8, 8, 8, 8, 0, 4, 4, 8, 4, 4, 4, 4, 4, 8, 4, 8, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 7, 7, 7, 7, 7, 7, 7, 3, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
diff --git a/golden/modules/search-page-sk/search-page-sk.ts b/golden/modules/search-page-sk/search-page-sk.ts
index 029e6f8..e64217d 100644
--- a/golden/modules/search-page-sk/search-page-sk.ts
+++ b/golden/modules/search-page-sk/search-page-sk.ts
@@ -3,20 +3,32 @@
  * @description <h2><code>search-page-sk</code></h2>
  *
  */
-import { html } from 'lit-html';
-import { define } from 'elements-sk/define';
-import { jsonOrThrow } from 'common-sk/modules/jsonOrThrow';
-import { deepCopy } from 'common-sk/modules/object';
-import { stateReflector } from 'common-sk/modules/stateReflector';
-import { ParamSet, fromParamSet, fromObject } from 'common-sk/modules/query';
+import {html} from 'lit-html';
+import {define} from 'elements-sk/define';
+import {jsonOrThrow} from 'common-sk/modules/jsonOrThrow';
+import {deepCopy} from 'common-sk/modules/object';
+import {stateReflector} from 'common-sk/modules/stateReflector';
+import {fromObject, fromParamSet, ParamSet} from 'common-sk/modules/query';
 import dialogPolyfill from 'dialog-polyfill';
-import { HintableObject } from 'common-sk/modules/hintable';
-import { ElementSk } from '../../../infra-sk/modules/ElementSk';
-import { ChangelistControlsSkChangeEventDetail } from '../changelist-controls-sk/changelist-controls-sk';
-import { SearchCriteria, SearchCriteriaToHintableObject, SearchCriteriaFromHintableObject } from '../search-controls-sk/search-controls-sk';
-import { sendBeginTask, sendEndTask, sendFetchError } from '../common';
-import { defaultCorpus } from '../settings';
-import { SearchResponse, StatusResponse, ParamSetResponse, SearchResult, ChangelistSummaryResponse, TriageRequestData, Label } from '../rpc_types';
+import {HintableObject} from 'common-sk/modules/hintable';
+import {ElementSk} from '../../../infra-sk/modules/ElementSk';
+import {ChangelistControlsSkChangeEventDetail} from '../changelist-controls-sk/changelist-controls-sk';
+import {
+  SearchCriteria,
+  SearchCriteriaFromHintableObject,
+  SearchCriteriaToHintableObject
+} from '../search-controls-sk/search-controls-sk';
+import {sendBeginTask, sendEndTask, sendFetchError} from '../common';
+import {defaultCorpus} from '../settings';
+import {
+  ChangelistSummaryResponse,
+  Label,
+  ParamSetResponse,
+  SearchResponse,
+  SearchResult,
+  StatusResponse,
+  TriageRequestData
+} from '../rpc_types';
 
 import 'elements-sk/checkbox-sk';
 import 'elements-sk/styles/buttons';
@@ -183,7 +195,7 @@
   private _blame: string | null = null;
   private _crs: string | null = null;
   private _changelistId: string | null = null;
-  private _useSQL: boolean = false;
+  private _useNewAPI: boolean = false;
 
   // stateReflector update function.
   private _stateChanged: (() => void) | null = null;
@@ -214,7 +226,7 @@
         state.blame = this._blame || '';
         state.crs = this._crs || '';
         state.issue = this._changelistId || '';
-        state.use_sql = this._useSQL || '';
+        state.use_new_api = this._useNewAPI || '';
         state.master = this._includeDigestsFromPrimary || '';
         state.patchsets = this._patchset || '';
         return state;
@@ -227,7 +239,7 @@
         this._blame = (newState.blame as string) || null;
         this._crs = (newState.crs as string) || null;
         this._changelistId = (newState.issue as string) || null;
-        this._useSQL = (newState.use_sql as boolean) || false;
+        this._useNewAPI = (newState.use_new_api as boolean) || false;
         this._includeDigestsFromPrimary = (newState.master as boolean) || null;
         this._patchset = (newState.patchsets as number) || null;
 
@@ -329,7 +341,7 @@
     }
   }
 
-  private _makeSearchRequest() {
+  private _makeSearchRequest(): SearchRequest {
     // Utility function to insert the selected corpus into the left- and right-hand trace filters,
     // as required by the /json/v1/search RPC.
     const insertCorpus = (paramSet: ParamSet) => {
@@ -356,7 +368,6 @@
 
     // Populate optional query parameters.
     if (this._blame) searchRequest.blame = this._blame;
-    if (this._useSQL) searchRequest.use_sql = this._useSQL;
     if (this._crs) searchRequest.crs = this._crs;
     if (this._changelistId) searchRequest.issue = this._changelistId;
     if (this._includeDigestsFromPrimary) searchRequest.master = this._includeDigestsFromPrimary;
@@ -376,12 +387,17 @@
 
     try {
       sendBeginTask(this);
-      const searchResponse: SearchResponse =
-        await fetch(
+      if (!this._useNewAPI) {
+        this._searchResponse = await fetch(
             '/json/v1/search?' + fromObject(searchRequest as any),
             {method: 'GET', signal: this._searchResultsFetchController.signal})
-          .then(jsonOrThrow);
-      this._searchResponse = searchResponse;
+            .then(jsonOrThrow);
+      } else {
+        this._searchResponse = await fetch(
+            '/json/v2/search?' + fromObject(searchRequest as any),
+            {method: 'GET', signal: this._searchResultsFetchController.signal})
+            .then(jsonOrThrow);
+      }
 
       // Reset UI and render.
       this._selectedSearchResultIdx = -1;