How to Search Inside Your Videos: Caption Search with Apache Solr

Make a video library searchable: index WebVTT captions in Apache Solr and link each match to the second it's spoken. Tested code for Solr 10.1.

How to Search Inside Your Videos: Caption Search with Apache Solr

How Do You Search Inside a Video?

To search inside your videos, index their caption files. Every cue in a WebVTT file carries a line of text and the time it's spoken, so a search engine like Apache Solr can find a phrase and send the viewer to the exact second. The one design choice that matters: index each video as one document with its whole transcript, and keep each cue's start time beside it.

That's the difference between a search box that finds "touch saturation and usually less" and one that finds nothing. In my tests, a one-document-per-cue index returned 0 hits for that phrase, because it runs across two cues. The one-document-per-video index found it and linked to 0:15.99.

This post walks through the whole setup with code I ran against Solr 10.1.0: reading WebVTT, indexing transcripts, and turning a highlighted match into a timestamped link. If you run a course library, a podcast archive or a long-running channel, this is how viewers find the moment they remember.

Why One Document Per Video, Not Per Cue

The obvious design is one Solr document per caption cue, with its start time. It falls apart as soon as someone searches a phrase, because captions break lines where the speaker breathes, not where the idea ends:

00:00:15.986 --> 00:00:18.522
Only after that do you touch saturation,

00:00:18.772 --> 00:00:20.718
and usually less than you think.

A phrase query for "touch saturation and usually less" needs both cues in one field, so per-cue documents can never match it.

Per-cue documents split a phrase across two cues and return no match, while one document per video holds the whole transcript and finds the phrase at 0:15.99
Per-cue documents split a phrase across two cues and return no match, while one document per video holds the whole transcript and finds the phrase at 0:15.99

So each video becomes one document: the transcript as one text field, plus two stored lists, where each cue starts in the transcript (a character offset) and where it starts in the video (seconds). Search matches the transcript, and the offsets turn a match back into a time.

Setting Up Solr for Captions

Solr runs in one Docker command, and solr-precreate makes the core (Solr in Docker):

docker run -d -p 127.0.0.1:8983:8983 --name solr solr:10.1.0 solr-precreate captions

Then define the fields through the Schema API rather than letting Solr guess types from the first document:

{"add-field": [
  {"name": "title", "type": "text_en", "stored": true},
  {"name": "transcript", "type": "text_en", "stored": true},
  {"name": "url", "type": "string", "stored": true, "indexed": false},
  {"name": "cue_offsets", "type": "pint", "multiValued": true, "stored": true, "indexed": false, "docValues": false},
  {"name": "cue_starts", "type": "pfloat", "multiValued": true, "stored": true, "indexed": false, "docValues": false}
]}

text_en stems words, so a search for "tone" also finds "tones". The transcript has to be stored, because the highlighter works on the stored text. The cue lists are stored only, never searched, and stored values come back in the order you wrote them, which the timestamp lookup depends on.

Reading WebVTT Without Losing Text

Real caption files carry more than text: a STYLE block, NOTE comments, cue identifiers, position settings, voice spans like <v Mark>, italics, class spans and character references like &amp;. A parser that only strips the timing line leaves <v Mark> in your search results. This reader handles all of them:

TIMING = re.compile(r"(?:(\d+):)?(\d{2}):(\d{2})\.(\d{3})\s+-->\s+(?:(\d+):)?(\d{2}):(\d{2})\.(\d{3})")
TAG = re.compile(r"<[^>]*>")  # <v Name>, <i>, <c.class>, and inline <00:00:01.000> timestamps

def seconds(h, m, s, ms):
    return int(h or 0) * 3600 + int(m) * 60 + int(s) + int(ms) / 1000

def read_vtt(text):
    blocks = re.split(r"\n\s*\n", text.replace("\r\n", "\n").strip())
    cues = []
    for block in blocks[1:]:
        lines = block.split("\n")
        if lines[0].startswith(("NOTE", "STYLE", "REGION")):
            continue
        i = 0 if "-->" in lines[0] else 1          # optional cue identifier
        m = TIMING.match(lines[i].strip()) if i < len(lines) else None
        if m:
            text = " ".join(html.unescape(TAG.sub("", " ".join(lines[i + 1:]))).split())
            cues.append((seconds(*m.group(1, 2, 3, 4)), text))
    return cues

Each video's document then joins the cue texts with spaces and records where each cue starts:

text, offsets, starts = "", [], []
for start, cue in read_vtt(vtt):
    if text:
        text += " "
    offsets.append(len(text))
    starts.append(start)
    text += cue
doc = {"id": slug, "title": title, "transcript": text, "url": f"/videos/{slug}.mp4",
       "cue_offsets": offsets, "cue_starts": starts}

POST the list of documents to /solr/captions/update?commit=true and the library is searchable. If your captions came from an editor or a tool, run them through a caption checker first: broken timings index without complaint and then link to the wrong second.

Turning a Match into a Timestamp

Solr's unified highlighter does the hard part. With hl.fragsize=0 it returns the whole transcript with every match marked, and it highlights a phrase as one span rather than word by word. Use marker characters that can't appear in captions, then count characters:

params = {
    "q": query, "defType": "edismax", "qf": "transcript title^2",
    "hl": "true", "hl.method": "unified", "hl.fl": "transcript",
    "hl.fragsize": "0", "hl.maxAnalyzedChars": "10000000",
    "hl.tag.pre": "\x02", "hl.tag.post": "\x03",
}
# For each \x02 in the highlighted transcript, its offset in the plain text
# (markers removed) looked up in cue_offsets gives the cue and its start time:
cue = bisect.bisect_right(offsets, plain_offset) - 1
link = f"{url}#t={starts[cue]:.2f}"

The #t= suffix is a Media Fragments time, which HTML5 video players honour, so the link opens the video at that moment. On YouTube the same idea is &t=15s.

One parameter is easy to miss. hl.maxAnalyzedChars defaults to 51,200 characters, and the highlighter stops looking after that. On a 66,891-character test transcript with the match near the end, Solr still returned the document, but with no highlight, so there was no timestamp to link to. At the speaking rate in my test files, 51,200 characters is roughly an hour of speech, which a long podcast or webinar passes easily. Raise the limit, as above.

  1. Validate every caption file before indexing; a cue that ends before it starts still indexes.
  2. One document per video, with cue offsets and start times stored beside the transcript.
  3. Explicit schema, so cue_starts is a float list and not whatever Solr guessed.
  4. Store the transcript and highlight with hl.method=unified and hl.fragsize=0.
  5. Raise hl.maxAnalyzedChars above your longest transcript.
  6. Test a phrase that crosses two cues and confirm the link opens at the right second.

The free caption checker handles step 1 in the browser.

Where Captions Come From

All of this assumes accurate, well-timed captions, and that's where most libraries fall short. Mark Studios has delivered 8,000+ orders for 4,500+ clients, holds a 4.9★ rating and Top Rated status on Fiverr, delivers 99% on time and has edited work behind 100M+ views. Caption files in SRT or WebVTT are a paid add-on with any edit, and they're the same files this search reads. If your library is a course, the course creator setup pairs edits with a lesson library workflow. For choosing between burned-in and closed captions, only closed caption files can be searched like this. What they cost is in our caption pricing guide.

The search pipeline in four steps: WebVTT files, one Solr document per video with cue offsets, the unified highlighter marking each match, and a timestamped link that opens the video at that second
The search pipeline in four steps: WebVTT files, one Solr document per video with cue offsets, the unified highlighter marking each match, and a timestamped link that opens the video at that second

The Bottom Line

Searching inside video comes down to captions and one design choice: index each video as a whole transcript, keep every cue's start time beside it, and let Solr's highlighter show where the match is. Raise the highlighter's character limit, test a phrase that crosses two cues, and viewers land on the second they were looking for.

Start Your Project Now

Mark Santos

Written by

Mark Santos

Founder & CEO of Mark Studios. The studio has delivered 8,000+ projects and 4,500+ reviews on Fiverr; Mark has passed $1M+ in Fiverr orders. More about Mark

Frequently asked questions

Can you search inside a video by what is said in it?

Yes. Index the video's caption or transcript file. Each cue in a caption file pairs a line of speech with the time it's spoken, so a search engine such as Apache Solr can match a word or phrase and return the second it starts. The video itself is never analysed; the captions do all the work, which is why accurate, well-timed caption files matter.

What is a WebVTT file?

A WebVTT file (.vtt) is the W3C's plain-text caption format for web video. It starts with the line WEBVTT, then lists cues: a start and end time, optional settings for position, and one or more lines of text. It can also hold comments, styles, voice tags and italics, which a search indexer has to strip so they don't appear in results.

Why does phrase search fail when each caption cue is its own document?

A phrase search fails on per-cue documents because captions break lines where the speaker pauses, so many phrases start in one cue and end in the next. Neither cue holds the whole phrase, so neither matches. Indexing each video as one document with the full transcript keeps the words together, and stored cue offsets map the match back to a timestamp.

How do I link to a specific second of a video?

Add a time fragment to the video's URL. For a video file played in an HTML5 player, the W3C Media Fragments syntax #t=15.99 starts playback at 15.99 seconds. On YouTube, add t=15s to the watch URL. A caption search returns the start time of the matching cue, so building the link is a matter of appending that number.

Why does Solr find a long video but not highlight the match?

Solr's highlighter stops looking after hl.maxAnalyzedChars characters, which defaults to 51,200. A match beyond that point still makes the document a hit, but it comes back with no highlight, so there is no position to turn into a timestamp. At a normal speaking rate that limit is roughly an hour of speech, so raise it above your longest transcript.

Can burned-in captions be searched?

No. Burned-in (open) captions are part of the picture, so there is no text for a search engine to read without running text recognition on every frame. Caption search needs closed caption files such as WebVTT or SRT, which store the words and their timings as text. Many videos ship both: burned-in captions for social feeds and a caption file for players and search.

Rather not do this part yourself?

Mark Studios handles captions, dubbing and localization for creators and brands: you send the footage, the studio handles post.

  • Founder-led US company
  • NDAs by default
  • Stripe & PayPal payments
  • Top Rated on Fiverr

Weighing a freelancer instead? See how the options compare.