Skip to content

Commit

Permalink
Make ByteSpan a GraphQL scalar
Browse files Browse the repository at this point in the history
This helps shrink GraphQL search responses a bit, makes queries less
repetitive (no need to specify `start end` all the time), generally
saves processing time (fewer JSON objects) and avoids having to use
signed integers. In an example request, it shrank the response size
from 210 to 190KB. Not a ton but still 10%.
  • Loading branch information
LukasKalbertodt committed Feb 4, 2025
1 parent d51a580 commit 86f0ed9
Show file tree
Hide file tree
Showing 6 changed files with 36 additions and 28 deletions.
2 changes: 1 addition & 1 deletion backend/src/api/model/search/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ impl SearchEvent {
.iter()
.filter_map(|m| {
m.indices.as_ref().and_then(|v| v.get(0)).map(|index| ArrayMatch {
span: ByteSpan { start: m.start as i32, len: m.length as i32 },
span: ByteSpan { start: m.start as u32, len: m.length as u32 },
index: *index as i32,
})
})
Expand Down
23 changes: 17 additions & 6 deletions backend/src/api/model/search/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use chrono::{DateTime, Utc};
use juniper::GraphQLObject;
use juniper::{GraphQLObject, GraphQLScalar, InputValue, ScalarValue};
use meilisearch_sdk::search::{FederationOptions, MatchRange, QueryFederationOptions};
use once_cell::sync::Lazy;
use regex::Regex;
Expand Down Expand Up @@ -81,11 +81,22 @@ make_search_results_object!("EventSearchResults", SearchEvent);
make_search_results_object!("SeriesSearchResults", SearchSeries);
make_search_results_object!("PlaylistSearchResults", search::Playlist);


#[derive(Debug, GraphQLObject)]
/// A byte range, encoded as two hex numbers separated by `-`.
#[derive(Debug, Clone, Copy, GraphQLScalar)]
#[graphql(parse_token(String))]
pub struct ByteSpan {
pub start: i32,
pub len: i32,
pub start: u32,
pub len: u32,
}

impl ByteSpan {
fn to_output<S: ScalarValue>(&self) -> juniper::Value<S> {
juniper::Value::scalar(format!("{:x}-{:x}", self.start, self.len))
}

fn from_input<S: ScalarValue>(_input: &InputValue<S>) -> Result<Self, String> {
unimplemented!("not used right now")
}
}

#[derive(Debug, Clone, Copy, juniper::GraphQLEnum)]
Expand Down Expand Up @@ -579,7 +590,7 @@ fn field_matches_for(
field: &str,
) -> Vec<ByteSpan> {
match_ranges_for(match_positions, field).iter()
.map(|m| ByteSpan { start: m.start as i32, len: m.length as i32 })
.map(|m| ByteSpan { start: m.start as u32, len: m.length as u32 })
.take(8) // The frontend can only show a limited number anyway
.collect()
}
Expand Down
4 changes: 2 additions & 2 deletions backend/src/search/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -451,8 +451,8 @@ impl TextSearchIndex {

let highlights = matches.iter().map(|m| {
ByteSpan {
start: (m.start - range_with_context.start) as i32,
len: m.length as i32,
start: (m.start - range_with_context.start) as u32,
len: m.length as u32,
}
}).collect();

Expand Down
1 change: 1 addition & 0 deletions frontend/relay.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ module.exports = {
customScalarTypes: {
"DateTime": "string",
"Cursor": "string",
"ByteSpan": "string",
"ExtraMetadata": "Record<string, Record<string, string[]>>",
"TranslatedString": "Record<string, string>",
},
Expand Down
26 changes: 12 additions & 14 deletions frontend/src/routes/Search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,13 @@ const query = graphql`
duration
text
ty
highlights { start len }
highlights
}
matches {
title { start len }
description { start len }
seriesTitle { start len }
creators { index span { start len }}
title
description
seriesTitle
creators { index span }
}
}
... on SearchSeries {
Expand All @@ -192,19 +192,14 @@ const query = graphql`
description
thumbnails { thumbnail isLive audioOnly }
hostRealms { path ancestorNames }
matches {
title { start len }
description { start len }
}
matches { title description }
}
... on SearchRealm {
id
name
path
ancestorNames
matches {
name { start len }
}
matches { name }
}
}
totalHits
Expand Down Expand Up @@ -1226,13 +1221,16 @@ const byteSlice = (s: string, start: number, len: number): readonly [string, str
*/
const highlightText = (
s: string,
spans: readonly { start: number; len: number }[],
spans: readonly string[],
maxUnmarkedSectionLen = Infinity,
) => {
const textParts = [];
let remainingText = s;
let offset = 0;
for (const span of spans) {
for (const encodedSpan of spans) {
const [start, len] = encodedSpan.split("-").map(v => parseInt(v, 16));
const span = { start, len };

const highlightStart = span.start - offset;
const [prefix_, middle, rest]
= byteSlice(remainingText, highlightStart, span.len);
Expand Down
8 changes: 3 additions & 5 deletions frontend/src/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,9 @@ interface Node {
id: ID!
}

"A byte range, encoded as two hex numbers separated by `-`."
scalar ByteSpan

"An opaque cursor used for pagination"
scalar Cursor

Expand Down Expand Up @@ -291,11 +294,6 @@ type AuthorizedPlaylist implements Node {
entries: [VideoListEntry!]!
}

type ByteSpan {
start: Int!
len: Int!
}

type Caption {
uri: String!
lang: String
Expand Down

0 comments on commit 86f0ed9

Please sign in to comment.